TT Lab
블로그 시작하기
배우기 러닝패스 코스

리눅스 커널과 eBPF

드라이버 앞에서 패킷을 세고 떨어뜨린다

TT Lab 에서 이어서 보기

이 실습은 VM 에서 돕니다

우분투 24.04 VM 한 대입니다. 네트워크 네임스페이스와 veth 를 만들고 장치에 BPF 프로그램을 붙이는 일은 CAP_NET_ADMIN 과 CAP_BPF 가 필요합니다. 실습 파드에는 그 권한이 없습니다. VM 의 바깥 연결(enp1s0)은 건드리지 않고 veth 에만 붙이니, 잘못 붙여도 터미널이 끊기지 않습니다.

목표

veth 한 쌍으로 작은 네트워크를 만들고, 호스트 쪽 장치의 XDP 자리에 패킷을 프로토콜별로 세는 프로그램을 드라이버 모드로 붙입니다. 그 프로그램을 ICMP 만 떨어뜨리게 고치고, 반대 방향(송신)은 tc 의 egress 자리에서 셉니다. 마지막으로 bpftool net 이 보여 주는 그림을 Cilium 이 실제로 쓰는 자리와 이어 봅니다.

왜 중요한가

XDP 는 드라이버가 패킷을 받자마자, 커널이 소켓 버퍼(sk_buff)를 만들기도 전에 프로그램을 돌립니다. 그래서 DDoS 를 거르는 장비와 로드 밸런서가 이 자리를 씁니다. 대신 들어오는 쪽만 보고, 장치가 드라이버 모드를 지원하지 않으면 generic 모드로 붙어 이점이 대부분 사라집니다. tc 는 조금 늦은 자리지만 들어오고 나가는 양쪽을 다 보고 sk_buff 의 정보를 쓸 수 있습니다. Cilium 은 파드의 veth 에 tc(최신 커널에서는 tcx)로 정책을 걸고, NodePort 가속에는 XDP 를 씁니다. bpftool net show 한 줄을 읽을 줄 알면 Cilium 노드에서 무엇이 어디에 붙어 있는지 직접 확인할 수 있습니다.

단계

  1. 네임스페이스 kx-peer 와 veth kx-h(호스트, 10.88.0.1/24)·kx-p(kx-peer 안, 10.88.0.2/24)를 만드세요.
  2. 프로토콜별로 세는 XDP 프로그램 /root/kx/xdp/xdp_count.bpf.c 를 /root/kx/xdp/xdp_count.bpf.o 로 컴파일하세요.
  3. 그 프로그램을 kx-h 에 드라이버 모드로 붙이세요.
  4. kx-peer 에서 ping 을 다섯 번 보낸 뒤 맵을 JSON 으로 /root/kx/xdp/04-map.json 에 저장하세요.
  5. 같은 프로그램을 ICMP 만 떨어뜨리게 고쳐 다시 붙이세요.
  6. 송신을 세는 tc 프로그램 /root/kx/xdp/tc_count.bpf.c 를 kx-h 의 egress 에 붙이세요.
  7. bpftool net show dev kx-h 의 결과를 /root/kx/xdp/07-net.txt 에 저장하세요.
  8. Cilium 과의 연결을 /root/kx/xdp/08-report.md 에 정리하세요.

참고

작은 네트워크를 만든다

네트워크 네임스페이스 kx-peer 를 만들고 veth 한 쌍을 만들어 한 끝 kx-h 는 호스트에 10.88.0.1/24, 다른 끝 kx-p 는 kx-peer 안에 10.88.0.2/24 로 두고 모두(kx-peer 의 lo 포함) 올리세요. kx-peer 에서 10.88.0.1 의 22번 포트로 TCP 연결이 되어야 합니다.

ip netns add, ip link add … type veth peer name …, ip link set … netns … 순서입니다. 네임스페이스 안의 설정은 ip -n kx-peer … 또는 ip netns exec kx-peer … 로 합니다. TCP 확인은 ip netns exec kx-peer bash -c 'echo > /dev/tcp/10.88.0.1/22' 로 할 수 있습니다.

패킷을 세는 XDP 프로그램

/root/kx/xdp/xdp_count.bpf.c 를 쓰세요. SEC("xdp") 함수 xdp_count 가 IPv4 패킷의 protocol 값을 키로 배열 맵 proto_count(256칸, 키 __u32, 값 __u64)를 1 늘리고 모든 패킷을 통과(XDP_PASS)시켜야 합니다. /root/kx/xdp/xdp_count.bpf.o 로 컴파일하세요.

패킷은 ctx->data 부터 ctx->data_end 까지입니다. 이더넷 머리와 IP 머리를 읽기 전에 각각 끝이 data_end 를 넘지 않는지 비교해야 검증기가 받아 줍니다. h_proto 는 네트워크 바이트 순서라 bpf_htons(ETH_P_IP) 와 비교합니다. 표본은 목적지 주소로 세는 프로그램이니 키를 바꾸세요.

드라이버 모드로 붙인다

xdp_count.bpf.o 를 kx-h 에 드라이버 모드(xdpdrv)로 붙이세요. 채점기는 bpftool net 에서 모드가 driver 인지 보고, kx-peer 에서 TCP 연결을 한 번 열어 맵의 TCP(6번) 칸이 오르는지 봅니다.

ip link set dev kx-h xdpdrv obj … sec xdp 입니다. xdpgeneric 으로 붙여도 패킷은 세어지지만, 드라이버가 아니라 그보다 늦은 자리(sk_buff 를 만든 뒤)에서 돕니다. bpftool net show dev kx-h 에서 driver 와 generic 을 구별하세요.

셈을 읽는다

kx-peer 에서 10.88.0.1 로 ping 을 다섯 번 보낸 뒤, 맵 proto_count 를 JSON 으로 /root/kx/xdp/04-map.json 에 저장하세요. ICMP(1번) 칸이 5 이상이어야 합니다.

bpftool map dump name proto_count -j 로 뽑습니다. 256칸 배열이라 0 인 칸도 다 나옵니다. 1번이 ICMP, 6번이 TCP, 17번이 UDP 입니다. 아직 아무것도 떨어뜨리지 않으니 ping 은 성공해야 합니다.

ICMP 만 떨어뜨린다

xdp_count.bpf.c 를 고쳐 프로토콜이 ICMP 이면 세고 나서 XDP_DROP 을, 나머지는 XDP_PASS 를 돌려주게 하고, 다시 컴파일해 kx-h 에 드라이버 모드로 다시 붙이세요. 함수와 맵 이름은 그대로 둡니다. kx-peer 에서 ping 은 실패하고 TCP 22 는 그대로 되어야 합니다.

ip->protocol == IPPROTO_ICMP 와 비교하려면 <linux/in.h> 가 필요합니다. 이미 붙어 있는 장치에 바꿔 붙이려면 ip -force link set … 입니다. 전부 떨어뜨리면 TCP 도 끊겨서 채점기가 알아챕니다.

나가는 쪽은 tc 에서 센다

/root/kx/xdp/tc_count.bpf.c 를 쓰세요. SEC("tc") 함수 tc_count 가 지나가는 패킷마다 한 칸짜리 배열 맵 egress_count 를 1 늘리고 TC_ACT_OK 를 돌려줘야 합니다. 컴파일해 kx-h 의 egress 에 direct-action 으로 붙이세요. 채점기는 호스트에서 10.88.0.2 로 ping 을 보내 값이 오르는지 봅니다.

tc 프로그램은 struct __sk_buff * 를 받습니다. 반환값 상수는 <linux/pkt_cls.h> 에 있습니다. 붙이기 전에 clsact qdisc 가 있어야 합니다. 호스트에서 보낸 ping 의 응답은 XDP 가 떨어뜨리므로 ping 자체는 실패하지만, 나가는 패킷은 tc 에서 세어집니다.

어디에 무엇이 붙었나

bpftool net show dev kx-h 의 출력을 /root/kx/xdp/07-net.txt 에 저장하세요. XDP 가 드라이버 모드로, tc 프로그램이 clsact/egress 에 붙어 있는 것이 보여야 합니다.

이 명령은 XDP·tc·tcx 부착을 한 화면에 보여 줍니다. 최신 커널에서 Cilium 이 쓰는 tcx 부착은 tc filter show 에는 보이지 않고 여기에만 보입니다. 지금 붙은 XDP 프로그램의 id 가 출력과 같은지도 보세요.

Cilium 의 자리와 잇는다

/root/kx/xdp/08-report.md 에 ## XDP 와 tc 가 보는 자리·## Cilium 은 어디에 붙이나·## generic 모드를 피하는 이유 세 절을 쓰세요. 본문에 tcx·bpftool net·XDP_DROP 이 나오고, 4단계 맵의 ICMP(1번) 칸 값을 그대로 인용해야 합니다.

ICMP 칸 값은 04-map.json 에서 key 가 1 인 항목의 formatted.value 입니다. 둘째 절에는 파드 veth 의 tc(tcx) 자리, 노드 바깥 장치의 XDP 자리를 나눠 적습니다. 셋째 절에는 sk_buff 를 만들기 전과 뒤의 차이를 적습니다.