De eXpress Data Path (XDP)-technologie maakt het mogelijk om willekeurige verwerking van verkeer op Linux-interfaces uit te voeren voordat pakketten de netwerkniveau van de kernel bereiken. Toepassingen van XDP omvatten bescherming tegen DDoS-aanvallen (CloudFlare), complexe filters en statistiekenverzameling (Netflix). XDP-programma's worden uitgevoerd door een eBPF-virtuele machine, waardoor ze beperkingen hebben voor zowel hun code als de beschikbare kernelfuncties, afhankelijk van het type filter.
Dit artikel is bedoeld om de tekortkomingen van talrijke materialen over XDP te verhelpen. Ten eerste bieden zij kant-en-klare code die meteen de kenmerken van XDP omzeilt: klaar voor verificatie of te simpel om problemen te veroorzaken. Bij het later proberen om zelf code vanaf nul te schrijven, is er geen begrip van hoe om te gaan met typische fouten. Ten tweede worden er geen manieren belicht om XDP lokaal te testen zonder VM en ‘hardware’, terwijl ze hun eigen ‘onderwater’, problemen hebben. De tekst is gericht op programmeurs die bekend zijn met netwerken en Linux en die geïnteresseerd zijn in XDP en eBPF.
In dit deel zullen we in detail bespreken hoe een XDP-filter wordt opgebouwd en hoe deze getest kan worden, om vervolgens een eenvoudige versie van het bekende SYN cookies-mechanisme te schrijven op pakketverwerkingsniveau. We gaan voorlopig geen 'witte lijst'
van goedgekeurde klanten maken, tellers bijhouden en de filter beheren – logs zijn voldoende.
We zullen in C schrijven – het is misschien niet trendy, maar wel praktisch. De volledige code is beschikbaar op GitHub via de link aan het einde en is verdeeld in commits volgens de in het artikel beschreven fases.
Disclaimer. In de loop van dit artikel zal er een mini-oplossing worden ontwikkeld voor afscherming tegen DDoS-aanvallen, omdat dit een realistische taak is voor XDP en mijn expertisegebied. De belangrijkste doelstelling is echter om inzicht te krijgen in de technologie; dit is geen handleiding voor het creëren van een kant-en-klare bescherming. De leercode is niet geoptimaliseerd en laat enkele nuances buiten beschouwing.
Korte overzicht van XDP
Ik zal alleen de belangrijkste punten bespreken, om niet de documentatie en bestaande artikelen te dupliceren.
Dus, code van het filter wordt in de kernel geladen. Binnenkomende pakketten worden aan de filter doorgegeven. Uiteindelijk moet de filter een beslissing nemen: doorgaan met het pakket in de kernel (XDP_PASS), het pakket afkeuren (XDP_DROP) of het terugsturen (XDP_TX). De filter kan het pakket ook wijzigen, wat vooral relevant is voor XDP_TX. Ook kan het programma onvoorzien worden onderbroken (XDP_ABORTED) en het pakket worden afgebroken, maar dit is vergelijkbaar met assert(0) — voor debugging.
De virtuele machine eBPF (extended Berkley Packet Filter) is speciaal gemaakt om eenvoudig te zijn, zodat de kernel kan controleren of de code niet vastloopt en geen vreemde geheugenbeschadigingen veroorzaakt. Gecombineerde beperkingen en controles:
- Lussen (terugwaartse overgangen) zijn verboden.
- Er is een stack voor gegevens, maar geen functies (alle C-functies moeten ingebed worden).
- Toegang tot geheugen buiten de stack en de pakketbuffer is verboden.
- De codegrootte is beperkt, maar in de praktijk is dit niet erg significant.
- Alleen oproepen naar speciale kernel-functies (eBPF helpers) zijn toegestaan.
De ontwikkeling en installatie van de filter zien er als volgt uit:
- De broncode (bijvoorbeeld,
kernel.c) wordt gecompileerd naar objectcode (kernel.o) voor de architectuur van de eBPF virtuele machine. Sinds oktober 2019 wordt compilatie naar eBPF ondersteund door Clang en is beloofd in GCC 10.1. - Als deze objectcode verwijzingen naar kernelstructuren bevat (bijvoorbeeld naar tabellen en tellers), staan er nullen in plaats van hun ID's, wat betekent dat dergelijke code niet kan worden uitgevoerd. Voordat je het in de kernel laadt, moeten deze nullen worden vervangen door de ID's van specifieke objecten die zijn aangemaakt via kerneloproepen (de code linken). Dit kan gedaan worden met externe utilities, maar je kunt ook een programma schrijven dat de specifieke filter linkt en laadt.
- De kernel verifieert het geladen programma. Er wordt gecontroleerd op afwezigheid van lussen en overschrijding van de grenzen van het pakket en de stack. Als de verifier niet kan bewijzen dat de code correct is, wordt het programma afgewezen — men moet in staat zijn om hem te behagen.
- Na succesvolle verificatie compileert de kernel de objectcode van de eBPF-architectuur naar machincode van de systeemarchitectuur (just-in-time).
- Het programma wordt gekoppeld aan de interface en begint met het verwerken van pakketten.
Aangezien XDP in de kernel werkt, wordt de debugging uitgevoerd op basis van traceerlogs en, feitelijk, op de pakketten die het programma filtert of genereert. Desalniettemin biedt eBPF veiligheid voor de geladen code voor het systeem, zodat het mogelijk is om met XDP te experimenteren op een lokale Linux.
Omgeving voorbereiden
Bouwen
Clang kan niet rechtstreeks objectcode voor de eBPF-architectuur genereren, daarom bestaat het proces uit twee stappen:
- Compileer de C-code naar LLVM bytecode (
clang -emit-llvm). - Converteer de bytecode naar eBPF-objectcode (
llc -march=bpf -filetype=obj).
Bij het schrijven van een filter is een paar bestanden met ondersteunende functies en macro's handig Het is belangrijk dat ze overeenkomen met de kernelversie (KVER). We laden ze binnen in 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 BASEMakefile voor 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 ./*.oKDIR bevat het pad naar de kernelheaders, ARCH — de architectuur van het systeem. Paden en tools kunnen iets verschillen tussen distributies.
Voorbeeld van verschillen voor 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 bevatten de directory met hulphandtekeningen en verschillende directories met kernelheaders. Het symbool __KERNEL__ betekent dat de UAPI-headers (userspace API) worden gedefinieerd voor kernelcode, aangezien de filter in de kernel wordt uitgevoerd.
Stackbescherming kan worden uitgeschakeld (-fno-stack-protector), omdat de eBPF-codeverifier nog steeds controleert op stackoverflow. Het is meteen verstandig om optimalisaties in te schakelen, omdat de bytecodegrootte van eBPF beperkt is.
Laten we beginnen met een filter dat alle pakketten doorlaat en verder niets doet:
#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";Opdracht make bouwt xdp_filter.o. Waar kunnen we het nu test?
Teststand
De testomgeving moet twee interfaces bevatten: een waarop de filter wordt toegepast en een waarvan de pakketten worden verzonden. Dit moeten volledige Linux-apparaten zijn met hun eigen IP's om te controleren hoe normale applicaties met onze filter omgaan.
Veth-apparaten (virtual Ethernet) zijn geschikt: dit is een paar virtuele netwerkintefaces die direct met elkaar zijn "verbonden". Je kunt ze als volgt aanmaken (in dit gedeelte worden alle commando's ip uitgevoerd door root):
ip link add xdp-remote type veth peer name xdp-localHier xdp-remote en xdp-local — de namen van de apparaten. Op xdp-local (192.0.2.1/24) zal de filter worden aangesloten, en xdp-remote (192.0.2.2/24) zal het binnenkomende verkeer verzenden. Er is echter een probleem: de interfaces bevinden zich op één machine en Linux zal geen verkeer naar de ene interface sturen via de andere. Dit kan worden opgelost met slimme regels iptables, maar dit zou de pakketten moeten wijzigen, wat onhandig is tijdens het debuggen. Het is beter om netwerk namespaces (network namespaces, hierna netns) te gebruiken.
Netwerknaamruimte bevat een set interfaces, routeringstabellen en NetFilter-regels, geïsoleerd van vergelijkbare objecten in andere netns. Elk proces draait in een bepaalde naamruimte en heeft alleen toegang tot objecten van deze netns. Standaard is er in het systeem slechts één netwerknaamruimte voor alle objecten, zodat je in Linux kunt werken zonder kennis van netns.
Laten we een nieuwe naamruimte maken xdp-test en verplaatsen we dit naar xdp-remote.
ip netns add xdp-test
ip link set dev xdp-remote netns xdp-testDan zal het proces dat draait in xdp-test, het niet "zien" xdp-local (het blijft in de standaard netns) en bij het verzenden van een pakket naar 192.0.2.1 doorgeef het via xdp-remote, omdat dit de enige interface is in 192.0.2.0/24, beschikbaar voor dit proces. Dit geldt ook omgekeerd.
Bij het verplaatsen tussen netns wordt de interface neergelaten en verliest hij zijn adres. Om de interface in netns in te stellen, moet je ip ... in deze naamruimte uitvoeren 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 upZoals je kunt zien, verschilt dit niet van de configuratie xdp-local in de standaard naamruimte:
ip address add 192.0.2.1/24 dev xdp-local
ip link set xdp-local upAls je tcpdump -tnevi xdp-local, je kunt zien dat de pakketten, verzonden uit xdp-test, worden afgeleverd op deze interface:
ip netns exec xdp-test ping 192.0.2.1Het is handig om een shell te starten in xdp-test. In de repository is er een script dat het werken met de testautomatisatie, bijvoorbeeld, je kunt de teststand instellen met het commando sudo ./stand up en deze verwijderen sudo ./stand down.
Traceroute
De filter wordt aan het apparaat gekoppeld als volgt:
ip -force link set dev xdp-local xdp object xdp_filter.o verboseSleutel -force is nodig om een nieuw programma te koppelen als er al een verbonden is. "No news is good news" is niet van toepassing op dit commando, uitvoer is in elk geval omvangrijk. Verplichten verbose is niet verplicht, maar met dit krijg je een rapport van de werkingsanalyse van de verifier met een assemblerlisting:
Verifier-analyse:
0: (b7) r0 = 2
1: (95) exitOntkoppel het programma van de interface:
ip link set dev xdp-local xdp offIn het script zijn dit de commando's sudo ./stand attach en sudo ./stand detach.
Nadat de filter is gekoppeld, kun je controleren of ping werkt door te zien of het programma functioneert. Laten we loggen toevoegen. De functie lijkt op printf(), maar ondersteunt slechts tot drie argumenten, exclusief het sjabloon, en een beperkte lijst van specificaties. De macro bpf_printk() vereenvoudigt de aanroep.
SEC("prog")
int xdp_main(struct xdp_md* ctx) {
+ bpf_printk("pakket ontvangen: %pn", ctx);
return XDP_PASS;
}De uitvoer gaat naar de kernel traceerpoort, die moet worden ingeschakeld:
echo -n 1 | sudo tee /sys/kernel/debug/tracing/options/trace_printkBerichtstroom bekijken:
cat /sys/kernel/debug/tracing/trace_pipeBeide commando's maken een oproep sudo ./stand log.
Ping zou nu dergelijke berichten moeten genereren:
-110930 [004] ..s1 78803.244967: 0: pakket ontvangen: 00000000ac510377Als je naar de uitvoer van de verifier kijkt, zie je vreemde berekeningen:
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#6Het probleem is dat programma's op eBPF geen datasectie hebben, dus de enige manier om een formatteringsreeks te coderen zijn immediate-argumenten van de VM-commando's:
$ python -c "import binascii; print(bytes(reversed(binascii.unhexlify('0a7025203a74656b63617020746f67'))))"
b'pakket ontvangen: %pn'Om deze reden leidt de debugging-uitvoer tot een aanzienlijke toename van de uiteindelijke code.
Verzending van XDP-pakketten
Laten we de filter aanpassen: laat het alle binnenkomende pakketten terugsturen. Dit is vanuit netwerkperspectief niet correct, omdat je de adressen in de koppen zou moeten wijzigen, maar nu is het belangrijk dat het in principe werkt.
bpf_printk("pakket ontvangen: %pn", ctx);
- return XDP_PASS;
+ return XDP_TX;
}We starten tcpdump en een werkende opdracht krijgen. xdp-remote. Het zou identieke uitgaande en binnenkomende ICMP Echo Request moeten tonen en zou moeten stoppen met laten zien van ICMP Echo Reply. Maar het doet het niet. Blijkbaar, voor de werking XDP_TX in een programma op xdp-local , zodat de bijbehorende interface xdp-remote ook een programma toegewezen heeft gekregen, zelfs als het leeg is, en het is actief.
Hoe weet ik dit?
staat het perf events-mechanisme toe, dat trouwens dezelfde virtuele machine gebruikt, wat betekent dat eBPF wordt toegepast voor de afhandeling.
Je moet goed doen uit kwaad, omdat daar niet meer iets van gemaakt kan worden.
$ sudo perf trace --call-graph dwarf -e 'xdp:*'
0.000 ping/123455 xdp:xdp_bulk_tx:ifindex=19 action=TX verzonden=0 drops=1 err=-6
veth_xdp_flush_bq ([veth])
veth_xdp_flush_bq ([veth])
veth_poll ([veth])Wat is code 6?
$ errno 6
ENXIO 6 Geen dergelijk apparaat of adresFunctie veth_xdp_flush_bq() ontvangt de foutcode van veth_xdp_xmit(), waar we naar zoeken door ENXIO en vinden het commentaar.
Herstellen van de minimale filter (XDP_PASS) in het bestand xdp_dummy.c, voegen we dit toe aan de Makefile, koppelen we het aan xdp-remote:
ip netns exec remote
ip link set dev int xdp object dummy.oNu tcpdump toont wat verwacht wordt:
62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), lengte 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), lengte 84)
192.0.2.2 > 192.0.2.1: ICMP echo verzoek, id 46966, seq 1, lengte 64
62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), lengte 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), lengte 84)
192.0.2.2 > 192.0.2.1: ICMP echo verzoek, id 46966, seq 1, lengte 64Als in plaats daarvan alleen ARP wordt weergegeven, moeten de filters worden verwijderd (dit doet sudo ./stand detach), voer ping, vervolgens de filters in en probeer het opnieuw. Het probleem is dat de filter XDP_TX ook van toepassing is op ARP, en als de stack
de namenruimten xdp-test de MAC-adres 192.0.2.1 'vergeten' is, kan het deze IP niet oplossen.
Taakstelling
Laten we overgaan naar de opgegeven taak: een SYN cookies mechanisme op XDP schrijven.
Tot nu toe blijft SYN flood een populaire DDoS-aanval, waarvan de essentie als volgt is. Bij het tot stand brengen van een verbinding (TCP-handshake) ontvangt de server een SYN, wijst bronnen toe voor de toekomstige verbinding, antwoordt met een SYNACK-pakket en wacht op een ACK. De aanvaller stuurt gewoon duizenden SYN-pakketten per seconde van elk host van een duizenden botnet. De server moet middelen onmiddellijk toewijzen bij ontvangst van het pakket, en bevrijdt deze na een grote time-out, waardoor geheugen of limieten uitgeput raken, nieuwe verbindingen niet worden geaccepteerd, de service is niet beschikbaar.
Als er geen middelen per SYN-pakket worden toegewezen, maar alleen met een SYNACK-pakket wordt geantwoord, hoe kan de server dan begrijpen dat het ACK-pakket, dat later arriveert, betrekking heeft op het SYN-pakket dat niet werd opgeslagen? De aanvaller kan immers ook valse ACK's genereren. De essentie van SYN cookie is om de seqnum verbinding parameters te coderen als een hash van adressen, poorten en veranderlijk zout. Als het ACK voor de verandering van het zout arriveert, kan de hash opnieuw worden berekend en vergeleken met acknum. Valsificeren acknum kan de aanvaller niet, aangezien het zout een geheim bevat, en hij zal het niet kunnen proberen vanwege de beperkte bandbreedte.
SYN cookie is al lang geïmplementeerd in de Linux-kernel en kan zelfs automatisch worden ingeschakeld als de SYN's te snel en massaal binnenkomen.
TCP handshake basiskennis
TCP zorgt voor gegevensoverdracht als een stroom van bytes, bijvoorbeeld, HTTP-verzoeken worden bovenop TCP verzonden. De stroom wordt in stukjes over pakketten verzonden. Alle TCP-pakketten hebben logische vlaggen en 32-bits volgnummer:
De combinatie van vlaggen bepaalt de rol van een specifiek pakket. De SYN-vlag betekent dat dit het eerste pakket van de afzender in de verbinding is. De ACK-vlag betekent dat de afzender alle gegevens van de verbinding tot byte
acknum. Een pakket kan meerdere vlaggen hebben en wordt genoemd naar hun combinatie, bijvoorbeeld een SYNACK-pakket.Het volgnummer (seqnum) bepaalt de offset in de datastroom voor het eerste byte dat in dit pakket wordt verzonden. Bijvoorbeeld, als het volgnummer in het eerste pakket met X bytes data N was, zal het in het volgende pakket met nieuwe data N+X zijn. Aan het begin van de verbinding kiest elke zijde dit nummer willekeurig.
Het erkenningsnummer (acknum) is hetzelfde offset als seqnum, maar bepaalt niet het nummer van het verzonden byte, maar het nummer van het eerste byte van de ontvanger dat de zender nog niet heeft gezien.
Aan het begin van de verbinding moeten de partijen het eens worden over seqnum en acknum. De client verzendt een SYN-pakket met zijn seqnum = X. De server antwoordt met een SYNACK-pakket waarin zijn seqnum = Y wordt vastgelegd en acknum = X + 1. De client antwoordt op SYNACK met een ACK-pakket waarin seqnum = X + 1, acknum = Y + 1. Hierna begint de eigenlijke gegevensoverdracht.
Als de gesprekspartner de ontvangst van het pakket niet bevestigt, verzendt TCP het opnieuw na een time-out.
Waarom worden SYN-cookies niet altijd gebruikt?
Ten eerste, als SYNACK of ACK verloren gaat, moet er gewacht worden op een nieuwe verzending - dit vertraagt de totstandkoming van de verbinding. Ten tweede, in het SYN-pakket - en alleen daarin! - worden verschillende opties verzonden die invloed hebben op de verdere werking van de verbinding. Door de binnenkomende SYN-pakketten niet op te slaan, negeert de server deze opties, en de client zal ze in de volgende pakketten niet meer verzenden. TCP kan nog steeds functioneren, maar de kwaliteit van de verbinding zal in ieder geval in het begin verminderen.
Vanuit het perspectief van pakketten moet het XDP-programma het volgende doen:
- beantwoord SYN met SYNACK met cookie;
- beantwoord ACK met RST (breek de verbinding);
- reset overige pakketten.
Pseudocode van het algoritme samen met de pakketontleding:
Als dit geen Ethernet is,
sla het pakket over.
Als dit geen IPv4 is,
sla het pakket over.
Als het adres in de tabel van geverifieerde (*) staat,
verminder de teller van overgebleven controles,
sla het pakket over.
Als dit geen TCP is,
reset het pakket. (**)
Als dit SYN is,
antwoord met SYN-ACK met cookie.
Als dit ACK is,
als acknum geen cookie bevat,
reset het pakket.
Voeg het adres toe aan de tabel met N overgebleven controles. (*)
Beantwoord met RST. (**)
In andere gevallen reset het pakket.Eén (*) zijn de punten gemarkeerd waar de systeemstatus moet worden beheerd - in de eerste fase kan dit worden omzeild door eenvoudigweg de TCP-handshake te implementeren met de generatie van het SYN-cookie als seqnum.
Op de plaats (**), zolang we geen tabel hebben, zullen we het pakket overslaan.
Implementatie van de TCP-handshake
Pakketontleding en codeverificatie
We hebben de netwerkheaderstructuren nodig: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) en TCP (uapi/linux/tcp.h). Het is me niet gelukt om de laatste te verbinden vanwege fouten met atomic64_t, ik moest de benodigde definities in de code kopiëren.
Alle functies die in C zijn gedefinieerd voor leesgemak, moeten op de plaats van aanroep worden ingebouwd, omdat de eBPF-verificator in de kernel terugkeerpunten verbiedt, dat wil zeggen, feitelijk, lussen en functieaanroepen.
#define INTERNAL static __attribute__((always_inline))Macro LOG() zet de afdruk in de release-build uit.
Het programma is een pijplijn van functies. Elke functie ontvangt een pakket, waarin de header van het betreffende niveau is toegewezen, bijvoorbeeld, process_ether() verwacht dat etheris ingevuld. Op basis van de analyse van de velden kan de functie het pakket naar een hoger niveau doorgeven. Het resultaat van de functie is een XDP-actie. Tot nu toe negeren de SYN- en ACK-handlers alle pakketten.
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);
}Let op de controles die A en B zijn gemarkeerd. Als je A commentarieert, compileert het programma, maar er zal een verificatiefout optreden bij het laden:
Verifier-analyse:
11: (7b) *(u64 *)(r10 -48) = r1
12: (71) r3 = *(u8 *)(r7 +13)
ongeldige toegang tot pakket, off=13 grootte=1, R7(id=0,off=0,r=0)
R7-offset bevindt zich buiten het pakket
verwerkt 11 insns (limiet 1000000) max_states_per_insn 0 totaal_states 0 piek_states 0 mark_read 0
Fout bij het ophalen van programma/map!Sleutelrij ongeldige toegang tot pakket, off=13 grootte=1, R7(id=0,off=0,r=0): er zijn uitvoeringspaden waarbij de dertiende byte vanaf het begin van de buffer buiten het pakket ligt. Aan de hand van de listing is het lastig te begrijpen over welke regel het gaat, maar er is een instructienummer (12) en een disassembler die de regels van de broncode toont:
llvm-objdump -S xdp_filter.o | lessIn dit geval verwijst hij naar de regel
LOG("Ether(proto=0x%x)", bpf_ntohs(ether->h_proto));waaruit blijkt dat het probleem ligt in ether. Altijd zo.
Het antwoord op SYN
Het doel in deze fase is om een correcte SYNACK-pakket te vormen met een vaste seqnum, die in de toekomst vervangen zal worden door SYN cookie. Alle wijzigingen vinden plaats in process_tcp_syn() en omgeving.
Pakketcontrole
Verrassend genoeg, hier is de meest opmerkelijke regel, beter gezegd, de opmerking hierbij:
/* Required to verify checksum calculation */
const void* data_end = (const void*)ctx->data_end;Bij het schrijven van de eerste versie van de code werd kernel 5.1 gebruikt, waarvoor er een verschil was tussen data_end en (const void*)ctx->data_end. Bij het schrijven van het artikel had kernel 5.3.1 dit probleem niet. Het is mogelijk dat de compiler anders met lokale variabelen omging dan met velden. Moraal van het verhaal: bij grote diepgang kan vereenvoudiging van de code helpen.
Vervolgens zijn er routinematige lengtecontroles ter ondersteuning van de verifier; over MAX_CSUM_BYTES hieronder.
const u32 ip_len = ip->ihl * 4;
if ((void*)ip + ip_len > data_end) {
return XDP_DROP; /* verkeerd pakket */
}
if (ip_len > MAX_CSUM_BYTES) {
return XDP_ABORTED; /* implementatiebeperking */
}
const u32 tcp_len = tcp->doff * 4;
if ((void*)tcp + tcp_len > (void*)ctx->data_end) {
return XDP_DROP; /* verkeerd pakket */
}
if (tcp_len > MAX_CSUM_BYTES) {
return XDP_ABORTED; /* implementatiebeperking */
}Pakket omdraaien
We vullen seqnum en acknum, zetten ACK (SYN is al gezet):
const u32 cookie = 42;
tcp->ack_seq = bpf_htonl(bpf_ntohl(tcp->seq) + 1);
tcp->seq = bpf_htonl(cookie);
tcp->ack = 1;Twitch de TCP-poorten, IP-adressen en MAC-adressen om. De standaardbibliotheek is niet beschikbaar vanuit het XDP-programma, dus memcpy() is een macro die de Clang-intrinsic verbergt.
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);Herberekening van checksums
IPv4- en TCP-checksums vereisen het optellen van alle 16-bits woorden in de headers, en de grootte van de headers is daarin opgenomen, dus het is op het moment van compilatie onbekend. Dit is een probleem omdat de verifier niet een gewone lus zal toestaan tot de grens van de variabele. De grootte van de headers is echter beperkt: tot 64 bytes elk. Je kunt een lus maken met een vast aantal iteraties die vroegtijdig kan eindigen.
Ik merk op dat er is over hoe de checksum gedeeltelijk te herberekenen, als alleen vaste woorden van pakketten zijn gewijzigd. Deze methode is echter niet universeel, en het zou moeilijker zijn om de implementatie te ondersteunen.
De functie voor het berekenen van de checksum:
#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;
}Ondanks dat size Gecontroleerd door de aanroepende code, de tweede uitgangsvoorwaarde is noodzakelijk zodat de verifier kan bewijzen dat de cyclus voltooid is.
Voor 32-bit woorden is een eenvoudigere versie gerealiseerd:
INTERNAL u32
sum16_32(u32 v) {
return (v >> 16) + (v & 0xffff);
}Eigenlijk het herschrijven van de controle sommen en het terugsturen van het pakket:
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 <check = 0;
tcp_csum += sum16(tcp, tcp_len, data_end);
tcp->check = carry(tcp_csum);
return XDP_TX;Functie carry() maakt van een 32-bit som 16-bit woorden van de controle som, volgens RFC 791.
Controle van de TCP-handshake
De filter stelt correct verbinding met netcat, waarbij de laatste ACK wordt overgeslagen, waarop Linux reageerde met een RST-pakket, omdat de netwerkstack geen SYN ontving — deze werd omgezet in een SYNACK en teruggestuurd — en vanuit het perspectief van het OS arriveerde er een pakket dat niet gerelateerd was aan open verbindingen.
$ sudo ip netns exec xdp-test nc -nv 192.0.2.1 6666
192.0.2.1 6666: Verbinding gereset door peerHet is belangrijk om dit te controleren met echte applicaties en te observeren tcpdump en een werkende opdracht krijgen. xdp-remote omdat bijvoorbeeld, hping3 reageert niet op onjuiste controle sommen.
SYN cookie
Vanuit het perspectief van XDP is de controle triviaal. Het algoritme voor berekening is primitief en waarschijnlijk kwetsbaar voor een geraffineerde aanvaller. De Linux-kern gebruikt bijvoorbeeld de cryptografische SipHash, maar de implementatie daarvan voor XDP valt duidelijk buiten het bereik van dit artikel.
Er zijn nieuwe TODO's ontstaan met betrekking tot externe interactie:
De XDP-programma kan geen
cookie_seed(het geheime deel van het zout) in een globale variabele opslaan, er is opslag in de kernel nodig waar de waarde periodiek zal worden bijgewerkt vanuit een betrouwbare generator.Bij het overeenkomen van de SYN cookie in het ACK-pakket moet je geen bericht afdrukken, maar het IP van de geverifieerde klant onthouden, zodat je later pakketten van hem kunt doorlaten.
Controle door een legitieme klant:
$ sudo ip netns exec xdp-test nc -nv 192.0.2.1 6666
192.0.2.1 6666: Verbinding gereset door peerIn de logs is het doorlopen van de controle geregistreerd (flags=0x2 — dit is SYN, flags=0x10 — dit is 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 komt overeen voor klant 20200c0Er is nog geen lijst met geverifieerde IP's, er zal geen bescherming tegen SYN flood zijn, maar hier is de reactie op ACK flood, gestart met dit commando:
sudo ip netns exec xdp-test hping3 --flood -A -s 1111 -p 2222 192.0.2.1Records in het log:
Ether(proto=0x800)
IP(src=0x15bd11a dst=0x15bd11e proto=6)
TCP(sport=3236 dport=2222 flags=0x10)
cookie mismatchConclusie
Soms wordt eBPF in het algemeen en XDP in het bijzonder meer gezien als een hulpmiddel voor de gevorderde beheerder dan als een platform voor ontwikkeling. Inderdaad, XDP is een instrument voor het ingrijpen in de pakketverwerking door de kernel, en geen alternatief voor de kernelstack, zoals DPDK en andere kernel-bypass opties. Aan de andere kant stelt XDP in staat om vrij complexe logica te implementeren, die bovendien gemakkelijk te updaten is zonder pauzes in de verkeer verwerking. De verifier veroorzaakt geen grote problemen, persoonlijk zou ik niet afzien van zoiets voor delen van userspace-code.
In het tweede deel, als het onderwerp interessant is, zullen we een tabel maken van goedgekeurde klanten en verbindingonderbrekingen, tellers implementeren en een userspace-utiliteit schrijven voor het beheren van de filter.
Links:
Bron: habr.com
