Berkeley Packet Filters (BPF) — bu, artıq bir neçə ildir ki, ingilisdilli texniki nəşrlərin ilk səhifələrindən düşməyən Linux nüvəsinin bir texnologiyasıdır. Konfranslar BPF-nin istifadəsi və inkişafı ilə bağlı təqdimatlarla doludur. Linux şəbəkə alt sisteminin saxlayıcısı David Miller, Linux Plumbers 2018-dəki təqdimatını (XDP — BPF-nin istifadə variantlarından biridir). Brendan Gregg, " . Toke Høiland-Jørgensen , indi nüvənin mikro nüvəyə çevrildiyini bildirir. Thomas Graf, .
Hələ də Habrda BPF haqqında sistemli bir təsvir yoxdur, buna görə də bir sıra məqalələrdə bu texnologiyanın tarixini, arxitekturasını və inkişaf vasitələrini izah etməyə çalışacağam, BPF-nin tətbiq sahələrini və istifadə praktikalarını qeyd edəcəyəm. Bu dövrün sıfırıncı məqaləsində, klassik BPF-nin tarixi və arxitekturası, eləcə də tcpdump, seccomp, strace, və bir çox şeylər izah edilir.
BPF inkişafı Linux şəbəkə icması tərəfindən idarə olunur, BPF-nin mövcud əsas tətbiqləri şəbəkələrlə bağlıdır, buna görə də, icazə ilə , bu seriyaya "BPF ən kiçiklər üçün" adını verdim, böyük seriyaya hörmət olaraq .
BPF-nin qısa tarix kursu (c)
Müasir BPF texnologiyası, indiki bir adla, lakin qarışıqlığın qarşısını almaq məqsədilə, klassik BPF adlanan köhnə bir texnologiyanın təkmilləşdirilmiş və genişləndirilmiş versiyasıdır. Klassik BPF əsasında tanınmış alət tcpdump, mexanizm seccomp, eləcə də daha az tanınmış modul xt_bpf üçün iptables və təsnifatçı cls_bpf. Müasir Linux-da klassik BPF proqramları avtomatik olaraq yeni formaya çevrilir, lakin istifadəçi baxımından, API olduğu kimi qalmışdır və klassik BPF-nin yeni tətbiqləri, bu məqalədə görəcəyimiz kimi, hələ də vardır. Buna görə də, həmçinin klassik BPF-nin Linux-dakı inkişaf tarixini izləmək, onun müasir formaya necə və niyə evrildiyini anlamağa kömək edəcək, bu səbəbdən məhz klassik BPF haqqında bir məqalə ilə başlamağa qərar verdim.
Sovet dövrünün sonlarında, 1980-ci illərin sonunda, məşhur Lawrence Berkeley Laboratory-dan mühəndislər, o dövr üçün müasir avadanlıqda şəbəkə paketlərini necə düzgün filtrləmək məsələsinə maraq göstərdilər. Filtrasiyanın əsas ideyası, ilkin olaraq CSPF (CMU/Stanford Packet Filter) texnologiyasında həyata keçirilmişdir, mümkün qədər əvvəl, yəni nüvə qoynunda artıq paketləri filtrləmək idi, çünki bu, artıq məlumatları istifadəçi qoynuna kopyalamağa imkan vermir. Nüvə qoynunda istifadəçi kodunu idarə etmək üçün icra müddətinin təhlükəsizliyini təmin etmək üçün virtual maşın — sandbox istifadə edilmişdir.
Ancaq mövcud filtr üçün virtual maşınlar yığın arxitekturasında işləmək üçün layihələndirilmişdi və yeni RISC maşınları üzərində effektiv çalışmırdı. Nəticədə Berkeley Labs-dan mühəndislərin səyləri ilə BPF (Berkeley Paket Filtrləri) yeni texnologiyası hazırlanmışdır və onun virtual maşın arxitekturası Motorola 6502 prosessoruna əsaslanaraq layihələndirilmişdir — bu prosessor tanınmış məhsulların iş atasıdır. və . Yeni virtual maşın mövcud həllərlə müqayisədə filtrin məhsuldarlığını dəfələrlə artırdı.
BPF maşın arxitekturası
Biz işçi şəkildə arxitektura ilə tanış olacağıq, nümunələri araşdıraraq. Ancaq əvvəlcə qeyd edək ki, maşında istifadəçilər üçün iki 32-bit qeyd var, akkumulyator A və indeks qeydidir X, yazma və daha sonra oxumaq üçün əlçatan 64 bayt (16 söz) yaddaş və bu obyektlərlə işləmək üçün kiçik bir əmrlər sistemi var. Proqramlarda şərti ifadələrin icrası üçün keçid əmrləri də mövcuddur, lakin proqramın zamanında bitməsini təmin etmək üçün yalnız irəlilədikcə keçid etmək mümkündür, yəni, xüsusilə döngələr yaratmağa icazə verilmir.
Maşının işə salma qrafiki belədir. İstifadəçi BPF arxitekturası üçün bir proqram yaradır və hansısa nüvənin mexanizmi (məsələn, sistem çağırışı) vasitəsilə proqramı yükləyir və onu hansısa nüvədəki hadisə generatoruna (məsələn, hadisə — şəbəkə kartına gələn paket) qoşur. Hadisə baş verdikdə, nüvə proqramı işə salır (məsələn, interpretator daxilində) və maşının yaddaşı hansısa nüvə yaddaşının bölgəsinə (məsələn, gələn paketin məlumatları) uyğun gəlir.
Yuxarıda qeyd etdiklərimiz nümunələri araşdırmağa başlamaq üçün kifayətdir: sistem və əmrlər formatı ilə tanış olacağıq. Lakin virtual maşının əmrlər sistemini öyrənmək və onun bütün imkanlarını öyrənmək istəyirsinizsə, orijinal məqaləni oxumaq olar. və ya nüvənin sənədləşdirilməsindən. Bununla yanaşı, təqdimatla tanış ola bilərsiniz , burada McCanne, BPF-nin müəlliflərindən biri, yaradılışın tarixini danışır. libpcap.
İndi Linux-da klassik BPF-nin əhəmiyyətli tətbiq nümunələrini araşdırmağa keçir: tcpdump (libpcap), seccomp, xt_bpf, cls_bpf.
tcpdump
BPF-nin inkişafı paketləri filtr etmək üçün ön yüzün inkişafı ilə paralel aparılır — tanınmış utilita tcpdump. Və bu klassik BPF istifadəsinin ən qədim və tanınmış nümunəsi olduğundan, texnologiyaya burada öyrənməyə başlayacağıq.
(Bu məqalədəki bütün nümunələri mən Linux-da işlətdim, 5.6.0-rc6. Bəzi əmrlərin çıxışı daha yaxşı oxunaqlılıq üçün redaktə edilmişdir.)
Müqayisə: IPv6 paketlərinə baxırıq
Təsəvvür edin ki, interfeysdə bütün IPv6 paketlərinə baxmaq istəyirik eth0. Bunun üçün biz proqramı işə sala bilərik tcpdump sadə filtr ilə ip6:
$ sudo tcpdump -i eth0 ip6Bu halda tcpdump filtri ip6 BPF arxitekturasının bayt koduna tərtib edəcək və onu nüvəyə göndərəcək (detalları 'Tcpdump: yüklənməsi' bölməsində baxın) . Əgər filtr sıfırdan fərqli dəyər qaytararsa eth0, onda npaketin n baytları istifadəçi mühitinə kopyalanacaq və nəticədə biz onu çıxışda görəcəyik tcpdump.

Məlum olur ki, biz nüvəyə göndərilən bayt kodunu asanlıqla öyrənə bilərik tcpdump özünün köməyi ilə tcpdump, əgər onu -d:
$ sudo tcpdump -i eth0 -d ip6
(000) ldh [12]
(001) jeq #0x86dd jt 2 jf 3
(002) ret #262144
(003) ret #0Sıfırıncı sırada biz ldh [12]əmrlisini işə salırıq ki, bu da "12-ci ünvan üzrə yarım söz (16 bit) registrə yüklə" kimi açıqlanır və tək sual budur ki, hansı yaddaşı ünvanlayırıq? Cavab odur ki, ünvan üzrə A analiz olunan şəbəkə paketinin x başlanğıcıdır (x+1)-ci bayt. Ethernet interfeysindən paketləri oxuyuruq eth0, bu da , paketin aşağıdakı şəkildə görünməsidir (sadəlik üçün VLAN etiketləri olmadığını qəbul edək):
6 6 2
|Təyinat MAC|Mənbə MAC|Ether Type|...|Deməli, bu əmri icra etdikdən sonra ldh [12] registrdə A şöbə Ether Type — bu Ethernet çərçivəsində göndərilən paket növü. 1-ci sırada biz registrin məzmununu A (paket tipi) 0x86dd ilə müqayisə edirik və bu da, bu da jt 2 jf 3 və — əgər müqayisə uğurludursa ( A == 0x86dd) keçiləcək etiketlər. Beləliklə, uğurlu halda (IPv6) 2-ci sıraya, uğursuz halda isə 3-cü sıraya keçirik. 3-cü sırada proqram 0 kodu ilə bitir (paketi kopyalama), 2-ci sırada isə 262144 kodu ilə bitir (mənə maksimum 256 kilobayt paketi kopyala).Bir az daha mürəkkəb nümunəyə: təyinat portu üzrə TCP paketlərinə baxaq
Gəlin, 666 təyinat portu olan bütün TCP paketlərini kopyalayan filtrin necə görünəcəyinə nəzər salaq. Biz IPv4 halını götürəcəyik, çünki IPv6 halı daha sadədir. Bu nümunəni öyrəndikdən sonra, siz özünüz IPv6 (
ip6 and tcp dst port 666) və ümumi hal üçün filtrə (tcp dst port 666) baxmağı öz üzərinizə götürə bilərsiniz. Beləliklə, maraqlandıran filtr aşağıdakı kimidir:$ 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
Sıraların 0 və 1-də etdiklərimizi artıq bilirik. 2-ci sırada IPv4 paketi olduğunu (Ether Type =) yoxlayırıq və 24-cü baytı registrə yükləyirik. Bizim paket görünüşü: 0x80014 8 1 1 |ethernet header|ip fields|ttl|protocol|... A bu da deməkdir ki, biz registrə yükləyirik
yarım baytıpaketin A IP başlığının Protokol alanı, mantıklı çünkü yalnızca TCP paketlerini kopyalamak istiyoruz. Protokol ile karşılaştırıyoruz 0x6 () 3. satırda.
4. ve 5. satırlarda, adresi 20 olan yarım kelimeleri yüklüyoruz ve jset komutuyla, üç verilen maskede jset üç en yüksek biti temizliyoruz. Üç bitin ikisi, paketin parçalı bir IP paketinin parçası olup olmadığını ve eğer öyleyse son parçanın olup olmadığını belirtir. Üçüncü bit ayrılmıştır ve sıfır olması gerekir. Tam olmayan veya bozuk paketleri kontrol etmek istemiyoruz, bu nedenle üç bitin hepsini kontrol ediyoruz.
6. satır, bu listingde en ilginç olanıdır. İfade ldxb 4*([14]&0xf) şunları yüklediğimiz anlamına gelir: paketimizin onuncu baytının dört en düşük bitini, 4 ile çarpılmış olarak. Onuncu baytın dört en düşük biti, X Internet Header Length 4*([14]&0xf) özel bir adresleme şeklinin tanımıdır, yalnızca bu şekliyle ve yalnızca bu kayıt için kullanılabilir. , yani ne Xldb 4*([14]&0xf) ne de ldxb 5*([14]&0xf) (sadece başka bir offset belirtebiliriz, örneğin ldxb 4*([16]&0xf) ). Bu adresleme şeklinin, IPv4 başlığının uzunluğunu almak için BPF'ye tam olarak entegre edildiği açıktır.Böylece, 7. satırda, adresdeki yarım kelimeyi yüklemeye çalışıyoruz X (X+16)
. Ethernet başlığının 14 bayt tuttuğunu ve IPv4 başlığının uzunluğunu içerdiğini hatırlarsak,TCP hedef portunu yüklüyoruz: X 14 X 2 2 |ethernet header|ip header|source port|destination port| A Son olarak, 8. satırda hedef portu aranan değerle karşılaştırıyoruz ve 9. ya da 10. satırlarda sonucu döndürüyoruz — paketi kopyalayıp kopyalamamak.
Önceki örneklerde, paketi filtrelemek için BPF bayt kodunu çekirdek ile nasıl yüklediğimize detaylı olarak girmedik. Genel olarak,farklı sistemlere taşındı ve filtrelerle çalışmak için
. Yüklənmiş filtr hər bir paket interfeysdən keçdikcə işə düşəcək
. Kısaca, bir filtreyi arayüze yerleştirmek için tcpdump , şunları yapmanız gerekir: bibilotekanı istifadə etməklə bir libpcappcap_t
- tipindeki tanımlayıcıyı arayüz adıyla oluşturmak:
pcap_createarayüzü aktif hale getirmek: , - filtreyi derlemek: ,
- filtreyi bağlamak: ,
- Fonksiyonun Linux'ta nasıl uygulandığını görmek için .
(bazı satırlar silinmiştir): kullanıyoruz $ 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 ... strace Çıktının ilk iki satırında,
raw soketyaratıyoruz ve bunu arayüze bağlıyoruz filtremizi bildiğimiz için eth0ip Yeni bağlantılar açmak için başka yollar da var. ip dörd BPF instruktsiyasından ibarət olacaq və üçüncü sətirdə biz görürük ki, SO_ATTACH_FILTER seçimi ilə sistem çağırışı setsockopt filtrimizi uzunluğu 4 olan yükləyir və birləşdiririk. Bu, bizim filtrimizdir.
Qeyd etmək lazımdır ki, klassik BPF-də filtrin yüklənməsi və birləşdirilməsi həmişə atomik əməliyyat kimi baş verir, yeni versiyasında isə proqramın yüklənməsi və hadisə generatoruna bağlanması zaman baxımından ayrı-seçkidir.
Gizli həqiqət
Biraz daha tam versiya belə görünür:
$ 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 (Resurs müvəqqəti olaraq mövcud deyil)
setsockopt(3, SOL_SOCKET, SO_ATTACH_FILTER, {len=4, filter=0xb00bb00bb00b}, 16) = 0
...Yuxarıda qeyd edildiyi kimi, biz filtrimizi beşinci sətirdə soketə yükləyirik və birləşdiririk, amma üçüncü və dördüncü sətirlərdə nə baş verir? Göründüyü kimi, bu libpcap bizim üçün qayğı göstərir - filtrimizin çıxışına uyğun gəlməyən paketlərin düşməməsi üçün, kitabxana saxta filtr ret #0 (bütün paketləri atmaq), soketi bloklanmayan rejimə çevirir və əvvəlki filtrelərdən qalmış bütün paketləri oxumağa çalışır.
Nəticə olaraq, Linux-da klassik BPF vasitəsilə paketləri filtr etmək üçün, struct sock_fprog tipli bir struktura malik və açıq soket olmalıdır, bundan sonra filtr soketə sistem çağırışı setsockopt.
ilə birləşdirilə bilər. Maraqlıdır ki, filtr hər hansı bir soketə, yalnız raw olmadıqda belə, birləşdirilə bilər. Budur
girişdəki UDP datagramlarının yalnız iki ilk baytını kəsmək üçün proqram. (Şərhləri kodda əlavə etdim ki, məqaləni yükləməyim.) setsockopt Filtrlərin birləşdirilməsi üçün istifadə etməyi daha ətraflı nəzərdən keçirin , öz filtrinizi yazmağın yolu struct sock_fprog kömək olmadan tcpdump Biz .
Klassik BPF və XXI əsr
BPF 1997-ci ildən Linux-a daxil edilmişdir və uzun müddətə çalışmağına davam edib libpcap heç bir xüsusi dəyişikliyə məruz qalmamışdır (Linux-a xas dəyişikliklər, əlbəttə ki, , amma bunlar qlobal mənzərəni dəyişmədi). BPF-nin inkişaf edəcəyinə dair ilk ciddi əlamətlər 2011-ci ildə Eric Dumazet tərəfindən təklif ediləndə meydana çıxdı , noyabrda Just In Time Compiler-i nüvəyə əlavə edən — BPF bytecode-nu yerli x86_64 koda çevirmək üçün tərcüməçi.
JIT compiler, dəyişikliklər silsiləsində ilk idi: 2012-ci ildə BPF istifadə edərək filtr yazma imkanını, 2013-cü ilin yanvarında bir modul , BPF vasitəsilə qaydalar yazmağı mümkün edən, 2013-cü ilin oktyabrında isə xt_bpfhəmçinin bir modul iptables , BPF ilə trafik təsnifatçıları yazmağı mümkün edən. əlavə olaraq modul cls_bpf, BPF vasitəsilə trafik təsnifatçıları yazmağa imkan verən.
Biz bu nümunələrin hər birini daha ətraflı müzakirə edəcəyik, lakin əvvəlcə BPF üçün təsadüfi proqramlar yazmağı və tərtib etməyi öyrənmək faydalı olacaq, çünki kitabxana tərəfindən təqdim olunan imkanlar libpcap məhduddur (sadə bir nümunə: yaradılan filtr yalnız iki dəyər döndərə bilər — 0 və ya 0x40000) və ya seccomp vəziyyətində olduğu kimi, tətbiq edilə bilməz. libpcap BPF təlimatlarının ikili formatı ilə tanış olaq, o, çox sadədir:
öz əllərimizlə BPF proqramlaşdırma
16 8 8 32 | kod | jt | jf | k |
Hər bir təlimat 64 bit ölçüsündədir, burada ilk 16 bit əmrin kodudur, ardından iki 8 bit yerdəyişmə gəlir,jt jf və , və 32 bit argument üçünK , onun təyinatı əmrə görə dəyişir. Məsələn,ret , proqramın işini bitirən əmrin kodu, qaytarılan dəyər isə sabitdən götürülür 6. C dilində bir BPF təlimatı aşağıdakı strukturda təqdim olunur , onun təyinatı əmrə görə dəyişir. Məsələn,struct sock_filter { __u16 kod; __u8 jt; __u8 jf; __u32 k; }
və tam bir proqram aşağıdakı strukturda təqdim olunurstruct sock_fprog { unsigned short len; struct sock_filter *filter; }
Beləliklə, artıq proqramlar yaza bilərik (tətbiq olunan əmr kodlarını, fərz edək ki, biz bilirük). Filtr aşağıdakı kimi görünəcək struct sock_filter kod[] = { { 0x28, 0, 0, 0x0000000c }, { 0x15, 0, 1, 0x000086dd }, { 0x06, 0, 0, 0x00040000 }, { 0x06, 0, 0, 0x00000000 }, }; struct sock_fprog prog = { .len = ARRAY_SIZE(kod), .filter = kod, }; ip6 biz :
Biz proqramı çağırmada qanuni şəkildə istifadə edə biləriksetsockopt(sk, SOL_SOCKET, SO_ATTACH_FILTER, &prog, sizeof(prog)) Proqramları maşın kodları şəklində yazmaq çox rahat deyil, amma bəzən bunu etmək lazım olur (məsələn, debuq üçün, unit-test yazmaq, Habr-da məqalələr yazmaq və s.). Rahatlıq üçün <linux/filter.h>
faylında köməkçi makrolar müəyyən edilir — yuxarıdakı eyni nümunəni belə yenidən yaza bilərdikstruct sock_filter kod[] = { 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), } Ancaq, belə bir variant da çox rahat deyil. Belə düşünən Linux nüvəsinin proqramçıları olduğuna görə, tools/bpf
direktoriyasında klassik BPF ilə işləmək üçün assembler və debugger tapmaq mümkündür.Assembler dili debuq çıxışına çox bənzəyir $ cat /tmp/tcp-over-ipv4.bpf ldh [12] jne #0x800, drop ldb [23] jneq #6, drop ret #-1 drop: ret #0
Varsayılan olaraq, assembler kodu aşağıdakı formatda istehsal edir: tcpdump<təlimat sayı>,<kod1> <jt1> <jf1> <k1>,...
, TCP üçün bizim nümunəmizdə alınacaq$ 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, Proqramçı C üçün daha rahatlıqları təmin etmək məqsədilə başqa çıxış formatı da istifadə oluna bilər:$ 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, 0x00000000 },
Bu mətnistruct sock_filter
tipinin tərifinə kopyalamaq mümkündür, dediyimiz kimi bu bölmənin əvvəlində.Linux genişləndirmələri və netsniff-ng struct sock_filter, dediyimiz kimi, bu hissənin əvvəllərində etdik.
Linux genişləndirmələri və netsniff-ng
BPF standart əmrindən əlavə, Linux və tools/bpf/bpf_asm daxildir və . Əsasən, əmr strukturların sahələrinə girişi təmin edir struct sk_buff, nüvədə şəbəkə paketi təsvir edir. Ancaq, digər tipdə köməkçi əmr də var, məsələn, ldw cpu registrə yükləyəcək A nüvə funksiyasının icra nəticəsini raw_smp_processor_id(). (BPF-in yeni versiyasında bu standart olmayan genişləmələr, proqramlara yaddaş, strukturlar və hadisələrə çıxış üçün kernel helper dəstini təqdim etməklə genişləndirilmişdir.) Burada, yalnız paket başlıqlarını istifadəçi sahəsinə köçürdüyümüz bir filtrin maraqlı bir nümunəsi var, istifadə edərək genişlənməni poff, yük mənbəyi:
ld poff
ret aBPF genişləndirmələrini istifadəyə vermək mümkün olmayacaq tcpdump, amma bu, istifadəçi alətləri paketini tanış etmək üçün yaxşı bir səbəbdir , bu, hər şeydən əlavə, qabaqcıl proqramı ehtiva edir netsniff-ng, BPF ilə filtrasiya etməklə yanaşı, effektiv trafik generatoruna və daha inkişaf etmiş bir tools/bpf/bpf_asm, BPF assembleri adlanan bpfc. Paket olduqca ətraflı sənəd təqdim edir, məqalənin sonunda da linkləri tapa bilərsiniz.
seccomp
Beləliklə, artıq biz müxtəlif mürəkkəblikdə BPF proqramları yaza bilirik və yeni nümunələrə baxmağa hazırıq, bunlardan biri - seccomp texnologiyasıdır ki, BPF filtrları vasitəsilə prosesin və onun varislərinin icra edə biləcəyi sistem çağırışlarının çoxluğunu və argument dəstini idarə etməyə imkan verir.
Seccomp-un birinci versiyası 2005-ci ildə nüvəyə əlavə olundu və geniş bir populyarlıq qazanmadı, çünki yalnız bir imkanı təmin edirdi - prosesə təklif olunan sistem çağırışlarının çoxluğunu məhdudlaşdırmaq: read, write, exit və sigreturn, və qaydaları pozan proses SIGKILLilə öldürülürdü. Ancaq 2012-ci ildə seccomp-a BPF filtrlərindən istifadə etmə imkanı əlavə edildi ki, bu da icazə verilmiş sistem çağırışlarının çoxluğunu müəyyən etməyə və hətta onların arqumentlərinə yoxlamalar aparmağa imkan verir. (Qeyd edək ki, bu funksionallığın ilk istifadəçilərindən biri Chrome idi, hal-hazırda isə Chrome-un nümayəndələri yeni versiya BPF-a əsaslanan KRSI mexanizmini inkişaf etdirirlər ki, bu da Linux Təhlükəsizlik Modulunu fərdiləşdirməyə imkan verir.) Əlavə sənəd üçün linklərdən məqalənin sonunda tapa bilərsiniz.
Qeyd edək ki, Habr-də artıq seccomp istifadəsi ilə bağlı məqalələr var, bəlkə kimlərsə bu bölmələri oxumaq istəyə bilər. Məqalədə seccomp-ın istifadəsi nümunələri verilmişdir, həm 2007-ci ildən, həm də BPF istifadə edərək versiyasını (filtrlər libseccomp vasitəsilə yaradılır), seccomp-un Docker ilə əlaqəsi haqqında danışılır, eləcə də çox sayda faydalı linklər təqdim olunur. Məqalədə xüsusilə, systemd altında işləyən daemonlar üçün sistem çağırışlarının qara və ağ siyahılarını necə əlavə etmək lazım olduğunu izah edir.
Sonra, yazmağı və yükləməyi öyrənəcəyik seccomp sıfırdan C dilində və libseccomp kitabxanası ilə, hər bir variantın hansı üstünlükləri və mənfi cəhətləri olduğunu müzakirə edəcəyik, və sonda seccomp-un proqram ilə necə istifadə edildiyinə baxacağıq. strace.
Seccomp üçün filtr yazırıq və yükləyirik
Biz artıq BPF proqramlarını yazmağı öyrənmişik, ona görə də əvvəlcə seccomp-un proqram interfeysinə baxacağıq. Filtri proses səviyyəsində quraşdıra bilərik, burada bütün övlad proseslər bu məhdudiyyətləri miras alacaq. Bu, :
seccomp(SECCOMP_SET_MODE_FILTER, flags, &filter)burada &filter – artıq tanıdığımız quruluşun göstəricisidir struct sock_fprog, yəni BPF proqramı.
Seccomp üçün proqramlar, soketlər üçün proqramlardan nə ilə fərqlənir? Keçirilən kontekstə görə. Soketlər üçün bizə paketi içərən yaddaş sahəsi təqdim olundu, seccomp üçün isə bizə
struct seccomp_data {
int nr;
__u32 arch;
__u64 instruction_pointer;
__u64 args[6];
};Burada nr – çağırılan sistem çağırışının nömrəsidir, arch – cari arxitekturadır (bunun altında daha çox məlumat verəcəyik), args – altı sistem çağırışı üçün arqument, və instruction_pointer – bu, istifadəçi məkanında həmin sistem çağırışını edən təlimatın göstəricisidir. Beləliklə, məsələn, sistem çağırışının nömrəsini registrdən yükləmək üçün A biz deməliyik
ldw [0]Seccomp proqramları üçün bəzi digər xüsusiyyətlər də var, məsələn, konteksə giriş yalnız 32-bit düzülmə əsasında mümkündür və yarım-söz və ya bayt yükləmək mümkün deyil — filtr yükləməyə çalışarkən ldh [0] sistem çağırışı seccomp dönəcək EINVAL. Yüklənən filtrlərin yoxlanılması nüvəsi ilə həyata keçirilir. (Maraqlı bir fakt, seccomp funksionallığını əlavə edən orijinal commitdə bu funksiyaya mod instruizasiyasının istifadəsi üçün icazə əlavə etməyi unutmuşdular mod (bölünmənin qalığı) və indi seccomp BPF proqramları üçün mövcud deyil, çünki onun əlavə olunması ABI-ni.)
Əslində, biz artıq seccomp proqramlarını yazıb oxumaq üçün lazım olan hər şeyi bilirik. Adətən proqramın mantiqi sistem çağırışlarının ağ və ya qara siyahısı şəklindədir, məsələn, proqram
ld [0]
jeq #304, bad
jeq #176, bad
jeq #239, bad
jeq #279, bad
good: ret #0x7fff0000 /* SECCOMP_RET_ALLOW */
bad: ret #0304, 176, 239, 279 nömrəli dörd sistem çağırışı olan qara siyahını yoxlayır. Bəs bu sistem çağırışları nələrdir? Tam dəqiq deyə bilmirik, çünki bu proqramın hansı arxitektura üçün yazıldığını bilmirik. Buna görə seccomp müəllifləri bütün proqramları arxitekturanı yoxlamaqla başlamaq (cari arxitektura kontekstdə arch struct seccomp_data şəklində göstərilir). Arxitekturanın yoxlanılması ilə nümunənin başlanğıcı belə görünəcək:ld [4] jne #0xc000003e, bad_arch ; SCMP_ARCH_X86_64
və o zaman sistem çağırışları üçün nömrələr müəyyən dəyərləri alacaq.və onda sistem çağırışlarımız müəyyən dəyərlərə sahib olardı.
Seccomp filtrini yazırıq və yükləyirik libseccomp
Maşın kodları və ya BPF assembly üçün filtr yazmaq, nəticələr üzərində tam nəzarət əldə etməyə imkan verir, amma bəzən portativ və ya oxunaqlı kodu əldə etmək daha üstün olardı. Bunun üçün bizə kömək edəcək kitabxana , qara və ya ağ filtr yazmaq üçün standart interfeysi təqdim edir.
Məsələn, istifadəçi tərəfindən seçilmiş binar faylı işə salan bir proqram yazaq, əvvəlcə sistem çağırışları üçün qara siyahı quraraq (proqram daha anlaşılır olması üçün sadələşdirilmişdir, tam versiya ):
#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]);
}İlk növbədə, biz sys_numbers adında 40-dan artıq bloklanacaq sistem çağırış nömrələrini əhatə edən bir massiv müəyyən edirik. Sonra isə konteksti ctx təyin edirik və kitabxanaya bildirilir ki, biz bütün sistem çağırışlarını (SCMP_ACT_ALLOW) standart olaraq icazə vermək istəyirik (qara siyahılardan istifadə daha asandır). Sonra, tək-tək, biz qara siyahıdan bütün sistem çağırışlarını əlavə edirik. Siyahıdakı sistem çağırışına qarşı reaksiya olaraq, biz SCMP_ACT_TRAPistəyirik, bu zaman seccomp prosesə SIGSYS sinyalini göndərəcək, hansı sistem çağırışının qaydaları pozduğunu açıqlayacaq. Nəhayət, proqramı yükləyirik seccomp_loadilə, bu da proqramı kompilyasiya edəcək və onu sistem çağırışı vasitəsilə prosesə bağlayacaq. seccomp(2).
Proqramın uğurla kompilyasiyası üçün onu kitabxanaya bağlantı etməlisiniz libseccomp, məsələn:
cc -std=c17 -Wall -Wextra -c -o seccomp_lib.o seccomp_lib.c
cc -o seccomp_lib seccomp_lib.o -lseccompUğurlu işə salma nümunəsi:
$ ./seccomp_lib echo ok
okBloklanan sistem çağırışı nümunəsi:
$ sudo ./seccomp_lib mount -t bpf bpf /tmp
Pis sistem çağırışıİstifadə olunur strace, ətraflı məlumat üçün:
$ 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} ---
+++ SIGSYS tərəfindən öldürüldü (core dumped) +++
Pis sistem çağırışıburadan belə bir nəticəyə gələ bilərik ki, proqram qadağan edilmiş sistem çağırışının istifadəsi səbəbindən dayandırılıb mount(2).
Özetlə, kitabxananı istifadə edərək filtr yaratdıq libseccomp, mürəkkəb kodu dörd sətirə sığdırmış olduq. Yuxarıdakı nümunədə sistem çağırışlarının çoxluğu ilə işin icra müddəti əhəmiyyətli dərəcədə aşağı düşə bilər, çünki yoxlama sadəcə bir müqayisə siyahısıdır. Optimallaşdırma üçün keçən günlərdə libseccomp-a daxil olunub, bu da filtr atributu dəstəyini əlavə edir SCMP_FLTATR_CTL_OPTIMIZE. Bu atributu 2-yə təyin etsəniz, filtr ikili axtarış proqramına çevriləcək.
İkili axtarışla filtrlərin necə qurulduğuna baxmaq istəyirsinizsə, sistem çağırışlarının nömrələri əsasında BPF assembler proqramları yaradan göz atın, məsələn:
$ 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 #0Heç bir şey daha sürətli yazmaq mümkün olmayacaq, çünki BPF proqramları müstəvi keçidlərini həyata keçirə bilmir (məsələn, biz edə bilmirik, jmp A və jmp [label+X]) və buna görə bütün keçidlər statikdir.
seccomp və strace
Hər kəs bu proqramdan xəbərdardır strace — Linux-da proseslərin davranışını araşdırmaq üçün əvəzolunmaz bir alətdir. Ancaq bir çoxları da bu alətin istifadəsi ilə bağlı. Məsələ budur ki, strace cəmi ptrace(2), bu mexanizmda biz prosesin hansı sistem çağırışlarının dəstəklənəcəyini göstərə bilmirik, yəni, məsələn, əmr
$ time strace du /usr/share/ >/dev/null 2>&1
real 0m3.081s
user 0m0.531s
sys 0m2.073svə
$ time strace -e open du /usr/share/ >/dev/null 2>&1
real 0m2.404s
user 0m0.193s
sys 0m1.800shəm eyni vaxtda başa çatır, halbuki ikinci halda yalnız bir sistem çağırışını izləmək istəyirik.
Yeni seçim --seccomp-bpf, 5.3 versiyası ilə əlavə edilmişdir və prosesi dəfələrlə sürətləndirir, bir sistem çağırışı altında izləmə müddəti artıq adi başlanma müddəti ilə müqayisə edilə bilər: strace $ time strace --seccomp-bpf -e open du /usr/share/ >/dev/null 2>&1real 0m0.148s user 0m0.017s sys 0m0.131s$ time du /usr/share/ >/dev/null 2>&1real 0m0.140s user 0m0.024s sys 0m0.116s
(Burada, əlbəttə ki, bir az aldatma var, çünki biz bu komandanın əsas sistem çağırışını izləmirik. Əgər biz, məsələn,newfsstat həmçinin eyni dərəcədə yavaşdır, necə ki, olmadan., o strace Bu seçim necə işləyir? Olmadan --seccomp-bpf.)
prosesə qoşulur və onu strace PTRACE_SYSCALL . İdarə olunan proses hər hansı bir sistem çağırışı işə saldıqda, idarəetmə, sistem çağırışının arqumentlərinə nəzər salır və onu straceişə salır. Bir müddətdən sonra proses sistem çağırışını başa vurur və çıxışda idarəetmə yenidən . İdarə olunan proses hər hansı bir sistem çağırışı işə saldıqda, idarəetmə, dönəcəkdir, hansı ki, geri dönən dəyərləri yoxlayır və prosesi straceişə salır, və s. . İdarə olunan proses hər hansı bir sistem çağırışı işə saldıqda, idarəetməLakin seccomp vasitəsi ilə bu prosesi istədiyimiz kimi optimallaşdırmaq mümkündür. Yəni, əgər yalnız sistem çağırışına baxmaq istəyiriksə,

, BPF filtri yaza bilərik ki, bu Xdəyərini X SECCOMP_RET_TRACE , bizim üçün maraqlı olmayan çağırışlar üçün isə —SECCOMP_RET_ALLOW ld [0] jneq #X, ignore trace: ret #0x7ff00000 ignore: ret #0x7fff0000:
Bu haldailk növbədə prosesi strace PTRACE_CONT kimi işə salırıq, hər sistem çağırışı üçün filtrimiz yerinə yetirilir, əgər sistem çağırışı, proses biznesi davam edir, amma əgər bu X, seccomp idarəetməni X, hansı ki, arqumentlərə baxacaq və prosesi işə salacaq strace(çünki seccomp altında sistem çağırışı başa çatanda proqramı işə salmaq mümkün deyil). Sistem çağırışı geri döndükdə, . İdarə olunan proses hər hansı bir sistem çağırışı işə saldıqda, idarəetmə prosesini strace yenidən işə salacaq kimi işə salırıq, hər sistem çağırışı üçün filtrimiz yerinə yetirilir, əgər sistem çağırışı və seccomp-dan yeni mesajlar gözləyəcəkdir.

Bu seçimdən istifadə edərkən --seccomp-bpf iki məhdudiyyət var. Birincisi, artıq mövcud olan bir prosesə qoşulmaq mümkün deyil (seçim -p proqramlar strace), çünki bu seccomp tərəfindən dəstəklənmir. İkincisi, bu imkan yoxdur deyil uşaqlara baxmaq, çünki seccomp süzgəcləri bütün uşaqlara miras qalır və bunu söndürmək mümkün deyil.
Bir az daha ətraflı məlumat strace çalışır seccomp öyrənə bilərsiniz . Bizim üçün ən maraqlı fakt, klassik BPF-nin seccomp şəklində hələ də tətbiqi tapmasıdır.
xt_bpf
İndi şəbəkələr dünyasına qayıdaq.
İkincil hekayə: çoxdan, 2007-ci ildə, nüvəyə daxil edildi , BPF vasitəsilə qaydalar yazmağı mümkün edən, 2013-cü ilin oktyabrında isə xt_u32 netfilter üçün. Bu, daha qədim trafik təsnifatçısı cls_u32 ilə analoqu olaraq yazılmışdır və iptables üçün aşağıdakı sadə əməliyyatlarla istənilən ikili qaydalar yazmağa imkan verir: paketdən 32 bit yükləyin və onlarla bir sıra arifmetik əməliyyatlar keçirin. Məsələn,
sudo iptables -A INPUT -m u32 --u32 "6&0xFF=1" -j LOG --log-prefix "xt_u32 tərəfindən görünən"Başlanğıc 6 olan IP başlığından 32 bit yükləyir və onlara maska tətbiq edir 0xFF (ən aşağı byte-ı götürün). Bu - protokol IP başlığının sahəsi və biz onu 1 ilə müqayisə edirik (ICMP). Bir qaydada bir çox yoxlama birləşdirmək mümkündür, həm də @ operatorunu yerinə yetirmək mümkündür
iptables -m u32 --u32 "6&0xFF=0x6 && 0>>22&0x3C@4=0x29"TCP ardıcıllıq nömrəsinin bərabər olmadığını yoxlayır 0x29. Ətraflı izah etməyəcəm, çünki artıq aydındır ki, belə qaydaları əl ilə yazmaq rahat deyil. Makalədə , kullanım örneği ve yaratma örnekleri için birkaç bağlantı var xt_u32. Bu məqalənin sonunda da bağlantılara baxın.
2013-cü ildən etibarən modulu dəyişdirmək mümkün xt_u32 BPF əsasında modul xt_bpf. Bura qədər oxuyan hər kəsin iş prinsipi artıq aydın olmalıdır: iptables qaydaları kimi BPF bayt kodunu çalışdırmaq. Yeni bir qayda yaratmaq mümkündür, məsələn, belə:
iptables -A INPUT -m bpf --bytecode <bayt kodu> -j LOGburada <bayt kodu> yeni formatda kod bpf_asm default olaraq, məsələn,
$ 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 LOGBu nümunədə biz bütün UDP paketlərini filterləyirik. BPF proqramı üçün modul konteksti xt_bpf, əlbəttə ki, paketin məlumatlarına, iptables üçün isə IPv4 başlığının başlanğıcına işarə edir. BPF proqramından qaytarılan dəyər , burada false paketin uyğun olmadığını göstərir.
Məlumdur ki, modul xt_bpf yuxarıdakı nümunədən daha mürəkkəb süzgəcləri dəstəkləyir. Cloudfare-dan real nümunələrə baxaq. Son zamanlarda onlar xt_bpf DDoS hücumlarından qorunmaq üçün modul onlar BPF süzgəclərini necə (və niyə) yaratdıqlarını izah edirlər və belə süzgəclərin yaradılması üçün bir vasitələr dəstinə bağlantılar dərc edirlər. Məsələn, vasitəsi olan bpfgen DNS sorğusunu adla uyğunlaşdıran BPF proqramı yarada bilərsiniz 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 #0Proqramda əvvəlcə qeydiyyata yükləyirik X stringin başlanğıc ünvanı x04habrx03comx00 UDP datagramlarının içində və sonra sorğunu yoxlayırıq: 0x04686162 "x04hab" və s.
Bir az sonra Cloudfare p0f tərtibçisinin kodunu təqdim etdi -> BPF. Məqalədə onlar p0f-in nə olduğunu və p0f imzalarını BPF-yə necə çevirmək lazım olduğunu izah edirlər:
$ ./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,
...Hal-hazırda Cloudfare artıq istifadə etmir xt_bpf, çünki onlar yeni BPF versiyasının bir növü olan XDP-yə keçdilər, bax: .
cls_bpf
Klassik BPF-nin nümunələrindən biri nüvədə trafik nəzarət sistemləri üçün classifier-dir, cls_bpf 2013-cü ilin sonlarında Linux sisteminə əlavə edilərək köhnə cls_u32.
Lakin, biz indi bunun işləməsi haqqında danışmayacağıq cls_bpf, çünki klassik BPF ilə bağlı biliklər baxımından bu, bizə heç nə verəcək - artıq bütün funksionallıqla tanış olmuşuq. Üstəlik, Extended BPF haqqında danışan sonrakı məqalələrdə biz bu classifier ilə dəfələrlə görüşəcəyik.
Klassik BPF-nin istifadəsindən danışmamağın bir səbəbi də cls_bpf odur ki, Extended BPF ilə müqayisədə bu halda tətbiq sahəsi kəskin şəkildə daralır: klassik proqramlar paketlərin məzmununu dəyişə bilmir və çağırışlar arasında vəziyyət saxlayabilmir.
Beləliklə, klassik BPF ilə vidalaşmağın vaxtıdır və gələcəyə baxaq.
Klassik BPF ilə vidalaşma
Biz BPF texnologiyasına baxdıq, 90-cı illərin başlanğıcında hazırlanmış və bir əsrdən çox yaşayaraq yeni tətbiqlər tapmağa davam etdi. Lakin, klassik BPF-nin inkişafına səbəb olan 32-bitli sistemlərdən 64-bit sistemlərə keçid baş verdi və klassik BPF köhnəldikcə, köhnəlmiş arxitektura'sı ilə birgə onun imkanları da kəskin şəkildə məhdudlaşdı; bpf proqramları arasında vəziyyət saxlama, istifadəçi ilə birbaşa əlaqə yaratma, yalnız strukturun yalnız bir neçə sahəsini oxumaqla kernel ilə əlaqə qurma imkanı yoxdur. sk_buff və ən sadə yardımçı funksiyaları icra etmək mümkün deyil, paketlərin məzmununu dəyişmək və yönləndirmək olmaz.
Əslində, hazırda Linux-da klassik BPF-dən yalnız API interfeysi qalmışdır, və nüvədə bütün klassik proqramlar, sock filtrleri və seccomp filtrleri olsun, avtomatik olaraq yeni formata, Extended BPF-yə çevrilir. (Bunun necə baş verdiyi haqqında növbəti məqalədə danışacaq.)
Yeni arxitekturaya keçid 2013-cü ildə Aleksey Starovoytovun BPF-nin yenilənməsi planını təqdim etdiyi zaman başladı. 2014-cü ildə müvafiq patch-lər nüvədə. Bildiyimə görə, ilkin olaraq arxitekturanı və 64-bitli maşınlarda daha effektiv işləməsi üçün JIT-kompilyatorunu optimallaşdırmaq planlaşdırılırdı, lakin bunun əvəzində bu optimizasiyalar Linux-un inkişafında yeni bir fəsilə başlanğıc qoydu.
Bu seriyadakı gələcək məqalələr əvvəlcə daxili BPF, sonra genişlənmiş BPF, indiki isə sadəcə BPF kimi tanınan yeni texnologiyanın arxitekturası və tətbiqlərindən danışacaq.
Bağlantılar
- Steven McCanne və Van Jacobson, "BSD Paket Filter: İstifadəçi səviyyəsində Paket Tutma üçün Yeni Arxitektura",
https://www.tcpdump.org/papers/bpf-usenix93.pdf - Steven McCanne, "libpcap: Paket Tutma üçün Arxitektura və Optimallaşdırma Metodologiyası",
https://sharkfestus.wireshark.org/sharkfest.11/presentations/McCanne-Sharkfest'11_Keynote_Address.pdf tcpdump,libpcap:- .
- BPF — unudulmuş bayt kodu:
https://blog.cloudflare.com/bpf-the-forgotten-bytecode/ - BPF Alətini T Einführung:
https://blog.cloudflare.com/introducing-the-bpf-tools/ bpf_cls:http://man7.org/linux/man-pages/man8/tc-bpf.8.html- Seccomp icmalı:
https://lwn.net/Articles/656307/ https://github.com/torvalds/linux/blob/master/Documentation/userspace-api/seccomp_filter.rst- Paul Chaignon, "strace — seccomp-bpf: mexanizmin altına qısa bir baxış",
https://fosdem.org/2020/schedule/event/debugging_strace_bpf/ netsniff-ng:http://netsniff-ng.org/
Mənbə: habr.com
