Kirjoitamme XDP:lle suojan DDoS-hyökkäyksiä vastaan. Ydinosa

eXpress Data Path (XDP) -teknologia mahdollistaa satunnaisen liikenteen käsittelyn Linux-liitännöissä ennen kuin paketit tulevat ytimen verkkopinoon. XDP:n sovellus - suojaus DDoS-hyökkäyksiä vastaan ​​(CloudFlare), monimutkaiset suodattimet, tilastojen kerääminen (Netflix). XDP-ohjelmat suoritetaan eBPF-virtuaalikoneella, joten niillä on sekä koodin että käytettävissä olevien ytimen toimintojen rajoituksia suodattimen tyypistä riippuen.

Artikkelin tarkoituksena on täyttää lukuisten XDP-materiaalien puutteet. Ensinnäkin ne tarjoavat valmiin koodin, joka ohittaa välittömästi XDP:n ominaisuudet: se on valmis tarkistettavaksi tai on liian yksinkertainen aiheuttaakseen ongelmia. Kun yrität sitten kirjoittaa koodisi tyhjästä, sinulla ei ole aavistustakaan, mitä tehdä tyypillisille virheille. Toiseksi tapoja testata XDP:tä paikallisesti ilman virtuaalikonetta ja laitteistoa ei kata, vaikka niillä on omat sudenkuopat. Teksti on tarkoitettu XDP:stä ja eBPF:stä kiinnostuneille ohjelmoijille, jotka tuntevat verkottumisen ja Linuxin.

Tässä osassa ymmärrämme yksityiskohtaisesti, kuinka XDP-suodatin kootaan ja miten se testataan, ja sitten kirjoitamme yksinkertaisen version tunnetusta SYN-evästemekanismista paketinkäsittelytasolla. Emme vielä luo "valkoista listaa".
vahvistetut asiakkaat, pidä laskurit ja hallitse suodatinta - tarpeeksi lokeja.

Kirjoitamme C:llä - se ei ole muodikasta, mutta se on käytännöllistä. Kaikki koodi on saatavilla GitHubissa lopussa olevan linkin kautta, ja se on jaettu sitoumuksiin artikkelissa kuvattujen vaiheiden mukaisesti.

Vastuuvapauslauseke. Tämän artikkelin aikana kehitän miniratkaisun DDoS-hyökkäysten torjumiseksi, koska tämä on realistinen tehtävä XDP:lle ja osaamisalueelleni. Päätavoitteena on kuitenkin tekniikan ymmärtäminen, tämä ei ole opas valmiin suojan luomiseen. Opetuskoodia ei ole optimoitu ja siitä on jätetty pois joitakin vivahteita.

XDP lyhyt yleiskatsaus

Esitän vain keskeiset kohdat, jotta en toista dokumentaatiota ja olemassa olevia artikkeleita.

Joten suodatinkoodi ladataan ytimeen. Saapuvat paketit välitetään suodattimelle. Tämän seurauksena suodattimen on tehtävä päätös: välittää paketti ytimeen (XDP_PASS), pudota paketti (XDP_DROP) tai lähetä se takaisin (XDP_TX). Suodatin voi muuttaa pakkausta, tämä pätee erityisesti XDP_TX. Voit myös keskeyttää ohjelman (XDP_ABORTED) ja nollaa paketti, mutta tämä on analoginen assert(0) - virheenkorjaukseen.

eBPF (Extended Berkley Packet Filter) -virtuaalikone on tehty tarkoituksella yksinkertaiseksi, jotta ydin voi tarkistaa, että koodi ei silmukkaa eikä vahingoita muiden ihmisten muistia. Kumulatiiviset rajoitukset ja tarkistukset:

  • Silmukat (taaksepäin) ovat kiellettyjä.
  • Tiedoilla on pino, mutta ei funktioita (kaikkien C-funktioiden on oltava rivillä).
  • Muistin käyttö pinon ja pakettipuskurin ulkopuolella on kielletty.
  • Koodin koko on rajoitettu, mutta käytännössä se ei ole kovin merkittävää.
  • Vain kutsut erityisille ytimen funktioille (eBPF-apuohjelmat) ovat sallittuja.

Suodattimen suunnittelu ja asennus näyttää tältä:

  1. Lähdekoodi (esim kernel.c) on käännetty objektiksi (kernel.o) eBPF-virtuaalikonearkkitehtuurille. Lokakuusta 2019 lähtien eBPF:ään kääntämistä tukee Clang, ja se on luvattu GCC 10.1:ssä.
  2. Jos tämä objektikoodi sisältää kutsuja ydinrakenteisiin (esimerkiksi taulukoihin ja laskureihin), niiden tunnukset korvataan nolilla, mikä tarkoittaa, että tällaista koodia ei voida suorittaa. Ennen kuin lataat ytimeen, sinun on korvattava nämä nollat ​​tiettyjen ytimen kutsujen avulla luotujen objektien tunnuksilla (linkitä koodi). Voit tehdä tämän ulkoisilla apuohjelmilla tai kirjoittaa ohjelman, joka linkittää ja lataa tietyn suodattimen.
  3. Ydin tarkistaa ladatun ohjelman. Jaksojen puuttuminen ja paketti- ja pinorajojen ylittämisen epäonnistuminen tarkistetaan. Jos varmentaja ei pysty todistamaan, että koodi on oikea, ohjelma hylätään - sinun on voitava miellyttää häntä.
  4. Onnistuneen tarkistuksen jälkeen ydin kokoaa eBPF-arkkitehtuurin objektikoodin järjestelmäarkkitehtuurin konekoodiksi (just-in-time).
  5. Ohjelma kiinnittyy käyttöliittymään ja alkaa käsitellä paketteja.

Koska XDP toimii ytimessä, virheenkorjaus suoritetaan käyttämällä jäljityslokeja ja itse asiassa paketteja, jotka ohjelma suodattaa tai luo. eBPF kuitenkin varmistaa, että ladattu koodi on suojattu järjestelmälle, joten voit kokeilla XDP:tä suoraan paikallisella Linuxillasi.

Ympäristön valmistelu

kokoonpano

Clang ei pysty suoraan tuottamaan objektikoodia eBPF-arkkitehtuurille, joten prosessi koostuu kahdesta vaiheesta:

  1. Käännä C-koodi LLVM-tavukoodiksi (clang -emit-llvm).
  2. Muunna tavukoodi eBPF-objektikoodiksi (llc -march=bpf -filetype=obj).

Suodatinta kirjoitettaessa pari tiedostoa, joissa on aputoimintoja ja makroja, on hyödyllinen ydintesteistä. On tärkeää, että ne vastaavat ytimen versiota (KVER). Lataa ne kohteeseen 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 Arch Linuxille (ydin 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 sisältää polun ytimen otsikoihin, ARCH - Järjestelmäarkkitehtuuri. Polut ja työkalut voivat vaihdella hieman jakelujen välillä.

Esimerkki eroista Debian 10:ssä (ydin 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 yhdistää hakemiston lisäotsikoineen ja useita hakemistoja ytimen otsikoilla. Symboli __KERNEL__ tarkoittaa, että UAPI (userspace API) -otsikot on määritelty ydinkoodille, koska suodatin suoritetaan ytimessä.

Pinosuojaus voidaan poistaa käytöstä (-fno-stack-protector), koska eBPF-koodin todentaja tarkistaa edelleen pinon ulkopuolisten rajojen rikkomukset. Optimoinnit kannattaa ottaa heti käyttöön, koska eBPF-tavukoodin koko on rajallinen.

Aloitetaan suodattimella, joka läpäisee kaikki paketit eikä tee mitään:

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

Joukkue make kerää xdp_filter.o. Missä voin kokeilla sitä nyt?

Testiteline

Telineessä tulee olla kaksi rajapintaa: joissa on suodatin ja josta paketteja lähetetään. Näiden on oltava täysimittaisia ​​Linux-laitteita, joissa on omat IP-osoitteet, jotta voidaan tarkistaa, kuinka tavalliset sovellukset toimivat suodattimemme kanssa.

Meille sopivat veth (virtual Ethernet) -tyyppiset laitteet: nämä ovat pari virtuaalista verkkorajapintaa, jotka on "liitetty" suoraan toisiinsa. Voit luoda ne näin (tässä osiossa kaikki komennot ip suoritetaan alkaen root):

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

Täällä xdp-remote и xdp-local — laitteiden nimet. Päällä xdp-local (192.0.2.1/24) suodatin kiinnitetään, kanssa xdp-remote (192.0.2.2/24) saapuva liikenne lähetetään. On kuitenkin ongelma: liitännät ovat samassa koneessa, eikä Linux lähetä liikennettä yhdelle niistä toisen kautta. Voit ratkaista tämän hankalia sääntöjä noudattaen iptables, mutta heidän on vaihdettava paketteja, mikä on hankalaa virheenkorjauksen kannalta. On parempi käyttää verkon nimiavaruuksia (jäljempänä netns).

Verkon nimiavaruus sisältää joukon rajapintoja, reititystaulukoita ja NetFilter-sääntöjä, jotka on eristetty samankaltaisista objekteista muissa netnsissä. Jokainen prosessi toimii nimiavaruudessa, ja sillä on pääsy vain kyseisen netns:n objekteihin. Oletuksena järjestelmässä on yksi verkon nimiavaruus kaikille objekteille, joten voit työskennellä Linuxissa etkä tiedä netnsistä.

Luodaan uusi nimiavaruus xdp-test ja siirrä se sinne xdp-remote.

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

Sitten prosessi käynnissä xdp-test, ei "näe" xdp-local (se pysyy netnsissä oletuksena) ja lähetettäessä paketti 192.0.2.1 se välittää sen läpi xdp-remotekoska se on ainoa tämän prosessin käytettävissä oleva liitäntä 192.0.2.0/24:ssä. Tämä toimii myös päinvastaiseen suuntaan.

Netn-verkkojen välillä liikkuessa käyttöliittymä menee alas ja menettää osoitteensa. Netns-liittymän konfiguroimiseksi sinun on suoritettava ip ... tässä komennon nimiavaruudessa 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

Kuten näet, tämä ei eroa asetuksesta xdp-local oletusnimiavaruudessa:

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

Jos juokset tcpdump -tnevi xdp-local, näet, että paketit on lähetetty xdp-test, toimitetaan tähän käyttöliittymään:

ip netns exec xdp-test   ping 192.0.2.1

On kätevää käynnistää kuori sisään xdp-test. Arkistossa on skripti, joka automatisoi telineen kanssa työskentelyn; voit esimerkiksi konfiguroida jalustan komennolla sudo ./stand up ja poista se sudo ./stand down.

Jäljitys

Suodatin liittyy laitteeseen seuraavasti:

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

avain -force tarvitaan uuden ohjelman linkittämiseen, jos toinen on jo linkitetty. "Mikään uutinen ei ole hyvä uutinen" ei koske tätä käskyä, johtopäätös on joka tapauksessa laaja. osoittaa verbose valinnainen, mutta sen mukana tulee raportti koodin vahvistajan työstä kokoonpanoluettelolla:

Verifier analysis:

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

Poista ohjelman linkitys käyttöliittymästä:

ip link set dev xdp-local xdp off

Skriptissä nämä ovat komentoja sudo ./stand attach и sudo ./stand detach.

Kiinnittämällä suodattimen voit varmistaa sen ping jatkaa toimintaansa, mutta toimiiko ohjelma? Lisätään lokit. Toiminto bpf_trace_printk() samanlainen kuin printf(), mutta tukee enintään kolmea muuta argumenttia kuin mallia ja rajoitettua luetteloa määrityksistä. Makro bpf_printk() yksinkertaistaa puhelua.

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

Tulos menee ytimen jäljityskanavalle, joka on otettava käyttöön:

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

Katso viestiketju:

cat /sys/kernel/debug/tracing/trace_pipe

Molemmat komennot soittavat sudo ./stand log.

Pingin pitäisi nyt laukaista tällaisia ​​viestejä:

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

Jos tarkastelet tarkasti todentajan tulosta, huomaat outoja laskelmia:

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

Tosiasia on, että eBPF-ohjelmissa ei ole tietoosiota, joten ainoa tapa koodata muotomerkkijono on VM-komentojen välittömät argumentit:

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

Tästä syystä debug-tulostus paisuttaa tuloksena olevan koodin suuresti.

XDP-pakettien lähettäminen

Vaihdetaan suodatin: anna sen lähettää takaisin kaikki saapuvat paketit. Tämä on väärin verkon näkökulmasta, koska otsikoiden osoitteita joutuisi muuttamaan, mutta nyt periaatteellinen työ on tärkeää.

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

Tuoda markkinoille tcpdump päälle xdp-remote. Sen pitäisi näyttää identtiset lähtevät ja saapuvat ICMP Echo Request ja lopettaa ICMP Echo Reply -vastauksen näyttäminen. Mutta se ei näy. Se käy ilmi työhön XDP_TX ohjelmassa päällä xdp-local täytyypariliitäntään xdp-remote myös ohjelma määrättiin, vaikka se oli tyhjä, ja hänet kasvatettiin.

Mistä tiesin tämän?

Jäljitä paketin polku ytimessä Perf-tapahtumamekanismi sallii muuten saman virtuaalikoneen käytön, eli eBPF:ää käytetään purkamiseen eBPF:n kanssa.

Sinun täytyy tehdä pahasta hyvää, koska ei ole mitään muuta, mistä sitä tehdä.

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

Mikä on koodi 6?

$ errno 6
ENXIO 6 No such device or address

Toiminto veth_xdp_flush_bq() vastaanottaa virhekoodin osoitteesta veth_xdp_xmit(), josta hae ENXIO ja etsi kommentti.

Palautetaan vähimmäissuodatin (XDP_PASS) tiedostossa xdp_dummy.c, lisää se Makefile-tiedostoon, sido se siihen xdp-remote:

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

Nyt tcpdump näyttää mitä odotetaan:

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

Jos sen sijaan näytetään vain ARP:t, sinun on poistettava suodattimet (tämä tekee sudo ./stand detach), päästä irti ping, aseta sitten suodattimet ja yritä uudelleen. Ongelma on suodatin XDP_TX voimassa sekä ARP:ssä että pinossa
nimitilat xdp-test onnistui "unnottamaan" MAC-osoitteen 192.0.2.1, se ei pysty ratkaisemaan tätä IP-osoitetta.

Ongelma

Siirrytään esitettyyn tehtävään: kirjoita SYN-evästemekanismi XDP:hen.

SYN-tulva on edelleen suosittu DDoS-hyökkäys, jonka ydin on seuraava. Kun yhteys on muodostettu (TCP-kättely), palvelin vastaanottaa SYN:n, varaa resurssit tulevaa yhteyttä varten, vastaa SYNACK-paketilla ja odottaa ACK:ta. Hyökkääjä yksinkertaisesti lähettää tuhansia SYN-paketteja sekunnissa väärennetyistä osoitteista kustakin isännästä usean tuhannen vahvuuden botnetissä. Palvelin on pakotettu allokoimaan resursseja heti paketin saapuessa, mutta vapauttaa ne suuren aikakatkaisun jälkeen, minkä seurauksena muisti tai rajoitukset loppuvat, uusia yhteyksiä ei hyväksytä ja palvelu ei ole käytettävissä.

Jos et varaa resursseja SYN-paketin perusteella, vaan vastaa vain SYNACK-paketilla, miten palvelin sitten voi ymmärtää, että myöhemmin saapunut ACK-paketti viittaa SYN-pakettiin, jota ei ole tallennettu? Hyökkääjä voi loppujen lopuksi tuottaa myös väärennettyjä kuittauksia. SYN-evästeen tarkoitus on koodata se sisään seqnum yhteysparametrit osoitteiden, porttien ja muuttuvan suolan hajautusarvona. Jos ACK onnistui saapumaan ennen suolan vaihtoa, voit laskea hashin uudelleen ja verrata sitä acknum. Forge acknum hyökkääjä ei voi, koska suola sisältää salaisuuden, eikä hänellä ole aikaa lajitella sitä rajallisen kanavan vuoksi.

SYN-eväste on otettu käyttöön Linux-ytimessä pitkään, ja se voidaan jopa ottaa automaattisesti käyttöön, jos SYN-eväste saapuu liian nopeasti ja massaa.

Koulutusohjelma TCP-kättelystä

TCP tarjoaa tiedonsiirron tavuvirtana, esimerkiksi HTTP-pyynnöt lähetetään TCP:n kautta. Virta välitetään kappaleina paketteina. Kaikilla TCP-paketeilla on loogiset liput ja 32-bittiset järjestysnumerot:

  • Lippujen yhdistelmä määrittää tietyn paketin roolin. SYN-lippu osoittaa, että tämä on lähettäjän ensimmäinen paketti yhteydessä. ACK-lippu tarkoittaa, että lähettäjä on vastaanottanut kaikki yhteystiedot tavuun asti acknum. Paketissa voi olla useita lippuja ja sitä kutsutaan niiden yhdistelmällä, esimerkiksi SYNACK-paketti.

  • Sekvenssinumero (seqnum) määrittää datavirran siirtymän ensimmäiselle tavulle, joka lähetetään tässä paketissa. Esimerkiksi jos ensimmäisessä paketissa, jossa on X tavua dataa, tämä luku oli N, seuraavassa uutta dataa sisältävässä paketissa se on N+X. Yhteyden alussa kumpikin osapuoli valitsee tämän numeron satunnaisesti.

  • Kuittausnumero (acknum) - sama siirtymä kuin seqnum, mutta se ei määritä lähetettävän tavun numeroa, vaan vastaanottajan ensimmäisen tavun numeroa, jota lähettäjä ei nähnyt.

Yhteyden alussa osapuolten on sovittava seqnum и acknum. Asiakas lähettää SYN-paketin mukanaan seqnum = X. Palvelin vastaa SYNACK-paketilla, johon se tallentaa sen seqnum = Y ja paljastaa acknum = X + 1. Asiakas vastaa SYNACKiin ACK-paketilla, jossa seqnum = X + 1, acknum = Y + 1. Tämän jälkeen alkaa varsinainen tiedonsiirto.

Jos vertaiskumppani ei kuitata paketin vastaanottamista, TCP lähettää sen uudelleen aikakatkaisun jälkeen.

Miksi SYN-evästeitä ei aina käytetä?

Ensinnäkin, jos SYNACK tai ACK katoaa, sinun on odotettava sen lähettämistä uudelleen - yhteyden muodostaminen hidastuu. Toiseksi, SYN-paketissa - ja vain siinä! — lähetetään useita vaihtoehtoja, jotka vaikuttavat yhteyden jatkotoimintaan. Muistamatta saapuvia SYN-paketteja, palvelin jättää nämä vaihtoehdot huomioimatta, eikä asiakas lähetä niitä seuraavissa paketeissa. TCP voi toimia tässä tapauksessa, mutta ainakin alkuvaiheessa yhteyden laatu heikkenee.

Pakettien suhteen XDP-ohjelman tulee tehdä seuraavaa:

  • vastaa SYN:ään SYNACK:llä evästeellä;
  • vastaa ACK:iin RST:llä (katkaise yhteys);
  • hävitä loput paketit.

Algoritmin pseudokoodi ja paketin jäsentäminen:

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

Yksi (*) kohdat, joissa sinun täytyy hallita järjestelmän tilaa, on merkitty - ensimmäisessä vaiheessa voit pärjätä ilman niitä yksinkertaisesti toteuttamalla TCP-kättelyn generoimalla SYN-evästeen sekvenssinä.

Paikan päällä (**), vaikka meillä ei ole pöytää, ohitamme paketin.

TCP-kättelyn toteuttaminen

Paketin jäsentäminen ja koodin tarkistaminen

Tarvitsemme verkon otsikkorakenteet: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) ja TCP (uapi/linux/tcp.h). En pystynyt yhdistämään jälkimmäistä virheiden vuoksi atomic64_t, minun piti kopioida tarvittavat määritelmät koodiin.

Kaikki funktiot, jotka on korostettu C:ssä luettavuuden vuoksi, on siivottava kutsupisteessä, koska ytimen eBPF-vahvistin estää paluumatkan eli itse asiassa silmukat ja funktiokutsut.

#define INTERNAL static __attribute__((always_inline))

makro LOG() poistaa tulostuksen käytöstä julkaisuversiossa.

Ohjelma on toimintojen kuljetin. Jokainen vastaanottaa paketin, jossa vastaava tason otsikko on korostettu, esim. process_ether() odottaa sen täyttyvän ether. Kenttäanalyysin tulosten perusteella funktio voi siirtää paketin korkeammalle tasolle. Toiminnon tulos on XDP-toiminto. Toistaiseksi SYN- ja ACK-käsittelijät välittävät kaikki paketit.

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

Kiinnitän huomionne A:lla ja B:llä merkittyihin tarkistuksiin. Jos kommentoit A:ta, ohjelma rakentuu, mutta latauksessa tulee vahvistusvirhe:

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!

Näppäinjono invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): On olemassa suorituspolkuja, kun kolmastoista tavu puskurin alusta on paketin ulkopuolella. Listauksesta on vaikea ymmärtää, mistä rivistä puhumme, mutta siellä on ohjenumero (12) ja disassembler, joka näyttää lähdekoodin rivit:

llvm-objdump -S xdp_filter.o | less

Tässä tapauksessa se osoittaa viivaan

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

mikä tekee selväksi, että ongelma on ether. Se olisi aina näin.

Vastaa SYN:lle

Tavoitteena tässä vaiheessa on luoda oikea SYNACK-paketti kiinteällä seqnum, joka korvataan tulevaisuudessa SYN-evästeellä. Kaikki muutokset tapahtuvat sisällä process_tcp_syn() ja ympäröivät alueet.

Paketin vahvistus

Kummallista kyllä, tässä on merkittävin rivi tai pikemminkin sen kommentti:

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

Koodin ensimmäistä versiota kirjoitettaessa käytettiin 5.1-ydintä, jonka todentajassa oli ero data_end и (const void*)ctx->data_end. Kirjoitushetkellä ytimessä 5.3.1 ei ollut tätä ongelmaa. On mahdollista, että kääntäjä käytti paikallista muuttujaa eri tavalla kuin kenttää. Tarinan moraali: Koodin yksinkertaistaminen voi auttaa, kun sisäkkäisiä on paljon.

Seuraavaksi rutiininomaiset pituustarkastukset todentajan kunniaksi; O MAX_CSUM_BYTES jäljempänä.

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 avaaminen

täyttää seqnum и acknum, aseta ACK (SYN on jo asetettu):

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

Vaihda TCP-portteja, IP-osoitetta ja MAC-osoitteita. Vakiokirjasto ei ole käytettävissä XDP-ohjelmasta, joten memcpy() - makro, joka piilottaa Clangin ominaispiirteet.

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

Tarkistussummien uudelleenlaskenta

IPv4- ja TCP-tarkistussummat edellyttävät kaikkien 16-bittisten sanojen lisäämistä otsikoihin, joihin kirjoitetaan otsikoiden koko, eli käännöshetkellä tuntematon. Tämä on ongelma, koska todentaja ei ohita normaalia silmukkaa rajamuuttujaan. Mutta otsikoiden koko on rajoitettu: jopa 64 tavua kukin. Voit tehdä silmukan kiinteällä iteraatiomäärällä, joka voi päättyä aikaisin.

Huomautan, että on RFC 1624 siitä, kuinka tarkistussumma lasketaan osittain uudelleen, jos vain pakettien kiinteitä sanoja muutetaan. Menetelmä ei kuitenkaan ole universaali, ja toteutusta olisi vaikeampi ylläpitää.

Tarkistussumman laskentatoiminto:

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

Siitä huolimatta size Todennetaan kutsukoodilla, toinen poistumisehto on välttämätön, jotta varmentaja voi todistaa silmukan valmistumisen.

32-bittisille sanoille on toteutettu yksinkertaisempi versio:

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

Itse asiassa tarkistussummien uudelleenlaskeminen ja paketin lähettäminen takaisin:

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;

Toiminto carry() tekee tarkistussumman 32-bittisten sanojen 16-bittisestä summasta RFC 791:n mukaan.

TCP-kättelyn vahvistus

Suodatin muodostaa oikein yhteyden netcat, puuttui lopullinen kuittaus, johon Linux vastasi RST-paketilla, koska verkkopino ei saanut SYN:ää - se muutettiin SYNACKiksi ja lähetettiin takaisin - ja käyttöjärjestelmän näkökulmasta saapui paketti, joka ei liittynyt avaamiseen. yhteyksiä.

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

On tärkeää tarkistaa täydellisillä sovelluksilla ja tarkkailla tcpdump päälle xdp-remote koska esim. hping3 ei vastaa vääriin tarkistussummiin.

XDP:n näkökulmasta itse varmennus on triviaali. Laskenta-algoritmi on primitiivinen ja todennäköisesti alttiina kehittyneelle hyökkääjälle. Esimerkiksi Linux-ydin käyttää kryptografista SipHashia, mutta sen toteutus XDP:lle ei selvästikään kuulu tämän artikkelin piiriin.

Otettu käyttöön uusille ulkoiseen viestintään liittyville TODOille:

  • XDP-ohjelmaa ei voi tallentaa cookie_seed (suolan salainen osa) globaalissa muuttujassa, tarvitset tallennusta ytimeen, jonka arvoa päivitetään säännöllisesti luotettavasta generaattorista.

  • Jos SYN-eväste täsmää ACK-paketissa, sinun ei tarvitse tulostaa viestiä, vaan muistaa vahvistetun asiakkaan IP-osoite, jotta voit jatkaa pakettien välittämistä siitä.

Laillisen asiakkaan vahvistus:

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

Lokit osoittavat, että tarkistus läpäisi (flags=0x2 - tämä on SYN, flags=0x10 on 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

Vaikka vahvistetuista IP-osoitteista ei ole luetteloa, itse SYN-tulvaa vastaan ​​ei ole suojaa, mutta tässä on reaktio seuraavan komennon käynnistämään ACK-tulvaan:

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

Lokimerkinnät:

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

Johtopäätös

Joskus eBPF yleensä ja erityisesti XDP esitetään enemmän edistyneenä järjestelmänvalvojan työkaluna kuin kehitysalustana. Itse asiassa XDP on työkalu, joka häiritsee ytimen pakettien käsittelyä, eikä vaihtoehto ytimen pinolle, kuten DPDK ja muut ytimen ohitusvaihtoehdot. Toisaalta XDP mahdollistaa varsin monimutkaisen logiikan toteuttamisen, joka on lisäksi helppo päivittää ilman liikenteen käsittelyn keskeytyksiä. Todentaja ei aiheuta suuria ongelmia; henkilökohtaisesti en kieltäisi tätä käyttäjätilakoodin osien osalta.

Toisessa osassa, jos aihe kiinnostaa, täydennämme taulukkoa vahvistetuista asiakkaista ja katkoksista, toteutamme laskurit ja kirjoitamme käyttäjätila-apuohjelman suodattimen hallintaan.

viitteet:

Lähde: will.com

Lisää kommentti