스터디/Cilium

[Cilium] eBPF 기반 NAT Masquerading 동작 및 ip-masq-agent를 통한 SNAT 제외 확인하기

안녕유지 2025. 8. 3. 02:57
Cloudnet Cilium 3주차 스터디를 진행하며 정리한 글입니다.



이번 포스팅에서는 Cilium이 제공하는 Masquerading 기능에 대해 살펴보고, eBPF 기반 vs iptables 기반의 차이, 그리고 이를 실습으로 어떻게 확인할 수 있는지를 정리합니다.

 

Masquerading

Kubernetes Pod에 할당되는 IP는 일반적으로 사설 IP 주소입니다. 이들은 공용 인터넷이나 사내 외부 네트워크에서 직접 라우팅이 불가능하므로, 외부로 나가는 패킷의 소스 IP를 변경(Masquerade)해야 합니다.

Cilium은 클러스터 외부로 나가는 트래픽의 경우, Pod의 IP를 노드의 IP로 자동 변환(Masquerade) 하여 라우팅을 가능하게 만듭니다.

 

Masquerading 기능을 끄고 싶다면 Helm 설치 시 아래 옵션을 설정합니다

--set enable-ipv4-masquerade=false
--set enable-ipv6-masquerade=false

 

Masquerading 예외: Native Routing이 가능한 경우

 

특정 CIDR 대역에서는 Pod IP가 그대로 라우팅 가능할 수 있습니다.

(Native Routing에 대해 더 알고 싶으시다면 다음 포스팅을 확인해주세요 : https://hellouz818.tistory.com/82)

 

[Cilium] Cilium Native-Routing 모드로 Pod 통신 확인하기

Cloudnet Cilium 3주차 스터디를 진행하며 정리한 글입니다. 쿠버네티스 환경에서 Cilium을 CNI로 사용할 경우, 네트워크 패킷이 노드 간 어떻게 전달되는지에 따라 다양한 데이터 경로 설정이 존재합

hellouz818.tistory.com

 

이 경우 불필요한 Masquerading을 방지하기 위해 Cilium은 다음 옵션을 제공합니다. 이 설정에 포함된 목적지로 향하는 패킷은 Masquerading 되지 않습니다.

--set ipv4-native-routing-cidr=10.0.0.0/8
--set ipv6-native-routing-cidr=fd00::/100

 

 

Cilium은 두 가지 방식으로 Masquerading을 지원합니다.

1. eBPF Based

Cilium의 기본이자 권장 방식이며, bpf.masquerade=true 설정 시 활성화됩니다.

  • BPF Host-Routing 모드도 함께 활성화됩니다.
  • 기본적으로 ipv4-native-routing-cidr 범위를 벗어난 IP 주소로 향하는 포드의 모든 패킷은 Masquerading되지만, 다른 (클러스터) 노드(Node IP)로 향하는 패킷은 제외됩니다.
  • eBPF 마스커딩이 활성화되면 포드에서 클러스터 노드의 External IP로의 트래픽도 Masquerading되지 않습니다.
  • eBPF 프로그램이 패킷이 나가는 장치(eth0, eth1 등)에서 실행되어야 동작합니다.
  • 좀 더 정교한 설정은 (Cilium 의 eBPF 구현) ip-masq-agent 를 통해서 가능합니다.

 

root@k8s-ctr:~# cilium config view  | grep ipv4-native-routing-cidr
ipv4-native-routing-cidr                          172.20.0.0/16

# 노드 IP로 통신 시 확인
root@k8s-ctr:~# tcpdump -i eth1 icmp -nn
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), snapshot length 262144 bytes
02:21:21.808133 IP 172.20.1.137 > 192.168.10.101: ICMP echo request, id 49, seq 1, length 64
02:21:21.808561 IP 192.168.10.101 > 172.20.1.137: ICMP echo reply, id 49, seq 1, length 64

root@k8s-ctr:~# kubectl exec -it curl-pod -- ping 192.168.10.101
PING 192.168.10.101 (192.168.10.101) 56(84) bytes of data.
64 bytes from 192.168.10.101: icmp_seq=1 ttl=63 time=0.626 ms
^C
--- 192.168.10.101 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.626/0.626/0.626/0.000 ms

 

2. iptables Based

레거시 방식이며, 모든 커널에서 동작 가능합니다. 특별한 사유가 없다면 eBPF 방식 사용을 권장합니다.

  • 모든 비-Cilium 네트워크 디바이스로 나가는 트래픽에 대해 Masquerading 수행합니다.

 

Masquerading 실습

현재 구성한 환경은 다음과 같습니다.

 

설정 및 통신이 가능한지 확인해보겠습니다.

# 현재 설정 확인
root@k8s-ctr:~# kubectl exec -it -n kube-system ds/cilium -c cilium-agent  -- cilium status | grep Masquerading
Masquerading:            BPF   [eth0, eth1]   172.20.0.0/16 [IPv4: Enabled, IPv6: Disabled]

root@k8s-ctr:~# cilium config view  | grep ipv4-native-routing-cidr
ipv4-native-routing-cidr                          172.20.0.0/16

# 통신 확인
root@k8s-ctr:~# kubectl exec -it curl-pod -- curl -s webpod | grep Hostname
Hostname: webpod-7f475cbd84-z8qhj

 

워커노드 및 라우터와 통신 확인해보겠습니다.

# router와 k8s-ctr에서 tcpdump
root@k8s-ctr:~# tcpdump -i eth1 icmp -nn
root@router:~# tcpdump -i eth1 icmp -nn

### ping 테스트 (192.168.10.101)
# source ip로 바뀌지 않고 Pod ip로 나오는것 확인
root@k8s-ctr:~# kubectl exec -it curl-pod -- ping 192.168.10.101
PING 192.168.10.101 (192.168.10.101) 56(84) bytes of data.
64 bytes from 192.168.10.101: icmp_seq=1 ttl=63 time=2.07 ms
64 bytes from 192.168.10.101: icmp_seq=2 ttl=63 time=0.455 ms

# k8s-ctr
root@k8s-ctr:~# tcpdump -i eth1 icmp -nn
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), snapshot length 262144 bytes
02:29:00.692423 IP 172.20.1.137 > 192.168.10.101: ICMP echo request, id 61, seq 1, length 64
02:29:00.693537 IP 192.168.10.101 > 172.20.1.137: ICMP echo reply, id 61, seq 1, length 64
02:29:01.692528 IP 172.20.1.137 > 192.168.10.101: ICMP echo request, id 61, seq 2, length 64
02:29:01.692932 IP 192.168.10.101 > 172.20.1.137: ICMP echo reply, id 61, seq 2, length 64

# router
root@router:~# tcpdump -i eth1 icmp -nn


### ping 테스트 (192.168.10.200)
# 라우터로 ping 했을 시 source ip가 control plane의 노드 ip로 바뀜
root@k8s-ctr:~# kubectl exec -it curl-pod -- ping 192.168.10.200
PING 192.168.10.200 (192.168.10.200) 56(84) bytes of data.
64 bytes from 192.168.10.200: icmp_seq=1 ttl=63 time=0.842 ms
64 bytes from 192.168.10.200: icmp_seq=2 ttl=63 time=0.622 ms
^C
--- 192.168.10.200 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 0.622/0.732/0.842/0.110 ms

# k8s-ctr
root@k8s-ctr:~# tcpdump -i eth1 icmp -nn
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), snapshot length 262144 bytes
02:30:32.057470 IP 192.168.10.100 > 192.168.10.200: ICMP echo request, id 52713, seq 1, length 64
02:30:32.058044 IP 192.168.10.200 > 192.168.10.100: ICMP echo reply, id 52713, seq 1, length 64
02:30:33.058409 IP 192.168.10.100 > 192.168.10.200: ICMP echo request, id 52713, seq 2, length 64
02:30:33.058956 IP 192.168.10.200 > 192.168.10.100: ICMP echo reply, id 52713, seq 2, length 64

# router
root@router:~# tcpdump -i eth1 icmp -nn
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), snapshot length 262144 bytes
02:30:32.055845 IP 192.168.10.100 > 192.168.10.200: ICMP echo request, id 52713, seq 1, length 64
02:30:32.055992 IP 192.168.10.200 > 192.168.10.100: ICMP echo reply, id 52713, seq 1, length 64
02:30:33.056807 IP 192.168.10.100 > 192.168.10.200: ICMP echo request, id 52713, seq 2, length 64
02:30:33.056907 IP 192.168.10.200 > 192.168.10.100: ICMP echo reply, id 52713, seq 2, length 64

 

 

이 실습을 통해 다음과 같은 eBPF 기반 cilium의 Masquerading의 기본 동작 원리를 확인하였습니다.

  • Pod → 같은 클러스터 노드: Masquerading ❌ (Pod IP 그대로)
  • Pod → 외부 네트워크: Masquerading ⭕

 

ip-masq-agent 설정을 통한 Masquerading 예외처리

하지만 항상 해당 규칙이 적용되지 않을 수 있습니다. Cilium의 eBPF 기반 ip-masq-agent**를 활용해, 특정 CIDR 대역에 대해 Masquerading을 예외 처리(nonMasquerade) 하는 설정을 적용해보겠습니다.

 

기본적으로 Pod → 외부 네트워크로 향하는 패킷은 SNAT(Masquerading) 처리됩니다.

하지만 ip-masq-agent를 사용하면 특정 CIDR로 향하는 트래픽에 대해 Masquerading을 하지 않도록 설정 가능하고, 이를 통해 사내망 통신 시 원래 Pod IP 보존하거나 Cilium ClusterMesh 등의 Native-Routing 환경에 필수 구성할 수 있습니다.

 

설정값이 없다면 다음 값이 기본값입니다.

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

 

Cilium Helm Chart를 통해 ip-masq-agent를 활성화하고, Masquerade 예외 CIDR을 지정합니다.

root@k8s-ctr:~# helm upgrade cilium cilium/cilium --namespace kube-system --reuse-values --set ipMasqAgent.enabled=true --set ipMasqAgent.config.nonMasqueradeCIDRs='{10.10.1.0/24,10.10.2.0/24}' --version 1.17.6
Release "cilium" has been upgraded. Happy Helming!
NAME: cilium
LAST DEPLOYED: Sun Aug  3 02:44:50 2025
NAMESPACE: kube-system
STATUS: deployed
REVISION: 3
TEST SUITE: None
NOTES:
You have successfully installed Cilium with Hubble Relay and Hubble UI.

Your release version is 1.17.6.

For any further help, visit https://docs.cilium.io/en/v1.17/gettinghelp

root@k8s-ctr:~# kubectl get cm -n kube-system ip-masq-agent -o yaml | yq
{
  "apiVersion": "v1",
  "data": {
    "config": "{\"nonMasqueradeCIDRs\":[\"10.10.1.0/24\",\"10.10.2.0/24\"]}"
  },
  "kind": "ConfigMap",
  "metadata": {
    "annotations": {
      "meta.helm.sh/release-name": "cilium",
      "meta.helm.sh/release-namespace": "kube-system"
    },
    "creationTimestamp": "2025-08-02T17:44:52Z",
    "labels": {
      "app.kubernetes.io/managed-by": "Helm"
    },
    "name": "ip-masq-agent",
    "namespace": "kube-system",
    "resourceVersion": "17665",
    "uid": "c513625c-0ee9-4103-821d-5b9d06d9be79"
  }
}

root@k8s-ctr:~# cilium config view  | grep -i ip-masq
enable-ip-masq-agent                              true

root@k8s-ctr:~# kubectl -n kube-system exec ds/cilium -c cilium-agent -- cilium-dbg bpf ipmasq list

IP PREFIX/ADDRESS
10.10.2.0/24
169.254.0.0/16
10.10.1.0/24

 

 

 

통신을 확인해보겠습니다.

 

# router와 k8s-ctr에서 tcpdump
root@k8s-ctr:~# tcpdump -i eth1 icmp -nn
root@router:~# tcpdump -i eth1 icmp -nn

### ping 테스트 (192.168.10.200)
# 라우터로 ping 했을 시 source ip가 control plane의 노드 ip로 바뀜
root@k8s-ctr:~# kubectl exec -it curl-pod -- curl -s 10.10.1.200

# k8s-ctr
root@k8s-ctr:~# tcpdump -i eth1 tcp port 80 -nnq
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), snapshot length 262144 bytes
02:51:15.016042 IP 172.20.1.137.32822 > 10.10.1.200.80: tcp 0
02:51:16.019373 IP 172.20.1.137.32822 > 10.10.1.200.80: tcp 0
02:51:17.043441 IP 172.20.1.137.32822 > 10.10.1.200.80: tcp 0

# router
root@router:~# tcpdump -i eth1 tcp port 80 -nnq
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), snapshot length 262144 bytes
02:51:15.014678 IP 172.20.1.137.32822 > 10.10.1.200.80: tcp 0
02:51:16.017940 IP 172.20.1.137.32822 > 10.10.1.200.80: tcp 0
02:51:17.041780 IP 172.20.1.137.32822 > 10.10.1.200.80: tcp 0

# 노드 cidr 미리 확인
root@k8s-ctr:~# kubectl get ciliumnode -o json
{
    "apiVersion": "v1",
    "items": [
        {
            "apiVersion": "cilium.io/v2",
            "kind": "CiliumNode",
            "metadata": {
                "creationTimestamp": "2025-08-02T16:23:05Z",
                "generation": 3,
                "labels": {
                    "beta.kubernetes.io/arch": "arm64",
                    "beta.kubernetes.io/os": "linux",
                    "kubernetes.io/arch": "arm64",
                    "kubernetes.io/hostname": "k8s-ctr",
                    "kubernetes.io/os": "linux",
                    "node-role.kubernetes.io/control-plane": "",
                    "node.kubernetes.io/exclude-from-external-load-balancers": ""
                },
                "name": "k8s-ctr",
                "ownerReferences": [
                    {
                        "apiVersion": "v1",
                        "kind": "Node",
                        "name": "k8s-ctr",
                        "uid": "e63eb69a-ec11-49ba-bc89-356fbd2b2a9c"
                    }
                ],
                "resourceVersion": "7977",
                "uid": "faffb35a-8b7b-42cf-8d80-20b754efe1b3"
            },
            "spec": {
                "addresses": [
                    {
                        "ip": "192.168.10.100",
                        "type": "InternalIP"
                    },
                    {
                        "ip": "172.20.1.6",
                        "type": "CiliumInternalIP"
                    }
                ],
                "alibaba-cloud": {},
                "azure": {},
                "bootid": "9c657734-7345-469b-b58b-e65661fbd5c5",
                "encryption": {},
                "eni": {},
                "health": {},
                "ingress": {},
                "ipam": {
                    "podCIDRs": [
                        "172.20.1.0/24"
                    ],
                    "pools": {}
                }
            },
            "status": {
                "alibaba-cloud": {},
                "azure": {},
                "eni": {},
                "ipam": {
                    "operator-status": {}
                }
            }
        },
        {
            "apiVersion": "cilium.io/v2",
            "kind": "CiliumNode",
            "metadata": {
                "creationTimestamp": "2025-08-02T16:22:17Z",
                "generation": 3,
                "labels": {
                    "beta.kubernetes.io/arch": "arm64",
                    "beta.kubernetes.io/os": "linux",
                    "kubernetes.io/arch": "arm64",
                    "kubernetes.io/hostname": "k8s-w1",
                    "kubernetes.io/os": "linux"
                },
                "name": "k8s-w1",
                "ownerReferences": [
                    {
                        "apiVersion": "v1",
                        "kind": "Node",
                        "name": "k8s-w1",
                        "uid": "08d1bd38-1069-4406-a358-07e0373d5d55"
                    }
                ],
                "resourceVersion": "7734",
                "uid": "45a1e437-c8e8-4cb0-b366-8bff756dcc5a"
            },
            "spec": {
                "addresses": [
                    {
                        "ip": "192.168.10.101",
                        "type": "InternalIP"
                    },
                    {
                        "ip": "172.20.0.48",
                        "type": "CiliumInternalIP"
                    }
                ],
                "alibaba-cloud": {},
                "azure": {},
                "bootid": "b8e727ad-d9e6-4f76-9dc0-7d7bbbaf9059",
                "encryption": {},
                "eni": {},
                "health": {},
                "ingress": {},
                "ipam": {
                    "podCIDRs": [
                        "172.20.0.0/24"
                    ],
                    "pools": {}
                }
            },
            "status": {
                "alibaba-cloud": {},
                "azure": {},
                "eni": {},
                "ipam": {
                    "operator-status": {}
                }
            }
        }
    ],
    "kind": "List",
    "metadata": {
        "resourceVersion": ""
    }
}

# router 에 static route 설정 : 아래 노드별 PodCIDR에 대한 static routing 설정
root@router:~# ip route add 172.20.1.0/24 via 192.168.10.100
root@router:~# ip route add 172.20.0.0/24 via 192.168.10.101
root@router:~# ip -c route | grep 172.20
172.20.0.0/24 via 192.168.10.101 dev eth1
172.20.1.0/24 via 192.168.10.100 dev eth1

### ping 테스트 (192.168.10.200)
root@k8s-ctr:~# kubectl exec -it curl-pod -- curl -s 10.10.1.200

# k8s-ctr
root@k8s-ctr:~# tcpdump -i eth1 tcp port 80 -nnq
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), snapshot length 262144 bytes
02:53:27.249647 IP 172.20.1.137.58272 > 10.10.1.200.80: tcp 0
02:53:27.250315 IP 10.10.1.200.80 > 172.20.1.137.58272: tcp 0
02:53:27.250454 IP 172.20.1.137.58272 > 10.10.1.200.80: tcp 0
02:53:27.250528 IP 172.20.1.137.58272 > 10.10.1.200.80: tcp 75
02:53:27.250892 IP 10.10.1.200.80 > 172.20.1.137.58272: tcp 0
02:53:27.254750 IP 10.10.1.200.80 > 172.20.1.137.58272: tcp 256
02:53:27.254819 IP 172.20.1.137.58272 > 10.10.1.200.80: tcp 0
02:53:27.255321 IP 172.20.1.137.58272 > 10.10.1.200.80: tcp 0
02:53:27.257197 IP 10.10.1.200.80 > 172.20.1.137.58272: tcp 0
02:53:27.257348 IP 172.20.1.137.58272 > 10.10.1.200.80: tcp 0

# router
root@router:~# tcpdump -i eth1 tcp port 80 -nnq
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), snapshot length 262144 bytes
02:53:27.248054 IP 172.20.1.137.58272 > 10.10.1.200.80: tcp 0
02:53:27.248271 IP 10.10.1.200.80 > 172.20.1.137.58272: tcp 0
02:53:27.248754 IP 172.20.1.137.58272 > 10.10.1.200.80: tcp 0
02:53:27.248755 IP 172.20.1.137.58272 > 10.10.1.200.80: tcp 75
02:53:27.248883 IP 10.10.1.200.80 > 172.20.1.137.58272: tcp 0
02:53:27.252663 IP 10.10.1.200.80 > 172.20.1.137.58272: tcp 256
02:53:27.253054 IP 172.20.1.137.58272 > 10.10.1.200.80: tcp 0
02:53:27.253677 IP 172.20.1.137.58272 > 10.10.1.200.80: tcp 0
02:53:27.255045 IP 10.10.1.200.80 > 172.20.1.137.58272: tcp 0
02:53:27.255564 IP 172.20.1.137.58272 > 10.10.1.200.80: tcp 0

 

이 실습을 통해 ip-masq-agent의 nonMasqueradeCIDRs 설정이 정상적으로 적용되어, 해당 CIDR 대역으로 가는 트래픽의 출발지 IP가 Masquerading 되지 않는지 확인할 수 있었습니다.

curl-pod에서 사내망 라우터 loopback 주소 (예: 10.10.1.200) 로 curl 요청하고 라우터와 k8s 노드에서 동시에 tcpdump 수행했습니다. 

패킷 캡처 결과, 출발지 IP는 Pod IP(172.20.1.137) Masquerading되지 않았습니다. 즉, SNAT 안 되었습니다. 

이는 라우터에 Pod CIDR에 대한 static route 가 존재하여 반환 경로가 정확히 설정되어 있고, ip-masq-agent에 10.10.1.0/24 대역이 nonMasqueradeCIDRs 로 지정되어 있기 때문에 Cilium은 해당 목적지로 가는 패킷에 대해 Masquerading 하지 않았습니다.