Cilium Masquerading (BPF-based vs iptables-based)

cilium에서는 마스커레이드를 BPF를 사용하거나 iptables를 사용할 수 있습니다. 마스커레이드는 클러스터 내부 파드(Pod)에서 외부 서비스(인터넷 등)로 나갈 때, 출발지 IP를 노드(Node)의 IP로 변경하여 응답 패킷이 돌아올 수 있도록 해 주는 기능입니다. bpf 마스커레이드를 설정하면 host routing도 bpf로 설정해야합니다. 

 

 

마스커레이드 설정 확인하기

kubectl exec -it -n kube-system ds/cilium -c cilium-agent  -- cilium status | grep Masquerading

 

기본적으로, 파드에서 보내는 모든 패킷 중 목적지가 ipv4-native-routing-cidr 범위 밖에 있는 IP 주소인 경우 마스커레이딩됩니다. 단, 클러스터의 노드 아이피는 제외입니다. 

cilium config view  | grep ipv4-native-routing-cidr

 

 

ebpf 테스트 환경 설정하기

 

외부 파드 대신 kind와 동일 테트워크 대역에 whoami 컨테이너를 띄워보도록 하겠습니다. 

docker run -d --rm --name nginx --network kind nginx:latest

 

이 컨테이너의 아이피 주소를 확인해보겠습니다.

docker inspect nginx | jq '.[0].NetworkSettings.Networks.kind.IPAddress' -r

 

컨테이너에 접속해서 tcpdump를 설치합니다.

docker exec nginx apt update
docker exec nginx apt install tcpdump

 

테스트 해보기

tcpdump를 켜둔 상태에서 nginx컨테이너를 호출해보겠습니다.

docker exec -it nginx bash
tcpdump -i eth0 -n port 80

 

이번에는 지난번 네이티브 라우팅 통신흐름때와 다르게 출발지 주소가 노드의 아이피로 되어있는 것을 볼 수 있습니다. 

2025.07.31 - [K8S/🔥 network study🔥] - [cilium] native routing 통신 흐름 확인해보기

root@49b71dac045f:/# tcpdump -i eth0 -n port 80
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
11:56:28.960087 IP 172.17.0.4.53938 > 172.17.0.6.80: Flags [S], seq 2618216972, win 64240, options [mss 1460,sackOK,TS val 1152497573 ecr 0,nop,wscale 7], length 0
11:56:28.960210 IP 172.17.0.6.80 > 172.17.0.4.53938: Flags [S.], seq 1746492749, ack 2618216973, win 65160, options [mss 1460,sackOK,TS val 2216167557 ecr 1152497573,nop,wscale 7], length 0
11:56:28.960318 IP 172.17.0.4.53938 > 172.17.0.6.80: Flags [.], ack 1, win 502, options [nop,nop,TS val 1152497573 ecr 2216167557], length 0
11:56:28.960601 IP 172.17.0.4.53938 > 172.17.0.6.80: Flags [P.], seq 1:76, ack 1, win 502, options [nop,nop,TS val 1152497574 ecr 2216167557], length 75: HTTP: HEAD / HTTP/1.1
11:56:28.960610 IP 172.17.0.6.80 > 172.17.0.4.53938: Flags [.], ack 76, win 509, options [nop,nop,TS val 2216167558 ecr 1152497574], length 0
11:56:28.961323 IP 172.17.0.6.80 > 172.17.0.4.53938: Flags [P.], seq 1:239, ack 76, win 509, options [nop,nop,TS val 2216167558 ecr 1152497574], length 238: HTTP: HTTP/1.1 200 OK
11:56:28.961419 IP 172.17.0.4.53938 > 172.17.0.6.80: Flags [.], ack 239, win 501, options [nop,nop,TS val 1152497574 ecr 2216167558], length 0
11:56:28.961684 IP 172.17.0.4.53938 > 172.17.0.6.80: Flags [F.], seq 76, ack 239, win 501, options [nop,nop,TS val 1152497575 ecr 2216167558], length 0
11:56:28.961741 IP 172.17.0.6.80 > 172.17.0.4.53938: Flags [F.], seq 239, ack 77, win 509, options [nop,nop,TS val 2216167559 ecr 1152497575], length 0
11:56:28.961812 IP 172.17.0.4.53938 > 172.17.0.6.80: Flags [.], ack 240, win 501, options [nop,nop,TS val 1152497575 ecr 2216167559], length 0

 

kubelet의 헬스체크 포트로 노드 아이피로 호출해보도록 하겠습니다.

클러스터의 노드 아이피가 목적지이기 때문에  출발지 아이피가 파드 아이피인것을 확인할 수 있습니다. 

root@kind-worker:/# tcpdump -i eth0 dst 172.17.0.4 and port 10248 -n
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
12:18:02.187353 IP 10.244.0.101.52844 > 172.17.0.4.10248: Flags [S], seq 4222611365, win 64240, options [mss 1460,sackOK,TS val 3687075721 ecr 0,nop,wscale 7], length 0
12:18:02.187471 IP 10.244.0.101.52844 > 172.17.0.4.10248: Flags [.], ack 2286795463, win 502, options [nop,nop,TS val 3687075722 ecr 3835880220], length 0
12:18:02.187594 IP 10.244.0.101.52844 > 172.17.0.4.10248: Flags [P.], seq 0:87, ack 1, win 502, options [nop,nop,TS val 3687075722 ecr 3835880220], length 87
12:18:02.187996 IP 10.244.0.101.52844 > 172.17.0.4.10248: Flags [.], ack 152, win 501, options [nop,nop,TS val 3687075722 ecr 3835880221], length 0
12:18:02.188188 IP 10.244.0.101.52844 > 172.17.0.4.10248: Flags [F.], seq 87, ack 152, win 501, options [nop,nop,TS val 3687075722 ecr 3835880221], length 0
12:18:02.188509 IP 10.244.0.101.52844 > 172.17.0.4.10248: Flags [.], ack 153, win 501, options [nop,nop,TS val 3687075723 ecr 3835880221], length 0

 

ipMasqAgent 설정하기

에이전트는 Fsnotify를 이용해 설정 파일의 변경 사항을 감지하므로, resyncInterval 옵션은 필요하지 않습니다.

이 에이전트는 설정 파일에서 지정된 다음 옵션들을 지원합니다:

  • nonMasqueradeCIDRs: 여기 CIDR 목록에 포함된 목적지로의 패킷은 마스커레이딩되지 않습니다.
  • masqLinkLocal: false인 경우 non-masquerade 목록에 169.254.0.0/16(RFC 3927 IPv4 링크 로컬 주소)가 추가됩니다. 
  • masqLinkLocalIPv6: false인 경우 non-masquerade 목록에 fe80::/10가 추가됩니다. 

설정 파일이 비어 있으면, 기본적으로 다음 CIDR 범위들이 마스커레이딩 예외로 설정됩니다

10.0.0.0/8  
172.16.0.0/12  
192.168.0.0/16  
100.64.0.0/10  
192.0.0.0/24  
192.0.2.0/24  
192.88.99.0/24  
198.18.0.0/15  
198.51.100.0/24  
203.0.113.0/24  
240.0.0.0/4

 

설정 예시 (ConfigMap 사용)

apiVersion: v1
kind: ConfigMap
metadata:
  name: ip-masq-agent
data:
  config: |
    nonMasqueradeCIDRs:
    - 10.0.0.0/8
    - 172.16.0.0/12
    - 192.168.0.0/16
    masqLinkLocal: true
 
적용 후 확인해보기
kubectl -n kube-system exec ds/cilium -- cilium-dbg bpf ipmasq list
 

iptables 기반 마스커레이드 설정하기 

기본적으로 iptables로 마스커레이드를 하게 되면 외부로 나가는 모든 트래픽에 마스커레이드가 되지만

특정 인터페이스에 대해서만도 지정할 수 있습니다. 

egress-masquerade-interfaces: eth+ ens+

 

목적지에 따라 어떤 인터페이스를 선택할지 정하게 하려면 아래의 설정을 키면 됩니다. 

enable-masquerade-to-route-source: "true"

 

확실하게 확인하기 위해 새로 클러스터를 생성해보겠습니다. 

helm install cilium cilium/cilium --version 1.17.6 --namespace kube-system \
  --set k8sServiceHost=auto --set ipam.mode="kubernetes" \
  --set k8s.requireIPv4PodCIDR=true --set ipv4NativeRoutingCIDR=10.244.0.0/16 \
  --set routingMode=native --set autoDirectNodeRoutes=true --set endpointRoutes.enabled=true \
  --set kubeProxyReplacement=true --set bpf.masquerade=false \
  --set endpointHealthChecking.enabled=false --set healthChecking=false \
  --set operator.replicas=1 --set debug.enabled=true

 

iptables 룰 확인해보기

ebpf와 다르게 새로 생긴 건 CILIUM_POST_nat 테이블이 새로 생겼습니다. 

 

룰을 자세히 보면 이렇습니다. 

-A CILIUM_POST_nat -s 10.244.2.0/24 -m set --match-set cilium_node_set_v4 dst -m comment --comment "exclude traffic to cluster nodes from masquerade" -j ACCEPT
-A CILIUM_POST_nat -s 10.244.2.0/24 ! -d 10.244.0.0/16 ! -o cilium_+ -m comment --comment "cilium masquerade non-cluster" -j MASQUERADE
-A CILIUM_POST_nat -m mark --mark 0xa00/0xe00 -m comment --comment "exclude proxy return traffic from masquerade" -j ACCEPT
-A CILIUM_POST_nat -s 127.0.0.1/32 -o lxc+ -m comment --comment "cilium host->cluster from 127.0.0.1 masquerade" -j SNAT --to-source 10.244.2.122

 

 

첫 번째 룰 

  • -A CILIUM_POST_nat -s 10.244.2.0/24 -m set --match-set cilium_node_set_v4 dst -m comment --comment "exclude traffic to cluster nodes from masquerade" -j ACCEPT
  • 10.244.2.0/24 에서 시작하며 내부 노드로 트래픽을 보내는 경우 마스커레이드를 하지 않는다. 

--match-set cilium_node_set_v4 의 리스트는 ipset 명령어로 리스트를 확인할 수 있습니다.

ipset list cilium_node_set_v4

 

여기서 보이는 아이피는 노드 아이피 입니다. 

 

노드 아이피 리스트 

 

두 번째 룰

  • -A CILIUM_POST_nat -s 10.244.2.0/24 ! -d 10.244.0.0/16 ! -o cilium_+ -m comment -comment "cilium masquerade non-cluster" -j MASQUERADE
  • 10.244.2.0/24 (노드 내의 파드 아이피)에서 목적지가 cilium_ 으로 시작하는 인터페이스가 아니면서 10.244.0.0/16 (클러스터 전체의 파드 대역)대역이 아닌 곳으로 통신하려고 할 때  노드의 아이피로 마스커레이드를 합니다. 

 

세 번째 룰

  • -A CILIUM_POST_nat -m mark --mark 0xa00/0xe00 -m comment - comment "exclude proxy return traffic from masquerade" -j ACCEPT
  • Cilium L7 프록시(예: Envoy)를 통과한 리턴 트래픽은 MASQUERADE하지 않음. 프록시 리턴 트래픽은 이미 적절히 처리되었기 때문에 추가 NAT를 피하려는 목적입니다.

 

네 번째 룰

  • -A CILIUM_POST_nat -s 127.0.0.1/32 -o lxc+ -m comment --comment "cilium host>cluster from 127.0.0.1 masquerade" -j SNAT --to-source 10.244.2.122
  • 호스트에서 루프백(127.0.0.1) 인터페이스를 통해 lxc+ 로 시작하는 인터페이스(Pod로 가는 트래픽)로 가는 경우 NAT 처리하여 출발지를 해당 노드의 cilium_host 인터페이스로 바꿉니다.