Приветствую уважаемое сообщество.
Будучи обычным «админом локалхоста» столкнулся с задачей:
Необходимо на уровне файервола (до попадания пакета к демону BIND9) фильтровать пакеты с запросом DNS ANY. По сути надо в кадре DNS после доменного имени найти последовательность 0x00FF0001 (QTYPE ANY, CLASS IN).
Условия задачи и проблемы, с которыми я столкнулся:
- BIND9 не патчим, пользуемся ванильной сборкой;
- Модуль string для iptables отрабатывает не совсем хорошо, да ещё и создаёт большую нагрузку.
- Модуль u32 не даёт достаточной гибкости, чтобы вычислить требуемое смещение для сравнения.
- Кадр DNS QUERY содержит также анонс EDNS(0), так что прочитать последние 4 байта в пакете уже не канает.
- Надо рассчитывать на то, что доменное имя может иметь произвольную длину.
В ходе исследования вопроса я пришёл к выводу, что наиболее разумный вариант - написать код для регистровой машины cBPF. Писать решил сразу на ассемблере cBPF, так как на сишечке - лютый геморрой и куча промежуточных ступеней. Перевожу в опкоды с помощью компилятора bpfc из пакета netsniff-ng. Отлаживаю в bpf_dbg из сборки Cloudflare bpftools.
Сам код фильтра такой:
; Check if we've got the IP packet on top of Ethernet frame:
check_ip:
ldh [12] ; Load the value from offset 12 into A register.
jneq #0x800, pass ; Check if we're dealing with IP packet or pass it AS IS.
; Check the protocol field for packet type == 17 (UDP)
check_proto:
ldb [23] ; Check. tcpdump has advised me to use offset 23 instead of 27.
jneq #0x11, pass ; Pass the packet if it's type is not UDP.
; Check for the packet dport == 53.
check_dport:
ldh [36] ; Load the u16 into A register from offset 36.
jneq #0x35, pass ; Check if the dport is 53.
; Check if we have the unfragmented packet (or the first fragment).
check_fragmentation:
ldh [20] ; Skip all headers right to UDP payload (Load UDP Fragment Offset).
jset #0x1fff, pass ; Returns 0. Test if we have the unfragmented packet or packet's first fragment, drop the packet otherwise.
ldxb 4*([14]&0xf)
ldb [x + 24]
jset #0x80, pass
; Check for DNS QTYPE == ANY and QCLASS == IN
find_qtype:
ldb [54] ; Load first byte of the query RR into A. It contains relative count/offset of query bytes or 0x00 in the end.
add #0x37 ; Add 54 (query offset byte position) + 1 (+ 1 byte forward) to the offset value in A to make the offset absolute.
tax ; Move the absolute offset to X.
ldb [x + 0x0] ; Load the value from the absolute offset.
jeq #0x00, check_qtype ; Move to the QTYPE check code if it's the end of the query string.
add x ; Add the absolute offset from A to relative offset in X.
add #0x1 ; Move the absolute offset 1 byte forward, to the next offset value.
tax ; Move the new (adjusted) absolute offset from A to X.
ldb [x + 0x0] ; Load the value by the new calcaulated offset.
jeq #0x00, check_qtype ; Check against end of the query once again.
add x
add #0x1
tax
ldb [x + 0x0]
jeq #0x00, check_qtype ; And again, and again...
jmp pass ; Pass the packet if we were unable to find the desired value. A little failover.
; Check the query type in case we were able to find the end of the string!
check_qtype:
txa ; Move the absolute offset from X into A.
add #0x1 ; Move the offset 1 byte forward from query end.
tax ; Move the absolute offset value to the X register from A.
ld [x + 0x0] ; Load the u32 from absolute offset specified in X register.
jeq #0x00FF0001, drop ; Drop the packet if the value in A is 0x00FF0001 or else pass the packet.
pass:
ret #-1 ; Label/Return code signaling to pass the inspected packet.
drop:
ret #0 ; Label/Return code signaling to drop packet.
Код гружу в xt_bpf командой
iptables -I INPUT 1 -m bpf --bytecode '<байт-код>'
.Проблема: В отладчике оно пакет матчит и исправно уходит на метку drop:, но в реальности, как BIND9 ругался на избыток таких пакетов, так и ругается. Может, не всё матчит, а, может, неправильно правило прописываю.
Если у кого есть идеи, но нет знания тематики вопроса, то в комментариях я готов пояснить нюансы размещения данных в пакете и описанные в коде действия. Сам я, конечно, не великий специалист.