Scriem protecție împotriva atacurilor DDoS pe XDP. Partea nucleară

Tehnologia eXpress Data Path (XDP) permite procesarea aleatorie a traficului pe interfețele Linux înainte ca pachetele să intre în stiva rețelei kernel. Aplicarea XDP - protecție împotriva atacurilor DDoS (CloudFlare), filtre complexe, colectare de statistici (Netflix). Programele XDP sunt executate de mașina virtuală eBPF, așa că au restricții atât la codul lor, cât și la funcțiile kernel-ului disponibile, în funcție de tipul de filtru.

Articolul este destinat să completeze deficiențele numeroaselor materiale pe XDP. În primul rând, oferă cod gata făcut care ocolește imediat caracteristicile XDP: este pregătit pentru verificare sau este prea simplu pentru a cauza probleme. Atunci când încercați să scrieți codul de la zero, nu aveți idee ce să faceți cu erorile tipice. În al doilea rând, modalitățile de a testa local XDP fără un VM și hardware nu sunt acoperite, în ciuda faptului că au propriile capcane. Textul este destinat programatorilor familiarizați cu rețelele și Linux, care sunt interesați de XDP și eBPF.

În această parte, vom înțelege în detaliu cum este asamblat filtrul XDP și cum să-l testăm, apoi vom scrie o versiune simplă a binecunoscutului mecanism cookie SYN la nivel de procesare a pachetelor. Deocamdată, nu vom crea o „listă albă”
clienți verificați, păstrați contoare și gestionați filtrul - suficiente jurnale.

Vom scrie în C - nu e la modă, dar este practic. Tot codul este disponibil pe GitHub prin linkul de la sfârșit și este împărțit în commit-uri conform etapelor descrise în articol.

Disclaimer. Pe parcursul acestui articol, o mini-soluție va fi dezvoltată pentru a evita atacurile DDoS, deoarece aceasta este o sarcină realistă pentru XDP și domeniul meu. Cu toate acestea, scopul principal este înțelegerea tehnologiei; acesta nu este un ghid pentru crearea unei protecții gata făcute. Codul tutorial nu este optimizat și omite unele nuanțe.

Scurtă prezentare XDP

Voi sublinia doar punctele cheie pentru a nu duplica documentația și articolele existente.

Deci, codul filtrului este încărcat în nucleu. Pachetele primite sunt trecute la filtru. Ca rezultat, filtrul trebuie să ia o decizie: trece pachetul în nucleu (XDP_PASS), aruncați pachetul (XDP_DROP) sau trimite-l înapoi (XDP_TX). Filtrul poate schimba pachetul, acest lucru este valabil mai ales pentru XDP_TX. De asemenea, puteți anula programul (XDP_ABORTED) și resetați pachetul, dar acest lucru este analog assert(0) - pentru depanare.

Mașina virtuală eBPF (extended Berkley Packet Filter) este simplificată în mod deliberat, astfel încât nucleul să poată verifica dacă codul nu intră în bucle și nu dăunează memoriei altor persoane. Restricții și verificări cumulate:

  • Buclele (înapoi) sunt interzise.
  • Există o stivă pentru date, dar nu există funcții (toate funcțiile C trebuie să fie aliniate).
  • Accesele la memorie în afara stivei și a bufferului de pachete sunt interzise.
  • Dimensiunea codului este limitată, dar în practică acest lucru nu este foarte semnificativ.
  • Sunt permise doar apelurile la funcții speciale ale nucleului (ajutoare eBPF).

Proiectarea și instalarea unui filtru arată astfel:

  1. Cod sursă (de ex kernel.c) este compilat în obiect (kernel.o) sub arhitectura mașinii virtuale eBPF. Din octombrie 2019, compilarea către eBPF este susținută de Clang și este promisă în GCC 10.1.
  2. Dacă acest cod obiect conține apeluri la structurile nucleului (de exemplu, tabele și contoare), ID-urile acestora sunt înlocuite cu zerouri, ceea ce înseamnă că un astfel de cod nu poate fi executat. Înainte de a încărca în nucleu, trebuie să înlocuiți aceste zerouri cu ID-urile unor obiecte specifice create prin apeluri la kernel (legați codul). Puteți face acest lucru cu utilități externe sau puteți scrie un program care va lega și va încărca un anumit filtru.
  3. Nucleul verifică programul încărcat. Se verifică absența ciclurilor și netraversarea granițelor pachetelor și stivei. Dacă verificatorul nu poate dovedi că codul este corect, programul este respins - trebuie să-l poți mulțumi.
  4. După verificarea cu succes, kernel-ul compilează codul obiect al arhitecturii eBPF în codul mașinii arhitecturii sistemului (just-in-time).
  5. Programul se atașează la interfață și începe procesarea pachetelor.

Deoarece XDP rulează în nucleu, depanarea se realizează folosind jurnalele de urmărire și, de fapt, pachetele pe care programul le filtrează sau le generează. Cu toate acestea, eBPF asigură că codul încărcat este securizat pentru sistem, astfel încât să puteți experimenta cu XDP direct pe Linuxul dvs. local.

Pregătirea Mediului

asamblare

Clang nu poate produce direct cod obiect pentru arhitectura eBPF, așa că procesul constă din doi pași:

  1. Compilați codul C în bytecode LLVM (clang -emit-llvm).
  2. Convertește bytecode în cod obiect eBPF (llc -march=bpf -filetype=obj).

Când scrieți un filtru, vor fi utile câteva fișiere cu funcții auxiliare și macrocomenzi din testele nucleului. Este important ca acestea să se potrivească cu versiunea nucleului (KVER). Descărcați-le pe 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

Makefile pentru Arch Linux (kernel 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 conține calea către anteturile nucleului, ARCH - Arhitectura sistemului. Căile și instrumentele pot varia ușor între distribuții.

Exemplu de diferențe pentru Debian 10 (kernel 4.19.67)

# другая команда
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 conectați un director cu anteturi auxiliare și mai multe directoare cu anteturi de nucleu. Simbol __KERNEL__ înseamnă că anteturile UAPI (userspace API) sunt definite pentru codul nucleului, deoarece filtrul este executat în nucleu.

Protecția stivei poate fi dezactivată (-fno-stack-protector), deoarece verificatorul de cod eBPF încă verifică dacă există încălcări ale stivei în afara limitelor. Merită să activați optimizările imediat, deoarece dimensiunea codului de octet eBPF este limitată.

Să începem cu un filtru care trece toate pachetele și nu face nimic:

#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";

Echipă make colectează xdp_filter.o. Unde sa incerc acum?

stand de testare

Standul trebuie să includă două interfețe: pe care va fi un filtru și din care vor fi trimise pachete. Acestea trebuie să fie dispozitive Linux cu drepturi depline, cu propriul lor IP, pentru a verifica cum funcționează aplicațiile obișnuite cu filtrul nostru.

Dispozitivele de tip veth (Ethernet virtual) sunt potrivite pentru noi: acestea sunt o pereche de interfețe de rețea virtuale „conectate” direct între ele. Le puteți crea astfel (în această secțiune toate comenzile ip sunt efectuate din root):

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

Aici xdp-remote и xdp-local — numele dispozitivelor. Pe xdp-local (192.0.2.1/24) se va atașa un filtru, cu xdp-remote (192.0.2.2/24) traficul de intrare va fi trimis. Cu toate acestea, există o problemă: interfețele sunt pe aceeași mașină, iar Linux nu va trimite trafic către una dintre ele prin cealaltă. Puteți rezolva acest lucru cu reguli complicate iptables, dar vor trebui să schimbe pachetele, ceea ce este incomod pentru depanare. Este mai bine să folosiți spații de nume de rețea (denumite în continuare netns).

Un spațiu de nume de rețea conține un set de interfețe, tabele de rutare și reguli NetFilter, izolate de obiecte similare din alte rețele. Fiecare proces rulează într-un spațiu de nume și are acces doar la obiectele netn-ului respectiv. În mod implicit, sistemul are un singur spațiu de nume de rețea pentru toate obiectele, astfel încât să puteți lucra în Linux și să nu știți despre netns.

Să creăm un nou spațiu de nume xdp-test și mutați-l acolo xdp-remote.

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

Apoi procesul rulează xdp-test, nu va „vedea” xdp-local (va rămâne implicit în netns) iar la trimiterea unui pachet către 192.0.2.1 îl va transmite prin xdp-remote, deoarece aceasta este singura interfață pe 192.0.2.0/24 accesibilă acestui proces. Acest lucru funcționează și în direcția opusă.

Când vă deplasați între netns, interfața scade și își pierde adresa. Pentru a configura interfața în netns, trebuie să rulați ip ... în acest spațiu de nume de comandă 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

După cum puteți vedea, aceasta nu este diferită de setare xdp-local în spațiul de nume implicit:

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

Dacă fugi tcpdump -tnevi xdp-local, puteți vedea că pachetele trimise de la xdp-test, sunt livrate la această interfață:

ip netns exec xdp-test   ping 192.0.2.1

Este convenabil să lansați un shell xdp-test. Depozitul are un script care automatizează lucrul cu standul; de exemplu, puteți configura standul cu comanda sudo ./stand up și ștergeți-l sudo ./stand down.

Urmărirea

Filtrul este asociat cu un dispozitiv ca acesta:

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

Ключ -force necesare pentru a lega un nou program dacă altul este deja conectat. „Nici o veste este o veste bună” nu este despre această comandă, rezultatul este voluminos în orice caz. indica verbose opțional, dar odată cu acesta apare un raport despre activitatea verificatorului de cod cu lista de asamblare:

Verifier analysis:

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

Deconectați programul de la interfață:

ip link set dev xdp-local xdp off

Într-un script acestea sunt comenzi sudo ./stand attach и sudo ./stand detach.

Prin atașarea unui filtru, vă puteți asigura că ping continuă să funcționeze, dar funcționează programul? Să adăugăm jurnalele. Funcţie bpf_trace_printk() similar cu printf(), dar acceptă doar până la trei argumente, altele decât modelul, și o listă limitată de specificatori. Macro bpf_printk() simplifică apelul.

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

Ieșirea merge către canalul de urmărire a nucleului, care trebuie activat:

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

Vezi firul de mesaje:

cat /sys/kernel/debug/tracing/trace_pipe

Ambele comenzi efectuează un apel sudo ./stand log.

Ping ar trebui acum să declanșeze mesaje ca acesta:

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

Dacă te uiți cu atenție la ieșirea verificatorului, vei observa calcule ciudate:

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

Faptul este că programele eBPF nu au o secțiune de date, așa că singura modalitate de a codifica un șir de format este argumentele imediate ale comenzilor VM:

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

Din acest motiv, ieșirea de depanare umfla foarte mult codul final.

Trimiterea pachetelor XDP

Să schimbăm filtrul: lăsați-l să trimită înapoi toate pachetele primite. Acest lucru este incorect din punct de vedere al rețelei, deoarece ar fi necesar să se schimbe adresele din anteturi, dar acum lucrul în principiu este important.

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

Lansa tcpdump pe xdp-remote. Ar trebui să afișeze Solicitarea Echo ICMP de ieșire și cea de intrare identică și să nu mai afișeze Răspuns Echo ICMP. Dar nu se vede. Se pare că pentru muncă XDP_TX în program pe xdp-local mustla interfața pereche xdp-remote i s-a atribuit și un program, chiar dacă era gol, și a fost ridicat.

De unde am știut asta?

Urmăriți calea unui pachet în nucleu Mecanismul de evenimente perf permite, de altfel, utilizarea aceleiași mașini virtuale, adică eBPF este folosit pentru a face față eBPF.

Trebuie să faci bine din rău, pentru că nu există nimic altceva din care să-l faci.

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

Ce este codul 6?

$ errno 6
ENXIO 6 No such device or address

Funcție veth_xdp_flush_bq() primește un cod de eroare de la veth_xdp_xmit(), unde caută după ENXIO și găsiți comentariul.

Să restabilim filtrul minim (XDP_PASS) în dosar xdp_dummy.c, adăugați-l la Makefile, conectați-l la xdp-remote:

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

acum tcpdump arată ceea ce este de așteptat:

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

Dacă în schimb sunt afișate numai ARP-urile, trebuie să eliminați filtrele (acest lucru face sudo ./stand detach), dă drumul ping, apoi setați filtre și încercați din nou. Problema este ca filtrul XDP_TX valabil atât pentru ARP, cât și pentru stivă
spații de nume xdp-test a reușit să „uite” adresa MAC 192.0.2.1, nu va putea rezolva acest IP.

Declarație de problemă

Să trecem la sarcina menționată: să scriem un mecanism de cookie-uri SYN pe XDP.

SYN flood rămâne un atac DDoS popular, a cărui esență este următoarea. Când se stabilește o conexiune (strângere de mână TCP), serverul primește un SYN, alocă resurse pentru conexiunea viitoare, răspunde cu un pachet SYNACK și așteaptă un ACK. Atacatorul trimite pur și simplu pachete SYN de la adrese false în mii pe secundă de la fiecare gazdă într-o rețea botnet de mai multe mii. Serverul este forțat să aloce resurse imediat după sosirea pachetului, dar le eliberează după un timeout mare; ca urmare, memoria sau limitele sunt epuizate, conexiunile noi nu sunt acceptate și serviciul este indisponibil.

Dacă nu alocați resurse pe baza pachetului SYN, ci răspundeți doar cu un pachet SYNACK, atunci cum poate serverul să înțeleagă că pachetul ACK care a sosit mai târziu se referă la un pachet SYN care nu a fost salvat? La urma urmei, un atacator poate genera și ACK-uri false. Scopul cookie-ului SYN este de a-l codifica seqnum parametrii de conectare ca un hash de adrese, porturi și sare în schimbare. Dacă ACK-ul a reușit să sosească înainte ca sarea să fie schimbată, puteți calcula din nou hash-ul și îl puteți compara cu acknum. Forja acknum atacatorul nu poate, deoarece sarea include un secret și nu va putea să-l trimită din cauza unui canal limitat.

Modulul cookie SYN a fost implementat de mult în kernel-ul Linux și poate fi chiar activat automat dacă SYN-urile ajung prea repede și în masă.

Program educațional privind strângerea de mână TCP

TCP oferă transmisie de date ca un flux de octeți, de exemplu, cererile HTTP sunt transmise prin TCP. Fluxul este transmis în bucăți în pachete. Toate pachetele TCP au steaguri logice și numere de secvență pe 32 de biți:

  • Combinația de steaguri determină rolul unui anumit pachet. Indicatorul SYN înseamnă că acesta este primul pachet al expeditorului din conexiune. Indicatorul ACK înseamnă că expeditorul a primit toate datele de conectare până la octet acknum. Un pachet poate avea mai multe steaguri și este denumit după combinația lor, de exemplu, un pachet SYNACK.

  • Numărul de secvență (seqnum) specifică offset-ul din fluxul de date pentru primul octet care este transmis în acest pachet. De exemplu, dacă în primul pachet cu X octeți de date acest număr a fost N, în următorul pachet cu date noi va fi N+X. La începutul conexiunii, fiecare parte alege acest număr la întâmplare.

  • Numărul de confirmare (acknum) este același offset ca seqnum, dar nu determină numărul octetului transmis, ci numărul primului octet de la destinatar, pe care expeditorul nu l-a văzut.

La începutul conexiunii, părțile trebuie să fie de acord seqnum и acknum. Clientul trimite un pachet SYN cu acesta seqnum = X. Serverul răspunde cu un pachet SYNACK, unde îl înregistrează seqnum = Y și exponate acknum = X + 1. Clientul răspunde la SYNACK cu un pachet ACK, unde seqnum = X + 1, acknum = Y + 1. După aceasta, începe transferul efectiv de date.

Dacă peer-ul nu confirmă primirea pachetului, TCP îl retrimite după un timeout.

De ce cookie-urile SYN nu sunt întotdeauna folosite?

În primul rând, dacă SYNACK sau ACK-ul este pierdut, va trebui să așteptați o retrimitere - încetinind stabilirea conexiunii. În al doilea rând, în pachetul SYN - și numai în el! — sunt transmise o serie de opțiuni care afectează funcționarea ulterioară a conexiunii. Prin faptul că nu își amintește pachetele SYN primite, serverul ignoră astfel aceste opțiuni; clientul nu le va mai trimite în următoarele pachete. TCP poate funcționa în acest caz, dar cel puțin în stadiul inițial calitatea conexiunii va scădea.

Din punct de vedere al pachetelor, un program XDP trebuie să facă următoarele:

  • răspunde la SYN cu SYNACK cu un cookie;
  • răspunde la ACK cu RST (deconectare);
  • aruncați pachetele rămase.

Pseudocod al algoritmului împreună cu analizarea pachetului:

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

unu (*) Punctele în care trebuie să gestionați starea sistemului sunt marcate - în prima etapă puteți face fără ele prin simpla implementare a unui handshake TCP cu generarea unui cookie SYN ca secvnum.

Pe loc (**), în timp ce nu avem masă, vom sări peste pachet.

Implementarea strângerii de mână TCP

Analizarea pachetului și verificarea codului

Vom avea nevoie de structuri de antet de rețea: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) și TCP (uapi/linux/tcp.h). Nu am reușit niciodată să-l conectez pe acesta din urmă din cauza erorilor legate de atomic64_t, a trebuit să copiez definițiile necesare în cod.

Toate funcțiile care sunt alocate pentru lizibilitate în C trebuie să fie aliniate la punctul de apel, deoarece verificatorul eBPF din nucleu interzice salturile înapoi, adică, de fapt, buclele și apelurile de funcție.

#define INTERNAL static __attribute__((always_inline))

macro LOG() dezactivează imprimarea în versiunea de versiune.

Programul este un transportor de funcții. Fiecare primește un pachet în care este evidențiat un antet de nivelul corespunzător, de exemplu, process_ether() se asteapta sa fie umplut ether. Pe baza rezultatelor analizei de câmp, funcția poate trece pachetul la un nivel superior. Rezultatul funcției este o acțiune XDP. Deocamdată, handlerele SYN și ACK trec toate pachetele.

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

Vă atrag atenția asupra verificărilor marcate A și B. Dacă comentați A, programul se va construi, dar va apărea o eroare de verificare la încărcare:

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!

Șirul de cheie invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): Există căi de execuție când al treisprezecelea octet de la începutul buffer-ului se află în afara pachetului. Este greu de înțeles din lista despre ce linie vorbim, dar există un număr de instrucțiune (12) și un dezasamblator care arată liniile codului sursă:

llvm-objdump -S xdp_filter.o | less

În acest caz, arată spre linie

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

ceea ce face clar că problema este ether. Ar fi mereu așa.

Răspunde la SYN

Scopul în această etapă este de a genera un pachet SYNACK corect cu un pachet fix seqnum, care va fi înlocuit în viitor cu cookie-ul SYN. Toate schimbările au loc în process_tcp_syn() și zonele învecinate.

Verificarea pachetului

Destul de ciudat, iată cea mai remarcabilă replică, sau mai degrabă, comentariul la ea:

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

La scrierea primei versiuni a codului a fost folosit nucleul 5.1, pentru verificatorul căruia a existat o diferență între data_end и (const void*)ctx->data_end. La momentul scrierii articolului, kernel-ul 5.3.1 nu avea această problemă. Poate că compilatorul a accesat o variabilă locală diferit de un câmp. Morala povestirii: Cu situații mari de cuibărire, simplificarea codului poate ajuta.

Urmează verificările de rutină a lungimii pentru gloria verificatorului; O MAX_CSUM_BYTES de mai jos.

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 */
}

Desfacerea pachetului

umple seqnum и acknum, setați ACK (SYN este deja setat):

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

Schimbați porturile TCP, adresa IP și adresele MAC. Biblioteca standard nu este accesibilă din programul XDP, deci memcpy() — o macrocomandă care ascunde intrinsecile Clang.

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);

Recalcularea sumelor de control

Sumele de verificare IPv4 și TCP necesită adăugarea tuturor cuvintelor de 16 biți în anteturi, iar dimensiunea antetelor este scrisă în ele, adică necunoscută la momentul compilării. Aceasta este o problemă deoarece verificatorul nu va trece prin bucla normală până la limita variabilă. Dar dimensiunea antetelor este limitată: până la 64 de octeți fiecare. Puteți face o buclă cu un număr fix de iterații, care se poate termina mai devreme.

Observ că există RFC 1624 despre cum se recalculează parțial suma de control dacă sunt modificate numai cuvintele fixe ale pachetelor. Cu toate acestea, metoda nu este universală, iar implementarea ar fi mai dificil de întreținut.

Funcția de calcul a sumei de control:

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

Cu toate că size verificată prin codul de apelare, a doua condiție de ieșire este necesară pentru ca verificatorul să poată dovedi finalizarea buclei.

Pentru cuvintele pe 32 de biți, este implementată o versiune mai simplă:

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

De fapt, recalculând sumele de control și trimiterea pachetului înapoi:

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;

Funcție carry() face o sumă de control dintr-o sumă de 32 de biți de cuvinte de 16 biți, conform RFC 791.

Verificare TCP handshake

Filtrul stabilește corect o conexiune cu netcat, sărind peste ACK-ul final, la care Linux a răspuns cu un pachet RST, deoarece stiva de rețea nu a primit SYN - a fost convertit în SYNACK și trimis înapoi - și din punct de vedere al sistemului de operare a sosit un pachet care nu avea legătură cu conexiuni deschise.

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

Este important să verificați cu aplicații și monitorizare cu drepturi depline tcpdump pe xdp-remote pentru că, de exemplu, hping3 nu răspunde la sumele de control incorecte.

Din punct de vedere XDP, verificarea în sine este banală. Algoritmul de calcul este primitiv și probabil vulnerabil la un atacator sofisticat. Nucleul Linux, de exemplu, folosește SipHash criptografic, dar implementarea sa pentru XDP depășește în mod clar scopul acestui articol.

A apărut pentru TODO noi legate de interacțiunea externă:

  • Programul XDP nu poate stoca cookie_seed (partea secretă a sării) într-o variabilă globală, aveți nevoie de stocare în nucleu, valoarea în care va fi actualizată periodic de la un generator de încredere.

  • Dacă cookie-ul SYN se potrivește în pachetul ACK, nu trebuie să tipăriți un mesaj, dar rețineți IP-ul clientului verificat pentru a continua să transmiteți pachete din acesta.

Verificarea legitimă a clientului:

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

Jurnalele înregistrează finalizarea verificării (flags=0x2 - acesta este SYN, flags=0x10 este ACK):

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

Deși nu există o listă de IP-uri verificate, nu va exista nicio protecție împotriva inundației SYN în sine, dar iată reacția la o inundație ACK lansată de următoarea comandă:

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

Intrări în jurnal:

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

Concluzie

Uneori, eBPF în general și XDP în special sunt prezentate mai mult ca un instrument avansat de administrator decât ca o platformă de dezvoltare. Într-adevăr, XDP este un instrument pentru a interfera cu procesarea pachetelor de către nucleu și nu o alternativă la stiva de nucleu, cum ar fi DPDK și alte opțiuni de ocolire a nucleului. Pe de altă parte, XDP vă permite să implementați o logică destul de complexă, care, în plus, este ușor de actualizat fără întreruperi în procesarea traficului. Verificatorul nu creează probleme mari; personal, nu aș refuza acest lucru pentru părți din codul spațiului utilizator.

În a doua parte, dacă subiectul este interesant, vom completa tabelul de clienți verificați și deconectări, vom implementa contoare și vom scrie un utilitar pentru spațiul de utilizare pentru a gestiona filtrul.

referințe:

Sursa: www.habr.com

Adauga un comentariu