VXLAN in Kubernetes - 이론부터 패킷 분석까지
목차
- VXLAN이란 무엇인가?
- Kubernetes 네트워킹 기초
- VXLAN 패킷 구조
- Kubernetes에서 VXLAN 사용
- 실습 환경 구축
- VXLAN 패킷 캡처 및 분석
- Golang으로 VXLAN 파싱하기
- 트러블슈팅
VXLAN이란 무엇인가?
정의
VXLAN (Virtual eXtensible Local Area Network) 은 L2 (Ethernet) 네트워크를 L3 (IP) 네트워크 위에 터널링하는 기술입니다.
왜 필요한가?
전통적인 VLAN의 한계
┌─────────────────────────────────────────────────────┐
│ Traditional VLAN │
├─────────────────────────────────────────────────────┤
│ │
│ - VLAN ID: 12 bits → 최대 4096개 네트워크 │
│ - L2 스위치에 의존 │
│ - 물리적 위치에 제약 │
│ - 클라우드/가상화 환경에 부적합 │
│ │
└─────────────────────────────────────────────────────┘문제점:
- 4096개 제한: 대규모 멀티테넌트 환경에서 부족
- L2 확장의 어려움: 데이터센터 간 L2 연결 복잡
- 물리적 의존성: 스위치 설정 필요
VXLAN의 해결책
┌─────────────────────────────────────────────────────┐
│ VXLAN │
├─────────────────────────────────────────────────────┤
│ │
│ - VNI (VXLAN Network Identifier): 24 bits │
│ → 최대 16,777,216개 네트워크 │
│ - L3 네트워크 위에서 동작 │
│ - 물리적 위치 무관 │
│ - 소프트웨어로 완전 제어 가능 │
│ │
└─────────────────────────────────────────────────────┘VXLAN의 핵심 개념
┌──────────────────────────────────────────────────────────────┐
│ VXLAN Overlay Network │
│ │
│ VM/Container 1 VM/Container 2 │
│ IP: 10.1.1.10 IP: 10.1.1.20 │
│ MAC: aa:bb:cc:dd:ee:01 MAC: aa:bb:cc:dd:ee:02 │
│ │ │ │
│ │ Overlay Network │ │
│ │ (가상 L2) │ │
│ └─────────┬───────────────┘ │
│ │ │
│ ┌──────▼──────┐ │
│ │ VXLAN │ │
│ │ VNI: 100 │ │
│ └──────┬──────┘ │
│ │ │
└───────────────────┼──────────────────────────────────────────┘
│
┌─────────▼─────────┐
│ Encapsulation │
│ (L2 → L3) │
└─────────┬─────────┘
│
┌───────────────────▼──────────────────────────────────────────┐
│ Underlay Network (물리 네트워크) │
│ │
│ Host A Host B │
│ IP: 192.168.1.10 IP: 192.168.1.20 │
│ MAC: 11:22:33:44:55:01 MAC: 11:22:33:44:55:02 │
│ │ │ │
│ └─────────────┬───────────────────┘ │
│ │ │
│ Physical Switch │
│ Router, etc. │
│ │
└──────────────────────────────────────────────────────────────┘용어 설명:
- Overlay Network: 가상 네트워크 (VM/Container가 보는 네트워크)
- Underlay Network: 물리 네트워크 (실제 호스트 간 통신)
- VNI (VXLAN Network Identifier): 가상 네트워크 ID (24 bits)
- VTEP (VXLAN Tunnel Endpoint): VXLAN 캡슐화/역캡슐화를 수행하는 지점
Kubernetes 네트워킹 기초
Kubernetes 네트워크 모델
Kubernetes는 다음 요구사항을 만족해야 합니다:
- 모든 Pod는 고유한 IP를 가져야 함
- 모든 Pod는 NAT 없이 서로 통신 가능
- 모든 Node는 모든 Pod와 통신 가능
┌─────────────────────────────────────────────────────────────┐
│ Kubernetes Cluster │
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ Node 1 │ │ Node 2 │ │
│ │ IP: 192.168.1.10 │ │ IP: 192.168.1.20 │ │
│ │ │ │ │ │
│ │ ┌───────────────┐ │ │ ┌───────────────┐ │ │
│ │ │ Pod A │ │ │ │ Pod C │ │ │
│ │ │ 10.244.1.10 │ │ │ │ 10.244.2.10 │ │ │
│ │ └───────────────┘ │ │ └───────────────┘ │ │
│ │ ┌───────────────┐ │ │ ┌───────────────┐ │ │
│ │ │ Pod B │ │ │ │ Pod D │ │ │
│ │ │ 10.244.1.20 │ │ │ │ 10.244.2.20 │ │ │
│ │ └───────────────┘ │ │ └───────────────┘ │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │ │ │
│ └─────────────┬───────────────┘ │
│ │ │
│ VXLAN Tunnel │
│ VNI: 1 │
│ │
└─────────────────────────────────────────────────────────────┘CNI (Container Network Interface)
Kubernetes는 CNI 플러그인을 통해 네트워킹을 구현합니다.
VXLAN을 사용하는 CNI 플러그인:
- Flannel: 가장 간단, VXLAN 기본 지원
- Calico: VXLAN + BGP 옵션
- Cilium: eBPF + VXLAN
- Weave Net: VXLAN 지원
Flannel의 VXLAN 동작
Pod A (Node 1) → Pod C (Node 2) 통신 과정
1. Pod A: "10.244.2.10으로 패킷 전송"
↓
2. Node 1의 VTEP (flannel.1):
- Destination: 10.244.2.10
- "이건 Node 2에 있네!"
- VXLAN으로 캡슐화
↓
3. Underlay Network:
- UDP 패킷으로 Node 2 (192.168.1.20)로 전송
↓
4. Node 2의 VTEP (flannel.1):
- VXLAN 역캡슐화
- 원본 패킷 추출
↓
5. Pod C: 패킷 수신VXLAN 패킷 구조
완전한 VXLAN 패킷
┌────────────────────────────────────────────────────────────┐
│ Complete VXLAN Packet │
├────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Outer Ethernet Header (14 bytes) [L2] │ │
│ ├──────────────────────────────────────────────────┤ │
│ │ Outer Dst MAC: Node 2 MAC │ │
│ │ Outer Src MAC: Node 1 MAC │ │
│ │ EtherType: 0x0800 (IPv4) │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Outer IP Header (20 bytes) [L3] │ │
│ ├──────────────────────────────────────────────────┤ │
│ │ Outer Src IP: 192.168.1.10 (Node 1) │ │
│ │ Outer Dst IP: 192.168.1.20 (Node 2) │ │
│ │ Protocol: 17 (UDP) │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Outer UDP Header (8 bytes) [L4] │ │
│ ├──────────────────────────────────────────────────┤ │
│ │ Src Port: Random (e.g., 54321) │ │
│ │ Dst Port: 4789 (VXLAN default) │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ VXLAN Header (8 bytes) [VXLAN] │ │
│ ├──────────────────────────────────────────────────┤ │
│ │ Flags: 0x08 (I flag set) │ │
│ │ Reserved: 0 │ │
│ │ VNI: 1 (24 bits) - Network ID │ │
│ │ Reserved: 0 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Inner Ethernet Header (14 bytes) [L2] │ │
│ ├──────────────────────────────────────────────────┤ │
│ │ Inner Dst MAC: Pod C MAC │ │
│ │ Inner Src MAC: Pod A MAC │ │
│ │ EtherType: 0x0800 (IPv4) │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Inner IP Header (20 bytes) [L3] │ │
│ ├──────────────────────────────────────────────────┤ │
│ │ Inner Src IP: 10.244.1.10 (Pod A) │ │
│ │ Inner Dst IP: 10.244.2.10 (Pod C) │ │
│ │ Protocol: 6 (TCP) or 17 (UDP) or 1 (ICMP) │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Inner TCP/UDP Header [L4] │ │
│ ├──────────────────────────────────────────────────┤ │
│ │ Application ports │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Payload (Application Data) [L7] │ │
│ └──────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────┘VXLAN 헤더 상세
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|R|R|R|R|I|R|R|R| Reserved (24 bits) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| VNI (24 bits) | Reserved |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Flags (8 bits):
- I (bit 4): VNI 필드가 유효함을 표시 (일반적으로 1)
- R: Reserved (0으로 설정)
VNI (VXLAN Network Identifier):
- 24 bits
- 가상 네트워크 ID
- 0 ~ 16,777,215 (2^24 - 1)바이트 레벨 분석
실제 VXLAN 패킷 예시 (Hex dump):
Outer Ethernet Header (14 bytes):
00 50 56 c0 00 08 ← Dst MAC (Node 2)
00 50 56 c0 00 01 ← Src MAC (Node 1)
08 00 ← EtherType: IPv4
Outer IP Header (20 bytes):
45 00 00 92 ← Version, IHL, TOS, Total Length
ab cd 40 00 ← ID, Flags, Fragment Offset
40 11 xx xx ← TTL=64, Protocol=17(UDP), Checksum
c0 a8 01 0a ← Src IP: 192.168.1.10
c0 a8 01 14 ← Dst IP: 192.168.1.20
Outer UDP Header (8 bytes):
d4 31 ← Src Port: 54321
12 b5 ← Dst Port: 4789 (VXLAN)
00 7e ← Length
00 00 ← Checksum (optional, often 0)
VXLAN Header (8 bytes):
08 00 00 00 ← Flags (I=1), Reserved
00 00 01 00 ← VNI=1, Reserved
^^^^^^^^
VNI: 0x000001 = 1
Inner Ethernet Header (14 bytes):
aa bb cc dd ee 02 ← Inner Dst MAC (Pod C)
aa bb cc dd ee 01 ← Inner Src MAC (Pod A)
08 00 ← EtherType: IPv4
Inner IP Header (20 bytes):
45 00 00 54 ← Version, IHL, TOS, Total Length
12 34 40 00 ← ID, Flags
40 01 xx xx ← TTL=64, Protocol=1(ICMP), Checksum
0a f4 01 0a ← Src IP: 10.244.1.10 (Pod A)
0a f4 02 0a ← Dst IP: 10.244.2.10 (Pod C)
Inner ICMP (for ping):
08 00 xx xx ← Type=8 (Echo Request), Code=0, Checksum
12 34 00 01 ← ID, Sequence
... payload ...Kubernetes에서 VXLAN 사용
Flannel VXLAN 설정
# flannel-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: kube-flannel-cfg
namespace: kube-system
data:
cni-conf.json: |
{
"name": "cbr0",
"cniVersion": "0.3.1",
"plugins": [
{
"type": "flannel",
"delegate": {
"hairpinMode": true,
"isDefaultGateway": true
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
}
]
}
net-conf.json: |
{
"Network": "10.244.0.0/16",
"Backend": {
"Type": "vxlan",
"VNI": 1,
"Port": 4789
}
}주요 설정:
Network: Pod IP 범위 (10.244.0.0/16)Type: "vxlan" (VXLAN 모드 사용)VNI: 1 (기본값)Port: 4789 (VXLAN 기본 포트)
Node별 서브넷 할당
Node 1: 10.244.1.0/24
- Pod A: 10.244.1.10
- Pod B: 10.244.1.20
- ...
Node 2: 10.244.2.0/24
- Pod C: 10.244.2.10
- Pod D: 10.244.2.20
- ...
Node 3: 10.244.3.0/24
- Pod E: 10.244.3.10
- ...VTEP 인터페이스 확인
각 Node에는 flannel.1 인터페이스가 생성됩니다:
# Node에서 실행
ip addr show flannel.1출력:
4: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN
link/ether 8e:6d:cf:8c:3f:4e brd ff:ff:ff:ff:ff:ff
inet 10.244.1.0/32 scope global flannel.1
valid_lft forever preferred_lft forever
inet6 fe80::8c6d:cfff:fe8c:3f4e/64 scope link
valid_lft forever preferred_lft forever특징:
- MTU: 1450 (1500 - 50, VXLAN 오버헤드 때문)
- Type: VXLAN
# VXLAN 인터페이스 상세 정보
ip -d link show flannel.1출력:
4: flannel.1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT group default
link/ether 8e:6d:cf:8c:3f:4e brd ff:ff:ff:ff:ff:ff promiscuity 0
vxlan id 1 local 192.168.1.10 dev eth0 srcport 0 0 dstport 4789 nolearning ageing 300 noudpcsum noudp6zerocsumtx noudp6zerocsumrx
numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535주요 정보:
vxlan id 1: VNI = 1local 192.168.1.10: 이 Node의 IPdev eth0: Underlay 인터페이스dstport 4789: VXLAN 포트
FDB (Forwarding Database) 확인
VTEP는 어느 Pod가 어느 Node에 있는지 알아야 합니다:
# FDB 확인
bridge fdb show dev flannel.1출력:
8e:6d:cf:8c:3f:4e dev flannel.1 dst self permanent
aa:bb:cc:dd:ee:02 dev flannel.1 dst 192.168.1.20 self
aa:bb:cc:dd:ee:03 dev flannel.1 dst 192.168.1.30 self의미:
- Pod MAC
aa:bb:cc:dd:ee:02는 Node192.168.1.20에 있음 - Pod MAC
aa:bb:cc:dd:ee:03는 Node192.168.1.30에 있음
ARP 및 라우팅
# Node의 라우팅 테이블
ip route show출력:
default via 192.168.1.1 dev eth0
10.244.0.0/16 dev cni0 proto kernel scope link src 10.244.1.1
10.244.1.0/24 dev cni0 proto kernel scope link src 10.244.1.1
10.244.2.0/24 via 10.244.2.0 dev flannel.1 onlink
10.244.3.0/24 via 10.244.3.0 dev flannel.1 onlink
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.10의미:
10.244.1.0/24: 로컬 Pod (cni0 브리지로)10.244.2.0/24: Node 2의 Pod (flannel.1 VXLAN 터널로)10.244.3.0/24: Node 3의 Pod (flannel.1 VXLAN 터널로)
실습 환경 구축
Option 1: Minikube (로컬 테스트)
# Minikube 설치 (이미 설치되어 있다면 스킵)
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube
# Minikube 시작 (멀티노드)
minikube start --nodes 3 --cni flannel --driver=docker
# 노드 확인
kubectl get nodes출력:
NAME STATUS ROLES AGE VERSION
minikube Ready control-plane 2m v1.28.0
minikube-m02 Ready <none> 1m v1.28.0
minikube-m03 Ready <none> 1m v1.28.0Option 2: Kind (Kubernetes in Docker)
# Kind 설치
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.20.0/kind-linux-amd64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind
# 멀티노드 클러스터 설정
cat <<EOF > kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker
networking:
disableDefaultCNI: true # Flannel 설치를 위해
podSubnet: "10.244.0.0/16"
EOF
# 클러스터 생성
kind create cluster --config kind-config.yaml
# Flannel 설치
kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.ymlOption 3: 실제 서버 (권장)
# 3대의 VM 또는 물리 서버 준비
# Node 1: 192.168.1.10
# Node 2: 192.168.1.20
# Node 3: 192.168.1.30
# kubeadm으로 클러스터 초기화 (Master에서)
sudo kubeadm init --pod-network-cidr=10.244.0.0/16
# Flannel 설치
kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml
# Worker Node 조인
sudo kubeadm join 192.168.1.10:6443 --token <token> --discovery-token-ca-cert-hash sha256:<hash>테스트 Pod 배포
# test-pods.yaml
apiVersion: v1
kind: Pod
metadata:
name: pod-a
labels:
app: test
spec:
containers:
- name: nginx
image: nginx:alpine
command: ["/bin/sh", "-c", "sleep 3600"]
nodeSelector:
kubernetes.io/hostname: minikube # Node 1
---
apiVersion: v1
kind: Pod
metadata:
name: pod-b
labels:
app: test
spec:
containers:
- name: nginx
image: nginx:alpine
command: ["/bin/sh", "-c", "sleep 3600"]
nodeSelector:
kubernetes.io/hostname: minikube-m02 # Node 2
---
apiVersion: v1
kind: Pod
metadata:
name: pod-c
labels:
app: test
spec:
containers:
- name: nginx
image: nginx:alpine
command: ["/bin/sh", "-c", "sleep 3600"]
nodeSelector:
kubernetes.io/hostname: minikube-m03 # Node 3# Pod 배포
kubectl apply -f test-pods.yaml
# Pod IP 확인
kubectl get pods -o wide출력:
NAME READY STATUS IP NODE
pod-a 1/1 Running 10.244.1.10 minikube
pod-b 1/1 Running 10.244.2.10 minikube-m02
pod-c 1/1 Running 10.244.3.10 minikube-m03VXLAN 패킷 캡처 및 분석
준비: tcpdump 설치
# 모든 Node에 설치
sudo apt-get update
sudo apt-get install -y tcpdump wireshark-common
# 또는 (Red Hat 계열)
sudo yum install -y tcpdump wireshark실습 1: 기본 VXLAN 패킷 캡처
Step 1: 패킷 캡처 시작
Node 1에서:
# Underlay 네트워크에서 VXLAN 패킷 캡처
sudo tcpdump -i eth0 -nn 'udp port 4789' -w /tmp/vxlan-capture.pcap옵션 설명:
-i eth0: 물리 인터페이스-nn: IP/포트를 숫자로 표시 (DNS 조회 안 함)udp port 4789: VXLAN 포트 필터-w: PCAP 파일로 저장
Step 2: 트래픽 생성
다른 터미널에서:
# Pod A에서 Pod B로 ping
kubectl exec -it pod-a -- ping -c 5 10.244.2.10Step 3: 캡처 중지 및 분석
# Ctrl+C로 tcpdump 중지
# 캡처한 패킷 확인
sudo tcpdump -r /tmp/vxlan-capture.pcap -nn -v출력 예시:
15:30:45.123456 IP (tos 0x0, ttl 64, id 12345, offset 0, flags [none], proto UDP (17), length 134)
192.168.1.10.54321 > 192.168.1.20.4789: [udp sum ok] VXLAN, flags [I] (0x08), vni 1
IP (tos 0x0, ttl 64, id 54321, offset 0, flags [DF], proto ICMP (1), length 84)
10.244.1.10 > 10.244.2.10: ICMP echo request, id 1, seq 1, length 64분석:
- Outer: 192.168.1.10 → 192.168.1.20 (Node 간 통신)
- VXLAN: VNI = 1, flags = I
- Inner: 10.244.1.10 → 10.244.2.10 (Pod 간 통신)
- ICMP: Echo Request (ping)
실습 2: Wireshark로 상세 분석
# PCAP 파일을 로컬로 복사
scp user@node1:/tmp/vxlan-capture.pcap .
# Wireshark 실행
wireshark vxlan-capture.pcapWireshark에서 확인할 내용
Frame 선택
- VXLAN 패킷 하나 클릭
계층별 확인
Frame └─ Ethernet II └─ Internet Protocol Version 4 (Outer) └─ User Datagram Protocol (Outer) └─ Virtual eXtensible Local Area Network └─ Ethernet II (Inner) └─ Internet Protocol Version 4 (Inner) └─ Internet Control Message Protocol (ICMP)VXLAN 헤더 확인
Virtual eXtensible Local Area Network Flags: 0x0800 0... .... = Reserved: 0 .0.. .... = Reserved: 0 ..0. .... = Reserved: 0 ...0 .... = Reserved: 0 .... 1... = VNI: 1 .... .0.. = Reserved: 0 .... ..0. = Reserved: 0 .... ...0 = Reserved: 0 Group Policy ID: 0 VXLAN Network Identifier (VNI): 1 Reserved: 0MTU 오버헤드 확인
Outer Ethernet: 14 bytes Outer IP: 20 bytes Outer UDP: 8 bytes VXLAN: 8 bytes ────────────────────────── Total overhead: 50 bytes따라서 VXLAN 인터페이스 MTU = 1500 - 50 = 1450
실습 3: 계층별 상세 분석
# Hex dump와 함께 출력
sudo tcpdump -r /tmp/vxlan-capture.pcap -nn -X | less출력:
15:30:45.123456 IP 192.168.1.10.54321 > 192.168.1.20.4789: VXLAN, flags [I] (0x08), vni 1
IP 10.244.1.10 > 10.244.2.10: ICMP echo request, id 1, seq 1, length 64
0x0000: 4500 0092 abcd 4000 4011 xxxx c0a8 010a E.....@.@.......
0x0010: c0a8 0114 d431 12b5 007e 0000 0800 0000 .....1...~......
0x0020: 0000 0100 aabb ccdd ee02 aabb ccdd ee01 ................
0x0030: 0800 4500 0054 1234 4000 4001 xxxx 0af4 ..E..T.4@.@.....
0x0040: 010a 0af4 020a 0800 xxxx 0001 0001 6162 ..............ab
0x0050: 6364 6566 6768 696a 6b6c 6d6e 6f70 7172 cdefghijklmnopqr
...바이트 분석:
0x0000: 45 00 00 92 ... = Outer IP Header
^^
Version (4) + IHL (5)
0x0010: ... 40 11 ... = TTL (0x40=64), Protocol (0x11=17=UDP)
0x0010: ... d4 31 12 b5 = Outer UDP: Src Port 0xd431, Dst Port 0x12b5 (4789)
0x0020: 08 00 00 00 = VXLAN Flags (0x08 = I flag)
0x0020: 00 00 01 00 = VNI (0x000001 = 1)
0x0028: aa bb cc dd ee 02 = Inner Dst MAC (Pod B)
0x002e: aa bb cc dd ee 01 = Inner Src MAC (Pod A)
0x0034: 08 00 = Inner EtherType (0x0800 = IPv4)
0x0036: 45 00 00 54 = Inner IP Header
0x0046: 0a f4 01 0a = Inner Src IP (10.244.1.10 = Pod A)
0x004a: 0a f4 02 0a = Inner Dst IP (10.244.2.10 = Pod B)
0x004e: 08 00 = ICMP Type 8 (Echo Request)실습 4: 양방향 트래픽 분석
# 양방향 캡처
sudo tcpdump -i eth0 -nn 'udp port 4789' -c 10 -v동시에:
# Pod A → Pod B
kubectl exec -it pod-a -- ping -c 3 10.244.2.10출력:
# Request (Node 1 → Node 2)
IP 192.168.1.10.54321 > 192.168.1.20.4789: VXLAN, flags [I] (0x08), vni 1
IP 10.244.1.10 > 10.244.2.10: ICMP echo request, id 1, seq 1, length 64
# Reply (Node 2 → Node 1)
IP 192.168.1.20.54322 > 192.168.1.10.4789: VXLAN, flags [I] (0x08), vni 1
IP 10.244.2.10 > 10.244.1.10: ICMP echo reply, id 1, seq 1, length 64관찰 포인트:
- Request와 Reply 모두 VXLAN으로 캡슐화됨
- Outer IP는 반대로 (Node 2 → Node 1)
- Inner IP도 반대로 (Pod B → Pod A)
실습 5: TCP 트래픽 캡처
# HTTP 서버 Pod 배포
kubectl run http-server --image=nginx --port=80
# 서버 IP 확인
HTTP_SERVER_IP=$(kubectl get pod http-server -o jsonpath='{.status.podIP}')
# 캡처 시작 (Node 1에서)
sudo tcpdump -i eth0 -nn 'udp port 4789' -w /tmp/vxlan-tcp.pcap
# HTTP 요청 (Pod A에서)
kubectl exec -it pod-a -- wget -O- http://$HTTP_SERVER_IP분석:
sudo tcpdump -r /tmp/vxlan-tcp.pcap -nn -v | grep -A 5 "TCP"출력:
# TCP 3-way handshake (VXLAN으로 캡슐화됨)
1. SYN: Pod A → HTTP Server (VNI 1)
2. SYN-ACK: HTTP Server → Pod A (VNI 1)
3. ACK: Pod A → HTTP Server (VNI 1)
# HTTP Request
4. PSH-ACK: GET / HTTP/1.1 (VNI 1)
# HTTP Response
5. PSH-ACK: HTTP/1.1 200 OK (VNI 1)
# Connection Close
6. FIN-ACK: (VNI 1)
7. ACK: (VNI 1)Golang으로 VXLAN 파싱하기
VXLAN Parser 구현
// vxlan_parser.go
package main
import (
"encoding/binary"
"flag"
"fmt"
"log"
"net"
"syscall"
"time"
)
// VXLAN Header 구조체
type VXLANHeader struct {
Flags uint8 // First byte
Reserved1 [3]byte
VNI uint32 // 24 bits (3 bytes)
Reserved2 uint8
}
func (vh *VXLANHeader) Parse(data []byte) error {
if len(data) < 8 {
return fmt.Errorf("VXLAN header too short: %d bytes", len(data))
}
vh.Flags = data[0]
copy(vh.Reserved1[:], data[1:4])
// VNI는 3 bytes (24 bits)
vh.VNI = uint32(data[4])<<16 | uint32(data[5])<<8 | uint32(data[6])
vh.Reserved2 = data[7]
return nil
}
func (vh *VXLANHeader) IsValid() bool {
// I flag (bit 4) must be set
return (vh.Flags & 0x08) != 0
}
func (vh *VXLANHeader) String() string {
return fmt.Sprintf("VNI: %d, Flags: 0x%02x", vh.VNI, vh.Flags)
}
// Packet Statistics
type PacketStats struct {
TotalPackets int
VXLANPackets int
VNIStats map[uint32]int
BytesReceived uint64
StartTime time.Time
}
func NewPacketStats() *PacketStats {
return &PacketStats{
VNIStats: make(map[uint32]int),
StartTime: time.Now(),
}
}
func (ps *PacketStats) Update(isVXLAN bool, vni uint32, size int) {
ps.TotalPackets++
ps.BytesReceived += uint64(size)
if isVXLAN {
ps.VXLANPackets++
ps.VNIStats[vni]++
}
}
func (ps *PacketStats) Print() {
duration := time.Since(ps.StartTime).Seconds()
fmt.Println("\n╔══════════════════════════════════════════════════════════╗")
fmt.Println("║ VXLAN Packet Statistics ║")
fmt.Println("╠══════════════════════════════════════════════════════════╣")
fmt.Printf("║ Total Packets: %-10d (%.2f pkt/s)\n",
ps.TotalPackets, float64(ps.TotalPackets)/duration)
fmt.Printf("║ VXLAN Packets: %-10d (%.2f%%)\n",
ps.VXLANPackets, float64(ps.VXLANPackets)*100/float64(ps.TotalPackets))
fmt.Printf("║ Total Bytes: %-10d (%.2f KB/s)\n",
ps.BytesReceived, float64(ps.BytesReceived)/duration/1024)
fmt.Printf("║ Runtime: %.2f seconds\n", duration)
fmt.Println("║")
fmt.Println("║ VNI Distribution:")
for vni, count := range ps.VNIStats {
fmt.Printf("║ VNI %-6d: %d packets\n", vni, count)
}
fmt.Println("╚══════════════════════════════════════════════════════════╝")
}
func parseVXLAN(packet []byte) {
if len(packet) < 14 {
return
}
// Outer Ethernet Header
outerDstMAC := packet[0:6]
outerSrcMAC := packet[6:12]
etherType := binary.BigEndian.Uint16(packet[12:14])
if etherType != 0x0800 { // IPv4만 처리
return
}
if len(packet) < 34 {
return
}
// Outer IP Header
outerIPHeader := packet[14:34]
outerIHL := int(outerIPHeader[0]&0x0F) * 4
outerProtocol := outerIPHeader[9]
outerSrcIP := net.IP(outerIPHeader[12:16])
outerDstIP := net.IP(outerIPHeader[16:20])
if outerProtocol != 17 { // UDP만
return
}
if len(packet) < 14+outerIHL+8 {
return
}
// Outer UDP Header
outerUDPStart := 14 + outerIHL
outerUDPHeader := packet[outerUDPStart : outerUDPStart+8]
outerSrcPort := binary.BigEndian.Uint16(outerUDPHeader[0:2])
outerDstPort := binary.BigEndian.Uint16(outerUDPHeader[2:4])
if outerDstPort != 4789 && outerSrcPort != 4789 {
// VXLAN이 아님
return
}
// VXLAN Header
vxlanStart := outerUDPStart + 8
if len(packet) < vxlanStart+8 {
return
}
vxlanHeader := VXLANHeader{}
if err := vxlanHeader.Parse(packet[vxlanStart : vxlanStart+8]); err != nil {
return
}
if !vxlanHeader.IsValid() {
return
}
fmt.Println("\n╔════════════════════════════════════════════════════════════════╗")
fmt.Println("║ VXLAN PACKET DETECTED ║")
fmt.Println("╠════════════════════════════════════════════════════════════════╣")
// Outer Headers
fmt.Println("║ Outer (Underlay) Headers:")
fmt.Printf("║ Ethernet: %s → %s\n",
formatMAC(outerSrcMAC), formatMAC(outerDstMAC))
fmt.Printf("║ IP: %s → %s\n", outerSrcIP, outerDstIP)
fmt.Printf("║ UDP: %d → %d\n", outerSrcPort, outerDstPort)
// VXLAN Header
fmt.Println("║")
fmt.Println("║ VXLAN Header:")
fmt.Printf("║ VNI: %d\n", vxlanHeader.VNI)
fmt.Printf("║ Flags: 0x%02x (I=%d)\n",
vxlanHeader.Flags, (vxlanHeader.Flags>>3)&1)
// Inner Headers
innerStart := vxlanStart + 8
if len(packet) < innerStart+14 {
fmt.Println("╚════════════════════════════════════════════════════════════════╝")
return
}
innerDstMAC := packet[innerStart : innerStart+6]
innerSrcMAC := packet[innerStart+6 : innerStart+12]
innerEtherType := binary.BigEndian.Uint16(packet[innerStart+12 : innerStart+14])
fmt.Println("║")
fmt.Println("║ Inner (Overlay) Headers:")
fmt.Printf("║ Ethernet: %s → %s\n",
formatMAC(innerSrcMAC), formatMAC(innerDstMAC))
if innerEtherType == 0x0800 && len(packet) >= innerStart+34 {
innerIPStart := innerStart + 14
innerIPHeader := packet[innerIPStart : innerIPStart+20]
innerIHL := int(innerIPHeader[0]&0x0F) * 4
innerProtocol := innerIPHeader[9]
innerSrcIP := net.IP(innerIPHeader[12:16])
innerDstIP := net.IP(innerIPHeader[16:20])
fmt.Printf("║ IP: %s → %s\n", innerSrcIP, innerDstIP)
switch innerProtocol {
case 1: // ICMP
fmt.Printf("║ Protocol: ICMP")
if len(packet) >= innerIPStart+innerIHL+8 {
icmpStart := innerIPStart + innerIHL
icmpType := packet[icmpStart]
icmpCode := packet[icmpStart+1]
typeStr := "Unknown"
if icmpType == 8 {
typeStr = "Echo Request (ping)"
} else if icmpType == 0 {
typeStr = "Echo Reply (pong)"
}
fmt.Printf(" (%s, code: %d)\n", typeStr, icmpCode)
} else {
fmt.Println()
}
case 6: // TCP
if len(packet) >= innerIPStart+innerIHL+20 {
tcpStart := innerIPStart + innerIHL
srcPort := binary.BigEndian.Uint16(packet[tcpStart : tcpStart+2])
dstPort := binary.BigEndian.Uint16(packet[tcpStart+2 : tcpStart+4])
flags := packet[tcpStart+13]
fmt.Printf("║ TCP: %d → %d [", srcPort, dstPort)
printTCPFlags(flags)
fmt.Printf("]\n")
}
case 17: // UDP
if len(packet) >= innerIPStart+innerIHL+8 {
udpStart := innerIPStart + innerIHL
srcPort := binary.BigEndian.Uint16(packet[udpStart : udpStart+2])
dstPort := binary.BigEndian.Uint16(packet[udpStart+2 : udpStart+4])
fmt.Printf("║ UDP: %d → %d\n", srcPort, dstPort)
}
default:
fmt.Printf("║ Protocol: %d\n", innerProtocol)
}
}
fmt.Println("╚════════════════════════════════════════════════════════════════╝")
}
func main() {
interfaceName := flag.String("i", "", "Network interface to capture")
vniFilter := flag.Int("vni", -1, "Filter by VNI (-1 = all)")
statsInterval := flag.Int("stats", 0, "Print statistics every N seconds (0 = disabled)")
flag.Parse()
if *interfaceName == "" {
fmt.Println("Usage: sudo ./vxlan_parser -i <interface> [-vni <vni>] [-stats <seconds>]")
fmt.Println()
fmt.Println("Examples:")
fmt.Println(" sudo ./vxlan_parser -i eth0")
fmt.Println(" sudo ./vxlan_parser -i eth0 -vni 1")
fmt.Println(" sudo ./vxlan_parser -i eth0 -stats 10")
return
}
// Raw Socket 생성
fd, err := syscall.Socket(
syscall.AF_PACKET,
syscall.SOCK_RAW,
int(htons(syscall.ETH_P_ALL)),
)
if err != nil {
log.Fatalf("Socket creation failed: %v (root 권한이 필요합니다)", err)
}
defer syscall.Close(fd)
// 인터페이스 바인딩
iface, err := net.InterfaceByName(*interfaceName)
if err != nil {
log.Fatalf("Interface not found: %v", err)
}
addr := syscall.SockaddrLinklayer{
Protocol: htons(syscall.ETH_P_ALL),
Ifindex: iface.Index,
}
if err := syscall.Bind(fd, &addr); err != nil {
log.Fatalf("Bind failed: %v", err)
}
fmt.Printf("╔═══════════════════════════════════════════════════════════╗\n")
fmt.Printf("║ VXLAN Packet Parser Started ║\n")
fmt.Printf("╠═══════════════════════════════════════════════════════════╣\n")
fmt.Printf("║ Interface: %-47s║\n", *interfaceName)
if *vniFilter >= 0 {
fmt.Printf("║ VNI Filter: %-46d║\n", *vniFilter)
} else {
fmt.Printf("║ VNI Filter: All ║\n")
}
fmt.Printf("╚═══════════════════════════════════════════════════════════╝\n")
stats := NewPacketStats()
buffer := make([]byte, 65536)
// 통계 출력 고루틴
if *statsInterval > 0 {
go func() {
ticker := time.NewTicker(time.Duration(*statsInterval) * time.Second)
for range ticker.C {
stats.Print()
}
}()
}
for {
n, _, err := syscall.Recvfrom(fd, buffer, 0)
if err != nil {
log.Printf("Recvfrom error: %v", err)
continue
}
if n < 14 {
continue
}
packet := buffer[:n]
// VXLAN 패킷인지 확인
isVXLAN := false
vni := uint32(0)
if len(packet) >= 42 { // 최소 VXLAN 패킷 크기
// Quick check: UDP port 4789
etherType := binary.BigEndian.Uint16(packet[12:14])
if etherType == 0x0800 {
protocol := packet[23]
if protocol == 17 { // UDP
ihl := int(packet[14]&0x0F) * 4
udpStart := 14 + ihl
if len(packet) >= udpStart+8 {
dstPort := binary.BigEndian.Uint16(packet[udpStart+2 : udpStart+4])
if dstPort == 4789 {
isVXLAN = true
vxlanStart := udpStart + 8
if len(packet) >= vxlanStart+8 {
// VNI 추출
vni = uint32(packet[vxlanStart+4])<<16 |
uint32(packet[vxlanStart+5])<<8 |
uint32(packet[vxlanStart+6])
}
}
}
}
}
}
stats.Update(isVXLAN, vni, n)
// VNI 필터 적용
if *vniFilter >= 0 && int(vni) != *vniFilter {
continue
}
if isVXLAN {
parseVXLAN(packet)
}
}
}
func htons(v uint16) uint16 {
return (v<<8)&0xff00 | v>>8
}
func formatMAC(mac []byte) string {
return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5])
}
func printTCPFlags(flags byte) {
flagNames := []string{}
if flags&0x02 != 0 {
flagNames = append(flagNames, "SYN")
}
if flags&0x10 != 0 {
flagNames = append(flagNames, "ACK")
}
if flags&0x08 != 0 {
flagNames = append(flagNames, "PSH")
}
if flags&0x01 != 0 {
flagNames = append(flagNames, "FIN")
}
if flags&0x04 != 0 {
flagNames = append(flagNames, "RST")
}
for i, name := range flagNames {
if i > 0 {
fmt.Printf(",")
}
fmt.Printf("%s", name)
}
}컴파일 및 실행
# 컴파일
go build -o vxlan_parser vxlan_parser.go
# 실행 (모든 VXLAN 패킷)
sudo ./vxlan_parser -i eth0
# VNI 1만 필터링
sudo ./vxlan_parser -i eth0 -vni 1
# 10초마다 통계 출력
sudo ./vxlan_parser -i eth0 -stats 10테스트
다른 터미널에서:
# Pod 간 통신 생성
kubectl exec -it pod-a -- ping -c 10 10.244.2.10출력 예시:
╔════════════════════════════════════════════════════════════════╗
║ VXLAN PACKET DETECTED ║
╠════════════════════════════════════════════════════════════════╣
║ Outer (Underlay) Headers:
║ Ethernet: 00:50:56:c0:00:01 → 00:50:56:c0:00:08
║ IP: 192.168.1.10 → 192.168.1.20
║ UDP: 54321 → 4789
║
║ VXLAN Header:
║ VNI: 1
║ Flags: 0x08 (I=1)
║
║ Inner (Overlay) Headers:
║ Ethernet: aa:bb:cc:dd:ee:01 → aa:bb:cc:dd:ee:02
║ IP: 10.244.1.10 → 10.244.2.10
║ Protocol: ICMP (Echo Request (ping), code: 0)
╚════════════════════════════════════════════════════════════════╝트러블슈팅
문제 1: VXLAN 패킷이 캡처되지 않음
증상:
sudo tcpdump -i eth0 'udp port 4789' -c 10
# (패킷이 안 잡힘)원인 및 해결:
잘못된 인터페이스
bash# 모든 인터페이스 확인 ip link show # Flannel 인터페이스 확인 ip -d link show flannel.1 # 올바른 물리 인터페이스 사용 ip route get 8.8.8.8 # → dev eth0 src 192.168.1.10같은 Node의 Pod끼리 통신
- 같은 Node의 Pod는 VXLAN을 사용하지 않음 (로컬 브리지 사용)
bash# 다른 Node의 Pod와 통신해야 함 kubectl get pods -o wide # pod-a: Node 1 # pod-b: Node 2 # Node 1의 pod-a에서 Node 2의 pod-b로 kubectl exec -it pod-a -- ping 10.244.2.10CNI가 VXLAN 모드가 아님
bash# Flannel 설정 확인 kubectl get cm -n kube-system kube-flannel-cfg -o yaml # Backend Type이 "vxlan"인지 확인 # 만약 "host-gw"라면 VXLAN 사용 안 함
문제 2: MTU 관련 문제
증상:
- 작은 패킷(ping)은 되는데 큰 패킷(HTTP)은 안 됨
- "Destination Unreachable (Fragmentation needed)" 에러
원인: VXLAN 오버헤드(50 bytes) 때문에 MTU가 작아짐
해결:
# 1. VXLAN 인터페이스 MTU 확인
ip link show flannel.1
# mtu 1450
# 2. 물리 인터페이스 MTU 확인
ip link show eth0
# mtu 1500
# 3. Pod의 MTU 확인
kubectl exec -it pod-a -- ip link show eth0
# mtu 1450 (정상)
# 4. MTU가 1500이면 문제 → 1450으로 조정
kubectl exec -it pod-a -- ip link set eth0 mtu 1450
# 또는 CNI 설정에서 전역 설정
# /etc/cni/net.d/10-flannel.conflist
{
"mtu": 1450
}문제 3: VNI 충돌
증상: 여러 클러스터가 같은 물리 네트워크를 사용할 때 패킷이 잘못된 곳으로 감
해결:
# 클러스터마다 다른 VNI 사용
# Flannel 설정 수정
kubectl edit cm -n kube-system kube-flannel-cfg
# VNI 변경 (예: 100)
{
"Network": "10.244.0.0/16",
"Backend": {
"Type": "vxlan",
"VNI": 100, # ← 여기를 변경
"Port": 4789
}
}
# Flannel Pod 재시작
kubectl delete pods -n kube-system -l app=flannel문제 4: 방화벽 차단
증상: VXLAN 패킷이 전송되지 않음
해결:
# UDP 4789 포트 열기 (모든 Node에서)
sudo iptables -A INPUT -p udp --dport 4789 -j ACCEPT
sudo iptables -A OUTPUT -p udp --sport 4789 -j ACCEPT
# 또는 firewalld 사용 시
sudo firewall-cmd --permanent --add-port=4789/udp
sudo firewall-cmd --reload
# 확인
sudo iptables -L -n -v | grep 4789문제 5: FDB(Forwarding Database) 문제
증상: 패킷이 잘못된 Node로 전송됨
진단:
# FDB 확인
bridge fdb show dev flannel.1
# 잘못된 엔트리 삭제
sudo bridge fdb del <mac> dev flannel.1
# Flannel이 자동으로 다시 학습함심화 학습
VXLAN vs 다른 Overlay 기술
| 기술 | 방식 | 장점 | 단점 |
|---|---|---|---|
| VXLAN | L2 over UDP/IP | 표준, 호환성 좋음 | 50 bytes 오버헤드 |
| IPIP | IP over IP | 오버헤드 작음 (20 bytes) | L2 기능 없음 |
| WireGuard | Encrypted tunnel | 보안성 높음 | CPU 오버헤드 |
| Host-GW | Direct routing | 오버헤드 없음 | L2 adjacency 필요 |
VXLAN Offload (하드웨어 가속)
최신 NIC는 VXLAN 캡슐화/역캡슐화를 하드웨어에서 처리:
# VXLAN offload 지원 확인
ethtool -k eth0 | grep vxlan
# tx-udp_tnl-segmentation: on
# tx-udp_tnl-csum-segmentation: on
# 활성화
sudo ethtool -K eth0 tx-udp_tnl-segmentation on장점:
- CPU 부하 감소
- 처리량 향상
- 지연 시간 감소
eBPF를 사용한 VXLAN 처리
Cilium 같은 CNI는 eBPF를 사용하여 커널 바이패스:
// eBPF 프로그램 (pseudo-code)
SEC("tc")
int vxlan_encap(struct __sk_buff *skb) {
// 패킷 파싱
struct ethhdr *eth = (void *)(long)skb->data;
// VXLAN 헤더 추가
bpf_skb_adjust_room(skb, 50, BPF_ADJ_ROOM_MAC);
// Outer headers 설정
// ...
// 패킷 전송
return bpf_redirect(ifindex, 0);
}장점:
- Userspace/Kernel context switch 없음
- 매우 빠른 처리
- 프로그래밍 가능
정리
핵심 개념
VXLAN은 Overlay 네트워크 기술
- L2 (Ethernet)를 L3 (IP) 위에 터널링
- VNI로 16백만 개 이상의 네트워크 격리 가능
Kubernetes는 VXLAN을 Pod 간 통신에 사용
- 다른 Node의 Pod와 통신 시 자동으로 캡슐화
- 같은 Node의 Pod는 로컬 브리지 사용 (VXLAN 안 씀)
VXLAN 패킷 구조
- Outer: 물리 네트워크 헤더 (Node IP)
- VXLAN: 8 bytes (VNI 포함)
- Inner: 가상 네트워크 헤더 (Pod IP)
- 총 오버헤드: 50 bytes → MTU 1450
패킷 캡처
- UDP 포트 4789 필터링
- 물리 인터페이스(eth0)에서 캡처
- Wireshark/tcpdump로 분석
Golang 구현
- Raw Socket (AF_PACKET)으로 캡처
- Binary parsing으로 VXLAN 헤더 추출
- Inner/Outer 헤더 분리
다음 단계
성능 최적화
- VXLAN offload 활성화
- MTU 튜닝
- NIC bonding/teaming
보안 강화
- IPSec over VXLAN
- Network Policy 적용
- mTLS for Pod 통신
고급 네트워킹
- BGP를 사용한 라우팅
- Multi-cluster networking
- Service Mesh (Istio, Linkerd)
이제 Kubernetes의 VXLAN 네트워킹을 완벽히 이해하고 패킷 레벨에서 분석할 수 있게 되었습니다!