XDP üzerinde DDoS saldırılarına karşı koruma yazıyoruz. nükleer kısım

eXpress Veri Yolu (XDP) teknolojisi, paketler çekirdek ağ yığınına girmeden önce Linux arayüzlerinde rastgele trafik işlemenin gerçekleştirilmesine olanak tanır. XDP uygulaması - DDoS saldırılarına (CloudFlare), karmaşık filtrelere, istatistik toplamaya (Netflix) karşı koruma. XDP programları eBPF sanal makinesi tarafından yürütülür, dolayısıyla filtre türüne bağlı olarak hem kodlarında hem de mevcut çekirdek işlevlerinde kısıtlamalar vardır.

Makale, XDP'deki çok sayıda materyalin eksikliklerini gidermeyi amaçlamaktadır. İlk olarak, XDP'nin özelliklerini hemen atlayan hazır kod sağlarlar: doğrulama için hazırlanmıştır veya sorun yaratmayacak kadar basittir. Daha sonra kodunuzu sıfırdan yazmaya çalıştığınızda, tipik hatalarla ne yapacağınız hakkında hiçbir fikriniz olmaz. İkinci olarak, XDP'yi sanal makine ve donanım olmadan yerel olarak test etme yolları, kendi tuzakları olmasına rağmen kapsanmamaktadır. Metin, XDP ve eBPF ile ilgilenen, ağ oluşturma ve Linux'a aşina programcılar için hazırlanmıştır.

Bu bölümde XDP filtresinin nasıl birleştirildiğini ve nasıl test edileceğini detaylı olarak anlayacağız, ardından iyi bilinen SYN çerezleri mekanizmasının paket işleme düzeyinde basit bir versiyonunu yazacağız. Henüz bir “beyaz liste” oluşturmayacağız
doğrulanmış istemciler, sayaçları tutmak ve filtreyi yönetmek için yeterli günlük.

C ile yazacağız - moda değil ama pratik. Kodun tamamına sondaki bağlantıdan GitHub üzerinden ulaşabilirsiniz ve makalede anlatılan aşamalara göre commitlere bölünmüştür.

Yasal Uyarı. Bu yazı boyunca DDoS saldırılarını engellemek için mini bir çözüm geliştireceğim çünkü bu, XDP ve benim uzmanlık alanım için gerçekçi bir görev. Ancak asıl amaç teknolojiyi anlamaktır; bu hazır koruma oluşturma rehberi değildir. Eğitim kodu optimize edilmemiştir ve bazı nüansları atlamaktadır.

XDP'ye Kısa Genel Bakış

Belgeleri ve mevcut makaleleri çoğaltmamak için yalnızca önemli noktaları özetleyeceğim.

Böylece filtre kodu çekirdeğe yüklenir. Gelen paketler filtreye geçirilir. Sonuç olarak filtrenin bir karar vermesi gerekir: paketi çekirdeğe iletin (XDP_PASS), paketi bırak (XDP_DROP) veya geri gönderin (XDP_TX). Filtre paketi değiştirebilir; bu özellikle aşağıdakiler için geçerlidir: XDP_TX. Ayrıca programı iptal edebilirsiniz (XDP_ABORTED) ve paketi sıfırlayın, ancak bu benzer assert(0) - hata ayıklama için.

eBPF (genişletilmiş Berkley Paket Filtresi) sanal makinesi, çekirdeğin kodun döngüye girmediğini ve diğer insanların hafızasına zarar vermediğini kontrol edebilmesi için kasıtlı olarak basitleştirilmiştir. Kümülatif kısıtlamalar ve kontroller:

  • Döngüler (geriye doğru) yasaktır.
  • Veriler için bir yığın var, ancak hiçbir işlev yok (tüm C işlevlerinin satır içi olması gerekir).
  • Yığın ve paket arabelleği dışındaki belleğe erişimler yasaktır.
  • Kod boyutu sınırlıdır ancak pratikte bu çok önemli değildir.
  • Yalnızca özel çekirdek işlevlerine (eBPF yardımcıları) yapılan çağrılara izin verilir.

Bir filtre tasarlamak ve kurmak şuna benzer:

  1. Kaynak kodu (örn. kernel.c) nesneye derlenir (kernel.o) eBPF sanal makine mimarisi için. Ekim 2019 itibarıyla eBPF'ye derleme Clang tarafından destekleniyor ve GCC 10.1'de vaat ediliyor.
  2. Bu nesne kodu, çekirdek yapılarına (örneğin tablolar ve sayaçlar) çağrılar içeriyorsa, bunların kimlikleri sıfırlarla değiştirilir; bu, bu tür kodun çalıştırılamayacağı anlamına gelir. Çekirdeğe yüklemeden önce, bu sıfırları, çekirdek çağrıları yoluyla oluşturulan belirli nesnelerin kimlikleriyle değiştirmeniz gerekir (kodu bağlayın). Bunu harici yardımcı programlarla yapabilirsiniz veya belirli bir filtreyi bağlayacak ve yükleyecek bir program yazabilirsiniz.
  3. Çekirdek yüklenen programı doğrular. Döngülerin olmaması ve paket ve yığın sınırlarının aşılmaması kontrol edilir. Doğrulayıcı kodun doğruluğunu kanıtlayamazsa program reddedilir; onu memnun edebilmeniz gerekir.
  4. Başarılı doğrulamanın ardından çekirdek, eBPF mimarisi nesne kodunu sistem mimarisi için makine koduna derler (tam zamanında).
  5. Program arayüze bağlanır ve paketleri işlemeye başlar.

XDP çekirdekte çalıştığı için hata ayıklama, izleme günlükleri ve aslında programın filtrelediği veya oluşturduğu paketler kullanılarak gerçekleştirilir. Ancak eBPF, indirilen kodun sistem için güvenli olmasını sağlar, böylece XDP'yi doğrudan yerel Linux'unuzda deneyebilirsiniz.

Ortamın Hazırlanması

montaj

Clang, eBPF mimarisi için doğrudan nesne kodu üretemez, dolayısıyla süreç iki adımdan oluşur:

  1. C kodunu LLVM bayt koduna derleyin (clang -emit-llvm).
  2. Bayt kodunu eBPF nesne koduna dönüştürün (llc -march=bpf -filetype=obj).

Filtre yazarken yardımcı fonksiyon ve makro içeren birkaç dosya faydalı olacaktır çekirdek testlerinden. Çekirdek sürümüyle eşleşmeleri önemlidir (KVER). Bunları şuraya indirin: helpers/:

export KVER=v5.3.7
export BASE=https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/plain/tools/testing/selftests/bpf
wget -P helpers --content-disposition "${BASE}/bpf_helpers.h?h=${KVER}" "${BASE}/bpf_endian.h?h=${KVER}"
unset KVER BASE

Arch Linux için Makefile (çekirdek 5.3.7):

CLANG ?= clang
LLC ?= llc

KDIR ?= /lib/modules/$(shell uname -r)/build
ARCH ?= $(subst x86_64,x86,$(shell uname -m))

CFLAGS = 
    -Ihelpers 
    
    -I$(KDIR)/include 
    -I$(KDIR)/include/uapi 
    -I$(KDIR)/include/generated/uapi 
    -I$(KDIR)/arch/$(ARCH)/include 
    -I$(KDIR)/arch/$(ARCH)/include/generated 
    -I$(KDIR)/arch/$(ARCH)/include/uapi 
    -I$(KDIR)/arch/$(ARCH)/include/generated/uapi 
    -D__KERNEL__ 
    
    -fno-stack-protector -O2 -g

xdp_%.o: xdp_%.c Makefile
    $(CLANG) -c -emit-llvm $(CFLAGS) $< -o - | 
    $(LLC) -march=bpf -filetype=obj -o $@

.PHONY: all clean

all: xdp_filter.o

clean:
    rm -f ./*.o

KDIR çekirdek başlıklarına giden yolu içerir, ARCH - sistem mimarisi. Yollar ve araçlar dağıtımlar arasında biraz farklılık gösterebilir.

Debian 10 (çekirdek 4.19.67) için farklılıklara örnek

# другая команда
CLANG ?= clang
LLC ?= llc-7

# другой каталог
KDIR ?= /usr/src/linux-headers-$(shell uname -r)
ARCH ?= $(subst x86_64,x86,$(shell uname -m))

# два дополнительных каталога -I
CFLAGS = 
    -Ihelpers 
    
    -I/usr/src/linux-headers-4.19.0-6-common/include 
    -I/usr/src/linux-headers-4.19.0-6-common/arch/$(ARCH)/include 
    # далее без изменений

CFLAGS yardımcı başlıklara sahip bir dizine ve çekirdek başlıklarına sahip birkaç dizine bağlanın. Sembol __KERNEL__ filtre çekirdekte yürütüldüğünden, UAPI (kullanıcı alanı API) başlıklarının çekirdek kodu için tanımlandığı anlamına gelir.

Yığın koruması devre dışı bırakılabilir (-fno-stack-protector), çünkü eBPF kod doğrulayıcı hala yığın dışı sınır ihlallerini kontrol ediyor. eBPF bayt kodunun boyutu sınırlı olduğundan optimizasyonları hemen açmaya değer.

Tüm paketleri geçiren ve hiçbir şey yapmayan bir filtreyle başlayalım:

#include <uapi/linux/bpf.h>

#include <bpf_helpers.h>

SEC("prog")
int xdp_main(struct xdp_md* ctx) {
    return XDP_PASS;
}

char _license[] SEC("license") = "GPL";

Ekip make toplar xdp_filter.o. Şimdi nerede denemeli?

deneme standı

Stand iki arayüz içermelidir: filtrenin bulunacağı ve paketlerin gönderileceği. Normal uygulamaların filtremizle nasıl çalıştığını kontrol etmek için bunların kendi IP'lerine sahip tam teşekküllü Linux cihazları olması gerekir.

Veth (sanal Ethernet) tipindeki cihazlar bizim için uygundur: bunlar doğrudan birbirine "bağlı" bir çift sanal ağ arayüzüdür. Bunları bu şekilde oluşturabilirsiniz (bu bölümde tüm komutlar ip itibaren gerçekleştirilir root):

ip link add xdp-remote type veth peer name xdp-local

öyle xdp-remote и xdp-local — cihaz adları. Açık xdp-local (192.0.2.1/24) filtre eklenecektir. xdp-remote (192.0.2.2/24) gelen trafik gönderilecektir. Ancak bir sorun var: arayüzler aynı makinede ve Linux bunlardan birine diğerine trafik göndermiyor. Bunu zor kurallarla çözebilirsiniz iptables, ancak paketleri değiştirmeleri gerekecek ve bu da hata ayıklama için uygun değildir. Ağ ad alanlarını (bundan sonra netns olarak anılacaktır) kullanmak daha iyidir.

Bir ağ ad alanı, diğer ağlardaki benzer nesnelerden yalıtılmış bir dizi arabirim, yönlendirme tabloları ve NetFilter kuralları içerir. Her süreç bir ad alanında çalışır ve yalnızca o ağın nesnelerine erişebilir. Varsayılan olarak sistemin tüm nesneler için tek bir ağ ad alanı vardır, dolayısıyla Linux'ta çalışabilir ve ağlar hakkında bilgi sahibi olmayabilirsiniz.

Yeni bir ad alanı oluşturalım xdp-test ve onu oraya taşı xdp-remote.

ip netns add xdp-test
ip link set dev xdp-remote netns xdp-test

Daha sonra süreç çalışıyor xdp-test, görmeyecek" xdp-local (varsayılan olarak ağlarda kalacaktır) ve 192.0.2.1'e paket gönderirken onu iletecektir xdp-remoteçünkü 192.0.2.0/24'te bu işleme erişilebilen tek arayüz budur. Bu aynı zamanda ters yönde de çalışır.

Ağlar arasında geçiş yaparken arayüz kapanıyor ve adresini kaybediyor. Arayüzü netns'te yapılandırmak için çalıştırmanız gerekir. ip ... bu komut ad alanında ip netns exec:

ip netns exec xdp-test 
    ip address add 192.0.2.2/24 dev xdp-remote
ip netns exec xdp-test 
    ip link set xdp-remote up

Gördüğünüz gibi bu ayardan farklı değil xdp-local varsayılan ad alanında:

    ip address add 192.0.2.1/24 dev xdp-local
    ip link set xdp-local up

Eğer koşarsan tcpdump -tnevi xdp-local, şuradan gönderilen paketlerin olduğunu görebilirsiniz: xdp-test, bu arayüze iletilir:

ip netns exec xdp-test   ping 192.0.2.1

Bir kabuk başlatmak uygundur xdp-test. Depoda standla çalışmayı otomatikleştiren bir komut dosyası bulunur; örneğin, standı şu komutla yapılandırabilirsiniz: sudo ./stand up ve onu sil sudo ./stand down.

İzleme

Filtre cihazla şu şekilde ilişkilendirilir:

ip -force link set dev xdp-local xdp object xdp_filter.o verbose

Anahtar -force Başka bir program zaten bağlıysa yeni bir programın bağlanması gerekir. “Hiçbir haber iyi haber değildir” bu emirle ilgili değil, sonuç zaten hacimli. belirtmek verbose isteğe bağlıdır, ancak bununla birlikte kod doğrulayıcının çalışmasına ilişkin bir derleme listesiyle birlikte bir rapor görüntülenir:

Verifier analysis:

0: (b7) r0 = 2
1: (95) exit

Programın arayüzle bağlantısını kaldırın:

ip link set dev xdp-local xdp off

Komut dosyasında bunlar komutlardır sudo ./stand attach и sudo ./stand detach.

Bir filtre takarak şunları sağlayabilirsiniz: ping çalışmaya devam ediyor ancak program çalışıyor mu? Günlükleri ekleyelim. İşlev bpf_trace_printk() benzer printf(), ancak kalıp dışında yalnızca üç bağımsız değişkeni ve sınırlı sayıda belirticiyi destekler. Makro bpf_printk() aramayı basitleştirir.

   SEC("prog")
   int xdp_main(struct xdp_md* ctx) {
+      bpf_printk("got packet: %pn", ctx);
       return XDP_PASS;
   }

Çıktı, etkinleştirilmesi gereken çekirdek izleme kanalına gider:

echo -n 1 | sudo tee /sys/kernel/debug/tracing/options/trace_printk

Mesaj dizisini görüntüle:

cat /sys/kernel/debug/tracing/trace_pipe

Bu komutların her ikisi de bir çağrı yapar sudo ./stand log.

Ping artık şuna benzer mesajları tetiklemelidir:

<...>-110930 [004] ..s1 78803.244967: 0: got packet: 00000000ac510377

Doğrulayıcının çıktısına yakından bakarsanız tuhaf hesaplamalar fark edeceksiniz:

0: (bf) r3 = r1
1: (18) r1 = 0xa7025203a7465
3: (7b) *(u64 *)(r10 -8) = r1
4: (18) r1 = 0x6b63617020746f67
6: (7b) *(u64 *)(r10 -16) = r1
7: (bf) r1 = r10
8: (07) r1 += -16
9: (b7) r2 = 16
10: (85) call bpf_trace_printk#6
<...>

Gerçek şu ki, eBPF programlarının bir veri bölümü yoktur, dolayısıyla bir format dizesini kodlamanın tek yolu VM komutlarının doğrudan argümanlarıdır:

$ python -c "import binascii; print(bytes(reversed(binascii.unhexlify('0a7025203a74656b63617020746f67'))))"
b'got packet: %pn'

Bu nedenle hata ayıklama çıktısı, ortaya çıkan kodu büyük ölçüde şişirir.

XDP Paketlerini Gönderme

Filtreyi değiştirelim: gelen tüm paketleri geri göndermesine izin verin. Bu, ağ açısından yanlıştır, çünkü başlıklardaki adresleri değiştirmek gerekli olacaktır, ancak şimdi prensip olarak çalışma önemlidir.

       bpf_printk("got packet: %pn", ctx);
-      return XDP_PASS;
+      return XDP_TX;
   }

koşmak tcpdump üzerinde xdp-remote. Aynı giden ve gelen ICMP Yankı İsteğini göstermeli ve ICMP Yankı Yanıtını göstermeyi bırakmalıdır. Ama görünmüyor. İş için olduğu ortaya çıktı XDP_TX programda xdp-local gerekliçift ​​arayüzüne xdp-remote boş da olsa bir program da verildi ve büyütüldü.

Bunu nasıl bildim?

Çekirdekteki bir paketin yolunu izleme Mükemmel olay mekanizması, bu arada, aynı sanal makinenin kullanılmasına izin verir, yani eBPF, eBPF ile sökmeler için kullanılır.

Kötülüğü iyilik yapmalısın, çünkü onu çıkaracak başka bir şey yok.

$ sudo perf trace --call-graph dwarf -e 'xdp:*'
   0.000 ping/123455 xdp:xdp_bulk_tx:ifindex=19 action=TX sent=0 drops=1 err=-6
                                     veth_xdp_flush_bq ([veth])
                                     veth_xdp_flush_bq ([veth])
                                     veth_poll ([veth])
                                     <...>

Kod 6 nedir?

$ errno 6
ENXIO 6 No such device or address

Fonksiyon veth_xdp_flush_bq() adresinden bir hata kodu alır veth_xdp_xmit(), nereye göre ara ENXIO ve yorumu bulun.

Minimum filtreyi geri yükleyelim (XDP_PASS) dosyada xdp_dummy.c, Makefile'a ekleyin, ona bağlayın xdp-remote:

ip netns exec remote 
    ip link set dev int xdp object dummy.o

şimdi tcpdump beklendiğini gösteriyor:

62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), length 84)
    192.0.2.2 > 192.0.2.1: ICMP echo request, id 46966, seq 1, length 64
62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), length 84)
    192.0.2.2 > 192.0.2.1: ICMP echo request, id 46966, seq 1, length 64

Bunun yerine yalnızca ARP'ler gösteriliyorsa filtreleri kaldırmanız gerekir (bu, sudo ./stand detach), bırak ping, ardından filtreleri ayarlayıp tekrar deneyin. Sorun şu ki, filtre XDP_TX hem ARP'de hem de yığında geçerlidir
ad alanları xdp-test 192.0.2.1 MAC adresini "unutmayı" başardıysa, bu IP'yi çözemeyecektir.

Sorunun formüle edilmesi

Belirtilen göreve geçelim: XDP'ye bir SYN çerezleri mekanizması yazın.

SYN Flood, özü aşağıdaki gibi olan popüler bir DDoS saldırısı olmaya devam ediyor. Bağlantı kurulduğunda (TCP anlaşması), sunucu bir SYN alır, gelecekteki bağlantı için kaynakları tahsis eder, SYNACK paketiyle yanıt verir ve ACK'yı bekler. Saldırgan, binlerce güçlü botnet'teki her bir ana bilgisayarın sahte adreslerinden saniyede binlerce SYN paketi gönderir. Sunucu, paketin gelmesi üzerine kaynakları hemen tahsis etmek zorunda kalır, ancak uzun bir zaman aşımından sonra bunları serbest bırakır; bunun sonucunda bellek veya sınırlar tükenir, yeni bağlantılar kabul edilmez ve hizmet kullanılamaz.

Kaynakları SYN paketine göre tahsis etmezseniz ve yalnızca SYNACK paketiyle yanıt verirseniz, sunucu daha sonra gelen ACK paketinin kaydedilmemiş bir SYN paketine atıfta bulunduğunu nasıl anlayabilir? Sonuçta bir saldırgan sahte ACK'ler de oluşturabilir. SYN çerezinin amacı onu kodlamaktır. seqnum adreslerin, bağlantı noktalarının ve değişen tuzun karma değeri olarak bağlantı parametreleri. ACK, tuz değiştirilmeden önce ulaşmayı başardıysa, hash'i tekrar hesaplayabilir ve bunu karşılaştırabilirsiniz. acknum. Dövmek acknum Saldırganın bunu yapması mümkün değildir, çünkü tuz sırrı içermektedir ve kanalın sınırlı olması nedeniyle bunu çözecek zamanı olmayacaktır.

SYN çerezi uzun süredir Linux çekirdeğinde uygulanmaktadır ve hatta SYN'lerin çok hızlı ve toplu olarak gelmesi durumunda otomatik olarak etkinleştirilebilir.

TCP anlaşmasına ilişkin eğitim programı

TCP, bayt akışı olarak veri iletimi sağlar; örneğin, HTTP istekleri TCP üzerinden iletilir. Akış paketler halinde parçalar halinde iletilir. Tüm TCP paketlerinde mantıksal bayraklar ve 32 bitlik sıra numaraları bulunur:

  • Bayrakların kombinasyonu belirli bir paketin rolünü belirler. SYN bayrağı, bunun gönderenin bağlantıdaki ilk paketi olduğunu gösterir. ACK bayrağı, gönderenin bayta kadar tüm bağlantı verilerini aldığı anlamına gelir acknum. Bir paket birden fazla bayrağa sahip olabilir ve bunların birleşimiyle çağrılır; örneğin bir SYNACK paketi.

  • Sıra numarası (sıra numarası), bu pakette iletilen ilk bayt için veri akışındaki uzaklığı belirtir. Örneğin, X bayt veri içeren ilk pakette bu sayı N ise, yeni veri içeren sonraki pakette N+X olacaktır. Bağlantının başlangıcında her iki taraf da bu sayıyı rastgele seçer.

  • Onay numarası (acknum) - sıra numarasıyla aynı ofsettir, ancak iletilen baytın sayısını değil, gönderenin görmediği alıcıdan gelen ilk baytın sayısını belirler.

Bağlantının başlangıcında tarafların anlaşması gerekir. seqnum и acknum. İstemci kendisiyle birlikte bir SYN paketi gönderir. seqnum = X. Sunucu bir SYNACK paketiyle yanıt verir ve burada verilerini kaydeder. seqnum = Y ve ortaya çıkarır acknum = X + 1. İstemci SYNACK'e bir ACK paketiyle yanıt verir; seqnum = X + 1, acknum = Y + 1. Bundan sonra gerçek veri aktarımı başlar.

Eş, paketin alındığını onaylamazsa, TCP zaman aşımından sonra paketi yeniden gönderir.

SYN çerezleri neden her zaman kullanılmıyor?

İlk olarak, SYNACK veya ACK kaybolursa tekrar gönderilmesini beklemeniz gerekir; bağlantı kurulumu yavaşlar. İkincisi, SYN paketinde - ve yalnızca içinde! — bağlantının daha fazla çalışmasını etkileyen bir dizi seçenek iletilir. Sunucu, gelen SYN paketlerini hatırlamadan bu seçenekleri göz ardı eder; istemci bunları sonraki paketlerde göndermez. TCP bu durumda çalışabilir ancak en azından ilk aşamada bağlantının kalitesi düşecektir.

Paketler açısından bakıldığında bir XDP programının aşağıdakileri yapması gerekir:

  • SYN'ye bir çerezle SYNACK ile yanıt verin;
  • ACK'ya RST ile yanıt verin (bağlantıyı kes);
  • kalan paketleri atın.

Paket ayrıştırmayla birlikte algoritmanın sözde kodu:

Если это не Ethernet,
    пропустить пакет.
Если это не IPv4,
    пропустить пакет.
Если адрес в таблице проверенных,               (*)
        уменьшить счетчик оставшихся проверок,
        пропустить пакет.
Если это не TCP,
    сбросить пакет.     (**)
Если это SYN,
    ответить SYN-ACK с cookie.
Если это ACK,
    если в acknum лежит не cookie,
        сбросить пакет.
    Занести в таблицу адрес с N оставшихся проверок.    (*)
    Ответить RST.   (**)
В остальных случаях сбросить пакет.

bir (*) sistemin durumunu yönetmeniz gereken noktalar işaretlenmiştir - ilk aşamada, bir sıralı sayı olarak bir SYN çerezi oluşturarak TCP el sıkışmasını uygulayarak onlarsız yapabilirsiniz.

Yerinde (**), masamız yokken paketi atlayacağız.

TCP anlaşmasının uygulanması

Paketi ayrıştırma ve kodu doğrulama

Ağ başlık yapılarına ihtiyacımız olacak: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) ve TCP (uapi/linux/tcp.h). İlgili hatalar nedeniyle ikincisini bağlayamadım atomic64_t, gerekli tanımları koda kopyalamam gerekiyordu.

Çekirdekteki eBPF doğrulayıcı geri izlemeyi, yani döngüleri ve işlev çağrılarını yasakladığından, okunabilirlik için C'de vurgulanan tüm işlevlerin çağrı noktasında satır içi olması gerekir.

#define INTERNAL static __attribute__((always_inline))

Makro LOG() sürüm yapısında yazdırmayı devre dışı bırakır.

Program bir işlevler taşıyıcısıdır. Her biri, ilgili seviye başlığının vurgulandığı bir paket alır; örneğin, process_ether() doldurulmasını bekliyor ether. Alan analizi sonuçlarına göre fonksiyon paketi daha yüksek bir seviyeye aktarabilir. İşlevin sonucu XDP eylemidir. Şimdilik SYN ve ACK işleyicileri tüm paketleri aktarıyor.

struct Packet {
    struct xdp_md* ctx;

    struct ethhdr* ether;
    struct iphdr* ip;
    struct tcphdr* tcp;
};

INTERNAL int process_tcp_syn(struct Packet* packet) { return XDP_PASS; }
INTERNAL int process_tcp_ack(struct Packet* packet) { return XDP_PASS; }
INTERNAL int process_tcp(struct Packet* packet) { ... }
INTERNAL int process_ip(struct Packet* packet) { ... }

INTERNAL int
process_ether(struct Packet* packet) {
    struct ethhdr* ether = packet->ether;

    LOG("Ether(proto=0x%x)", bpf_ntohs(ether->h_proto));

    if (ether->h_proto != bpf_ntohs(ETH_P_IP)) {
        return XDP_PASS;
    }

    // B
    struct iphdr* ip = (struct iphdr*)(ether + 1);
    if ((void*)(ip + 1) > (void*)packet->ctx->data_end) {
        return XDP_DROP; /* malformed packet */
    }

    packet->ip = ip;
    return process_ip(packet);
}

SEC("prog")
int xdp_main(struct xdp_md* ctx) {
    struct Packet packet;
    packet.ctx = ctx;

    // A
    struct ethhdr* ether = (struct ethhdr*)(void*)ctx->data;
    if ((void*)(ether + 1) > (void*)ctx->data_end) {
        return XDP_PASS;
    }

    packet.ether = ether;
    return process_ether(&packet);
}

A ve B olarak işaretlenmiş kontrollere dikkatinizi çekiyorum. A'yı yorumlarsanız program derlenecek ancak yüklenirken doğrulama hatası oluşacaktır:

Verifier analysis:

<...>
11: (7b) *(u64 *)(r10 -48) = r1
12: (71) r3 = *(u8 *)(r7 +13)
invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0)
R7 offset is outside of the packet
processed 11 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0

Error fetching program/map!

Anahtar dizisi invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): Tamponun başlangıcından itibaren on üçüncü bayt paketin dışında olduğunda yürütme yolları vardır. Listelemeden hangi satırdan bahsettiğimizi anlamak zor ama kaynak kod satırlarını gösteren bir talimat numarası (12) ve bir disassembler var:

llvm-objdump -S xdp_filter.o | less

Bu durumda çizgiye işaret eder

LOG("Ether(proto=0x%x)", bpf_ntohs(ether->h_proto));

bu da sorunun ne olduğunu açıkça ortaya koyuyor ether. Her zaman böyle olurdu.

SYN'e yanıt veriyorsun

Bu aşamadaki amaç sabit bir SYNACK paketi oluşturmaktır. seqnumgelecekte SYN çerezi ile değiştirilecektir. Tüm değişiklikler şu şekilde gerçekleşir: process_tcp_syn() ve çevresindeki alanlar.

Paket doğrulama

İşin garibi, işte en dikkat çekici satır, daha doğrusu onun yorumu:

/* Required to verify checksum calculation */
const void* data_end = (const void*)ctx->data_end;

Kodun ilk versiyonunu yazarken, doğrulayıcı için arasında bir fark olan 5.1 çekirdeği kullanıldı. data_end и (const void*)ctx->data_end. Bu yazının yazıldığı sırada çekirdek 5.3.1'de bu sorun yoktu. Derleyicinin yerel bir değişkene alandan farklı şekilde erişmesi mümkündür. Hikayenin ana fikri: Çok fazla iç içe geçme olduğunda kodu basitleştirmek yardımcı olabilir.

Daha sonra doğrulayıcının başarısı için rutin uzunluk kontrolleri gelir; Ö MAX_CSUM_BYTES Aşağıda.

const u32 ip_len = ip->ihl * 4;
if ((void*)ip + ip_len > data_end) {
    return XDP_DROP; /* malformed packet */
}
if (ip_len > MAX_CSUM_BYTES) {
    return XDP_ABORTED; /* implementation limitation */
}

const u32 tcp_len = tcp->doff * 4;
if ((void*)tcp + tcp_len > (void*)ctx->data_end) {
    return XDP_DROP; /* malformed packet */
}
if (tcp_len > MAX_CSUM_BYTES) {
    return XDP_ABORTED; /* implementation limitation */
}

Paketin açılması

Doldurmak seqnum и acknumACK'yi ayarlayın (SYN zaten ayarlanmıştır):

const u32 cookie = 42;
tcp->ack_seq = bpf_htonl(bpf_ntohl(tcp->seq) + 1);
tcp->seq = bpf_htonl(cookie);
tcp->ack = 1;

TCP bağlantı noktalarını, IP adresini ve MAC adreslerini değiştirin. Standart kitaplığa XDP programından erişilemez, dolayısıyla memcpy() — Clang'ın esaslarını gizleyen bir makro.

const u16 temp_port = tcp->source;
tcp->source = tcp->dest;
tcp->dest = temp_port;

const u32 temp_ip = ip->saddr;
ip->saddr = ip->daddr;
ip->daddr = temp_ip;

struct ethhdr temp_ether = *ether;
memcpy(ether->h_dest, temp_ether.h_source, ETH_ALEN);
memcpy(ether->h_source, temp_ether.h_dest, ETH_ALEN);

Sağlama toplamlarının yeniden hesaplanması

IPv4 ve TCP sağlama toplamları, başlıklara 16 bitlik kelimelerin tamamının eklenmesini gerektirir ve başlıkların boyutu bunlara yazılır, yani derleme zamanında bilinmez. Bu bir sorundur çünkü doğrulayıcı normal döngüyü sınır değişkenine atlamayacaktır. Ancak başlıkların boyutu sınırlıdır: her biri 64 bayta kadar. Sabit sayıda yinelemeyle erken sona erebilecek bir döngü oluşturabilirsiniz.

var olduğunu not ediyorum RFC 1624 paketlerin yalnızca sabit sözcükleri değiştirilirse sağlama toplamının kısmen nasıl yeniden hesaplanacağı hakkında. Ancak yöntem evrensel değildir ve uygulamanın sürdürülmesi daha zor olacaktır.

Sağlama toplamı hesaplama fonksiyonu:

#define MAX_CSUM_WORDS 32
#define MAX_CSUM_BYTES (MAX_CSUM_WORDS * 2)

INTERNAL u32
sum16(const void* data, u32 size, const void* data_end) {
    u32 s = 0;
#pragma unroll
    for (u32 i = 0; i < MAX_CSUM_WORDS; i++) {
        if (2*i >= size) {
            return s; /* normal exit */
        }
        if (data + 2*i + 1 + 1 > data_end) {
            return 0; /* should be unreachable */
        }
        s += ((const u16*)data)[i];
    }
    return s;
}

Rağmen size Çağıran kod tarafından doğrulandığında, doğrulayıcının döngünün tamamlandığını kanıtlayabilmesi için ikinci çıkış koşulu gereklidir.

32 bitlik kelimeler için daha basit bir sürüm uygulanır:

INTERNAL u32
sum16_32(u32 v) {
    return (v >> 16) + (v & 0xffff);
}

Aslında sağlama toplamlarını yeniden hesaplamak ve paketi geri göndermek:

ip->check = 0;
ip->check = carry(sum16(ip, ip_len, data_end));

u32 tcp_csum = 0;
tcp_csum += sum16_32(ip->saddr);
tcp_csum += sum16_32(ip->daddr);
tcp_csum += 0x0600;
tcp_csum += tcp_len << 8;
tcp->check = 0;
tcp_csum += sum16(tcp, tcp_len, data_end);
tcp->check = carry(tcp_csum);

return XDP_TX;

Fonksiyon carry() RFC 32'e göre 16 bitlik sözcüklerin 791 bitlik toplamından bir sağlama toplamı yapar.

TCP el sıkışma doğrulaması

Filtre doğru bir şekilde bağlantı kurar netcatağ yığını SYN almadığı için - SYNACK'e dönüştürüldü ve geri gönderildi - ve işletim sistemi açısından, açılışla ilgili olmayan bir paket geldiğinden, Linux'un bir RST paketiyle yanıt verdiği son ACK'yı kaçırdı. bağlantılar.

$ sudo ip netns exec xdp-test   nc -nv 192.0.2.1 6666
192.0.2.1 6666: Connection reset by peer

Tam teşekküllü başvurularla kontrol edilmesi ve gözlemlenmesi önemlidir. tcpdump üzerinde xdp-remote çünkü örneğin hping3 Yanlış sağlama toplamlarına yanıt vermez.

XDP açısından bakıldığında doğrulamanın kendisi önemsizdir. Hesaplama algoritması ilkeldir ve muhtemelen gelişmiş bir saldırgana karşı savunmasızdır. Örneğin Linux çekirdeği kriptografik SipHash'ı kullanıyor ancak bunun XDP için uygulanması açıkça bu makalenin kapsamı dışındadır.

Harici iletişimle ilgili yeni TODO'lar için tanıtıldı:

  • XDP programı depolanamıyor cookie_seed (tuzun gizli kısmı) global bir değişkende, değeri güvenilir bir jeneratörden periyodik olarak güncellenecek olan çekirdekte depolamaya ihtiyacınız vardır.

  • SYN çerezi ACK paketinde eşleşirse, bir mesaj yazdırmanıza gerek yoktur, ancak doğrulanmış istemciden paket aktarmaya devam etmek için IP'sini hatırlamanız gerekir.

Meşru müşteri doğrulaması:

$ sudoip netns exec xdp-test   nc -nv 192.0.2.1 6666
192.0.2.1 6666: Connection reset by peer

Günlükler kontrolün başarılı olduğunu gösteriyor (flags=0x2 - bu SYN, flags=0x10 ACK'dir):

Ether(proto=0x800)
  IP(src=0x20e6e11a dst=0x20e6e11e proto=6)
    TCP(sport=50836 dport=6666 flags=0x2)
Ether(proto=0x800)
  IP(src=0xfe2cb11a dst=0xfe2cb11e proto=6)
    TCP(sport=50836 dport=6666 flags=0x10)
      cookie matches for client 20200c0

Doğrulanmış IP'lerin listesi olmasa da SYN seline karşı koruma olmayacaktır, ancak aşağıdaki komutla başlatılan bir ACK seline verilen tepki aşağıdadır:

sudo ip netns exec xdp-test   hping3 --flood -A -s 1111 -p 2222 192.0.2.1

Günlük girişleri:

Ether(proto=0x800)
  IP(src=0x15bd11a dst=0x15bd11e proto=6)
    TCP(sport=3236 dport=2222 flags=0x10)
      cookie mismatch

Sonuç

Bazen genel olarak eBPF ve özel olarak XDP, bir geliştirme platformundan çok gelişmiş bir yönetici aracı olarak sunulur. Aslında XDP, paketlerin çekirdek tarafından işlenmesine müdahale eden bir araçtır ve DPDK ve diğer çekirdek atlama seçenekleri gibi çekirdek yığınına bir alternatif değildir. Öte yandan XDP, trafik işlemede kesinti olmadan güncellenmesi kolay olan oldukça karmaşık bir mantık uygulamanıza olanak tanır. Doğrulayıcı büyük sorunlar yaratmaz; kişisel olarak kullanıcı alanı kodunun bazı kısımları için bunu reddetmem.

İkinci bölümde eğer konu ilginçse, doğrulanmış istemciler ve bağlantı kesintileri tablosunu tamamlayacağız, sayaçları uygulayacağız ve filtreyi yönetmek için bir kullanıcı alanı yardımcı programı yazacağız.

Bağlantılar:

Kaynak: habr.com

Yorum ekle