return

Что ты такое, dhclient?

21 ноября 2023, 08:45

Вот прям тут текст с иголками.
А на хабре можно лайкнуть — https://habr.com/ru/companies/yandex/articles/774462/.


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


Сетевой стек 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— когда мы пишем трафик напрямую в память, но не свою, а чужую — на удалённой по сети машине.

И это я ещё tc не вспомнил.

В этих копаниях можно уйти безгранично далеко.

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


DHCP

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


Источник: wikipedia.org


Сделаем тут небольшое отступление — мы тут в Яндексе придумали провести «Тренировки по 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: 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.

Источник

И ещё почитать подетальнее: Illustrated introduction to Linux 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 какой-то другой? Типа широковещательный или все 0? Всё ещё не будем запускать 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? Ну мало ли, может я дурак?
Тоже нет.
Давайте вообще сеть выключим через 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

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

Нас интересует кусок с 381-й строчки и до конца

В самом конце на 468-й строчке видим уже знакомые нам DHCPOFFER of 192.168.55.96 from …
Хмм, а что происходит прямо перед этим (на 465-й)?

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 байта из 5-го дескриптора.
Мотаем пораньше, на 462-ую, вот мы в него записываем что-то

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…
Хммм. что же это за загадочный 5й дескриптор?!

А это самая первая строчка нашего куска:

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

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

BPF!

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

А всё очень просто на самом деле. DHCP работает в тот момент, когда сети на хосте ещё нет. Хост не только не может нормально матчить dst-ip в свои адреса, чтобы отправить пакеты в сетевой стек, но даже броадкастный дестинейшен не обработает — потому что нет 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.

Отсюда

И например, в опёнках это история так же стара, как моя камри.
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

Я прям хочу обратить на это внимание — в dhclient реализовали работу с Ethernet, IP и UDP, потому что не могли воспользоваться ванильной реализацией в стеке в ядре!


Наконец решение! Да ведь?
Ну и кажется, у нас осталось только оружие судного дня.


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

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

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

ns-client-0:

❯ ip netns exec ns-client-0 ip link show
1: lo: 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: 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: 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: 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

Поехали!

Итак запустили в нужном NS dhcpd, тут проблем нет:

❯ 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: 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: 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 #include
#include #include #include
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 xdp_udp_drop

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

❯ 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, может, для кого-то и является странным зверьком. Но он есть, работает и используется (лично релеи на стоечных коммутаторах в дата-центрах настраивал).
Так вот, друзья, для него это всё не нужно. НЕ НУЖНО! BPF, своя реализация Ethernet Framing, UDP/IP заголовки самому крафтить. И его спокойно можно ограничить iptable/nftables.
Ведь в чём состоит основной конфликт сегодняшней истории? IP адреса на интерфейсе нет — сетевой стек отбрасывает пакет. Как только он появляется как результат работы DHCP — dhclient может использовать стандартные механизмы линукса — что он и делает для продления лиз через юникастовые сообщения, которые уже можно заблокировать.

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


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

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

  • Пунтим вопрос обратно на мозг: как так iptables пакет считает в статистику, но не может дропнуть? Или может?
  • Как libpcap видит все пакеты, которые никак не захватываются никакими x-tables?

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


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

  • Борис Лыточкин. За тренировки для DevOps, задачку, материал для статьи и помощь с лабой. Боря — сетевой прораб Яндекса, специализируется на Wi-Fi и сетевой безопасности (вот, кстати, его подкасты про файрволы).
  • Илья Шестопалов. За консультации и нырок в нору с XDP. Илья — SRE в сетевой инфраструктуре Yandex Cloud (А вот и эпизод с его участием про сеть в K8s).
  • Паша Пушкарёв — за тренировки для DevOps.

А ещё делюсь отпадным стикерпаком с котиками в коробках, который появился силами всех лекторов.


На последок

like 0 views 1760 message 0

0 коментариев

Ещё статьи

Задача №6.6
На маршрутизаторе klgr-balt-gw1 настроено перераспределение маршрутов EIGRP в OSPF. Далее по сети маршруты передаются как внешние с метрикой 20, которая не увеличивается по пути передачи маршрута. Необходимо изменить настройки так, ...
like 0 12972 1
29 октября 2012
Ответ к задаче №6.7
Задача 6.7На klgr-balt-gw1:klgr-balt-gw1(config)# router ospf 1klgr-balt-gw1(config-router)# network 172.16.2.32 0.0.0.3 area 0klgr-balt-gw1(config-router)# network 172.16.255.64 0.0.0.0 area 0klgr-balt-gw1(config-router)# network 172.16.2.36 0.0.0.3 area 1klgr-balt-gw1(config-router)# network 172.16.2.40 0.0.0.3 area 1klgr-balt-gw1(config-router)# network 10.0.0.0 0.255.255.255 area ...
like 0 15038 7
29 октября 2012
Ответ к задаче №6.6
Задача 6.6На klgr-balt-gw1:klgr-balt-gw1(config)# router ospf 1klgr-balt-gw1(config-router)# redistribute eigrp 1 metric-type 1 subnets
like 0 9375 2
29 октября 2012