Перейти до змісту

Bridge Networking: даємо контейнеру IP-адресу

Bridge Networking: даємо контейнеру IP-адресу

Written by:

Igor Gorovyy
DevOps Engineer Lead & Senior Solutions Architect

LinkedIn


Контейнер у новому 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