Pull to refresh

FreeBSD Netgraph на примере Ethernet тоннеля

Reading time 7 min
Views 17K
Всем привет.

Думаю многим системным администраторам, работающим с FreeBSD, известно о существовании ядерной подсистемы Netgraph. Но не многие знают/понимают как это работает, и что из этого можно построить.

Расскажу что это такое, а также разберу на простом примере сборку Ethernet моста через интернет.



Немного теории.


Википедия рассказывает http://ru.wikipedia.org/wiki/Netgraph

Netgraph — модульная сетевая подсистема ядра FreeBSD, основанная на принципе графов. В Netgraph строится граф из узлов различных типов, узел каждого типа имеет некоторое количество входов/выходов (далее хуков (hooks)). Узел netgraph позволяет производить определенные действия над пакетом, проходящим через него. Некоторые Netgraph узлы предоставляют поддержку различных протоколов, инкапсуляций, таких как L2TP, PPTP, PPPoE, PPP, ATM, bluetooth, другие служат для связки модулей и сортировки/маршрутизации между узлами netgraph, например bpf, split.

Подсистема Netgraph представляет собой набор модулей, каждый со своей специфичной задачей. Это такая горстка кубиков лего, которые всегда можно соединить.

Ярким примером использования множества netgraph модулей является массово используемый мелкими Российскими провайдерами демон mpd, для терминации клиентских туннелей, таких как PPPoE, PPTP, PPP и т.п. В версиях 3 ветки mpd многие операции делал сам, а к 5 текущей версии в основном занимается коммутацией модулей netgraph.

Посмотрим на наши кубики.


[root@bsd1] /boot/kernel/> ls | grep ng_
ng_UI.ko
ng_async.ko
ng_atm.ko
ng_atmllc.ko
ng_bluetooth.ko
ng_bpf.ko
ng_bridge.ko
ng_bt3c.ko
ng_btsocket.ko
ng_car.ko
ng_ccatm.ko
ng_cisco.ko
ng_deflate.ko
ng_device.ko
ng_echo.ko
ng_eiface.ko
ng_etf.ko
ng_ether.ko
ng_fec.ko
ng_frame_relay.ko
ng_gif.ko
ng_gif_demux.ko
ng_h4.ko
ng_hci.ko
ng_hole.ko
ng_hub.ko
ng_iface.ko
ng_ip_input.ko
ng_ipfw.ko
ng_ksocket.ko
ng_l2cap.ko
ng_l2tp.ko
ng_lmi.ko
ng_mppc.ko
ng_nat.ko
ng_netflow.ko
ng_one2many.ko
ng_ppp.ko
ng_pppoe.ko
ng_pptpgre.ko
ng_pred1.ko
ng_rfc1490.ko
ng_socket.ko
ng_source.ko
ng_split.ko
ng_sppp.ko
ng_sscfu.ko
ng_sscop.ko
ng_sync_ar.ko
ng_sync_sr.ko
ng_tag.ko
ng_tcpmss.ko
ng_tee.ko
ng_tty.ko
ng_ubt.ko
ng_uni.ko
ng_vjc.ko
ng_vlan.ko
[root@bsd1] /boot/kernel/> ls | grep ng_ | wc -l
58
[root@bsd1] /boot/kernel/>

На данный момент 58 модулей +1 основной модуль netgraph.ko (FreeBSD 7.3). Все они могут быть собраны в ядро статически, или загружены динамически. Т.к модули ядерные, работают они тоже прямо в ядре, за счет чего можно получить поразительные по производительности результаты.

Для каждого модуля существует man, в котором описываются возможности модуля, входы/выходы (hook, крючки, зацепки, далее буду называть их «хуками»), принимаемые контрольные сообщения и их действия.
Для управления подсистемой netgraph существует утилита ngctl воспринимающая разные команды, такие как list,mkpeer,connect,name и др., про них отдельно можно прочитать в man ngctl.

Задача.


Поставим простую задачу: необходимо соединить две физических сети Ethernet через интернет.
Имеем 2 сервера.

BSD1
2 сетевых интерфейса
Интерфейс 1 em0 — внешний IP 1.1.1.1
Интерфейс 2 em1 — внутренний IP 192.168.1.1

BSD2
2 сетевых интерфейса
Интерфейс 1 em0 — внешний IP 2.2.2.2
Интерфейс 2 em1 — внутренний IP 192.168.1.2

Между серверами имеется маршрутизируемая интернет связь через внешние интерфейсы.
Соединить нужно сети на интерфейсах em1 обоих серверов.

Посмотрим на наши кубики.


Нам потребуются:

netgraph.ko — основа.

ng_ether.gif ng_ether.ko — модуль подключения к физическим сетевым интерфейсам. При загрузке этого модуля в пространстве графов автоматически создаются по одному узлу на каждый физический интерфейс, имеющих имя этого интерфейса. В нашем случае это будут «em0:», «em1:». Каждый узел ng_ether имеет 3 хука: lower, upper, orphans. Хук lower — это прямой выход в место драйвера сетевого интерфейса, откуда пакеты отправляются или принимаются устройством. Хук upper это прямой выход в место ядра системы, откуда пакеты должны отправляться или приниматься драйвером сетевого устройства. Хук orphans, это тот же lower, только в него попадают ошибочные, испорченные пакеты с сетевого устройства. Его во внимание не берем.

Если сказать простым языком: при подключении ng_ether в обмене между ядром и драйвером сетевой карты появляется разрыв. Со стороны ядра этот разрыв называется upper, со стороны драйвера сетевого устройства lower. Этот разрыв соединен, до момента когда к lower или upper не подключится что-нибудь из подсистемы netgraph.


ng_bridge.gif ng_bridge.ko — модуль самого настоящего Ethernet свитча. Все, думаю, могут представить (а кто-то и сейчас видит), коробочку с Ethernet портами, моргающую лампочками, стоящую на столе и объединяющую компьютеры в сеть. ng_bridge — та самая коробочка в подсистеме netgraph. ng_bridge — простая реализация Ethernet свитча, снабженная arp таблицей и простым алгоритмом определения петель. Хуки ng_bridge именуются link0, link1 и т.д. К ним мы и будем подключать хуки наших ng_ether. Модуль принимает контрольные сообщения, такие как setconfig, getconfig, reset, getstats, clrstats, getclrstats, gettable. Действия сообщений интуитивно понятны, подробности в man ng_bridge.


ng_ksocket.gif ng_ksocket.ko — модуль, позволяющий открыть на прослушку сокет прямо из ядра (любой сокет, поддерживаемый системной функцией socket()), и подключиться к другому сокету. Принимает единственное подключение к хуку "<family>/<type>/<proto>". В man socket рассказано какие бывают family, type, proto. В нашем случае это будет «inet/dgram/udp». inet — ipv4, dgram — дейтаграммы, proto — udp. Так же модуль принимает несколько контрольных сообщений, такие как bind, listen, connect, accept, getname, getpeername, setopt, getopt. Подробнее можно прочитать в man ng_ksocket. Используя сообщение «connect» мы можем подключить наш входящий хук «inet/dgram/udp» к удаленному, который был создан аналогично, но командой «bind».

Составляем граф.


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

ethernet_over_udp_scheme.gif


1. Подключаем оба хука модуля ng_ether интерфейса em1 к ng_bridge, чтобы система видела и физическую сеть, подключенную к интерфейсу, и нашу виртуальную. Делается одинаково на обоих серверах.
2. Следующий свободный линк ng_bridge подключаем к ng_ksocket с параметрами inet/dgram/udp. (Почему я выбрал UDP? Потому что никто не гарантирует успешную доставку сигнала в кабеле или радио сети, так же как аналогично в IP протоколе никто не гарантирует доставку UDP.) Делается одинаково на обоих серверах.
3. Командуем модулю ng_ksocket занять определенный порт, на определенном IP адресе, а так же подключиться к удаленному IP по нужному порту. Делаем это на обоих серверах, в том отличии, что меняются IP адреса и порты на противоположные друг другу.


Собираем граф в системе.


Выше я уже упомянул об утилите ngctl. С её помощью мы создадим наши модули в системе и свяжем их.
Нам потребуются команды mkpeer, connect, name, msg. Опишу их простым языком.

Команда mkpeer.
Синтаксис: ngctl mkpeer модуль1 тип_модуля2 хук1 хук2
Создает «модуль2» с указанным типом и подключает его хук «хук2» к «хук1» модуля «модуль1»

Команда connect.
Синтаксис: ngctl connect модуль1 модуль2 хук1 хук2
Подключает «хук1» модуля «модуль1» к «хук2» модуля «модуль2».

Команда name.
Синтаксис: ngctl name модуль: хук имя
Присваивает имя созданному через mkpeer модулю.

Команда msg.
Синтаксис: ngctl msg модуль: сообщение параметры
Передает контрольное «сообщение» в «модуль» с «параметрами».

Ну а теперь как будет выглядеть наш граф в живую для сервера «bsd1». Для тоннеля будем использовать udp порт 7777

Создаем узел ng_bridge и подключаем к его хуку «link0» хук сетевого интерфейса «em1» «lower».
ngctl mkpeer em1: bridge lower link0
Называем только что созданный узел именем «switch», его можно найти по пути «em1:lower».
ngctl name em1:lower switch
Подключаем к «link1» нашего «switch» upper сетевого интерфейса «em1».
ngctl connect switch: em1: link1 upper
Создаем узел ng_ksocket и подключаем к его хуку «inet/dgram/udp» «link2» нашего «switch»
ngctl mkpeer switch: ksocket link2 inet/dgram/udp
Называем только что созданный ksocket «switch_socket», его можно найти по пути «switch:link2»
ngctl name switch:link2 switch_socket
Отправляем команду «bind» нашему «switch_socket», с параметрами. ksocket займет порт 7777 на IP 1.1.1.1.
ngctl msg switch_socket: bind inet/1.1.1.1:7777
Отправляем команду «connect» нашему «switch_socket», с параметрами. ksocket подключится к порту 7777 по IP адресу 2.2.2.2.
ngctl msg switch_socket: connect inet/2.2.2.2:7777
Отправляем команду модулю ng_ether сетевого интерфейса em1 перейти в режим прослушки пакетов, адресованных не ему. Нам ведь теперь необходимо принимать пакеты для устройств находящихся в нашей виртуальной сети.
ngctl msg em1: setpromisc 1
ngctl msg em1: setautosrc 0

Для сервера «bsd2» нам лишь нужно поменять параметры команд bind и connect местами.

Для простоты использования всё это я оформил в sh скрипт.
В скрипте используется ещё одна команда ngctl shutdown. Эта команда посылает специальное контрольное сообщение модулю, указанному в параметре. Это сообщение принимает каждый модуль, подробнее в «man модуль». Обычно эта команда вызывает уничтожение модуля и разрыв всех его связей.

#!/bin/sh
self=1.1.1.1
peer=2.2.2.2
port=7777
if=em1

case "$1" in
        start)
            echo "Starting netgraph switch."
            ngctl mkpeer ${if}: bridge lower link0
            ngctl name ${if}:lower switch
            ngctl connect switch: ${if}: link1 upper
            ngctl mkpeer switch: ksocket link2 inet/dgram/udp
            ngctl name switch:link2 switch_socket
            ngctl msg switch_socket: bind inet/${self}:${port}
            ngctl msg switch_socket: connect inet/${peer}:${port}
            ngctl msg ${if}: setpromisc 1
            ngctl msg ${if}: setautosrc 0
            echo "Ok."
            exit 0
            ;;
        stop)
            echo "Stopping netgraph switch."
            ngctl shutdown switch_socket:
            ngctl shutdown switch:
            ngctl shutdown ${if}:
            echo "Ok."
            exit 0
            ;;
        restart)
            sh $0 stop
            sh $0 start
            ;;
        *)
            echo "Usage: `basename $0` { start | stop | restart }"
            exit 64
            ;;
esac

Посмотрим что получилось
[root@bsd1] /usr/local/etc/rc.d/> ngctl list
There are 5 total nodes:
Name: em0 Type: ether ID: 00000001 Num hooks: 0
Name: em1 Type: ether ID: 00000002 Num hooks: 2
Name: switch Type: bridge ID: 000000f6 Num hooks: 3
Name: ngctl16408 Type: socket ID: 00000100 Num hooks: 0
Name: switch_socket Type: ksocket ID: 000000fa Num hooks: 1


Модуль «ngctl16408» с типом socket используется ngctl для управления, не обращайте на него внимания.

Пакетики бегут:
[root@bsd1] /root/> ping 192.168.1.2
PING 192.168.1.2 (192.168.1.2): 56 data bytes
64 bytes from 192.168.1.2: icmp_seq=0 ttl=64 time=3.760 ms
64 bytes from 192.168.1.2: icmp_seq=1 ttl=64 time=3.527 ms
64 bytes from 192.168.1.2: icmp_seq=2 ttl=64 time=3.479 ms
64 bytes from 192.168.1.2: icmp_seq=3 ttl=64 time=4.052 ms

[root@bsd1] /root/> ngctl msg switch: getstats 2
Rec'd response "getstats" (4) from "[f6]:":
Args:   { recvOctets=49333 recvPackets=532 recvMulticast=467 recvBroadcast=63 xmitOctets=580 xmitPackets=12 xmitMulticasts=10 xmitBroadcasts=1 }


Конец


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

В следующей статье опишу ещё несколько модулей netgraph — считалку трафика ng_netflow, нат ng_nat, добавим к получившейся сегодня схеме шейпинг через ng_car, а может быть придумаю еще чего-нибудь интересного.

Спасибо за внимание.
Tags:
Hubs:
+64
Comments 20
Comments Comments 20

Articles