Шейпінг пакетного IP-трафіку за допомогою tc+cls_bpf

Ахтунг: у цій замі­тці є над­се­кре­тне зна­н­ня, яким воло­діє дуже неве­ли­ка кіль­кість людей. Якщо ви усві­дом­лю­є­те, що від робо­ти з tc у вас може погір­ши­ти­ся само­по­чу­тя, може поча­ти випа­да­ти волос­ся в одних місцях і рости в інших, може піти дру­жи­на й забра­ти дітей, ви може­те захво­рі­ти на алко­го­лізм тощо, але вас це не лякає — читай­те далі.

Зазви­чай, для шей­пін­гу тра­фі­ку на мар­шру­ти­за­то­рах чи про­грам­них шей­пе­рах вико­ри­сто­ву­ють зв’язку tc+iptables. Тут iptables мар­кує паке­ти, а tc за допо­мо­гою кла­си­фі­ка­то­ра fwmark їх роз­ки­дає по ієрар­хії шей­пе­ра (пере­ва­жно, вико­ри­сто­ву­є­ться HTB). Однак цей під­хід має два сут­тє­вих заува­же­н­ня. Пер­ше поля­гає в тому, що дуже часто вико­ри­сто­ву­ва­ти такий поту­жний інстру­мент як iptables для мар­ку­ва­н­ня тра­фі­ку надли­шко­во. Дру­ге заува­же­н­ня сто­су­є­ться того, що ця зв’язка дуже повіль­но пра­цює. Я пам’ятаю, як на дру­го­му пні кіль­ка мега­біт могли вижер­ти софто­ви­ми пере­ри­ва­н­ня­ми весь проц. Незва­жа­ю­чи на це, над­зви­чай­но бага­то ману­а­лів у цих ваших інтер­не­тах про­по­ну­ють шей­пи­ти тра­фік саме так.

Дру­гий під­хід поля­гає в засто­су­ван­ні кла­си­фі­ка­то­ра u32. Він зна­чно більш обме­же­ний у сво­їх фун­кці­ях; навіть для того, щоб захо­пи­ти філь­тром пев­ний діа­па­зон пор­тів, дово­ди­ться або збо­чу­ва­ти­ся з маска­ми, або засто­со­ву­ва­ти інший кла­си­фі­ка­тор, ematch, який, до того ж, має про­ти­по­ка­за­н­ня, якщо його вико­ри­сто­ву­ва­ти для шей­пін­гу тра­фі­ку в кіль­ка напря­мів одно­ча­сно. Мені навіть вда­ло­ся успі­шно зашей­пи­ти за допо­мо­гою u32 модний і мімі­мі­шний IPv6, але скри­пти на 30 КіБ після цьо­го впер­то від­мов­ля­ли­ся вмі­ща­ти­ся в голо­ві пов­ні­стю. Однак, u32 наба­га­то швид­ший за зв’язку із засто­су­ва­н­ням iptables, і дуже бага­то ману­а­лів також про­по­ну­ють його вико­ри­сто­ву­ва­ти. Я сам його три­мав і в себе у про­да­кшні, і кіль­ка шей­пе­рів на попе­ре­дній робо­ті так і побу­до­ва­ні, і, підо­зрюю, успі­шно пра­цю­ють і досі, хоча я маю вели­кі сум­ні­ви, що хтось після мене в них розі­брав­ся.

Зро­зумі­ло, є ще й тре­тій шлях, і якраз ним ми й піде­мо.

Кла­си­фі­ка­тор cls_bpf дода­ли в ядро 3.13 у жов­тні 2013-го року. Сла­ва яйцям, це ядро потра­пи­ло в 14.04 LTS, а от у сімьо­ро­чкі (я про CentOS, зві­сно, а не про ту сра­мо­ту, про яку ви могли поду­ма­ти) цьо­го щастя нема. Ну та осо­би­сто в мене, зві­сно, май­же зав­жди найостан­ні­ше ядро, тому пофі­гу. Окрім під­трим­ки зі сто­ро­ни ядра потрі­бна ще й під­трим­ка в iproute2. Тому на це тре­ба зва­жа­ти, якщо хтось таки захо­че заве­сти цю кухню на цен­то­сі.

Так у чому ж при­кол цієї кухні?

BPF — це така потен­цій­но мега­у­ні­вер­саль­на шту­ка, яка заду­му­ва­ла­ся зов­сім не для того, для чого її пиля­ють зараз (гугліть про eBPF). На наше щастя, саме ця шту­ка дає змо­гу ском­пі­ли­ти потрі­бний люди­но­чи­та­бель­ний кла­си­фі­ка­тор тра­фі­ку із син­та­кси­сом libpcap у байт-код, який зро­зу­міє ядро, при­чо­му, ском­пі­ли­ти «на льо­ту». Якщо вас лякає libpcap, то я ска­жу інше сло­во: tcpdump. Саме ним і ком­пі­ли­ти­ме­мо.

Нага­даю, що зви­чай­не пра­ви­ло філь­тра­ції tc із засто­су­ва­н­ням кла­си­фі­ка­то­ра u32 має при­бли­зно такий нудний вигляд:

tc filter add dev eth0 protocol ip parent 1: u32 match ip dst 1.2.3.4 match ip dport 80 0xffff flowid 1:10

Ніфі­га непо­ня­тно, якщо, зві­сно, вас від tc уже дав­но не нудить. Насправ­ді, у кла­си­фі­ка­то­рі u32 нема нічо­го стра­шно­го, окрім, зві­сно, того, що його син­та­ксис не гну­чкий, і якщо тре­ба ж таки заби­ти діа­па­зон пор­тів, вила­зе ота­ке стра­хі­т­тя:

tc filter add dev eth0 protocol ip parent 1: u32 match ip dst 1.2.3.4 match ip dport 62464 0xfc00 flowid 1:10

Прав­да ж, не дуже оче­ви­дно, що я хотів виокре­ми­ти пор­ти від 62464 до 63487?

А що нато­мість про­по­нує cls_bpf? А щось типу тако­го:

tc filter add dev eth0 parent 1: bpf run bytecode "тут-іде-байткод" flowid 1:10

Уже лег­ше, прав­да? Тіль­ки звід­ки бра­ти бай­ткод? Зга­ду­є­мо, що його можна зге­не­ру­ва­ти із пра­вил tcpdump'а, і роби­ться це отак:

tcpdump -i lo -ddd "ip and dst host 1.2.3.4 and portrange 62464-63487 and udp" | tr '\n' ','

Ця коман­да видасть послі­дов­ність чисел, які тре­ба про­сто під­ста­ви­ти в коман­ду вище. Звер­ніть ува­гу на вираз для філь­тра. Прав­да, так кра­ще? Мере­же­вий інтер­фейс може­те вка­зу­ва­ти довіль­ний, це ніщо не змі­нить; я під­ста­вив луп­бек, бо він (май­же) гаран­то­ва­но є в систе­мі.

У скри­птах я б про­по­ну­вав роби­ти так:

function bpf_compile
{
        echo $(tcpdump -i lo -ddd $@ | tr '\n' ',')
}
...
tc filter add dev eth0 parent 1: bpf run bytecode "$(bpf_compile ip and dst host 1.2.3.4 and portrange 62464-63487 and udp)" flowid 1:10

Зда­є­ться, так уза­га­лі супер. Син­та­ксис для вира­зів філь­трів можна зна­йти в man 7 pcap-filter.

Якщо гово­ри­ти про вимі­рю­ва­н­ня швид­ко­дії, то можу вида­ти таку ста­ти­сти­ку. На моє­му дома­шньо­му мар­шру­ти­за­то­рі з тра­фі­ком 30–40 мега­біт на секун­ду кла­си­фі­ка­тор u32 віджи­рав при­бли­зно 6–7% CPU від­по­від­но до даних perf top ‑U (у мене зага­лом 72 філь­тра на вхід і вихід, я прі­о­ри­те­зую тра­фік на різні хости і пор­ти). Після того, як я замі­нив його на cls_bpf, заван­та­же­ність CPU новим кла­си­фі­ка­то­ром скла­ла ~2%. От вам і еко­но­мія.

One last thing. Не забудь­те вклю­чи­ти JIT через sysctl. Так воно пра­цює наба­га­то швид­ше:

net.core.bpf_jit_enable = 1

UPDATE: пре­зен­та­ція по темі.

UPDATE 2: iproute2 в 14.04 ці ніштя­ки не під­три­мує :(. Став­те 14.10.

UPDATE 3: я був би не я, якби не напи­сав свій ком­пі­ля­тор BPF-опко­дів, який пра­цює наба­га­то швид­ше за tcpdump.

UPDATE 4: вау, у сто­ко­во­му ядрі 14.10 нема BPF_JIT. Ком­піль­те рука­ми.

UPDATE 5: зда­є­ться, JIT є тіль­ки на 64-бітних ядрах. Ну ОК.

Мітки: , , , , ,

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *

*

Цей сайт використовує Akismet для зменшення спаму. Дізнайтеся, як обробляються ваші дані коментарів.