이번 시간에는 지난 시간에 이어 eBPF & XDP 기반의 고속 네트워크 프로그래밍에 관한 얘기(2번째 시간)를 좀 더 해 보고자 한다. 😎
목차
1. eBPF & XDP
2. Suricata IPS와 XDP Bypass
3. XDP 기반의 DDoS Mitigator
4. References
1. eBPF & XDP
eBPF(Extended Berkeley Packet Filter)는 사용자가 만든 프로그램을 실행하여 커널 기능을 확장시켜 주는 커널 내 가상 머신(virtual machine)이다. 이전 posting을 통해 한차례 소개한 바 있다.
https://slowbootkernelhacks.blogspot.com/2024/10/ebpf-xdp.html
XDP는 NIC driver로부터 전달받은 packet을 가로채어(hooking) 필요한 처리(eBPF VM 내에서 실행)을 하게 되는데, 그 결과에 따라서 패킷을 DROP하거나, 자신의 NIC으로 다시 돌려 보내거나(TX), 아니면 이웃한 NIC으로 전달(REDIRECT)할 수가 있다. 물론 이도 저도 아닌 상황이라면, 원래의 흐름대로 network stack으로 전달(PASS)하기도 한다.
- XDP_PASS: let the packet continue to the kernel network stack
- XDP_DROP: silently drop the packet
- XDP_ABORTED: drop the packet with trace point exception
- XDP_TX: bounce the packet back to the same NIC it arrived on
- XDP_REDIRECT: redirect the packet to another NIC or user space socket via the AF_XDP address family
마지막으로, 사용자 영역의 program과 XDP program(일반적으로는 eBPF program) 간에 정보를 공유하기 위해서는 BPF MAP(key-value store)이 사용된다.
- XDP는 network interface에 대한 hooking을 통해 NIC이 수신한 패킷을 network stack(kernel)으로 전달하기 전에 미리 처리하는 기술이다.
- XDP는 수신 패킷(rx)에 대한 처리만 정의하고 있다. 따라서, 송신 패킷(tx)에 대해서는 hooking 처리하지 않는다.
- NIC으로 부터 수신된 패킷은 XDP_DROP, XDP_TX, XDP_REDIRECT, XDP_PASS, XDP_ABORTED 등의 action을 통해 그 운명이 결정된다.
- XDP의 action을 결정하는데 사용자 영역의 program이 영향(도움)을 주기 위해서는 BPF map(key-value store)을 통해 필요한 정보를 XDP(kernel code)와 공유해야 한다.
- XDP에서 사용자 영역으로 패킷을 전달하고자 할 경우에도 XDP_REDIRECT를 이용해야 한다. 또한, 이웃한 NIC으로의 REDIRECT 상황과 구분하기 위해 BPF_MAP_TYPE_XSKMAP라는 MAP type 정보를 이용한다.
- AF_XDP는 AF_PACKET 처럼 kernel에 추가된 socket family로, XDP와 연계하여 userspace에서 고속 패킷 처리가 가능하도록 해준다.
2. Suricata IPS와 XDP Bypass
이번 장에서는 지난 시간(DPDK 기반 IPS)과는 달리, (상대적으로 조금은 느릴 것으로 예상되지만) AF_PACKET을 기반으로 Suricata IPS를 구성해 보고자 한다. 또한 여기에 XDP bypass 기능을 추가해 보고, 어떻게 동작하는지 확인해 보도록 하자.
<Testbed>
이 상태에서 source code를 build한 후, 돌려 보도록 하자.
$ git clone https://github.com/libbpf/libbpf.git
$ cd libbpf/src/
$ make
$ sudo make install
$ sudo make install_headers
$ sudo ldconfig
-> XDP bypass 기능을 사용하여 위해 suricata 최신 코드를 이용하기로 한다.
$ cd suricata && ./scripts/bundle.sh
$ ./autogen.sh
$ ./configure --enable-ebpf --enable-ebpf-build --with-clang=/usr/bin/clang --enable-non-bundled-htp --with-libhtp-includes=/usr/local/include --with-libhtp-libraries=/usr/local/lib
$ make clean && make
$ sudo make install-full
$ sudo ldconfig
$ sudo cp ebpf/xdp_filter.bpf /usr/local/libexec/suricata/ebpf/
-> xdp bypass code를 복사해 준다.
$ sudo vi /usr/local/etc/suricata/suricata.yaml
-> 아래와 같이 af-packet를 기반으로 하는 ips mode 설정을 한다.
...
af-packet:
- interface: enp1s0
threads: 1
cluster-id: 99
cluster-type: cluster_qm
defrag: no
buffer-size: 64535
copy-mode: ips
copy-iface: enp4s0
- interface: enp4s0
threads: 1
cluster-id: 98
defrag: no
cluster-type: cluster_flow
copy-mode: ips
copy-iface: enp1s0
buffer-size: 64535
...
자, 모든 준비가 끝났으니, suricata를 ips mode로 구동 시켜 보자.
$ sudo /usr/local/bin/suricata -c /usr/local/etc/suricata/suricata.yaml --af-packet
Notice: suricata: This is Suricata version 8.0.0-rc1-dev (ca429ef5e 2025-04-12) running in SYSTEM mode [LogVersion:suricata.c:1159]
Error: affinity: worker-cpu-set: upper bound (2) of cpu set is too high, only 2 cpu(s) [BuildCpusetWithCallback:util-affinity.c:128]
Warning: runmodes: disabling livedev.use-for-tracking with IPS mode. See ticket #6726. [RunModeEngineIsIPS:runmodes.c:388]
Notice: threads: Threads created -> W: 2 FM: 1 FR: 1 Engine started. [TmThreadWaitOnThreadRunning:tm-threads.c:1954]
-> 현재 test 중인 장비의 cpu 갯수가 2개 뿐이다. 일단 위의 에러는 무시하도록 한다.
위와 같이 구성한 후, iperf3로 간단히 확인해 보니, 2.5GbE 환경에서 대략 1Gbps 정도가 나온다. 그렇다면, 1GbE 환경이라면 대략 400Mbps 정도가 나온다는 얘기가 되는데... 😓
-> 아래와 같이 xdp bypass 설정을 추가해 준다.
...
af-packet:
- interface: enp1s0
threads: 1
cluster-id: 99
cluster-type: cluster_qm
defrag: no
buffer-size: 64535
copy-mode: ips
copy-iface: enp4s0
xdp-mode: driver
xdp-filter-file: /usr/local/libexec/suricata/ebpf/xdp_filter.bpf
bypass: yes
ring-size: 200000
- interface: enp4s0
threads: 1
cluster-id: 98
defrag: no
cluster-type: cluster_flow
copy-mode: ips
copy-iface: enp1s0
buffer-size: 64535
xdp-mode: driver
xdp-filter-file: /usr/local/libexec/suricata/ebpf/xdp_filter.bpf
bypass: yes
ring-size: 200000
...
stream:
memcap: 64mb
bypass: true
#memcap-policy: ignore
checksum-validation: yes # reject incorrect csums
midstream: false
#midstream-policy: ignore
midstream-policy: bypass
inline: auto # auto will use inline mode in IPS mode, yes or no set it statically
reassembly:
# experimental TCP urgent handling logic
#urgent:
# policy: inline # drop, inline, oob (1 byte, see RFC 6093, 3.1), gap
# oob-limit-policy: drop
memcap: 256mb
#memcap-policy: ignore
depth: 1mb # reassemble 1mb into a stream
toserver-chunk-size: 2560
toclient-chunk-size: 2560
randomize-chunk-size: yes
...
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: enp1s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdp qdisc mq state UP mode DEFAULT group default qlen 1000
link/ether 00:e0:1d:6b:bb:44 brd ff:ff:ff:ff:ff:ff
prog/xdp id 27
3: enp2s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
link/ether 00:e0:1d:6b:bb:45 brd ff:ff:ff:ff:ff:ff
4: enp3s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
link/ether 00:e0:1d:6b:bb:46 brd ff:ff:ff:ff:ff:ff
5: enp4s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdp qdisc mq state UP mode DEFAULT group default qlen 1000
link/ether 00:e0:1d:6b:bb:47 brd ff:ff:ff:ff:ff:ff
prog/xdp id 29
<Testbed>