2025년 4월 13일 일요일

DPDK 기반의 고성능 Network Appliance 만들기

이번 시간에는 DPDK를 이용하여 고성능 network 장비(Suricata IPS, NGINX web server)를 만드는 방법을 소개해 보고자 한다. 😎



목차
1. DPDK 이야기
2. Suricata IPS 만들기
3. FD.io VPP와 NGINX로 고성능 Web Server 만들기
4. References



패킷을 한 port에서 읽어서 다른 port로 전달(forwarding)하는 일은 L2/L3 switch/router, L4 switch는 물론이고 L7 security gateway(IPS, web filtering 등) 등에서 없어서는 안되는 매우 중요한 기술이다. 대부분의 경우는 성능 문제를 해결하기 위해 값비싼 ASIC이나 FPGA를 이용하지만, DPDK 기술을 이용한다면 값싼 산업용 appliance를 가지고도 고성능 패킷 처리가 가능한 제품을 충분히 만들어 낼 수가 있다. 😙

1. DPDK 이야기
이번 장에서는 고성능 network appliance(혹은 server)를 만들기 위해 제일 먼저 고려해야 할 사항인 DPDK project에 대해 소개하고자 한다.

1.1  DPDK의 개요
DPDK(Data Plane Development Kit)는 Linux kernel의 고질적인 네트워크 성능 문제(대용량 network traffic 처리 시 interrupt & system call 등에 의한 속도 저하)를 해결하기 위해 Intel이 주도하여 개발하고 있는 project(현재는 Linux Foundation Project임)로 NIC driver(주로 Intel, Marvell, Mellanox 등 중심) 개선과 아울러, kernel이 아닌 user space에서 packet을 처리(polling 방식)하도록 함으로써 network 성능을 획기적으로 향상시키는 기술이다. 경쟁 관계에 있는 기술로는 PF_RING(zero copy - 유료), Netmap, XDP(eBPF 기반) 등을 생각해 볼 수 있다.
📌 고성능 packet processing을 위한 한결 같은 공통 점은, Ring buffer, Zero Copy(DMA), Polling mode 등을 사용한다는 점과 kernel 대신 사용자 영역에서 동작한다는 점을 들 수 있다.
[그림 1.1] DPDK의 개념(1) [출처 - 참고문헌 3]


[그림 1.2] DPDK의 개념(2) [출처 - 참고문헌 5]


[그림 1.3] DPDK의 개념(3) [출처 - 참고문헌 5]

[그림 1.4] DPDK의 개념(4) [출처 - 참고문헌 18]

DPDK는 1000여개가 넘는 수많은 library로 구성되어 있다. 이 중 가장 핵심적인 요소만을 별도로 정리해 보면 다음과 같다.

[그림 1.5] DPDK의 핵심 구성 요소(1) [출처 - 참고문헌 6]

[그림 1.6] DPDK의 핵심 구성 요소(2) [출처 - 참고문헌 6]

[그림 1.7] DPDK의 핵심 구성 요소(3) [출처 - 참고문헌 2]


1.2  DPDK source code build 및 설치하기
DPDK source code를 build하는 절차는 아주 간단하다.

<Ubuntu 24.04 LTS>
sudo apt-get install meson ninja-build
sudo apt install python3-pyelftools
sudo apt-get install libnuma-dev

$ tar xvJf dpdk-24.11.1.tar.xz -> https://core.dpdk.org/download/에서 stable version을 내려 받는다.
$ cd dpdk-stable-24.11.1/
$ meson setup build $ cd build $ ninja $ sudo ninja install
  -> /usr/local/lib/x86_64-linux-gnu 아래에 1182개의 library 파일(librte*)이 생성된다.
-> header file(rte*.h)은 /usr/local/include에 복사된다.

1.3  DPDK example code 분석하기
백문이 불여일견~ 이 절에서는 basic forwarding sample application의 코드를 통해 DPDK의 동작 방식을 살펴 보도록 하자.


시험 환경은 다음과 같다.
<Testbed>
PC(Linux) => DPDK router(2 ports ethernet) => Access Point => Internet

<basic forwarding sample app 실행하기>
먼저, dpdk-devbind.py script를 이용하여, dpdk binding을 시도한다.
[그림 1.8] DPDK binding/unbinding 개념
📌 위의 그림에서는 DPDK -> kernel driver로 다시 binding 시, e1000e를 사용하도록 표시해 두었으나, 실제로는 자신이 사용하고 있는 ethernet driver에 맞는 이름으로 교체해 주어야 한다.

$ sudo echo 1024 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
$ sudo ifconfig enp1s0 down $ sudo ifconfig enp4s0 down $ sudo /usr/bin/dpdk-devbind.py --bind uio_pci_generic 0000:01:00.0 $ sudo /usr/bin/dpdk-devbind.py --bind uio_pci_generic 0000:04:00.0

이어서, basic forwarding app을 실행한다.
chyi@dpdk2:~/workspace/dpdk-stable-24.11.1/examples/skeleton/build$ sudo ./basicfwd
EAL: Detected CPU lcores: 4
EAL: Detected NUMA nodes: 1
EAL: Detected shared linkage of DPDK
EAL: Multi-process socket /var/run/dpdk/rte/mp_socket
EAL: Selected IOVA mode 'PA'
Port 0 MAC: 10 90 27 e0 4c 23
Port 1 MAC: 10 90 27 e0 4c 26

WARNING: Too many lcores enabled. Only 1 used.

Core 0 forwarding packets. [Ctrl+C to quit]

이 상태에서 PC로 부터 DPDK router를 거쳐 internet으로 ping test를 해 보니, 정상적으로 동작(forwarding)되는 것을 알 수 있다.

$ ping 8.8.8.8 PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data. 64 bytes from 8.8.8.8: icmp_seq=1 ttl=116 time=32.2 ms 64 bytes from 8.8.8.8: icmp_seq=2 ttl=116 time=32.8 ms ^C --- 8.8.8.8 ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 1001ms rtt min/avg/max/mdev = 32.169/32.464/32.760/0.295 ms

자, 이번에는 코드를 들여다 볼 차례이다. 코드량이 많지 않으니, 가벼운 마음으로 들여다 보도록 하자. 😋

<examples/skeleton/basicfwd.c>
__________________________________________________________________________________
제일 먼저, main( ) 함수는 rte_eal_init( ) 함수를 호출하여 dpdk 초기화 작업 및 각각의 lcore에 대한 thread를 구동시킨다.

int ret = rte_eal_init(argc, argv);
if (ret < 0)
	rte_exit(EXIT_FAILURE, "Error with EAL initialization\n");
[그림 1.9] DPDK EAL(Environment Abstraction Layer) init 함수 [출처 - 참고문헌 2]

다음으로, application에서 송/수신용으로 사용할 mbufs(Message Buffer)를 위한 mempool(memory pool)을 할당한다. mbufs는 dpdk에서 사용하는 packet buffer로 이해하면 된다.
📌 DPDK의 message buffer는 (네트워크에서 우수성을 보이는) FreeBSD의 mbuf에서 영향을 받은 것으로 보인다.

mbuf_pool = rte_pktmbuf_pool_create("MBUF_POOL", NUM_MBUFS * nb_ports,
	MBUF_CACHE_SIZE, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());
그 다음에 할 일은 port_init( ) 함수를 사용하여 각각의 port를 초기화하는 것이다. port_init( ) 함수는 rte_eth_dev_configure( ) 함수를 사용하여 ethernet device를 설정한 후, rte_eth_rx_queue_setup( )와 rte_eth_tx_queue_setup( ) 함수를 통해 1개의 Rx queue와 1개의 Tx queue를 할당 및 초기화한다. 또한, rte_eth_dev_start( ) 함수를 호출하므로써 ethernet port를 구동시킨다. port_init( ) 함수에서 마지막으로 수행하는 일은 rte_eth_promiscuous_enable( )를 호출하여 Rx port를 promiscuous mode로 설정하는 일이다.

RTE_ETH_FOREACH_DEV(portid)
	if (port_init(portid, mbuf_pool) != 0)
		rte_exit(EXIT_FAILURE, "Cannot init port %"PRIu16 "\n",
				portid);
이후, main( ) 함수는 가용한 lcore에서 수행할 application function(여기서는 lcore_main( ))을 호출한다. lcore_main( ) 함수가 하는 일은 무한 loop를 돌면서 특정 port로 부터 packet을 수신한 후, 이를 paired port(port ^ 1)로 전달하는 일이다. 즉, packet forwarding을 반복해서 처리하는 것이다.

lcore_main()
  for (;;) {
	/*
	 * Receive packets on a port and forward them on the paired
	 * port. The mapping is 0 -> 1, 1 -> 0, 2 -> 3, 3 -> 2, etc.
	 */
	RTE_ETH_FOREACH_DEV(port) {

		/* Get burst of RX packets, from first port of pair. */
		struct rte_mbuf *bufs[BURST_SIZE];
		const uint16_t nb_rx = rte_eth_rx_burst(port, 0,
				bufs, BURST_SIZE);

		if (unlikely(nb_rx == 0))
			continue;

		/* Send burst of TX packets, to second port of pair. */
		const uint16_t nb_tx = rte_eth_tx_burst(port ^ 1, 0,
				bufs, nb_rx);

		/* Free any unsent packets. */
		if (unlikely(nb_tx < nb_rx)) {
			uint16_t buf;
			for (buf = nb_tx; buf < nb_rx; buf++)
				rte_pktmbuf_free(bufs[buf]);
		}
	}
    }

끝으로, program을 종료할 조건이 되면, 아래 함수를 호출하면서 program을 마친다.
rte_eal_cleanup();
__________________________________________________________________________________

<여기서 잠깐 - DPDK와 GPU에 관하여>
DPDK는 NVIDIA CUDA GPU driver를 이용하여 packet processing을 할 수 있는 방법도 제공한다. 😍

CUDA(Compute Unified Devie Architecture) toolkit 사용법과 관련해서는 아래 site 내용을 참조할 필요가 있다.

또한, 아래 코드는 GPU를 사용하여 DPDK mbuf pool을 생성하여 사용하는 layer 2 forwarding example code이다.
__________________________________________________________________________

이상으로 DPDK의 대략적인 구조와 동작 방식을 간략히 살펴 보았다. DPDK 관련 내용은 생각보다 방대하므로, 관련 문서를 지속적으로 살펴 볼 필요가 있다. 🚩 이어지는 장에서는 (L2 forwarding의 심화 과정이라고 할 수 있는) DPDK를 기반으로 Intrusion Prevention System을 구축하는 방법을 소개하도록 하자.


2. Suricata IPS 만들기
SuricataSnort와 더불어 IDS/IPS용 open source로 유명한 project이다. 이번 장에서는 Suricata를 DPDK 기반으로 돌려 보고, suricata rule을 통해 간단한 DDoS mitigation system을 구축하는 과정을 소개해 보도자 한다.
📌 Snort에 관해서는 이전 posting을 통해 한 차례 소개한 바 있다.

2.1  Suricata project의 개요
사실, suricata는 이 분야에서 워낙 유명한 open source project이다 보니, blog의 주제로 삼기에도 약간은 민망한 느낌이 있다(참신함이 떨어진다). 😓 그럼에도 불구하고, 이번 posting에서 suricata를 굳이 다시 소환한 이유는 dpdk와의 연동 방법을 확인하기 위해서이다.



[그림 2.1] DPDK 기반의 Suricata 개요도(1)


[그림 2.2] DPDK 기반의 Suricata 개요도(2)


[그림 2.3] Suricata attack detection engine 개요


[그림 2.4] Suricata suricata rule example


2.2 Suricata build  하기
먼저, Suricata를 build하는 절차를 소개하면 다음과 같다.

<Ubuntu 24.04 LTS>
$ sudo apt -y install autoconf automake build-essential cargo \
   cbindgen libjansson-dev libpcap-dev libpcre2-dev libtool \
   libyaml-dev make pkg-config rustc zlib1g-dev
$ sudo apt-get install liblz4-dev
$ sudo apt-get install libmagic-dev
$ sudo apt-get install libcap-ng-dev

$ git clone https://github.com/OISF/libhtp
$ cd libhtp/
$ ./autogen.sh
$ ./configure; make; sudo make install

$ cargo install --force cbindgen
    -> .bashrc에 PATH를 잡아주도록 한다.
export PATH=/home/YOUR_ID/.cargo/bin:$PATH

$ cbindgen --version
cbindgen 0.28.0

<how to build suricata>
$ tar xvzf suricata-suricata-7.0.10.tar.gz
$ cd suricata-suricata-7.0.10/
$ ./autogen.sh
$ ./configure --enable-dpdk --enable-non-bundled-htp --with-libhtp-includes=/usr/local/include --with-libhtp-libraries=/usr/local/lib
$ make
$ sudo make install-full
    -> 이렇게 해야 configuration을 포함한 전체 내용을 설치할 수 있다.
    -> /usr/local에 설치하는 것을 기준으로 directory가 생성된다. drwxr-xr-x 2 chyi chyi 4096 4월 4 18:21 bin drwxr-xr-x 3 chyi chyi 4096 4월 4 18:21 etc drwxr-xr-x 3 chyi chyi 4096 4월 4 18:21 lib drwxrwxr-x 5 chyi chyi 4096 4월 4 18:21 share drwxr-xr-x 5 chyi chyi 4096 4월 4 18:21 var


2.3 Testbed 준비하기
Suricata build가 끝났으니, 다음으로 할 일은 target 장치에서 돌려 보는 것이다.

[그림 2.5] 1GbE 4 ports intel x86_64 appliance
(Intel(R) Celeron(R) CPU J1900 @ 1.99GHz, CPU 4 cores, RAM 4GB)

테스트 환경은 아래와 같이 구성하기로 한다. 😜
<Testbed>
PC(Ubuntu 22.04 LTS) ---> [enp1s0] Suricata DPDK IPS [enp4s0] ---> Access Point --> Internet


2.4 Target board 상에서 DPDK 설정하기

<hugepages 갯수 설정>
$ sudo echo 1024 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
-> DPDK application은 memory page fault 횟수를 줄이기 위해 hugepages memory를 사용한다(속도 개선 차원).
-> 이 명령을 입력하거나 혹은 아래 shell script를 실행해 주도록 하자.

#!/bin/sh
HUGEPAGES_NUM=1024 HUGEPAGES_PATH=/dev/hugepages sync && echo 3 > /proc/sys/vm/drop_caches echo $HUGEPAGES_NUM > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages if [ `cat /proc/mounts | grep hugetlbfs | grep $HUGEPAGES_PATH | wc -l` -eq 0 ]; then if [ ! -d $HUGEPAGES_PATH ]; then mkdir $HUGEPAGES_PATH fi mount -t hugetlbfs nodev $HUGEPAGES_PATH fi
~

<dpdk binding 설정>
-> user space driver를 위해 아래 kernel module을 구동시킨다.
$ sudo modprobe uio $ sudo modprobe uio_pci_generic

-> 부팅 시, 자동으로 올려 주려면, 아래 shell script를 실행해주면 된다.
_________________________________________
#!/bin/sh
if ! grep -q "uio" /etc/modules; then
  echo "uio" | sudo tee -a /etc/modules
fi
if ! grep -q "uio_pci_generic" /etc/modules; then
  echo "uio_pci_generic" | sudo tee -a /etc/modules
fi
_________________________________________

다음으로, dpdk 설치를 통해 얻게된 dpdk-devbind.py script를 이용하여, dpdk binding을 시도한다.

$ ifconfig enp1s0 down $ ifconfig enp4s0 down -> dpdk binding 전에 interface를 down시켜야 한다.
$ dpdk-devbind.py --bind=uio_pci_generic enp1s0 $ dpdk-devbind.py --bind=uio_pci_generic enp4s0

$ dpdk-devbind.py -s
Network devices using DPDK-compatible driver ============================================ 0000:01:00.0 'I211 Gigabit Network Connection 1539' drv=uio_pci_generic unused=igb 0000:04:00.0 'I211 Gigabit Network Connection 1539' drv=uio_pci_generic unused=igb -> 이런 내용이 출력되어야 정상적으로 binding된 것이다.
Network devices using kernel driver =================================== 0000:02:00.0 'I211 Gigabit Network Connection 1539' if=enp2s0 drv=igb unused=uio_pci_generic 0000:03:00.0 'I211 Gigabit Network Connection 1539' if=enp3s0 drv=igb unused=uio_pci_generic *Active* No 'Baseband' devices detected ============================== Crypto devices using kernel driver ================================== 0000:00:1a.0 'Atom Processor Z36xxx/Z37xxx Series Trusted Execution Engine 0f18' drv=mei_txe unused=uio_pci_generic No 'DMA' devices detected ========================= No 'Eventdev' devices detected ============================== No 'Mempool' devices detected ============================= No 'Compress' devices detected ============================== No 'Misc (rawdev)' devices detected =================================== No 'Regex' devices detected =========================== No 'ML' devices detected ========================

2.5 Suricata configuration file 설정 후 돌려 보기
다음으로 할 일은, 아래 site 내용을 참조하여, suricata configuration을 dpdk IPS mode로 변경하는 것이다.


[그림 2.6] suricata dpdk ips mode를 위한 설정 파일 변경 [출처 - 참고문헌 8]


$ sudo vi /usr/local/etc/suricata/suricata.yaml
...
vars:   
  # more specific is better for alert accuracy and performance
  address-groups:
    HOME_NET: "[192.168.5.0/24]"
...
dpdk: eal-params: proc-type: primary
interfaces: - interface: 0000:01:00.0
threads: 1
promisc: true
multicast: true
checksum-checks: true
checksum-checks-offload: true
mtu: 1500
mempool-size: 65535
mempool-cache-size: 257
rx-descriptors: 1024
tx-descriptors: 1024
copy-mode: ips
copy-iface: 0000:04:00.0

- interface: 0000:04:00.0 threads: 1 promisc: true multicast: true checksum-checks: true checksum-checks-offload: true mtu: 1500 rss-hash-functions: auto mempool-size: 65535 mempool-cache-size: 257 rx-descriptors: 1024 tx-descriptors: 1024 copy-mode: ips copy-iface: 0000:01:00.0
...
threading: set-cpu-affinity: yes
cpu-affinity:
- management-cpu-set:
cpu: [ 0 ]
- receive-cpu-set:
cpu: [ 0 ]
- worker-cpu-set: cpu: [ "all" ] mode: "exclusive"
prio: low: [ 0 ] medium: [ "1-2" ] high: [ 3 ] default: "medium"
...
~

다음으로, suricata용 rules이 준비되어 있는지 확인한다.

$ sudo vi /usr/local/var/lib/suricata/rules/suricata.rules

[그림 2.7] Suricata rules 확인

자, 모든 것이 준비되었으니, suricata daemon을 구동시켜 보도록 하자.

$ sudo /usr/local/bin/suricata -c /usr/local/etc/suricata/suricata.yaml --dpdk
i: suricata: This is Suricata version 7.0.10 RELEASE running in SYSTEM mode
W: runmodes: disabling livedev.use-for-tracking with IPS mode. See ticket #6726.
i: conf: unable to find interface default in DPDK config
W: dpdk: "all" specified in worker CPU cores affinity, excluding management threads
i: conf: unable to find interface default in DPDK config
i: conf: unable to find interface default in DPDK config
i: conf: unable to find interface default in DPDK config
i: threads: Threads created -> W: 2 FM: 1 FR: 1   Engine started.
  -> 약간의 문제가 있는 것 처럼 보이지만, 동작은 하는 듯하다.

$ ps -efT|grep suricata
  -> 많은 수의 thread가 떠 있는 것이 보인다.
[그림 2.8] 구동 중인 Suricata threads 확인

이 상태에서 top 명령(top -H)을 실행해 보니, 전체 CPU 사용율이 200%에 육박하는 것이 보인다(DPDK 기반으로 동작하는 것이 확인됨).

[그림 2.9] Suricata 동작 시 CPU 사용량 확인
📌 CPU 사용율이 99.9%인 thread가 2개 보이는데, 이는 dpdk가 2개의 core를 독점하고 있음을 보여준다.

이 상태에서 (테스트베드) PC의 network 설정을 dhcp로 변경한 후, internet 연결을 시도해 보도록 하자.

chyi@earth:~$ netstat -nr
Kernel IP routing table
Destination Gateway Genmask Flags MSS Window irtt Iface
0.0.0.0 192.168.8.1 0.0.0.0 UG 0 0 0 enp4s0
10.1.1.0 0.0.0.0 255.255.255.0 U 0 0 0 wg0
100.109.0.0 0.0.0.0 255.255.0.0 U 0 0 0 wt0
169.254.0.0 0.0.0.0 255.255.0.0 U 0 0 0 enp4s0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
192.168.8.0 0.0.0.0 255.255.255.0 U 0 0 0 enp4s0

chyi@earth:~$ ping 8.8.8.8 PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data. 64 bytes from 8.8.8.8: icmp_seq=1 ttl=115 time=31.6 ms 64 bytes from 8.8.8.8: icmp_seq=2 ttl=115 time=31.9 ms 64 bytes from 8.8.8.8: icmp_seq=3 ttl=115 time=31.3 ms 64 bytes from 8.8.8.8: icmp_seq=4 ttl=115 time=32.3 ms ^C --- 8.8.8.8 ping statistics --- 4 packets transmitted, 4 received, 0% packet loss, time 3004ms rtt min/avg/max/mdev = 31.301/31.777/32.300/0.365 ms

OK, IPS 장비를 통해 internet 연결이 정상적으로 진행된다. 이 상태에서 1-2일 정도 인터넷 surfing을 해 보아도, 느리거나 연결이 안되는 등의 증상은 보이지 않는다. 😎

2.6 차단 시험하기
다음으로, 확인할 사항은 IPS mode를 통해 차단이 정상적으로 이루어지는지를 확인하는 일이다. 아래 rule을 /usr/local/var/lib/suricata/rules/suricata.rules 파일의 맨 앞에 추가해 보자.

drop icmp 192.168.8.0/24 any -> any any (msg:"SURICATA ping test"; sid:3260001; rev:1;)

이후, suricata를 재구동한 후, ping test를 해 보면, 정상적으로 차단되는 것을 알 수 있다.


2.7 DDoS 차단 rule 적용해 보기
아래 3개의 차단 rule을 적용한 상태에서 hping3 tool을 이용하여 다량의 traffic을 발생시켜 보면, 일정 시간 경과 후, 해당 traffic이 차단되는 것을 알 수 있다.

<Rule to detect TCP SYN Flood attack>
drop tcp any any -> $HOME_NET any (msg: "Possible DDoS attack"; flags: S; flow: stateless; threshold: type both, track by_dst, count 200, seconds 5; classtype:denial-of-service; sid:1000001; rev:1;)

<Rule to detect UDP Flood attack>
drop udp any any -> any any (msg:"Possible UDP Flood DoS"; threshold: type both, track by_dst, count 200, seconds 1; classtype:attempted-dos; sid:1000055; rev:1;)

<Rule to detect ICMP Flood attack>
drop icmp any any -> any any (msg:"Possible ICMP Flood DoS"; itype:8; threshold: type both, track by_dst, count 200, seconds 1; classtype:attempted-dos; sid:1000054; rev:1;)

<TCP SYN Flood attack using hping3>
$ sudo hping3 -S -p 22 --flood --rand-source 192.168.8.100

<UDP Flood attack using hping3>
$ sudo hping3 --udp --flood -p 80 192.168.8.100

<ICMP Flood attack using hping3>
$ sudo hping3 --icmp --flood -c 1000 --spoof 192.168.8.100 192.168.8.255


<여기서 잠깐 - Gatekeeper DDoS Migitator Project에 관하여>
사실 이번 posting의 주제에 맞는 open source를 찾던 중, (DPDK 기반으로 만들어진) Gatekeeper라는 project를 알게 되었다. 따라서 이를 자세히 소개할 계획이었으나, 생각보다 개념이 복잡하고, 실제 테스트 환경을 구축하는 일이 간단치가 않아서, 당초 계획을 변경할 수 밖에 없게 되었다. 😓. 향후에 소개할 것을 대비해, 아쉬운 대로 몇개의 그림만을 옮겨 보기로 한다. 자세한 설명은 후일을 기약하도록 하자. 💢



[그림 2.10] Gatekeeper 구성 요소 [출처 - 참고문헌 7]

Gatekeeper project는 GatekeeperGrantor라는 2개의 daemon과 BGP(Border Gateway Protocol) daemon(BIRD open source project 활용)으로 구성되어 있다.

[그림 2.11] Gatekeeper block diagram [출처 - 참고문헌 7]


[그림 2.12] Grantor block diagram [출처 - 참고문헌 7]


[그림 2.13] Gatekeeper flow 처리 루틴 개요  [출처 - 참고문헌 7]

자세한 사용 방법과 관련해서는 아래 site를 참조하기 바란다.


____________________________________________________________________________

2.8 Napatech SmartNIC 이야기
Suricata가 모든 packet을 실시간 검사할 것으로 생각하겠지만, 그렇게 할 경우, 심각한 성능 저하로 이어질 수 있기 때문에, 특정 traffic에 대해서는 bypass(단순 forwarding)할 수 있는 기능을 제공한다.

<Bypass 트래픽의 예>
  • 암호화된 traffic - TLS, IPsec, Kerberos(단, initial handshaking 이후의 트랙픽을 대상으로 함)
  • Streaming services - music, movies, etc. from Netflix, YouTube, Spotify, Apple Music
  • 사용자가 지정한 특정 traffic
📌 단, 무조건 해당 packet을 bypass하는 것이 아니라, 초기 패킷을 확인하여 1개의 flow로 인지한 상태에서 같은 flow에 포함되는 나머지 패킷을 bypass하게 된다.

Suricata rule 내에 아래와 같이 bypass keyword가 포함되어 있는 경우에는, 해당 packet을 bypass 즉, 특별한 packet 검사를 수행하지 않고, 반대쪽 interface로 패킷을 내보내게 된다. 이렇게 할 경우, (당연히) forwarding 성능이 향상될 수 밖에 없다.

pass ip 1.2.3.4 any <> any any (msg:”pass all traffic from/to 1.2.3.4”; bypass; sid:1;)

Bypass 처리는 간단한 방식이기 때문에 h/w 적으로 처리할 수 있다면, forwarding speed를 확실히 개선할 수가 있다.
Napatech SmartNIC은 이러한 요구사항에 맞게 bypass traffic을 h/w(FPGA) 적으로 처리하기 위해 등장하였는데, 자세한 사항은 아래 site 내용을 참조해 주기 바란다.

[그림 2.14] Napatech 25Gbps SmartNIC [출처 - 참고문헌 11]




지금까지 DPDK와 Suricata open source project를 활용하여 IPS 장비를 만드는 방법을 간략히 소개해 보았다. 이어지는 장에서는 FD.io VPP와 NGINX를 가지고, 고성능 web server를 만드는 방법을 소개해 보고자 한다.


3. FD.io VPP와 NGINX로 고성능 Web Server 만들기
이 장에서는 DPDK와 FD.io VPP를 기반으로 고성능 web server를 만드는 방법을 소개하고자 한다.

3.1 FD.io VPP Host Stack 이야기
이미 이전 posting을 통해서, FD.io VPP를 잘 활용할 경우, 고성능 network 장비(예: L2 Switch, L3 Router, NAT Router, Load Balancer, VPN Gateway 등)를 쉽게 만들 수 있다는 사실을 확인하였다. 


[그림 3.1] FD.io VPP 기반의 고성능 network appliance 

그렇다면, 동일한 방법을 사용하여 고성능 Server(Web, DNS, DHCP 등)를 구축할 수는 없을까 ?  

[그림 3.2] FD.io VPP 기반의 고성능 서버

아, 물론 FD.io VPP에는 tap interface(혹은 lcp plugin)를 통해 linux host 상의 다양한 application과 연동하는 방법이 이미 존재한다(이 방법의 장점: 기존 application code를 수정할 필요가 전혀 없음).

[그림 3.3] FD.io VPP tap interface를 통한 server 연결

________________________________________________________________________
chyi@dpdk2:~$ sudo vppctl
    _______    _        _   _____  ___ 
 __/ __/ _ \  (_)__    | | / / _ \/ _ \
 _/ _// // / / / _ \   | |/ / ___/ ___/
 /_/ /____(_)_/\___/   |___/_/  /_/

vpp# set interface state GigabitEthernet1/0/0 up
vpp# set interface state GigabitEthernet4/0/0 up
vpp# create bridge-domain 1
vpp# set interface l2 bridge GigabitEthernet1/0/0 1
vpp# set interface l2 bridge GigabitEthernet4/0/0 1
vpp# lcp create GigabitEthernet1/0/0 host-if eth0
vpp# set interface l2 bridge tap4096 1

chyi@dpdk2:~$ sudo ip link set eth0 up mtu 9000
chyi@dpdk2:~$ sudo ip addr add 192.168.8.10/24 dev eth0
chyi@dpdk2:~$ sudo route add default gw 192.168.8.1
________________________________________________________________________

문제는 이렇게 할 경우, 고성능을 낼 수 있는가하는 의문이 남는다(그렇다고 실제로 성능 측정을 해 본 것은 아니다. 😋). 이번 시간에는 이에 대한 답을 찾기 위해 그동안 살펴보지 않았던, VPP Host Stack에 관한 이야기를 꺼내 보고자 한다. VPP Host Stack은 글자 그대로 VPP 위에 application을 위한 통신 stack(transport -> session -> vcl 등의 계층 구조)을 구축해 놓은 것을 말한다.

VPP Host Stack의 구성 요소
  • 다양한 트랜스포트 프로토콜을 받아들이는 세션 계층 
  • VPP와 application 간의 data 전달을 위한 공유 메모리 구조(아래 그림의 mq, rx/tx 부분에 해당함)
  • 각종 트랜스포트 계층에 대한 구현. 즉, TCP, QUIC, TLS, UDP
  • VCL(VPP Comms Library)
  • LD_PRELOAD 라이브러리
    • POSIX API (for legacy application)
    • 기존 app을 수정 없이 그대로 사용 가능
[그림 3.4] FD.io VPP Host Stack [출처 - 참고문헌 12]

Application에서 VPP Host Stack을 사용하기 위해서는 VCL(VPP Comms Library)과 LD_PRELOAD library를 적절히 활용할 수 있어야 한다.

[그림 3.5] FD.io VPP Host Stack [출처 - 참고문헌 14]


3.2 FD.io VPP Build 하기
VPP Host Stack 설정을 위해, 오랫만에 vpp code를 다시 build해 보도록 하자.

<Ubuntu 24.04 LTS>
git clone https://github.com/FDio/vpp
$ cd vpp
git checkout stable/2502
  -> stable version을 사용하도록 하자.
make install-dep
  -> vpp build에 필요한 package 설치가 누락되었을 있으니, 관련 package를 설치해 주자.

$ make build
...
Target processor    : x86_64
Prefix path         : /opt/vpp/external/x86_64 /mnt/hdd/workspace/VPP/04082025/vpp/build-root/install-vpp_debug-native/external
Install prefix      : /mnt/hdd/workspace/VPP/04082025/vpp/build-root/install-vpp_debug-native/vpp
Library dir         : lib/x86_64-linux-gnu
Multiarch variants  : hsw skx icl
-- Configuring done
-- Generating done
-- Build files have been written to: /mnt/hdd/workspace/VPP/04082025/vpp/build-root/build-vpp_debug-native/vpp
@@@@ Building vpp in /mnt/hdd/workspace/VPP/04082025/vpp/build-root/build-vpp_debug-native/vpp @@@@
[1714/2793] Linking C shared library lib/x86_64-linux-gnu/vpp_plugins/af_xdp_plugin.so
FAILED: lib/x86_64-linux-gnu/vpp_plugins/af_xdp_plugin.so 
: && /usr/lib/ccache/clang -fPIC    -shared  -o lib/x86_64-linux-gnu/vpp_plugins/af_xdp_plugin.so CMakeFiles/plugins/af_xdp/CMakeFiles/af_xdp_plugin_hsw.dir/input.c.o CMakeFiles/plugins/af_xdp/CMakeFiles/af_xdp_plugin_hsw.dir/output.c.o CMakeFiles/plugins/af_xdp/CMakeFiles/af_xdp_plugin_skx.dir/input.c.o CMakeFiles/plugins/af_xdp/CMakeFiles/af_xdp_plugin_skx.dir/output.c.o CMakeFiles/plugins/af_xdp/CMakeFiles/af_xdp_plugin_icl.dir/input.c.o CMakeFiles/plugins/af_xdp/CMakeFiles/af_xdp_plugin_icl.dir/output.c.o CMakeFiles/plugins/af_xdp/CMakeFiles/af_xdp_plugin.dir/api.c.o CMakeFiles/plugins/af_xdp/CMakeFiles/af_xdp_plugin.dir/cli.c.o CMakeFiles/plugins/af_xdp/CMakeFiles/af_xdp_plugin.dir/device.c.o CMakeFiles/plugins/af_xdp/CMakeFiles/af_xdp_plugin.dir/format.c.o CMakeFiles/plugins/af_xdp/CMakeFiles/af_xdp_plugin.dir/unformat.c.o CMakeFiles/plugins/af_xdp/CMakeFiles/af_xdp_plugin.dir/plugin.c.o CMakeFiles/plugins/af_xdp/CMakeFiles/af_xdp_plugin.dir/input.c.o CMakeFiles/plugins/af_xdp/CMakeFiles/af_xdp_plugin.dir/output.c.o  -Wl,-rpath,::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::  -Wl,--gc-sections  /mnt/hdd/workspace/VPP/04082025/vpp/build-root/install-vpp_debug-native/external/lib/libxdp.a  /mnt/hdd/workspace/VPP/04082025/vpp/build-root/install-vpp_debug-native/external/lib64/libbpf.a  /usr/lib/x86_64-linux-gnu/libelf.so  /usr/lib/x86_64-linux-gnu/libz.so && :
/usr/bin/ld: /mnt/hdd/workspace/VPP/04082025/vpp/build-root/install-vpp_debug-native/external/lib64/libbpf.a(usdt.o): warning: relocation against `libbpf_mode' in read-only section `.text'
/usr/bin/ld: /mnt/hdd/workspace/VPP/04082025/vpp/build-root/install-vpp_debug-native/external/lib64/libbpf.a(btf.o): relocation R_X86_64_PC32 against symbol `libbpf_mode' can not be used when making a shared object; recompile with -fPIC
/usr/bin/ld: final link failed: bad value
clang: error: linker command failed with exit code 1 (use -v to see invocation)
[1737/2793] Building C object CMakeFiles/plugins/avf/CMakeFiles/avf_plugin_hsw.dir/output.c.o
ninja: build stopped: subcommand failed.
make[1]: *** [Makefile:700: vpp-build] 오류 1
make[1]: 디렉터리 '/mnt/hdd/workspace/VPP/04082025/vpp/build-root' 나감
make: *** [Makefile:439: build] 오류 2

$ vi src/plugins/af_xdp/CMakeLists.txt
-> Hmm, 뭔가 좀 이상하다. 아래와 같이 수정 후, 다시  build해 보자.
...
set_property(GLOBAL PROPERTY FIND_LIBRARY_USE_LIB64_PATHS TRUE)
vpp_plugin_find_library(af_xdp XDP_LIB libxdp.a)
#vpp_plugin_find_library(af_xdp BPF_LIB libbpf.a)
vpp_plugin_find_library(af_xdp BPF_LIB libbpf)
...

$ make wipe
$ make build
  -> OK, 넘어 간다.  make rebuild를 해도 된다.

$ make pkg-deb
  -> debian package 파일을 만들자.

$ cd build-root

$ ls -l *.deb
-rw-r--r-- 1 chyi chyi    165862  4월  8 18:16 libvppinfra-dev_25.02.0-2~gaa19c526a-dirty_amd64.deb
-rw-r--r-- 1 chyi chyi    206292  4월  8 18:16 libvppinfra_25.02.0-2~gaa19c526a-dirty_amd64.deb
-rw-r--r-- 1 chyi chyi     30458  4월  8 18:16 python3-vpp-api_25.02.0-2~gaa19c526a-dirty_amd64.deb
-rw-r--r-- 1 chyi chyi   1047038  4월  8 18:16 vpp-crypto-engines_25.02.0-2~gaa19c526a-dirty_amd64.deb
-rw-r--r-- 1 chyi chyi 102031480  4월  8 18:16 vpp-dbg_25.02.0-2~gaa19c526a-dirty_amd64.deb
-rw-r--r-- 1 chyi chyi   1403816  4월  8 18:16 vpp-dev_25.02.0-2~gaa19c526a-dirty_amd64.deb
-rw-r--r-- 1 chyi chyi   4667856  4월  8 18:16 vpp-plugin-core_25.02.0-2~gaa19c526a-dirty_amd64.deb
-rw-r--r-- 1 chyi chyi    410838  4월  8 18:16 vpp-plugin-devtools_25.02.0-2~gaa19c526a-dirty_amd64.deb
-rw-r--r-- 1 chyi chyi   5874060  4월  8 18:16 vpp-plugin-dpdk_25.02.0-2~gaa19c526a-dirty_amd64.deb
-rw-r--r-- 1 chyi chyi   5537412  4월  8 18:16 vpp_25.02.0-2~gaa19c526a-dirty_amd64.deb

_____________________________________________________________
📌 주의: make build를 수행하기 전, configure 파일을 먼저 실행할 경우, 아래와 같이 ninja를 이용한 build 방법이 출력된다. 문제는 ninja; nina pkg-deb를 수행해 본 결과, 설치 파일이 원하는 대로 동작되지 않았다. 원인 파악이 필요해 보인다. 😈
  Useful build commands:

  ninja                   Build VPP
  ninja set-build-type-*  Change build type to <debug|release|gcov|...>
  ninja config            Start build configuration TUI
  ninja run               Runs VPP using startup.conf in the build directory
  ninja debug             Runs VPP inside GDB using startup.conf in the build directory
  ninja pkg-deb           Create .deb packages
  ninja install           Install VPP to /usr/local
_____________________________________________________________


3.3 FD.io VPP 환경 구축하기
Build 작업이 끝났으니, vpp를 설치하고 network 설정을 진행하도록 하자.

$ sudo dpkg -i ./*.deb
$ sudo dpkg --list | grep vpp
ii  libvppinfra                          25.02.0-2~gaa19c526a-dirty        amd64        Vector Packet Processing--runtime libraries
ii  libvppinfra-dev                      25.02.0-2~gaa19c526a-dirty        amd64        Vector Packet Processing--runtime libraries
ii  python3-vpp-api                      25.02.0-2~gaa19c526a-dirty        amd64        VPP Python3 API bindings
ii  vpp                                  25.02.0-2~gaa19c526a-dirty        amd64        Vector Packet Processing--executables
ii  vpp-crypto-engines                   25.02.0-2~gaa19c526a-dirty        amd64        Vector Packet Processing--runtime crypto engines
ii  vpp-dbg                              25.02.0-2~gaa19c526a-dirty        amd64        Vector Packet Processing--debug symbols
ii  vpp-dev                              25.02.0-2~gaa19c526a-dirty        amd64        Vector Packet Processing--development support
ii  vpp-plugin-core                      25.02.0-2~gaa19c526a-dirty        amd64        Vector Packet Processing--runtime core plugins
ii  vpp-plugin-devtools                  25.02.0-2~gaa19c526a-dirty        amd64        Vector Packet Processing--runtime developer tool plugins
ii  vpp-plugin-dpdk                      25.02.0-2~gaa19c526a-dirty        amd64        Vector Packet Processing--runtime dpdk plugin

$ ps -ef|grep vpp
root         846       1 99 01:35 ?        03:04:08 /usr/bin/vpp -c /etc/vpp/startup.conf
chyi        1453    1443  0 04:39 pts/1    00:00:00 grep --color=auto vpp

이어서, dpdk bind 설정을 해 준다.
$ ifconfig enp1s0 down $ ifconfig enp4s0 down $ dpdk-devbind.py --bind=uio_pci_generic enp1s0 $ dpdk-devbind.py --bind=uio_pci_generic enp4s0

sudo service vpp restart
  ->  설정이 변경되었으니, vpp daemon을 재구동시킨다.

$ sudo vi /etc/vpp/startup.conf
  -> 먼저 vpp config file 맨 끝에 아래 내용(vpp session 설정)을 추가하도록 한다.
...
session {
   enable
   use-app-socket-api
}
~

$ sudo service vpp restart
  ->  설정이 변경되었으니, vpp daemon을 재구동시킨다.

sudo vppctl
  -> 이어서 network 설정을 진행하도록 한다. 참고로, nginx server는 WAN port를 통해 통신되도록 설정하기로 하자.

vpp# show interface 
              Name               Idx    State  MTU (L3/IP4/IP6/MPLS)     Counter          Count     
GigabitEthernet1/0/0              1     down         9000/0/0/0     
GigabitEthernet4/0/0              2     down         9000/0/0/0     
local0                            0     down          0/0/0/0      
 
vpp# set interface state GigabitEthernet1/0/0 up
vpp# set interface state GigabitEthernet4/0/0 up
vpp# set int ip address GigabitEthernet4/0/0 192.168.8.200/24  <= wan port

vpp# show int addr
GigabitEthernet1/0/0 (up):
GigabitEthernet4/0/0 (up):
  L3 192.168.8.200/24
local0 (dn):


3.4 NGINX 올리기
VPP 설정이 완료되었으니, 다음으로 할 일은 아래 그림 우측과 같은 형태로 nginx를 구동시키는 것이다.

[그림 3.6] Linux kernel 스택과 VPP Host 스택의 비교 [출처 - 참고문헌 17]

<nginx build 하기>
먼저, nginx source를 내려 받아 build & install 과정을 진행한다.

$ sudo apt install libpcre3-dev zlib1g-dev

$ git clone https://github.com/nginx/nginx
$ cd nginx

$ auto/configure
$ make
$ sudo make install

$ ls -l /usr/local/nginx/sbin/nginx
-rwxr-xr-x 1 root root 4052480 Apr  8 09:47 /usr/local/nginx/sbin/nginx

$ ls -la /usr/local/nginx/
total 24
drwxr-xr-x  6 root root 4096 Apr  8 09:47 .
drwxr-xr-x 12 root root 4096 Apr  8 09:47 ..
drwxr-xr-x  2 root root 4096 Apr  8 09:47 conf
drwxr-xr-x  2 root root 4096 Apr  8 09:47 html
drwxr-xr-x  2 root root 4096 Apr  8 09:47 logs
drwxr-xr-x  2 root root 4096 Apr  8 09:47 sbin
  -> OK, nginx가 정상적으로 설치되었다.

<VPP Host Stack 위에서 nginx를 구동시키기>
마지막으로 vcl.conf 파일을 하나 만든 후, VCL & LD_PRELOAD 환경하에서 nginx를 구동시켜 보도록 하자.

$ vi /etc/vpp/vi vcl.conf
  -> 먼저 아래와 같은 내용이 포함된 vcl.conf  파일을 생성한다.
vcl {
  heapsize 64M
  segment-size 4000000000
  add-segment-size 4000000000
  rx-fifo-size 4000000
  tx-fifo-size 4000000
  app-socket-api /var/run/vpp/app_ns_sockets/default
}
~

$ sudo taskset -c 1-4 sh -c "LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libvcl_ldpreload.so VCL_CONFIG=/etc/vpp/vcl.conf /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf"
  -> 위와 같이 좀 복잡한 명령어를 실행하여 nginx를 구동시키자.

$ ps -ef|grep nginx
root        4170       1  0 12:28 ?        00:00:00 nginx: master process /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
nobody      4171    4170 99 12:28 ?        00:00:12 nginx: worker process
chyi        4173    4026  0 12:29 pts/3    00:00:00 grep --color=auto nginx

자, 모든 준비가 끝났다. PC에서 VPP 기반 web server로 제대로 연결되는지 확인해 보자.

PC(web browser/192.168.8.156) ---> VPP WNA(192.168.8.100) ---> Host Stack -> NGINX


[그림 3.7] NGINX web page 출력

와우~ 신기하게도 (정말로) web page가 뜬다. 😍
📌 위의 연결이 실제로 vpp host stack을 통해 연결되고 있는 것인지를 확인하기 위해서는, show session verbose 명령을 이용하면 된다(연결된 세션 수를 확인할 수 있음).

top -H 명령을 실행해 보니, vpp_main은 물론이고, nginx 또한 CPU 점유율이 100%에 육박한다(DPDK & VPP 기반으로 동작한다는 얘기).


[그림 3.8] VPP & Nginx cpu 사용률 확인

아, 그런데, ... 세상에는 다양한 서버가 존재한다.
동일한 원리로, sshd를 vpp host stack 상에서 구동시켜 보려고 했으나, (설정에 문제가 있는지) 제대로 동작하지 않는다. VPP Host Stack은 (단정할 수는 없으나 아직까지는) 임의의 application을 구동시킬 수 있는 일반적인 방법은 아닌듯 싶다. 😂 시간을 두고 좀 더 고민해 보아야 겠다.

3.5 F-Stack 이야기
세상에는 참으로 재밌는 project가 많이 있다. DPDK가 고성능을 낼 수 있는 framework인 것은 맞지만, linux kernel stack을 bypass하는 관계로, 기존에 사용하던 다양한 application(server)을 재 사용하기 위해서는 linux kernel stack에 상응하는 것을 새로 구현해 주어야 하는 부담이 남는다. 아래 project는 FreeBSD의 tcp/ip stack을 사용자 영역에서 동작하도록 porting하고, 이를 DPDK 위에 올려 사용하도록 해주는 open source project이다. 어찌보면 앞서 소개한 VPP Host Stack과 동일한 역할을 하는 녀석인데, 앞으로 깊이 들여다 보아야 겠다. 😍



[그림 3.9] F-Stack(FreeBSD Stack on DPDK)


To be continued ...


4. References
<DPDK>
[1] https://doc.dpdk.org/guides/linux_gsg/
[2] https://doc.dpdk.org/guides/prog_guide/
[3] https://media.frnog.org/FRnOG_28/FRnOG_28-3.pdf
[4] https://medium.com/google-cloud/forwarding-over-100-mpps-with-fd-io-vpp-on-x86-62b9447da554
[5] https://embedx.medium.com/introduction-to-dpdk-architecture-and-principles-40db9a61a6f5
[6] https://s3.us-east-2.amazonaws.com/university-video-cdn/2_DPDK/1_DPDK_101/DPDK_101.pdf
[7] AN ARCHITECTURAL APPROACH FOR MITIGATING NEXT-GENERATION DENIAL OF SERVICE ATTACKS by CODY DOUCETTE
<Suricata>
[9] https://www.kubelynx.com/article/detect-dos-attack-using-custom-rule-file-suricata
<SmartNIC>
[10] https://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=8789414
[11] https://www.napatech.com/support/resources/solution-descriptions/scaling-suricata-performance-to-100-gbps-with-napatech-smartnics/
<FD.io VPP>
[12] https://wiki.fd.io/images/b/b6/Fcoras-vpp-hoststack-kc-na18.pdf
[13] https://wiki.fd.io/images/f/f2/Vpp-hoststack.pdf
[14] https://wiki.fd.io/view/VPP/HostStack
[15] https://wiki.fd.io/view/VPP/HostStack/LDP/nginx
[16] https://wiki.fd.io/images/0/08/Using_vpp_as_envoys_network_stack.pdf
[17] https://dataplane-stack.docs.arm.com/en/nw-ds-2023.05.31/user_guide/ssl_reverse_proxy.html

[18] https://docs.ngkore.com/blogs/dpdk/dpdk.html#hardware-requirements
  -> Cool site~
[19] And, Google~


Slowboot
FastPacket Company