Wy skriuwe beskerming tsjin DDoS-oanfallen op XDP. Nukleêre diel

eXpress Data Path (XDP) technology lit willekeurige ferkearsferwurking wurde útfierd op Linux-ynterfaces foardat de pakketten yn 'e kernel-netwurkstapel komme. Tapassing fan XDP - beskerming tsjin DDoS-oanfallen (CloudFlare), komplekse filters, statistyksamling (Netflix). XDP-programma's wurde útfierd troch de eBPF firtuele masine, sadat se beheiningen hawwe op sawol har koade as de beskikbere kernelfunksjes ôfhinklik fan it filtertype.

It artikel is bedoeld om de tekoarten fan in protte materialen op XDP te foljen. As earste leverje se klearmakke koade dy't de funksjes fan XDP fuortendaliks omgiet: it is taret foar ferifikaasje of is te ienfâldich om problemen te feroarsaakjen. As jo ​​​​dan besykje jo koade fanôf it begjin te skriuwen, hawwe jo gjin idee wat jo moatte dwaan mei typyske flaters. Twad, manieren om XDP lokaal te testen sûnder in VM en hardware binne net bedekt, nettsjinsteande it feit dat se har eigen falkûlen hawwe. De tekst is bedoeld foar programmeurs dy't bekend binne mei netwurken en Linux dy't ynteressearre binne yn XDP en eBPF.

Yn dit diel sille wy yn detail begripe hoe't it XDP-filter is gearstald en hoe't jo it kinne testen, dan sille wy in ienfâldige ferzje skriuwe fan it bekende SYN-koekjesmeganisme op it pakketferwurkingsnivo. Wy sille noch gjin "wite list" oanmeitsje
ferifiearre kliïnten, hâld tellers en beheare it filter - genôch logs.

Wy sille skriuwe yn C - it is net modieuze, mar it is praktysk. Alle koade is beskikber op GitHub fia de keppeling oan 'e ein en is ferdield yn commits neffens de stadia beskreaun yn it artikel.

Disclaimer. Yn 'e rin fan dit artikel sil ik in mini-oplossing ûntwikkelje om DDoS-oanfallen ôf te hâlden, om't dit in realistyske taak is foar XDP en myn gebiet fan ekspertize. It haaddoel is lykwols om de technology te begripen; dit is gjin hantlieding foar it meitsjen fan klearmakke beskerming. De tutorial koade is net optimalisearre en weglating guon nuânses.

XDP Koarte Oersjoch

Ik sil allinich de haadpunten sketse om dokumintaasje en besteande artikels net te duplisearjen.

Dat, de filterkoade wurdt yn 'e kernel laden. Ynkommende pakketten wurde trochjûn oan it filter. As resultaat moat it filter in beslút nimme: it pakket trochjaan yn 'e kernel (XDP_PASS), drop pakket (XDP_DROP) of stjoer it werom (XDP_TX). It filter kin feroarje it pakket, dit is benammen wier foar XDP_TX. Jo kinne it programma ek ôfbrekke (XDP_ABORTED) en reset it pakket, mar dit is analoog assert(0) - foar debuggen.

De firtuele masine eBPF (útwreide Berkley Packet Filter) is mei opsetsin ienfâldich makke, sadat de kearn kin kontrolearje dat de koade net loopt en it ûnthâld fan oaren net beskeadiget. Kumulative beheiningen en kontrôles:

  • Loops (efterút) binne ferbean.
  • D'r is in stapel foar gegevens, mar gjin funksjes (alle C-funksjes moatte ynlined wurde).
  • Unthâld tagong bûten de stack en pakket buffer binne ferbean.
  • De koadegrutte is beheind, mar yn 'e praktyk is dit net heul wichtich.
  • Allinich oproppen nei spesjale kernelfunksjes (eBPF-helpers) binne tastien.

It ûntwerpen en ynstallearjen fan in filter sjocht der sa út:

  1. Boarnekoade (bgl kernel.c) wurdt gearstald yn objekt (kernel.o) foar de eBPF firtuele masine-arsjitektuer. Fanôf oktober 2019 wurdt kompilaasje nei eBPF stipe troch Clang en tasein yn GCC 10.1.
  2. As dizze objektkoade opropen nei kernelstruktueren befettet (bygelyks tabellen en tellers), wurde har ID's ferfongen troch nullen, wat betsjut dat sa'n koade net kin wurde útfierd. Foardat jo yn 'e kernel laden, moatte jo dizze nullen ferfange troch de ID's fan spesifike objekten dy't makke binne troch kerneloproppen (keppelje de koade). Jo kinne dit dwaan mei eksterne nutsbedriuwen, of jo kinne in programma skriuwe dat in spesifyk filter sil keppelje en laden.
  3. De kernel ferifiearret it laden programma. It ûntbrekken fan syklusen en it mislearjen fan pakket- en stapelgrinzen wurdt kontrolearre. As de ferifiearder net kin bewize dat de koade korrekt is, wurdt it programma ôfwiisd - jo moatte him kinne behagen.
  4. Nei suksesfolle ferifikaasje kompilearret de kernel de eBPF-arsjitektuerobjektkoade yn masinekoade foar de systeemarsjitektuer (just-in-time).
  5. It programma hechtet oan de ynterface en begjint pakketten te ferwurkjen.

Sûnt XDP rint yn 'e kernel, wurdt debuggen útfierd mei trace logs en, yn feite, pakketten dy't it programma filtert of genereart. eBPF soarget der lykwols foar dat de ynladen koade feilich is foar it systeem, sadat jo direkt kinne eksperimintearje mei XDP op jo lokale Linux.

It tarieden fan it miljeu

Assembly

Clang kin net direkt objektkoade produsearje foar de eBPF-arsjitektuer, dus it proses bestiet út twa stappen:

  1. C-koade kompilearje nei LLVM-bytekoade (clang -emit-llvm).
  2. Konvertearje bytekoade nei eBPF-objektkoade (llc -march=bpf -filetype=obj).

By it skriuwen fan in filter sille in pear bestannen mei helpfunksjes en makro's nuttich wêze út kernel tests. It is wichtich dat se oerienkomme mei de kernelferzje (KVER). Download se nei 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 foar 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 befettet it paad nei de kernel-headers, ARCH - systeem arsjitektuer. Paden en ark kinne wat ferskille tusken distribúsjes.

Foarbyld fan ferskillen foar 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 ferbine in map mei helpkoppen en ferskate mappen mei kernelkoppen. Symboal __KERNEL__ betsjut dat UAPI (userspace API) headers wurde definiearre foar kernel koade, sûnt it filter wurdt útfierd yn de kearn.

Stapelbeskerming kin útskeakele wurde (-fno-stack-protector), om't de eBPF-koade-ferifiearder noch altyd kontrolearret op oertredings fan stapels bûten de grinzen. It is it wurdich om optimisaasjes direkt yn te skeakeljen, om't de grutte fan 'e eBPF-bytekoade beheind is.

Litte wy begjinne mei in filter dat alle pakketten trochjaan en neat docht:

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

team make sammelt xdp_filter.o. Wêr te besykjen it no?

test stand

De stand moat twa ynterfaces befetsje: wêrop in filter sil wêze en wêrfan pakketten ferstjoerd wurde. Dit moatte folweardige Linux-apparaten wêze mei har eigen IP's om te kontrolearjen hoe gewoane applikaasjes wurkje mei ús filter.

Apparaten fan it type veth (firtuele Ethernet) binne geskikt foar ús: dit binne in pear firtuele netwurkynterfaces "ferbûn" direkt mei elkoar. Jo kinne se sa oanmeitsje (yn dizze seksje alle kommando's ip wurde útfierd út root):

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

it is xdp-remote и xdp-local - apparaat nammen. Op xdp-local (192.0.2.1/24) sil in filter wurde taheakke, mei xdp-remote (192.0.2.2/24) ynkommende ferkear wurdt ferstjoerd. D'r is lykwols in probleem: de ynterfaces binne op deselde masine, en Linux sil gjin ferkear nei ien fan har troch de oare stjoere. Jo kinne dit oplosse mei lestige regels iptables, mar se sille pakketten feroarje moatte, wat ûngemaklik is foar debuggen. It is better om netwurknammeromten te brûken (hjirnei netns).

In netwurk nammeromte befettet in set fan ynterfaces, routing tabellen, en NetFilter regels dy't binne isolearre fan ferlykbere objekten yn oare netns. Elk proses rint yn in nammeromte en hat allinich tagong ta de objekten fan dy netns. Standert hat it systeem ien netwurk nammeromte foar alle objekten, sadat jo kinne wurkje yn Linux en net witte oer netns.

Litte wy in nije nammeromte oanmeitsje xdp-test en ferpleatse it dêr xdp-remote.

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

Dan rint it proses yn xdp-test, sil net "sjen" xdp-local (it bliuwt standert yn netns) en by it ferstjoeren fan in pakket nei 192.0.2.1 sil it trochjaan xdp-remoteomdat it is de ienige ynterface op 192.0.2.0/24 tagonklik foar dit proses. Dit wurket ek yn 'e tsjinoerstelde rjochting.

By it ferpleatsen tusken netns giet de ynterface del en ferliest syn adres. Om de ynterface yn netns te konfigurearjen, moatte jo útfiere ip ... yn dizze kommando nammeromte 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

Sa't jo sjen kinne, dit is net oars as de ynstelling xdp-local yn de standert nammeromte:

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

As jo ​​rinne tcpdump -tnevi xdp-local, kinne jo sjen dat pakketten ferstjoerd fan xdp-test, wurde levere oan dizze ynterface:

ip netns exec xdp-test   ping 192.0.2.1

It is handich om in shell yn te lansearjen xdp-test. It repository hat in skript dat it wurk mei de stand automatisearret; Jo kinne bygelyks de stand konfigurearje mei it kommando sudo ./stand up en wiskje it sudo ./stand down.

Tracing

It filter is sa ferbûn mei it apparaat:

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

Key -force nedich om in nij programma te keppeljen as in oar al keppele is. "Gjin nijs is goed nijs" giet net oer dit kommando, de konklúzje is yn alle gefallen folume. oanjaan verbose opsjoneel, mar dêrmei ferskynt in rapport oer it wurk fan 'e koadeferifiearder mei in gearstallingslist:

Verifier analysis:

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

Unlink it programma fan 'e ynterface:

ip link set dev xdp-local xdp off

Yn it skript binne dit kommando's sudo ./stand attach и sudo ./stand detach.

Troch in filter te befestigjen kinne jo der wis fan wêze ping bliuwt te rinnen, mar wurket it programma? Litte wy logs tafoegje. Funksje bpf_trace_printk() gelyk oan printf(), mar stipet allinnich maksimaal trije arguminten oars as it patroan, en in beheinde list fan spesifisjers. Makro bpf_printk() simplifies de oprop.

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

De útfier giet nei it kernel-trace-kanaal, dat moat wurde ynskeakele:

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

Besjoch berjochttried:

cat /sys/kernel/debug/tracing/trace_pipe

Beide fan dizze kommando's meitsje in oprop sudo ./stand log.

Ping soe no berjochten lykas dit triggerje moatte:

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

As jo ​​​​nei de útfier fan 'e ferifiearder sjogge, sille jo frjemde berekkeningen fernimme:

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

It feit is dat eBPF-programma's gjin gegevensseksje hawwe, dus de ienige manier om in opmaakstring te kodearjen is de direkte arguminten fan VM-kommando's:

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

Om dizze reden blaast debug-útfier de resultearjende koade sterk op.

It ferstjoeren fan XDP-pakketten

Litte wy it filter feroarje: lit it alle ynkommende pakketten weromstjoere. Dit is ferkeard út it eachpunt fan it netwurk, om't it nedich wêze soe om de adressen yn 'e kopteksten te feroarjen, mar no is it wurk yn prinsipe wichtich.

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

Launch tcpdump op xdp-remote. It moat identike útgeande en ynkommende ICMP Echo Request sjen litte en stopje mei it werjaan fan ICMP Echo Reply. Mar it docht net sjen. It docht bliken dat foar wurk XDP_TX yn it programma op xdp-local is nedichnei it pear ynterface xdp-remote in programma waard ek tawiisd, sels as it wie leech, en hy waard grutbrocht.

Hoe wist ik dit?

Trace it paad fan in pakket yn 'e kernel It perf-evenemintenmeganisme lit trouwens deselde firtuele masine brûke, dat is, eBPF wurdt brûkt foar disassemblies mei eBPF.

Jo moatte goed meitsje út it kwea, want der is neat oars om it út te meitsjen.

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

Wat is koade 6?

$ errno 6
ENXIO 6 No such device or address

function veth_xdp_flush_bq() krijt in flater koade fan veth_xdp_xmit(), dêr't sykje troch ENXIO en fyn it kommentaar.

Litte wy it minimale filter weromsette (XDP_PASS) yn triem xdp_dummy.c, foegje it ta oan it Makefile, bine it oan xdp-remote:

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

No tcpdump lit sjen wat der ferwachte wurdt:

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

As allinich ARP's wurde werjûn ynstee, moatte jo de filters fuortsmite (dit docht sudo ./stand detach), lit gean ping, set dan filters yn en besykje it nochris. It probleem is dat it filter XDP_TX jildich sawol op ARP en as de stack
nammeromten xdp-test slagge om "ferjitte" it MAC-adres 192.0.2.1, it sil net by steat wêze om te lossen dit IP.

Probleemintwurding

Litte wy trochgean nei de neamde taak: skriuw in SYN-koekjesmeganisme op XDP.

SYN-oerstreaming bliuwt in populêre DDoS-oanfal, wêrfan de essinsje as folget is. As in ferbining wurdt oprjochte (TCP-handshake), ûntfangt de tsjinner in SYN, allocates boarnen foar de takomstige ferbining, reagearret mei in SYNACK-pakket en wachtet op in ACK. De oanfaller stjoert gewoan tûzenen SYN-pakketten per sekonde fan spoofed adressen fan elke host yn in multi-tûzen-sterk botnet. De tsjinner wurdt twongen om middels direkt by de oankomst fan it pakket te allocearjen, mar makket se frij nei in grutte time-out; as gefolch binne ûnthâld of grinzen útput, nije ferbiningen wurde net akseptearre, en de tsjinst is net beskikber.

As jo ​​gjin boarnen allocearje basearre op it SYN-pakket, mar allinich reagearje mei in SYNACK-pakket, hoe kin de tsjinner dan begripe dat it ACK-pakket dat letter oankaam ferwiist nei in SYN-pakket dat net bewarre is? In oanfaller kin ommers ek falske ACK's generearje. It punt fan it SYN-koekje is om it yn te kodearjen seqnum ferbining parameters as in hash fan adressen, havens en wikseljende sâlt. As de ACK slagge om te kommen foardat it sâlt waard feroare, kinne jo de hash wer berekkenje en fergelykje mei acknum. Forge acknum de oanfaller kin net, sûnt it sâlt omfiemet it geheim, en sil gjin tiid om te sortearjen troch it fanwege de beheinde kanaal.

It SYN-koekje is al lang yn 'e Linux-kernel ymplementearre en kin sels automatysk ynskeakele wurde as SYN's te fluch en massaal oankomme.

Edukatyf programma oer TCP-handshake

TCP jout gegevens oerdracht as in stream fan bytes, bygelyks, HTTP fersiken wurde oerdroegen oer TCP. De stream wurdt oerdroegen yn stikken yn pakketten. Alle TCP-pakketten hawwe logyske flaggen en 32-bit folchoardernûmers:

  • De kombinaasje fan flaggen bepaalt de rol fan in bepaald pakket. De SYN-flagge jout oan dat dit it earste pakket fan de stjoerder is op 'e ferbining. De ACK-flagge betsjut dat de stjoerder alle ferbiningsgegevens oant de byte ûntfongen hat acknum. In pakket kin ferskate flaggen hawwe en wurdt neamd troch har kombinaasje, bygelyks in SYNACK-pakket.

  • Sequence number (seqnum) spesifiseart de offset yn 'e gegevensstream foar de earste byte dy't yn dit pakket wurdt oerdroegen. Bygelyks, as yn it earste pakket mei X bytes fan gegevens dit nûmer N wie, yn it folgjende pakket mei nije gegevens sil it N + X wêze. Oan it begjin fan de ferbining kiest elke kant dit nûmer willekeurich.

  • Acknowledgement number (acknum) - deselde offset as seqnum, mar it bepaalt net it nûmer fan 'e byte dy't wurdt oerdroegen, mar it nûmer fan' e earste byte fan 'e ûntfanger, dy't de stjoerder net seach.

Oan it begjin fan de ferbining moatte de partijen it iens wurde seqnum и acknum. De kliïnt stjoert in SYN-pakket mei syn seqnum = X. De tsjinner reagearret mei in SYNACK pakket, dêr't it registrearret syn seqnum = Y en bleatsteld acknum = X + 1. De kliïnt reagearret op SYNACK mei in ACK-pakket, wêr seqnum = X + 1, acknum = Y + 1. Hjirnei begjint de eigentlike gegevensferfier.

As de peer de ûntfangst fan it pakket net erkent, stjoert TCP it opnij nei in time-out.

Wêrom wurde SYN cookies net altyd brûkt?

As earste, as SYNACK of ACK ferlern is, moatte jo wachtsje oant it wer ferstjoerd wurdt - de ferbiningsopstelling sil fertrage. Twad, yn it SYN-pakket - en allinich yn it! - in oantal opsjes wurde oerdroegen dy't de fierdere wurking fan 'e ferbining beynfloedzje. Sûnder it ûnthâlden fan ynkommende SYN-pakketten negeart de tsjinner dizze opsjes dus; de kliïnt sil se net yn 'e folgjende pakketten stjoere. TCP kin yn dit gefal wurkje, mar op syn minst yn 'e earste faze sil de kwaliteit fan' e ferbining ôfnimme.

Yn termen fan pakketten moat in XDP-programma it folgjende dwaan:

  • reagearje op SYN mei SYNACK mei in koekje;
  • reagearje op ACK mei RST (ferbrekke);
  • smyt de oerbleaune pakketten.

Pseudokoade fan it algoritme tegearre mei pakketparsing:

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

Ien (*) punten wêr't jo de steat fan it systeem moatte beheare wurde markearre - yn 'e earste faze kinne jo sûnder har dwaan troch gewoan in TCP-handshake te ymplementearjen mei it generearjen fan in SYN-koekje as seqnum.

Op it plak sels (**), wylst wy gjin tafel hawwe, sille wy it pakket oerslaan.

It útfieren fan TCP-handshake

It pakket parsearje en de koade ferifiearje

Wy sille netwurkheaderstruktueren nedich wêze: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) en TCP (uapi/linux/tcp.h). Ik koe de lêste net ferbine troch flaters yn ferbân mei atomic64_t, Ik moast de nedige definysjes kopiearje yn 'e koade.

Alle funksjes dy't yn C markearre binne foar lêsberens moatte wurde ynlined op it punt fan oprop, om't de eBPF-ferifiearder yn 'e kearn ferbiedt backtracking, dat is, yn feite, loops en funksjeoproppen.

#define INTERNAL static __attribute__((always_inline))

Makro LOG() skeakelet printsjen yn 'e release build út.

It programma is in conveyor fan funksjes. Elk krijt in pakket wêryn't de korrespondearjende nivo-koptekst markearre is, bygelyks, process_ether() ferwachtet dat it fol wurdt ether. Op grûn fan de resultaten fan fjildanalyse kin de funksje it pakket nei in heger nivo trochjaan. It resultaat fan 'e funksje is de XDP-aksje. Foar no passe de SYN- en ACK-hannelers alle pakketten troch.

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

Ik tekenje jo oandacht op 'e kontrôles markearre A en B. As jo ​​kommentaar út A, it programma sil bouwe, mar der sil in ferifikaasje flater by it laden:

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!

Key string invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): Der binne eksekúsjepaden as de trettjinde byte fan it begjin fan 'e buffer bûten it pakket is. It is lestich te begripen út 'e list oer hokker line wy ​​it hawwe, mar d'r is in ynstruksjenûmer (12) en in disassembler dy't de rigels fan boarnekoade toant:

llvm-objdump -S xdp_filter.o | less

Yn dit gefal ferwiist it nei de line

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

dat makket dúdlik dat it probleem is ether. It soe altyd sa wêze.

Antwurdzje op SYN

It doel op dit poadium is om in korrekt SYNACK-pakket te generearjen mei in fêste seqnum, dat yn 'e takomst ferfongen wurdt troch it SYN-koekje. Alle feroarings komme foar yn process_tcp_syn() en omlizzende gebieten.

Pakket ferifikaasje

Geweldich genôch, hjir is de meast opmerklike rigel, of leaver, it kommentaar dêrop:

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

By it skriuwen fan 'e earste ferzje fan' e koade waard de 5.1-kernel brûkt, foar de ferifikaasje wêrfan d'r in ferskil wie tusken data_end и (const void*)ctx->data_end. Op it momint fan skriuwen hie kernel 5.3.1 dit probleem net. It is mooglik dat de kompilator in oare lokale fariabele tagong hat as in fjild. Moraal fan it ferhaal: ferienfâldigjen fan de koade kin helpe as der in soad nêst.

Folgjende binne routine lingte kontrôles foar de gloarje fan 'e verifier; O MAX_CSUM_BYTES ûnder.

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

Unfolding it pakket

Wy folje yn seqnum и acknum, set ACK (SYN is al ynsteld):

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

Ruilje TCP-poarten, IP-adres en MAC-adressen. De standertbibleteek is net tagonklik fanút it XDP-programma, dus memcpy() - in makro dy't de Clang-yntrinsiken ferberget.

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

Herberekkening fan kontrôlesummen

IPv4- en TCP-kontrôlesummen fereaskje de tafoeging fan alle 16-bit wurden yn 'e kopteksten, en de grutte fan' e kopteksten wurdt yn har skreaun, dat is ûnbekend op kompilaasjetiid. Dit is in probleem om't de ferifiearder de normale loop net nei de grinsfariabele sil oerslaan. Mar de grutte fan 'e kopteksten is beheind: elk oant 64 bytes. Jo kinne in loop meitsje mei in fêst oantal iteraasjes, dy't betiid einigje kinne.

Ik merk op dat der is RFC 1624 oer hoe't jo de kontrôlesum foar in part opnij berekkenje as allinich de fêste wurden fan 'e pakketten feroare wurde. De metoade is lykwols net universele, en de ymplemintaasje soe dreger wêze om te ûnderhâlden.

Checksum berekkening funksje:

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

Alhoewol size ferifiearre troch de calling koade, de twadde útgong betingst is nedich, sadat de ferifier kin bewize it foltôgjen fan de lus.

Foar 32-bit wurden wurdt in ienfâldiger ferzje ymplementearre:

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

Eigentlik de kontrôlesummen opnij berekkenje en it pakket weromstjoere:

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;

function carry() makket in kontrôlesum út in 32-bit som fan 16-bit wurden, neffens RFC 791.

TCP handshake ferifikaasje

It filter makket korrekt in ferbining mei netcat, ûntbrekt de lêste ACK, wêrop Linux reagearre mei in RST-pakket, om't de netwurkstapel gjin SYN ûntfong - it waard omboud ta SYNACK en weromstjoerd - en fanút it OS-perspektyf kaam in pakket oan dat net relatearre wie oan iepen ferbinings.

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

It is wichtich om te kontrolearjen mei folweardige applikaasjes en observearje tcpdump op xdp-remote omdat bgl. hping3 reagearret net op ferkearde kontrôlesummen.

Ut in XDP-perspektyf is de ferifikaasje sels triviaal. It berekkeningsalgoritme is primityf en wierskynlik kwetsber foar in ferfine oanfaller. De Linux-kernel brûkt bygelyks de kryptografyske SipHash, mar de ymplemintaasje foar XDP is dúdlik bûten it berik fan dit artikel.

Yntrodusearre foar nije TODO's yn ferbân mei eksterne kommunikaasje:

  • XDP programma kin net opslaan cookie_seed (it geheime diel fan it sâlt) yn in globale fariabele, jo hawwe opslach nedich yn 'e kearn, wêrfan de wearde periodyk bywurke wurdt fan in betroubere generator.

  • As it SYN-koekje oerienkomt mei it ACK-pakket, hoege jo gjin berjocht te printsjen, mar ûnthâlde de IP fan 'e ferifiearre kliïnt om troch te gean mei it trochjaan fan pakketten derfan.

Legitime kliïntferifikaasje:

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

De logs litte sjen dat de kontrôle trochjûn is (flags=0x2 - dit is SYN, flags=0x10 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 matches for client 20200c0

Wylst d'r gjin list mei ferifiearre IP's is, sil d'r gjin beskerming wêze tsjin 'e SYN-oerstreaming sels, mar hjir is de reaksje op in ACK-oerstreaming lansearre troch it folgjende kommando:

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

Log yngongen:

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

konklúzje

Soms wurde eBPF yn it algemien en XDP yn it bysûnder mear presintearre as in avansearre administrator-ark dan as in ûntwikkelingsplatfoarm. Yndied, XDP is in ark foar bemuoienis mei de ferwurking fan pakketten troch de kernel, en gjin alternatyf foar de kernel stack, lykas DPDK en oare kernel bypass opsjes. Oan 'e oare kant lit XDP jo frij komplekse logika ymplementearje, dy't boppedat maklik te aktualisearjen is sûnder ûnderbrekking yn ferkearsferwurking. De ferifiearder makket gjin grutte problemen; persoanlik soe ik dit net wegerje foar dielen fan brûkersromtekoade.

Yn it twadde diel, as it ûnderwerp nijsgjirrich is, sille wy de tabel foltôgje mei ferifiearre kliïnten en loskoppen, tellers ymplementearje en in brûkersromte-hulpprogramma skriuwe om it filter te behearjen.

Ferwizings:

Boarne: www.habr.com

Add a comment