soobook
NETWORK

Macvlan

Macvlan은 물리 interface(parent) 위에 MAC address가 서로 다른 interface(child)를 여러 개 만드는 Linux kernel driver다.

Intro

Docker의 기본 bridge network에서 container는 host 안에서만 쓰는 private address를 받는다. 그래서 LAN의 다른 장비가 container에 닿으려면 host의 address로 요청을 보내고, host가 port forwarding으로 그 요청을 container에게 넘겨야 한다.

Macvlan을 쓰면 container가 LAN의 IP address와 자기 MAC address를 직접 받으므로, LAN의 장비는 container를 별도의 machine처럼 대하고 그 address로 바로 통신한다. bridge 글에서 본 bridged mode도 물리 NIC를 Linux bridge의 port로 붙여서 VM에 LAN address를 직접 줬다. 반면 Macvlan은 Linux bridge와 veth 없이 host의 NIC 위에 container용 interface를 바로 만든다.

Docker 문서는 오래된 application이나 network traffic을 감시하는 application처럼 물리 network에 직접 연결되어 있기를 기대하는 application에 macvlan network를 쓰라고 안내한다. Docker의 macvlan network driver와 CNI의 macvlan plugin이 이 kernel driver로 container와 Kubernetes Pod에 LAN address를 준다. Kubernetes에서는 주로 Multus가 Pod에 추가 interface(net1)를 붙일 때 CNI의 macvlan plugin을 쓰며, 그 구성은 Multus CNI에서 다뤘다.

다만 switch가 port마다 MAC address 수를 제한하거나 host가 Wi-Fi나 cloud VM으로 network에 연결되어 있으면, link 반대쪽 장비가 child의 MAC address를 받아들이지 않아서 Macvlan을 쓸 수 없다.

MAC address, switch의 MAC table, ARP, VLAN은 L2 Switching and L3 Routing 글에서, netns, veth, Linux bridge는 bridge 글에서 설명했으므로 여기서는 다시 설명하지 않는다.

What is Macvlan

Macvlan은 interface 하나(parent) 위에 MAC address가 서로 다른 가상 interface(child)를 여러 개 만드는 Linux kernel driver다.

child는 parent의 물리 NIC를 함께 쓰지만, 물리 network에서는 자기 MAC address와 IP address를 가진 별도의 장비로 보인다. 예를 들어 host H에서 실행 중인 container A에게 줄 child mv1은 다음 명령 한 줄로 만든다.

하나의 interface 밑에 가상의 child interface를 여러 개 만들고, child는 자기만의 MAC/IP address를 가진다!

$ ip link add link eth0 name mv1 type macvlan mode bridge

link eth0는 parent를 정하고, mode bridge는 child끼리 통신하는 방식을 정한다.

mode는 Modes section에서 따로 본다.

child의 MAC address를 따로 정하지 않으면 kernel이 임의로 만들어 주지만, 이 글에서는 알아보기 쉽도록 MAC address의 끝자리를 IP address의 마지막 숫자와 맞춰 정해두고 가자.

이렇게 만든 mv1을 container A의 netns로 옮기고 IP address를 준다. 같은 방식으로 mv2를 만들어 container B의 netns로 옮기면 다음 구성이 된다.

flowchart TB
  accTitle: Macvlan child mv1, mv2를 만든 뒤의 host H, host X와 switch
  accDescr: switch의 port p1에는 host H의 eth0(192.168.1.10/24, MAC 02:00:00:00:00:10)가, port p2에는 host X(192.168.1.20, MAC 02:00:00:00:00:20)가 연결되어 있다. H 위에는 eth0를 parent로 쓰는 Macvlan child mv1(192.168.1.51, MAC …:51)과 mv2(192.168.1.52, MAC …:52)가 있고, mv1은 container A의 netns에, mv2는 container B의 netns에 들어 있다.
  subgraph H["Host H"]
    subgraph NA["netns A"]
      MV1["mv1<br/>192.168.1.51<br/>MAC …:51"]
    end
    subgraph NB["netns B"]
      MV2["mv2<br/>192.168.1.52<br/>MAC …:52"]
    end
    ETH0["eth0 (parent)<br/>192.168.1.10/24<br/>MAC …:10"]
  end
  SW["Switch"]
  X["Host X<br/>192.168.1.20<br/>MAC …:20"]
  MV1 -.-|macvlan| ETH0
  MV2 -.-|macvlan| ETH0
  ETH0 ---|p1| SW
  SW ---|p2| X

H의 eth0는 switch의 port p1에, 같은 LAN의 host X는 port p2에 연결되어 있다.

Case 1: X asks for mv1’s MAC address

X가 mv1의 IP인 192.168.1.51로 ping을 보낸다. X는 mv1의 MAC address를 모르므로, 먼저 ARP request를 broadcast로 보낸다.

switch는 “src MAC …:20이 p2로 들어왔네” 하고 X의 MAC address를 MAC table에 기록한 뒤(MAC learning), 들어온 port를 뺀 모든 port로 frame을 복사한다. 그래서 이 frame은 p1을 거쳐 H의 NIC에 도착한다.

H의 kernel은 eth0로 들어온 frame을 IP stack에 올리기 전에 Macvlan driver에게 먼저 넘긴다.

이 순서가 필요한 이유는 eth0, mv1, mv2가 각각 root netns, netns A, netns B에 들어 있고, netns마다 IP stack이 따로 있기 때문이다. 그래서 kernel은 frame의 dst MAC으로 주인 interface를 먼저 골라야 어느 netns의 IP stack이 이 frame을 처리할지 정할 수 있다. 이 단계가 없으면 eth0로 들어온 frame은 모두 root netns의 IP stack으로 가고, dst MAC이 mv1의 MAC address인 frame은 eth0에게 온 것이 아니므로 버려진다.

Macvlan driver는 “dst MAC이 broadcast네. parent와 모든 child에게 복사하자” 하고 eth0, mv1, mv2에게 frame을 하나씩 넘긴다. 셋은 모두 ARP request가 묻는 IP를 확인하고, 그 IP가 자기 address인 mv1만 “그거 나야” 하고 ARP reply를 보낸다.

mv1의 ARP reply는 src MAC이 mv1의 MAC address(…:51)이고 dst MAC이 X의 MAC address(…:20)인 unicast frame이다.

이 frame은 eth0의 물리 NIC를 거쳐 나가지만, src MAC은 eth0의 MAC address로 바뀌지 않고 mv1의 MAC address 그대로 남는다.

switch는 “src MAC …:51이 p1로 들어왔네” 하고 mv1의 MAC address를 기록한 뒤 frame을 p2로 내보내고, X의 ARP cache에는 mv1의 IP와 MAC address가 짝지어 기록된다.

Case 2: X sends the ping

X는 dst MAC에 mv1의 MAC address(…:51)를 적어 ping을 보내고, switch는 MAC table에서 …:51이 p1 쪽에 있다는 기록을 찾아 이 frame을 p1로만 내보낸다.

H의 kernel이 frame을 다시 Macvlan driver에게 넘기면, driver는 “dst MAC이 …:51이네. 이건 mv1의 MAC이니 mv1로 넘기자” 하고 frame을 mv1에 넘긴다.

이후 처리는 A의 netns에 있는 IP stack이 맡고, ping 응답은 ARP reply와 같은 경로로 X에게 돌아간다.

mv2도 한 번이라도 frame을 내보내고 나면, switch의 MAC table은 다음과 같아진다.

MAC address          Port
02:00:00:00:00:10    p1
02:00:00:00:00:51    p1
02:00:00:00:00:52    p1
02:00:00:00:00:20    p2

port p1에는 eth0, mv1, mv2의 MAC address 세 개가 기록되어 있다. switch가 보기에 p1 뒤에는 장비 세 대가 연결되어 있고, 이 장비들이 실제로는 host H 한 대라는 사실은 switch가 알 수 없다. X가 보기에도 mv1은 LAN에 연결된 다른 machine과 구별되지 않는다.

H의 NIC가 eth0의 MAC address가 아닌 mv1의 MAC address로 온 frame을 버리지 않는 이유는 Where Macvlan Fails section에서 본다.

Bridge and Macvlan

여기서 비교하는 bridged mode는 eth0를 Linux bridge의 port로 붙이는 구성이다. Docker의 기본 bridge network도 Linux bridge(docker0)를 쓰지만 eth0는 port로 붙이지 않으므로, 이 비교에는 해당하지 않는다. container A를 bridged mode와 Macvlan으로 각각 LAN에 연결해 나란히 놓으면 다음과 같다.

flowchart LR
  accTitle: bridged mode와 Macvlan에서 container A가 LAN에 연결되는 구성
  accDescr: 왼쪽 bridged mode에서는 switch에 연결된 eth0가 address 없이 br0의 port가 되고, H의 address 192.168.1.10은 br0로 옮겨 간다. br0는 FDB를 MAC learning으로 채우고, container A의 eth0(192.168.1.51)는 veth pair로 br0와 이어진다. 오른쪽 Macvlan에서는 eth0가 192.168.1.10을 그대로 가지고, eth0를 parent로 쓰는 mv1(192.168.1.51)이 container A의 netns에 들어 있다. kernel은 dst MAC으로 mv1을 고르며, bridge와 veth는 없다.
  subgraph BRM["bridged mode"]
    direction TB
    S1["Switch"]
    E1["eth0<br/>port, no IP"]
    B1["br0<br/>192.168.1.10<br/>FDB learning"]
    A1["A: eth0<br/>192.168.1.51"]
    S1 --- E1
    E1 --- B1
    B1 ===|veth| A1
  end
  subgraph MVM["Macvlan"]
    direction TB
    S2["Switch"]
    E2["eth0 (parent)<br/>192.168.1.10"]
    M2["A: mv1<br/>192.168.1.51"]
    S2 --- E2
    E2 -.-|"pick by dst MAC"| M2
  end
  BRM ~~~ MVM

bridged mode에서는 eth0를 br0의 port로 붙이고, H 자신의 연결이 끊기지 않도록 H의 IP address와 default route를 eth0에서 br0로 옮겨야 했다. 또 container마다 veth pair를 하나씩 만들어 br0에 붙였다.

Macvlan에서는 eth0가 bridge port가 되지 않으므로, child를 만들어도 eth0의 address와 route는 그대로이고, container의 netns에는 veth 대신 child가 직접 들어간다.

frame을 받을 대상을 고르는 방식도 다르다. br0는 port 뒤에 어떤 MAC address가 있는지 미리 알지 못하므로, 들어오는 frame의 src MAC으로 FDB를 채우고(MAC learning) FDB에 없는 dst MAC은 모든 port로 복사한다(flooding).

FDB, Forwarding Database는 Linux bridge가 쓰는 MAC table이다.

반면 Macvlan driver가 찾는 MAC address는 child 자신의 MAC address뿐이고, kernel은 child를 만들 때 이미 그 MAC address를 알고 있다. 그래서 Macvlan driver는 MAC learning 없이 dst MAC으로 child를 바로 고르고, 어느 child의 MAC address와도 맞지 않는 frame은 parent에게 넘긴다.

child 뒤에 MAC address가 다른 장비를 붙이면, 그 장비로 가는 frame은 child가 아니라 parent로 간다. Macvlan driver가 아는 MAC address는 child 자신의 것뿐이기 때문이다.

Host and Child

Case 1: H pings mv1

이번에는 H 자신이 mv1에게 ping을 보낸다. mv1은 H 안에 있지만 netns A에 들어 있으므로, H의 root netns에서는 mv1이 보이지 않는다. root netns에서 LAN으로 이어진 interface는 eth0뿐이므로, root netns가 보기에 mv1의 IP(192.168.1.51)는 eth0 너머 LAN 어딘가에 있는 machine의 address다.

root netns의 routing table에는 다음 route가 있다.

H$ ip route
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.10

이 route는 “192.168.1.0/24에 속한 address는 eth0가 연결된 LAN에 바로 있으니 eth0로 보내라”는 뜻이다. 그래서 kernel은 “192.168.1.51은 eth0 쪽 LAN에 있네. eth0로 ARP request를 보내서 MAC address부터 알아내자” 하고 broadcast frame을 eth0로 내보낸다. 이 frame이 지나는 경로는 다음과 같다.

flowchart TB
  accTitle: host H가 eth0로 보낸 ARP request가 mv1에 닿지 않는 경로
  accDescr: host H의 root netns에서 kernel이 mv1의 IP 192.168.1.51로 ping을 보내려고 eth0로 ARP request를 내보낸다. 이 frame은 Macvlan driver를 거치지 않고 물리 NIC를 지나 switch의 port p1로 들어간다. switch는 broadcast frame을 p1을 뺀 port로만 복사하므로 frame은 p2의 host X에게 가고, frame이 들어온 p1로는 돌려보내지 않는다. mv1은 netns A에 있고 Macvlan으로 eth0에 연결되어 있지만, Macvlan driver는 eth0가 받은 frame만 child에게 넘기므로 mv1은 이 frame을 받지 못한다.
  subgraph H["Host H"]
    subgraph NA["netns A"]
      M["mv1<br/>192.168.1.51"]
    end
    subgraph ROOT["root netns"]
      R["ping 192.168.1.51"]
      E["eth0"]
    end
  end
  SW["Switch"]
  X["Host X"]
  M -.-|"macvlan<br/>receive only"| E
  R -->|"ARP request"| E
  E -->|"out via p1"| SW
  SW -->|"copy to p2"| X
  SW -.-x|"never back via p1"| E

Macvlan driver가 frame을 child에게 나눠 주는 시점은 eth0가 frame을 받았을 때뿐이다. H의 kernel이 eth0로 내보낸 frame은 Macvlan driver를 거치지 않고 바로 물리 NIC로 나가서 switch의 p1에 도착한다. switch는 broadcast frame을 p1을 뺀 모든 port로 복사하므로 frame은 X에게만 가고, X는 “내 IP가 아니네” 하고 무시한다. 그래서 mv1은 ARP request를 받지 못하고, H의 ping은 Destination Host Unreachable로 끝난다.

H가 mv1의 MAC address를 이미 알고 있어도 결과는 같다. dst MAC이 mv1의 MAC address(…:51)인 unicast frame이 p1로 들어오면, switch는 “…:51은 p1 쪽에 있네. 그런데 이 frame도 p1로 들어왔으니, 들어온 port로 다시 내보낼 수는 없다” 하고 frame을 버린다.

반대 방향도 마찬가지다. mv1이 eth0의 MAC address를 알아내려고 ARP request를 보내면, bridge mode의 Macvlan driver는 이 frame을 다른 child에게 복사하고 물리 NIC로 내보내지만 parent의 IP stack에는 넘기지 않는다. 물리 NIC로 나간 frame은 위와 같은 이유로 H에게 돌아오지 않는다.

switch 대신 Linux bridge나 Open vSwitch 같은 vSwitch가 있어도 결과는 같다. 이런 장비도 frame을 들어온 port로 돌려보내지 않기 때문이다. 다만 Linux bridge의 port에 hairpin mode가 켜져 있으면 bridge가 ARP request를 eth0로 돌려보내고, Macvlan driver가 돌아온 frame을 mv1에게 넘기므로 ping이 성공한다. vepa mode에서 다루는 reflective relay와 같은 동작이다.

Case 2: H gets its own child

해결 방법은 H에도 Macvlan child를 하나 만들고, child와 통신할 때는 그 child를 쓰는 것이다.

“이미 mv1과 mv2가 eth0의 child가 아닌가?” 할 수 있지만 둘은 현재 다른 netns에 있으니, root netns에도 Macvlan child를 만들어주자는 것.

H의 root netns에 bridge mode child mv0(192.168.1.50/32, MAC …:50)를 만들고, mv1과 mv2로 가는 route를 mv0 쪽으로 넣는다.

H$ ip route
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.10
192.168.1.51 dev mv0 scope link
192.168.1.52 dev mv0 scope link

둘째 줄과 셋째 줄이 mv1과 mv2의 IP로 가는 route다. H가 다시 mv1에게 ping을 보내면, kernel은 “192.168.1.51만 가리키는 route가 /24 route보다 길게 일치하네. mv0로 보내자” 하고 mv0의 IP를 src IP로 쓴다.

bridge mode의 child끼리는 frame이 kernel 안에서 바로 전달되므로, mv0가 보낸 ARP request는 mv1에 도착하고 mv1의 ARP reply도 같은 방식으로 mv0에 도착한다. 이제 H와 mv1 사이의 frame은 물리 NIC로 나가지 않는다.

child가 보내는 frame은 반드시 Macvlan driver를 거치고, Macvlan driver는 같은 parent에 붙은 child의 MAC address를 모두 알고 있다.

Docker 문서도 macvlan network의 container와 host가 통신해야 할 때, 같은 parent 위에 host용 macvlan interface를 만들어 address를 주는 방법을 안내한다. 이 방법은 child끼리 H 안에서 frame을 주고받는 bridge mode를 전제로 한다.

Modes

Macvlan의 mode는 같은 parent 위의 child끼리 frame을 어떻게 주고받는지를 정한다.

ip link가 받는 mode는 bridge, vepa, private, passthru, source 다섯 가지다.

ip link add에서 mode를 생략하면 vepa가 되지만, CNI의 macvlan plugin은 mode를 생략하면 bridge를 쓴다.

mv1이 mv2에게 ping을 보낼 때, 앞의 세 mode에서 frame이 지나는 경로는 다음과 같다.

passthru와 source는 다루지 않음.

flowchart LR
  accTitle: Macvlan mode별로 mv1이 mv2에게 보낸 frame이 지나는 경로
  accDescr: bridge mode에서는 mv1이 보낸 frame을 kernel이 물리 NIC로 내보내지 않고 mv2에게 바로 넘긴다. vepa mode에서는 frame이 eth0의 물리 NIC를 거쳐 switch의 port p1로 나가고, p1에 reflective relay가 켜져 있으면 switch가 frame을 p1로 돌려보내며, kernel이 dst MAC을 보고 mv2를 고른다. private mode에서는 mv1의 frame이 mv2에 전달되지 않는다.
  subgraph BR["bridge"]
    direction TB
    B1["mv1"] -->|"in kernel"| B2["mv2"]
  end
  subgraph VE["vepa"]
    direction TB
    V1["mv1"] -->|"out via p1"| VS["Switch<br/>reflective<br/>relay"]
    VS -->|"back via p1"| V2["mv2"]
  end
  subgraph PR["private"]
    direction TB
    P1["mv1"] -.-x|"blocked"| P2["mv2"]
  end
  BR ~~~ VE ~~~ PR

Case 1: bridge mode

mv1이 보낸 frame은 물리 NIC로 나가지 않는다. Macvlan driver는 “dst MAC …:52는 mv2의 MAC이네. 같은 parent의 bridge mode child이니 NIC로 내보낼 필요 없이 바로 넘기자” 하고 frame을 mv2에 넘긴다. broadcast frame은 다른 bridge mode child에게 복사하는 동시에 물리 NIC로도 내보낸다. Host and Child의 Case 2에서 mv0와 mv1이 H 안에서 frame을 주고받은 것도 이 동작 덕분이다.

Case 2: vepa mode

VEPA(Virtual Ethernet Port Aggregator)는 child가 보낸 frame을 dst와 관계없이 모두 물리 NIC로 내보낸다.

mv1의 frame은 p1을 거쳐 switch로 간다. switch는 MAC table에서 dst MAC인 mv2의 MAC address(…:52)를 찾고 “p1 쪽에 있네. 그런데 이 frame도 p1로 들어왔는데?” 하고 frame을 들어온 port로 돌려보낼지 정해야 한다.

보통의 switch는 이 frame을 버리고, p1에 reflective relay(hairpin mode라고도 부른다)를 켜 둔 switch만 frame을 p1로 다시 내보낸다.

돌아온 frame은 Macvlan driver가 dst MAC으로 mv2를 골라 넘긴다.

vepa mode에서는 child끼리 주고받는 frame도 모두 switch를 지나므로, switch에 설정한 ACL이나 traffic monitoring을 child 사이의 traffic에도 적용할 수 있다. 다만 switch가 reflective relay를 지원하지 않으면 child끼리는 통신하지 못한다.

Case 3: private mode

private mode는 child끼리의 통신을 막는다.

vepa mode처럼 frame을 물리 NIC로 내보내지만, switch가 reflective relay로 frame을 돌려보내더라도 Macvlan driver는 private child가 보낸 broadcast frame을 다른 child에게 넘기지 않는다.

그래서 mv1의 ARP request가 mv2에 닿지 않고, 두 child는 H 바깥의 장비와만 통신할 수 있다.

Macvlan over VLAN

VLAN(Virtual LAN)은 switch 한 대의 port를 여러 개의 독립된 L2 network로 나눈다.

host가 연결된 port는 보통 VLAN 하나에만 속하는 access port다. switch는 access port로 들어온 frame을 그 port에 설정된 VLAN의 frame으로 처리하므로, host는 VLAN 정보가 없는 frame만 주고받고 자기가 어느 VLAN에 있는지도 모른다.

반면 switch끼리 잇는 cable처럼 여러 VLAN의 frame이 함께 지나가는 port에서는, switch가 frame마다 VLAN 번호(VLAN ID)를 적은 802.1Q tag를 붙여서 VLAN을 구분한다. 이런 port를 trunk port라고 한다. 받는 쪽은 tag를 보고 frame의 VLAN을 알아내고, frame을 access port로 내보낼 때는 tag를 뗀다. 자세한 내용은 L2 Switching and L3 Routing 글의 VLAN section에서 다뤘다.

지금까지 H의 eth0는 X가 있는 LAN 하나에만 연결되어 있었다. 이번에는 같은 H에서 실행 중인 container C를 VLAN 100에 연결해야 한다고 하자.

flowchart TB
  accTitle: VLAN sub-interface eth0.100을 parent로 쓰는 Macvlan child mv3
  accDescr: host H에는 eth0 위에 VLAN 100의 sub-interface eth0.100이 있다. eth0를 parent로 쓰는 mv1은 netns A에, eth0.100을 parent로 쓰는 mv3(192.168.100.53)는 netns C에 들어 있다. eth0.100은 frame을 eth0에 넘길 때 VLAN 100 tag를 붙이고, eth0에게서 넘겨받을 때 tag를 뗀다. eth0는 switch의 trunk port p1에 연결되어 있어서, 원래 LAN의 frame은 tag 없이, VLAN 100의 frame은 tag를 단 채로 p1을 지난다. switch의 p2에는 원래 LAN의 host X가, VLAN 100의 access port p3에는 host Y(192.168.100.30)가 연결되어 있고, 두 port를 지나는 frame에는 tag가 없다.
  subgraph H["Host H"]
    subgraph NA["netns A"]
      MV1["mv1<br/>192.168.1.51"]
    end
    subgraph NC["netns C"]
      MV3["mv3<br/>192.168.100.53"]
    end
    VLAN["eth0.100"]
    ETH0["eth0"]
  end
  SW["Switch"]
  X["Host X<br/>192.168.1.20"]
  Y["Host Y<br/>192.168.100.30"]
  MV1 -..-|macvlan| ETH0
  MV3 -.-|macvlan| VLAN
  VLAN ---|"add / strip<br/>VLAN 100 tag"| ETH0
  ETH0 ===|"p1 trunk<br/>untagged +<br/>VLAN 100 tag"| SW
  SW ---|"p2<br/>untagged"| X
  SW ---|"p3 VLAN 100<br/>untagged"| Y

VLAN 100은 X가 있는 LAN과 분리된 L2 network이고 subnet 192.168.100.0/24를 쓴다. 같은 switch의 p3에 연결된 host Y가 VLAN 100에 속해 있다. H가 두 network의 frame을 eth0 하나로 주고받으려면, p1을 trunk port로 설정해서 원래 LAN의 frame은 지금처럼 tag 없이, VLAN 100의 frame은 tag를 붙인 채로 주고받게 해야 한다.

그러면 H도 eth0로 들어온 frame 중 VLAN 100 tag가 붙은 frame을 따로 골라내야 한다. 그림의 eth0.100이 이 일을 하는 VLAN sub-interface다. Macvlan은 물리 interface 대신 VLAN sub-interface를 parent로 쓸 수 있으므로, container C에 줄 mv3는 eth0.100 위에 만든다.

H$ ip link add link eth0 name eth0.100 type vlan id 100
H$ ip link add link eth0.100 name mv3 type macvlan mode bridge

첫 줄은 eth0 위에 VLAN 100의 sub-interface eth0.100을 만든다. eth0.100은 자기가 내보내는 frame에 VLAN 100 tag를 붙여 eth0에 넘기고, eth0가 받은 frame 중에서는 VLAN 100 tag가 붙은 frame만 tag를 떼고 넘겨받는다. 둘째 줄은 eth0.100을 parent로 쓰는 child mv3를 만든다. mv3를 container C의 netns로 옮기고 VLAN 100의 IP address를 주면 앞의 그림과 같은 구성이 된다.

앞의 그림에서 tag가 붙은 frame이 지나는 구간은 eth0.100과 eth0 사이, 그리고 eth0와 switch 사이뿐이다. Y가 mv3에게 ping을 보낼 때, echo request와 echo reply가 이 구간을 어떻게 지나는지 방향별로 살펴보자. Y는 mv3의 MAC address를 이미 알고 있다고 하자.

Case 1: Y pings mv3

Y는 tag가 없는 frame을 p3로 보낸다. switch는 “p3는 VLAN 100의 access port이니, 이 frame은 VLAN 100의 frame이다” 하고 이 frame을 VLAN 100에 속한 port로만 전달한다. MAC table을 보면 dst MAC인 mv3의 MAC address는 p1 쪽에 있으므로, switch는 frame을 p1로 내보낸다. p1은 trunk port이므로 이때 frame에 VLAN 100 tag를 붙인다.

eth0가 이 frame을 받으면 kernel은 “tag에 적힌 VLAN이 100이네. eth0.100에게 넘길 frame이구나” 하고 tag를 떼어 eth0.100에 넘긴다. eth0.100은 mv3의 parent이므로, 그다음 과정은 eth0 위의 child와 같다. Macvlan driver가 dst MAC으로 mv3를 골라 frame을 넘긴다.

Case 2: mv3 replies to Y

mv3가 보내는 echo reply는 Case 1과 반대 방향으로 지나간다. frame은 Macvlan driver를 거쳐 parent인 eth0.100으로 가고, eth0.100은 VLAN 100 tag를 붙여 eth0에 넘긴다. eth0는 이 frame을 물리 NIC로 내보낸다. switch는 p1으로 들어온 frame의 tag를 보고 VLAN 100의 frame임을 알고, Y가 연결된 p3는 access port이므로 tag를 떼고 frame을 내보낸다.

두 Case 모두 tag를 붙이고 떼는 일은 eth0.100과 switch가 한다. 그래서 Y와 container C는 tag를 한 번도 보지 않고, container C는 아무 VLAN 설정 없이 VLAN 100에 속한다.

eth0 위의 mv1과 eth0.100 위의 mv3는 같은 H 안에 있고 둘 다 bridge mode이지만, 서로 다른 L2 network에 있다. 두 child는 parent가 다르므로 Macvlan driver도 둘 사이의 frame을 H 안에서 바로 넘기지 않는다. 그래서 둘 사이의 통신은 router를 거쳐야 한다.

Where Macvlan Fails

Macvlan child가 물리 network에 나타나려면, link 하나로 MAC address 여러 개의 frame이 오가는 상황을 link 양쪽 장비가 모두 받아들여야 한다. 한쪽은 H의 NIC이고, 다른 쪽은 p1 같은 switch port나 Wi-Fi AP, cloud의 가상 switch다.

먼저 NIC 쪽을 보자. L2 Switching and L3 Routing 글에서 봤듯이 NIC는 dst MAC이 자기 MAC address가 아닌 unicast frame을 버린다.

그래서 kernel은 child가 켜질 때 child의 MAC address를 parent의 unicast address 목록에 추가하고, NIC는 이 목록을 hardware unicast filter에 넣어서 그 MAC address의 frame도 받는다.

NIC에 unicast filter가 없으면 kernel은 NIC를 promiscuous mode로 바꾸고, filter에 넣을 수 있는 MAC address 수가 모자랄 때도 많은 driver가 같은 일을 한다. promiscuous mode의 NIC는 dst MAC과 관계없이 link의 모든 frame을 받으므로, kernel이 필요 없는 frame을 직접 걸러야 해서 성능이 떨어질 수 있다.

H의 NIC가 이 조건을 만족하더라도, link 반대쪽 장비가 MAC address 수를 제한하면 Macvlan은 동작하지 않는다.

Case 1: The switch allows one MAC per port

관리자는 허가되지 않은 장비의 연결을 막으려고 switch port마다 MAC address 수를 제한하기도 한다. Cisco switch의 port security가 이런 기능이다. p1에 MAC address 하나만 허용하도록 설정되어 있으면, switch는 eth0의 MAC address(…:10)를 먼저 배운 뒤, mv1의 ARP reply가 들어올 때 “p1에는 MAC address 하나만 허용되는데, …:10에 이어 …:51이 또 들어왔네” 하고 이를 위반으로 처리한다. 설정에 따라 switch는 새 MAC address의 frame만 버리기도 하고, p1 자체를 꺼서 H의 eth0까지 통신하지 못하게 만들기도 한다.

Case 2: H joins Wi-Fi

이번에는 H가 cable 대신 Wi-Fi로 AP에 연결되어 있다고 해보자. Wi-Fi 단말은 AP에 연결(association)할 때 자기 MAC address 하나를 등록하고, AP는 등록된 MAC address로 오가는 frame만 처리한다. H가 등록한 MAC address는 eth0의 MAC address(…:10)뿐이므로, AP는 mv1의 MAC address(…:51)를 src MAC으로 쓴 frame을 받지 않고, mv1에게 가는 frame을 H에게 보내지도 않는다. CNI의 macvlan plugin 문서도 대부분의 Wi-Fi NIC는 Macvlan의 parent로 쓸 수 없다고 안내한다. bridge 글의 bridged mode도 같은 이유로 Wi-Fi에서는 동작하지 않는다.

Case 3: H is a cloud VM

이번에는 H가 cloud의 VM이라고 해보자. VM의 eth0는 hypervisor가 만든 가상 NIC이고, cloud는 이 가상 NIC에 MAC address 하나를 할당한다. 가상 NIC와 cloud의 가상 switch는 할당하지 않은 MAC address의 frame을 버리는 경우가 많아서, mv1의 frame은 VM 밖으로 나가지 못한다. Docker 문서도 대부분의 cloud provider가 macvlan network를 막는다고 안내한다.

세 경우 모두 link 반대쪽 장비가 받아들이는 MAC address는 H에게 할당된 하나뿐이다. 이런 환경에서는 child가 모두 parent의 MAC address를 함께 쓰는 IPvlan 같은 다른 driver를 써야 한다.

Summary

  • Macvlan child는 parent와 다른 MAC address로 물리 network에 나타난다. kernel은 들어온 frame의 dst MAC으로 child를 고르고, switch의 MAC table에는 p1 하나에 MAC address가 여러 개 기록된다. bridged mode와 달리 parent의 address와 route는 바뀌지 않으며, MAC learning도 없다.
  • parent로 내보낸 frame은 Macvlan driver를 거치지 않고 물리 NIC로 나가고, switch는 frame을 들어온 port로 다시 내보내지 않는다. 그래서 host는 parent로 child와 통신할 수 없고, host에 bridge mode child를 하나 더 만들어 그 child로 통신한다.
  • Macvlan의 mode는 child끼리의 경로를 정한다. bridge mode는 kernel 안에서 바로 전달하고, vepa mode는 switch의 reflective relay를 거치며, private mode는 child끼리의 통신을 막는다. VLAN sub-interface를 parent로 쓰면 child는 그 VLAN에 속한다.
  • switch의 port security처럼 port당 MAC address 수를 제한하는 환경, AP가 등록된 MAC address만 받는 Wi-Fi, 할당된 MAC address 외의 frame을 버리는 cloud VM에서는 Macvlan을 쓸 수 없다. parent의 NIC도 unicast filter나 promiscuous mode로 child의 MAC address를 받아야 한다.

References