Nagsusulat kami ng proteksyon laban sa mga pag-atake ng DDoS sa XDP. Nuklear na bahagi

Ang teknolohiya ng eXpress Data Path (XDP) ay nagpapahintulot sa random na pagproseso ng trapiko na maisagawa sa mga interface ng Linux bago pumasok ang mga packet sa kernel network stack. Application ng XDP - proteksyon laban sa mga pag-atake ng DDoS (CloudFlare), kumplikadong mga filter, koleksyon ng mga istatistika (Netflix). Ang mga XDP program ay isinasagawa ng eBPF virtual machine, kaya may mga paghihigpit sila sa kanilang code at sa mga available na kernel function depende sa uri ng filter.

Ang artikulo ay inilaan upang punan ang mga pagkukulang ng maraming materyales sa XDP. Una, nagbibigay sila ng yari na code na agad na nilalampasan ang mga tampok ng XDP: handa ito para sa pag-verify o masyadong simple para magdulot ng mga problema. Kapag sinubukan mong isulat ang iyong code mula sa simula, wala kang ideya kung ano ang gagawin sa mga karaniwang error. Pangalawa, ang mga paraan upang lokal na subukan ang XDP nang walang VM at hardware ay hindi sakop, sa kabila ng katotohanan na mayroon silang sariling mga pitfalls. Ang teksto ay inilaan para sa mga programmer na pamilyar sa networking at Linux na interesado sa XDP at eBPF.

Sa bahaging ito, mauunawaan namin nang detalyado kung paano binuo ang filter ng XDP at kung paano ito subukan, pagkatapos ay magsusulat kami ng isang simpleng bersyon ng kilalang mekanismo ng cookies ng SYN sa antas ng pagproseso ng packet. Hindi pa kami gagawa ng "white list".
na-verify na mga kliyente, panatilihin ang mga counter at pamahalaan ang filter - sapat na mga log.

Magsusulat kami sa C - hindi ito sunod sa moda, ngunit praktikal ito. Available ang lahat ng code sa GitHub sa pamamagitan ng link sa dulo at nahahati sa mga commit ayon sa mga yugto na inilarawan sa artikulo.

Disclaimer. Sa paglipas ng artikulong ito, bubuo ako ng isang mini-solusyon upang maiwasan ang mga pag-atake ng DDoS, dahil ito ay isang makatotohanang gawain para sa XDP at sa aking lugar ng kadalubhasaan. Gayunpaman, ang pangunahing layunin ay upang maunawaan ang teknolohiya; Ang tutorial code ay hindi na-optimize at nag-aalis ng ilang mga nuances.

Maikling Pangkalahatang-ideya ng XDP

Babalangkasin ko lamang ang mga pangunahing punto upang hindi madoble ang dokumentasyon at mga umiiral nang artikulo.

Kaya, ang filter code ay na-load sa kernel. Ang mga papasok na packet ay ipinasa sa filter. Bilang resulta, ang filter ay dapat gumawa ng desisyon: ipasa ang packet sa kernel (XDP_PASS), drop packet (XDP_DROP) o ipadala ito pabalik (XDP_TX). Maaaring baguhin ng filter ang pakete, ito ay totoo lalo na para sa XDP_TX. Maaari mo ring i-abort ang programa (XDP_ABORTED) at i-reset ang package, ngunit ito ay kahalintulad assert(0) - para sa pag-debug.

Ang eBPF (extended Berkley Packet Filter) virtual machine ay sadyang ginawang simple upang masuri ng kernel na ang code ay hindi umiikot at hindi makapinsala sa memorya ng ibang tao. Pinagsama-samang mga paghihigpit at pagsusuri:

  • Ang mga loop (paatras) ay ipinagbabawal.
  • Mayroong isang stack para sa data, ngunit walang mga function (lahat ng C function ay dapat na inline).
  • Ang mga access sa memorya sa labas ng stack at packet buffer ay ipinagbabawal.
  • Ang laki ng code ay limitado, ngunit sa pagsasanay na ito ay hindi masyadong makabuluhan.
  • Ang mga tawag lamang sa mga espesyal na kernel function (mga katulong sa eBPF) ang pinapayagan.

Ang pagdidisenyo at pag-install ng isang filter ay ganito ang hitsura:

  1. Source code (hal kernel.c) ay pinagsama-sama sa object (kernel.o) para sa eBPF virtual machine architecture. Simula Oktubre 2019, ang compilation sa eBPF ay sinusuportahan ng Clang at ipinangako sa GCC 10.1.
  2. Kung ang object code na ito ay naglalaman ng mga tawag sa mga istruktura ng kernel (halimbawa, mga talahanayan at mga counter), ang kanilang mga ID ay papalitan ng mga zero, na nangangahulugan na ang naturang code ay hindi maaaring isagawa. Bago mag-load sa kernel, kailangan mong palitan ang mga zero na ito ng mga ID ng mga partikular na bagay na nilikha sa pamamagitan ng mga kernel call (i-link ang code). Magagawa mo ito sa mga panlabas na kagamitan, o maaari kang magsulat ng isang program na magli-link at maglo-load ng isang partikular na filter.
  3. Bine-verify ng kernel ang na-load na programa. Sinusuri ang kawalan ng mga cycle at pagkabigo na lumampas sa mga hangganan ng packet at stack. Kung hindi mapapatunayan ng verifier na tama ang code, tatanggihan ang program - kailangan mong mapasaya siya.
  4. Pagkatapos ng matagumpay na pag-verify, kino-compile ng kernel ang eBPF architecture object code sa machine code para sa system architecture (just-in-time).
  5. Ang programa ay nakakabit sa interface at nagsisimula sa pagproseso ng mga packet.

Dahil ang XDP ay tumatakbo sa kernel, ang pag-debug ay isinasagawa gamit ang mga trace log at, sa katunayan, mga packet na sinasala o binubuo ng programa. Gayunpaman, tinitiyak ng eBPF na secure ang na-download na code para sa system, kaya maaari kang direktang mag-eksperimento sa XDP sa iyong lokal na Linux.

Paghahanda sa Kapaligiran

Assembly

Hindi direktang makagawa ng object code ang Clang para sa arkitektura ng eBPF, kaya ang proseso ay binubuo ng dalawang hakbang:

  1. I-compile ang C code sa LLVM bytecode (clang -emit-llvm).
  2. I-convert ang bytecode sa eBPF object code (llc -march=bpf -filetype=obj).

Kapag nagsusulat ng filter, magiging kapaki-pakinabang ang ilang file na may mga pantulong na function at macro mula sa mga pagsubok sa kernel. Mahalagang tumugma ang mga ito sa bersyon ng kernel (KVER). I-download ang mga ito sa 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 para sa 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 naglalaman ng landas sa mga header ng kernel, ARCH - arkitektura ng system. Maaaring bahagyang mag-iba ang mga landas at tool sa pagitan ng mga distribusyon.

Halimbawa ng mga pagkakaiba para sa 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 ikonekta ang isang direktoryo na may mga auxiliary na header at ilang mga direktoryo na may mga kernel header. Simbolo __KERNEL__ nangangahulugan na ang mga UAPI (userspace API) na mga header ay tinukoy para sa kernel code, dahil ang filter ay isinasagawa sa kernel.

Maaaring hindi paganahin ang proteksyon ng stack (-fno-stack-protector), dahil ang eBPF code verifier ay tumitingin pa rin para sa stack out-of-bounds na mga paglabag. Sulit na i-on kaagad ang mga pag-optimize, dahil limitado ang laki ng eBPF bytecode.

Magsimula tayo sa isang filter na pumasa sa lahat ng packet at walang ginagawa:

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

Koponan make nangongolekta xdp_filter.o. Saan subukan ito ngayon?

Test stand

Dapat may kasamang dalawang interface ang stand: kung saan magkakaroon ng filter at kung saan ipapadala ang mga packet. Ang mga ito ay dapat na ganap na mga Linux device na may sariling mga IP upang masuri kung paano gumagana ang mga regular na application sa aming filter.

Ang mga device na may uri ng veth (virtual Ethernet) ay angkop para sa amin: ang mga ito ay isang pares ng mga virtual na interface ng network na "nakakonekta" nang direkta sa isa't isa. Maaari mong gawin ang mga ito tulad nito (sa seksyong ito ang lahat ng mga utos ip ay isinasagawa mula sa root):

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

Dito xdp-remote и xdp-local — mga pangalan ng device. Naka-on xdp-local (192.0.2.1/24) may ikakabit na filter, na may xdp-remote (192.0.2.2/24) ang papasok na trapiko ay ipapadala. Gayunpaman, may problema: ang mga interface ay nasa parehong makina, at ang Linux ay hindi magpapadala ng trapiko sa isa sa kanila sa pamamagitan ng isa pa. Maaari mong lutasin ito gamit ang nakakalito na mga panuntunan iptables, ngunit kailangan nilang baguhin ang mga pakete, na hindi maginhawa para sa pag-debug. Mas mainam na gumamit ng mga namespace ng network (pagkatapos dito ay netns).

Ang isang network namespace ay naglalaman ng isang hanay ng mga interface, mga routing table, at mga panuntunan sa NetFilter na nakahiwalay sa mga katulad na bagay sa ibang netns. Ang bawat proseso ay tumatakbo sa isang namespace at mayroon lamang access sa mga bagay ng netns na iyon. Bilang default, ang system ay may isang network namespace para sa lahat ng mga bagay, kaya maaari kang magtrabaho sa Linux at hindi alam ang tungkol sa netns.

Gumawa tayo ng bagong namespace xdp-test at ilipat ito doon xdp-remote.

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

Pagkatapos ay tumatakbo ang proseso xdp-test, hindi "makikita" xdp-local (ito ay mananatili sa netns bilang default) at kapag nagpapadala ng isang packet sa 192.0.2.1 ito ay ipapasa ito xdp-remotedahil ito ang tanging interface sa 192.0.2.0/24 na naa-access sa prosesong ito. Gumagana rin ito sa kabaligtaran na direksyon.

Kapag lumilipat sa pagitan ng netns, bumababa ang interface at nawawala ang address nito. Upang i-configure ang interface sa netns, kailangan mong tumakbo ip ... sa namespace ng command na ito 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

Tulad ng nakikita mo, ito ay hindi naiiba sa setting xdp-local sa default na namespace:

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

Kung tatakbo ka tcpdump -tnevi xdp-local, makikita mo na ang mga packet na ipinadala mula sa xdp-test, ay inihahatid sa interface na ito:

ip netns exec xdp-test   ping 192.0.2.1

Ito ay maginhawa upang ilunsad ang isang shell sa xdp-test. Ang repository ay may script na nag-o-automate ng trabaho sa stand, halimbawa, maaari mong i-configure ang stand gamit ang command sudo ./stand up at tanggalin ito sudo ./stand down.

Pagsubaybay

Ang filter ay nauugnay sa device tulad nito:

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

Key -force kailangan upang mag-link ng isang bagong programa kung ang isa ay naka-link na. "Walang balita ang mabuting balita" ay hindi tungkol sa utos na ito, ang konklusyon ay napakalaki sa anumang kaso. ipahiwatig verbose opsyonal, ngunit kasama nito ang isang ulat ay lilitaw sa gawain ng code verifier na may listahan ng pagpupulong:

Verifier analysis:

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

I-unlink ang program mula sa interface:

ip link set dev xdp-local xdp off

Sa script ito ay mga utos sudo ./stand attach и sudo ./stand detach.

Sa pamamagitan ng pag-attach ng filter, matitiyak mo iyon ping patuloy na tumatakbo, ngunit gumagana ba ang programa? Magdagdag tayo ng mga log. Function bpf_trace_printk() kapareho ng printf(), ngunit sinusuportahan lamang ang hanggang tatlong argumento maliban sa pattern, at isang limitadong listahan ng mga specifier. Macro bpf_printk() pinapasimple ang tawag.

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

Ang output ay papunta sa kernel trace channel, na kailangang paganahin:

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

Tingnan ang thread ng mensahe:

cat /sys/kernel/debug/tracing/trace_pipe

Ang dalawang utos na ito ay tumatawag sudo ./stand log.

Dapat na ngayong mag-trigger ng mga mensahe ang ping tulad nito:

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

Kung titingnan mong mabuti ang output ng verifier, mapapansin mo ang mga kakaibang kalkulasyon:

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

Ang katotohanan ay ang mga programang eBPF ay walang seksyon ng data, kaya ang tanging paraan upang mag-encode ng string ng format ay ang mga agarang argumento ng mga utos ng VM:

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

Para sa kadahilanang ito, ang output ng debug ay lubos na nagpapalaki sa resultang code.

Nagpapadala ng mga XDP Packet

Baguhin natin ang filter: hayaan itong ibalik ang lahat ng mga papasok na packet. Ito ay hindi tama mula sa isang network point of view, dahil ito ay kinakailangan upang baguhin ang mga address sa mga header, ngunit ngayon ang trabaho sa prinsipyo ay mahalaga.

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

Ilunsad tcpdump sa xdp-remote. Dapat itong magpakita ng magkaparehong papalabas at papasok na ICMP Echo Request at ihinto ang pagpapakita ng ICMP Echo Reply. Ngunit hindi ito nagpapakita. Ito pala ay para sa trabaho XDP_TX sa programa sa xdp-local dapatsa interface ng pares xdp-remote isang programa din ang itinalaga, kahit walang laman, at siya ay pinalaki.

Paano ko nalaman ito?

Sundan ang landas ng isang pakete sa kernel Ang mekanismo ng mga kaganapan sa perf ay nagbibigay-daan, sa pamamagitan ng paraan, gamit ang parehong virtual machine, iyon ay, ang eBPF ay ginagamit para sa mga disassembly na may eBPF.

Dapat kang gumawa ng mabuti sa kasamaan, dahil wala nang magagawa pa.

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

Ano ang code 6?

$ errno 6
ENXIO 6 No such device or address

Tungkulin veth_xdp_flush_bq() tumatanggap ng error code mula sa veth_xdp_xmit(), kung saan maghanap sa pamamagitan ng ENXIO at hanapin ang komento.

Ibalik natin ang pinakamababang filter (XDP_PASS) sa file xdp_dummy.c, idagdag ito sa Makefile, itali ito sa xdp-remote:

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

ngayon tcpdump nagpapakita kung ano ang inaasahan:

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

Kung ARP lang ang ipinapakita, kailangan mong alisin ang mga filter (ginagawa nito sudo ./stand detach), pakawalan ping, pagkatapos ay magtakda ng mga filter at subukang muli. Ang problema ay ang filter XDP_TX wasto pareho sa ARP at kung ang stack
mga namespace xdp-test pinamamahalaang "nakalimutan" ang MAC address na 192.0.2.1, hindi nito malulutas ang IP na ito.

Pahayag ng problema

Lumipat tayo sa nakasaad na gawain: sumulat ng mekanismo ng SYN cookies sa XDP.

Ang SYN flood ay nananatiling isang sikat na pag-atake ng DDoS, ang esensya nito ay ang mga sumusunod. Kapag ang isang koneksyon ay naitatag (TCP handshake), ang server ay tumatanggap ng isang SYN, naglalaan ng mga mapagkukunan para sa hinaharap na koneksyon, tumugon sa isang SYNACK packet at naghihintay para sa isang ACK. Nagpapadala lang ang attacker ng libu-libong SYN packet bawat segundo mula sa mga spoofed address mula sa bawat host sa isang multi-thousand-strong botnet. Ang server ay napipilitang maglaan ng mga mapagkukunan kaagad sa pagdating ng packet, ngunit inilabas ang mga ito pagkatapos ng isang malaking timeout bilang isang resulta, ang memorya o mga limitasyon ay naubos, ang mga bagong koneksyon ay hindi tinatanggap, at ang serbisyo ay hindi magagamit.

Kung hindi ka naglalaan ng mga mapagkukunan batay sa SYN packet, ngunit tumugon lamang gamit ang isang SYNACK packet, paano mauunawaan ng server na ang ACK packet na dumating sa ibang pagkakataon ay tumutukoy sa isang SYN packet na hindi na-save? Pagkatapos ng lahat, ang isang umaatake ay maaari ding bumuo ng mga pekeng ACK. Ang punto ng SYN cookie ay i-encode ito seqnum mga parameter ng koneksyon bilang isang hash ng mga address, port at pagbabago ng asin. Kung nakarating ang ACK bago palitan ang asin, maaari mong kalkulahin muli ang hash at ihambing ito sa acknum. Forge acknum hindi magagawa ng umaatake, dahil kasama sa asin ang sikreto, at hindi magkakaroon ng oras upang ayusin ito dahil sa limitadong channel.

Ang SYN cookie ay matagal nang ipinatupad sa Linux kernel at maaari pa ngang awtomatikong paganahin kung ang mga SYN ay dumating nang masyadong mabilis at sa maraming bilang.

Programang pang-edukasyon sa TCP handshake

Nagbibigay ang TCP ng paghahatid ng data bilang isang stream ng mga byte, halimbawa, ang mga kahilingan sa HTTP ay ipinapadala sa TCP. Ang stream ay ipinadala nang paisa-isa sa mga packet. Ang lahat ng TCP packet ay may mga lohikal na flag at 32-bit na sequence number:

  • Tinutukoy ng kumbinasyon ng mga flag ang papel ng isang partikular na pakete. Ang bandila ng SYN ay nagpapahiwatig na ito ang unang packet ng nagpadala sa koneksyon. Ang bandila ng ACK ay nangangahulugan na natanggap ng nagpadala ang lahat ng data ng koneksyon hanggang sa byte acknum. Ang isang packet ay maaaring magkaroon ng ilang mga flag at tinatawag sa pamamagitan ng kanilang kumbinasyon, halimbawa, isang SYNACK packet.

  • Ang sequence number (seqnum) ay tumutukoy sa offset sa stream ng data para sa unang byte na ipinadala sa packet na ito. Halimbawa, kung sa unang packet na may X bytes ng data ang numerong ito ay N, sa susunod na packet na may bagong data ito ay magiging N+X. Sa simula ng koneksyon, random na pinipili ng bawat panig ang numerong ito.

  • Numero ng pagkilala (acknum) - ang parehong offset bilang seqnum, ngunit hindi nito tinutukoy ang bilang ng byte na ipinapadala, ngunit ang bilang ng unang byte mula sa tatanggap, na hindi nakita ng nagpadala.

Sa simula ng koneksyon, dapat magkasundo ang mga partido seqnum и acknum. Nagpapadala ang kliyente ng SYN packet kasama nito seqnum = X. Tumutugon ang server gamit ang isang SYNACK packet, kung saan itinatala nito ito seqnum = Y at naglalantad acknum = X + 1. Tumugon ang kliyente sa SYNACK gamit ang isang ACK packet, kung saan seqnum = X + 1, acknum = Y + 1. Pagkatapos nito, magsisimula ang aktwal na paglipat ng data.

Kung hindi kinikilala ng peer ang resibo ng packet, muling ipapadala ito ng TCP pagkatapos ng timeout.

Bakit hindi palaging ginagamit ang SYN cookies?

Una, kung nawala ang SYNACK o ACK, kailangan mong hintayin itong maipadala muli - bumagal ang pag-setup ng koneksyon. Pangalawa, sa pakete ng SYN - at dito lamang! — isang bilang ng mga opsyon ang ipinadala na nakakaapekto sa karagdagang operasyon ng koneksyon. Nang hindi naaalala ang mga papasok na SYN packet, hindi pinapansin ng server ang mga opsyong ito; Maaaring gumana ang TCP sa kasong ito, ngunit hindi bababa sa paunang yugto ay bababa ang kalidad ng koneksyon.

Sa mga tuntunin ng mga pakete, dapat gawin ng isang XDP program ang sumusunod:

  • tumugon sa SYN gamit ang SYNACK gamit ang isang cookie;
  • tumugon sa ACK gamit ang RST (disconnect);
  • itapon ang natitirang mga pakete.

Pseudocode ng algorithm kasama ang pag-parse ng package:

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

Isa (*) Ang mga punto kung saan kailangan mong pamahalaan ang estado ng system ay minarkahan - sa unang yugto magagawa mo nang wala ang mga ito sa pamamagitan lamang ng pagpapatupad ng TCP handshake na may pagbuo ng isang SYN cookie bilang isang seqnum.

On the spot (**), habang wala kaming table, laktawan namin ang packet.

Pagpapatupad ng TCP handshake

Pag-parse ng package at pag-verify ng code

Kakailanganin namin ang mga istruktura ng header ng network: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) at TCP (uapi/linux/tcp.h). Hindi ko maikonekta ang huli dahil sa mga error na nauugnay sa atomic64_t, Kinailangan kong kopyahin ang mga kinakailangang kahulugan sa code.

Ang lahat ng mga function na naka-highlight sa C para sa pagiging madaling mabasa ay dapat na inline sa punto ng tawag, dahil ang eBPF verifier sa kernel ay nagbabawal sa backtracking, iyon ay, sa katunayan, mga loop at function na tawag.

#define INTERNAL static __attribute__((always_inline))

Macro LOG() hindi pinapagana ang pag-print sa release build.

Ang programa ay isang conveyor ng mga function. Ang bawat isa ay tumatanggap ng isang packet kung saan naka-highlight ang kaukulang header ng antas, halimbawa, process_ether() inaasahan na mapupuno ito ether. Batay sa mga resulta ng field analysis, maaaring ipasa ng function ang packet sa mas mataas na antas. Ang resulta ng function ay ang XDP action. Sa ngayon, ipinapasa ng SYN at ACK handler ang lahat ng packet.

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

Iginuhit ko ang iyong pansin sa mga tseke na may markang A at B. Kung magkomento ka sa A, bubuo ang programa, ngunit magkakaroon ng error sa pag-verify kapag naglo-load:

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): May mga execution path kapag ang ikalabintatlong byte mula sa simula ng buffer ay nasa labas ng packet. Mahirap maunawaan mula sa listahan kung aling linya ang pinag-uusapan natin, ngunit mayroong isang numero ng pagtuturo (12) at isang disassembler na nagpapakita ng mga linya ng source code:

llvm-objdump -S xdp_filter.o | less

Sa kasong ito, tumuturo ito sa linya

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

na ginagawang malinaw na ang problema ay ether. Laging ganito.

Tumugon sa SYN

Ang layunin sa yugtong ito ay makabuo ng tamang SYNACK packet na may nakapirming seqnum, na papalitan sa hinaharap ng SYN cookie. Ang lahat ng mga pagbabago ay nangyayari sa process_tcp_syn() at mga nakapaligid na lugar.

Pag-verify ng package

Kakatwa, narito ang pinakakahanga-hangang linya, o sa halip, ang komentaryo dito:

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

Kapag isinusulat ang unang bersyon ng code, ginamit ang 5.1 kernel, para sa verifier kung saan mayroong pagkakaiba sa pagitan data_end и (const void*)ctx->data_end. Sa oras ng pagsulat, ang kernel 5.3.1 ay walang problemang ito. Posibleng iba ang pag-access ng compiler sa isang lokal na variable kaysa sa isang field. Moral of the story: Makakatulong ang pagpapasimple ng code kapag maraming pugad.

Susunod ay ang mga regular na pagsusuri sa haba para sa kaluwalhatian ng verifier; O MAX_CSUM_BYTES sa ibaba.

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

Paglalahad ng pakete

Pinuno namin seqnum и acknum, itakda ang ACK (nakatakda na ang SYN):

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

Magpalit ng mga TCP port, IP address at MAC address. Ang karaniwang library ay hindi naa-access mula sa XDP program, kaya memcpy() — isang macro na nagtatago ng Clang intrinsics.

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

Muling pagkalkula ng mga checksum

Ang mga IPv4 at TCP checksum ay nangangailangan ng pagdaragdag ng lahat ng 16-bit na salita sa mga header, at ang laki ng mga header ay nakasulat sa kanila, iyon ay, hindi alam sa oras ng pag-compile. Ito ay isang problema dahil hindi lalaktawan ng verifier ang normal na loop sa boundary variable. Ngunit ang laki ng mga header ay limitado: hanggang 64 bytes bawat isa. Maaari kang gumawa ng loop na may nakapirming bilang ng mga pag-ulit, na maaaring magtapos nang maaga.

Pansin ko na meron RFC 1624 tungkol sa kung paano bahagyang muling kalkulahin ang checksum kung ang mga nakapirming salita lamang ng mga pakete ay binago. Gayunpaman, ang pamamaraan ay hindi pangkalahatan, at ang pagpapatupad ay magiging mas mahirap na mapanatili.

Function ng pagkalkula ng 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;
}

Bagaman size na-verify sa pamamagitan ng calling code, ang pangalawang kondisyon sa paglabas ay kinakailangan upang mapatunayan ng verifier ang pagkumpleto ng loop.

Para sa mga 32-bit na salita, ipinapatupad ang isang mas simpleng bersyon:

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

Talagang muling pagkalkula ng mga checksum at pagpapadala ng packet pabalik:

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;

Tungkulin carry() gumagawa ng checksum mula sa 32-bit na kabuuan ng 16-bit na salita, ayon sa RFC 791.

TCP handshake verification

Ang filter ay wastong nagtatatag ng isang koneksyon sa netcat, nawawala ang panghuling ACK, kung saan tumugon ang Linux gamit ang isang RST packet, dahil ang network stack ay hindi nakatanggap ng SYN - ito ay na-convert sa SYNACK at ibinalik - at mula sa OS point of view, dumating ang isang packet na hindi nauugnay sa pagbukas mga koneksyon.

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

Mahalagang suriin sa mga ganap na aplikasyon at obserbahan tcpdump sa xdp-remote dahil, halimbawa, hping3 hindi tumutugon sa mga maling checksum.

Mula sa XDP point of view, ang pag-verify mismo ay walang halaga. Ang algorithm ng pagkalkula ay primitive at malamang na mahina sa isang sopistikadong umaatake. Ang Linux kernel, halimbawa, ay gumagamit ng cryptographic na SipHash, ngunit ang pagpapatupad nito para sa XDP ay malinaw na lampas sa saklaw ng artikulong ito.

Ipinakilala para sa mga bagong TODO na nauugnay sa panlabas na komunikasyon:

  • Hindi maiimbak ng XDP program cookie_seed (ang lihim na bahagi ng asin) sa isang pandaigdigang variable, kailangan mo ng imbakan sa kernel, ang halaga nito ay pana-panahong maa-update mula sa isang maaasahang generator.

  • Kung ang SYN cookie ay tumugma sa ACK packet, hindi mo kailangang mag-print ng mensahe, ngunit tandaan ang IP ng na-verify na kliyente upang magpatuloy sa pagpasa ng mga packet mula dito.

Lehitimong pag-verify ng kliyente:

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

Ang mga log ay nagpapakita na ang tseke ay pumasa (flags=0x2 - ito ay si SYN, flags=0x10 ay 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

Bagama't walang listahan ng mga na-verify na IP, walang proteksyon mula sa SYN flood mismo, ngunit narito ang reaksyon sa isang ACK flood na inilunsad ng sumusunod na command:

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

Mga entry sa log:

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

Konklusyon

Minsan ang eBPF sa pangkalahatan at ang XDP sa partikular ay mas ipinakita bilang isang advanced na tool ng administrator kaysa bilang isang platform ng pag-unlad. Sa katunayan, ang XDP ay isang tool para makagambala sa pagproseso ng mga packet ng kernel, at hindi isang alternatibo sa kernel stack, tulad ng DPDK at iba pang mga kernel bypass na opsyon. Sa kabilang banda, pinapayagan ka ng XDP na ipatupad ang medyo kumplikadong lohika, na, bukod dito, ay madaling i-update nang walang pagkagambala sa pagproseso ng trapiko. Ang verifier ay hindi gumagawa ng malalaking problema nang personal, hindi ko ito tatanggihan para sa mga bahagi ng userspace code.

Sa ikalawang bahagi, kung ang paksa ay kawili-wili, kukumpletuhin namin ang talahanayan ng mga na-verify na kliyente at mga disconnection, magpapatupad ng mga counter at magsulat ng isang userspace utility upang pamahalaan ang filter.

Link:

Pinagmulan: www.habr.com

Magdagdag ng komento