Bridge Networking
커널 안의 소프트웨어 switch인 Linux bridge와 VM, 컨테이너, KubeVirt가 거기 꽂히는 방식
Linux bridge는 커널 안에서 도는 소프트웨어 Ethernet switch다. VM의 tap과 컨테이너의 veth가 port로 꽂히고, bridge는 MAC address를 학습해 frame을 넘긴다.
Intro
Ethernet 글에서 switch가 하는 일을 봤다. 들어오는 frame의 source MAC을 기억해 두고, destination MAC을 보고 내보낼 port를 고른다.
물리 서버는 이 switch에 랜선으로 꽂힌다. 그런데 VM은 애매하다.
VM의 NIC은 QEMU가 만들어 준 가상 장치다. 랜선이 없다. guest가 보낸 frame이 밖으로 나가려면 어딘가에 꽂혀야 하는데, 그 꽂을 곳 자체가 소프트웨어여야 한다.
Linux bridge가 그 역할을 한다. 커널이 switch 한 대를 소프트웨어로 구현해 두고, 가상 NIC들을 port로 받아 준다.
- bridge가 어떻게 switch처럼 동작하는가?
- VM과 컨테이너가 각각 어떤 플러그(tap, veth)로 꽂히는가?
- KubeVirt의 bridge binding이 pod IP를 VM에 통째로 넘길 때 내부에서 무슨 일이 일어나는가?
하나씩 살펴보자.
A Switch in the Kernel
한 host 위에 VM 두 대가 떠 있고, 이 둘이 서로 통신해야 하는 상황이라고 가정해보자. 물리 세계라면 switch 한 대를 놓고 두 장비를 랜선으로 꽂으면 끝난다. Linux bridge를 사용하면 같은 일을 명령 몇 줄로 달성할 수 있다.
ip link add br0 type bridge # switch 한 대 생성
ip link set tap0 master br0 # VM A의 인터페이스를 port에 꽂기
ip link set tap1 master br0 # VM B의 인터페이스도 꽂기
master br0로 붙인 인터페이스는 그 순간부터 bridge의 port가 된다. 물리 NIC도, 가상 NIC도 똑같이 꽂을 수 있다. bridge 자신도 커널의 network device 중 하나라서, 만들고 나면 ip link에 다른 인터페이스와 나란히 조회된다.
지금 어떤 인터페이스가 꽂혀 있는지도 명령 하나로 확인된다.
ip link show master br0 # br0에 꽂힌 port 목록: tap0, tap1
tap0, tap1 같은 이름은 QEMU를 직접 띄우며 지정한 것이다. libvirt로 VM을 만들면 vnet0, vnet1처럼 자동으로 이름이 붙는데, 어느 tap이 어느 VM의 인터페이스인지는
virsh domiflist <vm>으로 확인할 수 있다.
이제 두 VM은 같은 switch에 꽂힌 두 장비처럼 frame을 주고받는다. 외부로 나가는 문은 아직 없는데, 이건 뒤에서 물리 NIC을 같은 bridge에 꽂으며 완성된다.
동작은 하드웨어 switch와 같다. Ethernet 글에서 말했듯 MAC address는 NIC마다 붙는 링크 계층 주소고, switch는 destination MAC을 보고 frame을 내보낼 port를 고른다.
Linux bridge에서 그 판단의 근거가 되는 표가 FDB(Forwarding Database)다.
Ethernet 글에서 MAC table이라 불렀던 바로 그 표다.
switch 벤더는 MAC address table, IEEE 표준은 filtering database라 부르는데, Linux는 표준 쪽 이름을 따랐다.
FDB는 “이 MAC address는 이 port 뒤에 있다”는 MAC → port 매핑을 담는다.
{
"diagram": "html-diagram",
"variant": "explainer",
"title": "Linux bridge: 커널 안의 switch",
"width": 960,
"height": 520,
"mobileWidth": 900,
"regions": [
{
"id": "host",
"label": "Host kernel",
"x": 36,
"y": 58,
"width": 888,
"height": 404
}
],
"nodes": [
{
"id": "br0",
"kind": "switch",
"label": "br0 (Linux bridge)",
"caption": "FDB: MAC → port 학습",
"x": 480,
"y": 210,
"width": 560,
"height": 64
},
{
"id": "tap0",
"kind": "nic",
"label": "tap0",
"caption": "VM A",
"x": 200,
"y": 360,
"width": 180,
"height": 60
},
{
"id": "tap1",
"kind": "nic",
"label": "tap1",
"caption": "VM B",
"x": 480,
"y": 360,
"width": 180,
"height": 60
},
{
"id": "eth0",
"kind": "nic",
"label": "eth0",
"caption": "물리 NIC",
"x": 760,
"y": 360,
"width": 180,
"height": 60
}
],
"links": [
{
"id": "in",
"points": [[200, 330], [200, 242]],
"tone": "primary",
"flow": { "speed": "normal", "count": 2, "emphasis": "strong" }
},
{
"id": "out",
"points": [[760, 242], [760, 330]],
"tone": "primary",
"flow": { "speed": "normal", "count": 2, "emphasis": "strong" }
},
{
"id": "idle",
"points": [[480, 330], [480, 242]],
"tone": "muted",
"dashed": true,
"flow": { "speed": "slow", "emphasis": "muted" }
}
],
"labels": [
{
"text": "FDB에 있으면 그 port로만, 없으면 모든 port로 flooding",
"x": 480,
"y": 440
}
]
}
frame이 어느 port로 들어오면 bridge는 두 가지 일을 한다.
- 학습: frame의 source MAC을 보고 “이 MAC은 이 port 뒤에 있다”고 FDB에 기록한다.
- 전달: destination MAC을 FDB에서 찾는다. 있으면 그 port로만 내보내고, 없으면(unknown unicast) broadcast와 마찬가지로 나머지 모든 port에 뿌린다. 이것이 flooding이다.
이 표는 직접 조회할 수 있다. VM A가 frame을 한 번이라도 보냈다면, 그 MAC이 어느 port 뒤에 있는지 학습된 것이 보인다.
bridge fdb show br br0
# 52:54:00:a1:b2:c3 dev tap0 VM A의 MAC은 tap0 뒤에 있다
FDB 항목은 영원하지 않다. 어떤 MAC에서 frame이 마지막으로 들어온 뒤 일정 시간이 지나면 항목을 지우는데, 이 ageing time의 기본값이 300초다. 장비가 다른 port로 옮겨 꽂혀도 표가 곧 따라잡는 이유다.
전달 판단이 Layer 2에서 끝난다는 점도 하드웨어 switch와 같다. bridge는 IP를 보지 않는다. 그래서 IP 위에 무엇이 실려 있든, 심지어 IP가 아닌 protocol이라도 투명하게 통과한다.
물리 switch끼리 고리를 만들면 broadcast가 무한히 도는 것처럼, bridge도 loop가 생기면 같은 문제를 겪는다. 그래서 STP(Spanning Tree Protocol)도 구현돼 있지만 기본은 꺼져 있다. 가상화 호스트 안의 bridge는 topology가 단순해 loop가 생길 일이 거의 없기 때문이다.
Two Kinds of Plugs: tap and veth
switch가 생겼으니 이제 꽂을 플러그가 필요하다. 가상 세계의 플러그는 두 종류다. tap 과 veth 를 알아보자.
tap은 커널과 user space 프로세스 사이를 잇는 가상 NIC이고, veth는 커널 안의 두 지점을 잇는 가상 케이블이다.
보통 tap은 VM을 bridge에 꽂을 때 쓰고, veth는 컨테이너를 꽂을 때 쓴다.
예시를 보자. 앞 섹션의 host에서 VM A는 그대로 두고, 그 옆에 컨테이너 하나를 새로 띄워 같은 br0에 꽂는다고 해보자.
bridge 입장에서는 둘 다 “port 하나 추가”로 똑같지만, port 반대편에 달린 플러그의 만듦새가 다르다.
{
"diagram": "html-diagram",
"variant": "explainer",
"title": "tap과 veth: 누가 frame을 나르나",
"width": 960,
"height": 560,
"mobileWidth": 900,
"regions": [
{
"id": "tap-side",
"label": "tap\nkernel ↔ user space",
"x": 36,
"y": 58,
"width": 430,
"height": 440
},
{
"id": "veth-side",
"label": "veth\nkernel ↔ kernel",
"x": 494,
"y": 58,
"width": 430,
"height": 440
}
],
"nodes": [
{
"id": "qemu",
"kind": "host",
"label": "QEMU 프로세스",
"caption": "user space에서 frame을 read/write",
"x": 251,
"y": 181,
"width": 300,
"height": 64
},
{
"id": "tap",
"kind": "nic",
"label": "tap device",
"caption": "/dev/net/tun 문자 장치",
"x": 251,
"y": 311,
"width": 300,
"height": 64
},
{
"id": "br-l",
"kind": "switch",
"label": "bridge port",
"caption": "kernel",
"x": 251,
"y": 441,
"width": 300,
"height": 64
},
{
"id": "ns",
"kind": "host",
"label": "container namespace",
"caption": "안에서는 eth0으로 보인다",
"x": 709,
"y": 181,
"width": 300,
"height": 64
},
{
"id": "veth",
"kind": "nic",
"label": "veth pair",
"caption": "양끝이 모두 커널 장치",
"x": 709,
"y": 311,
"width": 300,
"height": 64
},
{
"id": "br-r",
"kind": "switch",
"label": "bridge port",
"caption": "host namespace",
"x": 709,
"y": 441,
"width": 300,
"height": 64
}
],
"links": [
{
"id": "t1",
"points": [
[
251,
213
],
[
251,
279
]
],
"tone": "primary",
"flow": {
"speed": "normal",
"count": 2,
"emphasis": "strong"
}
},
{
"id": "t2",
"points": [
[
251,
343
],
[
251,
409
]
],
"tone": "primary",
"flow": {
"speed": "normal",
"count": 2,
"emphasis": "strong"
}
},
{
"id": "v1",
"points": [
[
709,
213
],
[
709,
279
]
],
"tone": "primary",
"flow": {
"speed": "normal",
"count": 2,
"emphasis": "strong"
}
},
{
"id": "v2",
"points": [
[
709,
343
],
[
709,
409
]
],
"tone": "primary",
"flow": {
"speed": "normal",
"count": 2,
"emphasis": "strong"
}
}
],
"labels": []
}
먼저 VM 쪽. tap은 반쪽짜리 인터페이스다.
한쪽 끝은 커널에 network device로 보이지만, 반대쪽 끝은 /dev/net/tun이라는 문자 장치(character device)로 user space에 열려 있다.
어떤 프로세스가 이 장치를 열고 read하면 커널이 그 인터페이스로 내보낸 Ethernet frame을 받고, write하면 커널에 frame을 밀어 넣는다.
QEMU가 정확히 이렇게 쓴다. guest가 virtio-net으로 내보낸 frame을 QEMU가 받아 tap에 write하면, 커널 입장에서는 tap0 인터페이스로 frame이 들어온 것이 된다.
반대 방향도 마찬가지다. VM의 네트워크 스택은 guest 커널 안에 있으므로, host 커널과 guest 사이를 user space의 QEMU가 날라 줘야 하고, 그 통로가 tap이다.
VM A 안의 앱이 외부 API를 호출한다고 하면, 그 HTTP 요청도 guest 커널에서 TCP/IP를 거쳐 결국 Ethernet frame으로 포장되고, 방금 본 경로를 그대로 타고 bridge까지 온다.
이번에는 컨테이너 쪽. veth는 양끝이 모두 커널 장치인 가상 케이블이다. 한쪽에 넣은 packet이 즉시 반대쪽으로 나온다. 주 용도는 network namespace의 경계를 잇는 것이다. 한쪽 끝을 컨테이너의 namespace에 넣어 eth0이라 부르고, 다른 쪽 끝을 host namespace의 bridge에 꽂는다.
컨테이너의 네트워크 스택은 host 커널 그 자체다. namespace로 시야만 갈라놨을 뿐이다. 그래서 커널을 벗어날 일이 없는 veth면 충분하고, user space를 거치는 tap이 필요 없다. VM은 반대로 스택이 guest 안에 있어 tap이 기본이 된다.
Wiring a VM to the LAN
이제 조립해 보자. VM A가 외부 API 서버를 호출하고, 반대로 외부 클라이언트가 VM A 안의 API 서버에 요청을 보내는 것까지 되어야 한다고 하자.
양방향이 다 열리는 가장 단순한 답은, bridge에 물리 NIC과 VM의 tap을 같이 꽂아 VM을 물리 LAN의 정식 구성원으로 만드는 것이다.
ip link add br0 type bridge
ip link set eth0 master br0 # 물리 NIC을 port로
ip link set tap0 master br0 # VM의 tap도 같은 bridge에
이때 조심할 것이 하나 있다. eth0이 bridge의 port가 되는 순간, eth0은 L2 전달만 하는 문이 된다. host의 IP는 이제 eth0이 아니라 br0에 붙여야 한다.
bridge 자체가 host의 network interface 역할을 이어받는 것이다.
이렇게 묶으면 VM A의 API 호출은 virtio-net → QEMU → tap0 → br0을 지나, FDB 판단에 따라 eth0 밖으로 나간다.
밖에서 보면 switch에 새 장비가 하나 꽂힌 것과 구분되지 않는다.
그래서 외부 클라이언트가 VM의 IP로 먼저 연결하는 것도 별도의 port 개방 설정 없이 그냥 된다. libvirt는 이 구성을 bridged mode라 부르고, “물리 머신과 똑같은 수준의 in/out 접근”이라고 설명한다.
단, 무선 인터페이스는 이 구성이 안 된다. WiFi는 AP에 등록된 MAC 하나로만 통신하도록 묶여 있어서, 여러 MAC을 대신 전달해야 하는 bridge port 역할을 할 수 없다.
NAT Mode: virbr0 and docker0
반대 패턴도 있다. bridge에 물리 NIC을 아예 꽂지 않는 것이다.
libvirt를 설치하면 생기는 virbr0가 이 방식이다. 물리 port가 없는 고립된 bridge를 만들고, 사설 대역(192.168.122.0/24)을 할당한다.
이 대역 안에서 IP를 나눠 주는 일은 DHCP가 맡는다.
DHCP란 새로 부팅한 장비가 “IP 좀 주세요”라고 broadcast하면, IP와 gateway, DNS 설정 한 벌을 자동으로 임대해 주는 protocol을 말한다.
그런데 이 bridge에는 물리 port가 없으니 broadcast가 LAN까지 나가지 못한다.
그래서 libvirt가 host 안에 dnsmasq라는 작은 DHCP 서버를 띄워 두고, guest들의 요청에 대신 답하게 한다.
밖으로 나가는 트래픽은 masquerade가 처리한다.
masquerade란 NAT의 한 형태로, 나가는 packet의 source IP를 guest의 사설 IP에서 host IP로 바꿔치기해 내보내는 것을 말한다.
커널은 이 연결을 기억해 두었다가(connection tracking), 응답이 돌아오면 주소를 되돌려 원래 guest에게 넘긴다.
이름 그대로 guest들이 host IP라는 가면을 쓰고 나가는 셈이다. libvirt는 이 규칙을 iptables에 심는다.
{
"diagram": "html-diagram",
"variant": "explainer",
"title": "NAT mode: 나가는 길은 masquerade, 들어오는 길은 없다",
"width": 960,
"height": 600,
"mobileWidth": 900,
"regions": [
{
"id": "host",
"label": "Host\n사설 대역 192.168.122.0/24",
"x": 36,
"y": 58,
"width": 628,
"height": 484
},
{
"id": "outside",
"label": "외부 네트워크\npublic IP 세계",
"x": 692,
"y": 58,
"width": 232,
"height": 484
}
],
"nodes": [
{
"id": "vm-a",
"kind": "nic",
"label": "VM A",
"caption": "192.168.122.11",
"x": 220,
"y": 202,
"width": 200,
"height": 60
},
{
"id": "vm-b",
"kind": "nic",
"label": "VM B",
"caption": "192.168.122.12",
"x": 480,
"y": 202,
"width": 200,
"height": 60
},
{
"id": "virbr0",
"kind": "switch",
"label": "virbr0",
"caption": "고립된 bridge, dnsmasq가 DHCP 응답",
"x": 350,
"y": 332,
"width": 460,
"height": 64
},
{
"id": "masq",
"kind": "memory",
"label": "masquerade (iptables)",
"caption": "src IP를 host IP로 바꿔 eth0으로",
"x": 350,
"y": 462,
"width": 460,
"height": 64
},
{
"id": "client",
"kind": "host",
"label": "외부 클라이언트",
"caption": "먼저 연결할 길이 없다",
"x": 808,
"y": 202,
"width": 184,
"height": 60
},
{
"id": "server",
"kind": "host",
"label": "외부 API 서버",
"caption": "host IP만 본다",
"x": 808,
"y": 462,
"width": 184,
"height": 60
}
],
"links": [
{
"id": "a-br",
"points": [
[
220,
232
],
[
220,
300
]
],
"tone": "primary",
"flow": {
"speed": "normal",
"count": 2,
"emphasis": "strong"
}
},
{
"id": "b-br",
"points": [
[
480,
232
],
[
480,
300
]
],
"tone": "muted",
"dashed": true,
"flow": {
"speed": "slow",
"emphasis": "muted"
}
},
{
"id": "br-masq",
"points": [
[
350,
364
],
[
350,
430
]
],
"tone": "primary",
"flow": {
"speed": "normal",
"count": 2,
"emphasis": "strong"
}
},
{
"id": "masq-out",
"points": [
[
580,
462
],
[
716,
462
]
],
"tone": "primary",
"flow": {
"speed": "normal",
"count": 2,
"emphasis": "strong"
}
},
{
"id": "blocked",
"points": [
[
716,
202
],
[
664,
202
]
],
"tone": "muted",
"dashed": true,
"flow": {
"speed": "slow",
"emphasis": "muted"
}
}
],
"labels": []
}
이 구성에서 VM끼리, 그리고 VM과 host는 자유롭게 통신하고, VM A가 외부 API를 호출하는 것도 잘 된다.
하지만 방향을 뒤집으면 막힌다. 외부 클라이언트가 VM A의 API를 호출하려 해도, VM의 사설 IP는 host 밖에서 보이지 않아 연결할 방법이 없다.
앞의 bridged mode와 정확히 이 지점에서 갈린다.
Docker의 docker0도 골격이 완전히 같다. 고립된 bridge, 사설 대역, egress는 iptables masquerade.
다른 점은 플러그가 tap이 아니라 veth pair라는 것, 그리고 외부에서 들어오는 길을 -p 8080:80 같은 port publish로 하나씩 뚫어 준다는 것 정도다.
VM이든 컨테이너든, “bridge에 꽂고 NAT로 내보낸다”는 설계는 하나다.
docker0도 결국 같은 Linux bridge라서, 앞에서 쓴 조회 명령이 Docker가 설치된 머신에서 그대로 통한다.
ip link show master docker0 # 컨테이너 수만큼 veth가 꽂혀 있다
bridge fdb show br docker0 # 컨테이너들의 MAC 학습 상태
컨테이너를 하나 띄울 때마다 vethXXXX 이름의 port가 하나씩 늘어나는 것을 볼 수 있다. VM 실습에서 tap이 늘어나던 자리에 veth가 있을 뿐이다.
bridge를 쓰는 방식은 크게 두 갈래다.
물리 NIC을 함께 꽂아 guest를 LAN의 peer로 만들거나(bridged), 고립된 bridge와 NAT로 guest를 사설망 뒤에 숨기거나(NAT mode).
Bridge in KubeVirt
KubeVirt는 VM을 virt-launcher pod 안에서 돌린다. 그러면 네트워크 경로는 세 구간으로 나뉜다.
- host까지: 노드를 물리 네트워크에 연결하는 것. 네트워크 장비와 host NIC의 몫이다.
- host에서 pod까지: pod에 인터페이스를 만들어 주는 것. CNI plugin의 몫이다.
- pod에서 guest까지: pod의 인터페이스와 VM의 가상 NIC을 잇는 것. KubeVirt는 이 마지막 구간을 binding이라 부른다.
CNI가 무엇을 해 주든, virt-launcher pod이 받는 것은 결국 인터페이스 하나와 IP 하나다.
문제는 그 IP의 주인이 pod이라는 점이다. 정작 통신해야 할 guest는 그 pod 안의 QEMU 뒤에 있다.
pod까지 온 트래픽을 guest까지 넘기는 부분이 binding이 푸는 문제다.
여기서 앞의 두 갈래가 그대로 재등장한다.
KubeVirt가 pod network에서 제공하는 대표 binding이 masquerade와 bridge인데, masquerade는 NAT mode의 축소판이고 bridge binding은 bridged mode의 축소판이다.
참고로 “bridge”라는 이름은 이 그림에서 두 번 나온다. CNI 중에도 bridge plugin이 있는데, 이것은 host namespace에 bridge를 만들어 pod들을 veth로 꽂는 host 쪽 장치다. KubeVirt의 bridge binding은 pod 안에 또 하나의 bridge를 만드는 pod 쪽 장치다. 같은 커널 기능을 다른 층에서 쓰는 것이다.
Bridge vs Masquerade Binding
두 binding이 pod 안에서 하는 일을 나란히 놓으면 이렇다.
{
"diagram": "html-diagram",
"variant": "explainer",
"title": "virt-launcher pod 안: masquerade vs bridge",
"width": 960,
"height": 620,
"mobileWidth": 900,
"regions": [
{
"id": "masq-side",
"label": "masquerade binding\npod network 기본 권장",
"x": 36,
"y": 58,
"width": 430,
"height": 500
},
{
"id": "bridge-side",
"label": "bridge binding\npod IP 위임",
"x": 494,
"y": 58,
"width": 430,
"height": 500
}
],
"nodes": [
{
"id": "eth-m",
"kind": "nic",
"label": "pod interface eth0",
"caption": "pod IP를 그대로 유지",
"x": 251,
"y": 211,
"width": 320,
"height": 64
},
{
"id": "nat",
"kind": "switch",
"label": "nftables NAT",
"caption": "선언한 port만 통과",
"x": 251,
"y": 341,
"width": 320,
"height": 64
},
{
"id": "vm-m",
"kind": "gpu",
"label": "VM",
"caption": "사설 IP를 DHCP로 수신",
"x": 251,
"y": 471,
"width": 320,
"height": 64
},
{
"id": "eth-b",
"kind": "nic",
"label": "pod interface",
"caption": "IP 없음, MAC까지 위임",
"x": 709,
"y": 211,
"width": 320,
"height": 64
},
{
"id": "br",
"kind": "switch",
"label": "bridge + tap",
"caption": "L2로 그대로 연결",
"x": 709,
"y": 341,
"width": 320,
"height": 64
},
{
"id": "vm-b",
"kind": "gpu",
"label": "VM",
"caption": "pod IP를 직접 사용, all ports",
"x": 709,
"y": 471,
"width": 320,
"height": 64
}
],
"links": [
{
"id": "m1",
"points": [
[
251,
243
],
[
251,
309
]
],
"tone": "primary",
"flow": {
"speed": "normal",
"emphasis": "soft"
}
},
{
"id": "m2",
"points": [
[
251,
373
],
[
251,
439
]
],
"tone": "primary",
"flow": {
"speed": "normal",
"emphasis": "soft"
}
},
{
"id": "b1",
"points": [
[
709,
243
],
[
709,
309
]
],
"tone": "primary",
"flow": {
"speed": "normal",
"count": 2,
"emphasis": "strong"
}
},
{
"id": "b2",
"points": [
[
709,
373
],
[
709,
439
]
],
"tone": "primary",
"flow": {
"speed": "normal",
"count": 2,
"emphasis": "strong"
}
}
],
"labels": [
{
"text": "live migration 가능",
"x": 251,
"y": 530
},
{
"text": "pod network에선 live migration 불가",
"x": 709,
"y": 530
}
]
}
masquerade binding은 pod 안에 작은 NAT mode를 차린다.
guest는 KubeVirt가 내부용으로 잡아 둔 사설 대역의 IP를 DHCP로 받고, pod interface의 IP는 pod이 그대로 갖는다.
guest가 내보내는 트래픽은 nftables 규칙이 pod IP로 source NAT하고, 들어오는 트래픽은 VM spec의 ports에 선언한 port만 guest로 forward된다.
클러스터의 다른 워크로드는 평소처럼 pod IP로 VM에 접근하면 된다.
bridge binding은 pod 안에 작은 bridged mode를 차린다.
pod 안에 Linux bridge를 만들고, CNI가 준 pod interface와 guest로 가는 tap을 양쪽에 꽂는다.
그리고 원래 pod interface가 갖고 있던 것들을 guest에 넘긴다.
MAC address가 VM 쪽으로 넘어가고, pod의 IPv4 주소는 KubeVirt가 pod 안에 띄운 DHCP 서버를 통해 guest에 위임된다.
guest는 DHCP를 돌리기만 하면 pod IP를 자기 주소로 받는다.
이 위임의 결과로 VM은 NAT 없이 pod IP의 온전한 주인이 된다.
ports 필드는 bridge binding에서 아무 효력이 없다. 골라 열 NAT 계층 자체가 없으니, pod IP의 모든 port가 그대로 VM의 port다.
VM을 IP 하나 가진 서버 한 대처럼 쓰는 그림이다.
공짜는 아니다. IP를 넘겨준 pod은 IP 없는 pod이 된다. pod IP가 pod에 붙어 있다고 가정하는 도구들, 예를 들어 Istio 같은 service mesh가 이 mode에서는 동작하지 못할 수 있다.
더 큰 제약은 live migration이다. pod network에서 bridge binding을 쓰면 live migration이 허용되지 않는다.
VM이 물려받은 IP는 특정 pod의 것이라서, 다른 노드의 새 pod으로 옮기면 그 IP를 가져갈 수 없기 때문이다.
그래서 KubeVirt 문서는 pod network에서는 masquerade를 기본으로 권하고, 관리자가 pod network의 bridge binding을 아예 금지하는 설정도 제공한다.
그럼에도 bridge binding을 고르는 경우는 뚜렷하다.
VM에 SSH로 들어가고, 임의의 port로 서비스를 열고, 분산 학습처럼 예측할 수 없는 port를 쓰는 워크로드라면, port를 하나씩 선언하는 masquerade보다 pod IP를 통째로 받는 bridge가 맞다.
GPU 클러스터에서 VM을 “손에 쥔 서버 한 대”로 제공하는 시나리오가 정확히 여기 해당한다.
이런 VM은 어차피 GPU passthrough 때문에 live migration이 안 되므로, bridge binding의 제약이 실질적으로 추가 비용이 아니게 된다.
두 mode 밖의 선택지도 있다. passt는 user space에서 L2와 L4 socket을 번역하는 binding으로, guest가 pod IP를 그대로 보면서 live migration도 되는 절충안이다. 그리고 pod network가 아닌 secondary network(Multus로 붙이는 추가 인터페이스)에서는 bridge binding이 오히려 표준이다. host의 bridge에 L2로 직접 붙는 용도라 NAT가 애초에 어울리지 않기 때문이다.
Summary
Linux bridge는 커널이 구현한 Ethernet switch다. FDB에 MAC → port를 학습하고, 아는 MAC은 해당 port로만, 모르는 MAC은 flooding으로 전달한다.
플러그는 두 종류다. VM은 user space의 QEMU가 frame을 나르는 tap으로, 컨테이너는 namespace 경계를 잇는 veth로 bridge에 꽂힌다.
물리 NIC을 같이 꽂으면 guest가 LAN의 peer가 되고(bridged), 물리 NIC 없이 NAT로 내보내면 guest가 사설망 뒤에 숨는다(NAT mode). libvirt의 virbr0와 Docker의 docker0가 후자다.
KubeVirt는 이 두 갈래를 pod 안에 옮겨 놓았다. masquerade binding은 pod 안의 NAT mode, bridge binding은 pod 안의 bridged mode다. bridge binding은 pod의 IP와 MAC을 guest에 위임해 all-ports짜리 서버 한 대를 만들어 주는 대신, pod network에서는 live migration을 포기한다.
References
- Linux Kernel Documentation — Bridge
https://docs.kernel.org/networking/bridge.html - Linux Kernel Documentation — TUN/TAP
https://docs.kernel.org/networking/tuntap.html - Linux Foundation Wiki — Bridge
https://wiki.linuxfoundation.org/networking/bridge - bridge(8) — Linux manual page
https://man7.org/linux/man-pages/man8/bridge.8.html - Red Hat Developer — Introduction to Linux interfaces for virtual networking
https://developers.redhat.com/articles/2026/04/03/introduction-to-linux-interfaces-for-virtual-networking - libvirt Wiki — Virtual Networking
https://wiki.libvirt.org/VirtualNetworking.html - Docker Docs — Bridge network driver
https://docs.docker.com/engine/network/drivers/bridge/ - KubeVirt — Interfaces and Networks
https://kubevirt.io/user-guide/network/interfaces_and_networks/ - KubeVirt — Network Binding Plugins
https://kubevirt.io/user-guide/network/network_binding_plugins/ - CNI — bridge plugin
https://www.cni.dev/plugins/current/main/bridge/