Привет коллегам!

На днях играясь с ng_netflow на тестовой машине нарвался на весьма нетривиальные эффекты, возникающие при взаимодействии связки из ipfw и netgraph'а с multicast-трафиком. В процессе разбирательств выяснилось, что причина загадочного поведения, которое я наблюдал (я его опишу ниже) не имеет отношения к собственно ng_netflow, а связана с ng_ipfw, через который трафик у меня попадал в ng_netflow. Поэтому ниже я буду описывать предельно упрощенный setup, в котором нет ng_netflow, но на котором эти эффекты можно наблюдать.

Итак, имеется FreeBSD 8.2-RELEASE:
#uname -a
FreeBSD tgw.icyb.net.ua 8.2-RELEASE FreeBSD 8.2-RELEASE #0: Fri Feb 18 02:24:46 UTC 2011 [email protected]:/usr/obj/usr/src/sys/GENERIC i386

Загружены следуюшие модули:
#kldstat
Id Refs Address    Size     Name
 1   12 0xc0400000 bd97b4   kernel
 2    2 0xc63cc000 11000    ipfw.ko
 3    1 0xc63dd000 d000     libalias.ko
 4    3 0xc668b000 b000     netgraph.ko
 5    1 0xc669b000 4000     ng_socket.ko
 6    1 0xc669f000 2000     ng_ipfw.ko

На машине есть такие интерфейсы (показаны только те, что в UP, остальные не имеют отношения к делу):
#ifconfig -au
em0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
        
options=209b<RXCSUM,TXCSUM,VLAN_MTU,VLAN_HWTAGGING,VLAN_HWCSUM,WOL_MAGIC>
        ether 00:19:d1:24:76:c4
        media: Ethernet autoselect (1000baseT <full-duplex>)
        status: active
lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> metric 0 mtu 16384
        options=3<RXCSUM,TXCSUM>
        inet6 fe80::1%lo0 prefixlen 64 scopeid 0x6
        inet6 ::1 prefixlen 128
        inet 127.0.0.1 netmask 0xff000000
        nd6 options=3<PERFORMNUD,ACCEPT_RTADV>
vlan2: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
        options=3<RXCSUM,TXCSUM>
        ether 00:19:d1:24:76:c4
        inet 192.168.1.141 netmask 0xfffffff0 broadcast 192.168.1.143
        media: Ethernet autoselect (1000baseT <full-duplex>)
        status: active
        vlan: 2 parent interface: em0
vlan3: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
        options=3<RXCSUM,TXCSUM>
        ether 00:19:d1:24:76:c4
        inet 10.1.0.14 netmask 0xfffff000 broadcast 10.1.15.255
        media: Ethernet autoselect (1000baseT <full-duplex>)
        status: active
        vlan: 3 parent interface: em0
ipfw0: flags=8801<UP,SIMPLEX,MULTICAST> metric 0 mtu 65536

На этой машине запущена quagga, в ней сконфигурен и работает OSPF.
OSPF общается с соседями по vlan2, vlan3 в OSPF'е не участвует:

router ospf
 ospf router-id 192.168.1.141
 redistribute connected metric 25 metric-type 1 route-map CONN
 redistribute static metric 30 metric-type 1 route-map NDR
 passive-interface default
 no passive-interface vlan2
 network 192.168.1.128/28 area 0.0.0.0

При этом OSPF, как ему и положено, шлет свои hello и некоторые прочие
пакеты по multicast-адресам 224.0.0.5 и 224.0.0.6 (вот и обещанный
multicast-трафик). Выглядит это так:

#tcpdump -n -i vlan2 proto ospf and src host 192.168.1.141
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on vlan2, link-type EN10MB (Ethernet), capture size 96 bytes
09:58:51.151532 IP 192.168.1.141 > 224.0.0.5: OSPFv2, Hello, length 76
09:58:57.345173 IP 192.168.1.141 > 224.0.0.6: OSPFv2, LS-Ack, length 44
09:59:01.152235 IP 192.168.1.141 > 224.0.0.5: OSPFv2, Hello, length 76
09:59:11.152865 IP 192.168.1.141 > 224.0.0.5: OSPFv2, Hello, length 76
09:59:21.153555 IP 192.168.1.141 > 224.0.0.5: OSPFv2, Hello, length 76
09:59:28.286692 IP 192.168.1.141 > 224.0.0.6: OSPFv2, LS-Ack, length 64
09:59:31.154143 IP 192.168.1.141 > 224.0.0.5: OSPFv2, Hello, length 76
^C
7 packets captured
78 packets received by filter
0 packets dropped by kernel

Таблица маршрутизации до запуска квагги выглядит совсем просто (IPv6 часть опущена, поскольку к делу не относится):
#netstat -rn
Routing tables

Internet:
Destination        Gateway            Flags    Refs      Use  Netif Expire
default            10.1.0.1           UGS         6       45  vlan3
10.1.0.0/20        link#8             U           1       49  vlan3
10.1.0.14          link#8             UHS         0        0    lo0
127.0.0.1          link#6             UH          0        2    lo0
192.168.1.128/28   link#7             U           2       54  vlan2
192.168.1.141      link#7             UHS         0        0    lo0

После запуска квагии по OSPF'у приезжает еще ряд маршрутов, которые я тут не показываю, ибо конкретное их содержание не важно, важен сам факт их наличия. Важно тут то, что default (который просто прописан ручками в /etc/rc.conf) при работе квагги сохраняется тем же и продолжает смотреть в интерфейс vlan3 на 10.1.0.1. (В принципе, по OSPF'у приезжает другой default, но квагга, оставлет в kernel forwarding table тот, что там был до ее запуска, как более приоритетный. Такая конфигурация может кому-то показаться странной, но это тестовая машина, тут "на сейчас" так задумано. Впрочем, это всё детали.)

ipfw сконфигурен предельно просто -- как "open" из стандартного 
/etc/rc.firewall:
#ipfw list
00100 allow ip from any to any via lo0
00200 deny ip from any to 127.0.0.0/8
00300 deny ip from 127.0.0.0/8 to any
00400 deny ip from any to ::1
00500 deny ip from ::1 to any
00600 allow ipv6-icmp from :: to ff02::/16
00700 allow ipv6-icmp from fe80::/10 to fe80::/10
00800 allow ipv6-icmp from fe80::/10 to ff02::/16
00900 allow ipv6-icmp from any to any ip6 icmp6types 1
01000 allow ipv6-icmp from any to any ip6 icmp6types 2,135,136
65000 allow ip from any to any
65535 deny ip from any to any

#sysctl net.inet.ip.fw.one_pass
net.inet.ip.fw.one_pass: 0

Теперь добавляем в картину netgraph:

#ngctl connect ipfw: ipfw: 1 2
#ngctl l -l
There are 2 total nodes:
  Name: ipfw            Type: ipfw            ID: 00000001   Num hooks: 2
  Local hook      Peer name       Peer type    Peer ID         Peer hook
  ----------      ---------       ---------    -------         ---------
  2               ipfw            ipfw         00000001        1
  1               ipfw            ipfw         00000001        2
  Name: ngctl1894       Type: socket          ID: 00000006   Num hooks: 0

(это просто node ng_ipfw, с двумя hook'ами соединенными между собой. На
практике так использовать ng_ipfw, конечно, смысла не имеет, ибо
отправленный а него пакет должен просто тут же вернуться обратно, но для
демонстрации проблемы этого оказывается достаточно)

...и добавляем элементарное правило:
ipfw add 10 netgraph 1 proto ip

В результате добавления этого правила пакеты из него попадают в узел
ng_ipfw и тут же без изменения возвращаются обратно, продолжая путешествовать по правилам ipfw (one_pass у нас отключен, см. выше). Т.е. повлиять это правило, по идее, вообще ни на что не должно. Тот факт, что пакеты в ng_ipfw попадают и нормально возвращаются обратно виден, например, по синхронному изменению счетчиков на правилах 10 и 65000:

#ipfw -t show 10 65000
00010  334  41895 Tue Jun 14 10:47:54 2011 netgraph 1 ip from any to any
65000 8358 855211 Tue Jun 14 10:47:54 2011 allow ip from any to any
#ipfw -t show 10 65000
00010  345  42991 Tue Jun 14 10:47:56 2011 netgraph 1 ip from any to any
65000 8369 856307 Tue Jun 14 10:47:56 2011 allow ip from any to any
#ipfw -t show 10 65000
00010  358  44243 Tue Jun 14 10:47:58 2011 netgraph 1 ip from any to any
65000 8382 857559 Tue Jun 14 10:47:58 2011 allow ip from any to any

Тем не менее, после добавления этого правила multicast OSPF-пакеты, которые продолжает слать (или пытаться слать) местный OSPF из vlan2 начисто пропадают (соответственно, машина выпадает из OSPF, и теряет все полученные ранее динамические маршруты, а таблица маршрутизации возвращается к девственно чистому виду, который я приводил выше).

Но что еще более интересно -- эти самые пропавшие multicast-пакеты появляются... (surprise! surprise!)... во vlan3! Который к OSPF'у вообще никакого отношения не имеет! Вот как это выглядит:

#tcpdump -n -i vlan3 proto ospf and src host 192.168.1.141
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on vlan3, link-type EN10MB (Ethernet), capture size 96 bytes
10:30:00.536356 IP 192.168.1.141 > 224.0.0.5: OSPFv2, Hello, length 76
10:30:04.221026 IP 192.168.1.141 > 224.0.0.6: OSPFv2, LS-Ack, length 44
10:30:10.543039 IP 192.168.1.141 > 224.0.0.5: OSPFv2, Hello, length 76
10:30:11.399434 IP 192.168.1.141 > 224.0.0.6: OSPFv2, LS-Ack, length 44
10:30:11.409279 IP 192.168.1.141 > 224.0.0.6: OSPFv2, LS-Update, length 64
10:30:12.430221 IP 192.168.1.141 > 224.0.0.6: OSPFv2, LS-Ack, length 44
10:30:20.553881 IP 192.168.1.141 > 224.0.0.5: OSPFv2, Hello, length 76
10:30:30.583664 IP 192.168.1.141 > 224.0.0.5: OSPFv2, Hello, length 76
10:30:40.599868 IP 192.168.1.141 > 224.0.0.5: OSPFv2, Hello, length 76
^C
9 packets captured
60 packets received by filter
0 packets dropped by kernel

Такой вот весьма неожиданный эффект. Дальнейшее разбирательство показало:

1) если добавлять в ipfw два отдельных правила:
ipfw add 10 netgraph 1 proto ip in
ipfw add 20 netgraph 1 proto ip out
то к указанному эффекту приводит именно out-правило;

2) сборка ядра с отключением FLOWTABLE и добавлением ipfw и netgraph'а непосредственно в ядро на данный эффект не влияет -- на таком самосборном ядре наблюдается все то же самое;

3) Изучение исходников ng_ipfw выявило, что когда к нему на какой-то hook приходит out-пакет (не из ipfw через "ipfw netgraph", а из недр netgraph'а), он его просто отправляет в ip_output(m, NULL, NULL, IP_FORWARDING, NULL, NULL). При этом похоже, что ip_output просто тупо маршрутизирует данный пакет (у которого, напомню, dst IP -- это multicast 224.0.0.X), как если бы dst IP был обычным unicast-адресом. А поскольку в kernel forwarding table для сети 224.0.0.0/8 никакого явного маршрута нет, то вот оно и отправляется по default'у во vlan3. Хотя, это только мой wild guess на основании внешних проявлений. До углубленного штудирования исходников ip_output() я еще не добрался.

В любом случае, поведение неправильное. Что-то где-то надо менять. Интересуют ваши комментарии по этому поводу. Может кто-то где-то уже с подобным сталкивался? Или хорошо разбирается в нюансах внутреннего устройства TCP/IP-стека FreeBSD, и ему очевидно, почему именно оно так себя ведет и как это можно легко и непринужденно исправить?

В качестве workaround'а, по идее, можно было бы совсем отказаться от ng_ipfw и просто вешать ng_netflow (ради которого вся эта возня и затевалась изначально) прямо на все нужные интерфейсы, но тут засада в том, что на той (боевой) машине, для которой это все предназначено, работет ipfw nat, и хотелось бы, чтобы трафик при выходе в мир попадал в ng_netflow до за'NAT'ивания, а при входе из мира -- после раз'NAT'ивания. Потому, цеплять ng_netflow непосредственно на внешний интерфейс не подходит.

--
Olwi

Ответить