Ni skribas protekton kontraŭ DDoS-atakoj sur XDP. Nuklea parto

eXpress Data Path (XDP) teknologio permesas hazardan trafikan prilaboradon esti farita sur Linuksaj interfacoj antaŭ ol la pakaĵetoj eniras la kernan retstakon. Apliko de XDP - protekto kontraŭ DDoS-atakoj (CloudFlare), kompleksaj filtriloj, statistika kolekto (Netflix). XDP-programoj estas ekzekutitaj per la virtuala maŝino eBPF, do ili havas restriktojn sur kaj sia kodo kaj la disponeblaj kernaj funkcioj depende de la filtrila tipo.

La artikolo celas plenigi la mankojn de multaj materialoj pri XDP. Unue, ili provizas pretan kodon, kiu tuj preterpasas la funkciojn de XDP: ĝi estas preta por kontroli aŭ estas tro simpla por kaŭzi problemojn. Kiam vi tiam provas skribi vian kodon de nulo, vi ne havas ideon, kion fari kun tipaj eraroj. Due, manieroj loke testi XDP sen VM kaj aparataro ne estas kovritaj, malgraŭ la fakto, ke ili havas siajn proprajn malfacilaĵojn. La teksto estas destinita por programistoj konataj kun retoj kaj Linukso, kiuj interesiĝas pri XDP kaj eBPF.

En ĉi tiu parto, ni komprenos detale kiel la XDP-filtrilo estas kunvenita kaj kiel testi ĝin, tiam ni skribos simplan version de la konata mekanismo de kuketoj SYN ĉe la paka pretiga nivelo. Nuntempe, ni ne kreos "blankan liston"
kontrolitaj klientoj, konservu nombrilojn kaj administru la filtrilon - sufiĉe da registroj.

Ni skribos per C - ĝi ne estas moda, sed ĝi estas praktika. Ĉiu kodo haveblas en GitHub per la ligilo ĉe la fino kaj estas dividita en kommitaĵojn laŭ la etapoj priskribitaj en la artikolo.

Malgarantio. En la kurso de ĉi tiu artikolo, mini-solvo estos evoluigita por forpuŝi DDoS-atakojn, ĉar ĉi tio estas realisma tasko por XDP kaj mia kampo. Tamen, la ĉefa celo estas kompreni la teknologion; ĉi tio ne estas gvidilo por krei pretan protekton. La lernilokodo ne estas optimumigita kaj preterlasas kelkajn nuancojn.

XDP Mallonga Superrigardo

Mi skizos nur la ĉefpunktojn por ne duobligi dokumentadon kaj ekzistantajn artikolojn.

Do, la filtrila kodo estas ŝarĝita en la kernon. Envenantaj pakoj estas pasitaj al la filtrilo. Kiel rezulto, la filtrilo devas fari decidon: pasi la pakaĵon en la kernon (XDP_PASS), faligi pakon (XDP_DROP) aŭ resendu ĝin (XDP_TX). La filtrilo povas ŝanĝi la pakaĵon, tio estas precipe vera por XDP_TX. Vi ankaŭ povas ĉesigi la programon (XDP_ABORTED) kaj restarigi la pakaĵon, sed ĉi tio estas analoga assert(0) - por senararigado.

La virtuala maŝino eBPF (plilongigita Berkley Packet Filter) estas intence simpligita, por ke la kerno povas kontroli, ke la kodo ne iras en buklojn kaj ne difektas la memoron de aliaj homoj. Akumulaj restriktoj kaj kontroloj:

  • Bukloj (malantaŭen) estas malpermesitaj.
  • Estas stako por datumoj, sed neniuj funkcioj (ĉiuj C-funkcioj devas esti enliniitaj).
  • Memoraliroj ekster la stako kaj paka bufro estas malpermesitaj.
  • La kodgrandeco estas limigita, sed praktike tio ne estas tre signifa.
  • Nur alvokoj al specialaj kernaj funkcioj (eBPF-helpiloj) estas permesitaj.

Projekti kaj instali filtrilon aspektas jene:

  1. Fontkodo (ekz kernel.c) estas kompilita en objekton (kernel.o) sub la eBPF virtuala maŝinarkitekturo. Ekde oktobro 2019, kompilo al eBPF estas subtenata de Clang kaj promesita en GCC 10.1.
  2. Se ĉi tiu objektokodo enhavas vokojn al kernaj strukturoj (ekzemple, tabeloj kaj nombriloj), iliaj ID-oj estas anstataŭigitaj per nuloj, kio signifas ke tia kodo ne povas esti efektivigita. Antaŭ ol ŝargi en la kernon, vi devas anstataŭigi ĉi tiujn nulojn per la identigiloj de specifaj objektoj kreitaj per kernaj vokoj (ligi la kodon). Vi povas fari tion per eksteraj utilecoj, aŭ vi povas skribi programon, kiu ligos kaj ŝargos specifan filtrilon.
  3. La kerno kontrolas la ŝarĝitan programon. La foresto de cikloj kaj ne-trapaso de pakaĵeto kaj staklimoj estas kontrolitaj. Se la kontrolisto ne povas pruvi, ke la kodo estas ĝusta, la programo estas malakceptita - vi devas povi plaĉi al li.
  4. Post sukcesa konfirmo, la kerno kompilas la eBPF-arkitekturan objektokodon en sisteman arkitekturan maŝinkodon (ĝustatempe).
  5. La programo aliĝas al la interfaco kaj komencas prilabori pakaĵojn.

Ĉar XDP funkcias en la kerno, senararigado estas farita uzante spurprotokolojn kaj, fakte, pakaĵetojn, kiujn la programo filtras aŭ generas. Tamen, eBPF certigas, ke la ŝarĝita kodo estas sekura por la sistemo, do vi povas eksperimenti kun XDP rekte en via loka Linukso.

Preparante la Medion

Asembleo

Clang ne povas rekte produkti objektokodon por la eBPF-arkitekturo, do la procezo konsistas el du ŝtupoj:

  1. Kompilu C-kodon al LLVM-bajtokodo (clang -emit-llvm).
  2. Konverti bajtokodon al eBPF-objektkodo (llc -march=bpf -filetype=obj).

Kiam vi verkas filtrilon, kelkaj dosieroj kun helpaj funkcioj kaj makrooj estos utilaj de kernaj testoj. Gravas, ke ili kongruas kun la kernversio (KVER). Elŝutu ilin al 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

Makedosiero por Arch Linukso (kerno 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 enhavas la vojon al la kernaj kaplinioj, ARCH — sistema arkitekturo. Padoj kaj iloj povas iomete varii inter distribuoj.

Ekzemplo de diferencoj por Debian 10 (kerno 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 konekti dosierujon kun helpaj kaplinioj kaj plurajn dosierujojn kun kernaj kaplinioj. Simbolo __KERNEL__ signifas ke UAPI (uzantspaco API) titoloj estas difinitaj por la kernokodo, ĉar la filtrilo estas efektivigita en la kerno.

Stakprotekto povas esti malŝaltita (-fno-stack-protector), ĉar la eBPF-kodkontrolilo ankoraŭ kontrolas por stakaj ekster-limaj malobservoj. Indas tuj ŝalti optimumojn, ĉar la grandeco de la eBPF-bajtokodo estas limigita.

Ni komencu per filtrilo, kiu pasas ĉiujn pakaĵojn kaj faras nenion:

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

teamo make kolektas xdp_filter.o. Kie provi ĝin nun?

Testbenko

La stando devas inkluzivi du interfacojn: sur kiuj estos filtrilo kaj el kiuj pakoj estos senditaj. Ĉi tiuj devas esti plenrajtaj Linuksaj aparatoj kun sia propra IP por kontroli kiel regulaj aplikaĵoj funkcias per nia filtrilo.

Aparatoj de la tipo veth (virtuala Ethernet) taŭgas por ni: ĉi tiuj estas paro da virtualaj retaj interfacoj "ligitaj" rekte unu al la alia. Vi povas krei ilin tiel (en ĉi tiu sekcio ĉiuj komandoj ip estas efektivigitaj de root):

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

estas xdp-remote и xdp-local — aparatnomoj. On xdp-local (192.0.2.1/24) filtrilo estos alfiksita, kun xdp-remote (192.0.2.2/24) envenanta trafiko estos sendita. Tamen estas problemo: la interfacoj estas sur la sama maŝino, kaj Linukso ne sendos trafikon al unu el ili per la alia. Vi povas solvi ĉi tion per malfacilaj reguloj iptables, sed ili devos ŝanĝi pakaĵojn, kio estas maloportuna por sencimigi. Pli bone estas uzi retajn nomspacojn (ĉi-poste netns).

Reta nomspaco enhavas aron de interfacoj, vojtabeloj kaj NetFilter-reguloj, izolitaj de similaj objektoj en aliaj retoj. Ĉiu procezo funkcias en nomspaco kaj nur havas aliron al la objektoj de tiu netns. Defaŭlte, la sistemo havas ununuran retnomspacon por ĉiuj objektoj, do vi povas labori en Linukso kaj ne scii pri netns.

Ni kreu novan nomspacon xdp-test kaj movi ĝin tien xdp-remote.

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

Tiam la procezo funkcias xdp-test, ne "vidos" xdp-local (ĝi restos en netns defaŭlte) kaj sendante pakaĵeton al 192.0.2.1 ĝi elsendos ĝin tra xdp-remote, ĉar ĉi tiu estas la sola interfaco sur 192.0.2.0/24 alirebla por ĉi tiu procezo. Ĉi tio ankaŭ funkcias en la kontraŭa direkto.

Moviĝante inter netns, la interfaco iras malsupren kaj perdas sian adreson. Por agordi la interfacon en netns, vi devas kuri ip ... en ĉi tiu komanda nomspaco 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

Kiel vi povas vidi, ĉi tio ne diferencas de la agordo xdp-local en la defaŭlta nomspaco:

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

Se vi kuras tcpdump -tnevi xdp-local, vi povas vidi ke pakoj senditaj de xdp-test, estas liveritaj al ĉi tiu interfaco:

ip netns exec xdp-test   ping 192.0.2.1

Estas oportune lanĉi ŝelon enen xdp-test. La deponejo havas skripton, kiu aŭtomatigas laboron kun la stando; ekzemple, vi povas agordi la standon per la komando sudo ./stand up kaj forigu ĝin sudo ./stand down.

Spurado

La filtrilo estas asociita kun aparato tia:

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

Ключ -force bezonata por ligi novan programon se alia jam estas ligita. "Neniu novaĵo estas bona novaĵo" ne temas pri ĉi tiu komando, la eligo estas ĉiukaze volumena. indiki verbose nedeviga, sed kun ĝi aperas raporto pri la laboro de la kodkontrolilo kun la asemblea listo:

Verifier analysis:

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

Malligi la programon de la interfaco:

ip link set dev xdp-local xdp off

En skripto ĉi tiuj estas komandoj sudo ./stand attach и sudo ./stand detach.

Alkroĉante filtrilon, vi povas certigi tion ping daŭre funkcias, sed ĉu la programo funkcias? Ni aldonu protokolojn. Funkcio bpf_trace_printk() simila al printf(), sed nur subtenas ĝis tri argumentojn krom la ŝablono, kaj limigitan liston de specifiloj. Makroo bpf_printk() simpligas la vokon.

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

La eligo iras al la kerna spurkanalo, kiu devas esti ebligita:

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

Rigardu mesaĝofadenon:

cat /sys/kernel/debug/tracing/trace_pipe

Ambaŭ ĉi tiuj komandoj faras vokon sudo ./stand log.

Ping nun devus ekigi mesaĝojn kiel ĉi tio:

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

Se vi rigardas atente la eligon de la kontrolilo, vi rimarkos strangajn kalkulojn:

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

La fakto estas, ke eBPF-programoj ne havas datumsekcion, do la nura maniero kodi formatan ĉenon estas la tujaj argumentoj de VM-komandoj:

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

Tial, sencimiga eligo multe ŝveligas la finan kodon.

Sendante XDP-Pakojn

Ni ŝanĝu la filtrilon: lasu ĝin resendi ĉiujn envenantajn pakaĵojn. Tio estas malĝusta el reta vidpunkto, ĉar necesus ŝanĝi la adresojn en la kaplinioj, sed nun la laboro principe gravas.

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

Lanĉo tcpdump sur xdp-remote. Ĝi devus montri identan elirantan kaj envenantan ICMP-Eĥan Peton kaj ĉesu montri ICMP-Eĥan Respondon. Sed ĝi ne montras. Rezultas ke por laboro XDP_TX en la programo sur xdp-local estas necesaal la parinterfaco xdp-remote programo ankaŭ estis asignita, eĉ se ĝi estis malplena, kaj li estis levita.

Kiel mi sciis ĉi tion?

Spuru la vojon de pako en la kerno La mekanismo de perf-okazaĵoj permesas, cetere, uzi la saman virtualan maŝinon, tio estas, eBPF estas uzata por trakti eBPF.

Vi devas bonfari el malbono, ĉar estas nenio alia por fari ĝin el malbono.

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

Kio estas kodo 6?

$ errno 6
ENXIO 6 No such device or address

funkcio veth_xdp_flush_bq() ricevas erarkodon de veth_xdp_xmit(), kie serĉu per ENXIO kaj trovu la komenton.

Ni restarigu la minimuman filtrilon (XDP_PASS) en dosiero xdp_dummy.c, aldonu ĝin al la Makefile, ligu ĝin al xdp-remote:

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

Nun tcpdump montras kio estas atendata:

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

Se nur ARP-oj estas montritaj anstataŭe, vi devas forigi la filtrilojn (tio faras sudo ./stand detach), lasu ping, tiam agordu filtrilojn kaj provu denove. La problemo estas ke la filtrilo XDP_TX valida por kaj ARP kaj stako
nomspacoj xdp-test sukcesis "forgesi" la MAC-adreson 192.0.2.1, ĝi ne povos solvi ĉi tiun IP.

Formulado de la problemo

Ni transiru al la deklarita tasko: skribi SYN-kukecan mekanismon sur XDP.

SYN-inundo restas populara DDoS-atako, kies esenco estas jena. Kiam konekto estas establita (TCP-manpremo), la servilo ricevas SYN, asignas rimedojn por la estonta konekto, respondas per SYNACK-pako kaj atendas ACK. La atakanto simple sendas SYN-pakojn de falsaj adresoj en miloj por sekundo de ĉiu gastiganto en multmil-forta botneto. La servilo estas devigita asigni resursojn tuj post la alveno de la pakaĵeto, sed liberigas ilin post granda tempodaŭro; kiel rezulto, memoro aŭ limoj estas elĉerpitaj, novaj ligoj ne estas akceptitaj, kaj la servo estas neatingebla.

Se vi ne asignas rimedojn surbaze de la SYN-pako, sed nur respondas per SYNACK-pako, kiel do la servilo povas kompreni, ke la ACK-pako, kiu alvenis poste, rilatas al SYN-pako, kiu ne estis konservita? Post ĉio, atakanto ankaŭ povas generi falsajn ACK-ojn. La celo de la SYN-kuketo estas kodi ĝin seqnum konektoparametroj kiel hash de adresoj, havenoj kaj ŝanĝanta salo. Se la ACK sukcesis alveni antaŭ ol la salo estis ŝanĝita, vi povas kalkuli la hash denove kaj kompari ĝin acknum. Forĝi acknum la atakanto ne povas, ĉar la salo inkluzivas sekreton, kaj ne povos ordigi ĝin pro limigita kanalo.

La SYN-kuketo estas delonge efektivigita en la Linukso-kerno kaj eĉ povas esti aŭtomate ebligita se SYN-oj alvenas tro rapide kaj amase.

Eduka programo pri TCP-manpremo

TCP disponigas datumtranssendon kiel fluo de bajtoj, ekzemple, HTTP-petoj estas elsenditaj super TCP. La rivereto estas elsendita en pecoj en pakoj. Ĉiuj TCP-pakaĵetoj havas logikan flagojn kaj 32-bitajn sekvencnombrojn:

  • La kombinaĵo de flagoj determinas la rolon de aparta pako. La flago SYN signifas, ke ĉi tiu estas la unua pako de la sendinto en la konekto. La ACK-flago signifas, ke la sendinto ricevis ĉiujn konektajn datumojn ĝis la bajto acknum. Pakaĵeto povas havi plurajn flagojn kaj estas nomita per ilia kombinaĵo, ekzemple, SYNACK-pakaĵeto.

  • Sekvencnumero (sekvnum) precizigas la ofseton en la datumfluo por la unua bajto kiu estas elsendita en ĉi tiu pakaĵeto. Ekzemple, se en la unua pako kun X bajtoj da datumoj ĉi tiu nombro estis N, en la sekva pako kun novaj datumoj ĝi estos N+X. Komence de la konekto, ĉiu partio elektas ĉi tiun numeron hazarde.

  • Agnoskonumero (acknum) estas la sama ofseto kiel seqnum, sed ĝi ne determinas la nombron de la transdona bajto, sed la nombron de la unua bajto de la ricevanto, kiun la sendinto ne vidis.

Komence de la konekto, la partioj devas konsenti seqnum и acknum. La kliento sendas SYN-pakaĵon kun ĝia seqnum = X. La servilo respondas per SYNACK-pako, kie ĝi registras sian seqnum = Y kaj ekspoziciaĵoj acknum = X + 1. La kliento respondas al SYNACK per ACK-pako, kie seqnum = X + 1, acknum = Y + 1. Post ĉi tio, la efektiva transdono de datumoj komenciĝas.

Se la kunulo ne agnoskas ricevon de la pako, TCP resendas ĝin post tempodaŭro.

Kial SYN-kuketoj ne ĉiam estas uzataj?

Unue, se la SYNACK aŭ ACK estas perdita, vi devos atendi resendon - malrapidigante la konekton. Due, en la SYN-pakaĵo - kaj nur en ĝi! — kelkaj opcioj estas transdonitaj, kiuj influas la pluan funkciadon de la konekto. Ne memorante envenantajn SYN-pakaĵojn, la servilo tiel ignoras ĉi tiujn opciojn; la kliento ne plu sendos ilin en la venontaj pakaĵetoj. TCP povas funkcii en ĉi tiu kazo, sed almenaŭ en la komenca etapo la kvalito de la konekto malpliiĝos.

De paka vidpunkto, XDP-programo devas fari la jenon:

  • respondi al SYN per SYNACK per kuketo;
  • respondi al ACK per RST (malkonekti);
  • forĵeti la ceterajn paketojn.

Pseŭdokodo de la algoritmo kune kun paka analizo:

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

Unu (*) La punktoj, kie vi bezonas administri la staton de la sistemo, estas markitaj - en la unua etapo vi povas malhavi ilin per simple efektivigo de TCP-manpremo kun la generacio de SYN-kuketo kiel sekvo.

Surloke (**), dum ni ne havas tablon, ni preterlasos la paketon.

Efektivigo de TCP-manpremo

Analizante la pakaĵon kaj kontrolante la kodon

Ni bezonos retajn titolajn strukturojn: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) kaj TCP (uapi/linux/tcp.h). Mi neniam povis konekti ĉi-lastan pro eraroj rilataj al atomic64_t, mi devis kopii la necesajn difinojn en la kodon.

Ĉiuj funkcioj kiuj estas asignitaj por legebleco en C devas esti enliniitaj ĉe la vokopunkto, ĉar la eBPF-kontrolilo en la kerno malpermesas malantaŭen saltojn, tio estas, fakte, buklojn kaj funkciovokojn.

#define INTERNAL static __attribute__((always_inline))

Makro LOG() malŝaltas presadon en la eldonkonstruaĵo.

La programo estas transportilo de funkcioj. Ĉiu ricevas pakaĵeton en kiu kaplinio de la taŭga nivelo estas elstarigita, ekzemple, process_ether() atendas ke ĝi estos plenigita ether. Surbaze de la rezultoj de kampa analizo, la funkcio povas pasi la pakaĵon al pli alta nivelo. La rezulto de la funkcio estas XDP-ago. Nuntempe, la SYN kaj ACK-traktiloj pasas ĉiujn pakaĵojn.

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

Mi atentigas vin pri la ĉekoj markitaj A kaj B. Se vi komentas A, la programo konstruos, sed estos kontrola eraro dum ŝarĝo:

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!

Ŝlosilŝnuro invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): Estas ekzekutvojoj kiam la dektria bajto de la komenco de la bufro estas ekster la pako. Estas malfacile kompreni el la listo, pri kiu linio ni parolas, sed ekzistas instrukcionumero (12) kaj malmuntilo montranta la liniojn de fontkodo:

llvm-objdump -S xdp_filter.o | less

En ĉi tiu kazo ĝi montras al la linio

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

kiu evidentigas, ke la problemo estas ether. Ĉiam estus tiel.

Respondu al SYN

La celo en ĉi tiu etapo estas generi ĝustan SYNACK-pakaĵon kun fiksita seqnum, kiu estos anstataŭigita estonte per SYN-kuketo. Ĉiuj ŝanĝoj okazas en process_tcp_syn() kaj ĉirkaŭaj regionoj.

Paka konfirmo

Sufiĉe strange, jen la plej rimarkinda linio, aŭ pli ĝuste, la komento al ĝi:

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

Skribante la unuan version de la kodo, oni uzis la kernon 5.1, por kies kontrolilo estis diferenco inter data_end и (const void*)ctx->data_end. En la momento de verkado de la artikolo, kerno 5.3.1 ne havis ĉi tiun problemon. Eble la kompililo aliris lokan variablon malsame ol kampo. Moralo de la rakonto: Kun grandaj nestaj situacioj, simpligi la kodon povas helpi.

Poste estas rutinaj longokontroloj por la gloro de la kontrolilo; O MAX_CSUM_BYTES sube.

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

Malfaldante la pakaĵon

Ni plenigas seqnum и acknum, agordu ACK (SYN jam estas agordita):

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

Interŝanĝu TCP-havenojn, IP-adreson kaj MAC-adresojn. La norma biblioteko ne estas alirebla de la programo XDP, do memcpy() — makroo kiu kaŝas la Clang-intrinkecojn.

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

Rekalkulo de ĉeksumoj

IPv4 kaj TCP-ĉeksumoj postulas la aldonon de ĉiuj 16-bitaj vortoj en la kaplinioj, kaj la grandeco de la kaplinioj estas skribita en ili, tio estas, nekonata en la kompila tempo. Ĉi tio estas problemo ĉar la kontrolilo ne buklos tra la normala buklo al la varia limo. Sed la grandeco de la kaplinioj estas limigita: ĝis 64 bajtoj ĉiu. Vi povas fari buklon kun fiksa nombro da ripetoj, kiuj povas finiĝi frue.

Mi rimarkas, ke ekzistas RFC 1624 pri kiel parte rekalkuli la kontrolsumon se nur la fiksitaj vortoj de la pakaĵoj estas ŝanĝitaj. Tamen, la metodo ne estas universala, kaj la efektivigo estus pli malfacile konservi.

Kontrolsumo-kalkulfunkcio:

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

Kvankam size kontrolita per la voka kodo, la dua elirkondiĉo estas necesa por ke la kontrolilo povas pruvi la kompletigon de la buklo.

Por 32-bitaj vortoj, pli simpla versio estas efektivigita:

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

Fakte rekalkulante la ĉeksumojn kaj resendante la pakaĵon:

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;

funkcio carry() faras ĉeksumon el 32-bita sumo de 16-bitaj vortoj, laŭ RFC 791.

TCP-manpremkontrolo

La filtrilo ĝuste establas rilaton kun netcat, preterlasante la finan ACK, al kiu Linukso respondis per RST-pako, ĉar la retstako ne ricevis la SYN - ĝi estis konvertita al SYNACK kaj resendita - kaj el la OS-punkto alvenis pakaĵeto, kiu ne estis rilata al malfermitaj ligoj.

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

Gravas kontroli kun plenrajtaj aplikoj kaj monitoro tcpdump sur xdp-remote ĉar, ekzemple, hping3 ne respondas al malĝustaj ĉeksumoj.

El vidpunkto de XDP, la konfirmo mem estas bagatela. La kalkulalgoritmo estas primitiva kaj verŝajne vundebla al sofistika atakanto. La Linukso-kerno, ekzemple, uzas la kriptografian SipHash, sed ĝia efektivigo por XDP estas klare ekster la amplekso de ĉi tiu artikolo.

Aperis por novaj TODO-oj rilataj al ekstera interagado:

  • La programo XDP ne povas konservi cookie_seed (la sekreta parto de la salo) en tutmonda variablo, vi bezonas stokadon en la kerno, la valoro en kiu estos periode ĝisdatigita de fidinda generatoro.

  • Se la SYN-kuketo kongruas en la ACK-pakaĵo, vi ne bezonas presi mesaĝon, sed memoru la IP de la kontrolita kliento por daŭrigi pasi pakojn de ĝi.

Laŭleĝa kliento-konfirmo:

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

La protokoloj registras la kompletigon de la kontrolo (flags=0x2 - ĉi tio estas SYN, flags=0x10 estas 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

Kvankam ne ekzistas listo de kontrolitaj IP-oj, ne estos protekto kontraŭ la SYN-inundo mem, sed jen la reago al ACK-inundo lanĉita per la sekva komando:

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

Protokoloj:

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

konkludo

Foje eBPF ĝenerale kaj XDP precipe estas prezentitaj pli kiel altnivela administra ilo ol kiel evoluplatformo. Efektive, XDP estas ilo por enmiksiĝi kun la prilaborado de pakaĵetoj de la kerno, kaj ne alternativo al la kernstako, kiel DPDK kaj aliaj kernaj pretervojaj opcioj. Aliflanke, XDP permesas efektivigi sufiĉe kompleksan logikon, kiu, krome, estas facile ĝisdatigebla sen interrompo en trafika prilaborado. La kontrolilo ne kreas grandajn problemojn; persone, mi ne rifuzus ĉi tion por partoj de uzantspaca kodo.

En la dua parto, se la temo estas interesa, ni kompletigos la tabelon de kontrolitaj klientoj kaj malkonektiĝoj, efektivigos nombrilojn kaj skribos uzantspacan utilecon por administri la filtrilon.

Referencoj

fonto: www.habr.com

Aldoni komenton