Сетевой стек 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.
Сделаю тут небольшое отступление: мы тут в Яндексе придумали провести Тренировки по 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
: мы их там никогда и не увидели бы.
Ок. Что же дальше?
Судя по всему, нам нужен 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, и он не умеет работать с исходящими пакетами.
Пруф номер один: рассылка.
Пруф номер два: stackoverflow.
В целом даже есть попытки затащить egress path в XDP. Но пока — нет.
И в результате нам удалось сломать работу DHCP за счёт того, что мы заблокировали входящие DHCPOFFER
, из-за чего процесс выдачи адреса не может завершиться. Но всё же нам не удалось заблокировать всю коммуникацию dhclient
. Но я думаю, что тут стоит остановиться и не открывать следующие двери.
Остаётся загадкой, как всякие Cilium блокируют Egress. Вероятно, используя связку интерфейсов, где из одного интерфейса в другой перекладывается пакет, и там он становится IN
, и его срезают в XDP-программе.
Похоже, самый простой вариант сломать DHCP — не запускать dhclient или выключить 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
?
Давайте докопаемся до истины в комментариях?
У меня куча времени! Что бы ещё такое изучить?
Get started with XDP - пишем первую программу
eBPF: Unlocking the Kernel - не техника, но история. Алексей Старовойтов тоже в кадре.
Эпизод linkmeup про реализацию промышленного файрвола на x86
Выводы
Каждый сделает для себя сам :)
В целом, пока вы не хотите делать что-то редкое и необычное в Linux, скорее всего, всё будет просто, понятно и работоспособно. А вот если хочется высокой производительности, хорошенько закопаться в трафик на ранних этапах, сделать инъекцию безопасности в ядре или там же потраблшутить, ну или вот заблокировать dhcp на клиенте, то вы очень быстро столкнётесь со всей красотой и безобразием сети в Linux.
А ещё спасибо коллегам, которые со мной это траблшутили в ночи
Борису Лыточкину — за тренировки для DevOps, задачку, материал для статьи и помощь с лабой. Боря — сетевой прораб Яндекса, специализируется на Wi-Fi и сетевой безопасности (вот, кстати, его подкасты про файрволы).
Илье Шестопалову — за консультации и нырок в нору с XDP. Илья — SRE в сетевой инфраструктуре Yandex Cloud (а вот и эпизод с его участием про сеть в K8s).
Павлу Пушкарёву — за тренировки для DevOps (делюсь отпадным стикерпаком с котиками в коробках, который появился силами всех лекторов).
И напоследок.
Комментарии (51)
me21
20.11.2023 08:51+3А почему, когда IP-адреса на интерфейсе нет — сетевой стек отбрасывает пакет. Почему бы не принимать бродкаст-пакеты?
eucariot Автор
20.11.2023 08:51+4Процитирую Бориса Лыточкина :)
Если очень грубо, для обработки ip-пакетов (через raw socket), в том числе броадкасты, сетевому стеку нужен настроенный ip-адрес. Собственно, именно так и работает dhcp-сервер. и чтобы получать броадкаст-поток с интерфейса и выдавать адреса он требует наличия адреса из раздаваемой сети, иначе не запустится.
raw socket - это raw ip socket, для него нужен сконфигурированный на интерфейсе IP стек. иначе работать нужно уже на уровне ethernet. Например, через bpf
MixaSg
20.11.2023 08:51+1Я однажды задал похожую, но намного более простую, задачку на собеседовании: "как работает ping". Хотел услышать хотя бы просто рассуждения на тему, даже не рассказ про RAW socket. А целый CCIE из интегратора мне сказал: "Ну вы же понимаете, как сдается CCIE..."
AlexGluck
20.11.2023 08:51+1icmp, osi level 2 protocol инкапсулируется в айпи пакет и использует 7,8,9,11 типы. Баян молодости)
UPD: запрос идёт с 8 типом, добавил его. А ответы в дампе могут быть уже 7,9,11 типов.
MixaSg
20.11.2023 08:51Да, всё так. Почему-то огромную сложность для понимания вызывает тот факт, что icmp reply отвечает само ядро ОС, а не какой-то сторонний daemon. И что мы можем, например, написать свой обработчик, отключив этот механизм через "echo 1 > /proc/sys/net/ipv4/icmp_echo_ignore_all" (в linux)
mihs0l
20.11.2023 08:51level 2?
AlexGluck
20.11.2023 08:51я тоже думал что 3й, но по факту второй. ICMP можно в fc сетях использовать, если из актуальных рассматривать.
mihs0l
20.11.2023 08:51А там именно icmp 8 0 используется, которая прямо на уровне фреймов инкапсулируется, или просто одноименная имплементация?
Потому что так-то и ATM ping и OSI ping (это не опечатка) существовали, но это просто название одинаковое.AlexGluck
20.11.2023 08:51Вам не кажется что вы напрашиваетесь уже на статью по истории развития вычислительных сетей?) Как сказали ниже, я не CCIE)
mihs0l
20.11.2023 08:51Не обязательно быть CCIE, чтобы сделать вывод о странности утверждения насчет layer 2, тем более в контексте IP, на основании совпадения названия одной специфической имплементации.
olegtsss
20.11.2023 08:51Мне вот почему-то кажется, что я блокировал DHCP на L2 firewall в RouterOS (мы понимаем, что это тот же linux плюс минус). У MikroTik в стеке есть отличный L2 firewall, который решает различные задачи, как раз до L3 (неожиданно )), мне больше всего нравится элегантность так называемого transpered firewall. А как насчет защиты от ложных (вредительских) DHCP-серверов дела обстоят в Linux, где это настраивается?
medvm
20.11.2023 08:51Жаль только, что почти любые (кроме vlan фильтрации) правила bridge filter ломают bridge fast path на большей части свич-чипов и пользовать железку на гигабите с натом становится невозможно.
front000
20.11.2023 08:51Вы не уловили суть: речь о блокировке на стороне клиента, а не о транзитном трафике.
olegtsss
20.11.2023 08:51Сегодня сервер. Завтра клиент, тут все понятно. А так L2 firewall это специфическая штука. Для решения общих задач она не нужна. По крайней мере мой опыт говорит об этом. А вот с академической точки зрения - интересно)
inkvizitor68sl
20.11.2023 08:51+1Боря Лыточкин и Паша Пушкарёв
Закидывайте лекцию в ExYa уже -)
inkvizitor68sl
20.11.2023 08:51+1https://yandex.ru/yaintern/training/devops-training
Выяснилось в личке, что если достаточно долго скроллить и смотреть глазами, а не как я, то записи там есть -)
Спасибо )
paveltyurikov
20.11.2023 08:51-3А какой практический смысл блокировки протокола? Если у вас есть сервер раздающий адреса, значит вам это нужно. Если вы не желаете получать адрес, то указываете его статическим и останавливаете клиент. И т.д.
Все эти задачи с подковыркой оторванные от реально опыта сделают специалиста хакером. Он будет хернёй на работе страдать с гордостью, конечно, но всё же. И будет думать, что занят скучными и бесполезными делами и от того хотеть "крутых" задач. И, возможно, создавать невероятные проблемы, которые можно героически решать.
А надо чтобы всё было по стандартам сделано. Скучно, да, но стандартно, чтобы когда уйдёшь с работы ваша смена смогла быстро понять почему на хосте dhclient не работает.
В итоге выяснилось, что вы давали задачу не убедившись, что она имеет решение, которого она не имеет. Я бы не стал считать допустимым решением задачи написание сишного кода. Такое нередко имеет отрицательные последствия, разделяя людей на тех кто уважает стандарты и тех кто стандарты не уважает.slonopotamus
20.11.2023 08:51+8Смысл в том, что это даёт понимание "а как эта штуковина вообще говоря работает", и мир начинает чуть меньше состоять из магии.
khajiit
20.11.2023 08:51+1Это было интересное чтиво, но применять полученное от его прочтения знание — негде, кроме парочки сверхузких ниш.
Так что оно обязательно забудется. И мир начинает чуть меньше состоять из магии на этом закончится.eucariot Автор
20.11.2023 08:51+1Зато ребята вот нашли, где на практике применить XDP-программку с пользой для дела)
khajiit
20.11.2023 08:51Несомненно )
Но польза одноразового прочтения развлекательных статей — а вне связей с профессиональной деятелньностью любое чтение будет развлекательным, несмотря на его хардкорность — слишком переоценена.З.Ы. Ваша статья получилась весьма хардконой, большое вам спасибо за труд )
paveltyurikov
20.11.2023 08:51+1Для тех кто пользуется документацией в линуксе нет магии. Вопрос был не "зачем вы занимались этим увлекательным исследованием?", а "зачем вы даёте задачи, которые сами не можете решить?" и которые без как раз "магии" решить не сможет никто.
eucariot Автор
20.11.2023 08:51Вы меня, конечно, извините, но
Сначала мы убрали эту задачку под звёздочку. Подумали — убрали под две. А потом вообще исключили из домашки.
Да, мы потом после нашего исследования предложили тем, кто чувствует в себе силу, посмотреть на это. Но как-то градус вашей категоричности не соответствует такой формулировке.
paveltyurikov
20.11.2023 08:51+1Простите, если я излишне резок в своих выражениях. Моё грандиозное почтение, кстати, за циклы замечательных статей "Для самых маленьких". Я упустил слово "домашка" и видимо подумал про условия найма, и думал что речь идёт о будущих сотрудниках в штате системных/сетевых администраторов. Бесспорно, поддерживать в учениках увлечённость и любознательность благое дело.
hogstaberg
20.11.2023 08:51+5И тут на сцену победным шагом выходит tc
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 обламывается, потому что клонирование пакета происходит раньше, чем эти инструменты вступают в работу. Это можно увидеть на прекрасной диаграмме из вики (на ней есть неточности, так что не верьте всему и перепроверяйте всегда).
EvilMan
20.11.2023 08:51+1UPD: ни через tc, ни через netdev / ingress хук невозможно заблокировать получение датаграм на сокеты af_packet / pf_packet, т.к. клонирование происходит вот здесь (при условии, что у нас в системе есть фильтры, которые отбирают данные датаграммы; если таких фильтров нет, то и клонирования не происходит - т.н. ленивое поведение), а обработка tc qdisc ingress происходит чуть дальше по коду, и только после происходит обработка через netdev / ingress хук. Так что, похоже, XDP является одним из немногих оставшихся вариантов дропнуть подобный трафик, т.к. выполнение XDP программы происходит чуть раньше, чем происходит клонирование датаграммы.
hogstaberg
20.11.2023 08:51На вход - да, af/pf_packet не дропнешь т.к. они раньше происходят. Я писал именно в контексте конкретной задачи "дропнуть на клиенте dhcp запросы". @eucariotдаже ближе к концу статьи сокрушался о том, что заблокировался только ответный трафик, но не исходящий.
Хотяяяяя... А если отправлять пакеты через af_packet и mmap tx ring...? Кажется опять придётся запускать коллайдер.
jFarsh
20.11.2023 08:51+4Эта статья настолько хороша, что я не поленился зарегистрироваться на хабре, чтобы просто поблагодарить вас. За 30 минут я узнал больше нового, чем за предшествующий месяц. Не уверен, что будет случай применить эти знания на практике, но, на какой-то момент у меня отпала челюсть.
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 и отвечал виртуалочке.
hodzanassredin
20.11.2023 08:51Просто интересно, а как с этим дело обстоит во FreeBSD и других ос?
mihs0l
20.11.2023 08:51Как минимум во FreeBSD, судя по всему -- аналогично. Нужен BPF перехват по той же причине. Про OpenBSD есть упоминание в статье у Марата.
Интересно, как в Windows это устроено.
EvilMan
20.11.2023 08:51Пунтим вопрос обратно на мозг: как так iptables пакет считает в статистику, но не может дропнуть? Или может?
На стековерфлоу есть ответ на вопрос, почему счётчики правил для отбрасывания пакетов инкрементируются (и эти пакеты на самом деле дропаются), а dhcp клиент продолжает работать без проблем.
hogstaberg
20.11.2023 08:51Дыг ответ достаточно очевиден даже если не подглядывать в ссылку. Потому, что dhcp клиент получает свою копию пакета, а iptables свою.
Aquahawk
Хабр торт и не торт. Великолепная статья, и уже почти два часа ни одного комментария. Я прямо представляю сколько автор это ковырял, как писал эту статью и насколько обидно что нет ни одного комментария по делу. На самом деле даже сложно что-то комментировать, очень много очень полезной информации
eucariot Автор
Ну как автор я к этому давно привык. Поэтому обиды нет никакой)
Спасибо за ваш комментарий. Ковыряли мы это втроём и с большим интересом!
AlexGluck
Рабочий день, вот к вечеру пойдёт жара. Статья топ, хабр для таких статей!
rezdm
Ну, это ж не про ремонт зубов заметка.