Docker – bereit für IPv6?

Allgemein

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' 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]

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.