Wire mini-container bridge networking into runtime - #9
Conversation
SuperSon7
left a comment
There was a problem hiding this comment.
코드 리뷰 요약
전체적으로 구조가 잘 잡혀 있고, runInNetworkNamespace의 OS thread 고정/복원 패턴(defer LIFO 순서 포함), pipe 기반 parent-child 동기화 방식 등 핵심 메커니즘은 올바르게 구현되어 있습니다.
다만 아래 몇 가지 버그와 정리 항목을 발견했습니다.
버그 (수정 필요)
childSyncRead/childSyncWrite이중 close (fd 재사용 위험)SetupBridge가 멱등적이지 않아 두 번째 컨테이너 실행부터 EEXIST로 실패Manager.Destroy()가 stub이라 bridge/iptables 룰이 영원히 남음 (→ 첫 번째 문제와 연결)- Setup 부분 실패 시 롤백 없음
SetupNAT이-A로 중복 룰을 누적Config.HostInterfaceName/ContainerInterfaceName이 선언만 되고 실제로는 사용되지 않음
개선 권장
iptablesstderr 미캡처로 에러가 불투명OutboundInterface: "eth0"하드코딩containerSubnetCIDR의 불필요한 재계산
자세한 내용은 인라인 코멘트를 참고해주세요.
| ContainerAddress: "10.0.0.2/24", | ||
| GatewayAddress: "10.0.0.1/24", | ||
| EnableNAT: true, | ||
| OutboundInterface: "eth0", |
There was a problem hiding this comment.
[개선] OutboundInterface: "eth0" 하드코딩
WSL2, 클라우드 VM 등 outbound 인터페이스가 eth0이 아닌 환경(ens3, enp0s3 등)에서는 NAT 룰이 잘못된 인터페이스에 설치됩니다. 컨테이너가 시작은 되지만 외부 통신이 조용히 실패합니다.
// 기본 경로의 인터페이스를 동적으로 조회 예시
routes, _ := netlink.RouteList(nil, netlink.FAMILY_V4)
for _, r := range routes {
if r.Dst == nil { // default route
link, _ := netlink.LinkByIndex(r.LinkIndex)
outboundIface = link.Attrs().Name
break
}
}There was a problem hiding this comment.
반영했습니다. OutboundInterface: "eth0" 하드코딩을 제거하고, host IPv4 default route의 link index로 outbound interface를 찾도록 바꿨습니다. 추가로 실제 환경에서 default route가 0.0.0.0/0로 들어오는 케이스를 놓쳐서 a73af70에서 nil/0.0.0.0/0 둘 다 default route로 인정하도록 보강했습니다. ping -c 1 8.8.8.8 e2e도 통과했습니다.
| la := netlink.NewLinkAttrs() | ||
| la.Name = name | ||
| br := &netlink.Bridge{LinkAttrs: la} | ||
| if err := netlink.LinkAdd(br); err != nil { |
There was a problem hiding this comment.
[버그] SetupBridge가 멱등적이지 않음 — 두 번째 컨테이너 실행부터 항상 실패
netlink.LinkAdd(br)는 mini0이 이미 존재하면 EEXIST를 반환합니다. Manager.Destroy()가 미구현이라 첫 번째 컨테이너 종료 후에도 bridge가 남아있고, 두 번째 run 호출은 여기서 즉시 실패합니다.
// 수정: 이미 존재하면 재사용
if _, err := netlink.LinkByName(name); err != nil {
la := netlink.NewLinkAttrs()
la.Name = name
br := &netlink.Bridge{LinkAttrs: la}
if err := netlink.LinkAdd(br); err != nil {
return fmt.Errorf("create bridge %s: %w", name, err)
}
}There was a problem hiding this comment.
반영했습니다. SetupBridge가 getOrCreateBridge()를 통해 기존 bridge를 재사용하고, gateway address도 ensureBridgeAddress()로 중복 추가를 피하도록 바꿨습니다. 단일 컨테이너 반복 실행의 mini0 file exists 문제는 해소했고, 여러 컨테이너가 공유하는 bridge lifecycle은 #11로 분리했습니다.
| return err | ||
| } | ||
|
|
||
| if err := m.connectContainer(m.config.BridgeName, pid, m.config.ContainerAddress, m.config.GatewayAddress); err != nil { |
There was a problem hiding this comment.
[버그] 부분 실패 시 롤백 없음
setupBridge 성공 후 connectContainer가 실패하면 생성된 mini0 bridge가 그대로 남습니다. Destroy()가 구현되더라도 이 에러 경로에서는 호출되지 않으므로 별도 처리가 필요합니다.
if err := m.connectContainer(...); err != nil {
_ = netlink.LinkDel(/* mini0 */)
return err
}There was a problem hiding this comment.
반영했습니다. Manager.Setup()에서 connectContainer 실패, subnet 계산 실패, setupNAT 실패 시 모두 rollbackSetup()을 호출하도록 바꿨습니다. cleanup 실패가 원래 setup 실패를 가리지 않도록 errors.Join으로 같이 반환합니다. TestManagerSetupRollsBackBridgeWhenConnectFails와 TestManagerSetupRollsBackNATAndBridgeWhenNATFails도 추가했습니다.
| return err | ||
| } | ||
|
|
||
| cmd := exec.Command( |
There was a problem hiding this comment.
[버그] iptables 중복 룰 누적
-A(append)는 멱등적이지 않습니다. 컨테이너를 n번 실행하면 같은 MASQUERADE 룰이 n개 쌓이고, DestroyNAT의 -D는 하나만 제거하므로 n-1개의 스테일 룰이 남습니다.
추가 전 -C로 존재 여부를 먼저 확인하세요:
checkCmd := exec.Command("iptables", "-t", "nat", "-C", "POSTROUTING",
"-s", config.SourceCIDR, "-o", config.OutboundInterface, "-j", "MASQUERADE")
if checkCmd.Run() == nil {
return nil // 이미 존재
}
// 없으면 -A로 추가There was a problem hiding this comment.
반영했습니다. SetupNAT()이 iptables -C에 해당하는 natRuleExists() 확인을 먼저 하고, 없는 경우에만 -A를 실행하도록 바꿨습니다. DestroyNAT()도 rule이 없으면 성공으로 처리합니다. 이로써 새 실행에서 같은 MASQUERADE rule을 반복 누적하지 않습니다. 기존에 이미 쌓인 stale duplicate를 일괄 청소하는 문제는 이번 범위 밖으로 두었습니다.
| "-j", "MASQUERADE", | ||
| ) | ||
|
|
||
| return cmd.Run() |
There was a problem hiding this comment.
[개선] iptables stderr 미캡처 — 에러 메시지가 버려짐
cmd.Run()이 실패하면 호출자는 exit status 1만 받고 실제 이유(모듈 없음, 권한 부족 등)를 알 수 없습니다.
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("iptables POSTROUTING: %w\n%s", err, strings.TrimSpace(string(out)))
}
return nilDestroyNAT의 line 51도 동일하게 수정이 필요합니다.
There was a problem hiding this comment.
반영했습니다. runIPTablesCommand()에서 CombinedOutput()을 사용하고, 실패 시 실행한 iptables args와 stderr/stdout을 error에 포함하도록 바꿨습니다. TestRunIPTablesCommandIncludesOutputInError로 출력이 에러에 포함되는 것도 확인했습니다.
| BridgeName string | ||
|
|
||
| // HostInterfaceName is the veth endpoint kept in the host network namespace. | ||
| HostInterfaceName string |
There was a problem hiding this comment.
[버그] HostInterfaceName / ContainerInterfaceName이 선언만 되고 실제로는 무시됨
veth.go의 ConnectContainer는 이 필드를 읽지 않습니다:
- host veth 이름 →
fmt.Sprintf("veth-%d", containerPID)(PID 기반 하드코딩) - container 인터페이스 이름 →
defaultContainerInterfaceName = "eth0"(상수 하드코딩)
manager_test.go에서도 이 필드를 채우고 있어 실제로 동작한다는 오해를 줍니다. ContainerInterfaceName: "eth1"로 설정해도 컨테이너는 항상 eth0을 갖게 됩니다.
ConnectContainer에 연결하거나, 사용하지 않을 것이라면 Config에서 제거하세요.
There was a problem hiding this comment.
반영했습니다. 실제로 사용하지 않던 HostInterfaceName / ContainerInterfaceName 필드는 Config와 테스트에서 제거했습니다. 지금 단계에서는 veth 이름을 PID 기반으로 만들고 container 쪽은 eth0로 고정합니다. 여러 컨테이너/구성 가능한 interface name은 #11에서 runtime state와 함께 다루도록 TODO를 남겼습니다.
연관 이슈
없음
작업 내용
container/network패키지에 bridge, veth, route, NAT setup 흐름 추가Manager.Setup(pid)에서 host bridge 생성, child netns veth 연결, loopback/default route, NAT 설정을 순서대로 연결main.go런타임에CLONE_NEWNET, parent-child sync pipe,network.Manager.Setup(cmd.Process.Pid)호출 추가exec하지 않도록 fd 기반 release 흐름 추가PR Point
main.go에 static 값으로 둔 상태입니다:mini0,10.0.0.1/24,10.0.0.2/24, outboundeth0.Manager.Destroy, idempotent 처리, IPAM은 후속 작업으로 남겨두었습니다.스크린샷
N/A
참고 사항
검증:
이 환경에서는
sudo비밀번호가 필요해 실제 root runtime 관찰은 완료하지 못했습니다. 로컬 터미널에서는 다음으로eth0,lo, default route, gateway ping을 확인하면 됩니다.sudo ./mini-container run /bin/sh -c '/sbin/ip addr; /sbin/ip route; ping -c 1 10.0.0.1'