ZURÜCK

KVM IPv6 forwarding?

Weiterleitung von IPv6 Datenverkehr an einen KVM/libvirt Gast

Datum: 2020-06, Tags: KVM, libvirt, IPv6, iptables, forwarding, routing

KVM oder Kernel Virtual Machine ist eine Server-Virtualisierung. Ich benutze KVM Gastsysteme, um Dienst oder Services von meinen eigentlichen Servern zu entkoppeln. Das macht es mir leichter, einzelne Komponenten zu aktualisieren oder auszutauschen.

Das Beispiel hier soll die Auslagerung eines Webservers in eine virtuelle Maschine sein. Ich habe ein Setup mit anonymisierten IP-Adressen wie folgt:

Die Weiterleitung der Ports 80 und 443 erfolgt auf dem Host über eine iptables Regelsatz:

host:~iptables-save | grep 192.168
-A FORWARD -d 192.168.1.11/32 -p tcp -m tcp -m multiport --dports 80,443 -j ACCEPT
-A PREROUTING -d 111.0.0.123/32 -p tcp -m tcp -m multiport --dports 80,443 -j DNAT --to-destination 192.168.1.11
-A POSTROUTING -s 192.168.1.0/24 -o enp0s31f6 -j MASQUERADE

Auf dem Guest läuft ein nginx Dienst, der die Requests für http und https entgegennimmt:

/etc/nginx/conf.d/default.conf
server {
    server_name _;

    listen 80 default_server;
    listen [::]:80 default_server;
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;

    ssl_certificate ...
    ssl_certificate_key ...

    root ...
        ...
}

Die Konfiguration ist nicht nut für IPv4 vorbereitet, sondern müsste auch für IPv6 gültig sein. Das wollen wir auch nutzen, denn IPv6 ist darauf ausgelegt, IPv4 komplett abzulösen und die freien IP-Adressbereiche sind schon lange knapp geworden.

IPv4

IP-Adressen Version 4 bestehen aus einer 32-Bit Zahl. Üblicherweise wird diese Zahl als vier einzelne "Oktette" als Dezimalzahlen dargestellt, wobei ein Oktett immer 8 Bit bzw. ein Byte bzw. eine Zahl zwischen 0 und 255 darstellt.

Beispiel:
IPv4-Adresse dezimal: 111.0.0.123
IPv4-Adresse binär: 01101111 00000000 00000000 01111011

Damit lassen sich 2^32 Adressen erstellen, das sind ca. 4,3 Milliarden Adressen. Davon sind allerdings gut 10% nicht nutzbar.

Die Subnetzmaske bestimmt das der Adresse zugehörige Netzwerksegment. In dem Netzwerksegment haben alle binären Adressen den gleichen Anfang, entsprechend der angegebenen Länge von links.

Beispiel:
IP-Adresse: 111.0.0.123/24
Die ersten 24 Bit der Adresse sind gleich.
Erste Adresse des Segmentes: 01101111 00000000 00000000 00000000 oder 111.0.0.0
Letzte Adresse des Segmentes: 01101111 00000000 00000000 11111111 oder 1111.0.0.255

Die Subnetzmaske wird bei IPv4 auch als Oktett-Kombination geschrieben. Binär dargestellt ergibt die Anzahl der Einsen von links dann wieder die Länge der Subnetzmaske in der Kurz-Notation:

Subnetzmaske: 255.255.255.0
Binär: 11111111 11111111 11111111 00000000
Kurz-Notation: /24

IPv6

In der Version 6 besteht eine Adresse aus 128 Bit. Davon bilden die ersten 64 Bit den "Präfix", die hinteren 64 Bit den "Interface Identifier". Üblicherweise wird eine IPv6 Adresse als acht Hexadezimalblöcke notiert, die jeweils von einem Doppelpunkt getrennt sind. Führende Nullen können pro Block weggelassen werden. Blöcke, die lediglich '0000' enthalten, können komplett weggelassen werden, was mit einem doppelten Doppelpunkt abgekürzt wird. Diese Abkürzung darf jedoch nur einmal verwendet werden.

Die Subnetzmasken funktionieren analog zu IPv4.

Beispiel:
IPv6-Adresse abgekürzt: 2001:fff:1057:c0de::2
IPv6-Adresse ausgeschrieben: 2001:0fff:1057:c0de:0000:0000:0000:0002

Damit lassen sich 2^64 Adressen darstellen, das sind etwa 340 Sextillionen (3,4·10^38) Adressen. Das ist die Vierer-Potenz der möglichen Adressen von IPv4. Die Subnetzmaske oder Präfix funktioniert analog zu IPv4. Vorgeschrieben ist, dass die Zuweisung eines IPv6 Adressraums mindestens einen 64-er Präfix hat, das bedeutet, dass man die letzten vier Hexadezimalblöcke für die eigene Verwendung hat, das sind imemrhin mehr als 18 Trillionen (1,8·10^19) Adressen!

Eine URL mit IPv6 Adresse wird übrigens mit eckigen Klammern um die IP-Adresse notiert: http://[2001:fff:1057:c0de::2]:80/. Der Adressbereich fc00::/7 ist analog zu IPv4 ein privater Adressbereich, der "Unique Local Unicast" bezeichnet wird. fe80::/64 sind "Link Local" Adressen, die automatisch für das Routing in einem Netzwerksegment angelegt werden.

Routing

Die IPv6 Adresse, die wir von unserem Provider zugeteilt bekommen haben, möchten wir nun an den Guest mit dem webserver weiterreichen. Als Vorlage könnten wir das Port-Forwarding benutzen, dass wir bereits für IPv4 konfiguriert haben. Doch Achtung: so ist das ein Denkfehler: Wir haben nicht eine IPv6 Adresse von unserem Provider erhalten, sondern einen IPv6 Adressbereich. Wir können und sollten also kein Port-Forwarding verwenden, sondern Routing. Das stellt uns aber noch vor andere Aufgaben:

Um das Folgende nachzustellen, benutze bitte echte IP-Adressen, nicht die von mir hier angegebenen, die sind wie gesagt ausgedacht.

Unser Provider hat uns den IP-Adressbereich 2001:fff:1057:c0de::/64 zugeteilt und routet den auf das primäre Netzwerk-Interface enp0s31f6 unseres Servers.

Um damit arbeiten zu können, versehen wir unser Interface mit einer Adresse aus dem Bereich. Die ::0 ist nicht nutzbar, die ::1 lassen wir frei und benutzen die ::2. Die Netzwerkeinstellungen unseres Hosts enthalten dann:

/etc/network/interfaces
        ...
auto enp0s31f6

iface enp0s31f6 inet static
  address 111.0.0.123
  netmask 255.255.255.192
  gateway 111.0.0.65
  up route add -host 111.0.0.65 dev enp0s31f6
  up route add -net 111.0.0.64 netmask 255.255.255.192 gw 111.0.0.65 dev enp0s31f6

iface enp0s31f6 inet6 static
  address 2001:fff:1057:c0de::2
  netmask 64
  gateway fe80::1

Damit ist das Netzwerk auf unserem Host nun wie folgt eingestellt:

host:~ip a
1: lo:  mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: enp0s31f6:  mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 90:1b:0e:fc:ec:cf brd ff:ff:ff:ff:ff:ff
    inet 111.0.0.123/26 brd 111.0.0.127 scope global enp0s31f6
       valid_lft forever preferred_lft forever
    inet6 2001:fff:1057:c0de::2/64 scope global
       valid_lft forever preferred_lft forever
    inet6 fe80::921b:eff:fefc:eccf/64 scope link
       valid_lft forever preferred_lft forever
23: virbr0:  mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 52:54:00:30:b9:06 brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.1/24 brd 192.168.1.255 scope global virbr0
       valid_lft forever preferred_lft forever
25: vnet0:  mtu 1500 qdisc pfifo_fast master virbr0 state UNKNOWN group default qlen 1000
    link/ether fe:54:00:84:64:f1 brd ff:ff:ff:ff:ff:ff
    inet6 fe80::fc54:ff:fe84:64f1/64 scope link
       valid_lft forever preferred_lft forever

host:~ip r
default via 111.0.0.65 dev enp0s31f6 onlink
111.0.0.64/26 via 111.0.0.65 dev enp0s31f6
111.0.0.64/26 dev enp0s31f6 proto kernel scope link src 111.0.0.123
111.0.0.65 dev enp0s31f6 scope link
192.168.1.0/24 dev virbr0 proto kernel scope link src 192.168.1.1

host:~ip -6 r
2001:fff:1057:c0de::/64 dev enp0s31f6 proto kernel metric 256  pref medium
fe80::/64 dev enp0s31f6 proto kernel metric 256  pref medium
fe80::/64 dev vnet0 proto kernel metric 256  pref medium
default via fe80::1 dev enp0s31f6 metric 1024  pref medium

Auf unserem primären Netzwerkinterface ist die IP-Adresse mit dem InterfaceIdentifier ::2 eingerichtet. Das komplette Netz wird auch über eben das Interface geroutet.

Wenn die Einstellungen so noch nicht vorhanden sind, kann die IPv6 Adresse auch "per Hand" hinzugefügt werden. Der Vorteil dabei ist, dass A) den laufenden Betrieb des Serevrs nicht stört, es sei denn, man macht einen groben Fehler, B) man den Server aber einfach neu starten kann, wenn man sich versehentlich aussperrt. Nach erfolgreichem Test muss die Konfiguration dann natürlich auch fixiert werden, und durch einen Neustart des Servers bei nächster Gelegenheit überprüft werden.

host:~ip -6 address add 2001:fff:1057:c0de::2/64 dev enp0s31f6

Der Host sollte nun per IPv6 erreichbar sein. Aber Achtung: Gerade bei den DSL-Verbindungen, die man zu Hause hat, ist möglicherweise IPv6 noch nicht freigeschaltet, so dass es scheint, als wäre der Host noch nicht erreichbar.
Die Verfügbarkeit von IPv6 für die aktuelle Internetanbindung kann z.B. über https://www.wieistmeineip.de/ geprüft werden, dort steht im oberen Bereich die aktuelle IPv4- und IPv6-Adresse.
Es gibt jedoch im Netz jede Menge Online-Tools, die einen IPv6 Ping ausführen und die Erreichbarkeit des Hosts testen können.

somewhere:~ping6 2001:fff:1057:c0de::2
PING 2001:fff:1057:c0de::2(2001:fff:1057:c0de::2) 56 data bytes
64 bytes from 2001:fff:1057:c0de::2: icmp_seq=1 ttl=54 time=29.7 ms
64 bytes from 2001:fff:1057:c0de::2: icmp_seq=2 ttl=54 time=27.9 ms
64 bytes from 2001:fff:1057:c0de::2: icmp_seq=3 ttl=54 time=23.3 ms
^C
--- 2001:fff:1057:c0de::2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 23.338/27.016/29.766/2.705 ms

Um nun die IP-Adresse an dem Guest nutzbar zu machen, benötigen wir:

IPv6 Subnetz

Für das IPv6 Subnetz benötigen wir einen Teil des Netzes, aus dem wir dann eine IP-Adresse dem Host zuweisen und eine andere dem Guest. Ich möchte hier den Bereich 2001:fff:1057:c0de:0:1::/96 nutzen, siehe auch internex IPv6 Subnet Calculator.

Unser Setup soll dann am Ende wie folgt aussehen:

Der Host muss erst einmal grundlegend als IPv6 Router eingestellt werden. Das geschieht über die Kernel-Parameter in dem Verzeichnis /proc/sys/net/ipv6/conf bzw. über das utility sysctl. Überprüfe die Konfiguration und korrigiere sie ggf., z.B. sysctl -w net.ipv6.conf.all.forwarding=1

host:~sysctl net.ipv6.conf.all.forwarding net.ipv6.conf.all.disable_ipv6
net.ipv6.conf.all.forwarding = 1
net.ipv6.conf.all.disable_ipv6 = 0

Schauen wir uns nun die Konfiguration des virtuellen Netzwerkes an, das heißt wahrscheinlich default:

host:~virsh net-list --all
 Name                 State      Autostart     Persistent
----------------------------------------------------------
 default              active     yes           yes

host:~virsh net-dumpxml default
<network>
  <name>default</name>
  <uuid>f4cbfd80-51e4-4086-b3da-481565f9cff8</uuid>
  <forward mode='open'/>
  <bridge name='virbr0' stp='on' delay='0'/>
  <mac address='52:54:00:30:b9:06'/>
  <ip address='192.168.1.1' netmask='255.255.255.0'>
    <dhcp>
      <range start='192.168.1.100' end='192.168.1.254'/>
      <host mac='52:54:00:84:64:f1' name='guets-1' ip='192.168.1.11'/>
    </dhcp>
  </ip>
</network>

Wir weisen also unseren Guests statische IP-Adressen über DHCP zu. Für uns ist das überigens der Prozess dnsmasq, der vom libvirtd gestartet wird.
Wenn wir diese Konfiguration nun ändern, müssen wir das "default" Netz neu starten (destroy und start), wobei aber leider unser Guest vom Netz getrennt wird und sich wahrscheinlich aufhängt, wenn er nicht vorher geregelt heruntergefahren wird. Es gibt die Möglichkeit, gewisse Konfigurationen im laufenden Betrieb nachzuziehen, das sind aber nicht alle und leider nicht die, die wir nun brauchen, siehe virsh net-update Dokumentation.

Der Guest ist so eingestellt, dass er IP-Adressen für Version 4 und 6 per DHCP bzw. DHCP6 bezieht.

/etc/network/interfaces
source /etc/network/interfaces.d/*

auto lo
iface lo inet loopback
iface lo inet6 loopback

allow-hotplug ens3
iface ens3 inet dhcp
iface ens3 inet6 dhcp

Wir editieren das Netz und fügen einen IPv6 Block, sowie einen DHCP6 Bereich hinzu. Wichtig: Editiere nicht die Dateien direkt, sondern benutze virsh net-edit default. Die virsh Konsole wird dir beim Speichern die Konfiguration prüfen und ggf. Syntax-Fehler bemängeln.

host:~virsh net-edit default
<network>
  <name>default</name>
  <uuid>f4cbfd80-51e4-4086-b3da-481565f9cff8</uuid>
  <forward mode='open'/>
  <bridge name='virbr0' stp='on' delay='0'/>
  <mac address='52:54:00:30:b9:06'/>
  <ip address='192.168.1.1' netmask='255.255.255.0'>
    <dhcp>
      <range start='192.168.1.100' end='192.168.1.254'/>
      <host mac='52:54:00:84:64:f1' name='guest-1' ip='192.168.1.11'/>
    </dhcp>
  </ip>
  <ip family='ipv6' address='2001:fff:1057:c0de:0:1:0:2' prefix='96'>
    <dhcp>
      <range start='2001:fff:1057:c0de:0:1:0:77' end=2001:fff:1057:c0de:0:1:0:ff'/>
    </dhcp>
  </ip>
</network>

Nun müssen wir leider das Netz einmal neu starten, denn diese Konfiguration kann nicht dynamisch nachgeladen werden. Doch bevor wir das tun, müssen wir noch einmal unsere Firewall-Konfiguration prüfen, denn wir müssen DHCP6 ermöglichen und generell das Neighbor Discovery ermöglichen. Das "Neighbor Discovery" ist das Pendant zu ARP für IPv4, benutzt jedoch nicht das APR-Protokoll, sondern ICMP6.

An dieser Stelle wird es leider etwas haarig, denn um Fehler in der Konfiguration zu finden, müssen die Logfiles analysiert werden, sowie ggf, der TCP traffic selbst. Benutze dafür journalctl -f um das Logging zu analysieren, tcpdump -i any host [ipv6-address] um den Traffic zwischen dem Host und dem Guets zu analysieren.

Die folgende ip6tables Konfiguration ist nur ein Auszug aus der komplette Konfiguration. Wichtig daran ist, dass der DHCP6 Traffic auf UDP Port 457 zugelassen wird, sowie einige ICMP-Paket-Typen, die für die Ermittlung der Nachbarschaft nötig sind. Beachte auch, dass der Status der ICMP Paktete für die Typen "neighbor-solicitation" und "neighbor-advertisement" INVALID ist, und diese daher akzeptiert werden müssen, bevor die Regel für INVALID Pakete diese verwirft. Ich habe das diesem Archlinux Wiki Artikel entnommen, weiter aber noch keinen Hinweis auf die Ursache dieses Verhaltens gefunden.

„ICMPv6 Neighbor Discovery packets remain untracked, and will always be classified "INVALID" though they are not corrupted or the like. Keep this in mind, and accept them before this rule! iptables -A INPUT -p 41 -j ACCEPT”
/etc/iptables/rules.v6
    ...
#
-A INPUT -p icmpv6 --icmpv6-type destination-unreachable -j ACCEPT
-A INPUT -p icmpv6 --icmpv6-type packet-too-big -j ACCEPT
-A INPUT -p icmpv6 --icmpv6-type time-exceeded -j ACCEPT
-A INPUT -p icmpv6 --icmpv6-type parameter-problem -j ACCEPT
# Allow some other types in the INPUT chain, but rate limit.
-A INPUT -p icmpv6 --icmpv6-type echo-request -m limit --limit 900/min -j ACCEPT
-A INPUT -p icmpv6 --icmpv6-type echo-reply -m limit --limit 900/min -j ACCEPT
# Allow others ICMPv6 types but only if the hop limit field is 255.
-A INPUT -p icmpv6 --icmpv6-type router-advertisement -m hl --hl-eq 255 -j ACCEPT
-A INPUT -p icmpv6 --icmpv6-type neighbor-solicitation"" -m hl --hl-eq 255 -j ACCEPT
-A INPUT -p icmpv6 --icmpv6-type neighbor-advertisement -m hl --hl-eq 255 -j ACCEPT
-A INPUT -p icmpv6 --icmpv6-type redirect -m hl --hl-eq 255 -j ACCEPT
#
# DHCP6
-A INPUT -i virbr+ -m udp -p udp --dport 547 -j ACCEPT
#
-A INPUT -m state --state INVALID -j LOG --log-level 6 --log-prefix "IPT-INVAID: "
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
#
-A INPUT -m state --state UNTRACKED -j LOG --log-level 6 --log-prefix "IPT-UNTRACKED: "
#
-A INPUT -j DROP
#
    ...
#
-A FORWARD -o virbr+ -p tcp -m tcp -m multiport --dports 80,443 -j ACCEPT
#
    ...

Starte das Bridge-Netz neu.

host:~virsh shutdown guest-1
Domain guest-1 is being shutdown

host:~virsh net-destroy default
Network default destroyed

host:~virsh net-start default
Network default started

host:~virsh start guest-1
Domain guest-1 started

Anschließend können wir uns wieder auf unserem Guest einloggen und finden folgende Konfiguration vor:

guest-1:~ip a
1: lo:  mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: ens3:  mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 52:54:00:84:64:f1 brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.11/24 brd 192.168.1.255 scope global dynamic ens3
       valid_lft 3487sec preferred_lft 3487sec
    inet6 2001:fff:1057:c0de:0:1:0:80/128 scope global
       valid_lft forever preferred_lft forever
    inet6 fe80::5054:ff:fe84:64f1/64 scope link
       valid_lft forever preferred_lft forever

guest-1:~ip -6 r
::1 dev lo proto kernel metric 256 pref medium
2001:fff:1057:c0de:0:1:0:80 dev ens3 proto kernel metric 256 pref medium
fe80::/64 dev ens3 proto kernel metric 256 pref medium

guest-1:~ping6 2001:fff:1057:c0de:0:1:0:2
connect: Network is unreachable

Der Guest hat also eine dynaimsche Adresse zugewiesen bekommen, das wolen wir gleich noch einmal auf der Seite DHCP-Server fest ziehen. Und der Guest hat noch keine Route für das IPv6 Netzwerk, bzw. kein Gateway bekommen.

Das können wir nun auf dem Host für das default Netz einstellen, diesmal können wir auch dynamisch das Netz aktualisieren, siehe net-update Dokumentation. Wir brauchen dafür jedoch noch die "Client ID" oder "DUID" des Guests und müssen beachten, dass wir den rchtigen DHCP Konten verwalten, den wir haben zwei, einen für IPv4 und einen für IPv6. Auch müssen wir mit den Anführungszeichen aufpassen und im "inneren XML" nur einfache Hochkommata verwenden.

host:~virsh dominfo guest-1
Id:             1
Name:           guest-1
UUID:           7dc6f99a-2e2a-4b2b-a35d-eadde15f948d
OS Type:        hvm
State:          running
CPU(s):         1
CPU time:       98.7s
Max memory:     1048576 KiB
Used memory:    1048576 KiB
Persistent:     yes
Autostart:      enable
Managed save:   no
Security model: none
Security DOI:   0

host:~virsh net-dhcp-leases default
 Expiry Time          MAC address        Protocol  IP address                Hostname        Client ID or DUID
-------------------------------------------------------------------------------------------------------------------
 2020-06-08 12:37:29  52:54:00:84:64:f1  ipv6      2001:fff:1057:c0de:0:1:0:80/96 -          00:01:00:01:26:3c:34:f5:52:54:00:b0:57:5f
 2020-06-08 12:49:42  52:54:00:84:64:f1  ipv4      192.168.1.11/24           guest-1         ff:00:84:64:f1:00:01:00:01:26:3c:34:f5:52:54:00:b0:57:5f

host:~virsh net-update default add ip-dhcp-host \
"<host id='00:01:00:01:26:3c:34:f5:52:54:00:b0:57:5f' \
        name='guest-1' \
        ip='2001:fff:1057:c0de:0:1:0:11' />" \
        --live --config --parent-index 1
Updated network default persistent config and live state

host:~virsh net-dumpxml default
<network>
  <name>default</name>
  <uuid>f4cbfd80-51e4-4086-b3da-481565f9cff8</uuid>
  <forward mode='open'/>
  <bridge name='virbr0' stp='on' delay='0'/>
  <mac address='52:54:00:30:b9:06'/>
  <ip address='192.168.1.1' netmask='255.255.255.0'>
    <dhcp>
      <range start='192.168.1.100' end='192.168.1.254'/>
      <host mac='52:54:00:84:64:f1' name='guest-1' ip='192.168.1.11'/>
    </dhcp>
  </ip>
  <ip family='ipv6' address='2001:fff:1057:c0de:0:1:0:2' prefix='96'>
    <dhcp>
      <range start='2001:fff:1057:c0de:0:1:0:77' end='2001:fff:1057:c0de:0:1:0:ff'/>
      <host id='00:01:00:01:26:3c:34:f5:52:54:00:b0:57:5f' name='guest-1' ip='2001:fff:1057:c0de:0:1:0:11'/>
    </dhcp>
  </ip>
</network>

Nun braucht der Guest noch einen neuen DHCP lease. Da habe ich vergeblich versucht auf Guest-Seite über dhclient den lease neu anzufordern, daraufhin habe ich dem Guest virtuell das Netzwerkkabel gezogen:

host:~virsh domiflist guest-1
Interface  Type       Source     Model       MAC
-------------------------------------------------------
vnet1      network    default    virtio      52:54:00:84:64:f1

host:~virsh domif-setlink guest-1 vnet1 down
Device updated successfully

host:~virsh domif-setlink guest-1 vnet1 up
Device updated successfully

guest:~ip a
1: lo:  mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: ens3:  mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 52:54:00:84:64:f1 brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.11/24 brd 192.168.1.255 scope global dynamic ens3
       valid_lft 3539sec preferred_lft 3539sec
    inet6 2001:fff:1057:c0de:0:1:0:11/128 scope global
       valid_lft forever preferred_lft forever
    inet6 2001:fff:1057:c0de:0:1:0:80/128 scope global
       valid_lft forever preferred_lft forever
    inet6 fe80::5054:ff:fe84:64f1/64 scope link
       valid_lft forever preferred_lft forever
guest:~ip -6 r
::1 dev lo proto kernel metric 256 pref medium
2001:fff:1057:c0de:0:1:0:11 dev ens3 proto kernel metric 256 pref medium
2001:fff:1057:c0de:0:1:0:80 dev ens3 proto kernel metric 256 pref medium
fe80::/64 dev ens3 proto kernel metric 256 pref medium

Nun hat der Guest die gewünschte IPv6-Adresse erhalten. Er hat auch die vorherige, aber diese und die entsprechende Route laufen in kurzer Zeit ab und werden dann automatisch entfernt werden.

Wir stellen fest, dass auch noch etwas Routig fehlt. Das hat mich etwas gewundert und ich habe viele Ansätze ausprobiert. Doch das war am Ende leider alles vergeblich, das Routing in der VM in Kombination mit DHCP6 ließ sich nicht einzustellen.
Das mag an meiner libvirt Version liegen, die ist nicht mehr ganz auf dem neuesten Stand.
In der Netzwerk-Definition von libvirt gibt es einen Abschnitt über Statische Routen, doch das trifft für unseren Fall nicht zu, denn das bezieht sich auf die Host-Seite für Subnetze, die in den VMs nicht direkt vom Host aus erreichbar sind.

Ich bin an dieser Stelle dazu übergegangen, die Adressvergabe per DHCP/DHCP6 beizubehalten, jedoch die benötigten Routen auf dem Guest selbst in den Netzwerk-Definitionen zu hinterlegen. Die Datei /etc/network/interfaces auf dem Guest wird wie folgt angepasst:

/etc/network/interfaces
source /etc/network/interfaces.d/*

auto lo
iface lo inet loopback
iface lo inet6 loopback

allow-hotplug ens3
iface ens3 inet dhcp
iface ens3 inet6 dhcp
  up ip -6 route add 2001:fff:1057:c0de:0:1::/96 dev ens3
  up ip -6 route add default via 2001:fff:1057:c0de:0:1:0:2

Wenn wir die Netzwerk-Konfiguration des Guests nun neu laden (z.B. reboot), erhalten wir folgende Konfiguration:

guest:~ip -6 r
::1 dev lo proto kernel metric 256 pref medium
2001:fff:1057:c0de:0:1:0:11 dev ens3 proto kernel metric 256 pref medium
2001:fff:1057:c0de:0:1::/96 dev ens3 metric 1024 pref medium
fe80::/64 dev ens3 proto kernel metric 256 pref medium
guest:~ping6 2001:fff:1057:c0de::2
64 bytes from 2001:fff:1057:c0de::2: icmp_seq=1 ttl=64 time=0.305 ms
64 bytes from 2001:fff:1057:c0de::2: icmp_seq=2 ttl=64 time=0.321 ms
64 bytes from 2001:fff:1057:c0de::2: icmp_seq=3 ttl=64 time=0.326 ms
^C
--- 2001:fff:1057:c0de::2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 40ms
rtt min/avg/max/mdev = 0.305/0.317/0.326/0.017 ms
guest:~ping6 google.de
PING google.de(fra16s14-in-x03.1e100.net (2a00:1450:4001:81a::2003)) 56 data bytes
64 bytes from fra16s14-in-x03.1e100.net (2a00:1450:4001:81a::2003): icmp_seq=1 ttl=56 time=5.18 ms
64 bytes from fra16s14-in-x03.1e100.net (2a00:1450:4001:81a::2003): icmp_seq=2 ttl=56 time=5.32 ms
64 bytes from fra16s14-in-x03.1e100.net (2a00:1450:4001:81a::2003): icmp_seq=3 ttl=56 time=5.36 ms
^C
--- google.de ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 6ms
rtt min/avg/max/mdev = 5.177/5.287/5.363/0.115 ms

Der Guest ist ebenfalls vom Host und von extern erreichbar (wenn auf dem externen Rechner IPv6 verfügbar ist) und auch der Webserver ist unter der IPv6 URL erreichbar https://[2001:fff:1057:c0de:0:1:0:11]/ :

host:~ping6 2001:fff:1057:c0de:0:1:0:11
PING 2001:fff:1057:c0de:0:1:0:11(2001:fff:1057:c0de:0:1:0:11) 56 data bytes
64 bytes from 2001:fff:1057:c0de:0:1:0:11: icmp_seq=1 ttl=64 time=0.255 ms
64 bytes from 2001:fff:1057:c0de:0:1:0:11: icmp_seq=2 ttl=64 time=0.164 ms
64 bytes from 2001:fff:1057:c0de:0:1:0:11: icmp_seq=3 ttl=64 time=0.430 ms
^C
--- 2001:fff:1057:c0de:0:1:0:11 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2037ms
rtt min/avg/max/mdev = 0.164/0.283/0.430/0.110 ms
somewhere:~ping6 2001:fff:1057:c0de:0:1:0:11
PING 2001:fff:1057:c0de:0:1:0:11(2001:fff:1057:c0de:0:1:0:11) 56 data bytes
64 bytes from 2001:fff:1057:c0de:0:1:0:11: icmp_seq=1 ttl=53 time=31.2 ms
64 bytes from 2001:fff:1057:c0de:0:1:0:11: icmp_seq=2 ttl=53 time=24.0 ms
64 bytes from 2001:fff:1057:c0de:0:1:0:11: icmp_seq=3 ttl=53 time=28.3 ms
^C
--- 2001:fff:1057:c0de:0:1:0:11 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2002ms
rtt min/avg/max/mdev = 24.021/27.874/31.208/2.956 ms

Zusammenfassung

IPv6 ist doch deutlich anderes als IPv4. Der ganze Bereich "Neighbor Detection" und "Router Advertising", kein ARP, kein Proxy-ARP, keine Broadcasts, dafür deutlich stärkere Nutzung von ICMP Nachrichten. Gerade ICMP6 in Kombination mit der ip6tables Firewall war für mich ein großes Hindernis. Dass ein ICMP6 Paket den Status INVALID haben kann, erscheint mir wie ein mitgewachsener Fehler.
Es würde mich nicht wundern, wenn viele Admins einfach ALLE ICMP6 Pakete erlauben.
Das ist auch so ein Punkt: kaum ist IPv6 für meinen Webhost aktiv, wird er schon reichlich "ausgespäht".
Dass das Routing auf Seiten der VMs nicht so funktioniert wie gedacht ist zwar schade, aber es fragt sich auch, ob man in dieser Konstellation überhaupt mit DHCP/DHCP6 arbeiten sollte, oder nicht besser eh' alles statisch konfiguriert.

Schickt Kommentare oder Anmerkungen bitte über Twitter an @limitland oder @jlue.