БПФ за малишане, нулти део: класични БПФ

Беркелеи Пацкет Филтерс (БПФ) је технологија Линук кернела која је већ неколико година на насловним страницама техничких публикација на енглеском језику. Конференције су испуњене извештајима о употреби и развоју БПФ-а. Дејвид Милер, одржавалац Линук мрежног подсистема, одржава свој говор на Линук Плумберс 2018 „Овај разговор није о КСДП-у“ (КСДП је један случај употребе за БПФ). Брендан Грегг води разговоре Линук БПФ Суперповерс. Токе Хøиланд-Јøргенсен смеје седа је језгро сада микројезгро. Томас Граф промовише идеју да БПФ је јавасцрипт за кернел.

Још увек нема систематског описа БПФ-а на Хабре-у, па ћу у низу чланака покушати да говорим о историји технологије, опишем архитектуру и развојне алате, и скицирам области примене и праксе коришћења БПФ-а. Овај чланак, нула, у серији, говори о историји и архитектури класичног БПФ-а, а такође открива тајне његових принципа рада. tcpdump, seccomp, strace, и још много тога.

Развој БПФ-а контролише Линук мрежна заједница, главне постојеће апликације БПФ-а су повезане са мрежама и стога, уз дозволу @еуцариот, серију сам назвао „БПФ за мале“, у част сјајне серије „Мреже за најмлађе“.

Кратак курс историје БПФ-а (c)

Модерна БПФ технологија је побољшана и проширена верзија старе технологије са истим именом, која се сада назива класични БПФ да би се избегла забуна. На основу класичног БПФ-а креиран је добро познати услужни програм tcpdump, механизам seccomp, као и мање познати модули xt_bpf за iptables и класификатор cls_bpf. У савременом Линуку, класични БПФ програми се аутоматски преводе у нови облик, међутим, са становишта корисника, АПИ је остао на месту и нове употребе за класични БПФ, као што ћемо видети у овом чланку, још увек се проналазе. Из тог разлога, а такође и зато што ће, пратећи историју развоја класичног БПФ-а у Линук-у, бити јасније како и зашто је еволуирао у свој савремени облик, одлучио сам да почнем са чланком о класичном БПФ-у.

Крајем осамдесетих година прошлог века, инжењери из чувене Лабораторије Лоренс Беркли заинтересовали су се за питање како правилно филтрирати мрежне пакете на хардверу који је био модеран крајем осамдесетих година прошлог века. Основна идеја филтрирања, првобитно имплементирана у технологији ЦСПФ (ЦМУ/Станфорд Пацкет Филтер), била је да се непотребни пакети филтрирају што је раније могуће, тј. у простору кернела, јер се тиме избегава копирање непотребних података у кориснички простор. Да би се обезбедила сигурност током извршавања за покретање корисничког кода у простору кернела, коришћена је виртуелна машина у заштићеном окружењу.

Међутим, виртуелне машине за постојеће филтере су дизајниране да раде на машинама заснованим на стеку и нису радиле тако ефикасно на новијим РИСЦ машинама. Као резултат тога, трудом инжењера из Беркелеи Лабс-а, развијена је нова технологија БПФ (Беркелеи Пацкет Филтерс), чија је архитектура виртуелне машине дизајнирана на основу Моторола 6502 процесора - радног коња познатих производа као што су Аппле ИИ или НСЗ. Нова виртуелна машина је повећала перформансе филтера десетине пута у поређењу са постојећим решењима.

БПФ машинска архитектура

Са архитектуром ћемо се упознати на радни начин, анализирајући примере. Међутим, за почетак, рецимо да је машина имала два 32-битна регистра доступна кориснику, акумулатор A и индексни регистар X, 64 бајта меморије (16 речи), доступне за писање и накнадно читање, и мали систем команди за рад са овим објектима. Инструкције за прескакање за имплементацију условних израза такође су биле доступне у програмима, али да би се гарантовао благовремени завршетак програма, скокови су могли да се врше само унапред, односно забрањено је прављење петљи.

Општа шема за покретање машине је следећа. Корисник креира програм за БПФ архитектуру и, користећи неки механизам кернела (као што је системски позив), учитава и повезује програм са некима генератору догађаја у кернелу (на пример, догађај је долазак следећег пакета на мрежну картицу). Када дође до догађаја, кернел покреће програм (на пример, у тумачу), а меморија машине одговара некима регион меморије кернела (на пример, подаци о долазном пакету).

Горе наведено ће нам бити довољно да почнемо да гледамо примере: упознаћемо се са системом и форматом команде по потреби. Ако желите одмах да проучите командни систем виртуелне машине и научите о свим њеним могућностима, онда можете прочитати оригинални чланак БСД филтер пакета и/или прву половину датотеке Доцументатион/нетворкинг/филтер.ткт из документације кернела. Поред тога, можете проучити презентацију libpcap: Архитектура и методологија оптимизације за хватање пакета, у којој Мекен, један од аутора БПФ-а, говори о историји стварања libpcap.

Сада прелазимо на разматрање свих значајних примера коришћења класичног БПФ-а на Линук-у: tcpdump (libpcap), сеццомп, xt_bpf, cls_bpf.

тцпдумп

Развој БПФ-а одвијао се паралелно са развојем фронтенда за филтрирање пакета - добро познатог услужног програма tcpdump. А пошто је ово најстарији и најпознатији пример коришћења класичног БПФ-а, доступан на многим оперативним системима, са њим ћемо започети наше проучавање технологије.

(Покренуо сам све примере у овом чланку на Линук-у 5.6.0-rc6. Излаз неких команди је уређен ради боље читљивости.)

Пример: посматрање ИПв6 пакета

Замислимо да желимо да погледамо све ИПв6 пакете на интерфејсу eth0. Да бисмо то урадили, можемо покренути програм tcpdump са једноставним филтером ip6:

$ sudo tcpdump -i eth0 ip6

У овом случају, tcpdump компајлира филтер ip6 у бајт код БПФ архитектуре и пошаљите га кернелу (погледајте детаље у одељку Тцпдумп: учитавање). Учитани филтер ће бити покренут за сваки пакет који пролази кроз интерфејс eth0. Ако филтер враћа вредност различиту од нуле n, затим до n бајтови пакета ће бити копирани у кориснички простор и видећемо га у излазу tcpdump.

БПФ за малишане, нулти део: класични БПФ

Испоставило се да лако можемо сазнати који је бајт код послат кернелу tcpdump уз помоћ tcpdump, ако га покренемо са опцијом -d:

$ sudo tcpdump -i eth0 -d ip6
(000) ldh      [12]
(001) jeq      #0x86dd          jt 2    jf 3
(002) ret      #262144
(003) ret      #0

На линији нула покрећемо команду ldh [12], што значи „учитавање у регистар A пола речи (16 бита) која се налази на адреси 12” и само је питање каквој меморији се обраћамо? Одговор је да на x почиње (x+1)бајта анализираног мрежног пакета. Читамо пакете са Етхернет интерфејса eth0и ово средствада пакет изгледа овако (ради једноставности претпостављамо да у пакету нема ВЛАН ознака):

       6              6          2
|Destination MAC|Source MAC|Ether Type|...|

Дакле, након извршења команде ldh [12] у регистру A биће поље Ether Type — тип пакета који се преноси у овом Етхернет оквиру. На линији 1 упоређујемо садржај регистра A (тип пакета) ц 0x86ddи ово и имају Тип који нас занима је ИПв6. У првом реду, поред команде за поређење, налазе се још две колоне - jt 2 и jf 3 — ознаке до којих треба да идете ако је поређење успешно (A == 0x86dd) и неуспешно. Дакле, у успешном случају (ИПв6) идемо на ред 2, а у неуспешном - на ред 3. На линији 3 програм се завршава кодом 0 (не копирајте пакет), на линији 2 програм завршава кодом 262144 (копирај ми пакет од максимално 256 килобајта).

Сложенији пример: посматрамо ТЦП пакете према одредишном порту

Хајде да видимо како изгледа филтер који копира све ТЦП пакете са одредишним портом 666. Размотрићемо ИПв4 случај, пошто је ИПв6 случај једноставнији. Након што проучите овај пример, можете сами истражити ИПв6 филтер као вежбу (ip6 and tcp dst port 666) и филтер за општи случај (tcp dst port 666). Дакле, филтер који нас занима изгледа овако:

$ sudo tcpdump -i eth0 -d ip and tcp dst port 666
(000) ldh      [12]
(001) jeq      #0x800           jt 2    jf 10
(002) ldb      [23]
(003) jeq      #0x6             jt 4    jf 10
(004) ldh      [20]
(005) jset     #0x1fff          jt 10   jf 6
(006) ldxb     4*([14]&0xf)
(007) ldh      [x + 16]
(008) jeq      #0x29a           jt 9    jf 10
(009) ret      #262144
(010) ret      #0

Већ знамо шта раде линије 0 и 1. У реду 2 смо већ проверили да ли је ово ИПв4 пакет (Етхер Типе = 0x800) и учитајте га у регистар A 24. бајт пакета. Наш пакет изгледа као

       14            8      1     1
|ethernet header|ip fields|ttl|protocol|...|

што значи да учитавамо у регистар A поље Протоцол ИП заглавља, што је логично, јер желимо да копирамо само ТЦП пакете. Упоређујемо Протокол са 0x6 (IPPROTO_TCP) на линији 3.

У редове 4 и 5 учитавамо полуречи које се налазе на адреси 20 и користимо команду jset проверите да ли је једно од три постављено заставе - ношење маске издато jset три најзначајнија бита се бришу. Два од три бита нам говоре да ли је пакет део фрагментованог ИП пакета, и ако јесте, да ли је последњи фрагмент. Трећи бит је резервисан и мора бити нула. Не желимо да проверавамо непотпуне или покварене пакете, па проверавамо сва три бита.

Ред 6 је најинтересантнији у овој листи. Израз ldxb 4*([14]&0xf) значи учитавамо у регистар X четири најмање значајна бита петнаестог бајта пакета помножена са 4. Најмања значајна четири бита петнаестог бајта је поље Дужина интернет заглавља ИПв4 заглавље, које чува дужину заглавља у речима, па онда треба да помножите са 4. Занимљиво, израз 4*([14]&0xf) је ознака за посебну шему адресирања која се може користити само у овом облику и само за регистар X, тј. не можемо ни рећи ldb 4*([14]&0xf) или ldxb 5*([14]&0xf) (можемо само да наведемо другачији офсет, нпр. ldxb 4*([16]&0xf)). Јасно је да је ова шема адресирања додата БПФ-у управо ради примања X (индексни регистар) Дужина ИПв4 заглавља.

Дакле, у реду 7 покушавамо да учитамо пола речи на (X+16). Имајући у виду да 14 бајтова заузима Етхернет заглавље, и X садржи дужину ИПв4 заглавља, разумемо да у A ТЦП одредишни порт је учитан:

       14           X           2             2
|ethernet header|ip header|source port|destination port|

Коначно, на линији 8 поредимо одредишни порт са жељеном вредношћу и на редовима 9 или 10 враћамо резултат - да ли да копирамо пакет или не.

Тцпдумп: учитавање

У претходним примерима, нисмо се посебно бавили детаљима о томе како тачно учитавамо БПФ бајт код у језгро за филтрирање пакета. Уопштено говорећи, tcpdump портовано на многе системе и за рад са филтерима tcpdump користи библиотеку libpcap. Укратко, да поставите филтер на интерфејс користећи libpcap, морате да урадите следеће:

  • креирајте дескриптор типа pcap_t из имена интерфејса: pcap_create,
  • активирај интерфејс: pcap_activate,
  • компајлирајте филтер: pcap_compile,
  • повежите филтер: pcap_setfilter.

Да видите како функционише pcap_setfilter имплементиран у Линук-у, ми користимо strace (неки редови су уклоњени):

$ sudo strace -f -e trace=%network tcpdump -p -i eth0 ip
socket(AF_PACKET, SOCK_RAW, 768)        = 3
bind(3, {sa_family=AF_PACKET, sll_protocol=htons(ETH_P_ALL), sll_ifindex=if_nametoindex("eth0"), sll_hatype=ARPHRD_NETROM, sll_pkttype=PACKET_HOST, sll_halen=0}, 20) = 0
setsockopt(3, SOL_SOCKET, SO_ATTACH_FILTER, {len=4, filter=0xb00bb00bb00b}, 16) = 0
...

На прве две линије излаза креирамо сирова утичница да прочитате све Етхернет оквире и повежете их са интерфејсом eth0. Од наш први пример знамо да филтер ip састојаће се од четири БПФ инструкције, а у трећем реду видимо како се користи опција SO_ATTACH_FILTER системски позив setsockopt учитавамо и повезујемо филтер дужине 4. Ово је наш филтер.

Вреди напоменути да се у класичном БПФ-у учитавање и повезивање филтера увек дешава као атомска операција, а у новој верзији БПФ-а учитавање програма и повезивање са генератором догађаја су временски раздвојени.

Хидден Трутх

Мало потпунија верзија излаза изгледа овако:

$ sudo strace -f -e trace=%network tcpdump -p -i eth0 ip
socket(AF_PACKET, SOCK_RAW, 768)        = 3
bind(3, {sa_family=AF_PACKET, sll_protocol=htons(ETH_P_ALL), sll_ifindex=if_nametoindex("eth0"), sll_hatype=ARPHRD_NETROM, sll_pkttype=PACKET_HOST, sll_halen=0}, 20) = 0
setsockopt(3, SOL_SOCKET, SO_ATTACH_FILTER, {len=1, filter=0xbeefbeefbeef}, 16) = 0
recvfrom(3, 0x7ffcad394257, 1, MSG_TRUNC, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
setsockopt(3, SOL_SOCKET, SO_ATTACH_FILTER, {len=4, filter=0xb00bb00bb00b}, 16) = 0
...

Као што је горе поменуто, учитавамо и повезујемо наш филтер са утичницом на линији 5, али шта се дешава на линијама 3 и 4? Испоставило се да ово libpcap брине о нама - тако да излаз нашег филтера не укључује пакете који га не задовољавају, библиотека повезује лажни филтер ret #0 (испусти све пакете), пребацује сокет у неблокирајући режим и покушава да одузме све пакете који би могли да остану од претходних филтера.

Укупно, да бисте филтрирали пакете на Линук-у користећи класични БПФ, потребно је да имате филтер у облику структуре као што је struct sock_fprog и отворену утичницу, након чега се филтер може прикључити на утичницу помоћу системског позива setsockopt.

Занимљиво је да се филтер може причврстити на било коју утичницу, а не само сирову. Ево пример програм који одсеца све осим прва два бајта из свих долазних УДП датаграма. (Додао сам коментаре у код како не бих затрпао чланак.)

Више детаља о употреби setsockopt за повезивање филтера, погледајте утичница (7), већ о писању сопствених филтера попут struct sock_fprog без помоћи tcpdump разговараћемо у одељку Програмирање БПФ-а сопственим рукама.

Класични БПФ и XNUMX. век

БПФ је укључен у Линук 1997. године и дуго је остао радни коњ libpcap без икаквих посебних промена (измена специфичних за Линук, наравно, били, али нису променили глобалну слику). Први озбиљни знаци да ће БПФ еволуирати појавили су се 2011, када је Ерик Думазет предложио закрпа, који језгру додаје компајлер Јуст Ин Тиме - преводилац за претварање БПФ бајткода у изворни x86_64 код.

ЈИТ компајлер је био први у ланцу промена: 2012 појавио се способност писања филтера за сеццомп, користећи БПФ, у јануару 2013. године било додато модул xt_bpf, што вам омогућава да пишете правила за iptables уз помоћ БПФ-а, а у октобру 2013. је додато такође модул cls_bpf, који вам омогућава да пишете класификаторе саобраћаја користећи БПФ.

Ускоро ћемо детаљније погледати све ове примере, али прво ће нам бити корисно да научимо како да пишемо и компајлирамо произвољне програме за БПФ, пошто су могућности које пружа библиотека libpcap ограничено (једноставан пример: генерисан филтер libpcap може да врати само две вредности - 0 или 0к40000) или генерално, као у случају сеццомп, нису применљиве.

Програмирање БПФ-а сопственим рукама

Хајде да се упознамо са бинарним форматом БПФ инструкција, врло је једноставно:

   16    8    8     32
| code | jt | jf |  k  |

Свака инструкција заузима 64 бита, у којима је првих 16 бита код инструкције, затим постоје два осмобитна увлака, jt и jf, и 32 бита за аргумент K, чија сврха варира од команде до команде. На пример, команда ret, који прекида програм има код 6, а повратна вредност се узима из константе K. У Ц, једна БПФ инструкција је представљена као структура

struct sock_filter {
        __u16   code;
        __u8    jt;
        __u8    jf;
        __u32   k;
}

а цео програм је у форми структуре

struct sock_fprog {
        unsigned short len;
        struct sock_filter *filter;
}

Дакле, већ можемо писати програме (на пример, знамо кодове инструкција из [КСНУМКС]). Овако ће изгледати филтер ip6 од наш први пример:

struct sock_filter code[] = {
        { 0x28, 0, 0, 0x0000000c },
        { 0x15, 0, 1, 0x000086dd },
        { 0x06, 0, 0, 0x00040000 },
        { 0x06, 0, 0, 0x00000000 },
};
struct sock_fprog prog = {
        .len = ARRAY_SIZE(code),
        .filter = code,
};

програм prog можемо легално користити у позиву

setsockopt(sk, SOL_SOCKET, SO_ATTACH_FILTER, &prog, sizeof(prog))

Писање програма у облику машинских кодова није баш згодно, али је понекад неопходно (на пример, за отклањање грешака, креирање јединичних тестова, писање чланака на Хабре, итд.). За практичност у датотеци <linux/filter.h> помоћни макрои су дефинисани - исти пример као горе може се преписати као

struct sock_filter code[] = {
        BPF_STMT(BPF_LD|BPF_H|BPF_ABS, 12),
        BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, ETH_P_IPV6, 0, 1),
        BPF_STMT(BPF_RET|BPF_K, 0x00040000),
        BPF_STMT(BPF_RET|BPF_K, 0),
}

Међутим, ова опција није баш згодна. То је оно што су програмери језгра Линука закључили, а самим тим и у директоријуму tools/bpf кернела можете пронаћи асемблер и дебагер за рад са класичним БПФ-ом.

Језик асемблера је веома сличан излазу за отклањање грешака tcpdump, али поред тога можемо навести симболичке ознаке. На пример, ево програма који испушта све пакете осим ТЦП/ИПв4:

$ cat /tmp/tcp-over-ipv4.bpf
ldh [12]
jne #0x800, drop
ldb [23]
jneq #6, drop
ret #-1
drop: ret #0

Подразумевано, асемблер генерише код у формату <количество инструкций>,<code1> <jt1> <jf1> <k1>,..., за наш пример са ТЦП то ће бити

$ tools/bpf/bpf_asm /tmp/tcp-over-ipv4.bpf
6,40 0 0 12,21 0 3 2048,48 0 0 23,21 0 1 6,6 0 0 4294967295,6 0 0 0,

За практичност Ц програмера, може се користити другачији излазни формат:

$ tools/bpf/bpf_asm -c /tmp/tcp-over-ipv4.bpf
{ 0x28,  0,  0, 0x0000000c },
{ 0x15,  0,  3, 0x00000800 },
{ 0x30,  0,  0, 0x00000017 },
{ 0x15,  0,  1, 0x00000006 },
{ 0x06,  0,  0, 0xffffffff },
{ 0x06,  0,  0, 0000000000 },

Овај текст се може копирати у дефиницију структуре типа struct sock_filter, као што смо урадили на почетку овог одељка.

Линук и нетснифф-нг екстензије

Поред стандардног БПФ-а, Линук и tools/bpf/bpf_asm подршка и нестандардни сет. У основи, инструкције се користе за приступ пољима структуре struct sk_buff, који описује мрежни пакет у језгру. Међутим, постоје и друге врсте помоћних упутстава, на пример ldw cpu учитаће се у регистар A резултат покретања функције кернела raw_smp_processor_id(). (У новој верзији БПФ-а, ова нестандардна проширења су проширена да обезбеде програме са скупом помоћника језгра за приступ меморији, структурама и генерисање догађаја.) Ево занимљивог примера филтера у који копирамо само заглавља пакета у кориснички простор помоћу екстензије poff, помак корисног оптерећења:

ld poff
ret a

БПФ екстензије се не могу користити у tcpdump, али ово је добар разлог да се упознате са услужним пакетом netsniff-ng, који између осталог садржи и напредни програм netsniff-ng, који поред филтрирања помоћу БПФ-а садржи и ефикасан генератор саобраћаја, и напреднији од tools/bpf/bpf_asm, позвао је БПФ асемблер bpfc. Пакет садржи прилично детаљну документацију, погледајте и линкове на крају чланка.

сеццомп

Дакле, већ знамо како да напишемо БПФ програме произвољне сложености и спремни смо да погледамо нове примере, од којих је први сеццомп технологија, која омогућава, користећи БПФ филтере, да управља скупом и скупом аргумената системског позива који су доступни за датог процеса и његових потомака.

Прва верзија сеццомп-а додата је кернелу 2005. године и није била веома популарна, јер је пружала само једну опцију - да ограничи скуп системских позива доступних процесу на следеће: read, write, exit и sigreturn, а процес који је прекршио правила је убијен коришћењем SIGKILL. Међутим, 2012. године, сеццомп је додао могућност коришћења БПФ филтера, омогућавајући вам да дефинишете скуп дозвољених системских позива, па чак и да извршите проверу њихових аргумената. (Занимљиво је да је Цхроме био један од првих корисника ове функционалности, а људи из Цхроме-а тренутно развијају КРСИ механизам заснован на новој верзији БПФ-а и омогућава прилагођавање Линук безбедносних модула.) Линкови до додатне документације се могу наћи на крају. чланка.

Имајте на уму да је већ било чланака на чворишту о коришћењу сеццомпа, можда ће неко желети да их прочита пре (или уместо) читања следећих пододељака. У чланку Контејнери и безбедност: сеццомп даје примере коришћења сеццомп-а, и верзију из 2007. и верзију која користи БПФ (филтери се генеришу помоћу либсеццомп-а), говори о вези сеццомп-а са Доцкер-ом, а такође пружа многе корисне везе. У чланку Изоловање демона помоћу системд-а или „не треба вам Доцкер за ово!“ Посебно покрива како додати црне листе или беле листе системских позива за демоне који покрећу системд.

Затим ћемо видети како написати и учитати филтере за seccomp у голом Ц и користећи библиотеку libseccomp и које су предности и мане сваке опције, и на крају, да видимо како сеццомп користи програм strace.

Записивање и учитавање филтера за сеццомп

Већ знамо како писати БПФ програме, па хајде да прво погледамо програмски интерфејс сеццомп. Можете поставити филтер на нивоу процеса, а сви подређени процеси ће наследити ограничења. Ово се ради помоћу системског позива seccomp(2):

seccomp(SECCOMP_SET_MODE_FILTER, flags, &filter)

где &filter - ово је показивач на структуру која нам је већ позната struct sock_fprog, тј. БПФ програм.

Како се програми за сеццомп разликују од програма за утичнице? Пренет контекст. У случају сокета, дата нам је меморијска област која садржи пакет, ау случају сеццомп структура као што је

struct seccomp_data {
    int   nr;
    __u32 arch;
    __u64 instruction_pointer;
    __u64 args[6];
};

Овде nr је број системског позива који треба да се покрене, arch - тренутна архитектура (више о томе у наставку), args - до шест аргумената системског позива, и instruction_pointer је показивач на инструкцију корисничког простора која је извршила системски позив. Тако, на пример, да учитате број системског позива у регистар A морамо рећи

ldw [0]

Постоје и друге функције за сеццомп програме, на пример, контексту се може приступити само 32-битним поравнањем и не можете учитати пола речи или бајт - када покушавате да учитате филтер ldh [0] системски позив seccomp ће се вратити EINVAL. Функција проверава учитане филтере seccomp_check_filter() језгра. (Смешно је што су у оригиналном урезивању који је додао сеццомп функционалност заборавили да додају дозволу за коришћење инструкција овој функцији mod (остатак поделе) и сада је недоступан за сеццомп БПФ програме, од његовог додавања сломиће се АБИ.)

У суштини, ми већ знамо све за писање и читање сеццомп програма. Обично је програмска логика уређена као бела или црна листа системских позива, на пример програма

ld [0]
jeq #304, bad
jeq #176, bad
jeq #239, bad
jeq #279, bad
good: ret #0x7fff0000 /* SECCOMP_RET_ALLOW */
bad: ret #0

проверава црну листу од четири системска позива под бројевима 304, 176, 239, 279. Шта су то системски позиви? Не можемо са сигурношћу рећи, јер не знамо за коју архитектуру је програм написан. Стога су аутори сеццомп понуда покрените све програме провером архитектуре (тренутна архитектура је назначена у контексту као поље arch структуре struct seccomp_data). Са провереном архитектуром, почетак примера би изгледао овако:

ld [4]
jne #0xc000003e, bad_arch ; SCMP_ARCH_X86_64

и тада би наши системски позивни бројеви добили одређене вредности.

Пишемо и учитавамо филтере за сеццомп коришћење libseccomp

Писање филтера у изворном коду или у БПФ асемблеру омогућава вам да имате потпуну контролу над резултатом, али је у исто време понекад пожељно имати преносиви и/или читљиви код. Библиотека ће нам помоћи у томе либсеццомп, који обезбеђује стандардни интерфејс за писање црних или белих филтера.

Хајде, на пример, да напишемо програм који покреће бинарну датотеку по избору корисника, након што је претходно инсталирао црну листу системских позива са горњи чланак (програм је поједностављен ради веће читљивости, пуна верзија се може наћи овде):

#include <seccomp.h>
#include <unistd.h>
#include <err.h>

static int sys_numbers[] = {
        __NR_mount,
        __NR_umount2,
       // ... еще 40 системных вызовов ...
        __NR_vmsplice,
        __NR_perf_event_open,
};

int main(int argc, char **argv)
{
        scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);

        for (size_t i = 0; i < sizeof(sys_numbers)/sizeof(sys_numbers[0]); i++)
                seccomp_rule_add(ctx, SCMP_ACT_TRAP, sys_numbers[i], 0);

        seccomp_load(ctx);

        execvp(argv[1], &argv[1]);
        err(1, "execlp: %s", argv[1]);
}

Прво дефинишемо низ sys_numbers од 40+ бројева системског позива за блокирање. Затим иницијализујте контекст ctx и кажемо библиотеци шта желимо да дозволимо (SCMP_ACT_ALLOW) подразумевано сви системски позиви (лакше је направити црне листе). Затим, један по један, додајемо све системске позиве са црне листе. Као одговор на системски позив са листе, тражимо SCMP_ACT_TRAP, у овом случају ће сеццомп послати сигнал процесу SIGSYS са описом који системски позив је прекршио правила. Коначно, учитавамо програм у кернел помоћу seccomp_load, који ће компајлирати програм и приложити га процесу помоћу системског позива seccomp(2).

Да би се компајлирао успешно, програм мора бити повезан са библиотеком libseccomp, на пример:

cc -std=c17 -Wall -Wextra -c -o seccomp_lib.o seccomp_lib.c
cc -o seccomp_lib seccomp_lib.o -lseccomp

Пример успешног лансирања:

$ ./seccomp_lib echo ok
ok

Пример блокираног системског позива:

$ sudo ./seccomp_lib mount -t bpf bpf /tmp
Bad system call

Користимо straceза детаље:

$ sudo strace -e seccomp ./seccomp_lib mount -t bpf bpf /tmp
seccomp(SECCOMP_SET_MODE_FILTER, 0, {len=50, filter=0x55d8e78428e0}) = 0
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0xboobdeadbeef, si_syscall=__NR_mount, si_arch=AUDIT_ARCH_X86_64} ---
+++ killed by SIGSYS (core dumped) +++
Bad system call

како можемо знати да је програм прекинут због коришћења недозвољеног системског позива mount(2).

Дакле, написали смо филтер користећи библиотеку libseccomp, уклапајући нетривијални код у четири реда. У горњем примеру, ако постоји велики број системских позива, време извршења може бити приметно смањено, пошто је провера само листа поређења. За оптимизацију, либсеццомп је недавно имао патцх укључен, који додаје подршку за атрибут филтера SCMP_FLTATR_CTL_OPTIMIZE. Постављање овог атрибута на 2 ће претворити филтер у бинарни програм за претрагу.

Ако желите да видите како функционишу филтери бинарне претраге, погледајте једноставна скрипта, који генерише такве програме у БПФ асемблеру бирањем бројева система, на пример:

$ echo 1 3 6 8 13 | ./generate_bin_search_bpf.py
ld [0]
jeq #6, bad
jgt #6, check8
jeq #1, bad
jeq #3, bad
ret #0x7fff0000
check8:
jeq #8, bad
jeq #13, bad
ret #0x7fff0000
bad: ret #0

Немогуће је написати било шта знатно брже, пошто БПФ програми не могу да врше скокове увлачења (ми не можемо да урадимо нпр. jmp A или jmp [label+X]) и стога су сви прелази статични.

сеццомп и страце

Сви знају корисност strace је незаменљив алат за проучавање понашања процеса на Линук-у. Међутим, многи су чули и за проблеми са перформансама када користите овај услужни програм. Чињеница је да strace имплементирано коришћењем ptrace(2), а у овом механизму не можемо одредити на ком скупу системских позива треба да зауставимо процес, тј.

$ time strace du /usr/share/ >/dev/null 2>&1

real    0m3.081s
user    0m0.531s
sys     0m2.073s

и

$ time strace -e open du /usr/share/ >/dev/null 2>&1

real    0m2.404s
user    0m0.193s
sys     0m1.800s

се обрађују у приближно истом времену, иако у другом случају желимо да пратимо само један системски позив.

Нова опција --seccomp-bpf, додат strace верзија 5.3 вам омогућава да убрзате процес много пута и време покретања под трагом једног системског позива је већ упоредиво са временом редовног покретања:

$ time strace --seccomp-bpf -e open du /usr/share/ >/dev/null 2>&1

real    0m0.148s
user    0m0.017s
sys     0m0.131s

$ time du /usr/share/ >/dev/null 2>&1

real    0m0.140s
user    0m0.024s
sys     0m0.116s

(Овде, наравно, постоји мала обмана у томе што не пратимо главни системски позив ове команде. Ако бисмо пратили нпр. newfsstat, Онда strace кочио би једнако снажно као и без --seccomp-bpf.)

Како ова опција функционише? Без ње strace повезује са процесом и покреће га користећи PTRACE_SYSCALL. Када управљани процес изда (било који) системски позив, контрола се преноси на strace, који гледа аргументе системског позива и покреће га користећи PTRACE_SYSCALL. Након неког времена, процес завршава системски позив и када изађе из њега, контрола се поново преноси strace, који гледа повратне вредности и покреће процес користећи PTRACE_SYSCALL, и тако даље.

БПФ за малишане, нулти део: класични БПФ

Са сеццомп-ом, међутим, овај процес се може оптимизовати тачно онако како бисмо желели. Наиме, ако желимо да погледамо само системски позив X, онда можемо написати БПФ филтер који за X враћа вредност SECCOMP_RET_TRACE, а за позиве који нас не интересују - SECCOMP_RET_ALLOW:

ld [0]
jneq #X, ignore
trace: ret #0x7ff00000
ignore: ret #0x7fff0000

У овом случају, strace у почетку почиње процес као PTRACE_CONT, наш филтер се обрађује за сваки системски позив, ако системски позив није X, онда процес наставља да ради, али ако ово X, онда ће сеццомп пренети контролу straceкоји ће сагледати аргументе и започети процес као PTRACE_SYSCALL (пошто сеццомп нема могућност покретања програма по изласку из системског позива). Када се системски позив врати, strace поново ће покренути процес користећи PTRACE_CONT и чекаће нове поруке од сеццомпа.

БПФ за малишане, нулти део: класични БПФ

Када користите опцију --seccomp-bpf постоје два ограничења. Прво, неће бити могуће придружити се већ постојећем процесу (опција -p programi strace), пошто сеццомп ово не подржава. Друго, нема могућности не погледајте подређене процесе, пошто сеццомп филтере наслеђују сви подређени процеси без могућности да се ово онемогући.

Мало детаљније о томе како тачно strace рад са seccomp може се наћи из недавни извештај. За нас је најинтересантнија чињеница да се класични БПФ који представља сеццомп и данас користи.

xt_bpf

Вратимо се сада у свет мрежа.

Позадина: давно, 2007. језгро је било додато модул xt_u32 за нетфилтер. Написан је по аналогији са још древнијим саобраћајним класификатором cls_u32 и омогућио вам да напишете произвољна бинарна правила за иптаблес користећи следеће једноставне операције: учитајте 32 бита из пакета и извршите скуп аритметичких операција над њима. На пример,

sudo iptables -A INPUT -m u32 --u32 "6&0xFF=1" -j LOG --log-prefix "seen-by-xt_u32"

Учитава 32 бита ИП заглавља, почевши од допуне 6, и примењује маску на њих 0xFF (узмите нижи бајт). Ово поље protocol ИП заглавље и поредимо га са 1 (ИЦМП). У једном правилу можете комбиновати многе провере, а можете и извршити оператор @ — померите Кс бајтова удесно. На пример, правило

iptables -m u32 --u32 "6&0xFF=0x6 && 0>>22&0x3C@4=0x29"

проверава да ли број ТЦП секвенце није једнак 0x29. Нећу даље улазити у детаље, пошто је већ јасно да писање таквих правила руком није баш згодно. У чланку БПФ - заборављени бајт код, постоји неколико веза са примерима коришћења и генерисања правила за xt_u32. Погледајте и линкове на крају овог чланка.

Од 2013. модул уместо модула xt_u32 можете користити модул заснован на БПФ-у xt_bpf. Свако ко је читао до сада већ би требало да буде јасан о принципу његовог рада: покрените БПФ бајт код као иптаблес правила. Можете креирати ново правило, на пример, овако:

iptables -A INPUT -m bpf --bytecode <байткод> -j LOG

овде <байткод> - ово је код у излазном формату асемблера bpf_asm подразумевано, нпр.

$ cat /tmp/test.bpf
ldb [9]
jneq #17, ignore
ret #1
ignore: ret #0

$ bpf_asm /tmp/test.bpf
4,48 0 0 9,21 0 1 17,6 0 0 1,6 0 0 0,

# iptables -A INPUT -m bpf --bytecode "$(bpf_asm /tmp/test.bpf)" -j LOG

У овом примеру филтрирамо све УДП пакете. Контекст за БПФ програм у модулу xt_bpf, наравно, указује на пакетне податке, у случају иптаблес-а, на почетак ИПв4 заглавља. Повратна вредност из БПФ програма боолеанГде false значи да се пакет није подударао.

Јасно је да модул xt_bpf подржава сложеније филтере од примера изнад. Погледајмо стварне примере из Цлоудфаре-а. До недавно су користили модул xt_bpf за заштиту од ДДоС напада. У чланку Представљамо БПФ алате они објашњавају како (и зашто) генеришу БПФ филтере и објављују везе до скупа услужних програма за креирање таквих филтера. На пример, коришћењем услужног програма bpfgen можете креирати БПФ програм који одговара ДНС упиту за име habr.com:

$ ./bpfgen --assembly dns -- habr.com
ldx 4*([0]&0xf)
ld #20
add x
tax

lb_0:
    ld [x + 0]
    jneq #0x04686162, lb_1
    ld [x + 4]
    jneq #0x7203636f, lb_1
    ldh [x + 8]
    jneq #0x6d00, lb_1
    ret #65535

lb_1:
    ret #0

У програму прво учитавамо у регистар X адреса почетка линије x04habrx03comx00 унутар УДП датаграма, а затим проверите захтев: 0x04686162 <-> "x04hab" итд

Мало касније, Цлоудфаре је објавио п0ф -> БПФ компајлерски код. У чланку Представљамо п0ф БПФ компајлер говоре о томе шта је п0ф и како претворити п0ф потписе у БПФ:

$ ./bpfgen p0f -- 4:64:0:0:*,0::ack+:0
39,0 0 0 0,48 0 0 8,37 35 0 64,37 0 34 29,48 0 0 0,
84 0 0 15,21 0 31 5,48 0 0 9,21 0 29 6,40 0 0 6,
...

Тренутно више не користим Цлоудфаре xt_bpf, пошто су прешли на КСДП - једну од опција за коришћење нове верзије БПФ-а, види. Л4Дроп: КСДП ДДоС ублажавања.

cls_bpf

Последњи пример коришћења класичног БПФ-а у језгру је класификатор cls_bpf за подсистем контроле саобраћаја у Линуку, додат Линуксу крајем 2013. и концептуално замењујући древни cls_u32.

Међутим, сада нећемо описивати рад cls_bpf, пошто нам са становишта знања о класичном БПФ-у то неће дати ништа - већ смо се упознали са свим функционалностима. Поред тога, у наредним чланцима који говоре о проширеном БПФ-у, срешћемо се са овим класификатором више пута.

Још један разлог да се не говори о коришћењу класичног БПФ в cls_bpf Проблем је у томе што је, у поређењу са проширеним БПФ-ом, обим применљивости у овом случају радикално сужен: класични програми не могу да мењају садржај пакета и не могу да сачувају стање између позива.

Дакле, време је да се опростимо од класичног БПФ-а и погледамо у будућност.

Збогом класичном БПФ-у

Погледали смо како је БПФ технологија, развијена почетком деведесетих, успешно живела четврт века и до краја нашла нове примене. Међутим, слично преласку са стек машина на РИСЦ, који је послужио као подстицај за развој класичног БПФ-а, 32-их је дошло до преласка са 64-битних на XNUMX-битне машине и класични БПФ је почео да застарева. Поред тога, могућности класичног БПФ-а су веома ограничене, а поред застареле архитектуре – немамо могућност да сачувамо стање између позива ка БПФ програмима, нема могућности директне интеракције корисника, нема могућности интеракције. са језгром, осим за читање ограниченог броја структурних поља sk_buff и покретањем најједноставнијих помоћних функција, не можете променити садржај пакета и преусмерити их.

У ствари, тренутно све што је остало од класичног БПФ-а у Линуку је АПИ интерфејс, а унутар кернела сви класични програми, било да се ради о филтерима соцкета или сеццомп филтерима, аутоматски се преводе у нови формат, Ектендед БПФ. (О томе како се то тачно дешава, причаћемо у следећем чланку.)

Прелазак на нову архитектуру почео је 2013. године, када је Алексеј Старовоитов предложио шему ажурирања БПФ-а. 2014. одговарајуће закрпе почео да се појављује у језгру. Колико сам разумео, првобитни план је био само да се оптимизује архитектура и ЈИТ компајлер за ефикасније покретање на 64-битним машинама, али уместо тога ове оптимизације су означиле почетак новог поглавља у развоју Линука.

Даљи чланци у овој серији ће покривати архитектуру и примене нове технологије, првобитно познате као интерни БПФ, затим проширени БПФ, а сада једноставно БПФ.

референце

  1. Стевен МцЦанне и Ван Јацобсон, "БСД филтер пакета: нова архитектура за хватање пакета на нивоу корисника", https://www.tcpdump.org/papers/bpf-usenix93.pdf
  2. Стевен МцЦанне, "либпцап: Архитектура и методологија оптимизације за хватање пакета", https://sharkfestus.wireshark.org/sharkfest.11/presentations/McCanne-Sharkfest'11_Keynote_Address.pdf
  3. tcpdump, libpcap: https://www.tcpdump.org/
  4. ИПтабле У32 водич за меч.
  5. БПФ - заборављени бајт код: https://blog.cloudflare.com/bpf-the-forgotten-bytecode/
  6. Представљамо БПФ алатку: https://blog.cloudflare.com/introducing-the-bpf-tools/
  7. bpf_cls: http://man7.org/linux/man-pages/man8/tc-bpf.8.html
  8. Сеццомп преглед: https://lwn.net/Articles/656307/
  9. https://github.com/torvalds/linux/blob/master/Documentation/userspace-api/seccomp_filter.rst
  10. хабр: Контејнери и обезбеђење: сеццомп
  11. хабр: Изоловање демона помоћу системд-а или „не треба вам Доцкер за ово!“
  12. Паул Цхаигнон, "страце --сеццомп-бпф: поглед испод хаубе", https://fosdem.org/2020/schedule/event/debugging_strace_bpf/
  13. netsniff-ng: http://netsniff-ng.org/

Извор: ввв.хабр.цом

Додај коментар