Küçükler için BPF, sıfır bölüm: klasik BPF

Berkeley Paket Filtreleri (BPF), birkaç yıldır İngilizce teknoloji yayınlarının ön sayfalarında yer alan bir Linux çekirdek teknolojisidir. Konferanslar BPF'nin kullanımı ve geliştirilmesine ilişkin raporlarla doludur. Linux ağ alt sistemi sorumlusu David Miller, Linux Plumbers 2018'de konuşmasını yapıyor “Bu konuşma XDP ile ilgili değil” (XDP, BPF'nin kullanım örneklerinden biridir). Brendan Gregg başlıklı konuşmalar yapıyor Linux BPF Süper Güçleri. Toke Høiland-Jørgensen gülüyorçekirdeğin artık bir mikro çekirdek olduğunu. Thomas Graf şu fikri destekliyor: BPF, çekirdek için javascripttir.

Habré'de BPF'nin hala sistematik bir açıklaması yok ve bu nedenle bir dizi makalede teknolojinin tarihi hakkında konuşmaya, mimariyi ve geliştirme araçlarını açıklamaya ve BPF kullanımının uygulama ve pratik alanlarını özetlemeye çalışacağım. Serideki sıfır makalesi, klasik BPF'nin tarihini ve mimarisini anlatırken, aynı zamanda çalışma prensiplerinin sırlarını da ortaya çıkarıyor. tcpdump, seccomp, strace, ve daha fazlası.

BPF'nin gelişimi Linux ağ topluluğu tarafından kontrol edilir, BPF'nin mevcut ana uygulamaları ağlarla ilgilidir ve bu nedenle izinlidir @eucariot, Bu harika serinin onuruna seriye "Küçükler için BPF" adını verdim "Küçükler için ağlar".

BPF tarihinde kısa bir kurs(c)

Modern BPF teknolojisi, karışıklığı önlemek için artık klasik BPF olarak adlandırılan, aynı adı taşıyan eski teknolojinin geliştirilmiş ve genişletilmiş bir versiyonudur. Klasik BPF'ye dayanarak iyi bilinen bir yardımcı program oluşturuldu tcpdump, mekanizma seccompve daha az bilinen modüller xt_bpf için iptables ve sınıflandırıcı cls_bpf. Modern Linux'ta klasik BPF programları otomatik olarak yeni forma çevrilir, ancak kullanıcı açısından bakıldığında API yerinde kalmıştır ve bu makalede göreceğimiz gibi klasik BPF'nin yeni kullanımları hala bulunmaktadır. Bu nedenle ve ayrıca klasik BPF'nin Linux'taki gelişim tarihini takip ederek, modern biçimine nasıl ve neden evrildiğini daha iyi anlayacağınız için, klasik BPF hakkında bir makale ile başlamaya karar verdim.

Geçen yüzyılın seksenli yıllarının sonunda, ünlü Lawrence Berkeley Laboratuvarı'ndan mühendisler, geçen yüzyılın seksenli yıllarının sonlarında modern olan donanımdaki ağ paketlerinin nasıl düzgün şekilde filtreleneceği sorusuyla ilgilenmeye başladılar. Başlangıçta CSPF (CMU/Stanford Paket Filtresi) teknolojisinde uygulanan filtrelemenin temel fikri, gereksiz paketleri mümkün olduğu kadar erken filtrelemekti; çekirdek alanında, çünkü bu, gereksiz verilerin kullanıcı alanına kopyalanmasını önler. Kullanıcı kodunu çekirdek alanında çalıştırmak için çalışma zamanı güvenliği sağlamak amacıyla, korumalı alana alınmış bir sanal makine kullanıldı.

Ancak mevcut filtrelere yönelik sanal makineler yığın tabanlı makinelerde çalışacak şekilde tasarlanmıştı ve daha yeni RISC makinelerinde o kadar verimli çalışmadı. Sonuç olarak, Berkeley Laboratuvarları'ndaki mühendislerin çabaları sayesinde, sanal makine mimarisi, aşağıdaki gibi iyi bilinen ürünlerin en güçlüsü olan Motorola 6502 işlemciye dayalı olarak tasarlanan yeni bir BPF (Berkeley Paket Filtreleri) teknolojisi geliştirildi. Apple II veya NES. Yeni sanal makine, mevcut çözümlerle karşılaştırıldığında filtre performansını onlarca kat artırdı.

BPF makine mimarisi

Örnekleri analiz ederek mimariyi çalışma yoluyla tanıyacağız. Ancak başlangıç ​​olarak, makinenin kullanıcının erişebileceği iki adet 32 ​​bitlik yazmacın, bir akümülatörün bulunduğunu varsayalım. A ve indeks kaydı X, 64 bayt bellek (16 kelime), yazma ve daha sonra okuma için kullanılabilir ve bu nesnelerle çalışmak için küçük bir komut sistemi. Koşullu ifadelerin uygulanmasına yönelik atlama talimatları da programlarda mevcuttu, ancak programın zamanında tamamlanmasını garanti etmek için atlamalar yalnızca ileriye doğru yapılabiliyordu, yani özellikle döngüler oluşturmak yasaktı.

Makineyi çalıştırmanın genel şeması aşağıdaki gibidir. Kullanıcı BPF mimarisi için bir program oluşturur ve biraz çekirdek mekanizması (sistem çağrısı gibi), programı yükler ve bağlar. bazılarına çekirdekteki olay oluşturucuya (örneğin, bir olay ağ kartındaki bir sonraki paketin gelişidir). Bir olay meydana geldiğinde, çekirdek programı çalıştırır (örneğin, bir yorumlayıcıda) ve makine belleği buna karşılık gelir. bazılarına çekirdek bellek bölgesi (örneğin, gelen bir paketin verileri).

Örneklere bakmaya başlamamız için yukarıdakiler yeterli olacaktır: Gerektiğinde sistemi ve komut formatını tanıyacağız. Bir sanal makinenin komut sistemini hemen incelemek ve tüm yeteneklerini öğrenmek istiyorsanız orijinal makaleyi okuyabilirsiniz. BSD Paket Filtresi ve/veya dosyanın ilk yarısı Belgeler/ağ/filter.txt çekirdek belgelerinden. Ayrıca sunumu inceleyebilirsiniz. libpcap: Paket Yakalama İçin Bir Mimari ve Optimizasyon MetodolojisiBPF'nin yazarlarından McCanne'nin yaratılış tarihini anlattığı libpcap.

Şimdi Linux'ta klasik BPF'yi kullanmanın tüm önemli örneklerini ele almaya geçiyoruz: tcpdump (libpcap), seccomp, xt_bpf, cls_bpf.

tcp dökümü

BPF'nin geliştirilmesi, iyi bilinen bir yardımcı program olan paket filtrelemeye yönelik ön ucun geliştirilmesine paralel olarak gerçekleştirildi. tcpdump. Ve bu, birçok işletim sisteminde mevcut olan klasik BPF'yi kullanmanın en eski ve en ünlü örneği olduğundan, teknoloji çalışmamıza bununla başlayacağız.

(Bu makaledeki tüm örnekleri Linux'ta çalıştırdım 5.6.0-rc6. Bazı komutların çıktısı daha iyi okunabilirlik için düzenlendi.)

Örnek: IPv6 paketlerini gözlemlemek

Bir arayüzdeki tüm IPv6 paketlerine bakmak istediğimizi hayal edelim. eth0. Bunun için programı çalıştırabiliriz tcpdump basit bir filtreyle ip6:

$ sudo tcpdump -i eth0 ip6

Bu durumda, tcpdump filtreyi derler ip6 BPF mimarisi bayt koduna girin ve çekirdeğe gönderin (bölümdeki ayrıntılara bakın) Tcpdump: yükleniyor). Yüklenen filtre arayüzden geçen her paket için çalıştırılacaktır. eth0. Filtre sıfırdan farklı bir değer döndürürse n, sonra şuna kadar n paketin baytları kullanıcı alanına kopyalanacak ve bunu çıktıda göreceğiz tcpdump.

Küçükler için BPF, sıfır bölüm: klasik BPF

Çekirdeğe hangi bayt kodunun gönderildiğini kolayca bulabileceğimiz ortaya çıktı tcpdump yardımıyla tcpdumpseçeneğiyle çalıştırırsak -d:

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

Sıfır satırında komutu çalıştırıyoruz ldh [12], "kaydına yükle" anlamına gelir A 16” adresinde yarım kelime (12 bit) yer alıyor ve tek soru ne tür bir hafızaya hitap ettiğimizdir. Cevap şu: x начинается (x+1)analiz edilen ağ paketinin inci baytı. Ethernet arayüzünden paketleri okuyoruz eth0ve bu araçpaketin şöyle göründüğünü (basitlik açısından pakette VLAN etiketi olmadığını varsayıyoruz):

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

Yani komutu yürüttükten sonra ldh [12] kayıt defterinde A bir alan olacak Ether Type — bu Ethernet çerçevesinde iletilen paketin türü. 1. satırda kaydın içeriğini karşılaştırıyoruz A (paket tipi) c 0x86ddve bu ve есть İlgilendiğimiz tür IPv6'dır. 1. satırda karşılaştırma komutuna ek olarak iki sütun daha var - jt 2 и jf 3 — karşılaştırma başarılı olursa gitmeniz gereken işaretler (A == 0x86dd) ve başarısız. Yani başarılı bir durumda (IPv6) 2. satıra, başarısız bir durumda ise 3. satıra gideriz. 3. satırda program 0 koduyla sonlandırılır (paket kopyalamayın), 2. satırda program kodla sonlandırılır 262144 (bana maksimum 256 kilobaytlık paket kopyalayın).

Daha karmaşık bir örnek: TCP paketlerine hedef bağlantı noktasına göre bakıyoruz

Hedef bağlantı noktası 666 ile tüm TCP paketlerini kopyalayan bir filtrenin nasıl göründüğüne bakalım. IPv4 durumu daha basit olduğu için IPv6 durumunu ele alacağız. Bu örneği inceledikten sonra IPv6 filtresini bir alıştırma olarak kendiniz keşfedebilirsiniz (ip6 and tcp dst port 666) ve genel durum için bir filtre (tcp dst port 666). Yani ilgilendiğimiz filtre şuna benzer:

$ 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 ve 1 satırlarının ne işe yaradığını zaten biliyoruz. 2. satırda bunun bir IPv4 paketi olduğunu zaten kontrol ettik (Ether Type = 0x800) ve kayıt defterine yükleyin A Paketin 24. baytı. Paketimiz şuna benziyor

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

bu, kayıt defterine yüklediğimiz anlamına gelir A Yalnızca TCP paketlerini kopyalamak istediğimiz için mantıksal olan IP başlığının Protokol alanıdır. Protokolü şununla karşılaştırıyoruz: 0x6 (IPPROTO_TCP) 3. satırda.

4. ve 5. satırlara adres 20'de bulunan yarım kelimeleri yükleyip komutu kullanıyoruz jset üçünden birinin ayarlanıp ayarlanmadığını kontrol edin bayraklar - Verilen maskeyi takmak jset en önemli üç bit temizlenir. Üç bitten ikisi bize paketin parçalanmış bir IP paketinin parçası olup olmadığını ve eğer öyleyse son parça olup olmadığını söyler. Üçüncü bit ayrılmıştır ve sıfır olmalıdır. Eksik veya bozuk paketleri kontrol etmek istemiyoruz, bu yüzden üç bitin tamamını kontrol ediyoruz.

Bu listedeki en ilginç satır 6'dır. İfade ldxb 4*([14]&0xf) kayıt defterine yüklediğimiz anlamına gelir X paketin onbeşinci baytının en az anlamlı dört biti 4 ile çarpılır. Onbeşinci baytın en az anlamlı dört biti alandır İnternet Başlığı Uzunluğu Başlığın uzunluğunu kelimelerle saklayan IPv4 başlığı, dolayısıyla daha sonra 4 ile çarpmanız gerekir. 4*([14]&0xf) yalnızca bu formda ve yalnızca bir kayıt için kullanılabilen özel bir adresleme şemasının tanımıdır Xyani biz de söyleyemeyiz ldb 4*([14]&0xf) veya ldxb 5*([14]&0xf) (yalnızca farklı bir ofset belirtebiliriz, örneğin, ldxb 4*([16]&0xf)). Bu adresleme şemasının BPF'ye tam olarak bilgi almak için eklendiği açıktır. X (indeks kaydı) IPv4 başlık uzunluğu.

Yani 7. satırda yarım kelimenin yarısını yüklemeye çalışıyoruz. (X+16). 14 baytın Ethernet başlığı tarafından işgal edildiğini hatırlayarak ve X IPv4 başlığının uzunluğunu içerdiğinden şunu anlıyoruz: A TCP hedef bağlantı noktası yüklendi:

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

Son olarak, 8. satırda hedef bağlantı noktasını istenen değerle karşılaştırırız ve 9. veya 10. satırlarda paketin kopyalanıp kopyalanmayacağına ilişkin sonucu döndürürüz.

Tcpdump: yükleniyor

Önceki örneklerde, paket filtreleme için BPF bayt kodunu çekirdeğe tam olarak nasıl yüklediğimiz üzerinde özellikle ayrıntılı olarak durmadık. Genel konuşma, tcpdump birçok sisteme taşınmış ve filtrelerle çalışmak için tcpdump kütüphaneyi kullanır libpcap. Kısaca, bir arayüze filtre yerleştirmek için libpcap, aşağıdakileri yapmanız gerekir:

Fonksiyonun nasıl çalıştığını görmek için pcap_setfilter Linux'ta uygulanan, kullanıyoruz strace (bazı satırlar kaldırıldı):

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

Oluşturduğumuz çıktının ilk iki satırında ham soket tüm Ethernet çerçevelerini okumak ve onu arayüze bağlamak için eth0. itibaren ilk örneğimiz filtre olduğunu biliyoruz ip dört BPF talimatından oluşacak ve üçüncü satırda seçeneğin nasıl kullanıldığını görüyoruz SO_ATTACH_FILTER sistem çağrısı setsockopt 4 uzunluğunda bir filtre yükleyip bağlıyoruz. Bu bizim filtremiz.

Klasik BPF'de bir filtrenin yüklenmesi ve bağlanmasının her zaman atomik bir işlem olarak gerçekleştiğini ve BPF'nin yeni sürümünde programın yüklenmesi ve onu olay oluşturucuya bağlamanın zaman içinde ayrıldığını belirtmekte fayda var.

Saklı gerçek

Çıktının biraz daha eksiksiz bir versiyonu şuna benzer:

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

Yukarıda da bahsettiğimiz gibi filtremizi 5. satırdaki sokete yükleyip bağlıyoruz fakat 3. ve 4. satırlarda ne oluyor? Görünüşe göre bu libpcap bizimle ilgilenir - böylece filtremizin çıktısı onu karşılamayan paketleri içermez, kütüphane bağlanır kukla filtre ret #0 (tüm paketleri bırak), soketi engellemesiz moda geçirir ve önceki filtrelerden kalabilecek tüm paketleri çıkarmaya çalışır.

Toplamda, Linux'ta klasik BPF kullanarak paketleri filtrelemek için aşağıdaki gibi bir yapı şeklinde bir filtreye sahip olmanız gerekir: struct sock_fprog ve açık bir soket, bundan sonra filtre bir sistem çağrısı kullanılarak sokete takılabilir setsockopt.

İlginç bir şekilde, filtre yalnızca ham değil herhangi bir sokete takılabilir. Burada örnek gelen tüm UDP datagramlarının ilk iki baytı dışındaki tüm baytlarını kesen bir program. (Yazıda karışıklık yaratmamak adına kod içerisine açıklamalar ekledim.)

Kullanım hakkında daha fazla ayrıntı setsockopt Filtreleri bağlamak için bkz. soket(7), ancak kendi filtrelerinizi yazma hakkında struct sock_fprog yardımsız tcpdump bölümde konuşacağız BPF'yi kendi ellerimizle programlamak.

Klasik BPF ve 21. yüzyıl

BPF, 1997 yılında Linux'a dahil edildi ve uzun süre güçlü bir iş gücü olarak kaldı libpcap herhangi bir özel değişiklik olmadan (Linux'a özgü değişiklikler elbette edildiancak küresel tabloyu değiştirmediler). BPF'nin gelişeceğine dair ilk ciddi işaretler 2011'de Eric Dumazet'in önerdiği zaman geldi. yama, çekirdeğe Tam Zamanında Derleyici ekler - BPF bayt kodunu yerele dönüştürmek için bir çevirmen x86_64 kodu.

JIT derleyicisi değişiklikler zincirindeki ilk kişiydi: 2012'de göründü için filtre yazma yeteneği saniyeOcak 2013'te BPF'yi kullanarak katma modül xt_bpfiçin kurallar yazmanıza olanak tanır. iptables BPF'nin yardımıyla ve Ekim 2013'te katma aynı zamanda bir modül cls_bpfBPF'yi kullanarak trafik sınıflandırıcıları yazmanıza olanak tanır.

Yakında tüm bu örneklere daha ayrıntılı olarak bakacağız, ancak önce kütüphanenin sağladığı yetenekler nedeniyle BPF için isteğe bağlı programların nasıl yazılacağını ve derleneceğini öğrenmek bizim için yararlı olacaktır. libpcap sınırlı (basit örnek: filtre oluşturuldu) libpcap yalnızca iki değer döndürebilir - 0 veya 0x40000) veya genel olarak seccomp durumunda olduğu gibi uygulanamaz.

BPF'yi kendi ellerimizle programlamak

BPF talimatlarının ikili formatını tanıyalım, çok basit:

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

Her talimat 64 bit kaplar; ilk 16 biti talimat kodudur, ardından iki adet sekiz bitlik girinti vardır, jt и jfve argüman için 32 bit K, amacı komuttan komuta değişir. Örneğin, komut retprogramı sonlandıran koda sahiptir 6ve dönüş değeri sabitten alınır K. C'de tek bir BPF talimatı bir yapı olarak temsil edilir

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

ve programın tamamı bir yapı biçimindedir

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

Böylece zaten programlar yazabiliyoruz (örneğin komut kodlarını biliyoruz) [1]). Filtre böyle görünecek ip6 arasında ilk örneğimiz:

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

Program prog bir aramada yasal olarak kullanabiliriz

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

Programları makine kodları biçiminde yazmak pek uygun değildir, ancak bazen gereklidir (örneğin, hata ayıklamak, birim testleri oluşturmak, Habré'de makale yazmak vb. için). Kolaylık sağlamak için dosyada <linux/filter.h> yardımcı makrolar tanımlanmıştır - yukarıdakiyle aynı örnek şu şekilde yeniden yazılabilir:

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

Ancak bu seçenek pek kullanışlı değil. Linux çekirdeği programcılarının gerekçesi budur ve bu nedenle dizinde tools/bpf çekirdeklerde klasik BPF ile çalışmak için bir birleştirici ve hata ayıklayıcı bulabilirsiniz.

Montaj dili hata ayıklama çıktısına çok benzer tcpdump, ancak ek olarak sembolik etiketleri de belirtebiliriz. Örneğin, TCP/IPv4 dışındaki tüm paketleri bırakan bir program:

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

Varsayılan olarak, birleştirici şu biçimde kod üretir: <количество инструкций>,<code1> <jt1> <jf1> <k1>,..., TCP ile olan örneğimiz için şöyle olacak:

$ 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 programcılarının rahatlığı için farklı bir çıktı formatı kullanılabilir:

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

Bu metin tip yapısı tanımına kopyalanabilir struct sock_filterBu bölümün başında yaptığımız gibi.

Linux ve netsniff-ng uzantıları

Standart BPF'ye ek olarak Linux ve tools/bpf/bpf_asm destek ve standart dışı set. Temel olarak talimatlar bir yapının alanlarına erişmek için kullanılır. struct sk_buffçekirdekteki bir ağ paketini tanımlayan. Ancak başka türde yardımcı talimatlar da vardır; örneğin ldw cpu kayıt defterine yüklenecek A bir çekirdek işlevini çalıştırmanın sonucu raw_smp_processor_id(). (BPF'nin yeni sürümünde, bu standart dışı uzantılar, programlara belleğe, yapılara ve olaylara erişim için bir dizi çekirdek yardımcısını sağlayacak şekilde genişletildi.) Burada yalnızca kopyaladığımız bir filtrenin ilginç bir örneği var: uzantıyı kullanarak paket başlıklarını kullanıcı alanına aktarın poff, yük dengelemesi:

ld poff
ret a

BPF uzantıları kullanılamaz tcpdump, ancak bu yardımcı program paketini tanımak için iyi bir neden netsniff-ngdiğer şeylerin yanı sıra gelişmiş bir program içeren netsniff-ngBPF kullanarak filtrelemenin yanı sıra etkili bir trafik oluşturucu da içerir ve diğerlerinden daha gelişmiştir. tools/bpf/bpf_asm, bir BPF derleyicisi olarak adlandırıldı bpfc. Paket oldukça ayrıntılı belgeler içeriyor; ayrıca makalenin sonundaki bağlantılara bakın.

saniye

Dolayısıyla, keyfi karmaşıklığa sahip BPF programlarının nasıl yazılacağını zaten biliyoruz ve yeni örneklere bakmaya hazırız; bunlardan ilki, BPF filtrelerini kullanarak mevcut sistem çağrısı argümanları kümesini ve kümesini yönetmeye olanak tanıyan seccomp teknolojisidir. Belirli bir süreç ve onun soyundan gelenler.

seccomp'un ilk sürümü çekirdeğe 2005 yılında eklendi ve yalnızca tek bir seçenek sunduğundan pek popüler değildi: bir işlem için mevcut sistem çağrıları kümesini aşağıdakilerle sınırlamak: read, write, exit и sigreturnve kuralları ihlal eden süreç kullanılarak sonlandırıldı SIGKILL. Bununla birlikte, 2012 yılında seccomp, BPF filtrelerini kullanma yeteneğini ekleyerek izin verilen sistem çağrıları kümesini tanımlamanıza ve hatta bunların argümanlarını kontrol etmenize olanak tanıdı. (İlginç bir şekilde, Chrome bu işlevin ilk kullanıcılarından biriydi ve Chrome çalışanları şu anda BPF'nin yeni bir sürümünü temel alan ve Linux Güvenlik Modüllerinin özelleştirilmesine olanak tanıyan bir KRSI mekanizması geliştiriyor.) Ek belgelere yönelik bağlantılar sonunda bulunabilir. makalenin.

Merkezde seccomp kullanımıyla ilgili makalelerin zaten bulunduğunu unutmayın; belki birileri aşağıdaki alt bölümleri okumadan önce (veya okumak yerine) bunları okumak isteyebilir. Makalede Konteynerler ve güvenlik: seccomp hem 2007 sürümü hem de BPF kullanan sürüm (filtreler libsecomp kullanılarak oluşturulur) seccomp kullanımına ilişkin örnekler sağlar, seccomp'un Docker ile bağlantısı hakkında konuşur ve ayrıca birçok yararlı bağlantı sağlar. Makalede Arka plan programlarını systemd ile yalıtmak veya "bunun için Docker'a ihtiyacınız yok!" Özellikle, systemd çalıştıran arka plan programları için sistem çağrılarının kara listelerinin veya beyaz listelerinin nasıl ekleneceğini kapsar.

Daha sonra filtrelerin nasıl yazılacağını ve yükleneceğini göreceğiz. seccomp çıplak C'de ve kütüphaneyi kullanma libseccomp ve her seçeneğin artıları ve eksileri nelerdir ve son olarak seccomp'un program tarafından nasıl kullanıldığına bakalım strace.

seccomp için filtre yazma ve yükleme

BPF programlarının nasıl yazılacağını zaten biliyoruz, o yüzden önce seccomp programlama arayüzüne bakalım. Süreç seviyesinde bir filtre ayarlayabilirsiniz ve tüm alt süreçler kısıtlamaları devralır. Bu bir sistem çağrısı kullanılarak yapılır seccomp(2):

seccomp(SECCOMP_SET_MODE_FILTER, flags, &filter)

nerede &filter - bu bize zaten tanıdık gelen bir yapıya işaret ediyor struct sock_fprogyani BPF programı.

seccomp programlarının soket programlarından farkı nedir? İletilen bağlam. Soketlerde bize paketi içeren bir hafıza alanı verildi ve seccomp durumunda bize şöyle bir yapı verildi:

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

öyle nr başlatılacak sistem çağrısının numarası, arch - mevcut mimari (bununla ilgili daha fazlası aşağıda), args - altı adede kadar sistem çağrısı bağımsız değişkeni ve instruction_pointer sistem çağrısını yapan kullanıcı alanı talimatının bir işaretçisidir. Böylece, örneğin sistem çağrı numarasını kayıt defterine yüklemek için A söylemek zorundayız

ldw [0]

Seccomp programlarının başka özellikleri de vardır; örneğin, içeriğe yalnızca 32 bit hizalamayla erişilebilir ve bir filtre yüklemeye çalışırken yarım kelime veya bir bayt yükleyemezsiniz. ldh [0] sistem çağrısı seccomp dönecek EINVAL. İşlev, yüklenen filtreleri kontrol eder seccomp_check_filter() çekirdekler. (Komik olan şu ki, seccomp işlevini ekleyen orijinal taahhütte, bu işleve talimat kullanma izni eklemeyi unutmuşlar mod (bölme kalanı) ve eklenmesinden bu yana seccomp BPF programları için artık kullanılamıyor kırılacak ABI.)

Temel olarak seccomp programlarını yazmak ve okumak için gereken her şeyi zaten biliyoruz. Genellikle program mantığı, sistem çağrılarının beyaz veya kara listesi olarak düzenlenir; örneğin program

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 numaralı dört sistem çağrısından oluşan kara listeyi kontrol eder. Bu sistem çağrıları nelerdir? Programın hangi mimari için yazıldığını bilmediğimiz için kesin bir şey söyleyemeyiz. Bu nedenle seccomp'un yazarları teklif tüm programları bir mimari kontrolüyle başlatın (geçerli mimari bağlamda bir alan olarak gösterilir) arch yapı struct seccomp_data). Mimari kontrol edildiğinde örneğin başlangıcı şöyle görünecektir:

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

ve daha sonra sistem çağrı numaralarımız belirli değerler alacaktır.

Seccomp için filtreleri kullanarak yazıp yüklüyoruz libseccomp

Filtreleri yerel kodda veya BPF derlemesinde yazmak, sonuç üzerinde tam kontrole sahip olmanızı sağlar, ancak aynı zamanda bazen taşınabilir ve/veya okunabilir koda sahip olmak tercih edilir. Kütüphane bize bu konuda yardımcı olacaktır. libseccompsiyah veya beyaz filtreler yazmak için standart bir arayüz sağlar.

Örneğin, önceden sistem çağrılarının kara listesini yüklemiş olan, kullanıcının seçtiği ikili dosyayı çalıştıran bir program yazalım. yukarıdaki makale (program daha iyi okunabilirlik sağlamak için basitleştirilmiştir, tam sürümünü burada bulabilirsiniz burada):

#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 önce bir dizi tanımlıyoruz sys_numbers Engellenecek 40'tan fazla sistem çağrısı numarası. Ardından bağlamı başlatın ctx ve kütüphaneye neye izin vermek istediğimizi söyleyin (SCMP_ACT_ALLOW) varsayılan olarak tüm sistem çağrıları (kara listeler oluşturmak daha kolaydır). Daha sonra kara listedeki tüm sistem çağrılarını tek tek ekliyoruz. Listeden yapılan bir sistem çağrısına yanıt olarak şunları talep ediyoruz: SCMP_ACT_TRAP, bu durumda seccomp sürece bir sinyal gönderecektir SIGSYS hangi sistem çağrısının kuralları ihlal ettiğinin açıklamasıyla. Son olarak programı çekirdeğe yüklüyoruz. seccomp_loadprogramı derleyecek ve bir sistem çağrısı kullanarak sürece ekleyecek seccomp(2).

Başarılı bir derleme için programın kütüphaneye bağlanması gerekir libseccompÖrneğin:

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

Başarılı bir lansman örneği:

$ ./seccomp_lib echo ok
ok

Engellenen bir sistem çağrısı örneği:

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

Kullanırız stracedetaylar için:

$ 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

Yasa dışı sistem çağrısı kullanımı nedeniyle programın sonlandırıldığını nasıl bilebiliriz? mount(2).

Kütüphaneyi kullanarak bir filtre yazdık libseccomp, önemsiz olmayan kodu dört satıra sığdırmak. Yukarıdaki örnekte, çok sayıda sistem çağrısı varsa, kontrol yalnızca bir karşılaştırma listesi olduğundan yürütme süresi gözle görülür şekilde azaltılabilir. Optimizasyon için libseccomp yakın zamanda yama dahilfiltre özelliği için destek ekleyen SCMP_FLTATR_CTL_OPTIMIZE. Bu özelliğin 2'ye ayarlanması, filtreyi ikili arama programına dönüştürecektir.

İkili arama filtrelerinin nasıl çalıştığını görmek istiyorsanız şu adrese göz atın: basit senaryo, sistem çağrı numaralarını çevirerek BPF derleyicisinde bu tür programlar oluşturur, örneğin:

$ 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 programları girinti atlamaları gerçekleştiremediği için önemli ölçüde daha hızlı bir şey yazmak imkansızdır (örneğin, jmp A veya jmp [label+X]) ve bu nedenle tüm geçişler statiktir.

seccomp ve strace

Faydasını herkes biliyor strace Linux'ta süreçlerin davranışını incelemek için vazgeçilmez bir araçtır. Ancak birçok kişi şunu da duymuştur: performans sorunları Bu yardımcı programı kullanırken. Gerçek şu ki strace kullanılarak uygulandı ptrace(2)ve bu mekanizmada, süreci durdurmak için hangi sistem çağrıları setine ihtiyacımız olduğunu, örneğin komutları belirleyemeyiz.

$ 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

yaklaşık olarak aynı sürede işlenir, ancak ikinci durumda yalnızca bir sistem çağrısını izlemek istiyoruz.

Yeni seçenek --seccomp-bpf, ilave strace sürüm 5.3, süreci birçok kez hızlandırmanıza olanak tanır ve bir sistem çağrısının izinde başlatma süresi, normal başlatma süresiyle zaten karşılaştırılabilir:

$ 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

(Tabii ki burada bu komutun ana sistem çağrısının izini sürmememiz gibi ufak bir yanılgı var. Eğer takip ediyor olsaydık örneğin newfsstatSonra strace olmadığı kadar sert fren yapardım --seccomp-bpf.)

Bu seçenek nasıl çalışır? Onsuz strace sürece bağlanır ve onu kullanarak başlatır PTRACE_SYSCALL. Yönetilen bir süreç (herhangi bir) sistem çağrısı yayınladığında kontrol, stracesistem çağrısının argümanlarına bakan ve onu çalıştıran PTRACE_SYSCALL. Bir süre sonra işlem sistem çağrısını tamamlar ve sistem çağrısından çıkıldığında kontrol tekrar aktarılır stracedönüş değerlerine bakan ve işlemi kullanarak başlatan PTRACE_SYSCALL, ve benzeri.

Küçükler için BPF, sıfır bölüm: klasik BPF

Ancak seccomp ile bu süreç tam olarak istediğimiz gibi optimize edilebilir. Yani sadece sistem çağrısına bakmak istiyorsak X, o zaman şunun için bir BPF filtresi yazabiliriz: X bir değer döndürür SECCOMP_RET_TRACEve bizi ilgilendirmeyen aramalar için - SECCOMP_RET_ALLOW:

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

Bu durumda, strace başlangıçta süreci şu şekilde başlatır: PTRACE_CONT, filtremiz her sistem çağrısı için işlenir, eğer sistem çağrısı yapılmazsa X, süreç çalışmaya devam eder, ancak eğer bu X, ardından seccomp kontrolü aktaracak straceargümanlara bakacak ve süreci şöyle başlatacak PTRACE_SYSCALL (çünkü seccomp, sistem çağrısından çıkışta bir programı çalıştırma yeteneğine sahip değildir). Sistem çağrısı geri döndüğünde, strace kullanarak işlemi yeniden başlatacak PTRACE_CONT ve seccomp'tan yeni mesajlar bekleyecek.

Küçükler için BPF, sıfır bölüm: klasik BPF

Seçeneği kullanırken --seccomp-bpf iki kısıtlama var. Öncelikle mevcut bir sürece katılmak mümkün olmayacaktır (seçenek -p program strace), çünkü bu seccomp tarafından desteklenmemektedir. İkinci olarak hiçbir ihtimal yok hayır alt süreçlere bakın, çünkü seccomp filtreleri bunu devre dışı bırakma yeteneği olmadan tüm alt süreçler tarafından miras alınır.

Tam olarak nasıl olduğuna dair biraz daha ayrıntı strace ile çalışır seccomp şuradan bulunabilir: son rapor. Bizim için en ilginç gerçek, seccomp tarafından temsil edilen klasik BPF'nin bugün hala kullanılıyor olmasıdır.

xt_bpf

Şimdi ağ dünyasına geri dönelim.

Arka plan: uzun zaman önce, 2007'de çekirdek katma modül xt_u32 net filtresi için. Çok daha eski bir trafik sınıflandırıcısına benzetilerek yazılmıştır. cls_u32 ve aşağıdaki basit işlemleri kullanarak iptables için isteğe bağlı ikili kurallar yazmanıza izin verdi: bir paketten 32 bit yükleyin ve bunlar üzerinde bir dizi aritmetik işlem gerçekleştirin. Örneğin,

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

Dolgu 32'dan başlayarak IP başlığının 6 bitini yükler ve bunlara bir maske uygular 0xFF (düşük baytı alın). Bu alan protocol IP başlığını ve bunu 1 (ICMP) ile karşılaştırıyoruz. Birçok kontrolü tek bir kuralda birleştirebilirsiniz ve ayrıca operatörü çalıştırabilirsiniz. @ — X baytı sağa taşıyın. Örneğin, kural

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

TCP Sıra Numarasının eşit olup olmadığını kontrol eder 0x29. Daha fazla ayrıntıya girmeyeceğim çünkü bu tür kuralları elle yazmanın pek uygun olmadığı zaten açık. Makalede BPF - unutulan bayt koduiçin kullanım ve kural oluşturma örnekleri içeren çeşitli bağlantılar vardır. xt_u32. Bu makalenin sonundaki bağlantılara da bakın.

2013'ten beri modül yerine modül xt_u32 BPF tabanlı bir modül kullanabilirsiniz xt_bpf. Buraya kadar okuyan herkes, çalışma prensibini açıkça anlamış olmalıdır: BPF bayt kodunu iptables kuralları olarak çalıştırın. Örneğin şunun gibi yeni bir kural oluşturabilirsiniz:

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

burada <байткод> - bu, montajcı çıktı formatındaki koddur bpf_asm varsayılan olarak örneğin,

$ 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

Bu örnekte tüm UDP paketlerini filtreliyoruz. Bir modüldeki BPF programının bağlamı xt_bpfelbette iptables durumunda paket verilerine IPv4 başlığının başlangıcına işaret eder. BPF programından dönüş değeri booleanNerede false paketin eşleşmediği anlamına gelir.

Modülün olduğu açıktır. xt_bpf yukarıdaki örnekten daha karmaşık filtreleri destekler. Cloudfare'den gerçek örneklere bakalım. Yakın zamana kadar bu modülü kullanıyorlardı xt_bpf DDoS saldırılarına karşı korunmak için. Makalede BPF Araçlarının Tanıtımı BPF filtrelerini nasıl (ve neden) oluşturduklarını açıklıyorlar ve bu tür filtrelerin oluşturulmasına yönelik bir dizi yardımcı programa bağlantılar yayınlıyorlar. Örneğin, yardımcı programı kullanarak bpfgen bir ad için DNS sorgusuyla eşleşen bir BPF programı oluşturabilirsiniz 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

Programda ilk önce kayıt defterine yüklüyoruz X hat adresinin başlangıcı x04habrx03comx00 bir UDP datagramının içinde bulun ve ardından isteği kontrol edin: 0x04686162 <-> "x04hab" vb

Kısa bir süre sonra Cloudfare p0f -> BPF derleyici kodunu yayınladı. Makalede p0f BPF derleyicisine giriş p0f'nin ne olduğu ve p0f imzalarının BPF'ye nasıl dönüştürüleceği hakkında konuşuyorlar:

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

Şu anda artık Cloudfare kullanmıyorum xt_bpf, BPF'nin yeni sürümünü kullanma seçeneklerinden biri olan XDP'ye taşındıklarından beri, bkz. L4Drop: XDP DDoS Azaltımları.

cls_bpf

Çekirdekte klasik BPF kullanımının son örneği sınıflandırıcıdır cls_bpf Linux'taki trafik kontrol alt sistemi için, 2013'ün sonunda Linux'a eklendi ve kavramsal olarak eski sistemin yerini aldı. cls_u32.

Ancak şimdi çalışmayı tarif etmeyeceğiz. cls_bpf, çünkü klasik BPF hakkındaki bilgi açısından bu bize hiçbir şey vermeyecektir - zaten tüm işlevselliğe aşina olduk. Ayrıca Genişletilmiş BPF'den bahseden sonraki makalelerimizde bu sınıflandırıcıyla birden çok kez karşılaşacağız.

Klasik BPF c kullanımı hakkında konuşmamanın bir başka nedeni cls_bpf Sorun, Genişletilmiş BPF ile karşılaştırıldığında bu durumda uygulanabilirlik kapsamının radikal bir şekilde daraltılmış olmasıdır: klasik programlar paketlerin içeriğini değiştiremez ve çağrılar arasında durumu kaydedemez.

Artık klasik BPF'ye veda edip geleceğe bakmanın zamanı geldi.

Klasik BPF'ye veda

Doksanlı yılların başında geliştirilen BPF teknolojisinin çeyrek yüzyıl boyunca nasıl başarılı bir şekilde yaşadığını ve sonuna kadar nasıl yeni uygulamalar bulduğuna baktık. Bununla birlikte, klasik BPF'nin geliştirilmesine ivme kazandıran yığın makinelerden RISC'ye geçişe benzer şekilde, 32'li yıllarda 64 bit makinelerden XNUMX bit makinelere geçiş yaşandı ve klasik BPF geçerliliğini yitirmeye başladı. Ek olarak, klasik BPF'nin yetenekleri çok sınırlıdır ve eski mimariye ek olarak - BPF programlarına yapılan çağrılar arasında durumu kaydetme yeteneğimiz yok, doğrudan kullanıcı etkileşimi olasılığı yok, etkileşim olasılığı yok Sınırlı sayıda yapı alanını okumak dışında çekirdek ile sk_buff ve en basit yardımcı fonksiyonları çalıştırarak paketlerin içeriğini değiştiremez ve yeniden yönlendiremezsiniz.

Aslında, şu anda Linux'ta klasik BPF'den geriye kalan tek şey API arayüzüdür ve çekirdeğin içindeki tüm klasik programlar, ister soket filtreleri ister seccomp filtreleri olsun, otomatik olarak yeni bir format olan Genişletilmiş BPF'ye çevrilir. (Bir sonraki makalede bunun tam olarak nasıl gerçekleştiğinden bahsedeceğiz.)

Yeni mimariye geçiş, 2013 yılında Alexey Starovoitov'un bir BPF güncelleme şeması önermesiyle başladı. 2014 yılında ilgili yamalar görünmeye başladı çekirdekte. Anladığım kadarıyla, ilk plan yalnızca mimariyi ve JIT derleyicisini 64 bit makinelerde daha verimli çalışacak şekilde optimize etmekti, ancak bu optimizasyonlar Linux geliştirmede yeni bir bölümün başlangıcını işaret ediyordu.

Bu serideki diğer makaleler, başlangıçta dahili BPF, daha sonra genişletilmiş BPF ve şimdi sadece BPF olarak bilinen yeni teknolojinin mimarisini ve uygulamalarını kapsayacaktır.

referanslar

  1. Steven McCanne ve Van Jacobson, "BSD Paket Filtresi: Kullanıcı Seviyesinde Paket Yakalama için Yeni Bir Mimari", https://www.tcpdump.org/papers/bpf-usenix93.pdf
  2. Steven McCanne, "libpcap: Paket Yakalama için Bir Mimari ve Optimizasyon Metodolojisi", https://sharkfestus.wireshark.org/sharkfest.11/presentations/McCanne-Sharkfest'11_Keynote_Address.pdf
  3. tcpdump, libpcap: https://www.tcpdump.org/
  4. IPtable U32 Maç Eğitimi.
  5. BPF - unutulan bayt kodu: https://blog.cloudflare.com/bpf-the-forgotten-bytecode/
  6. BPF Aracının Tanıtımı: https://blog.cloudflare.com/introducing-the-bpf-tools/
  7. bpf_cls: http://man7.org/linux/man-pages/man8/tc-bpf.8.html
  8. Seccomp'a genel bakış: https://lwn.net/Articles/656307/
  9. https://github.com/torvalds/linux/blob/master/Documentation/userspace-api/seccomp_filter.rst
  10. habr: Konteynerler ve güvenlik: seccomp
  11. habr: Arka plan programlarını systemd ile yalıtmak veya "bunun için Docker'a ihtiyacınız yok!"
  12. Paul Chaignon, "strace --seccomp-bpf: başlığın altına bir bakış", https://fosdem.org/2020/schedule/event/debugging_strace_bpf/
  13. netsniff-ng: http://netsniff-ng.org/

Kaynak: habr.com

Yorum ekle