Skip to content

VXLAN in Kubernetes - 이론부터 패킷 분석까지

목차

  1. VXLAN이란 무엇인가?
  2. Kubernetes 네트워킹 기초
  3. VXLAN 패킷 구조
  4. Kubernetes에서 VXLAN 사용
  5. 실습 환경 구축
  6. VXLAN 패킷 캡처 및 분석
  7. Golang으로 VXLAN 파싱하기
  8. 트러블슈팅

VXLAN이란 무엇인가?

정의

VXLAN (Virtual eXtensible Local Area Network) 은 L2 (Ethernet) 네트워크를 L3 (IP) 네트워크 위에 터널링하는 기술입니다.

왜 필요한가?

전통적인 VLAN의 한계

┌─────────────────────────────────────────────────────┐
│           Traditional VLAN                          │
├─────────────────────────────────────────────────────┤
│                                                     │
│  - VLAN ID: 12 bits → 최대 4096개 네트워크          │
│  - L2 스위치에 의존                                  │
│  - 물리적 위치에 제약                                │
│  - 클라우드/가상화 환경에 부적합                      │
│                                                     │
└─────────────────────────────────────────────────────┘

문제점:

  1. 4096개 제한: 대규모 멀티테넌트 환경에서 부족
  2. L2 확장의 어려움: 데이터센터 간 L2 연결 복잡
  3. 물리적 의존성: 스위치 설정 필요

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는 다음 요구사항을 만족해야 합니다:

  1. 모든 Pod는 고유한 IP를 가져야 함
  2. 모든 Pod는 NAT 없이 서로 통신 가능
  3. 모든 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 설정

yaml
# 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 인터페이스가 생성됩니다:

bash
# 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
bash
# 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 = 1
  • local 192.168.1.10: 이 Node의 IP
  • dev eth0: Underlay 인터페이스
  • dstport 4789: VXLAN 포트

FDB (Forwarding Database) 확인

VTEP는 어느 Pod가 어느 Node에 있는지 알아야 합니다:

bash
# 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는 Node 192.168.1.20에 있음
  • Pod MAC aa:bb:cc:dd:ee:03는 Node 192.168.1.30에 있음

ARP 및 라우팅

bash
# 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 (로컬 테스트)

bash
# 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.0

Option 2: Kind (Kubernetes in Docker)

bash
# 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.yml

Option 3: 실제 서버 (권장)

bash
# 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 배포

yaml
# 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
bash
# 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-m03

VXLAN 패킷 캡처 및 분석

준비: tcpdump 설치

bash
# 모든 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에서:

bash
# 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: 트래픽 생성

다른 터미널에서:

bash
# Pod A에서 Pod B로 ping
kubectl exec -it pod-a -- ping -c 5 10.244.2.10

Step 3: 캡처 중지 및 분석

bash
# 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로 상세 분석

bash
# PCAP 파일을 로컬로 복사
scp user@node1:/tmp/vxlan-capture.pcap .

# Wireshark 실행
wireshark vxlan-capture.pcap

Wireshark에서 확인할 내용

  1. Frame 선택

    • VXLAN 패킷 하나 클릭
  2. 계층별 확인

    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)
  3. 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: 0
  4. MTU 오버헤드 확인

    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: 계층별 상세 분석

bash
# 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: 양방향 트래픽 분석

bash
# 양방향 캡처
sudo tcpdump -i eth0 -nn 'udp port 4789' -c 10 -v

동시에:

bash
# 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 트래픽 캡처

bash
# 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

분석:

bash
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 구현

go
// 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)
    }
}

컴파일 및 실행

bash
# 컴파일
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

테스트

다른 터미널에서:

bash
# 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 패킷이 캡처되지 않음

증상:

bash
sudo tcpdump -i eth0 'udp port 4789' -c 10
# (패킷이 안 잡힘)

원인 및 해결:

  1. 잘못된 인터페이스

    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
  2. 같은 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.10
  3. CNI가 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가 작아짐

해결:

bash
# 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 충돌

증상: 여러 클러스터가 같은 물리 네트워크를 사용할 때 패킷이 잘못된 곳으로 감

해결:

bash
# 클러스터마다 다른 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 패킷이 전송되지 않음

해결:

bash
# 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로 전송됨

진단:

bash
# FDB 확인
bridge fdb show dev flannel.1

# 잘못된 엔트리 삭제
sudo bridge fdb del <mac> dev flannel.1

# Flannel이 자동으로 다시 학습함

심화 학습

VXLAN vs 다른 Overlay 기술

기술방식장점단점
VXLANL2 over UDP/IP표준, 호환성 좋음50 bytes 오버헤드
IPIPIP over IP오버헤드 작음 (20 bytes)L2 기능 없음
WireGuardEncrypted tunnel보안성 높음CPU 오버헤드
Host-GWDirect routing오버헤드 없음L2 adjacency 필요

VXLAN Offload (하드웨어 가속)

최신 NIC는 VXLAN 캡슐화/역캡슐화를 하드웨어에서 처리:

bash
# 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를 사용하여 커널 바이패스:

c
// 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 없음
  • 매우 빠른 처리
  • 프로그래밍 가능

정리

핵심 개념

  1. VXLAN은 Overlay 네트워크 기술

    • L2 (Ethernet)를 L3 (IP) 위에 터널링
    • VNI로 16백만 개 이상의 네트워크 격리 가능
  2. Kubernetes는 VXLAN을 Pod 간 통신에 사용

    • 다른 Node의 Pod와 통신 시 자동으로 캡슐화
    • 같은 Node의 Pod는 로컬 브리지 사용 (VXLAN 안 씀)
  3. VXLAN 패킷 구조

    • Outer: 물리 네트워크 헤더 (Node IP)
    • VXLAN: 8 bytes (VNI 포함)
    • Inner: 가상 네트워크 헤더 (Pod IP)
    • 총 오버헤드: 50 bytes → MTU 1450
  4. 패킷 캡처

    • UDP 포트 4789 필터링
    • 물리 인터페이스(eth0)에서 캡처
    • Wireshark/tcpdump로 분석
  5. Golang 구현

    • Raw Socket (AF_PACKET)으로 캡처
    • Binary parsing으로 VXLAN 헤더 추출
    • Inner/Outer 헤더 분리

다음 단계

  1. 성능 최적화

    • VXLAN offload 활성화
    • MTU 튜닝
    • NIC bonding/teaming
  2. 보안 강화

    • IPSec over VXLAN
    • Network Policy 적용
    • mTLS for Pod 통신
  3. 고급 네트워킹

    • BGP를 사용한 라우팅
    • Multi-cluster networking
    • Service Mesh (Istio, Linkerd)

이제 Kubernetes의 VXLAN 네트워킹을 완벽히 이해하고 패킷 레벨에서 분석할 수 있게 되었습니다!