Bridge Networking: даємо контейнеру IP-адресу¶
Written by:
Igor Gorovyy
DevOps Engineer Lead & Senior Solutions Architect
Контейнер у новому network namespace не має мережі. Взагалі. Навіть loopback не піднятий. Потрібно створити віртуальну мережу і з'єднати контейнер з хостом. Ось як це працює в Sheep.
Архітектура мережі¶
graph TB
subgraph "Хост"
ETH["eth0<br/>192.168.1.x"]
BR["sheep0 (bridge)<br/>10.20.0.1/16"]
VETH1H["veth_abc1"]
VETH2H["veth_def2"]
IPTABLES["iptables NAT<br/>MASQUERADE"]
end
subgraph "Контейнер 1"
VETH1G["eth0<br/>10.20.0.2/16"]
end
subgraph "Контейнер 2"
VETH2G["eth0<br/>10.20.0.3/16"]
end
BR --- VETH1H
BR --- VETH2H
VETH1H -.- VETH1G
VETH2H -.- VETH2G
BR --- IPTABLES
IPTABLES --- ETH
Три компоненти: bridge (віртуальний switch), veth пари (віртуальні кабелі), NAT (вихід в інтернет).
Константи¶
const (
BridgeName = "sheep0"
BridgeSubnet = "10.20.0.0/16"
BridgeGateway = "10.20.0.1"
)
var ipCounter uint32 = 1
Підмережа 10.20.0.0/16 дає 65534 адреси. Для навчального проекту - більш ніж достатньо.
Створення bridge¶
func ensureBridge() error {
// Перевіряємо, чи bridge вже існує
if _, err := net.InterfaceByName(BridgeName); err == nil {
return nil
}
// Створюємо bridge
run("ip", "link", "add", BridgeName, "type", "bridge")
run("ip", "addr", "add", BridgeGateway+"/16", "dev", BridgeName)
run("ip", "link", "set", BridgeName, "up")
// Вмикаємо IP forwarding
os.WriteFile("/proc/sys/net/ipv4/ip_forward", []byte("1"), 0644)
// NAT для вихідного трафіку
run("iptables", "-t", "nat", "-A", "POSTROUTING",
"-s", BridgeSubnet, "-j", "MASQUERADE")
return nil
}
Bridge створюється один раз — при запуску першого контейнера. ip_forward дозволяє ядру пересилати пакети між інтерфейсами.
Налаштування мережі контейнера¶
Ось повна функція:
func setupNetworkForContainer(c *Container, pid int) error {
if err := ensureBridge(); err != nil {
return fmt.Errorf("ensure bridge: %w", err)
}
ip := allocateIP()
vethHost := fmt.Sprintf("veth%s", ShortID(c.ID)[:8])
vethGuest := "eth0"
// Створюємо veth пару
run("ip", "link", "add", vethHost,
"type", "veth", "peer", "name", vethGuest)
// Хостовий кінець - в bridge
run("ip", "link", "set", vethHost, "master", BridgeName)
// Гостьовий кінець - в namespace контейнера
run("ip", "link", "set", vethGuest,
"netns", strconv.Itoa(pid))
// Активуємо хостовий кінець
run("ip", "link", "set", vethHost, "up")
// Налаштовуємо мережу всередині контейнера
nsRun(pid, "ip", "addr", "add", ip+"/16", "dev", "eth0")
nsRun(pid, "ip", "link", "set", "eth0", "up")
nsRun(pid, "ip", "link", "set", "lo", "up")
nsRun(pid, "ip", "route", "add", "default", "via", BridgeGateway)
c.Network = &NetworkSettings{
IPAddress: ip,
Gateway: BridgeGateway,
Bridge: BridgeName,
VethHost: vethHost,
VethGuest: vethGuest,
}
return nil
}
Veth пари - віртуальний кабель¶
Veth (virtual ethernet) - це пара мережевих інтерфейсів, з'єднаних між собою. Що входить в один кінець, виходить з іншого. Один кінець залишається на хості (підключений до bridge), інший переміщується в network namespace контейнера.
// nsRun виконує команду всередині network namespace процесу
func nsRun(pid int, name string, args ...string) error {
nsArgs := append([]string{
"-t", strconv.Itoa(pid), "-n", "--", name,
}, args...)
return run("nsenter", nsArgs...)
}
nsenter - утиліта, яка "входить" в namespace процесу з вказаним PID. Прапорець -n означає network namespace.
Алокація IP¶
func allocateIP() string {
n := atomic.AddUint32(&ipCounter, 1)
return fmt.Sprintf("10.20.%d.%d", (n>>8)&0xFF, n&0xFF)
}
Простий атомарний лічильник. IP зберігається на диск для персистентності:
func LoadIPCounter(baseDir string) {
data, _ := os.ReadFile(
filepath.Join(baseDir, "network", "ip_counter"))
if n, err := strconv.ParseUint(
strings.TrimSpace(string(data)), 10, 32); err == nil {
atomic.StoreUint32(&ipCounter, uint32(n))
}
}
Де це ламається¶
Наша алокація IP не враховує звільнені адреси. Лічильник тільки зростає. В Docker є повноцінний IPAM (IP Address Management), який відстежує пул і перевикористовує адреси.
Другий момент - ми використовуємо ip і nsenter як зовнішні команди. Docker і containerd використовують netlink через Go бібліотеку, що швидше і не залежить від встановлених утиліт. Це в планах переробити — задача вже в беклозі Sheep.
Спробуй сам¶
sudo ./sheep run --name net-test -d minimal /bin/sleep 3600
# На хості подивись bridge і veth:
ip link show type bridge
ip link show type veth
# IP контейнера:
sudo ./sheep inspect net-test | grep IP
Контейнер має IP. Далі - NAT і iptables: як пакети виходять в інтернет і повертаються назад.
Ресурси¶
- veth(4) — документація ядра про virtual ethernet pair
- network_namespaces(7) — огляд network namespace в Linux
- ip-link(8) — керування мережевими інтерфейсами
- nsenter(1) — запуск команд всередині namespace
- Docker bridge driver — офіційна документація драйвера
- RFC 1918 — приватні діапазони IPv4
Попередня: OverlayFS | Наступна: NAT і iptables
