Underlay and Overlay
underlay는 packet을 실제로 전달하는 물리 network이고, 그 network의 router와 switch는 host IP만 알고 있다. overlay는 underlay 위에 따로 만든 주소 체계의 network이고, overlay packet은 host IP가 적힌 underlay packet 안에 통째로 들어가서 host 사이를 이동한다. 그래서 underlay 장비는 overlay 주소를 몰라도 된다. VXLAN은 overlay packet 앞에 붙이는 바깥 header의 형식을 정한 protocol이다.
Intro
L2 Switching and L3 Routing에서는 router가 routing table을 보고 다른 subnet으로 packet을 넘기는 과정을 봤고, Linux Bridge Networking에서는 host 한 대 안의 container가 bridge를 거쳐 통신하는 과정을 봤다. 이 글은 서로 다른 host에 있는 container끼리 통신하는 경우를 다룬다. switch, MAC address, ARP, router, subnet은 L2 Switching and L3 Routing 글에서, netns, veth, Linux bridge는 bridge 글에서 설명했으므로 여기서는 다시 설명하지 않는다.
글 전체에서 쓸 상황 하나를 정해 두자.
flowchart TB
accTitle: 이 글에서 쓰는 host 1, host 2, router R과 container A, B
accDescr: host 1 안의 container A(10.244.1.5)는 veth로 bridge cni0(10.244.1.1/24)에 연결되어 있고, host 1의 eth0는 192.168.1.10/24다. host 2 안의 container B(10.244.2.5)는 veth로 cni0(10.244.2.1/24)에 연결되어 있고, host 2의 eth0는 192.168.2.20/24다. 각 host 안에서 cni0와 eth0 사이는 host의 routing이 잇는다. router R은 eth0(192.168.1.1)로 host 1 쪽 subnet에, eth1(192.168.2.1)로 host 2 쪽 subnet에 연결되어 있다.
subgraph H1["Host 1"]
A["Container A<br/>10.244.1.5"]
C1["cni0<br/>10.244.1.1/24"]
E1["eth0<br/>192.168.1.10/24"]
end
subgraph H2["Host 2"]
B["Container B<br/>10.244.2.5"]
C2["cni0<br/>10.244.2.1/24"]
E2["eth0<br/>192.168.2.20/24"]
end
R["Router R<br/>eth0: 192.168.1.1<br/>eth1: 192.168.2.1"]
A ===|veth| C1
C1 -.-|routing| E1
B ===|veth| C2
C2 -.-|routing| E2
E1 --- R
E2 --- R
host 1과 host 2는 router R을 사이에 둔 서로 다른 subnet에 있다. host 1의 eth0는 192.168.1.10/24이고, host 2의 eth0는 192.168.2.20/24다. R은 eth0(192.168.1.1)로 host 1 쪽 subnet에, eth1(192.168.2.1)로 host 2 쪽 subnet에 연결되어 있고, 두 host는 각자 subnet에 있는 R의 address를 gateway로 쓴다.
container가 쓸 address는 10.244.0.0/16 대역으로 먼저 정해 두고, 이 대역을 host마다 /24 하나씩 나눠 준다. 10.244.0.0/16 안에는 이런 /24 대역이 256개 들어간다. host 1의 container는 10.244.1.0/24에서, host 2의 container는 10.244.2.0/24에서 address를 받는다.
container A(10.244.1.5)는 host 1의 bridge cni0(10.244.1.1/24)에, container B(10.244.2.5)는 host 2의 cni0(10.244.2.1/24)에 veth로 연결되어 있다.
두 host 모두 bridge 글의 Bridge as Gateway처럼 cni0를 container의 gateway로 쓰고 ip_forward를 켜 두었다. 그리고 container끼리 주고받는 packet에는 NAT를 쓰지 않는다. B가 받는 packet의 src IP는 A의 address 그대로여야 하기 때문이다.
목표는 A가 B에게 보낸 ping이 B에게 도착하는 것이다. 조건을 하나씩 바꿔 가며 이 packet이 어디까지 가는지 따라가 보자.
What is Underlay
Case 1: Host 1 pings host 2
먼저 host끼리 통신해 보자. host 1이 host 2(192.168.2.20)에게 ping을 보내면, host 1의 kernel은 “192.168.2.20? 내 subnet이 아니네. gateway 192.168.1.1에게 넘기자” 하고 R에게 frame을 보낸다. R의 routing table은 다음과 같다고 해보자.
R$ ip route
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.1
192.168.2.0/24 dev eth1 proto kernel scope link src 192.168.2.1
R은 “dst IP가 192.168.2.20이네. 192.168.2.0/24는 eth1에 바로 붙어 있다” 하고 packet을 eth1로 내보낸다. 이 ping은 L2 Switching and L3 Routing 글의 router 예시와 같은 과정을 거쳐 host 2에 도착하고, 응답도 같은 방식으로 돌아온다.
Case 2: A pings B
이번에는 A가 B(10.244.2.5)에게 ping을 보낸다. 이 packet을 처리하는 것은 host 1의 kernel이지만, kernel은 A의 netns에 있는 routing table을 본다.
kernel은 “10.244.2.5? A의 subnet(10.244.1.0/24)이 아니네” 하고 gateway인 cni0에게 frame을 보내고, cni0는 이 frame을 host 1의 root netns IP stack으로 올려 보낸다. 그런데 host 1의 routing table에는 10.244.2.0/24로 가는 route가 없다.
host1$ ip route
default via 192.168.1.1 dev eth0
10.244.1.0/24 dev cni0 proto kernel scope link src 10.244.1.1
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.10
host 1은 “default route에 걸리네. R에게 넘기자” 하고 packet을 eth0로 내보낸다. 이 packet의 src IP는 10.244.1.5이고, dst IP는 10.244.2.5다.
R은 frame에서 IP packet을 꺼내 routing table을 찾는다. “dst IP가 10.244.2.5? 내 routing table에는 그런 대역이 없는데.”
R의 routing table에는 Case 1에서 본 두 subnet route만 있고 default route도 없으므로, R은 이 packet을 버린다.
R은 보낸 쪽에 ICMP Destination Unreachable을 돌려주려 하지만, 그 message의 dst IP인 10.244.1.5로 가는 route도 R에게는 없다.
그래서 A의 ping은 아무 응답 없이 끝난다.
R에 internet으로 나가는 default route가 있었다면, packet은 버려지지 않고 R 바깥으로 나간다. 하지만
10.244.0.0/16은 사설 대역이라서, 그 packet을 host 2로 보내 줄 route는 어디에도 없다.
Case 1과 Case 2의 packet은 같은 eth0를 거쳐 같은 R에 도착했지만 결과가 달랐다. R은 packet을 어디로 보낼지 dst IP로 정하는데, Case 1의 dst IP는 R이 아는 대역에 속했고 Case 2의 dst IP는 R이 모르는 대역에 속했다.
host 1, host 2, R과 그 사이의 switch와 cable처럼 packet을 실제로 전달하는 물리 network를 underlay라고 부른다.
underlay 장비는 underlay에 속한 주소, 즉 host IP만 알고 있다.
R의 routing table에 있는 대역도 192.168.1.0/24와 192.168.2.0/24 두 개뿐이어서, dst IP가 host IP인 packet은 underlay를 지나가고 dst IP가 container IP인 packet은 R에서 버려진다.
Two Approaches
A의 packet이 B에게 도착하려면 R이 이 packet을 host 2 쪽으로 보내야 한다.
방법은 두 가지다.
- underlay에게 container 대역을 알려 주는 방법
- underlay가 container 대역을 몰라도 되게 만드는 방법
Case 1: R learns the container routes
R의 routing table에 container 대역으로 가는 route를 넣는다.
R$ ip route
10.244.1.0/24 via 192.168.1.10 dev eth0
10.244.2.0/24 via 192.168.2.20 dev eth1
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.1
192.168.2.0/24 dev eth1 proto kernel scope link src 192.168.2.1
이제 R은 “dst IP가 10.244.2.5네. 10.244.2.0/24는 192.168.2.20에게 넘기면 되겠다” 하고, host 2의 MAC address를 dst MAC에 적은 frame을 eth1로 내보낸다.
host 2는 dst IP가 자기 address가 아니지만 ip_forward가 켜져 있으므로, 이 packet을 cni0로 routing해서 B에게 넘긴다.
A가 보낸 packet은 TTL 말고는 아무것도 바뀌지 않은 채 B에게 도착한다.
route는 관리자가 R에 직접 넣을 수도 있고, BGP로 주고받을 수도 있다.
BGP(Border Gateway Protocol)는 “어느 대역으로 가려면 누구에게 넘기면 되는지”에 대한 route 정보를 주고받는 protocol이다.
Kubernetes CNI인 Calico의 BGP mode는 각 host가 자기 container 대역을 BGP로 물리 router에 알리고, encapsulation 없이 container IP 그대로 packet을 routing한다.
다만 이 방법을 쓰려면 underlay router의 설정을 바꿀 수 있어야 하고, host가 늘어나면 underlay가 관리하는 route도 함께 늘어난다.
Case 2: R never reads the container address
다른 방법은 R의 routing table을 그대로 두는 것이다. host 1은 A의 packet을 보내기 전에, 그 앞에 dst IP가 192.168.2.20(host 2)인 header를 하나 더 붙인다. R은 이 바깥 header의 dst IP만 보고 “192.168.2.20? 이건 알지” 하고 packet을 host 2로 보낸다.
host 2는 바깥 header를 떼어 내고 안에 있던 A의 packet을 B에게 넘긴다. R은 A의 packet에 적힌 10.244.2.5를 한 번도 읽지 않는다.
이렇게 underlay가 container 대역을 몰라도 되게 만든 network가 overlay다. host2는 이걸 어떻게 알고 처리하는지는 뒤에서 다룬다.
두 방법을 나란히 놓으면 다음과 같다.
| underlay에 route를 알려 준다 | overlay를 만든다 | |
|---|---|---|
| R의 routing table | container 대역 route가 추가된다 | 바뀌지 않는다 |
| R이 읽는 dst IP | 10.244.2.5 (B) | 192.168.2.20 (host 2) |
| underlay 설정 변경 | 필요하다 (static route, BGP) | 필요 없다 |
| packet에 더해지는 byte | 없다 | 바깥 header 50 byte (IPv4 VXLAN 기준) |
| underlay의 다른 machine에서 container IP로 접근 | R을 거쳐 가능하다 | 불가능하다 |
| 예 | Calico BGP mode | flannel VXLAN backend, Calico VXLAN mode |
단순히 “underlay가 몰라도 되게 만들면 overlay구나”로는 잘 와닿지 않는다. 본격적으로 알아보자.
What is Overlay
overlay는 underlay 위에 따로 만든 주소 체계의 network다. 이 글에서는 container A, B와 10.244.0.0/16 대역이 overlay에 속한다.
물리가 아닌, 논리적으로 구성된 네트워크라고 생각해도 좋다.
overlay packet은 underlay packet 안에 통째로 들어가서 host 사이를 이동하므로, underlay 장비는 overlay 주소를 몰라도 된다.
원래 packet 앞에 바깥 header를 붙이는 일을 encapsulation이라고 하고, 받는 쪽에서 바깥 header를 떼어 내고 원래 packet을 꺼내는 일을 decapsulation이라고 한다.
바깥 header의 형식은 여러 가지가 있는데, 이 글에서는 VXLAN(Virtual eXtensible Local Area Network)을 예로 든다.
VXLAN은 안쪽에 IP packet만 넣는 것이 아니라 Ethernet header를 포함한 frame 전체를 넣는다.
이제 host 1과 host 2에는 VXLAN encapsulation을 하는 interface vxlan0가 하나씩 생겼다.
vxlan0, eth0, cni0은 모두 Host1의 root netns에 있다. cni0->vxlan0는 host1의 routing table(L3)을 사용해서 연결된다. vxlan0와 eth0도 마찬가지로 routing table(L3) 방식으로 연결이 된다.
flowchart TB
accTitle: vxlan0를 추가한 host 1과 host 2
accDescr: host 1에서는 container A(10.244.1.5)가 veth로 cni0(10.244.1.1/24)에 연결되어 있고, cni0와 vxlan0(10.244.1.0, MAC …:11) 사이를 10.244.2.0/24 route가 잇는다. vxlan0가 만든 바깥 packet은 UDP 4789로 eth0(192.168.1.10/24)를 거쳐 router R로 나간다. host 2도 같은 구성으로 container B(10.244.2.5), cni0(10.244.2.1/24), vxlan0(10.244.2.0, MAC …:22), eth0(192.168.2.20/24)가 있고, cni0와 vxlan0 사이를 10.244.1.0/24 route가 잇는다.
subgraph H1["Host 1"]
A["Container A<br/>10.244.1.5"]
C1["cni0<br/>10.244.1.1/24"]
V1["vxlan0<br/>10.244.1.0<br/>MAC …:11"]
E1["eth0<br/>192.168.1.10/24"]
end
subgraph H2["Host 2"]
B["Container B<br/>10.244.2.5"]
C2["cni0<br/>10.244.2.1/24"]
V2["vxlan0<br/>10.244.2.0<br/>MAC …:22"]
E2["eth0<br/>192.168.2.20/24"]
end
R["Router R<br/>eth0: 192.168.1.1<br/>eth1: 192.168.2.1"]
A ===|veth| C1
C1 -.-|"route 10.244.2.0/24"| V1
V1 -.-|"routing (UDP 4789)"| E1
B ===|veth| C2
C2 -.-|"route 10.244.1.0/24"| V2
V2 -.-|"routing (UDP 4789)"| E2
E1 --- R
E2 --- R
vxlan0는 cni0의 port가 아니다. cni0의 port는 container의 veth뿐이고, 그림의 점선처럼 cni0와 vxlan0, vxlan0와 eth0 사이는 host의 routing table이 잇는다.
host 1의 routing table에는 route가 한 줄 늘었다. 그림에서 cni0와 vxlan0를 잇는 선이 이 route이고, host 2에도 반대 방향인 10.244.1.0/24 route가 같은 형태로 들어 있다.
host1$ ip route
default via 192.168.1.1 dev eth0
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 vxlan0 onlink
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.10
세 번째 줄은 “10.244.2.0/24로 가는 packet은 vxlan0로 내보내고, 다음 장비는 10.244.2.0으로 정하라”는 뜻이다.
10.244.2.0은 host 2의 vxlan0에 붙은 address이고, onlink는 이 gateway가 interface의 subnet에 속하지 않더라도 vxlan0에 바로 붙어 있는 것으로 다루라는 표시다.
즉 overlay에서 host 1의 다음 장비는 R이 아니라 host 2의 vxlan0다. 실제 packet은 물론 여전히 R을 거치지만, R은 바깥 header에만 나타난다. 안쪽 frame의 dst MAC에는 host2 vxlan0의 MAC address가 들어간다.
Encapsulation
Case 1: Host 1 adds the outer header
A가 다시 B에게 ping을 보낸다. A가 cni0로 보낸 frame이 host 1의 IP stack까지 올라오는 과정은 What is Underlay의 Case 2와 같고, 달라지는 것은 그다음이다. host 1 안에서 이 packet이 거치는 순서를 먼저 그림으로 보면 다음과 같다.
flowchart TB
accTitle: host 1 안에서 A의 ping에 바깥 header가 붙는 순서
accDescr: container A가 src 10.244.1.5, dst 10.244.2.5인 packet을 veth로 cni0에 보낸다. frame의 dst MAC이 cni0의 MAC address이므로 packet은 host 1의 IP stack으로 올라가고, routing table의 10.244.2.0/24 route가 vxlan0를 고른다. kernel은 dst MAC이 host 2 vxlan0(…:22)인 안쪽 frame을 만들어 vxlan0에 넘기고, vxlan0는 VXLAN, UDP, 바깥 IPv4 header를 붙인다. 바깥 dst IP가 192.168.2.20인 이 packet은 다시 routing table을 거쳐 default route로 eth0를 고르고, R의 MAC address를 dst MAC으로 적은 바깥 Ethernet header를 붙여 router R로 나간다.
subgraph H1["Host 1"]
subgraph NA["netns A"]
A["Container A<br/>10.244.1.5 → 10.244.2.5"]
end
C1["cni0"]
RT1["Routing table<br/>10.244.2.0/24 → vxlan0"]
V1["vxlan0<br/>adds VXLAN, UDP,<br/>outer IPv4"]
RT2["Routing table<br/>192.168.2.20 →<br/>default via eth0"]
E1["eth0"]
end
R["Router R"]
A -->|"1. veth"| C1
C1 -->|"2. up to IP stack"| RT1
RT1 -->|"3. inner frame<br/>dst MAC …:22"| V1
V1 -->|"4. outer packet<br/>dst 192.168.2.20"| RT2
RT2 -->|"5. outer Ethernet<br/>dst MAC: R"| E1
E1 --> R
host 1의 kernel은 “10.244.2.5? 10.244.2.0/24는 vxlan0 쪽의 10.244.2.0에게 넘기라고 되어 있네” 하고 packet의 TTL을 1 줄인 뒤 vxlan0로 내보낸다.
이때 kernel은 다른 interface로 내보낼 때와 마찬가지로 Ethernet header를 붙인다. src MAC은 host 1 vxlan0의 MAC address(…:11)이고, dst MAC은 다음 장비인 host 2 vxlan0의 MAC address(…:22)다.
이렇게 만든 frame이 안쪽 frame이 된다.
vxlan0는 이 frame을 받아 “dst MAC …:22는 192.168.2.20 쪽에 있다고 table에 적혀 있네” 하고, frame 앞에 header 세 개를 붙인다.
- VXLAN header(8 byte)에는 이 overlay를 가리키는 번호인 VNI를 적는다.
- UDP header(8 byte)의 dst port는 IANA가 VXLAN에 할당한 4789다.
- 바깥 IPv4 header(20 byte)의 src IP는
192.168.1.10, dst IP는192.168.2.20이다.
이렇게 만든 바깥 IP packet은 host 1의 routing table을 한 번 더 거친다. kernel은 “192.168.2.20? default route네” 하고, R의 MAC address를 dst MAC에 적은 바깥 Ethernet header를 붙여 eth0로 내보낸다. host 1의 eth0를 떠나는 packet은 다음과 같이 생겼다.
packet-beta
accTitle: host 1의 eth0를 떠나는 VXLAN packet의 byte 배치
accDescr: 0~13 byte는 바깥 Ethernet header, 14~33 byte는 바깥 IPv4 header(192.168.1.10에서 192.168.2.20), 34~41 byte는 UDP header(dst port 4789), 42~49 byte는 VXLAN header다. 50 byte부터는 host 1의 kernel이 vxlan0로 내보낸 안쪽 frame이 들어 있다. 50~63 byte는 안쪽 Ethernet header, 64~83 byte는 안쪽 IPv4 header(10.244.1.5에서 10.244.2.5), 그 뒤는 payload다. 그림은 payload의 앞부분까지만 그렸고, 바깥 frame의 FCS는 뺐다.
title VXLAN packet on host 1 eth0 (byte offsets)
0-13: "Outer Ethernet"
14-33: "Outer IPv4"
34-41: "UDP"
42-49: "VXLAN"
50-63: "Inner Ethernet"
64-83: "Inner IPv4"
84-95: "Payload"
앞의 50 byte가 host 1이 붙인 바깥 header이고, 그 뒤에 host 1의 kernel이 vxlan0로 내보낸 안쪽 frame이 들어 있다. 안쪽 IPv4 header에는 A가 적은 src IP와 dst IP가 그대로 남아 있다. vxlan0가 …:22의 위치를 어떻게 알았는지는 VTEP section에서 본다.
Case 2: R reads only the outer header
R의 eth0는 frame을 받고 “dst MAC이 내 거네” 하고 IP packet을 꺼낸다. R이 읽는 것은 바깥 IPv4 header다.
“dst IP가 192.168.2.20이네. host 2 쪽으로 보내면 되겠다.” R의 routing table은 What is Underlay에서 본 두 줄 그대로지만, 이번에는 dst IP가 R이 아는 대역에 속한다.
R은 바깥 IPv4 header의 TTL을 1 줄이고, 바깥 Ethernet header를 eth1 쪽에 맞게 새로 만들어 host 2로 보낸다.
UDP header 뒤의 VXLAN header와 안쪽 frame은 R에게 UDP payload일 뿐이므로, R은 그 안을 읽지도 바꾸지도 않는다. 그래서 안쪽 IPv4 header의 TTL도 R을 지나는 동안 그대로 남는다.
Case 3: Host 2 removes the outer header
host 2의 eth0가 frame을 받는다. host 2의 kernel은 “바깥 dst IP가 192.168.2.20, 내 address네. UDP dst port가 4789이니 vxlan0가 받을 packet이다” 하고 packet을 vxlan0에 넘긴다.
vxlan0는 VXLAN header의 VNI가 자기 VNI와 같은지 확인한 뒤, 바깥 Ethernet, IPv4, UDP, VXLAN header를 모두 떼어 낸다. 남은 안쪽 frame은 vxlan0로 수신된 frame으로 처리된다.
안쪽 frame의 dst MAC은 host 2 vxlan0의 MAC address이므로, kernel은 frame을 IP stack으로 올려 보내고 이때 처음으로 안쪽 IPv4 header를 읽는다.
“10.244.2.5는 cni0 쪽 subnet이네.” host 2는 TTL을 1 줄여 packet을 cni0로 보내고, B는 src IP가 10.244.1.5인 ping을 받는다.
B가 받은 packet에는 바깥 header가 없으므로, B는 이 packet이 VXLAN으로 encapsulation되어 왔다는 사실을 알 수 없다.
B의 응답은 같은 과정을 반대 방향으로 거치며, 이번에는 host 2가 dst IP 192.168.1.10을 적은 바깥 header를 붙인다.
같은 packet을 세 지점에서 비교하면 다음과 같다.
| 지점 | 바깥 dst IP | 안쪽 dst IP | 장비가 읽는 header |
|---|---|---|---|
출발 직후 (host 1 eth0) | 192.168.2.20 | 10.244.2.5 | host 1은 안쪽 IPv4 header로 vxlan0를, 바깥 IPv4 header로 eth0를 고른다 |
| underlay 위 (router R) | 192.168.2.20 | 10.244.2.5 | R은 바깥 Ethernet header와 바깥 IPv4 header만 읽는다 |
도착 직후 (host 2 eth0) | 192.168.2.20 | 10.244.2.5 | host 2는 바깥 header와 VNI를 확인해 떼어 내고, 안쪽 IPv4 header로 B를 찾는다 |
세 지점에서 바깥 dst IP와 안쪽 dst IP는 한 번도 바뀌지 않는다. 지점마다 달라지는 것은 그 지점의 장비가 어느 header를 읽느냐다. 안쪽 header를 읽는 장비는 host 1과 host 2뿐이고, 그 사이의 underlay 장비는 바깥 header만 읽는다.
Two Layers
같은 통신을 overlay 층과 underlay 층으로 나눠 그리면 다음과 같다.
flowchart TB
accTitle: overlay 층과 underlay 층으로 나눈 같은 topology
accDescr: 위쪽 overlay 층(10.244.0.0/16)에서는 host 1의 container A(10.244.1.5)가 host 2의 container B(10.244.2.5)에게 packet을 보내고, 안쪽 header의 주소는 10.244.1.5에서 10.244.2.5다. 아래쪽 underlay 층(192.168.x.x)에서는 host 1(192.168.1.10)이 router R을 거쳐 host 2(192.168.2.20)에게 packet을 보내고, 바깥 header의 dst IP는 192.168.2.20이다. host 1의 VTEP가 바깥 header를 붙이고, host 2의 VTEP가 바깥 header를 떼어 낸다. R은 바깥 header만 읽는다.
subgraph OV["Overlay: 10.244.0.0/16"]
direction LR
A["Container A<br/>10.244.1.5<br/>on Host 1"]
B["Container B<br/>10.244.2.5<br/>on Host 2"]
A -->|"inner dst 10.244.2.5"| B
end
subgraph UL["Underlay: 192.168.x.x"]
direction LR
H1["Host 1 (VTEP)<br/>192.168.1.10"]
R["Router R<br/>reads outer only"]
H2["Host 2 (VTEP)<br/>192.168.2.20"]
H1 -->|"outer dst 192.168.2.20"| R
R --> H2
end
OV ===|"encap at Host 1<br/>decap at Host 2"| UL
위층에서 A와 B는 10.244.0.0/16 주소로 통신하고, 아래층에서 실제로 이동하는 packet은 host 1에서 host 2로 가는 192.168.x.x packet이다. 두 층의 주소를 함께 다루는 장비는 바깥 header를 붙이는 host 1과 떼어 내는 host 2뿐이다. R은 아래층의 주소만 다루므로, overlay에 container가 늘어나도 R의 설정은 바뀌지 않는다.
VTEP
encapsulation과 decapsulation을 하는 지점을 VTEP(VXLAN Tunnel End Point)라고 부른다.
이 글에서는 host 1과 host 2의
vxlan0가 VTEP다.
이처럼 host의 kernel이 VTEP를 맡을 수도 있고, data center에서는 물리 switch가 VTEP를 맡아 자기에게 연결된 server의 frame을 encapsulation하기도 한다.
host 1의 vxlan0가 바깥 header를 붙이려면 바깥 dst IP를 정해야 한다.
다시 말해 안쪽 frame의 dst MAC …:22가 어느 host 뒤에 있는지 알아야 한다. Linux의 vxlan device는 이 정보를 FDB(forwarding database)에 MAC address와 원격 VTEP IP의 짝으로 저장한다.
host1$ bridge fdb show dev vxlan0
02:00:00:00:00:22 dst 192.168.2.20 self permanent
이 entry는 “dst MAC이 …:22인 frame은 바깥 dst IP를 192.168.2.20으로 정해 보내라”는 뜻이다. Linux bridge의 FDB는 MAC address와 bridge port를 짝지었지만, vxlan device의 FDB는 MAC address와 원격 VTEP의 IP address를 짝짓는다. 이 table을 채우는 방법은 두 가지다.
Case 1: Flood and learn
vxlan0의 FDB가 비어 있는 상태에서 dst MAC이 …:22인 frame이 들어왔다. vxlan0는 “이 MAC address는 table에 없네.
어느 VTEP로 보내야 할지 모르겠다” 하고, Linux bridge가 unknown unicast를 모든 port로 복사했던 것처럼 이 frame을 encapsulation해서 모든 VTEP에 보낸다.
ARP request 같은 broadcast frame도 같은 방식으로 모든 VTEP에 간다. underlay가 multicast를 지원하지 않으면 모든 VTEP의 IP를 미리 적어 두고 VTEP마다 복사본을 하나씩 보낸다.
host 2의 vxlan0는 이 packet을 decapsulation하면서 “안쪽 src MAC …:11이 바깥 src IP 192.168.1.10에서 왔네” 하고 FDB에 기록한다. host 2가 응답을 보내면 host 1의 vxlan0도 같은 방식으로 …:22와 192.168.2.20의 짝을 배운다.
이렇게 위치를 모르는 frame은 모든 VTEP에 보내고, 받은 packet의 바깥 src IP로 위치를 배우는 방식을 flood-and-learn이라고 한다. 미리 설정할 것은 적지만, 위치를 모르는 frame마다 모든 VTEP에 복사본이 간다. 또 underlay가 multicast를 지원하거나, VTEP마다 다른 VTEP의 목록을 갖고 있어야 한다.
Case 2: Control Plane fills the table
다른 방법은 traffic이 생기기 전에 FDB를 미리 채워 두는 것이다. 어느 container가 어느 host에 있는지는 container를 만든 쪽이 이미 알고 있으므로, 그 정보를 모든 VTEP에 미리 전달하면 flooding이 필요 없다. 이렇게 packet을 직접 전달하지 않고 전달 규칙만 정해 주는 부분을 Control Plane이라고 한다.
flannel이 이 방식을 쓴다. host마다 실행되는 flanneld는 Kubernetes API에서 다른 host의 container 대역, host IP, VTEP MAC address를 읽고, route와 ARP entry, FDB entry를 직접 써 넣는다. 앞에서 본 10.244.2.0/24 via 10.244.2.0 dev vxlan0 onlink route와 …:22 dst 192.168.2.20 entry가 이렇게 만들어지며, flannel의 vxlan device는 기본으로 MAC learning을 끈 상태로 동작한다. 물리 switch가 VTEP인 data center network에서는 BGP EVPN이 같은 정보를 VTEP 사이에 주고받는다.
VNI
VXLAN header는 8 byte이고, 그 안에서 값을 담는 부분은 I flag와 24 bit VNI(VXLAN Network Identifier)다. I flag는 VNI가 유효하다는 표시로 항상 1이고, 나머지 bit는 예약되어 있어 0으로 보낸다.
packet-beta
accTitle: VXLAN header의 bit 배치
accDescr: VXLAN header는 8 byte, 즉 64 bit다. 0~7 bit는 flag이며 그중 I flag가 1이어야 VNI가 유효하다. 8~31 bit는 예약 field, 32~55 bit는 24 bit VNI, 56~63 bit는 예약 field다.
title VXLAN header (bits)
0-7: "Flags (I)"
8-31: "Reserved"
32-55: "VNI (24 bits)"
56-63: "Reserved"
VNI는 overlay network를 구분하는 번호다. 같은 underlay 위에 overlay를 여러 개 만들면, VTEP는 받은 packet의 VNI를 보고 어느 overlay의 packet인지 정한다.
예를 들어 tenant X의 overlay가 VNI 100을, tenant Y의 overlay가 VNI 200을 쓰면, 두 overlay의 packet은 같은 host 1과 R을 지나도 받는 VTEP에서 서로 다른 overlay로 나뉜다.
그래서 두 tenant가 모두 10.244.0.0/16 대역을 써도 packet이 섞이지 않는다. Linux에서는 VNI마다 vxlan device를 하나씩 만들고, device를 만들 때 id option으로 VNI를 정한다.
VNI는 L2 Switching and L3 Routing 글의 VLAN ID와 같은 일을 한다. 둘 다 하나의 물리 network 위에서 L2 network를 나누는 번호이지만, VLAN ID는 12 bit라서 쓸 수 있는 VLAN이 4094개이고, VNI는 24 bit라서 개까지 구분할 수 있다. 또 VLAN tag는 경로 위의 모든 switch가 읽는 반면, VNI는 VTEP만 읽고 underlay 장비는 읽지 않는다.
Summary
이 글의 내용은 Two Layers의 그림 하나로 정리된다. 핵심은 다음과 같다.
- underlay는 packet을 실제로 전달하는 물리 network이고, 그 network의 장비는 host IP만 알고 있다. R의 routing table에는
192.168.1.0/24와192.168.2.0/24만 있어서, dst IP가 container IP인 packet은 R에서 버려진다. - overlay는 underlay 위에 따로 만든 주소 체계의 network이고, overlay packet은 underlay packet 안에 통째로 들어가서 host 사이를 이동한다. underlay 장비는 바깥 header의 host IP만 읽으므로 overlay 주소를 몰라도 된다.
- underlay에 container 대역 route를 알려 주는 방법(static route, BGP)도 있다. 이 방법은 encapsulation이 없지만, underlay router의 설정을 바꿀 수 있어야 한다.
- 바깥 header를 붙이고 떼어 내는 지점을 VTEP라고 한다. VTEP는 MAC address와 원격 VTEP IP의 짝을 FDB에 두고, 이 table을 flood-and-learn이나 Control Plane으로 채운다.
- VXLAN header의 24 bit VNI는 같은 underlay 위의 overlay를 서로 구분한다. 경로 위의 모든 switch가 읽는 VLAN tag와 달리, VNI는 VTEP만 읽는다.
References
- RFC 7348: Virtual eXtensible Local Area Network (VXLAN)
- Linux kernel documentation: Virtual eXtensible Local Area Networking
- ip-link(8): network device configuration
- ip-route(8): routing table management
- bridge(8): show / manipulate bridge addresses and devices
- Calico Docs: Determine best networking option
- Calico Docs: Overlay networking
- flannel Documentation: Backends
- flannel Documentation: Kubernetes