IPAM이란 IP Address Management의 약자로, 네트워크의 IP 주소 할당을 자동화하고, 사용 상태를 추적하며, 충돌이나 낭비 없이 효과적으로 IP를 관리하는 시스템이다.
Cilium은 기본적으로 eBPF를 기반으로 한 고성능 네트워킹을 제공하는데, 이때 여러 IPAM 모드를 통해 다양한 IP 할당 정책 및 전략을 지원한다.
Cilium의 IPAM에는 다음과 같은 종류가 있다. 공식문서 참조
공식문서에 적혀있듯이 IPAM은 한번 설정한 이후 변경을 하지 않는 것을 권장한다.
라이브 환경에서 IPAM 모드를 변경하면 기존 워크로드의 지속적인 연결 중단이 발생할 수 있으며 IPAM 모드를 변경하는 가장 안전한 방법은 새로운 IPAM 구성으로 새로운 Kubernetes 클러스터를 설치하는 것이다.
각 IPAM에 대해 상세히 알아보자.
ipam:
mode: kubernetes
Kubernetes Host Scope IPAM 모드는 노드 단위 IP 풀을 사용하는 방식이다.
각 노드는 고유한 PodCIDR을 가지고 있으며, Cilium은 해당 범위 내에서 Pod에 IP를 할당한다.
Kubernetes의 kube-controller-manager
가 --allocate-node-cidrs
옵션을 통해 노드별 PodCIDR을 할당하면,
Cilium 에이전트는 v1.Node 리소스의 spec.podCIDR 또는 spec.podCIDRs 필드를 참조하여 IP를 배정한다.
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl cluster-info dump | grep -m 2 -E "cluster-cidr|service-cluster-ip-range"
"--service-cluster-ip-range=10.96.0.0/16",
"--cluster-cidr=10.244.0.0/16",
(⎈|HomeLab:N/A) root@k8s-ctr:~# cilium config view | grep kubernetes
ipam kubernetes
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.podCIDR}{"\n"}{end}'
k8s-ctr 10.244.0.0/24
k8s-w1 10.244.1.0/24
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl describe pod -n kube-system kube-controller-manager-k8s-ctr | grep kube-controller-manager -A3
...
Command:
kube-controller-manager
--allocate-node-cidrs=true
--authentication-kubeconfig=/etc/kubernetes/controller-manager.conf
--authorization-kubeconfig=/etc/kubernetes/controller-manager.conf
Kubernetes Host Scope IPAM설정에서 Sample Application을 배포하여 상세 사항을 확인해보자.
Hubble로 모니터링을 하기 전 알아두면 좋을 bpf trace 메시지를 코드와 함께 정리해본다. Cilium 관련 첫번째 글에서 분석한 Cilium 서비스 통신 코드 분석의 연장선이다.
ClusterIP를 받고, NAT가 수행되기 전 메시지에 해당한다.
//cilium/bpf/bpf_sock.c
static __always_inline int __sock4_xlate_fwd(struct bpf_sock_addr *ctx,
struct bpf_sock_addr *ctx_full,
const bool udp_only)
{
...
//서비스 매칭을 시도한다.
svc = lb4_lookup_service(&key, true);
if (!svc) {
/* Restore the original key's protocol as lb4_lookup_service
* has overwritten it.
*/
lb4_key_set_protocol(&key, protocol);
svc = sock4_wildcard_lookup_full(&key, in_hostns);
}
...
//pre-xlate-fwd 메시지를 보낸다.
send_trace_sock_notify4(ctx_full, XLATE_PRE_DIRECTION_FWD, dst_ip,
bpf_ntohs(dst_port));
...
}
NAT 직전에 pre-xlate-fwd 메시지가 보내지고 NAT(서비스 IP -> backend pod IP)가 실제 수행된다.
//cilium/bpf/bpf_sock.c
static __always_inline int __sock4_xlate_fwd(struct bpf_sock_addr *ctx,
struct bpf_sock_addr *ctx_full,
const bool udp_only)
{
...
//pre-xlate-fwd 메시지를 보낸다.
send_trace_sock_notify4(ctx_full, XLATE_PRE_DIRECTION_FWD, dst_ip,
bpf_ntohs(dst_port));
...
//backend 정보 수집
backend_id = backend_slot->backend_id;
backend = __lb4_lookup_backend(backend_id);
...
//post-xlate-fwd 메시지를 보낸다.
send_trace_sock_notify4(ctx_full, XLATE_POST_DIRECTION_FWD, backend->address,
bpf_ntohs(backend->port));
...
//NAT 수행
ctx->user_ip4 = backend->address;
ctx_set_port(ctx, backend->port);
}
RevNAT 변환 전, 현재 패킷의 목적지 IP/포트 상태와 함께 pre-xlate-rev 메시지를 보낸다.
//cilium/bpf/bpf_sock.c
static __always_inline int __sock4_xlate_rev(struct bpf_sock_addr *ctx,
struct bpf_sock_addr *ctx_full)
{
...
//pre-xlate-rev 메시지를 보낸다.
send_trace_sock_notify4(ctx_full, XLATE_PRE_DIRECTION_REV, dst_ip,
bpf_ntohs(dst_port));
...
//RevNAT 매핑 테이블을 조회한다.
val = map_lookup_elem(&cilium_lb4_reverse_sk, &key);
...
}
RevNAT 변환 후에 post-xlate-rev 메시지를 보내 패킷의 목적지 IP/포트가 원래 서비스 IP/포트로 바뀐 시점을 알린다.
//cilium/bpf/bpf_sock.c
static __always_inline int __sock4_xlate_rev(struct bpf_sock_addr *ctx,
struct bpf_sock_addr *ctx_full)
{
...
//RevNAT 매핑 테이블을 조회한다.
val = map_lookup_elem(&cilium_lb4_reverse_sk, &key);
...
//RevNAT 변환을 수행한다.
ctx->user_ip4 = val->address;
ctx_set_port(ctx, val->port);
//post-xlate-rev 메시지를 보낸다.
send_trace_sock_notify4(ctx_full, XLATE_POST_DIRECTION_REV, val->address,
bpf_ntohs(val->port));
return 0;
}
정책 통과 후 실제 목적지 Pod로 트래픽이 전달될 때 발생하는 이벤트이다.
//cilium/bpf/bpf_lxc.c
static __always_inline int
ipv4_policy(struct __ctx_buff *ctx, struct iphdr *ip4, __u32 src_label,
struct ipv4_ct_tuple *tuple_out, __s8 *ext_err, __u16 *proxy_port,
bool from_tunnel)
{
//1) 정책이 통과되고
if (verdict != CTX_ACT_OK || ret != CT_ESTABLISHED)
...
//2) Conntrack이 신규 생성되었으며
if (ret == CT_NEW)
...
//3) proxy로 redirect되지 않고, 바로 Pod로 전달하는 경로이면
if (*proxy_port > 0)
goto redirect_to_proxy;
//TRACE_TO_LXC 즉 to-endpoint 메시지를 보낸다.
send_trace_notify4(ctx, TRACE_TO_LXC, src_label, SECLABEL_IPV4, orig_sip,
LXC_ID, ifindex, trace.reason, trace.monitor);
...
}
참고로 TRACE 변수에 대한 값은 아래에서 확인 가능하다.
//cilium/pkg/monitoring/api/types.go
var TraceObservationPoints = map[uint8]string{
TraceToLxc: "to-endpoint",
TraceToNetwork: "to-network",
...
}
터널 모드가 아닐 경우에 한해서 패킷이 노드를 벗어나기 직전 TRACE_TO_NETWORK 메시지를 발생시킨다.
//cilium/bpf/bpf_lxc.c
static __always_inline int handle_ipv4_from_lxc(struct __ctx_buff *ctx, __u32 *dst_sec_identity,
__s8 *ext_err)
{
...
//터널 모드 조건 끝
#endif /* TUNNEL_MODE */
//터널 모드가 아닐 경우 + 호스트 라우팅이 되어야 하는 경우
if (is_defined(ENABLE_HOST_ROUTING)) {
int oif = 0;
//라우팅 테이블을 조회해서 목적지 IP에 맞는 인터페이스 번호를 채워넣는다.
ret = fib_redirect_v4(ctx, ETH_HLEN, ip4, false, false, ext_err, &oif);
if (fib_ok(ret))
//TRACE_TO_NETWORK 즉 to-network 메시지를 보낸다.
send_trace_notify(ctx, TRACE_TO_NETWORK, SECLABEL_IPV4,
*dst_sec_identity, TRACE_EP_ID_UNKNOWN, oif,
trace.reason, trace.monitor, bpf_htons(ETH_P_IP));
return ret;
}
//커널 네트워크 스택으로 패킷을 넘긴다.
goto pass_to_stack;
}
Sample Application을 배포하여 통신 모니터링을 진행해본다.
kubernetes IPAM 모드에서는 노드마다 Pod CIDR을 갖고 있으며, 같은 노드 내 Pod 간 통신 시 Cilium은 해당 노드의 인터페이스와 endpoint map만으로 로컬 라우팅을 처리한다.
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
curl-pod 1/1 Running 0 5d23h 10.244.0.164 k8s-ctr <none> <none>
webpod-697b545f57-hkjzn 1/1 Running 0 5d23h 10.244.0.111 k8s-ctr <none> <none>
webpod-697b545f57-qz7vp 1/1 Running 0 5d23h 10.244.1.47 k8s-w1 <none> <none>
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it curl-pod -- curl 10.244.0.111 | grep Hostname
Hostname: webpod-697b545f57-hkjzn
curl-pod에서 같은 노드 내에 위치한 webpod를 호출시키는 상황을 hubble로 관측하면 다음과 같다.
통신 시나리오:
curl-pod → ClusterIP(web-svc) → webpod (같은 노드 상의 Pod)
IPAM 모드: kubernetes
→ 각 노드에 PodCIDR가 할당되며, 이 범위 내에서 Pod IP가 고정적으로 할당됨.
Routing : Native Routing
모니터링 흐름:
1) curl-pod에서 webpod의 ClusterIP 호출
2) pre-xlate-fwd
3) post-xlate-fwd
4) to-endpoint
5) pre-xlate-rev
❌ cf) post-xlate-rev 없음
왜 없을까? 에 대해 chatGPT에게 물어본 결과 아래와 같은 답변을 얻었다.
kubernetes IPAM 모드에서 Pod 간 통신은, 노드가 다르더라도 Cilium이 IP 경로를 직선적으로 계산할 수 있다면, 응답 시 Reverse NAT 처리를 생략할 수 있습니다.
이때 Cilium은 cilium_lb4_reverse_sk에 항목을 생성하지 않으며, 이에 따라 post-xlate-rev 트레이스 이벤트가 발생하지 않습니다. 이는 같은 노드 간 통신뿐 아니라, 다른 노드 간 통신에서도 동일하게 발생할 수 있는 최적화 동작입니다.
통신 시나리오:
curl-pod → ClusterIP(web-svc) → webpod (다른 노드 상의 Pod)
Routing : Native Routing
모니터링 흐름:
1) curl-pod에서 webpod의 ClusterIP 호출
2) pre-xlate-fwd
3) post-xlate-fwd
4) to-network
5) to-endpoint
6) pre-xlate-rev
ipam:
podCIDRs: 10.1.1.0/24 (예시)
Cluster Scope IPAM 모드는 각 노드에 노드별 PodCIDR을 할당하고 각 노드에 호스트 범위 할당기를 사용하여 IP를 할당하는 방식이다.
Kubernetes Host Scope IPAM 모드와의 차이점은 Kubernetes가 Kubernetes v1.Node 리소스를 통해 노드별 PodCIDR을 할당하는 대신, Cilium 운영자가 v2.CiliumNode 리소스(CRD)를 통해 노드별 PodCIDR을 관리한다는 점이다.
각 노드는 고유한 PodCIDR을 가지고 있으며, Cilium은 해당 범위 내에서 Pod에 IP를 할당한다.
Kubernetes의 kube-controller-manager
가 --allocate-node-cidrs
옵션을 통해 노드별 PodCIDR을 할당하면,
Cilium 에이전트는 v1.Node 리소스의 spec.podCIDR 또는 spec.podCIDRs 필드를 참조하여 IP를 배정한다.
앞서 설치한 Kubernetes Host Scope IPAM모드를 Cluster Scope 모드로 마이그레이션 해보자.
# Cluster Scopre 로 설정 변경
helm upgrade cilium cilium/cilium --namespace kube-system --reuse-values \
--set ipam.mode="cluster-pool" --set ipam.operator.clusterPoolIPv4PodCIDRList={"172.20.0.0/16"} --set ipv4NativeRoutingCIDR=172.20.0.0/16
kubectl -n kube-system rollout restart deploy/cilium-operator # 오퍼레이터 재시작 필요
kubectl -n kube-system rollout restart ds/cilium
(⎈|HomeLab:N/A) root@k8s-ctr:~# cilium config view | grep ^ipam
ipam cluster-pool
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get ciliumnode -o json | grep podCIDRs -A2
"podCIDRs": [
"172.20.1.0/24"
],
--
"podCIDRs": [
"172.20.0.0/24"
],
# pod IP 재할당을 위해서는 pod들 재기동 필요
#
kubectl delete ciliumnode k8s-w1
kubectl -n kube-system rollout restart ds/cilium
#
kubectl delete ciliumnode k8s-ctr
kubectl -n kube-system rollout restart ds/cilium
#
kubectl -n kube-system rollout restart deploy/hubble-relay deploy/hubble-ui
kubectl -n cilium-monitoring rollout restart deploy/prometheus deploy/grafana
kubectl rollout restart deploy/webpod
kubectl delete pod curl-pod
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl get ciliumendpoints.cilium.io -A
NAMESPACE NAME SECURITY IDENTITY ENDPOINT STATE IPV4 IPV6
cilium-monitoring grafana-5554b5b99d-ngp5c 63656 ready 172.20.0.34
cilium-monitoring prometheus-56564cbb6f-pgtzp 60653 ready 172.20.0.1
default curl-pod 28108 ready 172.20.1.147
default webpod-5f99d864bd-6mbxt 5610 ready 172.20.0.125
default webpod-5f99d864bd-7kwbv 5610 ready 172.20.1.88
kube-system coredns-674b8bbfcf-n4wn5 63747 ready 172.20.1.239
kube-system coredns-674b8bbfcf-wnqpt 63747 ready 172.20.0.195
kube-system hubble-relay-7767cb5659-r6kjd 25120 ready 172.20.0.7
kube-system hubble-ui-869b77984-z9crh 36288 ready 172.20.0.149
위 실습에서 진행했다시피 IPAM모드 변경을 위해서는 cilium 관련 리소스 재기동이 필요하기 때문에, 운영중 IPAM 모드 마이그레이션은 최대한 지양해야 한다.
Routing에는 크게 두가지 방식이 있다.
하나는 앞에서 계속 다뤄왔던 Native-Routing 방식이고, 다른 하나는 Encapsulation 방식이다.
구분 | Native Routing (직접 라우팅) | Encapsulation (캡슐화, 예: VXLAN) |
---|---|---|
트래픽 처리 방식 | Pod IP 간 라우팅 테이블에 직접 경로 등록하여 노드 간 직접 라우팅 | 트래픽을 VXLAN 같은 터널 프로토콜로 캡슐화하여 터널을 통해 전달 |
네트워크 오버헤드 | 적음 (캡슐화 없음) | 캡슐화 오버헤드 존재 (UDP 헤더 + VXLAN 헤더 추가) |
MTU 이슈 | 기본 MTU 사용 (대체로 1500) | 캡슐화로 MTU 감소, Path MTU 문제 발생 가능 |
라우팅 설정 복잡도 | 노드 라우팅 테이블에 Pod CIDR 경로 추가 필요 | 라우팅 테이블 복잡도 낮음, 터널 엔드포인트 간 패킷 전달 |
네트워크 정책 적용 지점 | 노드와 Pod 모두에서 정책 적용 가능 | 캡슐화로 인해 터널 종단점에서 정책 적용 필요 (터널 내부는 패킷 변경) |
네트워크 환경 요구사항 | 클러스터 내 모든 노드가 Pod CIDR를 라우팅 가능해야 함 | 중간 네트워크(라우터, 스위치)에서 터널 프로토콜 지원 필요 없음 |
멀티테넌시 / 복잡한 네트워크 | 복잡한 네트워크 환경에서 라우팅 관리 어려움 | 복잡한 네트워크 환경에서 터널로 격리 및 네트워크 분리 가능 |
디버깅 난이도 | 비교적 쉬움 | 캡슐화로 인해 트래픽 분석 및 디버깅 어려움 |
성능 영향 | 일반적으로 더 낮은 지연 및 CPU 오버헤드 | 캡슐화/디캡슐화에 따른 CPU 오버헤드 및 약간의 지연 발생 |
노드 간 트래픽 흐름 | 노드 IP 기반 직접 전달 | 터널 엔드포인트 간 캡슐화된 패킷 전달 |
Native-Routing 통신에 관해서는 앞선 글과 위 실습에서 다루었기 때문에 이번에는 vxlan Encapsulation 실습을 진행해본다.
helm install cilium cilium/cilium --version 1.17.6 --namespace kube-system \
--set k8sServiceHost=192.168.10.100 \
--set k8sServicePort=6443 \
--set ipam.mode="kubernetes" \
--set k8s.requireIPv4PodCIDR=true \
--set ipv4NativeRoutingCIDR=10.244.0.0/16 \
--set routingMode=tunnel \
--set encapsulation=vxlan \
--set autoDirectNodeRoutes=false \
--set endpointRoutes.enabled=true \
--set kubeProxyReplacement=true \
--set bpf.masquerade=true \
--set installNoConntrackIptablesRules=false \
--set endpointHealthChecking.enabled=false \
--set healthChecking=false \
--set hubble.enabled=true \
--set hubble.relay.enabled=true \
--set hubble.ui.enabled=true \
--set hubble.ui.service.type=NodePort \
--set hubble.ui.service.nodePort=30003 \
--set prometheus.enabled=true \
--set operator.prometheus.enabled=true \
--set hubble.metrics.enableOpenMetrics=true \
--set hubble.metrics.enabled="{dns,drop,tcp,flow,port-distribution,icmp,httpV2:exemplars=true;labelsContext=source_ip\,source_namespace\,source_workload\,destination_ip\,destination_namespace\,destination_workload\,traffic_direction}" \
--set operator.replicas=1 \
--set debug.enabled=true
(⎈|HomeLab:N/A) root@k8s-ctr:~# ip -c route
default via 10.0.2.2 dev eth0 proto dhcp src 10.0.2.15 metric 100
10.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 metric 100
10.0.2.2 dev eth0 proto dhcp scope link src 10.0.2.15 metric 100
10.0.2.3 dev eth0 proto dhcp scope link src 10.0.2.15 metric 100
10.10.0.0/16 via 192.168.10.200 dev eth1 proto static
10.244.0.4 dev cilium_host proto kernel scope link
10.244.0.37 dev lxceae0fb68caf5 proto kernel scope link
10.244.0.145 dev lxcd1114ba6891e proto kernel scope link
10.244.0.169 dev lxc8810a263de82 proto kernel scope link
10.244.0.203 dev lxc1fa09f003b38 proto kernel scope link
10.244.0.209 dev lxc158567b48672 proto kernel scope link
10.244.1.0/24 via 10.244.0.4 dev cilium_host proto kernel src 10.244.0.4 mtu 1450
192.168.10.0/24 dev eth1 proto kernel scope link src 192.168.10.100
(⎈|HomeLab:N/A) root@k8s-ctr:~# ip addr show
4: cilium_net@cilium_host: <BROADCAST,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 6a:87:2e:a2:87:d7 brd ff:ff:ff:ff:ff:ff
inet6 fe80::6887:2eff:fea2:87d7/64 scope link
valid_lft forever preferred_lft forever
5: cilium_host@cilium_net: <BROADCAST,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether e2:0f:04:be:e7:f1 brd ff:ff:ff:ff:ff:ff
inet 10.244.0.4/32 scope global cilium_host
valid_lft forever preferred_lft forever
inet6 fe80::e00f:4ff:febe:e7f1/64 scope link
valid_lft forever preferred_lft forever
24: cilium_vxlan: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default
link/ether 36:da:0d:c5:8e:26 brd ff:ff:ff:ff:ff:ff
inet6 fe80::34da:dff:fec5:8e26/64 scope link
valid_lft forever preferred_lft forever
VXLAN은 기본적으로 UDP 포트 8472를 통해 encapsulated 트래픽을 전송한다. 실제 HTTP 트래픽이 VXLAN encapsulation 안에 포함되어 있기 때문에, eth1에서는 HTTP (TCP/80) 패킷을 직접 볼 수 없고 UDP 포트 8472를 통해 확인할 수 있다.
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it curl-pod -- curl webpod | grep Hostname
(⎈|HomeLab:N/A) root@k8s-ctr:~# tcpdump -i eth1 udp port 8472 -nn | grep 10.244.1.108
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), snapshot length 262144 bytes
IP 10.244.0.25.42626 > 10.244.1.108.80: Flags [S], seq 384054985, win 64860, options [mss 1410,sackOK,TS val 3044357904 ecr 0,nop,wscale 7], length 0
IP 10.244.1.108.80 > 10.244.0.25.42626: Flags [S.], seq 1361241289, ack 384054986, win 64308, options [mss 1410,sackOK,TS val 1148259990 ecr 3044357904,nop,wscale 7], length 0
IP 10.244.0.25.42626 > 10.244.1.108.80: Flags [.], ack 1, win 507, options [nop,nop,TS val 3044357905 ecr 1148259990], length 0
IP 10.244.0.25.42626 > 10.244.1.108.80: Flags [P.], seq 1:71, ack 1, win 507, options [nop,nop,TS val 3044357905 ecr 1148259990], length 70: HTTP: GET / HTTP/1.1
IP 10.244.1.108.80 > 10.244.0.25.42626: Flags [.], ack 71, win 502, options [nop,nop,TS val 1148259991 ecr 3044357905], length 0
IP 10.244.1.108.80 > 10.244.0.25.42626: Flags [P.], seq 1:322, ack 71, win 502, options [nop,nop,TS val 1148259993 ecr 3044357905], length 321: HTTP: HTTP/1.1 200 OK
IP 10.244.0.25.42626 > 10.244.1.108.80: Flags [.], ack 322, win 505, options [nop,nop,TS val 3044357908 ecr 1148259993], length 0
IP 10.244.0.25.42626 > 10.244.1.108.80: Flags [F.], seq 71, ack 322, win 505, options [nop,nop,TS val 3044357908 ecr 1148259993], length 0
IP 10.244.1.108.80 > 10.244.0.25.42626: Flags [F.], seq 322, ack 72, win 502, options [nop,nop,TS val 1148259994 ecr 3044357908], length 0
IP 10.244.0.25.42626 > 10.244.1.108.80: Flags [.], ack 323, win 505, options [nop,nop,TS val 3044357909 ecr 1148259994], length 0
^C73 packets captured
73 packets received by filter
0 packets dropped by kernel
(⎈|HomeLab:N/A) root@k8s-ctr:~# tcpdump -i eth1 tcp port 80 -nn | grep 10.244.1.108
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), snapshot length 262144 bytes
^C0 packets captured
0 packets received by filter
0 packets dropped by kernel
termshark 도구를 활용하여 udp port 8472 tcpdump 결과를 확인해보면 패킷이 캡슐화 되어 패킷 헤더의 Src, Dst IP가 노드의 eth1 IP임을 확인할 수 있다.
(⎈|HomeLab:N/A) root@k8s-ctr:~# tcpdump -i eth1 udp port 8472 -w /tmp/icmp.pcap
(⎈|HomeLab:N/A) root@k8s-ctr:~# termshark -r /tmp/icmp.pcap
vxlan 모드에서 다른 노드 위 Pod간 통신을 모니터링해본다.
통신 시나리오:
curl-pod → ClusterIP(web-svc) → webpod (다른 노드 상의 Pod)
Routing : vxlan Routing
모니터링 흐름:
1) curl-pod에서 webpod의 ClusterIP 호출
2) pre-xlate-fwd
3) post-xlate-fwd
4) to-overlay
5) to-endpoint
6) pre-xlate-rev
위에서 분석한 것과 같이 to-overaly BPF tracemessage 코드를 분석해보자.
//cilium/bpf/bpf_lxc.c
// 터널 모드의 경우
#if defined(TUNNEL_MODE)
...
//실제 터널링 정보가 있을 경우
if (info && info->flag_has_tunnel_ep) {
//캡슐화 및 리다이렉션 수행
ret = encap_and_redirect_lxc(ctx, info, SECLABEL_IPV4,
*dst_sec_identity, &trace,
bpf_htons(ETH_P_IP));
#endif /* TUNNEL_MODE */
//cilium/bpf/lib/encap.h
encap_and_redirect_lxc(struct __ctx_buff *ctx, struct remote_endpoint_info *info,
__u32 seclabel, __u32 dstid, const struct trace_ctx *trace, __be16 proto)
{
return encap_and_redirect_with_nodeid(ctx, info, seclabel, dstid, trace, proto);
}
//코드를 따라가다 보면 encap_with_nodeid4를 수행하고, 해당 함수에서 to-overlay 메시지를 호출 후 캡슐화 정보를 세팅함.
__encap_with_nodeid4(struct __ctx_buff *ctx, __u32 src_ip, __be16 src_port,
__be32 tunnel_endpoint,
__u32 seclabel, __u32 dstid, __u32 vni,
enum trace_reason ct_reason, __u32 monitor, int *ifindex,
__be16 proto)
{
...
send_trace_notify(ctx, TRACE_TO_OVERLAY, seclabel, dstid, TRACE_EP_ID_UNKNOWN,
*ifindex, ct_reason, monitor, proto);
return ctx_set_encap_info4(ctx, src_ip, src_port, tunnel_endpoint, seclabel, vni,
NULL, 0);
}
Masquerade (마스커레이드)란, 특정 네트워크 패킷의 출발지 IP 주소를 노드의 IP로 변경하는 SNAT(Source NAT) 동작을 의미한다.
Cilium은 클러스터를 떠나는 모든 트래픽의 소스 IP 주소를 자동으로 masquerade 하는데, 이는 Cilium의 경우 Pod에서 나가는 트래픽의 출발지 IP가 Pod IP이기 때문에, 그대로 외부로 보내면 외부에서 응답할 수 없기 때문이다.
따라서 출발지 IP를 해당 Pod가 위치한 노드의 IP로 바꿔서(SNAT) 보내고, 나중에 응답이 오면 다시 원래의 Pod IP를 반환하는 방식으로 동작한다.
(⎈|HomeLab:N/A) root@k8s-ctr:~# kubectl exec -it -n kube-system ds/cilium -c cilium-agent -- cilium status | grep Masquerading
Masquerading: BPF [eth0, eth1] 10.244.0.0/16 [IPv4: Enabled, IPv6: Disabled]
(⎈|HomeLab:N/A) root@k8s-ctr:~# cilium config view | grep ipv4-native-routing-cidr
ipv4-native-routing-cidr 10.244.0.0/16
ipv4-native-routing-cidr(10.244.0.0/16) 범위는 native routing (터널링 없이 직접 라우팅) 되는 네트워크이고, 이 범위 밖으로 나가는 트래픽은 SNAT(Masquerading) 된다.
단, 대상 IP가 클러스터 내부 Node IP라면 예외적으로 Masquerading 되지 않는다.
다음은 Pod에서 클러스터 내부 노드, 클러스터 외부 서버 호출을 비교한 실습이다.
클러스터 내부 노드 호출을 할 때에는 Pod IP가 잡히지만, 클러스터 외부 서버 호출을 할 때에는 Node IP로 SNAT되는 것을 확인할 수 있다.
ipMasqAgent란 쿠버네티스 클러스터에서 IP 마스커레이딩(즉, SNAT)을 제어하는 에이전트의 설정 항목이다.
ipMasqAgent로 다음 설정들을 할 수 있다.
설정 항목 | 설명 | 예시 값 |
---|---|---|
enabled | ip-masq-agent 기능 활성화 여부 | true , false |
config.nonMasqueradeCIDRs | SNAT이 적용되지 않을 CIDR 목록 | {10.10.1.0/24,10.10.2.0/24} |
config.masqLinkLocal | 링크 로컬 주소 (169.254.0.0/16)에 대해 SNAT 적용 여부 | true , false |
config.masqLinkLocalIPv6 | IPv6 링크 로컬 주소 (fe80::/10)에 대해 SNAT 적용 여부 | true , false |
config.masqAgentConfigPath | 사용자 정의 config 파일 경로 | /etc/cilium/masq-agent.json |
config.masqOutBoundCIDRs | SNAT이 항상 적용될 외부 CIDR 목록 | {0.0.0.0/0} |
config.masqOutBoundPortRanges | SNAT이 항상 적용될 포트 범위 목록 | {80,443,1000-2000} |
config.refreshInterval | iptables 규칙 갱신 주기 | 1m , 30s |
installIptablesRules | iptables 규칙을 ip-masq-agent가 직접 설치할지 여부 | true , false |