Сетевой стек Linux не прост даже на первый взгляд: приложение — в юзерспейсе, а всё, что после сокета, — в ядре операционки. И там тысяча реализаций TCP. Любое взаимодействие с сетью — системный вызов с переключением контекста в ядре.

Чтобы лишний раз не дёргать ядро прерываниями, придумали DMA — Direct Memory Access. Это когда трафик пишется напрямую в память, откуда он считывается приложением в обход ядра. И это дало жизнь классу софта с режимом работы kernel bypass. Например, при DPDK (Intel Data Plane Development Kit) сетевая карта целиком передаётся в userspace, а ядро даже не подозревает о её существовании. 

Потом был BPF. А ещё потом усилиями Алексея Старовойтова и компании миру была показана eBPF — штука, умеющая делать прокол в ядро и инжектировать туда микроскопические виртуальные машины с кодом, которые могут в обход всего и вся взаимодействовать с системными событиями, и в том числе с трафиком. Супербыстро и оптимально (на фоне стандартного стека, конечно же). А это в свою очередь дало возможность использовать XDP для ускорения обработки трафика.

Но даже помимо хаков работы с ядром есть такие штуки, как sk_buff, в которой хранятся метаданные всех миллионов протоколов (в большинстве случаев они вообще не нужны: тащим с собой легаси). Есть NAPI (New API), которая призвана уменьшить число прерываний. А 100500 вариантов разных tables? Iptables, arptables, ip6tables, ebtables, nftables…

Если вам мало — ещё придумали SR-IOV. Там тоже уже упомянутый DMA, а ещё можно посплитить физическую карточку на несколько виртуальных и раздать их в разные виртуалки и приложения. Под ручку с DMA идёт и RDMA, когда мы пишем трафик напрямую в память, но не в свою, а в чужую на удалённой по сети машине.

И в этих копаниях можно уйти безгранично далеко. Но сегодня мы всё же поговорим о вещах более приземлённых и повседневных, которые лишь приоткрывают вход в эту разветвлённую сеть кроличьих нор. Мы разберём одну любопытную задачку, на примере которой ужаснёмся (а кто-то ухмыльнётся деловито в усы) тому, как сложно может быть реализован такой простой протокол, как DHCP.

И прежде чем начнёте читать этот лонгрид, попробуйте сами заблокировать DHCP с помощью iptables — так будет интереснее.

DHCP

Один из самых простых протоколов с точки зрения сетевого взаимодействия — UDP 67+68. Это всего четыре сообщения: DHCPDISCOVER, DHCPOFFER, DHCPREQUEST, DHCPACK. И даже не будем усложнять себе жизнь, используя DHCP Relay.

Так выглядит DHCP. Источник
Так выглядит DHCP. Источник

Сделаю тут небольшое отступление: мы тут в Яндексе придумали провести Тренировки по DevOps. В рамках этого события есть пара уроков про сети, где Боря Лыточкин и Паша Пушкарёв рассказывают про фундаментальные нетворк-штуки, сетевой стек Linux и всякое такое. И в каждом уроке есть домашка. И вот Боря, просветлённый FreeBSD и повидавший разное в Linux, придумал каверзное задание: запускаем пару виртуалок, соединяем их бриджем, на одной настраиваем DHCP-сервер, на другой — клиент. Проверяем, что адрес выдаётся — супер. А теперь пробуем заблокировать через iptables на клиенте так, чтобы любой ценой адрес не выдавался.

Сначала мы убрали эту задачку под звёздочку. Подумали — убрали под две. А потом вообще исключили из домашки. И вот почему.

С первого взгляда всё же легко, да? Ну прямо в лоб можно? Прям IP блокируем.

❯ iptables -I INPUT -s 192.168.42.1 -j DROP

Ну да, норм — пинг перестал работать. И я благополучно потерял доступ к виртуалке.

Давайте отпустим адрес и перезапросим его снова:

❯ dhclient -r
Killed old client process
❯ ip -f inet a s enp0s1
❯  

Адреса на интерфейсе нет.

❯ dhclient
❯ ip -f inet a s enp0s1
2: enp0s1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    inet 192.168.42.6/24 brd 192.168.42.255 scope global dynamic enp0s1
       valid_lft 86389sec preferred_lft 86389sec 

Хм…

Ну ок. Возможно, там есть какие-то нюансы с IP-адресами. Может, нам не с того DHCPOFFER приходит? Ну мало ли. Не будем пока в tcpdump заглядывать.

Давайте поблочим UDP-порты.

❯ iptables -I INPUT -p udp --dport 67 -j DROP
❯ iptables -I INPUT -p udp --dport 68 -j DROP 

Релиз.

❯ dhclient -r
Killed old client process
❯ dhclient
❯ ip -f inet a s enp0s1 | grep inet
    inet 192.168.42.6/24 brd 192.168.42.255 scope global dynamic enp0s1 

Хм-м…

Ну, как бы тут очевидно уж должно попадать. Блокировка вообще всего UDP тоже результата не даёт.

`❯ iptables -I INPUT -p udp  -j DROP

Ну, я не очень умный в части линуксового стека. Может, как-то DCHP-клиент перехватывает пакеты до обработки транспортных заголовков? Заблочим MAC.

❯ iptables -I INPUT -m mac --mac-source fa:4d:89:c8:82:64 -j DROP

Та же фигня: пинг снова ломается, а DHCP — нет.

Хм-м-м…

Ладно, лезем читать форумы. Где-то дают советы про уже испробованное. Но если чуточку покопать по ключевым словам "iptables doesn't block dhcp", находим зацепку: dhclient использует не нативный стек, а raw socket. То есть этот подлец открывает сырой сокет как есть и сам реализует обработку IP и транспортных заголовков.

Что это означает для простых людей? Что такие пакеты не доходят до дефолтной таблицы filter и не попадают в цепочку INPUT: мы их там никогда и не увидели бы.

Ок. Что же дальше?

Есть вот такая картинка, дающая представление о том, как устроен процесс обработки трафика в простом iptables. Источник
Есть вот такая картинка, дающая представление о том, как устроен процесс обработки трафика в простом iptables. Источник

Судя по всему, нам нужен raw PREROUTING. И некоторый дальнейший гуглёж (и Боря Лыточкин) подсказывают, что так оно и есть. Таблица и цепочка обрабатывают самый сырой трафик до всего — ещё до того, как доходит дело до conntrack.

Кажется, это то, что нам нужно! Е-е-е, бой, мы на финишной прямой!

❯ iptables -t raw -I PREROUTING -m mac  --mac-source fa:4d:89:c8:82:64 -j DROP

Снова оторвал себе SSH…

❯ iptables -t raw -L
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
DROP       all  --  anywhere             anywhere             MACfa:4d:89:c8:82:66

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

Релиз.

❯ dhclient -r
Killed old client process
❯ dhclient
ip -f inet a s enp0s1 | grep inet
    inet 192.168.42.6/24 brd 192.168.42.255 scope global dynamic enp0s1

Хм-м-м-м….

Так! Ну ладно, может, там src MAC какой-то другой? Типа широковещательный или в нём вовсе одни нули? Всё ещё не будем запускать tcpdump — это для слабых духом. Но UDP 67 тоже не работает.

И смотрите, какой прикол:

❯ iptables -t raw -vnL
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination
    2   706 DROP       udp  --  *      *       0.0.0.0/0            0.0.0.0/0            udp dpt:68

Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination

iptables матчит пакеты! Статистика не врёт: один DHCPOFFER + один DHCPACK = 2. Очень интересно: матчит, но не дропает!

​​Давайте пока этот вопрос отложим в оперативку. Он очень интересный.

Хорошо, давайте заблокируем вообще весь UDP. Ну мало ли, вдруг мы как-то с портами напутали? Тоже нет, не напутали, и dhclient по-прежнему работает.

А давайте вообще выключим сеть через policy DROP!

❯ iptables -t raw -L | grep policy
Chain PREROUTING (policy DROP)
Chain OUTPUT (policy ACCEPT)

Всё, мы вообще оторвали сеть. Тут даже tcpdump не поможет. Нет смысла его теперь уже открывать.

Нет! Работает. Точнее, не работает. То есть что хотим — не работает, а чего не хотим — работает.

Хм-м-м-м-м.....

Осталось читать инструкцию смотреть логи.

❯ iptables -t raw -I PREROUTING -p udp -j LOG

Попробуем послать с клиента udp-пробу.

❯ nc -zvu 192.168.42.1 67
❯ tail -f /var/log/syslog | grep kernel
Nov 11 03:15:48 fish kernel: [14018.625457] IN=enp0s1 OUT= MAC=0e:21:3f:d8:09:36:12:e0:e2:b7:e0:f8:08:00 SRC=192.168.42.6 DST=192.168.42.1 LEN=29 TOS=0x00 PREC=0x00 TTL=64 ID=47682 DF PROTO=UDP SPT=39688 DPT=67 LEN=9

Видим пробу: значит, логирование работает. А вот при работе DHCP пусто: пакеты матчит, а в логи не пишет. Каков проказник!

Ну как бы… Кажется, мы что-то перестали понимать в этой жизни. Причём Боря утверждал, что в своё время он это точно делал, и оно работало. И именно поэтому он придумал такую задачку с закавыкой, чтобы ученику нужно было слегка углубиться — совсем чуть-чуть, чтобы безобразие всего линуксового стека поразило своей разносторонностью, но всё же не отпугнуло. А получилось наоборот.

Слабая надежда?

Ebtables? Постулируется, что он работает на канальном уровне, чуть пониже iptables. Я проверил — не работает.

Ну, может, хоть nftables, который весь такой молодец и приходит на смену всему зверинцу: iptables, ip6tables, arptables, ebtables. Может, в нём всё сделали хорошо? Нет. Ну, то есть, наверно, да, но dhcp как не ловился, так и не ловится. Похоже, все эти таблицы ловят одни хуки. Косвенно об этом говорит то, что в конфигурацию nftables попадают и те правила, которые я только что настроил через ebtables — первый chain INPUT.

table bridge filter {
 chain INPUT {
  type filter hook input priority filter; policy accept;
  ether type ip udp dport 67-68  counter packets 0 bytes 0 drop
  ether saddr fa:4d:89:c8:82:64 counter packets 0 bytes 0 drop
 }

 chain input {
  type filter hook input priority 0; policy accept;
  iifname "enp0s1" udp sport { 67, 68 } counter packets 0 bytes 0 drop
  iifname "enp0s1" udp dport { 67, 68 } counter packets 0 bytes 0 drop
 }

 chain output {
  type filter hook output priority 0; policy accept;
 }
}
table inet filter {
 chain input {
  type filter hook input priority filter; policy accept;
  iifname "enp0s1" udp sport { 67, 68 } counter packets 0 bytes 0 drop
 }

 chain output {
  type filter hook output priority filter; policy accept;
 }
}

Потрачено!

Хм-м-м-м-м-м...

Ещё один шаг

Ну, где бы вы думали, лежит разгадка? Наверняка уже догадываетесь: она написана в выводе strace — утилиты, отображающей системные вызовы любых процессов.

Запускаем:

❯ strace dhclient -d eth0

На нас льётся тонна текста.

Но возьмём важный для нас момент…
socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)) = 5
ioctl(5, SIOCGIFINDEX, {ifr_name="enp0s1", ifr_ifindex=3}) = 0
bind(5, {sa_family=AF_PACKET, sll_protocol=htons(ETH_P_ALL), sll_ifindex=if_nametoindex("enp0s1"), sll_hatype=ARPHRD_NETROM, sll_pkttype=PACKET_HOST, sll_halen=0}, 20) = 0
setsockopt(5, SOL_PACKET, PACKET_AUXDATA, [1], 4) = 0
setsockopt(5, SOL_SOCKET, SO_ATTACH_FILTER, {len=11, filter=0x559d30be3820}, 16) = 0
getpid()                                = 1194
sendto(3, "<30>Nov 11 11:48:03 dhclient[119"..., 77, MSG_NOSIGNAL, NULL, 0) = 77
write(2, "Listening on LPF/enp0s1/08:00:27"..., 41) = 41
write(2, "\n", 1)                       = 1
getpid()                                = 1194
sendto(3, "<30>Nov 11 11:48:03 dhclient[119"..., 77, MSG_NOSIGNAL, NULL, 0) = 77
write(2, "Sending on   LPF/enp0s1/08:00:27"..., 41) = 41
write(2, "\n", 1)                       = 1
fcntl(5, F_SETFD, FD_CLOEXEC)           = 0
socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) = 6
setsockopt(6, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
bind(6, {sa_family=AF_INET, sin_port=htons(68), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
getpid()                                = 1194
sendto(3, "<30>Nov 11 11:48:03 dhclient[119"..., 64, MSG_NOSIGNAL, NULL, 0) = 64
write(2, "Sending on   Socket/fallback", 28) = 28
write(2, "\n", 1)                       = 1
fcntl(6, F_SETFD, FD_CLOEXEC)           = 0
openat(AT_FDCWD, "/dev/random", O_RDONLY) = 7
newfstatat(7, "", {st_mode=S_IFCHR|0666, st_rdev=makedev(0x1, 0x8), ...}, AT_EMPTY_PATH) = 0
ioctl(7, TCGETS, 0x7ffe88f80c60)        = -1 EINVAL (Invalid argument)
read(7, "6\346\32p\341\265\234\343I\260\220\374\330\367^@\335|\266/\212\264\31\370\315~ \1x\224\217O"..., 4096) = 4096
close(7)                                = 0
openat(AT_FDCWD, "/etc/hostid", O_RDONLY) = -1 ENOENT (No such file or directory)
uname({sysname="Linux", nodename="client", ...}) = 0
newfstatat(AT_FDCWD, "/etc/resolv.conf", {st_mode=S_IFREG|0644, st_size=933, ...}, 0) = 0
openat(AT_FDCWD, "/etc/host.conf", O_RDONLY|O_CLOEXEC) = 7
newfstatat(7, "", {st_mode=S_IFREG|0644, st_size=92, ...}, AT_EMPTY_PATH) = 0
read(7, "# The \"order\" line is only used "..., 4096) = 92
read(7, "", 4096)                       = 0
close(7)                                = 0
futex(0x7efd50a0742c, FUTEX_WAKE_PRIVATE, 2147483647) = 0
openat(AT_FDCWD, "/etc/resolv.conf", O_RDONLY|O_CLOEXEC) = 7
newfstatat(7, "", {st_mode=S_IFREG|0644, st_size=933, ...}, AT_EMPTY_PATH) = 0
read(7, "# This is /run/systemd/resolve/s"..., 4096) = 933
read(7, "", 4096)                       = 0
newfstatat(7, "", {st_mode=S_IFREG|0644, st_size=933, ...}, AT_EMPTY_PATH) = 0
close(7)                                = 0
socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0) = 7
connect(7, {sa_family=AF_UNIX, sun_path="/var/run/nscd/socket"}, 110) = -1 ENOENT (No such file or directory)
close(7)                                = 0
socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0) = 7
connect(7, {sa_family=AF_UNIX, sun_path="/var/run/nscd/socket"}, 110) = -1 ENOENT (No such file or directory)
close(7)                                = 0
newfstatat(AT_FDCWD, "/etc/nsswitch.conf", {st_mode=S_IFREG|0644, st_size=526, ...}, 0) = 0
openat(AT_FDCWD, "/etc/hosts", O_RDONLY|O_CLOEXEC) = 7
newfstatat(7, "", {st_mode=S_IFREG|0644, st_size=221, ...}, AT_EMPTY_PATH) = 0
lseek(7, 0, SEEK_SET)                   = 0
read(7, "127.0.0.1 localhost\n127.0.1.1 cl"..., 4096) = 221
read(7, "", 4096)                       = 0
close(7)                                = 0
getpid()                                = 1194
sendto(3, "<30>Nov 11 11:48:03 dhclient[119"..., 119, MSG_NOSIGNAL, NULL, 0) = 119
write(2, "xid: warning: no netdev with use"..., 83) = 83
write(2, "\n", 1)                       = 1
getpid()                                = 1194
sendto(3, "<30>Nov 11 11:48:03 dhclient[119"..., 90, MSG_NOSIGNAL, NULL, 0) = 90
write(2, "xid: rand init seed (0x653068f2)"..., 54) = 54
write(2, "\n", 1)                       = 1
uname({sysname="Linux", nodename="client", ...}) = 0
getpid()                                = 1194
sendto(3, "<30>Nov 11 11:48:03 dhclient[119"..., 113, MSG_NOSIGNAL, NULL, 0) = 113
write(2, "DHCPDISCOVER on enp0s1 to 255.25"..., 77) = 77
write(2, "\n", 1)                       = 1
write(5, "\377\377\377\377\377\377\10\0'\255\220s\10\0E\20\1H\0\0\0\0\200\219\226\0\0\0\0\377\377"..., 342) = 342
openat(AT_FDCWD, "/var/run/dhclient.pid", O_WRONLY|O_CREAT|O_TRUNC, 0644) = 7
fcntl(7, F_GETFL)                       = 0x8001 (flags O_WRONLY|O_LARGEFILE)
getpid()                                = 1194
newfstatat(7, "", {st_mode=S_IFREG|0644, st_size=0, ...}, AT_EMPTY_PATH) = 0
write(7, "1194\n", 5)                   = 5
close(7)                                = 0
pselect6(7, [5 6], [], NULL, {tv_sec=0, tv_nsec=0}, NULL) = 0 (Timeout)
pselect6(7, [5 6], [], NULL, {tv_sec=2, tv_nsec=91713000}, NULL) = 0 (Timeout)
getpid()                                = 1194
sendto(3, "<30>Nov 11 11:48:06 dhclient[119"..., 113, MSG_NOSIGNAL, NULL, 0) = 113
write(2, "DHCPDISCOVER on enp0s1 to 255.25"..., 77) = 77
write(2, "\n", 1)                       = 1
write(5, "\377\377\377\377\377\377\10\0'\255\220s\10\0E\20\1H\0\0\0\0\200\219\226\0\0\0\0\377\377"..., 342) = 342
pselect6(7, [5 6], [], NULL, {tv_sec=0, tv_nsec=0}, NULL) = 0 (Timeout)
pselect6(7, [5 6], [], NULL, {tv_sec=7, tv_nsec=118920000}, NULL) = 1 (in [5], left {tv_sec=6, tv_nsec=204640291})
recvmsg(5, {msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="\10\0'\255\220s\10\0'&\334h\10\0E\300\1H\27\212\0\0@\21q\251\300\2507\1\300\250"..., iov_len=1536}], msg_iovlen=1, msg_control=[{cmsg_len=36, cmsg_level=SOL_PACKET, cmsg_type=0x8}], msg_controllen=36, msg_flags=0}, 0) = 342
getpid()                                = 1194
sendto(3, "<30>Nov 11 11:48:06 dhclient[119"..., 80, MSG_NOSIGNAL, NULL, 0) = 80
write(2, "DHCPOFFER of 192.168.42.6 from "..., 44) = 44
write(2, "\n", 1)                       = 1
uname({sysname="Linux", nodename="client", ...}) = 0
getpid()                                = 1194
sendto(3, "<30>Nov 11 11:48:06 dhclient[119"..., 119, MSG_NOSIGNAL, NULL, 0) = 119
write(2, "DHCPREQUEST for 192.168.42.6 on"..., 83) = 83
write(2, "\n", 1)                       = 1
write(5, "\377\377\377\377\377\377\10\0'\255\220s\10\0E\20\1H\0\0\0\0\200\219\226\0\0\0\0\377\377"..., 342) = 342
pselect6(7, [5 6], [], NULL, {tv_sec=2, tv_nsec=248626000}, NULL) = 1 (in [5], left {tv_sec=2, tv_nsec=248624007})
recvmsg(5, {msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="\10\0'\255\220s\10\0'&\334h\10\0E\300\1H\27\213\0\0@\21q\250\300\2507\1\300\250"..., iov_len=1536}], msg_iovlen=1, msg_control=[{cmsg_len=36, cmsg_level=SOL_PACKET, cmsg_type=0x8}], msg_controllen=36, msg_flags=0}, 0) = 342
pselect6(7, [5 6], [], NULL, {tv_sec=2, tv_nsec=248572000}, NULL) = 1 (in [5], left {tv_sec=2, tv_nsec=242423897})
recvmsg(5, {msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="\10\0'\255\220s\10\0'&\334h\10\0E\300\1H\27\215\0\0@\21q\246\300\2507\1\300\250"..., iov_len=1536}], msg_iovlen=1, msg_control=[{cmsg_len=36, cmsg_level=SOL_PACKET, cmsg_type=0x8}], msg_controllen=36, msg_flags=0}, 0) = 342
getpid()                                = 1194
sendto(3, "<30>Nov 11 11:48:06 dhclient[119"..., 95, MSG_NOSIGNAL, NULL, 0) = 95
write(2, "DHCPACK of 192.168.42.6 from 19"..., 59) = 59
write(2, "\n", 1)                       = 1
getpid()                                = 1194
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7efd50b79a10) = 1213
wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 1213
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=1213, si_uid=0, si_status=0, si_utime=0, si_stime=0} ---
write(4, "lease {\n  interface \"enp0s1\";\n  "..., 475) = 475
fsync(4)                                = 0
getpid()                                = 1194
sendto(3, "<30>Nov 11 11:48:07 dhclient[119"..., 87, MSG_NOSIGNAL, NULL, 0) = 87
write(2, "bound to 192.168.42.6 -- renewa"..., 51) = 51
write(2, "\n", 1)                       = 1
openat(AT_FDCWD, "/var/run/dhclient.pid", O_WRONLY|O_CREAT|O_TRUNC, 0644) = 7
fcntl(7, F_GETFL)                       = 0x8001 (flags O_WRONLY|O_LARGEFILE)
getpid()                                = 1194
newfstatat(7, "", {st_mode=S_IFREG|0644, st_size=0, ...}, AT_EMPTY_PATH) = 0
write(7, "1194\n", 5)                   = 5
close(7)                                = 0
pselect6(7, [5 6], [], NULL, {tv_sec=2, tv_nsec=206116000}, NULL) = 0 (Timeout)
pselect6(7, [5 6], [], NULL, {tv_sec=20364, tv_nsec=131507000}, NULL) = ? ERESTARTNOHAND (To be restarted if no handler)
--- SIGINT {si_signo=SIGINT, si_code=SI_KERNEL} ---
+++ killed by SIGINT +++

И только в самом конце, на 88-й строчке видим уже знакомые нам DHCPOFFER of 192.168.55.96 from.

Хм-м, а что происходит прямо перед этим, на 85-й строке?

recvmsg(5, {msg_name=NULL, 
            msg_namelen=0, 
            msg_iov=[{iov_base="\10\0'\255\220s\10\0'&\334h\10\0E\300\1H\27\212\0\0@\21q\251\300\2507\1\300\250"..., iov_len=1536}], 
            msg_iovlen=1,
            msg_control=[{cmsg_len=36, cmsg_level=SOL_PACKET, cmsg_type=0x8}], 
            msg_controllen=36,
            msg_flags=0},
        0) = 342

Получили 342 байта из пятого дескриптора.

Мотаем пораньше, на 82-ю строку. Вот мы что-то в него записываем:

write(5, "\377\377\377\377\377\377\10\0'\255\220s\10\0E\20\1H\0\0\0\0\200\219\226\0\0\0\0\377\377"..., 342) = 342

И рядом же, ещё чуть раньше, печатаем: DHCPDISCOVER on enp0s8 to 255.25... 

Хм-м-м… Что же это за загадочный пятый дескриптор? А это самая первая строчка нашего листинга!

socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)) = 5

Конечно же, это

BPF!

...

И мы приближаемся к ответу
И мы приближаемся к ответу

Но давайте сформулируем вопрос, который и у вас наверняка назрел: зачем вот такому элементарному протоколу, совсем не требующему высокой нагрузки, быть настолько бессмертным?

А всё очень просто. DHCP работает в тот момент, когда сети на хосте ещё нет. Хост не только не может нормально матчить dst-ip в свои адреса, чтобы отправить пакеты в сетевой стек, но даже броадкастный дестинейшен (широковещательный IP-адрес 255.255.255.255) не обработает, потому что нет IP-адреса.

   Receiving packets sent to 255.255.255.255 isn't a problem on most
   modern unixes...so long as the interface is configured.  When there
   is no IPv4 address on the interface, things become much more murky.

   So, for this convoluted and unfortunate state of affairs in the unix
   systems of the day ISC DHCP was manufactured, in order to do what it
   needs not only to implement the reference but to interoperate with
   other implementations, the software must create some form of raw
   socket to operate on.

   What it actually does is create, for each interface detected on the
   system, a Berkeley Packet Filter socket (or equivalent), and program
   it with a filter that brings in only DHCP packets.  A "fallback" UDP
   Berkeley socket is generally also created, a single one no matter how
   many interfaces.

И, например, в OpenBSD эта история так же стара, как моя «Камри». Вот переписка про эту проблему в marc.info, тут — ещё одна ветка на рассылке OpenBSD. А если взглянуть на сорцы, то истории вообще без малого 30 лет!

А вот и кусочек про raw socket:

   It's not clear how this should work, and that lack of clarity is
   terribly detrimental to the NetBSD 1.1 kernel - it crashes and
   burns.

   Using raw sockets ought to be a big win over using BPF or something
   like it, because you don't need to deal with the complexities of
   the physical layer, but it appears not to be possible with existing
   raw socket implementations.  This may be worth revisiting in the
   future.  For now, this code can probably be considered a curiosity.
   Sigh. */

В итоге, чтобы заработал скромный dhclient, он должен реализовать слой обработки Ethernet! Хуже того, он должен поддержать ещё и Token Ring (проклятое!) и FDDI (легаси!) — соответствующие файлы хедеров есть в репозитории.

А поскольку стандартный сетевой стек тут не при делах, то дырку между физическим уровнем и DHCP тоже приходится закрывать в коде, реализуя протоколы IP и UDP.

   It may surprise you to realize that ISC DHCP implements 802.1
   'Ethernet' framing, Token Ring, and FDDI. In order to bridge the gap
   there between these physical and DHCP layers, it must also implement
   IP and UDP framing.

На секундочку:

❯ git ls-files | grep '\.c' | xargs wc -l                                                                                           07:34:42
       7 .cvsignore
      81 client_tables.c
    2347 clparse.c
    6129 dhc6.c
    5890 dhclient.c
     786 dhclient.conf.5
      36 dhclient.conf.example
     136 tests/duid_unittest.c
   15412 total

Наконец решение! Да ведь?

И кажется, у нас осталось только оружие судного дня.

eBPF/XDP-программа

Будем воевать с dhclient  на его поле равнозначным оружием — в ядре с XDP.

Тут у нас другая лаба. Два неймспейса в виртуалке: в одном DHCP-клиент, в другом — DHCP-сервер. Они соединены друг с другом через veth-пару.

ns-client-0:

❯ ip netns exec ns-client-0 ip link show 
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
48: veth-client-0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/ether e2:45:5e:0c:89:c8 brd ff:ff:ff:ff:ff:ff

ns-server-0:

❯ ip netns exec ns-server-0 ip link show 
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
47: veth-server-0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/ether 4e:c1:ec:de:4a:95 brd ff:ff:ff:ff:ff:ff

Поехали! Запустили dhcpd в нужном NS, тут проблем нет.

❯ ip netns exec ns-server-0 /usr/sbin/dhcpd -f -cf /etc/dhcp/dhcpd.xdp.conf -user dhcpd -group dhcpd --no-pid
Internet Systems Consortium DHCP Server 4.4.2b1
Copyright 2004-2019 Internet Systems Consortium.
All rights reserved.
For info, please visit https://www.isc.org/software/dhcp/
ldap_gssapi_principal is not set,GSSAPI Authentication for LDAP will not be used
Not searching LDAP since ldap-server, ldap-port and ldap-base-dn were not specified in the config file
Config file: /etc/dhcp/dhcpd.xdp.conf
Database file: /var/lib/dhcpd/dhcpd.leases
PID file: /var/run/dhcpd.pid
Source compiled to use binary-leases
Wrote 0 deleted host decls to leases file.
Wrote 0 new dynamic host decls to leases file.
Wrote 0 leases to leases file.
Listening on LPF/veth-server-0/4e:c1:ec:de:4a:95/10.44.0.0/24
Sending on   LPF/veth-server-0/4e:c1:ec:de:4a:95/10.44.0.0/24
Sending on   Socket/fallback/fallback-net

Запускаем tcpdump в ns-client-0 и ns-server-0.

❯ ip netns exec ns-server-0 tcpdump -i any -nns0 port 67
❯ ip netns exec ns-client-0 tcpdump -i any -nns0 port 67

Запускаем dhclient в NS ns-client-0.

❯ ip netns exec ns-client-0 dhclient -1 -lf /var/lib/dhclient/dhclient--veth-client-0.lease -pf /var/run/dhclient--veth-client-0.pid

Логи DHCP-сервера:

Nov 10 22:05:22 jarvis dhcpd[201423]: DHCPDISCOVER from e2:45:5e:0c:89:c8 via veth-server-0
Nov 10 22:05:22 jarvis dhcpd[201423]: ns1.brokenpipe.pro: host unknown.
Nov 10 22:05:22 jarvis dhcpd[201423]: ns2.brokenpipe.pro: host unknown.
Nov 10 22:05:22 jarvis dhcpd[201423]: DHCPOFFER on 10.44.0.40 to e2:45:5e:0c:89:c8 via veth-server-0
Nov 10 22:05:22 jarvis dhcpd[201423]: Dynamic and static leases present for 10.44.0.40.
Nov 10 22:05:22 jarvis dhcpd[201423]: Remove host declaration node-client-0 or remove 10.44.0.40
Nov 10 22:05:22 jarvis dhcpd[201423]: from the dynamic address pool for 10.44.0.0/24
Nov 10 22:05:22 jarvis dhcpd[201423]: DHCPREQUEST for 10.44.0.40 (10.44.0.200) from e2:45:5e:0c:89:c8 via veth-server-0
Nov 10 22:05:22 jarvis dhcpd[201423]: DHCPACK on 10.44.0.40 to e2:45:5e:0c:89:c8 via veth-server-0

tcpdump на NS ns-server-0:

22:05:22.126936 veth-server-0 B   IP 0.0.0.0.68 > 255.255.255.255.67: BOOTP/DHCP, Request from e2:45:5e:0c:89:c8, length 300
22:05:22.127870 veth-server-0 Out IP 10.44.0.200.67 > 10.44.0.40.68: BOOTP/DHCP, Reply, length 300
22:05:22.128027 veth-server-0 B   IP 0.0.0.0.68 > 255.255.255.255.67: BOOTP/DHCP, Request from e2:45:5e:0c:89:c8, length 300
22:05:22.128238 veth-server-0 Out IP 10.44.0.200.67 > 10.44.0.40.68: BOOTP/DHCP, Reply, length 300

tcpdump на NS ns-client-0:

22:05:22.126819 veth-client-0 Out IP 0.0.0.0.68 > 255.255.255.255.67: BOOTP/DHCP, Request from e2:45:5e:0c:89:c8, length 300
22:05:22.127954 veth-client-0 In  IP 10.44.0.200.67 > 10.44.0.40.68: BOOTP/DHCP, Reply, length 300
22:05:22.128023 veth-client-0 Out IP 0.0.0.0.68 > 255.255.255.255.67: BOOTP/DHCP, Request from e2:45:5e:0c:89:c8, length 300
22:05:22.128241 veth-client-0 In  IP 10.44.0.200.67 > 10.44.0.40.68: BOOTP/DHCP, Reply, length 300

Так, собственно, всё и живет. IP получили.

❯ ip netns exec ns-client-0 ip a s 
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
48: veth-client-0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default qlen 1000
    link/ether e2:45:5e:0c:89:c8 brd ff:ff:ff:ff:ff:ff
    inet 10.44.0.40/24 brd 10.44.0.255 scope global dynamic veth-client-0
       valid_lft 146sec preferred_lft 146sec
    inet6 fe80::e045:5eff:fe0c:89c8/64 scope link 
       valid_lft forever preferred_lft forever

Супер! Но это у нас всё и так работало. Давайте блокировать. 

Пишем небольшую ebpf-программу.

Код xdp_drop.c

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/ip.h>
#include <linux/in.h>
#include <linux/if_ether.h>

SEC("xdp_udp_drop")
int xdp_udp_drop_prog(struct xdp_md *ctx) {
    int ipsize = 0;
    void *data = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;
    struct ethhdr *eth = data;

    ipsize = sizeof(*eth);
    struct iphdr *ip = data + ipsize;
    ipsize += sizeof(struct iphdr);
    if (data + ipsize > data_end) {
        return XDP_PASS;
    }
    if (ip->protocol == IPPROTO_UDP) {
        return XDP_DROP;
    }
    return XDP_PASS;
}

char _license[] SEC("license") = "GPL";

Компонуем её в объектный файл.

❯ clang -O2 -g -Wall -target bpf -c xdp_drop.c -o xdp_drop.o

Пробуем применить в неймспейсе клиента на интерфейс.

❯ ip netns exec ns-client-0 ip link set veth-client-0 xdpgeneric obj xdp_drop.o sec 

Перезапрашиваем адрес.

❯ ip netns exec ns-client-0 dhclient -r
❯ ip netns exec ns-client-0 dhclient

И….

Да!

❯ ip -f inet a s enp0s1 | grep inet
❯

Да! Да! Да!

Оно работает!

Хочется с чувством хорошо выполненной работы и удовлетворением захлопнуть терминал, но точит сомнение. А при попытке встать от ноутбука аж жечь начинает.

Ок, ладно.

❯ sudo journalctl -u isc-dhcp-server
Nov 10 22:13:04 jarvis dhcpd[201423]: DHCPDISCOVER from e2:45:5e:0c:89:c8 via veth-server-0
Nov 10 22:13:04 jarvis dhcpd[201423]: DHCPOFFER on 10.44.0.40 to e2:45:5e:0c:89:c8 via veth-server-0
Nov 10 22:13:07 jarvis dhcpd[201423]: DHCPDISCOVER from e2:45:5e:0c:89:c8 via veth-server-0
Nov 10 22:13:07 jarvis dhcpd[201423]: DHCPOFFER on 10.44.0.40 to e2:45:5e:0c:89:c8 via veth-server-0
Nov 10 22:13:12 jarvis dhcpd[201423]: DHCPDISCOVER from e2:45:5e:0c:89:c8 via veth-server-0
Nov 10 22:13:12 jarvis dhcpd[201423]: DHCPOFFER on 10.44.0.40 to e2:45:5e:0c:89:c8 via veth-server-0
Nov 10 22:13:17 jarvis dhcpd[201423]: DHCPDISCOVER from e2:45:5e:0c:89:c8 via veth-server-0
Nov 10 22:13:17 jarvis dhcpd[201423]: DHCPOFFER on 10.44.0.40 to e2:45:5e:0c:89:c8 via veth-server-0
Nov 10 22:13:28 jarvis dhcpd[201423]: DHCPDISCOVER from e2:45:5e:0c:89:c8 via veth-server-0
Nov 10 22:13:28 jarvis dhcpd[201423]: ns1.brokenpipe.pro: host unknown.
Nov 10 22:13:28 jarvis dhcpd[201423]: ns2.brokenpipe.pro: host unknown.
Nov 10 22:13:28 jarvis dhcpd[201423]: DHCPOFFER on 10.44.0.40 to e2:45:5e:0c:89:c8 via veth-server-0
Nov 10 22:13:46 jarvis dhcpd[201423]: DHCPDISCOVER from e2:45:5e:0c:89:c8 via veth-server-0
Nov 10 22:13:46 jarvis dhcpd[201423]: DHCPOFFER on 10.44.0.40 to e2:45:5e:0c:89:c8 via veth-server-0
Nov 10 22:14:03 jarvis dhcpd[201423]: DHCPDISCOVER from e2:45:5e:0c:89:c8 via veth-server-0
Nov 10 22:14:03 jarvis dhcpd[201423]: DHCPOFFER on 10.44.0.40 to e2:45:5e:0c:89:c8 via veth-server-0

Ну серьёзно!

Мы заблокировали на клиенте весь UDP, но на сервере видим от клиентаDHCPDISCOVER! И сервер отправляет DHCPOFFER.

Оказывается, в XDP не поддержан egress path, и он не умеет работать с исходящими пакетами.

В целом даже есть попытки затащить egress path в XDP. Но пока — нет.

И в результате нам удалось сломать работу DHCP за счёт того, что мы заблокировали входящие DHCPOFFER, из-за чего процесс выдачи адреса не может завершиться. Но всё же нам не удалось заблокировать всю коммуникацию dhclient. Но я думаю, что тут стоит остановиться и не открывать следующие двери.

Остаётся загадкой, как всякие Cilium блокируют Egress. Вероятно, используя связку интерфейсов, где из одного интерфейса в другой перекладывается пакет, и там он становится IN, и его срезают в XDP-программе.

Похоже, самый простой вариант сломать DHCP — не запускать dhclient или выключить dhcp в конфигурации сети.

DHCP такой
DHCP такой

DHCPv6

DHCPv6, может, для кого-то и странный зверёк, но он есть, работает и используется (я лично DHCP Relay на стоечных коммутаторах в дата-центрах настраивал).

Так вот, друзья, для него это всё не нужно. НЕ НУЖНО! BPF, своя реализация Ethernet Framing, UDP/IP-заголовки самому крафтить. И его спокойно можно ограничить iptable /nftables.

Ведь в чём состоит основной конфликт сегодняшней истории? IP-адреса на интерфейсе нет — сетевой стек отбрасывает пакет. Как только он появляется как результат работы DHCP, dhclient может использовать стандартные механизмы Линукса, что он и делает для продления аренды IP-адреса через юникастовые сообщения, которые уже можно заблокировать.

А в IPv6 на интерфейсе всегда есть адрес — Link-Local из сети FE80::/12 . Сеть там всегда инициализирована, и сетевой стек не отбросит пакеты. А, ну ещё там броадкастов нет!


Со всем разобрались?

Нет! У меня для вас пара вопросов:

  • Пунтим вопрос обратно на мозг: как так iptables пакет считает в статистику, но не может дропнуть? Или может?

  • Как libpcap видит все пакеты, которые никак не захватываются никакими *tables?

Давайте докопаемся до истины в комментариях?

У меня куча времени! Что бы ещё такое изучить?


Выводы

Каждый сделает для себя сам :)

В целом, пока вы не хотите делать что-то редкое и необычное в Linux, скорее всего, всё будет просто, понятно и работоспособно. А вот если хочется высокой производительности, хорошенько закопаться в трафик на ранних этапах, сделать инъекцию безопасности в ядре или там же потраблшутить, ну или вот заблокировать dhcp на клиенте, то вы очень быстро столкнётесь со всей красотой и безобразием сети в Linux.

А ещё спасибо коллегам, которые со мной это траблшутили в ночи

И напоследок.

Комментарии (51)


  1. Aquahawk
    20.11.2023 08:51
    +19

    Хабр торт и не торт. Великолепная статья, и уже почти два часа ни одного комментария. Я прямо представляю сколько автор это ковырял, как писал эту статью и насколько обидно что нет ни одного комментария по делу. На самом деле даже сложно что-то комментировать, очень много очень полезной информации


    1. eucariot Автор
      20.11.2023 08:51
      +14

      Ну как автор я к этому давно привык. Поэтому обиды нет никакой)

      Спасибо за ваш комментарий. Ковыряли мы это втроём и с большим интересом!


      1. AlexGluck
        20.11.2023 08:51

        Рабочий день, вот к вечеру пойдёт жара. Статья топ, хабр для таких статей!


    1. rezdm
      20.11.2023 08:51
      +10

      Ну, это ж не про ремонт зубов заметка.


  1. si1v3r
    20.11.2023 08:51
    +1

    Спасибо.


  1. me21
    20.11.2023 08:51
    +3

    А почему, когда IP-адреса на интерфейсе нет — сетевой стек отбрасывает пакет. Почему бы не принимать бродкаст-пакеты?


    1. eucariot Автор
      20.11.2023 08:51
      +4

      Процитирую Бориса Лыточкина :)

      Если очень грубо, для обработки ip-пакетов (через raw socket), в том числе броадкасты, сетевому стеку нужен настроенный ip-адрес. Собственно, именно так и работает dhcp-сервер. и чтобы получать броадкаст-поток с интерфейса и выдавать адреса он требует наличия адреса из раздаваемой сети, иначе не запустится.

      raw socket - это raw ip socket, для него нужен сконфигурированный на интерфейсе IP стек. иначе работать нужно уже на уровне ethernet. Например, через bpf


  1. MixaSg
    20.11.2023 08:51
    +1

    Я однажды задал похожую, но намного более простую, задачку на собеседовании: "как работает ping". Хотел услышать хотя бы просто рассуждения на тему, даже не рассказ про RAW socket. А целый CCIE из интегратора мне сказал: "Ну вы же понимаете, как сдается CCIE..."


    1. AlexGluck
      20.11.2023 08:51
      +1

      icmp, osi level 2 protocol инкапсулируется в айпи пакет и использует 7,8,9,11 типы. Баян молодости)

      UPD: запрос идёт с 8 типом, добавил его. А ответы в дампе могут быть уже 7,9,11 типов.


      1. Aelliari
        20.11.2023 08:51

        Ты просто не CCIE


      1. MixaSg
        20.11.2023 08:51

        Да, всё так. Почему-то огромную сложность для понимания вызывает тот факт, что icmp reply отвечает само ядро ОС, а не какой-то сторонний daemon. И что мы можем, например, написать свой обработчик, отключив этот механизм через "echo 1 > /proc/sys/net/ipv4/icmp_echo_ignore_all" (в linux)


      1. mihs0l
        20.11.2023 08:51

        level 2?


        1. AlexGluck
          20.11.2023 08:51

          я тоже думал что 3й, но по факту второй. ICMP можно в fc сетях использовать, если из актуальных рассматривать.


          1. mihs0l
            20.11.2023 08:51

            А там именно icmp 8 0 используется, которая прямо на уровне фреймов инкапсулируется, или просто одноименная имплементация?
            Потому что так-то и ATM ping и OSI ping (это не опечатка) существовали, но это просто название одинаковое.


            1. AlexGluck
              20.11.2023 08:51

              Вам не кажется что вы напрашиваетесь уже на статью по истории развития вычислительных сетей?) Как сказали ниже, я не CCIE)


              1. mihs0l
                20.11.2023 08:51

                Не обязательно быть CCIE, чтобы сделать вывод о странности утверждения насчет layer 2, тем более в контексте IP, на основании совпадения названия одной специфической имплементации.


            1. AlexGluck
              20.11.2023 08:51

              Насколько я могу судить там своя имплементация протокола.


    1. eucariot Автор
      20.11.2023 08:51
      +10


  1. dmitryrf
    20.11.2023 08:51
    +1

    Очень познавательно, спасибо!


  1. olegtsss
    20.11.2023 08:51

    Мне вот почему-то кажется, что я блокировал DHCP на L2 firewall в RouterOS (мы понимаем, что это тот же linux плюс минус). У MikroTik в стеке есть отличный L2 firewall, который решает различные задачи, как раз до L3 (неожиданно )), мне больше всего нравится элегантность так называемого transpered firewall. А как насчет защиты от ложных (вредительских) DHCP-серверов дела обстоят в Linux, где это настраивается?


    1. medvm
      20.11.2023 08:51

      Жаль только, что почти любые (кроме vlan фильтрации) правила bridge filter ломают bridge fast path на большей части свич-чипов и пользовать железку на гигабите с натом становится невозможно.


    1. front000
      20.11.2023 08:51

      Вы не уловили суть: речь о блокировке на стороне клиента, а не о транзитном трафике.


      1. olegtsss
        20.11.2023 08:51

        Сегодня сервер. Завтра клиент, тут все понятно. А так L2 firewall это специфическая штука. Для решения общих задач она не нужна. По крайней мере мой опыт говорит об этом. А вот с академической точки зрения - интересно)


  1. loskiq
    20.11.2023 08:51

    офигеть...
    я бы заблочил выше на свиче с помощью acl?


    1. eucariot Автор
      20.11.2023 08:51

      Да вопросов нет, что это можно разными способами сделать.

      Просто тут случился интересный, хотя и практически бесполезный разрыв привычных представлений)


  1. inkvizitor68sl
    20.11.2023 08:51
    +1

    Боря Лыточкин и Паша Пушкарёв

    Закидывайте лекцию в ExYa уже -)


    1. inkvizitor68sl
      20.11.2023 08:51
      +1

      https://yandex.ru/yaintern/training/devops-training
      Выяснилось в личке, что если достаточно долго скроллить и смотреть глазами, а не как я, то записи там есть -)
      Спасибо )


  1. paveltyurikov
    20.11.2023 08:51
    -3

    А какой практический смысл блокировки протокола? Если у вас есть сервер раздающий адреса, значит вам это нужно. Если вы не желаете получать адрес, то указываете его статическим и останавливаете клиент. И т.д.
    Все эти задачи с подковыркой оторванные от реально опыта сделают специалиста хакером. Он будет хернёй на работе страдать с гордостью, конечно, но всё же. И будет думать, что занят скучными и бесполезными делами и от того хотеть "крутых" задач. И, возможно, создавать невероятные проблемы, которые можно героически решать.
    А надо чтобы всё было по стандартам сделано. Скучно, да, но стандартно, чтобы когда уйдёшь с работы ваша смена смогла быстро понять почему на хосте dhclient не работает.
    В итоге выяснилось, что вы давали задачу не убедившись, что она имеет решение, которого она не имеет. Я бы не стал считать допустимым решением задачи написание сишного кода. Такое нередко имеет отрицательные последствия, разделяя людей на тех кто уважает стандарты и тех кто стандарты не уважает.


    1. slonopotamus
      20.11.2023 08:51
      +8

      Смысл в том, что это даёт понимание "а как эта штуковина вообще говоря работает", и мир начинает чуть меньше состоять из магии.


      1. khajiit
        20.11.2023 08:51
        +1

        Это было интересное чтиво, но применять полученное от его прочтения знание — негде, кроме парочки сверхузких ниш.
        Так что оно обязательно забудется. И мир начинает чуть меньше состоять из магии на этом закончится.


        1. eucariot Автор
          20.11.2023 08:51
          +1

          Зато ребята вот нашли, где на практике применить XDP-программку с пользой для дела)


          1. khajiit
            20.11.2023 08:51

            Несомненно )
            Но польза одноразового прочтения развлекательных статей — а вне связей с профессиональной деятелньностью любое чтение будет развлекательным, несмотря на его хардкорность — слишком переоценена.

            З.Ы. Ваша статья получилась весьма хардконой, большое вам спасибо за труд )


      1. paveltyurikov
        20.11.2023 08:51
        +1

        Для тех кто пользуется документацией в линуксе нет магии. Вопрос был не "зачем вы занимались этим увлекательным исследованием?", а "зачем вы даёте задачи, которые сами не можете решить?" и которые без как раз "магии" решить не сможет никто.


        1. eucariot Автор
          20.11.2023 08:51

          Вы меня, конечно, извините, но

          Сначала мы убрали эту задачку под звёздочку. Подумали — убрали под две. А потом вообще исключили из домашки.

          Да, мы потом после нашего исследования предложили тем, кто чувствует в себе силу, посмотреть на это. Но как-то градус вашей категоричности не соответствует такой формулировке.


          1. paveltyurikov
            20.11.2023 08:51
            +1

            Простите, если я излишне резок в своих выражениях. Моё грандиозное почтение, кстати, за циклы замечательных статей "Для самых маленьких". Я упустил слово "домашка" и видимо подумал про условия найма, и думал что речь идёт о будущих сотрудниках в штате системных/сетевых администраторов. Бесспорно, поддерживать в учениках увлечённость и любознательность благое дело.


            1. eucariot Автор
              20.11.2023 08:51

              Недопонимание устранено)

              Спасибо на добром слове!


  1. hogstaberg
    20.11.2023 08:51
    +5

    И тут на сцену победным шагом выходит tc


    1. eucariot Автор
      20.11.2023 08:51
      +1

      О, да, я как-то про него вообще забыл написать.

      Добрыйвечер.


    1. EvilMan
      20.11.2023 08:51
      +1

      Да, причём именно в egress направлении замечательно фильтруется. Вешаем любую classfull дисциплину и навешиваем несложный фильтр. Например:

      ~/# tc q add dev <iface> root handle 1: prio
      ~/# tc f add dev <iface> parent 1: protocol ip prio 1 flower ip_src 0.0.0.0/32 ip_dst 255.255.255.255 ip_protocol udp src_port 68 dst_port 67 action drop 
      

      Но нужно дополнительно исследовать поведение, когда на сокете выставлена опция PACKET_QDISC_BYPASS. Тут я навскидку не скажу - надо читать код ядра.

      Для ядер, начиная с 5.16 доступен дополнительный способ вместо tc - там добавлен хук netdev/egress, который можно использовать из nftables.

      ~/# nft -i
      nft> flush ruleset
      nft> add table netdev t
      nft> add chain netdev t egress_chain { type filter hook egress device <iface> priority 0; }
      nft> add rule netdev t egress_chain ip saddr 0.0.0.0/32 ip daddr 255.255.255.255/32 meta l4proto udp udp sport 68 udp dport 67 counter drop
      

      А вот в направлении ingress фильтрация через tc / nftables обламывается, потому что клонирование пакета происходит раньше, чем эти инструменты вступают в работу. Это можно увидеть на прекрасной диаграмме из вики (на ней есть неточности, так что не верьте всему и перепроверяйте всегда).


      1. EvilMan
        20.11.2023 08:51
        +1

        UPD: ни через tc, ни через netdev / ingress хук невозможно заблокировать получение датаграм на сокеты af_packet / pf_packet, т.к. клонирование происходит вот здесь (при условии, что у нас в системе есть фильтры, которые отбирают данные датаграммы; если таких фильтров нет, то и клонирования не происходит - т.н. ленивое поведение), а обработка tc qdisc ingress происходит чуть дальше по коду, и только после происходит обработка через netdev / ingress хук. Так что, похоже, XDP является одним из немногих оставшихся вариантов дропнуть подобный трафик, т.к. выполнение XDP программы происходит чуть раньше, чем происходит клонирование датаграммы.


        1. hogstaberg
          20.11.2023 08:51

          На вход - да, af/pf_packet не дропнешь т.к. они раньше происходят. Я писал именно в контексте конкретной задачи "дропнуть на клиенте dhcp запросы". @eucariotдаже ближе к концу статьи сокрушался о том, что заблокировался только ответный трафик, но не исходящий.

          Хотяяяяя... А если отправлять пакеты через af_packet и mmap tx ring...? Кажется опять придётся запускать коллайдер.


  1. Igelko
    20.11.2023 08:51
    +2

    блин, насколько же проще забанить DHCP кому-то со стороны сервера DHCP или где-то по дороге :-D


    1. eucariot Автор
      20.11.2023 08:51
      +2

      Не спортивно)


  1. aleksei_boroda
    20.11.2023 08:51
    +1

    спасибо за погружение!


  1. jFarsh
    20.11.2023 08:51
    +4

    Эта статья настолько хороша, что я не поленился зарегистрироваться на хабре, чтобы просто поблагодарить вас. За 30 минут я узнал больше нового, чем за предшествующий месяц. Не уверен, что будет случай применить эти знания на практике, но, на какой-то момент у меня отпала челюсть.


    1. eucariot Автор
      20.11.2023 08:51
      +2

      Приятно и забавно)


  1. aspsk
    20.11.2023 08:51
    +2

    Остаётся загадкой, как всякие Cilium блокируют Egress

    Во-первых, при помощи tc, а не XDP. XDP используется только на железках, на veth (и прочих виртуальных устройствах) его использовать смысла нет, так как skb все равно создается. (Кроме этого, xdpgeneric кривой, а нативный xdp на veth еще более кривой.)

    Во-вторых, tc программа сажается на внешний кусок veth, находящийся в рутовом namespace, так как внутренний может быть доступен приложению внутри контейнера, которое может поменять логику BPF программы. (Это поменяется, когда мы станем использовать новый виртуальный девайс netkit, см. https://lpc.events/event/17/contributions/1581/ и сможем сажать программу прямо внутрь.)

    А насчет сабжа у меня когда-то был dhcpclient на XDP (https://github.com/aspsk/xdp-dhcp), который сажался на tap и отвечал виртуалочке.


  1. hodzanassredin
    20.11.2023 08:51

    Просто интересно, а как с этим дело обстоит во FreeBSD и других ос?


    1. mihs0l
      20.11.2023 08:51

      Как минимум во FreeBSD, судя по всему -- аналогично. Нужен BPF перехват по той же причине. Про OpenBSD есть упоминание в статье у Марата.
      Интересно, как в Windows это устроено.


  1. EvilMan
    20.11.2023 08:51

    Пунтим вопрос обратно на мозг: как так iptables пакет считает в статистику, но не может дропнуть? Или может?

    На стековерфлоу есть ответ на вопрос, почему счётчики правил для отбрасывания пакетов инкрементируются (и эти пакеты на самом деле дропаются), а dhcp клиент продолжает работать без проблем.


    1. hogstaberg
      20.11.2023 08:51

      Дыг ответ достаточно очевиден даже если не подглядывать в ссылку. Потому, что dhcp клиент получает свою копию пакета, а iptables свою.