Docker – bereit für IPv6?

Linux

Zugegeben, die Headline ist rein rhetorischer Natur. Grundsätzlich ist Docker „ready for ipv6“ – allerdings musst du doch „ein wenig“ Hand anlegen. Man merkt Docker, im speziellen die Netzwerkarchitektur, deutlich an, das man bei der Entwicklung IPv6 offenbar gar nicht auf dem Schirm hatte.

Was genau alles zu tun ist, damit dein Container per IPv6 mit der Außenwelt kommuniziert, erkläre ich dir hier. Ziel ist es, Adguardhome (die Installation habe ich dir in diesem Beitrag erläutert), für IPv6 zu konfigurieren.

Absolute Vorasusetzung ist, das der Provider, der deinen Server (Docker-Instanz) hostet, auch IPv6 spricht. In diesem Zusammenhang konnte ich meinen alten Provider 1blu.de direkt kündigen, der hat es leider nicht so mit IPv6. Günstiges Hosting hat halt auch Nachteile. Mittels ip a schauen wir erst einmal, ob unser vServer eine IPv6-Anbindung hat.

# ip a
...
eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 4a:16:a2:85:af:18 brd ff:ff:ff:ff:ff:ff
    altname enp0s3
    altname ens3
    inet 202.61.205.19/22 brd 202.61.207.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 2a03:4000:5b:824:4816:a2ff:fe85:af18/64 scope global
       valid_lft forever preferred_lft forever
    inet6 fe80::4816:a2ff:fe85:af18/64 scope link
       valid_lft forever preferred_lft forever
...

Das schaut gut aus, wir haben wie erwartet ein 64er-Subnetz für IPv6 zur Verfügung. Die Funktionalität testen wir gleich mit einem Ping:

# ping6 -n -c4 google.com
PING google.com(2a00:1450:4001:808::200e) 56 data bytes
64 bytes from 2a00:1450:4001:808::200e: icmp_seq=1 ttl=120 time=3.68 ms
64 bytes from 2a00:1450:4001:808::200e: icmp_seq=2 ttl=120 time=3.76 ms
64 bytes from 2a00:1450:4001:808::200e: icmp_seq=3 ttl=120 time=3.73 ms
64 bytes from 2a00:1450:4001:808::200e: icmp_seq=4 ttl=120 time=3.73 ms

--- google.com ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3006ms
rtt min/avg/max/mdev = 3.675/3.725/3.762/0.031 ms

Nun musst du IPv6 auch im Docker-Daemon aktivieren, ändere dazu die Datei /etc/default/docker und starte Docker anschließend neu

# Use DOCKER_OPTS to modify the daemon startup options.
DOCKER_OPTS="--dns 1.1.1.1 --dns 8.8.4.4 --ipv6 --fixed-cidr-v6='2a03:4000:5b:824::/64'"

# systemctl restart docker

Wenn du meinen Blogbeitrag zur Netzwerkkonfiguration unter Docker bereits gelesen hast, dann weißt du, das als nächstes die Einrichtung eines entsprechenden Netzwerkes innerhalb der Docker-Instanz erfolgen muss. Ich empfehle dir, nicht das komplette 64er-Subnetz zu „verbraten“, sondern ein wenig Subnetting zu betreiben und wie im folgenden gezeigt z.B. nur ein 80er-Teilnetz zu generieren. Wer weiß, ob du später nicht gegebenenfalls noch weitere Subnetze brauchst.

# docker network create --driver bridge --ipv6 --subnet=2a03:4000:5b:824:20::/80 bridge_ipv6
So schaut das neue Interface bridge_ipv6 in Portainer aus

Jetzt schaust du als nächstes, welchen Namen das neue Interface im Hostsystem bekommen hat:

# ip -6 a
...
6: br-b39e13d99f61: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP
    inet6 2a03:4000:5b:824:20::1/80 scope global
       valid_lft forever preferred_lft forever
    inet6 fe80::42:a8ff:fe54:a895/64 scope link
       valid_lft forever preferred_lft forever
    inet6 fe80::1/64 scope link
       valid_lft forever preferred_lft forever
...

Okay, in meinem Fall gehört die 2a03:4000:5b:824:20::1/80 (IPv6-Gateway) also zu br-b39e13d99f61, also routen wir mit dem nächsten Kommando alle IP-Pakete für unser Subnetz an dieses neue Interface und aktivieren das Weiterleiten der Pakete durch hinzufügen oder auskommentieren der beiden u.a. Zeilen der Datei /etc/sysctl.conf:

# ip -6 route add 2a03:4000:5b:824:20::/80 dev br-b39e13d99f61

# nano /etc/sysctl.conf
net.ipv6.conf.default.forwarding=1
net.ipv6.conf.all.forwarding=1

Jetzt checkst du deine komplette Konfiguration der IPv6-Bridge:

# docker network inspect bridge_ipv6
[
    {
        "Name": "bridge_ipv6",
        "Id": "b39e13d99f613250b31d40c5c5a33a73058e1bb664a5e7f5b34d885c1907b064",
        "Created": "2021-10-19T13:38:53.853557047+02:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": true,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.20.0.0/16",
                    "Gateway": "172.20.0.1"
                },
                {
                    "Subnet": "2a03:4000:5b:824:20::/80"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {},
        "Labels": {}
    }
]

…und pingst bei der Gelegenheit das neue IPv6-Gateway an:

# ping -6 -c4 2a03:4000:5b:824:20::1
PING 2a03:4000:5b:824:20::1(2a03:4000:5b:824:20::1) 56 data bytes
64 bytes from 2a03:4000:5b:824:20::1: icmp_seq=1 ttl=64 time=0.043 ms
64 bytes from 2a03:4000:5b:824:20::1: icmp_seq=2 ttl=64 time=0.060 ms
64 bytes from 2a03:4000:5b:824:20::1: icmp_seq=3 ttl=64 time=0.062 ms
64 bytes from 2a03:4000:5b:824:20::1: icmp_seq=4 ttl=64 time=0.057 ms

--- 2a03:4000:5b:824:20::1 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3073ms
rtt min/avg/max/mdev = 0.043/0.055/0.062/0.007 ms

Ich habe bislang keinen Weg gefunden, ein IPv6-Netzwerk unter Docker ohne IPv4-Adressen zu erstellen. So muss ich mit dem Umstand erst einmal leben.

Um unseren Container (hier Container-Name adguardhome) nachträglich eine IPv6-Adresse zu verpassen, konnektieren wir ihn einfach mit einer passenden IPv6-Adresse aus dem o.a. 80er-Subnetz (das kannst du auch bequem über Portainer erledigen):

# docker network connect --ip6 '2a03:4000:5b:824:20::2' bridge_ipv6 adguardhome

Jetzt liegt es an dem jeweiligen Container, ob der selbst überhaupt in der Lage ist, IPv6 zu „sprechen“.

Adguardhome macht das für dich automatisch und unter Einrichtung siehst du jetzt sowohl die IPv4-, als auch IPv6-Adressen, auf die dein Netzwerkfilter lauscht. Wenn dein Container über die IPv6-Adresse erreichbar ist, kannst du an den nächsten Schritt gehen – DNS!

# ping -6 -c4 2a03:4000:5b:824:20::2
PING 2a03:4000:5b:824:20::2(2a03:4000:5b:824:20::2) 56 data bytes
64 bytes from 2a03:4000:5b:824:20::2: icmp_seq=1 ttl=64 time=0.047 ms
64 bytes from 2a03:4000:5b:824:20::2: icmp_seq=2 ttl=64 time=0.075 ms
64 bytes from 2a03:4000:5b:824:20::2: icmp_seq=3 ttl=64 time=0.068 ms
64 bytes from 2a03:4000:5b:824:20::2: icmp_seq=4 ttl=64 time=0.071 ms

--- 2a03:4000:5b:824:20::2 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3055ms
rtt min/avg/max/mdev = 0.047/0.065/0.075/0.010 ms

Da der Container ja direkt über seine Subdomain erreichbar sein (bei mir dns.kwellkorn.de) soll, reicht es nicht, einen CNAME- und A-Record auf dem DNS-Server zu erstellen, sondern du brauchst einen AAAA-Record, der direkt auf die IPv6-Adresse des Containers verweist. Wie das geht, ist abhängig von deinem vServer-Provider und der Art und Weise, wie dort DNS-Einträge verwaltet werden.

Wenn du dein DNS korrekt eingerichtet hast, solltest du den DNS-Namen auch in eine IPv6-Adresse auflösen können:

# ping -c4 dns.kwellkorn.de
PING dns.kwellkorn.de(dns.kwellkorn.de (2a03:4000:5b:824:20::2)) 56 data bytes
64 bytes from dns.kwellkorn.de (2a03:4000:5b:824:20::2): icmp_seq=1 ttl=64 time=0.050 ms
64 bytes from dns.kwellkorn.de (2a03:4000:5b:824:20::2): icmp_seq=2 ttl=64 time=0.073 ms
64 bytes from dns.kwellkorn.de (2a03:4000:5b:824:20::2): icmp_seq=3 ttl=64 time=0.059 ms
64 bytes from dns.kwellkorn.de (2a03:4000:5b:824:20::2): icmp_seq=4 ttl=64 time=0.055 ms

--- dns.kwellkorn.de ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 4854ms
rtt min/avg/max/mdev = 0.050/0.059/0.073/0.008 ms

Und jetzt aufgepasst! Dein Container bekommt mit einer öffentlichen IPv6-Adresse auch alle Möglichkeiten, von der Außenwelt (Internet) direkt erreichbar zu sein. Anders als bei IPv4, wo du mittels NAT und Port-Mapping (Docker-Run-Parameter –p Port:Port) explizit Verbindungen in die Außernwelt einrichtest, liegt jetzt nichts mehr zwischen deinem Container und der „großen bösen Welt“. Schau dir also genau an, auf welchen Ports dein Container lauscht. Hier mal das Ergebnis des Scans meines DNS-Server:


# nmap -6 -p 1-1024  -sT -sU 2a03:4000:5b:824:20::2
Starting Nmap 7.80 ( https://nmap.org ) at 2021-10-19 14:15 CEST
Nmap scan report for dns.kwellkorn.de (2a03:4000:5b:824:20::2)
Host is up (0.000051s latency).
Not shown: 2042 closed ports
PORT    STATE         SERVICE
53/tcp  open          domain
80/tcp  open          http
443/tcp open          https
853/tcp open          domain-s
53/udp  open          domain
784/udp open|filtered unknown
MAC Address: 02:42:AC:af:00:02 (Unknown)

Nmap done: 1 IP address (1 host up) scanned in 511.40 seconds

Wie du siehst, sind nur die zu erwartenden Ports geöffnet – 53 (klassisches DNS), 443 (DNS-over-HTTPS), 784 (DNS-over-QUIC) und 853 (DNS-over-TLS) für DNS sowie 80 für die Administration über die Weboberfläche. Port 80 ist unverschlüsselter Hypertext, was innerhalb eines Containers einer Docker-Instanz kein Problem darstellt, da in der Regel noch ein SSL-Proxy zwischen diesem und dem Internet steht, welcher den verschlüsselten Datenverkehr Richtung Internet sicherstellt. Port 80 willst du aber nicht Richtung Internet offen haben, denn bei einem möglichen Man-in-the-middle-Angriff wäre das Passwort für die Administrationsoberfläche sehr leicht mitlesbar. Also wird der direkte Weg zum Ziel mittels IPv6 über die Firewall dicht gemacht:

# ip6tables -A FORWARD -p tcp --dport 80 -d 2a03:4000:5b:824:20::2 -m state --state NEW -j DROP

# ip6tables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination
DROP       tcp      anywhere             dns.kwellkorn.de     tcp dpt:http state NEW

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination


# ip6tables-save > /etc/iptables/rules.v6

Jetzt bleibt ja immer noch die Option über IPv4 und das NAT-Gateway von Docker, so das die Administration grundsätzlich möglich ist.

Schließe also alle Ports, die du nicht wirklich nach außen offen haben möchtest und kümmere dich mehr als bisher um wirklich sichere Passwörter (mindestens 12 Zeichen, bestehend aus Groß- und Kleinbuchstaben, Ziffern und Sonderzeichen). Ein völlig anderer Ansatz ist https://github.com/robbertkl/docker-ipv6nat, aber ehrlich gesagt bin ich kein Freund von NAT und unter IPv6 gibt es für mich nur einen echten konstruktiven Einsatz von NAT – 6to4.

Auch wenn IPv6 schon mehr als 2 (in Worten zwei) Jahrzehnte alt ist, scheint sich niemand so richtig daran zu wagen – dabei liegen die Vorteile auf der Hand. Wenn du mehr über IPv6 erfahren willst, dann empfehle ich dir an dieser Stelle den dazu passenden Podcast.


Nachtrag:

Wenn du einen Container ausschließlich mit IPv6 (also defintiv ohne IPv4-Adresse) starten möchtest, dann kannst du das mit folgenden Kommando erledigen:

# docker run \
 -d \
 -p 443:443 \
......
 --network ipv6 \
 --ip6 '2a03:4000:5b:824::3' \
 --name [container_name] \
 [image_name]

5 thoughts on “Docker – bereit für IPv6?

  1. Hallo Lars,

    danke für das Tutorial und die Beschreibung. Ich bin gerade dabei, die Punkte auf mein System zu übertragen und anzupassen.

    Mir sind zwei Dinge in deiner Beschreibung aufgefallen, die möglicherweise angepasst werden müssen.
    1. Bei # ip -6 route add 2a03:4000:5b:824:20:/80 dev br-b39e13d99f61 fehlt vermutlich ein „:“ -> # ip -6 route add 2a03:4000:5b:824:20::/80 dev br-b39e13d99f61.

    2. Bei # docker network connect –ip6 ‚2a03:4000:5b:824:20::2‘ ipv6 adguardhome müsste es statt „ipv6“ „bridge_ipv6“ heißen, damit der container an das zuvor im Tutorial angelegte Netzwerk angeschlossen wird.

    Danke nochmal und Grüße!

  2. Hallo Lars,

    jetzt benutze ich doch nocheinmal die Kommentarfunktion zu deinem Artikel, weil ich es bei mir nicht hinbekomme.
    Allerdings habe ich auch ein etwas anderes (ich würde behaupten einfacheres) Setup als du.

    Ich benutze einen RPi und habe darauf neben einigen anderen Containern AdGuard (IPv4) erfolgreich am Laufen. Da mir dort einiges bzgl. Filterung etwas komisch vorgekommen ist,
    habe ich mich mal auf die Suche gemacht und eben festgestellt, dass vieles auch über IPv6 läuft, das aber am AdGuard-Container vorbeigeht.

    Als Übergangslösung benutze ich nun einen weiteren RPi mit nativ installiertem AdGuard – hier funktioniert natürlich das mit IPv6 out of the box.

    Nun möchte ich aber gerne meinen bereits existierenden Container entsprechend deiner Anleitung dazu ertüchtigen, dass er eben auch IPv6 kann.

    Folgendes Setup betreibe ich:
    – FritzBox teilt den Clients den zu verwendenden DNS-Server mit (derzeit eben IPv4, RPi mit AdGuard-Container)
    – RPi mit AdGuard-Container filtert und leitet die ungefilterten Anfragen an die FritzBox weiter, welche dann den Standard DNS des Providers benutzt.

    Das soll künftig auch mit IPv6 laufen.

    Mein Raspberry bekommt eine IPv6 von der FritzBox. Die kann ich auch von einem anderen Gerät aus pingen.
    Auch kann ich mit dieser IP andere Websites z.B. google.com über IPv6 pingen.

    2: eth0: mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether b8:27:eb:b0:3a:62 brd ff:ff:ff:ff:ff:ff
    inet 192.168.178.31/24 brd 192.168.178.255 scope global dynamic noprefixroute eth0
    valid_lft 863942sec preferred_lft 755942sec
    inet6 2003:d7:ff27:5a00:75d8:a584:b4f3:72da/64 scope global dynamic mngtmpaddr noprefixroute
    valid_lft 7159sec preferred_lft 1416sec
    inet6 fe80::c313:60df:bd8a:a4f9/64 scope link
    valid_lft forever preferred_lft forever

    ————————————————————————————————————————-

    ping6 -n -c4 google.com
    PING google.com(2a00:1450:4016:80b::200e) 56 data bytes
    64 bytes from 2a00:1450:4016:80b::200e: icmp_seq=1 ttl=118 time=5.10 ms
    64 bytes from 2a00:1450:4016:80b::200e: icmp_seq=2 ttl=118 time=4.74 ms
    64 bytes from 2a00:1450:4016:80b::200e: icmp_seq=3 ttl=118 time=4.62 ms
    64 bytes from 2a00:1450:4016:80b::200e: icmp_seq=4 ttl=118 time=4.65 ms

    — google.com ping statistics —
    4 packets transmitted, 4 received, 0% packet loss, time 8ms
    rtt min/avg/max/mdev = 4.623/4.780/5.101/0.190 ms

    ————————————————————————————————————————-

    Nachdem das Aktivieren von IPv6 über DOCKER_OPTS bei mir nicht funktioniert hat, habe ich eine entsprechende daemon.json in /etc/docker/ erstellt und dort die Optionen eingetragen.

    {
    „ipv6“: true,
    „fixed-cidr-v6“: „2003:d7:ff27:5a00::/64“
    }

    Die bridge zeigt nun an, dass IPv6 aktiv ist und zeigt auch das entsprechende Subnetz an.

    docker network inspect bridge
    [
    {
    „Name“: „bridge“,
    „Id“: „662dd77705e847237e6dcceac5bbaacf3f055bcec90ae8cb6f406c3955bc8100“,
    „Created“: „2022-05-31T18:21:06.557491855+02:00“,
    „Scope“: „local“,
    „Driver“: „bridge“,
    „EnableIPv6“: true,
    „IPAM“: {
    „Driver“: „default“,
    „Options“: null,
    „Config“: [
    {
    „Subnet“: „172.17.0.0/16“,
    „Gateway“: „172.17.0.1“
    },
    {
    „Subnet“: „2003:d7:ff27:5a00::/64“
    }
    ]
    },
    „Internal“: false,
    „Attachable“: false,
    „Ingress“: false,
    „ConfigFrom“: {
    „Network“: „“
    },
    „ConfigOnly“: false,
    „Containers“: {
    „f5669d6692140af4bcba92f9d124723a0e4c9be2e39b4bfb92ad90342eec5dcf“: {
    „Name“: „portainer“,
    „EndpointID“: „b9ec4f9dd413b6f1e5e3f4244c983da5df2618e1832fa878e06fe41d1945b80f“,
    „MacAddress“: „02:42:ac:11:00:02“,
    „IPv4Address“: „172.17.0.2/16“,
    „IPv6Address“: „2003:d7:ff27:5a00:0:242:ac11:2/64“
    }
    },
    „Options“: {
    „com.docker.network.bridge.default_bridge“: „true“,
    „com.docker.network.bridge.enable_icc“: „true“,
    „com.docker.network.bridge.enable_ip_masquerade“: „true“,
    „com.docker.network.bridge.host_binding_ipv4“: „0.0.0.0“,
    „com.docker.network.bridge.name“: „docker0“,
    „com.docker.network.driver.mtu“: „1500“
    },
    „Labels“: {}
    }
    ]

    Nach dem restart des docker services wollte ich dann im Anschluss das Netzwerk erstellen. Hierbei bekomme ich allerdings die Fehlermeldung, dass sich der Adresspool des Subnetzes mit einem bestehenden überschneidet.

    sudo docker network create –driver bridge –ipv6 –subnet=2003:d7:ff27:5a00:20::/80 bridge_ipv6
    Error response from daemon: Pool overlaps with other one on this address space

    Hast du evtl. eine Idee, warum das bei mir der Fall ist, bei dir aber nicht?

    Vielen Dank für deine Unterstützung vorab!

    Grüße

    1. Hallo Lars,

      sorry fürs „Spammen“. Ich denke, das Problem könnte bei mir sein, dass die System-Bridge bereits eine IPv6 bekommen hat, nachdem ich den Service neu gestartet habe und das Subnetz damit bedient.
      bridge System – bridge false default 172.17.0.0/16 172.17.0.1 2003:d7:ff27:5a00::/64

      Bei deinem Screenshot von Portainer, nachdem du die bridge_ipv6 erstellt hast, ist nicht zu sehen, dass bei der system bridge einen Eintrag bei ‚IPV6 IPAM Subnet‘ zu sehen ist.

      Hättest du hierzu noch etwas Input für mich?

      Danke und Grüße

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.