BPF փոքրերի համար, մաս զրոյական՝ դասական BPF

Berkeley Packet Filters-ը (BPF) Linux միջուկի տեխնոլոգիա է, որն արդեն մի քանի տարի է, ինչ գտնվում է անգլալեզու տեխնոլոգիական հրատարակությունների առաջին էջերում: Համաժողովները լցված են BPF-ի օգտագործման և զարգացման վերաբերյալ զեկույցներով: Դեյվիդ Միլլերը՝ Linux ցանցի ենթահամակարգի սպասարկողն, իր ելույթը հրավիրում է Linux Plumbers 2018-ում «Այս խոսակցությունը XDP-ի մասին չէ» (XDP-ն BPF-ի օգտագործման մեկ դեպք է): Բրենդան Գրեգը ելույթ է ունենում վերնագրով Linux BPF գերտերություններ. Toke Høiland-Jørgensen ծիծաղում էոր միջուկն այժմ միկրոմիջուկ է։ Թոմաս Գրաֆը քարոզում է այն գաղափարը, որ BPF-ը javascript է միջուկի համար.

Habré-ում դեռևս չկա BPF-ի համակարգված նկարագրություն, և, հետևաբար, մի շարք հոդվածներում ես կփորձեմ խոսել տեխնոլոգիայի պատմության մասին, նկարագրել ճարտարապետությունն ու զարգացման գործիքները և նախանշել BPF-ի օգտագործման կիրառման և պրակտիկայի ոլորտները: Այս հոդվածը, զրոյական, շարքում, պատմում է դասական BPF-ի պատմությունն ու ճարտարապետությունը, ինչպես նաև բացահայտում է դրա գործառնական սկզբունքների գաղտնիքները: tcpdump, seccomp, strace, և շատ ավելին:

BPF-ի զարգացումը վերահսկվում է Linux ցանցային համայնքի կողմից, BPF-ի հիմնական առկա հավելվածները կապված են ցանցերի հետ և, հետևաբար, թույլտվությամբ @eucariot, ես անվանել եմ «BPF փոքրերի համար» շարքը՝ ի պատիվ մեծ սերիալի «Ցանցեր փոքրերի համար».

Կարճ դասընթաց BPF-ի պատմության մեջ (c)

Ժամանակակից BPF տեխնոլոգիան հին տեխնոլոգիայի բարելավված և ընդլայնված տարբերակն է նույն անունով, որն այժմ կոչվում է դասական BPF՝ շփոթությունից խուսափելու համար: Դասական BPF-ի հիման վրա ստեղծվել է հայտնի կոմունալ ծրագիր tcpdump, մեխանիզմ seccomp, ինչպես նաև ավելի քիչ հայտնի մոդուլներ xt_bpf համար iptables և դասակարգիչ cls_bpf. Ժամանակակից Linux-ում դասական BPF ծրագրերն ավտոմատ կերպով թարգմանվում են նոր ձևով, սակայն, օգտագործողի տեսանկյունից, API-ն մնացել է տեղում, և դասական BPF-ի նոր կիրառությունները, ինչպես կտեսնենք այս հոդվածում, դեռևս գտնվել են: Այս պատճառով, և նաև այն պատճառով, որ հետևելով Linux-ում դասական BPF-ի զարգացման պատմությանը, ավելի պարզ կդառնա, թե ինչպես և ինչու է այն վերածվել իր ժամանակակից ձևի, ես որոշեցի սկսել դասական BPF-ի մասին հոդվածով:

Անցյալ դարի ութսունականների վերջում հայտնի Լոուրենս Բերքլիի լաբորատորիայի ինժեներները հետաքրքրվեցին այն հարցով, թե ինչպես ճիշտ զտել ցանցային փաթեթները անցյալ դարի ութսունականների վերջին ժամանակակից սարքավորումների վրա: Զտման հիմնական գաղափարը, որն ի սկզբանե իրականացվել է CSPF (CMU/Stanford Packet Filter) տեխնոլոգիայի մեջ, եղել է անհարկի փաթեթները հնարավորինս շուտ զտել, այսինքն. միջուկի տարածքում, քանի որ դա խուսափում է ավելորդ տվյալների պատճենահանումից օգտվողի տարածքում: Գործարկման ժամանակի անվտանգությունն ապահովելու համար միջուկի տարածքում օգտագործողի կոդը գործարկելու համար, օգտագործվել է ավազի տուփով վիրտուալ մեքենա:

Այնուամենայնիվ, գոյություն ունեցող ֆիլտրերի վիրտուալ մեքենաները նախագծված էին աշխատելու համար stack-ի վրա հիմնված մեքենաների վրա և այնքան արդյունավետ չէին աշխատում ավելի նոր RISC մեքենաների վրա: Արդյունքում, Berkeley Labs-ի ինժեներների ջանքերով մշակվել է BPF (Berkeley Packet Filters) նոր տեխնոլոգիա, որի վիրտուալ մեքենայի ճարտարապետությունը նախագծվել է Motorola 6502 պրոցեսորի հիման վրա՝ այնպիսի հայտնի արտադրանքի աշխատուժը, ինչպիսին է. Apple II- ը կամ Իրեր. Նոր վիրտուալ մեքենան տասնյակ անգամ ավելացրել է ֆիլտրի արդյունավետությունը՝ համեմատած առկա լուծումների:

BPF մեքենայի ճարտարապետություն

Ճարտարապետությանը կծանոթանանք աշխատանքային եղանակով՝ վերլուծելով օրինակներ։ Այնուամենայնիվ, սկզբից ասենք, որ մեքենան ուներ երկու 32-բիթանոց գրանցիչներ, որոնք հասանելի էին օգտագործողին՝ կուտակիչ։ A և ինդեքսային ռեգիստր X, 64 բայթ հիշողություն (16 բառ), որը հասանելի է գրելու և հետագա ընթերցման համար, և այս օբյեկտների հետ աշխատելու հրամանների փոքր համակարգ։ Ծրագրերում առկա էին նաև պայմանական արտահայտությունների իրականացման ցատկման հրահանգներ, սակայն ծրագրի ժամանակին ավարտը երաշխավորելու համար ցատկերը կարող էին կատարվել միայն առաջ, այսինքն, մասնավորապես, արգելվում էր հանգույցներ ստեղծել:

Մեքենան գործարկելու ընդհանուր սխեման հետևյալն է. Օգտագործողը ստեղծում է ծրագիր BPF ճարտարապետության համար և օգտագործելով մի քանի միջուկի մեխանիզմը (օրինակ՝ համակարգի կանչը), բեռնում և միացնում է ծրագիրը ոմանց միջուկում իրադարձությունների գեներատորին (օրինակ, իրադարձությունը ցանցային քարտի վրա հաջորդ փաթեթի ժամանումն է): Երբ իրադարձություն է տեղի ունենում, միջուկը գործարկում է ծրագիրը (օրինակ, թարգմանիչում), և մեքենայի հիշողությունը համապատասխանում է. ոմանց միջուկի հիշողության շրջան (օրինակ՝ մուտքային փաթեթի տվյալներ):

Վերոնշյալը բավական կլինի, որպեսզի սկսենք օրինակներ դիտարկել՝ անհրաժեշտության դեպքում կծանոթանանք համակարգին և հրամանի ձևաչափին։ Եթե ​​ցանկանում եք անմիջապես ուսումնասիրել վիրտուալ մեքենայի հրամանատարական համակարգը և ծանոթանալ դրա բոլոր հնարավորություններին, ապա կարող եք կարդալ բնօրինակ հոդվածը BSD փաթեթի զտիչ և/կամ ֆայլի առաջին կեսը Documentation/networking/filter.txt միջուկի փաստաթղթերից: Բացի այդ, դուք կարող եք ուսումնասիրել շնորհանդեսը libpcapՓաթեթների գրավման ճարտարապետություն և օպտիմալացման մեթոդիկա, որում BPF-ի հեղինակներից Մաքքենը խոսում է ստեղծման պատմության մասին libpcap.

Այժմ մենք անցնում ենք քննարկելու Linux-ում դասական BPF-ի օգտագործման բոլոր նշանակալից օրինակները. tcpdump (libpcap), հատված, xt_bpf, cls_bpf.

tcpdump

BPF-ի մշակումն իրականացվել է փաթեթների զտման ճակատային մասի մշակմանը զուգահեռ՝ հայտնի օգտակար գործիք: tcpdump. Եվ քանի որ սա դասական BPF-ի օգտագործման ամենահին և ամենահայտնի օրինակն է, որը հասանելի է բազմաթիվ օպերացիոն համակարգերում, մենք կսկսենք տեխնոլոգիայի մեր ուսումնասիրությունը դրանով:

(Այս հոդվածի բոլոր օրինակները ես գործարկեցի Linux-ում 5.6.0-rc6. Որոշ հրամանների ելքը խմբագրվել է ավելի լավ ընթեռնելի համար:)

Օրինակ՝ դիտարկելով IPv6 փաթեթները

Եկեք պատկերացնենք, որ մենք ցանկանում ենք դիտարկել բոլոր IPv6 փաթեթները ինտերֆեյսի վրա eth0. Դա անելու համար մենք կարող ենք գործարկել ծրագիրը tcpdump պարզ ֆիլտրով ip6:

$ sudo tcpdump -i eth0 ip6

Այս դեպքում, tcpdump կազմում է ֆիլտրը ip6 BPF ճարտարապետության բայթկոդի մեջ և ուղարկեք այն միջուկ (տես մանրամասները բաժնում Tcpdump: բեռնում). Բեռնված ֆիլտրը կգործարկվի ինտերֆեյսի միջով անցնող յուրաքանչյուր փաթեթի համար eth0. Եթե ​​զտիչը վերադարձնում է ոչ զրոյական արժեք n, ապա մինչև n Փաթեթի բայթերը պատճենվելու են օգտագործողի տարածության մեջ, և մենք այն կտեսնենք ելքում tcpdump.

BPF փոքրերի համար, մաս զրոյական՝ դասական BPF

Պարզվում է, որ մենք հեշտությամբ կարող ենք պարզել, թե որ բայթկոդն է ուղարկվել միջուկ 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], որը նշանակում է «load into register A կես բառ (16 բիթ), որը գտնվում է 12 հասցեում» և միակ հարցն այն է, թե ինչպիսի հիշողության ենք մենք դիմում: Պատասխանն այն է, որ ժամը x սկսվում է (x+1)Վերլուծված ցանցային փաթեթի րդ բայթը: Մենք կարդում ենք փաթեթներ Ethernet ինտերֆեյսից eth0եւ այս միջոցոր փաթեթն այսպիսի տեսք ունի (պարզության համար մենք ենթադրում ենք, որ փաթեթում VLAN պիտակներ չկան).

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

Այսպիսով, հրամանը կատարելուց հետո ldh [12] գրանցամատյանում A դաշտ կլինի Ether Type — այս Ethernet շրջանակում փոխանցված փաթեթի տեսակը: 1-ին տողում մենք համեմատում ենք ռեգիստրի բովանդակությունը A (փաթեթի տեսակը) գ 0x86ddեւ այս և կա Մեզ հետաքրքրող տեսակը IPv6 է: 1-ին տողում, բացի համեմատության հրամանից, կա ևս երկու սյունակ. jt 2 и jf 3 - նշաններ, որոնց պետք է գնալ, եթե համեմատությունը հաջող լինի (A == 0x86dd) և անհաջող։ Այսպիսով, հաջող դեպքում (IPv6) գնում ենք 2-րդ տող, իսկ անհաջող դեպքում՝ 3-րդ տող: 3-րդ տողում ծրագիրը ավարտվում է 0 կոդով (փաթեթը մի պատճենեք), 2-րդ տողում ծրագիրը ավարտվում է կոդով: 262144 (պատճենեք ինձ առավելագույնը 256 կիլոբայթ փաթեթ):

Ավելի բարդ օրինակ. մենք նայում ենք TCP փաթեթներին՝ ըստ նշանակման պորտի

Տեսնենք, թե ինչ տեսք ունի զտիչը, որը պատճենում է բոլոր TCP փաթեթները նպատակակետ 666 պորտով: Մենք կդիտարկենք IPv4 դեպքը, քանի որ IPv6 գործն ավելի պարզ է: Այս օրինակն ուսումնասիրելուց հետո դուք ինքներդ կարող եք ուսումնասիրել IPv6 ֆիլտրը որպես վարժություն (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-րդ տողում մենք արդեն ստուգել ենք, որ սա IPv4 փաթեթ է (Ether Type = 0x800) և բեռնեք այն գրանցամատյանում A Փաթեթի 24-րդ բայթը: Մեր փաթեթը նման է

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

ինչը նշանակում է, որ մենք բեռնում ենք գրանցամատյանում A IP վերնագրի Protocol դաշտը, որը տրամաբանական է, քանի որ մենք ցանկանում ենք պատճենել միայն TCP փաթեթները: Մենք համեմատում ենք արձանագրությունը 0x6 (IPPROTO_TCP) 3-րդ տողում:

4-րդ և 5-րդ տողերում մենք բեռնում ենք 20 հասցեում գտնվող կիսաբառերը և օգտագործում հրամանը jset ստուգեք, արդյոք երեքից մեկը սահմանված է դրոշներ - կրելով տրված դիմակ jset երեք ամենակարևոր բիթերը մաքրվում են: Երեք բիթերից երկուսը մեզ ասում են, թե արդյոք փաթեթը մասնատված IP փաթեթի մաս է, և եթե այո, արդյոք դա վերջին հատվածն է: Երրորդ բիթը վերապահված է և պետք է լինի զրո: Մենք չենք ցանկանում ստուգել ոչ ամբողջական, ոչ էլ կոտրված փաթեթները, ուստի մենք ստուգում ենք բոլոր երեք բիթերը:

6-րդ տողն ամենահետաքրքիրն է այս ցուցակում: Արտահայտություն ldxb 4*([14]&0xf) նշանակում է, որ մենք բեռնում ենք գրանցամատյանում X Փաթեթի տասնհինգերորդ բայթի ամենաքիչ նշանակալից չորս բիթը բազմապատկված 4-ով: Տասնհինգերորդ բայթի ամենաքիչ նշանակալից չորս բիթը դաշտն է Ինտերնետ վերնագրի երկարությունը IPv4 վերնագիր, որը պահպանում է վերնագրի երկարությունը բառերով, այնպես որ դուք պետք է բազմապատկեք 4-ով: Հետաքրքիր է, որ արտահայտությունը. 4*([14]&0xf) հատուկ հասցեավորման սխեմայի նշանակում է, որը կարող է օգտագործվել միայն այս ձևով և միայն ռեգիստրի համար X, այսինքն. մենք էլ չենք կարող ասել ldb 4*([14]&0xf) ոչ ldxb 5*([14]&0xf) (մենք կարող ենք միայն նշել այլ օֆսեթ, օրինակ, ldxb 4*([16]&0xf)) Հասկանալի է, որ հասցեավորման այս սխեման ավելացվել է BPF-ին հենց ստանալու համար X (ինդեքսի ռեգիստր) IPv4 վերնագրի երկարությունը:

Այսպիսով, 7-րդ տողում մենք փորձում ենք բեռնել կես բառ (X+16). Հիշելով, որ 14 բայթը զբաղեցնում է Ethernet վերնագիրը, և X պարունակում է IPv4 վերնագրի երկարությունը, մենք հասկանում ենք, որ in A TCP նպատակակետ նավահանգիստը բեռնված է.

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

Վերջապես, 8-րդ տողում մենք համեմատում ենք նպատակակետ նավահանգիստը ցանկալի արժեքի հետ, իսկ 9-րդ կամ 10-րդ տողերում մենք վերադարձնում ենք արդյունքը՝ պատճենե՞լ փաթեթը, թե՞ ոչ:

Tcpdump: բեռնում

Նախորդ օրինակներում մենք մանրամասնորեն չենք խոսել այն մասին, թե կոնկրետ ինչպես ենք BPF բայթկոդը բեռնում միջուկում՝ փաթեթների զտման համար: Ընդհանրապես ասած, tcpdump տեղափոխվում է բազմաթիվ համակարգեր և ֆիլտրերի հետ աշխատելու համար tcpdump օգտագործում է գրադարանը libpcap. Համառոտ, ինտերֆեյսի վրա զտիչ տեղադրելու համար՝ օգտագործելով libpcap, դուք պետք է անեք հետևյալը.

Տեսնելու համար, թե ինչպես է գործում pcap_setfilter ներդրված Linux-ում, մենք օգտագործում ենք 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
...

Արդյունքների առաջին երկու տողերի վրա մենք ստեղծում ենք հում վարդակից կարդալ բոլոր Ethernet շրջանակները և կապել այն ինտերֆեյսին eth0... Սկսած մեր առաջին օրինակը մենք գիտենք, որ զտիչը ip բաղկացած կլինի չորս BPF հրահանգներից, իսկ երրորդ տողում մենք տեսնում ենք, թե ինչպես օգտագործել տարբերակը SO_ATTACH_FILTER համակարգային զանգ setsockopt մենք բեռնում և միացնում ենք 4 երկարության ֆիլտրը: Սա մեր ֆիլտրն է:

Հարկ է նշել, որ դասական BPF-ում ֆիլտրի բեռնումը և միացումը միշտ տեղի է ունենում որպես ատոմային գործողություն, իսկ BPF-ի նոր տարբերակում ծրագրի բեռնումը և իրադարձությունների գեներատորին կապելը ժամանակին առանձնացված են:

Թաքնված ճշմարտություն

Արդյունքի մի փոքր ավելի ամբողջական տարբերակն ունի հետևյալ տեսքը.

$ 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 (թողնել բոլոր փաթեթները), միացնում է վարդակը չարգելափակման ռեժիմի և փորձում է հանել բոլոր փաթեթները, որոնք կարող էին մնալ նախորդ զտիչներից:

Ընդհանուր առմամբ, Linux-ում դասական BPF-ի միջոցով փաթեթները զտելու համար հարկավոր է ունենալ ֆիլտր այնպիսի կառուցվածքի տեսքով, ինչպիսին է. struct sock_fprog և բաց վարդակից, որից հետո ֆիլտրը կարելի է միացնել վարդակից՝ օգտագործելով համակարգային զանգ setsockopt.

Հետաքրքիր է, որ ֆիլտրը կարող է կցվել ցանկացած վարդակից, ոչ միայն հում վիճակում: Այստեղ օրինակ ծրագիր, որը կտրում է բոլորը, բացառությամբ առաջին երկու բայթերի բոլոր մուտքային UDP տվյալների գրամներից: (Կոդում մեկնաբանություններ եմ ավելացրել՝ հոդվածը չխառնելու համար):

Լրացուցիչ մանրամասներ օգտագործման մասին setsockopt ֆիլտրերի միացման համար տես վարդակից (7), բայց ձեր նման ֆիլտրերը գրելու մասին struct sock_fprog առանց օգնության tcpdump մենք կխոսենք բաժնում BPF-ի ծրագրավորում մեր սեփական ձեռքերով.

Դասական BPF և XNUMX-րդ դար

BPF-ն ընդգրկվել է Linux-ում 1997 թվականին և երկար ժամանակ մնացել է որպես աշխատուժ libpcap առանց հատուկ փոփոխությունների (իհարկե, Linux-ի հատուկ փոփոխություններ, Մենք էինք, բայց դրանք չփոխեցին համաշխարհային պատկերը)։ BPF-ի զարգացման առաջին լուրջ նշանները եղան 2011 թվականին, երբ Էրիկ Դումազետն առաջարկեց. patch, որն ավելացնում է Just In Time Compiler-ը միջուկում՝ թարգմանիչ՝ BPF բայթկոդը մայրենիի փոխակերպելու համար x86_64 ծածկագիրը։

JIT կոմպիլյատորը փոփոխությունների շղթայում առաջինն էր՝ 2012թ հայտնվեց համար ֆիլտրեր գրելու ունակություն seccomp, օգտագործելով BPF, 2013 թվականի հունվարին եղել է ավելացրեց մոդուլը xt_bpf, որը թույլ է տալիս կանոններ գրել iptables BPF-ի օգնությամբ, իսկ հոկտեմբերին 2013թ ավելացրեց նաև մոդուլ cls_bpf, որը թույլ է տալիս գրել երթևեկության դասակարգիչներ՝ օգտագործելով BPF:

Շուտով մենք ավելի մանրամասն կանդրադառնանք այս բոլոր օրինակներին, բայց նախ մեզ համար օգտակար կլինի սովորել, թե ինչպես գրել և կազմել կամայական ծրագրեր BPF-ի համար, քանի որ գրադարանի տրամադրած հնարավորությունները. libpcap սահմանափակ (պարզ օրինակ. ստեղծվել է ֆիլտր libpcap կարող է վերադարձնել միայն երկու արժեք՝ 0 կամ 0x40000) կամ ընդհանրապես, ինչպես seccomp-ի դեպքում, կիրառելի չեն:

BPF-ի ծրագրավորում մեր սեփական ձեռքերով

Եկեք ծանոթանանք BPF հրահանգների երկուական ձևաչափին, այն շատ պարզ է.

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

Յուրաքանչյուր հրահանգ զբաղեցնում է 64 բիթ, որոնցում առաջին 16 բիթերը հրահանգի կոդը են, այնուհետև կան երկու ութ բիթանոց նահանջներ, jt и jf, և 32 բիթ արգումենտի համար K, որի նպատակը տարբերվում է հրամանից հրաման։ Օրինակ, հրամանը ret, որը դադարեցնում է ծրագիրը ունի կոդը 6, իսկ վերադարձի արժեքը վերցված է հաստատունից K. C-ում մեկ BPF հրահանգը ներկայացված է որպես կառուցվածք

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

եւ ամբողջ ծրագիրը կառույցի տեսքով է

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

Այսպիսով, մենք արդեն կարող ենք գրել ծրագրեր (օրինակ, մենք գիտենք հրահանգների կոդերը [1]). Ահա թե ինչպիսի տեսք կունենա ֆիլտրը 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))

Մեքենայի կոդերի տեսքով ծրագրեր գրելն այնքան էլ հարմար չէ, բայց երբեմն դա անհրաժեշտ է (օրինակ՝ վրիպազերծման, միավորի թեստեր ստեղծելու, Habré-ում հոդվածներ գրելու և այլն): Հարմարության համար ֆայլում <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),
}

Այնուամենայնիվ, այս տարբերակը այնքան էլ հարմար չէ: Սա այն է, ինչ պատճառաբանել են Linux միջուկի ծրագրավորողները, և հետևաբար գրացուցակում tools/bpf միջուկները կարող եք գտնել assembler և debugger դասական BPF-ի հետ աշխատելու համար:

Ասամբլեայի լեզուն շատ նման է վրիպազերծման ելքին tcpdump, բայց ի լրումն մենք կարող ենք նշել խորհրդանշական պիտակներ։ Օրինակ, ահա մի ծրագիր, որը բաց է թողնում բոլոր փաթեթները, բացի TCP/IPv4-ից.

$ 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>,..., մեր օրինակի համար TCP-ով դա կլինի

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

C ծրագրավորողների հարմարության համար կարող է օգտագործվել ելքային այլ ձևաչափ.

$ 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, ինչպես արեցինք այս բաժնի սկզբում:

Linux և netsniff-ng ընդլայնումներ

Բացի ստանդարտ BPF-ից, Linux-ից և tools/bpf/bpf_asm աջակցություն և ոչ ստանդարտ հավաքածու. Հիմնականում հրահանգները օգտագործվում են կառուցվածքի դաշտերը մուտք գործելու համար struct sk_buff, որը նկարագրում է ցանցի փաթեթը միջուկում: Այնուամենայնիվ, կան նաև այլ տեսակի օգնական հրահանգներ, օրինակ ldw cpu կներբեռնվի գրանցամատյանում A միջուկի ֆունկցիայի գործարկման արդյունք raw_smp_processor_id(). (BPF-ի նոր տարբերակում այս ոչ ստանդարտ ընդլայնումները ընդլայնվել են՝ ծրագրերին տրամադրելու միջուկի օգնականների հավաքածու՝ հիշողություն, կառուցվածքներ և իրադարձություններ ստեղծելու համար:) Ահա ֆիլտրի հետաքրքիր օրինակ, որում մենք պատճենում ենք միայն փաթեթի վերնագրերը օգտագործողի տարածության մեջ՝ օգտագործելով ընդլայնումը poff, օգտակար բեռի օֆսեթ:

ld poff
ret a

BPF ընդլայնումները չեն կարող օգտագործվել tcpdump, բայց սա լավ առիթ է ծանոթանալու կոմունալ փաթեթին netsniff-ng, որը, ի թիվս այլ բաների, պարունակում է առաջադեմ ծրագիր netsniff-ng, որը, բացի BPF-ի միջոցով զտելուց, պարունակում է նաև արդյունավետ երթևեկության գեներատոր և ավելի առաջադեմ, քան tools/bpf/bpf_asm, զանգահարեց BPF հավաքողը bpfc. Փաթեթը պարունակում է բավականին մանրամասն փաստաթղթեր, տես նաև հոդվածի վերջում գտնվող հղումները։

seccomp

Այսպիսով, մենք արդեն գիտենք, թե ինչպես գրել BPF կամայական բարդության ծրագրեր և պատրաստ ենք նայելու նոր օրինակներ, որոնցից առաջինը seccomp տեխնոլոգիան է, որը թույլ է տալիս, օգտագործելով BPF ֆիլտրերը, կառավարել համակարգային կանչերի արգումենտների հավաքածուն և փաթեթը, որոնք հասանելի են: տվյալ գործընթացն ու դրա հետնորդները։

seccomp-ի առաջին տարբերակը ավելացվել է միջուկին 2005 թվականին և այնքան էլ տարածված չէր, քանի որ այն տրամադրում էր միայն մեկ տարբերակ՝ սահմանափակել գործընթացին հասանելի համակարգային զանգերի շարքը հետևյալով. read, write, exit и sigreturn, իսկ կանոնները խախտող գործընթացը սպանվեց՝ օգտագործելով SIGKILL. Այնուամենայնիվ, 2012-ին seccomp-ը ավելացրեց BPF ֆիլտրերի օգտագործման հնարավորությունը՝ թույլ տալով սահմանել թույլատրված համակարգային զանգերի մի շարք և նույնիսկ ստուգել դրանց փաստարկները: (Հետաքրքիր է, որ Chrome-ն այս ֆունկցիոնալության առաջին օգտագործողներից էր, և Chrome-ի մարդիկ ներկայումս մշակում են KRSI մեխանիզմ՝ հիմնված BPF-ի նոր տարբերակի վրա և թույլ է տալիս հարմարեցնել Linux-ի անվտանգության մոդուլները:) Լրացուցիչ փաստաթղթերի հղումները կարող եք գտնել վերջում: հոդվածի։

Նկատի ունեցեք, որ հանգույցում արդեն եղել են հոդվածներ seccomp-ի օգտագործման մասին, գուցե ինչ-որ մեկը ցանկանա կարդալ դրանք նախքան (կամ փոխարենը) կարդալ հետևյալ ենթաբաժինները: Հոդվածում Կոնտեյներներ և անվտանգություն՝ seccomp ներկայացնում է seccomp-ի օգտագործման օրինակներ՝ և՛ 2007 թվականի տարբերակը, և՛ BPF-ն օգտագործող տարբերակը (ֆիլտրերը ստեղծվում են libseccomp-ի միջոցով), խոսում է seccomp-ի միացման մասին Docker-ի հետ, ինչպես նաև տրամադրում է բազմաթիվ օգտակար հղումներ: Հոդվածում Դևոնների մեկուսացում systemd-ով կամ «դուքերի կարիք չունես դրա համար»: Այն ընդգրկում է, մասնավորապես, թե ինչպես ավելացնել սև ցուցակներ կամ համակարգային զանգերի սպիտակ ցուցակներ systemd-ով աշխատող դևերի համար:

Հաջորդը մենք կտեսնենք, թե ինչպես գրել և բեռնել ֆիլտրերը seccomp մերկ C-ում և օգտվելով գրադարանից libseccomp և որո՞նք են յուրաքանչյուր տարբերակի առավելություններն ու թերությունները, և վերջապես, տեսնենք, թե ինչպես է seccomp-ը օգտագործվում ծրագրի կողմից strace.

Seccomp-ի համար զտիչներ գրելը և բեռնելը

Մենք արդեն գիտենք, թե ինչպես գրել BPF ծրագրեր, ուստի եկեք նախ նայենք seccomp ծրագրավորման ինտերֆեյսին: Դուք կարող եք զտիչ սահմանել գործընթացի մակարդակում, և բոլոր մանկական գործընթացները կժառանգեն սահմանափակումները: Դա արվում է համակարգային զանգի միջոցով seccomp(2):

seccomp(SECCOMP_SET_MODE_FILTER, flags, &filter)

որտեղ &filter - Սա մեզ արդեն ծանոթ կառույցի ցուցիչ է struct sock_fprog, այսինքն. BPF ծրագիր.

Ինչպե՞ս են seccomp-ի ծրագրերը տարբերվում վարդակների համար նախատեսված ծրագրերից: Փոխանցված համատեքստ. Սոկետների դեպքում մեզ տրվեց հիշողության տարածք, որը պարունակում էր փաթեթը, իսկ seccomp-ի դեպքում մեզ տրվեց նման կառուցվածք.

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

Այստեղ nr գործարկվող համակարգային զանգի համարն է, arch - ընթացիկ ճարտարապետություն (այս մասին ավելին ստորև), args - մինչև վեց համակարգային կանչի փաստարկներ և instruction_pointer ցուցիչ է դեպի օգտվողի տարածքի հրահանգը, որը ստիպել է համակարգը զանգահարել: Այսպես, օրինակ, համակարգային զանգի համարը գրանցամատյանում բեռնելու համար A մենք պետք է ասենք

ldw [0]

Կան նաև այլ հնարավորություններ seccomp ծրագրերի համար, օրինակ, համատեքստը հասանելի է միայն 32-բիթանոց հավասարեցմամբ, և դուք չեք կարող բեռնել կես բառ կամ բայթ, երբ փորձում եք բեռնել ֆիլտրը: ldh [0] համակարգային զանգ seccomp կվերադառնա EINVAL. Գործառույթը ստուգում է բեռնված ֆիլտրերը seccomp_check_filter() միջուկներ. (Զվարճալի բանն այն է, որ սկզբնական commit-ում, որն ավելացրել է seccomp ֆունկցիոնալությունը, նրանք մոռացել են թույլտվություն ավելացնել այս ֆունկցիային օգտագործելու հրահանգը mod (բաժանման մնացորդը) և այժմ անհասանելի է seccomp BPF ծրագրերի համար, քանի որ դրա ավելացումը կկոտրվի ABI.)

Հիմնականում մենք արդեն ամեն ինչ գիտենք seccomp ծրագրեր գրելու և կարդալու համար: Սովորաբար ծրագրի տրամաբանությունը դասավորվում է որպես համակարգային զանգերի սպիտակ կամ սև ցուցակ, օրինակ՝ ծրագիրը

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 համարներով: Որո՞նք են այդ համակարգային զանգերը: Հստակ չենք կարող ասել, քանի որ չգիտենք, թե որ ճարտարապետության համար է գրվել ծրագիրը։ Հետեւաբար, seccomp-ի հեղինակները առաջարկ սկսել բոլոր ծրագրերը ճարտարապետության ստուգմամբ (ներկայիս ճարտարապետությունը համատեքստում նշված է որպես դաշտ arch կառուցվածքներ struct seccomp_data) Ստուգված ճարտարապետության դեպքում օրինակի սկիզբը նման կլինի.

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

և այնուհետև մեր համակարգի զանգերի համարները կստանան որոշակի արժեքներ:

Մենք գրում և բեռնում ենք զտիչներ seccomp-ի համար՝ օգտագործելով libseccomp

Զտիչներ գրելը հայրենի կոդով կամ BPF հավաքում թույլ է տալիս լիարժեք վերահսկողություն ունենալ արդյունքի վրա, բայց միևնույն ժամանակ երբեմն նախընտրելի է ունենալ շարժական և/կամ ընթեռնելի կոդ: Այս հարցում մեզ կօգնի գրադարանը 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, այս դեպքում seccomp-ը ազդանշան կուղարկի գործընթացին 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, ոչ տրիվիալ ծածկագիրը չորս տողում տեղավորելով: Վերևի օրինակում, եթե կան մեծ թվով համակարգային զանգեր, կատարման ժամանակը կարող է նկատելիորեն կրճատվել, քանի որ ստուգումը պարզապես համեմատությունների ցանկ է: Օպտիմալացման համար libseccomp-ը վերջերս ուներ կարկատել ներառված, որն ավելացնում է ֆիլտրի հատկանիշի աջակցությունը SCMP_FLTATR_CTL_OPTIMIZE. Այս հատկանիշը 2-ի սահմանելով՝ զտիչը կվերածվի երկուական որոնման ծրագրի:

Եթե ​​ցանկանում եք տեսնել, թե ինչպես են աշխատում երկուական որոնման ֆիլտրերը, նայեք պարզ սցենար, որը ստեղծում է նման ծրագրեր BPF assembler-ում՝ հավաքելով համակարգի զանգերի համարները, օրինակ.

$ 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

Անհնար է ինչ-որ բան զգալիորեն ավելի արագ գրել, քանի որ BPF ծրագրերը չեն կարող կատարել ներքևի թռիչքներ (մենք չենք կարող անել, օրինակ. jmp A կամ jmp [label+X]) և հետևաբար բոլոր անցումները ստատիկ են:

seccomp և strace

Բոլորը գիտեն օգտակարությունը strace Linux-ում պրոցեսների վարքագիծն ուսումնասիրելու անփոխարինելի գործիք է։ Այնուամենայնիվ, շատերը նույնպես լսել են դրա մասին կատարողականի խնդիրներ այս կոմունալից օգտվելիս: Փաստն այն է, որ 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, և այլն։

BPF փոքրերի համար, մաս զրոյական՝ դասական BPF

Այնուամենայնիվ, seccomp-ով այս գործընթացը կարող է օպտիմիզացվել այնպես, ինչպես մենք կցանկանայինք: Այսինքն, եթե ուզում ենք նայել միայն համակարգային զանգին X, ապա մենք կարող ենք գրել BPF ֆիլտր, որի համար X վերադարձնում է արժեքը SECCOMP_RET_TRACEև մեզ չհետաքրքրող զանգերի համար՝ SECCOMP_RET_ALLOW:

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

Այս դեպքում strace սկզբում սկսում է գործընթացը որպես PTRACE_CONT, մեր զտիչը մշակվում է յուրաքանչյուր համակարգային զանգի համար, եթե համակարգային զանգը չէ X, ապա գործընթացը շարունակում է գործել, բայց եթե սա X, ապա seccomp-ը կփոխանցի վերահսկողությունը straceորը կնայի փաստարկներին և կսկսի գործընթացը, ինչպես PTRACE_SYSCALL (քանի որ seccomp-ը համակարգային զանգից դուրս գալու դեպքում ծրագիր գործարկելու հնարավորություն չունի): Երբ համակարգի զանգը վերադառնում է, strace կվերսկսի գործընթացը՝ օգտագործելով PTRACE_CONT և կսպասի նոր հաղորդագրությունների seccomp-ից:

BPF փոքրերի համար, մաս զրոյական՝ դասական BPF

Տարբերակն օգտագործելիս --seccomp-bpf կա երկու սահմանափակում. Նախ, հնարավոր չի լինի միանալ արդեն գոյություն ունեցող գործընթացին (տարբերակ -p ծրագրեր strace), քանի որ սա չի աջակցվում seccomp-ի կողմից: Երկրորդ՝ հնարավորություն չկա ոչ նայեք երեխայի գործընթացներին, քանի որ seccomp ֆիլտրերը ժառանգվում են բոլոր երեխա գործընթացների կողմից՝ առանց սա անջատելու հնարավորության:

Մի փոքր ավելի մանրամասն, թե ինչպես ճիշտ strace աշխատում է seccomp կարելի է գտնել վերջին զեկույցը. Մեզ համար ամենահետաքրքիր փաստն այն է, որ seccomp-ի կողմից ներկայացված դասական BPF-ն այսօր էլ օգտագործվում է։

xt_bpf

Հիմա վերադառնանք ցանցերի աշխարհ:

Նախապատմություն. շատ վաղուց՝ 2007 թվականին, միջուկն էր ավելացրեց մոդուլը xt_u32 ցանցային ֆիլտրի համար: Այն գրվել է անալոգիայով ավելի հին երթևեկության դասակարգչի հետ cls_u32 և թույլ տվեց գրել կամայական երկուական կանոններ iptable-ների համար՝ օգտագործելով հետևյալ պարզ գործողությունները. բեռնել 32 բիթ փաթեթից և կատարել թվաբանական գործողություններ դրանց վրա: Օրինակ,

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

Բեռնում է IP-ի վերնագրի 32 բիթերը՝ սկսած 6-րդ ներդիրից, և դրանց վրա կիրառում է դիմակ 0xFF (վերցրեք ցածր բայթը): Այս դաշտը protocol IP վերնագիր և մենք այն համեմատում ենք 1-ի հետ (ICMP): Մեկ կանոնով դուք կարող եք միավորել բազմաթիվ ստուգումներ, ինչպես նաև կարող եք կատարել օպերատորը @ — տեղափոխել X բայթ դեպի աջ: Օրինակ՝ կանոնը

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

ստուգում է, արդյոք TCP հաջորդականության համարը հավասար չէ 0x29. Ավելի մանրամասն չեմ մանրամասնի, քանի որ արդեն պարզ է, որ նման կանոնները ձեռքով գրելն այնքան էլ հարմար չէ։ Հոդվածում BPF - մոռացված բայթ կոդը, կան մի քանի հղումներ՝ օգտագործման և կանոնների ստեղծման օրինակներով xt_u32. Տես նաև այս հոդվածի վերջում գտնվող հղումները:

2013 թվականից մոդուլի փոխարեն xt_u32 դուք կարող եք օգտագործել BPF-ի վրա հիմնված մոդուլ xt_bpf. Յուրաքանչյուր ոք, ով կարդացել է այսքանը, արդեն պետք է հստակ իմանա դրա գործողության սկզբունքը. գործարկեք BPF բայթկոդը, քանի որ կանոնները iptables են: Դուք կարող եք ստեղծել նոր կանոն, օրինակ, այսպես.

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

Այս օրինակում մենք զտում ենք բոլոր UDP փաթեթները: BPF ծրագրի համատեքստը մոդուլում xt_bpf, իհարկե, մատնանշում է փաթեթի տվյալները, iptables-ի դեպքում՝ IPv4 վերնագրի սկիզբը։ Վերադարձի արժեքը BPF ծրագրից բուլյանՈրտեղ false նշանակում է, որ փաթեթը չի համընկնում:

Հասկանալի է, որ մոդուլը xt_bpf աջակցում է ավելի բարդ զտիչներ, քան վերը նշված օրինակը: Եկեք նայենք իրական օրինակներին Cloudfare-ից: Մինչեւ վերջերս նրանք օգտագործում էին մոդուլը xt_bpf պաշտպանել DDoS հարձակումներից: Հոդվածում Ներկայացնելով BPF գործիքները նրանք բացատրում են, թե ինչպես (և ինչու) նրանք ստեղծում են BPF ֆիլտրեր և հրապարակում են նման ֆիլտրերի ստեղծման կոմունալ ծառայությունների մի շարք հղումներ: Օրինակ, օգտագործելով կոմունալ bpfgen դուք կարող եք ստեղծել BPF ծրագիր, որը համապատասխանում է անվանման DNS հարցումին 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 UDP տվյալների գրամի ներսում և այնուհետև ստուգեք հարցումը. 0x04686162 <-> "x04hab" եւ այլն:

Քիչ անց Cloudfare-ը հրապարակեց p0f -> BPF կոմպիլյատորի կոդը։ Հոդվածում Ներկայացնում ենք p0f BPF կոմպիլյատորը նրանք խոսում են այն մասին, թե ինչ է p0f-ը և ինչպես փոխարկել p0f ստորագրությունները 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,
...

Ներկայումս այլևս չի օգտագործում Cloudfare-ը xt_bpf, քանի որ նրանք տեղափոխվել են XDP - BPF-ի նոր տարբերակի օգտագործման տարբերակներից մեկը, տես. L4Drop՝ XDP DDoS մեղմացումներ.

cls_bpf

Միջուկում դասական BPF-ի օգտագործման վերջին օրինակը դասակարգիչն է cls_bpf Linux-ում երթևեկության վերահսկման ենթահամակարգի համար, որը ավելացվել է Linux-ին 2013-ի վերջին և կոնցեպտուալ կերպով փոխարինելով հնագույնը cls_u32.

Այնուամենայնիվ, մենք այժմ չենք նկարագրի աշխատանքը cls_bpf, քանի որ դասական BPF-ի մասին գիտելիքների տեսանկյունից դա մեզ ոչինչ չի տա, մենք արդեն ծանոթացել ենք բոլոր ֆունկցիոնալությանը: Բացի այդ, հետագա հոդվածներում, որոնք խոսում են Extended BPF-ի մասին, մենք կհանդիպենք այս դասակարգիչին մեկից ավելի անգամ:

Մեկ այլ պատճառ՝ չխոսելու դասական BPF-ի օգտագործման մասին c cls_bpf Խնդիրն այն է, որ, համեմատած Extended BPF-ի հետ, այս դեպքում կիրառելիության շրջանակը արմատապես նեղացել է. դասական ծրագրերը չեն կարող փոխել փաթեթների բովանդակությունը և չեն կարող պահպանել վիճակը զանգերի միջև:

Այսպիսով, ժամանակն է հրաժեշտ տալ դասական BPF-ին և նայել ապագային:

Հրաժեշտ դասական BPF-ին

Մենք նայեցինք, թե ինչպես իննսունականների սկզբին զարգացած BPF տեխնոլոգիան հաջողությամբ ապրեց քառորդ դար և մինչև վերջ գտավ նոր կիրառություններ: Այնուամենայնիվ, ինչպես stack մեքենաներից դեպի RISC անցումը, որը խթան հանդիսացավ դասական BPF-ի զարգացման համար, 32-ականներին տեղի ունեցավ անցում 64-բիթանոցից XNUMX-բիթանոց մեքենաների, և դասական BPF-ն սկսեց հնանալ: Բացի այդ, դասական BPF-ի հնարավորությունները շատ սահմանափակ են, և, ի լրումն հնացած ճարտարապետության, մենք հնարավորություն չունենք BPF-ի ծրագրերին զանգերի միջև պահելու վիճակը, չկա ուղղակի օգտագործողի փոխազդեցության հնարավորություն, չկա փոխազդեցության հնարավորություն: միջուկով, բացառությամբ սահմանափակ թվով կառուցվածքային դաշտերի ընթերցման sk_buff և գործարկելով ամենապարզ օգնական գործառույթները, դուք չեք կարող փոխել փաթեթների բովանդակությունը և վերահղել դրանք:

Փաստորեն, ներկայումս այն ամենը, ինչ մնում է դասական BPF-ից Linux-ում, դա API ինտերֆեյսն է, և միջուկի ներսում բոլոր դասական ծրագրերը, լինեն դա վարդակից ֆիլտրեր կամ seccomp ֆիլտրեր, ավտոմատ կերպով թարգմանվում են նոր ձևաչափով՝ Extended BPF: (Այն մասին, թե կոնկրետ ինչպես է դա տեղի ունենում, մենք կխոսենք հաջորդ հոդվածում):

Անցումը դեպի նոր ճարտարապետություն սկսվեց 2013 թվականին, երբ Ալեքսեյ Ստարովոյտովը առաջարկեց BPF-ի թարմացման սխեման: 2014 թվականին համապատասխան կարկատանները սկսեց հայտնվել միջուկում։ Որքան ես հասկացա, նախնական պլանը միայն ճարտարապետությունն ու JIT կոմպիլյատորը օպտիմալացնելն էր 64-բիթանոց մեքենաների վրա ավելի արդյունավետ աշխատելու համար, բայց փոխարենը այս օպտիմալացումները նշանավորեցին Linux-ի զարգացման նոր գլխի սկիզբը:

Այս շարքի հետագա հոդվածները կներառեն նոր տեխնոլոգիայի ճարտարապետությունն ու կիրառությունները, որոնք սկզբում հայտնի էին որպես ներքին BPF, այնուհետև ընդլայնված BPF, իսկ այժմ պարզապես BPF:

Սայլակ

  1. Սթիվեն ՄաքՔեն և Վան Ջեյքոբսոն, «BSD փաթեթի զտիչ. նոր ճարտարապետություն օգտագործողի մակարդակի փաթեթների գրավման համար», https://www.tcpdump.org/papers/bpf-usenix93.pdf
  2. Սթիվեն ՄաքՔեն, «libpcap. Փաթեթների գրավման ճարտարապետություն և օպտիմալացման մեթոդիկա», https://sharkfestus.wireshark.org/sharkfest.11/presentations/McCanne-Sharkfest'11_Keynote_Address.pdf
  3. tcpdump, libpcap: https://www.tcpdump.org/
  4. IPtable U32 Match ձեռնարկ.
  5. BPF - մոռացված բայթ կոդը. https://blog.cloudflare.com/bpf-the-forgotten-bytecode/
  6. Ներկայացնելով BPF գործիքը. 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. habr: Կոնտեյներներ և անվտանգություն: seccomp
  11. habr. Դևոնների մեկուսացում systemd-ով կամ «դուքերի կարիք չունես դրա համար»:
  12. Փոլ Շեյնյոն, «strace --seccomp-bpf. հայացք գլխարկի տակ», https://fosdem.org/2020/schedule/event/debugging_strace_bpf/
  13. netsniff-ng: http://netsniff-ng.org/

Source: www.habr.com

Добавить комментарий