BPF por la etuloj, parto nul: klasika BPF

Berkeley Packet Filters (BPF) estas Linukso-kerna teknologio kiu estas sur la ĉefpaĝoj de anglalingvaj teknikaj publikaĵoj jam de pluraj jaroj. Konferencoj estas plenigitaj kun raportoj pri la uzo kaj evoluo de BPF. David Miller, prizorganto de subsistem-retaj Linukso, vokas sian prelegon ĉe Linuksaj Plumbers 2018 "Ĉi tiu parolado ne temas pri XDP" (XDP estas unu uzkazo por BPF). Brendan Gregg donas paroladojn titolita Linukso BPF-superpotencoj. Toke Høiland-Jørgensen ridaske la kerno nun estas mikrokerno. Thomas Graf reklamas la ideon ke BPF estas javaskripto por la kerno.

Ankoraŭ ne ekzistas sistema priskribo de BPF ĉe Habré, kaj tial en serio de artikoloj mi provos paroli pri la historio de la teknologio, priskribi la arkitekturon kaj evoluilojn, kaj skizi la areojn de apliko kaj praktiko de uzado de BPF. Ĉi tiu artikolo, nulo, en la serio, rakontas la historion kaj arkitekturon de klasika BPF, kaj ankaŭ malkaŝas la sekretojn de ĝiaj funkciaj principoj. tcpdump, seccomp, strace, kaj multe pli.

La evoluo de BPF estas kontrolita de la Linukso-retokomunumo, la ĉefaj ekzistantaj aplikoj de BPF estas rilataj al retoj kaj tial, kun permeso. @eukariot, mi nomis la serion “BPF por la etuloj”, honore al la granda serio "Retoj por la etuloj".

Mallonga kurso en la historio de BPF(c)

Moderna BPF-teknologio estas plibonigita kaj vastigita versio de la malnova teknologio kun la sama nomo, nun nomita klasika BPF por eviti konfuzon. Bonkonata utileco estis kreita surbaze de la klasika BPF tcpdump, mekanismo seccomp, same kiel malpli konataj moduloj xt_bpf por iptables kaj klasigilo cls_bpf. En moderna Linukso, klasikaj BPF-programoj estas aŭtomate tradukitaj en la novan formon, tamen, de uzanta vidpunkto, la API restis en loko kaj novaj uzoj por klasika BPF, kiel ni vidos en ĉi tiu artikolo, ankoraŭ estas trovitaj. Tial, kaj ankaŭ ĉar sekvante la historion de la evoluo de klasika BPF en Linukso, estos pli klare kiel kaj kial ĝi evoluis al sia moderna formo, mi decidis komenci per artikolo pri klasika BPF.

Fine de la okdekaj de la pasinta jarcento, inĝenieroj de la fama Lawrence Berkeley Laboratory interesiĝis pri la demando kiel ĝuste filtri retajn pakaĵojn sur aparataro, kiu estis moderna fine de la okdekaj de la lasta jarcento. La baza ideo de filtrado, origine efektivigita en teknologio CSPF (CMU/Stanford Packet Filter), estis filtri nenecesajn pakaĵetojn kiel eble plej frue, t.e. en kernspaco, ĉar tio evitas kopii nenecesajn datumojn en uzantspacon. Por disponigi rultempan sekurecon por kurado de uzantkodo en kernspaco, sablokesita virtuala maŝino estis uzita.

Tamen, la virtualaj maŝinoj por ekzistantaj filtriloj estis dizajnitaj por funkcii per stak-bazitaj maŝinoj kaj ne funkciis tiel efike sur pli novaj RISC-maŝinoj. Kiel rezulto, per la klopodoj de inĝenieroj de Berkeley Labs, nova BPF (Berkeley Packet Filters) teknologio estis evoluigita, kies virtuala maŝina arkitekturo estis dizajnita surbaze de la Motorola 6502-procesoro - la laborĉevalo de tiaj konataj produktoj kiel Apple IINES. La nova virtuala maŝino pliigis filtrilan efikecon dekfoje kompare kun ekzistantaj solvoj.

BPF maŝinarkitekturo

Ni konatiĝos kun arkitekturo en labormaniero, analizante ekzemplojn. Tamen, por komenci, ni diru, ke la maŝino havis du 32-bitajn registrojn alireblajn por la uzanto, akumulilon. A kaj indeksa registro X, 64 bajtoj da memoro (16 vortoj), disponeblaj por skribo kaj posta legado, kaj malgranda sistemo de komandoj por labori kun ĉi tiuj objektoj. Salti instrukciojn por efektivigi kondiĉajn esprimojn ankaŭ estis haveblaj en la programoj, sed por garantii la ĝustatempan kompletigon de la programo, saltoj povus esti nur antaŭenigitaj, t.e., precipe, estis malpermesite krei buklojn.

La ĝenerala skemo por ekfunkciigi la maŝinon estas jena. La uzanto kreas programon por la arkitekturo BPF kaj, uzante iuj kernmekanismo (kiel sistemvoko), ŝarĝas kaj ligas la programon al al iuj al la okazaĵgeneratoro en la kerno (ekzemple, okazaĵo estas la alveno de la sekva pako sur la retkarto). Kiam okazaĵo okazas, la kerno rulas la programon (ekzemple, en interpretisto), kaj la maŝinmemoro respondas al al iuj kerna memorregiono (ekzemple, datumoj de envenanta pako).

La supre sufiĉos por ke ni komencu rigardi ekzemplojn: ni konatiĝos kun la sistemo kaj komandformato laŭbezone. Se vi volas tuj studi la komandan sistemon de virtuala maŝino kaj lerni pri ĉiuj ĝiaj kapabloj, tiam vi povas legi la originalan artikolon. La BSD Paka Filtrilo kaj/aŭ la unua duono de la dosiero Dokumentado/reto/filtrilo.txt el la dokumentado de la kerno. Krome, vi povas studi la prezenton libpcap: Arkitekturo kaj Optimumigo-Metodologio por Paka Kapto, en kiu McCanne, unu el la aŭtoroj de BPF, parolas pri la historio de kreado libpcap.

Ni nun konsideru ĉiujn signifajn ekzemplojn de uzado de klasika BPF en Linukso: tcpdump (libpcap), sekcomp, xt_bpf, cls_bpf.

tcpdump

La evoluo de BPF estis efektivigita paralele kun la evoluo de la fasado por paka filtrado - konata utileco tcpdump. Kaj, ĉar ĉi tiu estas la plej malnova kaj fama ekzemplo de uzado de klasika BPF, disponebla en multaj operaciumoj, ni komencos nian studon de la teknologio per ĝi.

(Mi prizorgis ĉiujn ekzemplojn en ĉi tiu artikolo pri Linukso 5.6.0-rc6. La eligo de kelkaj komandoj estis redaktita por pli bona legebleco.)

Ekzemplo: observante IPv6-pakojn

Ni imagu, ke ni volas rigardi ĉiujn IPv6-pakojn sur interfaco eth0. Por fari tion ni povas ruli la programon tcpdump per simpla filtrilo ip6:

$ sudo tcpdump -i eth0 ip6

tiel tcpdump kompilas la filtrilon ip6 en la BPF-arkitekturan bajtkodon kaj sendu ĝin al la kerno (vidu detalojn en la sekcio Tcpdump: ŝarĝo). La ŝarĝita filtrilo ruliĝos por ĉiu pako pasanta tra la interfaco eth0. Se la filtrilo liveras ne-nulan valoron n, tiam ĝis n bajtoj de la pako estos kopiitaj al uzantspaco kaj ni vidos ĝin en la eligo tcpdump.

BPF por la etuloj, parto nul: klasika BPF

Montriĝas, ke ni povas facile ekscii, kiu bajtkodo estis sendita al la kerno tcpdump kun la helpo de la tcpdump, se ni rulas ĝin kun la opcio -d:

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

Sur linio nulo ni rulas la komandon ldh [12], kiu signifas "ŝarĝi en registron A duona vorto (16 bitoj) situanta ĉe adreso 12” kaj la sola demando estas kian memoron ni traktas? La respondo estas, ke ĉe x komencas (x+1)la bajto de la analizita retpako. Ni legas pakaĵojn el la interfaco Ethernet eth0kaj ĉi tio signifaske la pako aspektas tiel (por simpleco, ni supozas, ke ne estas VLAN-etikedoj en la pako):

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

Do post ekzekuto de la komando ldh [12] en la registro A estos kampo Ether Type — la speco de pakaĵeto elsendita en ĉi tiu Ethernet-kadro. Sur la linio 1 ni komparas la enhavon de la registro A (pakospeco) c 0x86ddkaj ĉi tio kaj ekzistas La tipo, pri kiu ni interesiĝas, estas IPv6. Sur la linio 1, krom la kompara komando, estas du pliaj kolumnoj - jt 2 и jf 3 — markoj al kiuj vi devas iri se la komparo estas sukcesa (A == 0x86dd) kaj malsukcesa. Do, en sukcesa kazo (IPv6) ni iras al linio 2, kaj en malsukcesa kazo - al linio 3. Sur linio 3 la programo finiĝas per kodo 0 (ne kopiu la pakaĵon), ĉe linio 2 la programo finiĝas per kodo. 262144 (kopiu al mi maksimume 256 kilobajtan pakaĵon).

Pli komplika ekzemplo: ni rigardas TCP-pakojn laŭ celhaveno

Ni vidu kiel aspektas filtrilo, kiu kopias ĉiujn TCP-pakojn kun celhaveno 666. Ni konsideros la kazon de IPv4, ĉar la kazo de IPv6 estas pli simpla. Post studado de ĉi tiu ekzemplo, vi mem povas esplori la IPv6-filtrilon kiel ekzerco (ip6 and tcp dst port 666) kaj filtrilo por la ĝenerala kazo (tcp dst port 666). Do, la filtrilo pri kiu ni interesiĝas aspektas jene:

$ 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

Ni jam scias, kion faras la linioj 0 kaj 1. Sur linio 2 ni jam kontrolis, ke ĉi tio estas IPv4-pako (Ether Type = 0x800) kaj ŝarĝu ĝin en la registron A 24-a bajto de la pako. Nia pako aspektas

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

kio signifas, ke ni ŝarĝas en la registron A la Protokola kampo de la IP-kapo, kio estas logika, ĉar ni volas kopii nur TCP-pakojn. Ni komparas Protokolon kun 0x6 (IPPROTO_TCP) sur linio 3.

Sur linioj 4 kaj 5 ni ŝargas la duonvortojn situantajn ĉe la adreso 20 kaj uzas la komandon jset kontrolu ĉu unu el la tri estas agordita flagoj - portante la eldonitan maskon jset la tri plej signifaj bitoj estas malbaritaj. Du el la tri bitoj diras al ni ĉu la pako estas parto de fragmentita IP-pako, kaj se jes, ĉu ĝi estas la lasta fragmento. La tria bito estas rezervita kaj devas esti nul. Ni ne volas kontroli aŭ nekompletajn aŭ rompitajn pakaĵojn, do ni kontrolas ĉiujn tri bitojn.

Linio 6 estas la plej interesa en ĉi tiu listo. Esprimo ldxb 4*([14]&0xf) signifas, ke ni ŝarĝas en la registron X la malplej signifaj kvar bitoj de la dekkvina bajto de la pakaĵeto multiplikita per 4. La malplej signifaj kvar bitoj de la dekkvina bajto estas la kampo Interreta kaplongo IPv4-kapo, kiu konservas la longon de la kaplinio en vortoj, do vi tiam devas multobligi per 4. Interese, la esprimo 4*([14]&0xf) estas nomo por speciala adresskemo, kiu povas esti uzata nur en ĉi tiu formo kaj nur por registro X, t.e. ankaŭ ni ne povas diri ldb 4*([14]&0xf) nek ldxb 5*([14]&0xf) (ni povas nur specifi malsaman ofseton, ekzemple, ldxb 4*([16]&0xf)). Estas klare, ke ĉi tiu adresskemo estis aldonita al BPF ĝuste por ricevi X (indeksa registro) IPv4-kapa longo.

Do sur linio 7 ni provas ŝargi duonvorton ĉe (X+16). Memorante, ke 14 bajtoj estas okupataj de la kaplinio Ethernet, kaj X enhavas la longon de la IPv4-kapo, ni komprenas tion en A TCP-celloka haveno estas ŝarĝita:

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

Fine, sur la linio 8 ni komparas la celhavenon kun la dezirata valoro kaj sur la linioj 9 aŭ 10 ni resendas la rezulton - ĉu kopii la paketon aŭ ne.

Tcpdump: ŝarĝo

En la antaŭaj ekzemploj, ni specife ne detale detalis ĝuste kiel ni ŝargas BPF-bajtkodon en la kernon por paka filtrado. Ĝenerale parolante, tcpdump portita al multaj sistemoj kaj por labori kun filtriloj tcpdump uzas la bibliotekon libpcap. Mallonge, meti filtrilon sur interfaco uzante libpcap, vi devas fari la jenon:

Por vidi kiel funkcias pcap_setfilter efektivigita en Linukso, ni uzas strace (kelkaj linioj estis forigitaj):

$ 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
...

Sur la unuaj du linioj de eligo ni kreas kruda ingo legi ĉiujn Eterretajn kadrojn kaj ligi ĝin al la interfaco eth0... De nia unua ekzemplo ni scias ke la filtrilo ip konsistos el kvar instrukcioj de BPF, kaj sur la tria linio ni vidas kiel uzi la opcion SO_ATTACH_FILTER sistema voko setsockopt ni ŝarĝas kaj konektas filtrilon de longo 4. Ĉi tiu estas nia filtrilo.

Indas noti, ke en klasika BPF, ŝarĝo kaj konekto de filtrilo ĉiam okazas kiel atoma operacio, kaj en la nova versio de BPF, ŝarĝo de la programo kaj ligado de ĝi al la eventogeneratoro estas disigitaj en tempo.

Kaŝita Vero

Iom pli kompleta versio de la eligo aspektas jene:

$ 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
...

Kiel menciite supre, ni ŝarĝas kaj konektas nian filtrilon al la ingo sur la linio 5, sed kio okazas ĉe la linioj 3 kaj 4? Rezultas, ke ĉi tio libpcap prizorgas nin - por ke la eligo de nia filtrilo ne inkludu pakaĵetojn kiuj ne kontentigas ĝin, la biblioteko ligas imita filtrilo ret #0 (faligi ĉiujn pakaĵetojn), ŝanĝas la ingon al ne-bloka reĝimo kaj provas subtrahi ĉiujn pakaĵetojn kiuj povus resti de antaŭaj filtriloj.

Entute, por filtri pakaĵojn en Linukso uzante klasikan BPF, vi devas havi filtrilon en formo de strukturo kiel struct sock_fprog kaj malferma ingo, post kiu la filtrilo povas esti alkroĉita al la ingo uzante sistemvokon setsockopt.

Interese, la filtrilo povas esti alkroĉita al iu ajn ingo, ne nur kruda. Jen ekzemplo programo kiu fortranĉas ĉiujn krom la unuaj du bajtoj de ĉiuj envenantaj UDP-datumgramoj. (Mi aldonis komentojn en la kodon por ne malordigi la artikolon.)

Pli da detaloj pri uzo setsockopt por konekti filtrilojn, vidu ingo (7), sed pri skribi viajn proprajn filtrilojn kiel struct sock_fprog sen helpo tcpdump ni parolos en la sekcio Programante BPF per niaj propraj manoj.

Klasika BPF kaj la XNUMX-a jarcento

BPF estis inkludita en Linukso en 1997 kaj restis laborĉevalo dum longa tempo libpcap sen iuj specialaj ŝanĝoj (Linukso-specifaj ŝanĝoj, kompreneble, estis, sed ili ne ŝanĝis la tutmondan bildon). La unuaj seriozaj signoj, ke BPF evoluus venis en 2011, kiam Eric Dumazet proponis flikaĵo, kiu aldonas Just In Time Compiler al la kerno - tradukilo por konverti BPF-bajtkodon al indiĝena x86_64 la kodo.

JIT-kompililo estis la unua en la ĉeno de ŝanĝoj: en 2012 aperis kapablo skribi filtrilojn por sekkomp, uzante BPF, en januaro 2013 ekzistis aldonis modulo xt_bpf, kiu permesas skribi regulojn por iptables kun la helpo de BPF, kaj en oktobro 2013 estis aldonis ankaŭ modulo cls_bpf, kiu permesas vin skribi trafikklasigilojn uzante BPF.

Ni baldaŭ rigardos ĉiujn ĉi ekzemplojn pli detale, sed unue estos utile por ni lerni kiel verki kaj kompili arbitrajn programojn por BPF, ĉar la kapabloj provizitaj de la biblioteko. libpcap limigita (simpla ekzemplo: filtrilo generita libpcap povas resendi nur du valorojn - 0 aŭ 0x40000) aŭ ĝenerale, kiel en la kazo de seccomp, ne aplikeblas.

Programante BPF per niaj propraj manoj

Ni konatiĝu kun la binara formato de instrukcioj BPF, ĝi estas tre simpla:

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

Ĉiu instrukcio okupas 64 bitojn, en kiuj la unuaj 16 bitoj estas la instrukciokodo, tiam estas du ok-bitaj strekoj, jt и jf, kaj 32 bitoj por la argumento K, kies celo varias de komando al komando. Ekzemple, la komando ret, kiu finas la programon havas la kodon 6, kaj la revenvaloro estas prenita de la konstanto K. En C, ununura BPF-instrukcio estas reprezentita kiel strukturo

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

kaj la tuta programo estas en formo de strukturo

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

Tiel, ni jam povas skribi programojn (ekzemple, ni konas la instrukodojn de [1]). Jen kiel aspektos la filtrilo ip6 el nia unua ekzemplo:

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,
};

programo prog ni povas laŭleĝe uzi en voko

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

Verki programojn en formo de maŝinkodoj ne estas tre oportuna, sed foje necesas (ekzemple por sencimigi, krei unutestojn, verki artikolojn pri Habré ktp.). Por komforto, en la dosiero <linux/filter.h> helpaj makrooj estas difinitaj - la sama ekzemplo kiel supre povus esti reverkita kiel

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),
}

Tamen, ĉi tiu opcio ne estas tre oportuna. Jen kion rezonis la Linuksaj kernaj programistoj, kaj do en la dosierujo tools/bpf kernoj vi povas trovi asembleron kaj erarserĉilon por labori kun klasika BPF.

Asembla lingvo tre similas al sencimiga eligo tcpdump, sed krome ni povas specifi simbolajn etikedojn. Ekzemple, jen programo, kiu faligas ĉiujn pakaĵojn krom TCP/IPv4:

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

Defaŭlte, la asemblero generas kodon en la formato <количество инструкций>,<code1> <jt1> <jf1> <k1>,..., por nia ekzemplo kun TCP ĝi estos

$ 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,

Por la komforto de C-programistoj, malsama eligformato povas esti uzata:

$ 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 },

Ĉi tiu teksto povas esti kopiita en la tipstrukturdifinon struct sock_filter, kiel ni faris komence de ĉi tiu sekcio.

Linukso kaj netsniff-ng etendaĵoj

Krom norma BPF, Linukso kaj tools/bpf/bpf_asm subteno kaj ne-norma aro. Esence, instrukcioj estas uzataj por aliri la kampojn de strukturo struct sk_buff, kiu priskribas retpakaĵon en la kerno. Tamen ekzistas ankaŭ aliaj specoj de helpaj instrukcioj, ekzemple ldw cpu ŝarĝos en la registron A rezulto de rulado de kerna funkcio raw_smp_processor_id(). (En la nova versio de BPF, ĉi tiuj ne-normaj etendaĵoj estis etenditaj por provizi programojn per aro da kernaj helpantoj por aliri memoron, strukturojn kaj generi eventojn.) Jen interesa ekzemplo de filtrilo en kiu ni kopias nur la pakaj kaplinioj en uzantspacon uzante la etendon poff, utilŝarĝa ofseto:

ld poff
ret a

BPF-etendaĵoj ne povas esti uzataj en tcpdump, sed ĉi tio estas bona kialo por konatiĝi kun la utila pako netsniff-ng, kiu, interalie, enhavas altnivelan programon netsniff-ng, kiu, krom filtrado per BPF, enhavas ankaŭ efikan trafikgeneratoron, kaj pli altnivelan ol tools/bpf/bpf_asm, BPF asemblero vokis bpfc. La pako enhavas sufiĉe detalan dokumentadon, vidu ankaŭ la ligilojn ĉe la fino de la artikolo.

sekkomp

Do, ni jam scias kiel skribi BPF-programojn de arbitra komplekseco kaj estas pretaj rigardi novajn ekzemplojn, la unua el kiuj estas la sekcomp-teknologio, kiu permesas, uzante BPF-filtrilojn, administri la aron kaj aron de sistemvokaj argumentoj disponeblaj por donita procezo kaj ĝiaj posteuloj.

La unua versio de seccomp estis aldonita al la kerno en 2005 kaj ne estis tre populara, ĉar ĝi disponigis nur ununuran opcion - limigi la aron de sistemaj vokoj disponeblaj al procezo al la sekvanta: read, write, exit и sigreturn, kaj la procezo kiu malobservis la regulojn estis mortigita uzante SIGKILL. Tamen, en 2012, seccomp aldonis la kapablon uzi BPF-filtrilojn, permesante al vi difini aron da permesitaj sistemvokoj kaj eĉ fari kontrolojn pri iliaj argumentoj. (Interese, ke Chrome estis unu el la unuaj uzantoj de ĉi tiu funkcieco, kaj la Chrome-uloj nuntempe disvolvas KRSI-mekanismon bazitan sur nova versio de BPF kaj permesanta personigon de Linuksaj Sekurecaj Moduloj.) Ligiloj al plia dokumentaro troviĝas ĉe la fino. de la artikolo.

Notu, ke jam aperis artikoloj pri la nabo pri uzado de seccomp, eble iu volos legi ilin antaŭ (aŭ anstataŭ) legi la sekvajn subsekciojn. En la artikolo Ujoj kaj sekureco: seccomp provizas ekzemplojn pri uzado de seccomp, kaj la 2007-datita versio kaj la versio uzanta BPF (filtriloj estas generitaj per libseccomp), parolas pri la ligo de seccomp kun Docker, kaj ankaŭ disponigas multajn utilajn ligilojn. En la artikolo Izolante demonojn per systemd aŭ "vi ne bezonas Docker por ĉi tio!" Ĝi kovras, precipe, kiel aldoni nigrajn listojn aŭ blankajn listojn de sistemvokoj por demonoj kurantaj systemd.

Poste ni vidos kiel skribi kaj ŝargi filtrilojn por seccomp en nuda C kaj uzante la bibliotekon libseccomp kaj kiaj estas la avantaĝoj kaj malavantaĝoj de ĉiu opcio, kaj finfine, ni vidu kiel seccomp estas uzata de la programo strace.

Skribado kaj ŝarĝo de filtriloj por seccomp

Ni jam scias kiel skribi BPF-programojn, do ni unue rigardu la seccomp programan interfacon. Vi povas agordi filtrilon ĉe la proceza nivelo, kaj ĉiuj infanaj procezoj heredos la limigojn. Ĉi tio estas farita per sistemvoko seccomp(2):

seccomp(SECCOMP_SET_MODE_FILTER, flags, &filter)

kie &filter - ĉi tio estas montrilo al strukturo jam konata al ni struct sock_fprog, t.e. BPF-programo.

Kiel programoj por seccomp diferencas de programoj por ingoj? Transdona kunteksto. En la kazo de ingoj, ni ricevis memorareon enhavantan la pakaĵon, kaj en la kazo de seccomp ni ricevis strukturon kiel

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

estas nr estas la numero de la sistemvoko lanĉota, arch - nuna arkitekturo (pli pri tio ĉi malsupre), args - ĝis ses sistemvokaj argumentoj, kaj instruction_pointer estas montrilo al la uzantspaca instrukcio kiu faris la sistemvokon. Tiel, ekzemple, ŝarĝi la sisteman alvoknumeron en la registron A ni devas diri

ldw [0]

Estas aliaj funkcioj por seccomp-programoj, ekzemple, la kunteksto nur alireblas per 32-bita vicigo kaj vi ne povas ŝargi duonvorton aŭ bajton - kiam vi provas ŝargi filtrilon. ldh [0] sistema voko seccomp revenos EINVAL. La funkcio kontrolas la ŝarĝitajn filtrilojn seccomp_check_filter() kernoj. (Amuza afero estas, ke en la originala kommit kiu aldonis la seccomp-funkcion, ili forgesis aldoni permeson uzi la instrukcion al ĉi tiu funkcio. mod (divida resto) kaj nun estas neatingebla por seccomp BPF-programoj, ekde ĝia aldono rompos ABI.)

Esence, ni jam scias ĉion por skribi kaj legi seccomp-programojn. Kutime la programlogiko estas aranĝita kiel blanka aŭ nigra listo de sistemvokoj, ekzemple la programo

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

kontrolas nigran liston de kvar sistemvokoj numeritaj 304, 176, 239, 279. Kio estas ĉi tiuj sistemvokoj? Ni ne povas diri certe, ĉar ni ne scias por kiu arkitekturo la programo estis skribita. Tial, la aŭtoroj de seccomp proponu komencu ĉiujn programojn per arkitekturo-kontrolo (la nuna arkitekturo estas indikita en la kunteksto kiel kampo arch strukturoj struct seccomp_data). Kun la arkitekturo kontrolita, la komenco de la ekzemplo aspektus kiel:

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

kaj tiam niaj sistemaj voknumeroj ricevus certajn valorojn.

Ni skribas kaj ŝarĝas filtrilojn por seccomp uzante libseccomp

Skribi filtrilojn en denaska kodo aŭ en BPF-asembleo permesas havi plenan kontrolon de la rezulto, sed samtempe, estas foje preferinde havi porteblan kaj/aŭ legeblan kodon. La biblioteko helpos nin pri tio libseccomp, kiu disponigas norman interfacon por skribi nigrajn aŭ blankajn filtrilojn.

Ni ekzemple verku programon, kiu rulas binaran dosieron laŭ elekto de la uzanto, antaŭe instalinte nigran liston de sistemvokoj de la supra artikolo (la programo estis simpligita por pli granda legebleco, la plena versio troveblas tie):

#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]);
}

Unue ni difinas tabelon sys_numbers de 40+ sistemaj voknumeroj por bloki. Poste, pravalorigu la kuntekston ctx kaj diru al la biblioteko kion ni volas permesi (SCMP_ACT_ALLOW) ĉiuj sistemvokoj defaŭlte (estas pli facile konstrui nigrajn listojn). Poste, unu post alia, ni aldonas ĉiujn sistemajn vokojn el la nigra listo. Responde al sistemvoko de la listo, ni petas SCMP_ACT_TRAP, en ĉi tiu kazo seccomp sendos signalon al la procezo SIGSYS kun priskribo de kiu sistemvoko malobservis la regulojn. Fine, ni ŝargas la programon en la kernon uzante seccomp_load, kiu kompilos la programon kaj aldonos ĝin al la procezo uzante sistemvokon seccomp(2).

Por sukcesa kompilo, la programo devas esti ligita kun la biblioteko libseccompekzemple:

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

Ekzemplo de sukcesa lanĉo:

$ ./seccomp_lib echo ok
ok

Ekzemplo de blokita sistemvoko:

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

Ni uzas stracepor detaloj:

$ 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

kiel ni povas scii, ke la programo estis ĉesigita pro la uzo de kontraŭleĝa sistemvoko mount(2).

Do, ni skribis filtrilon uzante la bibliotekon libseccomp, konvenante ne-trivialan kodon en kvar liniojn. En la supra ekzemplo, se estas granda nombro da sistemvokoj, la ekzekuttempo povas esti rimarkeble reduktita, ĉar la kontrolo estas nur listo de komparoj. Por optimumigo, libseccomp lastatempe havis diakilo inkluzivita, kiu aldonas subtenon por la filtrila atributo SCMP_FLTATR_CTL_OPTIMIZE. Agordi ĉi tiun atributon al 2 transformos la filtrilon en binaran serĉprogramon.

Se vi volas vidi kiel funkcias binaraj serĉfiltriloj, rigardu simpla skripto, kiu generas tiajn programojn en BPF-asemblero diskante sistemajn voknumerojn, ekzemple:

$ 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

Estas neeble skribi ion signife pli rapide, ĉar BPF-programoj ne povas fari indentsaltojn (ni ne povas fari, ekzemple, jmp Ajmp [label+X]) kaj tial ĉiuj transiroj estas senmovaj.

seccomp kaj strace

Ĉiuj konas la utilecon strace estas nemalhavebla ilo por studi la konduton de procezoj en Linukso. Tamen multaj ankaŭ aŭdis pri tio agado problemoj kiam vi uzas ĉi tiun ilon. La fakto estas tio strace efektivigita uzante ptrace(2), kaj en ĉi tiu mekanismo ni ne povas specifi ĉe kia aro de sistemaj vokoj ni devas ĉesigi la procezon, t.e., ekzemple, komandoj

$ 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

estas prilaboritaj proksimume en la sama tempo, kvankam en la dua kazo ni volas spuri nur unu sistemvokon.

Nova opcio --seccomp-bpf, aldonita al strace versio 5.3, permesas vin akceli la procezon multfoje kaj la ektempo sub la spuro de unu sistemvoko jam estas komparebla al la tempo de regula ekfunkciigo:

$ 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

(Ĉi tie, kompreneble, estas eta trompo, ĉar ni ne spuras la ĉefan sisteman alvokon de ĉi tiu komando. Se ni spurus, ekzemple, newfsstat, tiam strace bremsus same forte kiel sen --seccomp-bpf.)

Kiel funkcias ĉi tiu opcio? Sen ŝi strace konektas al la procezo kaj komencas ĝin uzi PTRACE_SYSCALL. Kiam administrita procezo eligas (ajnan) sistemvokon, kontrolo estas transdonita al strace, kiu rigardas la argumentojn de la sistemvoko kaj rulas ĝin kun PTRACE_SYSCALL. Post iom da tempo, la procezo kompletigas la sisteman vokon kaj elirante ĝin, kontrolo denove estas translokigita strace, kiu rigardas la revenajn valorojn kaj komencas la procezon uzante PTRACE_SYSCALL, kaj tiel plu.

BPF por la etuloj, parto nul: klasika BPF

Kun seccomp, tamen, ĉi tiu procezo povas esti optimumigita ĝuste kiel ni ŝatus. Nome, se ni volas rigardi nur la sistemvokon X, tiam ni povas skribi BPF-filtrilon por tio X redonas valoron SECCOMP_RET_TRACE, kaj por alvokoj kiuj ne interesas nin - SECCOMP_RET_ALLOW:

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

En ĉi tiu kazo strace komence komencas la procezon kiel PTRACE_CONT, nia filtrilo estas procesita por ĉiu sistemvoko, se la sistemvoko ne estas X, tiam la procezo daŭre funkcias, sed se ĉi tio X, tiam seccomp transdonos kontrolon stracekiu rigardos la argumentojn kaj komencos la procezon kiel PTRACE_SYSCALL (ĉar seccomp ne havas la kapablon ruli programon ĉe eliro de sistema voko). Kiam la sistemvoko revenas, strace rekomencos la procezon uzante PTRACE_CONT kaj atendos novajn mesaĝojn de seccomp.

BPF por la etuloj, parto nul: klasika BPF

Kiam vi uzas la opcion --seccomp-bpf estas du limigoj. Unue, ne eblos aliĝi al jam ekzistanta procezo (opcio -p programoj strace), ĉar ĉi tio ne estas subtenata de seccomp. Due, ne ekzistas ebleco ne rigardu infanajn procezojn, ĉar seccomp-filtriloj estas hereditaj de ĉiuj infanprocezoj sen la kapablo malŝalti ĉi tion.

Iom pli da detaloj pri kiel ĝuste strace labori kun seccomp troveblas de lastatempa raporto. Por ni, la plej interesa fakto estas, ke la klasika BPF reprezentita de seccomp ankoraŭ estas uzata hodiaŭ.

xt_bpf

Ni nun reiru al la mondo de retoj.

Fono: antaŭ longe, en 2007, la kerno estis aldonis modulo xt_u32 por netfiltrilo. Ĝi estis skribita per analogio kun eĉ pli antikva trafikklasigilo cls_u32 kaj permesis al vi skribi arbitrajn binarajn regulojn por iptables uzante la sekvajn simplajn operaciojn: ŝargi 32 bitojn el pako kaj plenumi aron da aritmetikaj operacioj sur ili. Ekzemple,

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

Ŝarĝas la 32 bitojn de la IP-kapo, komencante ĉe la kompletigo 6, kaj aplikas maskon al ili 0xFF (prenu la malaltan bajton). Ĉi tiu kampo protocol IP-kapo kaj ni komparas ĝin kun 1 (ICMP). Vi povas kombini multajn kontrolojn en unu regulo, kaj vi ankaŭ povas ekzekuti la funkciigiston @ — movu X bajtojn dekstren. Ekzemple, la regulo

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

kontrolas ĉu TCP-sekvenca numero ne estas egala 0x29. Mi ne eniros pli detalojn, ĉar jam estas klare, ke skribi tiajn regulojn mane ne estas tre oportune. En la artikolo BPF - la forgesita bajtkodo, ekzistas pluraj ligiloj kun ekzemploj de uzado kaj regulgenerado por xt_u32. Vidu ankaŭ la ligilojn ĉe la fino de ĉi tiu artikolo.

Ekde 2013 modulo anstataŭ modulo xt_u32 vi povas uzi BPF-bazitan modulon xt_bpf. Ĉiu, kiu legis ĉi tien, devus jam esti klara pri la principo de ĝia funkciado: ruli BPF-bajtkodon laŭ reguloj de iptables. Vi povas krei novan regulon, ekzemple, jene:

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

tie <байткод> - ĉi tiu estas la kodo en eligo-formato de asemblero bpf_asm defaŭlte, ekzemple,

$ 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

En ĉi tiu ekzemplo ni filtras ĉiujn UDP-pakaĵojn. Kunteksto por BPF-programo en modulo xt_bpf, kompreneble, montras al la pakaj datumoj, en la kazo de iptables, al la komenco de la IPv4-kapo. Revenvaloro de BPF-programo buleakie false signifas, ke la pako ne kongruis.

Estas klare ke la modulo xt_bpf subtenas pli kompleksajn filtrilojn ol la supra ekzemplo. Ni rigardu realajn ekzemplojn de Cloudfare. Ĝis antaŭ nelonge ili uzis la modulon xt_bpf por protekti kontraŭ DDoS-atakoj. En la artikolo Prezentante la BPF-Ilojn ili klarigas kiel (kaj kial) ili generas BPF-filtrilojn kaj publikigas ligilojn al aro da utilecoj por krei tiajn filtrilojn. Ekzemple, uzante la utilecon bpfgen vi povas krei BPF-programon, kiu kongruas kun DNS-demando por nomo 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

En la programo ni unue ŝargas en la registron X komenco de linio adreso x04habrx03comx00 ene de UDP-dagramo kaj poste kontrolu la peton: 0x04686162 <-> "x04hab" kaj tiel plu.

Iom poste, Cloudfare publikigis la p0f -> BPF-kompilkodon. En la artikolo Prezentante la p0f BPF-kompililon ili parolas pri kio p0f estas kaj kiel konverti p0f subskribojn al BPF:

$ ./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,
...

Nuntempe ne plu uzas Cloudfare xt_bpf, ĉar ili moviĝis al XDP - unu el la ebloj por uzi la novan version de BPF, vidu. L4Drop: XDP DDoS Mildigoj.

cls_bpf

La lasta ekzemplo de uzado de klasika BPF en la kerno estas la klasigilo cls_bpf por la trafikkontrola subsistemo en Linukso, aldonita al Linukso fine de 2013 kaj koncipe anstataŭigante la antikvan cls_u32.

Tamen ni nun ne priskribos la laboron cls_bpf, ĉar el la vidpunkto de scio pri klasika BPF ĉi tio nenion donos al ni - ni jam konatiĝis kun ĉiuj funkcioj. Krome, en postaj artikoloj parolantaj pri Etendita BPF, ni renkontos ĉi tiun klasigilon pli ol unufoje.

Alia kialo por ne paroli pri uzado de klasika BPF c cls_bpf La problemo estas, ke, kompare kun Extended BPF, la aplikebleco en ĉi tiu kazo estas radikale malvastigita: klasikaj programoj ne povas ŝanĝi la enhavon de pakaĵoj kaj ne povas savi staton inter vokoj.

Do estas tempo adiaŭi klasikan BPF kaj rigardi al la estonteco.

Adiaŭ al klasika BPF

Ni rigardis kiel BPF-teknologio, evoluigita en la fruaj naŭdekaj, sukcese vivis dum kvarono de jarcento kaj ĝis la fino trovis novajn aplikojn. Tamen, simile al la transiro de stakmaŝinoj al RISC, kiu funkciis kiel impulso por la evoluo de klasika BPF, en la 32-aj jaroj ekzistis transiro de 64-bitaj ĝis XNUMX-bitaj maŝinoj kaj klasika BPF komencis malnoviĝi. Krome, la kapabloj de klasika BPF estas tre limigitaj, kaj krom la malmoderna arkitekturo - ni ne havas la kapablon konservi staton inter alvokoj al BPF-programoj, ne ekzistas ebleco de rekta uzanta interago, ne ekzistas ebleco de interagado. kun la kerno, krom legado de limigita nombro da strukturkampoj sk_buff kaj lanĉante la plej simplajn helpajn funkciojn, vi ne povas ŝanĝi la enhavon de pakoj kaj redirekti ilin.

Fakte, nuntempe ĉio, kio restas de la klasika BPF en Linukso, estas la API-interfaco, kaj ene de la kerno ĉiuj klasikaj programoj, ĉu ĝi estas socket-filtriloj aŭ sekcomp-filtriloj, estas aŭtomate tradukitaj en novan formaton, Extended BPF. (Ni parolos pri ĝuste kiel tio okazas en la sekva artikolo.)

La transiro al nova arkitekturo komenciĝis en 2013, kiam Alexey Starovoitov proponis ĝisdatigskemon de BPF. En 2014 la respondaj diakiloj komencis aperi en la kerno. Laŭ mia kompreno, la komenca plano estis nur optimumigi la arkitekturon kaj JIT-kompililon por funkcii pli efike sur 64-bitaj maŝinoj, sed anstataŭe ĉi tiuj optimumigoj markis la komencon de nova ĉapitro en Linukso-disvolviĝo.

Pliaj artikoloj en ĉi tiu serio kovros la arkitekturon kaj aplikojn de la nova teknologio, komence konata kiel interna BPF, tiam plilongigita BPF, kaj nun simple BPF.

referencoj

  1. Steven McCanne kaj Van Jacobson, "The BSD Packet Filter: A New Architecture for User-level Packet Capture (La BSD Pakaĵfiltrilo: Nova Arkitekturo por Uzantnivela Pakaĵeto-Kapto)", https://www.tcpdump.org/papers/bpf-usenix93.pdf
  2. Steven McCanne, "libpcap: Arkitekturo kaj Optimumigo-Metodologio por Pakaĵeto", https://sharkfestus.wireshark.org/sharkfest.11/presentations/McCanne-Sharkfest'11_Keynote_Address.pdf
  3. tcpdump, libpcap: https://www.tcpdump.org/
  4. IPtable U32 Match Tutorial.
  5. BPF - la forgesita bajtkodo: https://blog.cloudflare.com/bpf-the-forgotten-bytecode/
  6. Prezentante la BPF-ilon: https://blog.cloudflare.com/introducing-the-bpf-tools/
  7. bpf_cls: http://man7.org/linux/man-pages/man8/tc-bpf.8.html
  8. Sekcomp superrigardo: https://lwn.net/Articles/656307/
  9. https://github.com/torvalds/linux/blob/master/Documentation/userspace-api/seccomp_filter.rst
  10. habr: Ujoj kaj sekureco: seccomp
  11. habr: Izolante demonojn per systemd aŭ "vi ne bezonas Docker por ĉi tio!"
  12. Paul Chaignon, "strace --seccomp-bpf: rigardo sub la kapuĉo", https://fosdem.org/2020/schedule/event/debugging_strace_bpf/
  13. netsniff-ng: http://netsniff-ng.org/

fonto: www.habr.com

Aldoni komenton