In diesem Beitrag erkläre ich, wie ich stets aktuelle DNS-Einträge für die IPv6-Adressen meiner VMs sicherstelle, auch wenn sich das IPv6-Präfix ändert, weil mir der Provider ein neues zugewiesen hat. Dazu habe ich ein mehrteiliges System programmiert, bei dem Clients auf den VMs ihre aktuelle IP-Adresse an einen zentralen Server im Netzwerk schicken, welcher notwendige DNS-Änderungen bei CloudFlare, meinem DNS-Hoster im Internet, vornimmt.

Weitere Beiträge aus dieser Reihe:

Hintergrund

Ich möchte in meinem Netzwerk IPv6 mit DNS verwenden, dazu sind AAAA-Einträge notwendig (IPv4 hat A-Einträge). Die IP-Adressen der VMs sind allerdings nicht stabil genug, um einfach statische Einträge im DNS anzulegen und die Sache damit abzuhaken. Alle 24 Stunden bekomme ich von meinem Provider ein neues IPv6-Präfix, d.h. die ersten 56 Bit der Adressen ändern sich. Der „hintere” Teil bleibt gleich. Ich bekomme also zwar 4722366482869645213696 Adressen zu meiner Verfügung, aber eben jeden Tag andere 4722366482869645213696 Adressen; Knappheit der Adressen kann nicht der Grund sein. Dazu kommt außerdem noch, dass ich auch neue VMs anlege und die auch gleich im DNS erscheinen sollen.

Für IPv4-Adressen klappt das in meinem Netzwerk bereits automatisch. pfsense stellt einen DHCP-Server zur Vergabe der Adressen bereit und die dort registrierten VMs schicken bei der Anfrage nach einer IP-Adresse auch gleich ihren Hostname mit, also kann auch automatisch eine Zuordnung zwischen DNS-Eintrag und dazugehöriger IP-Adresse festgelegt werden. Bei IPv6 ist das leider anders: Es ist mit automatischer Konfiguration der IP-Adressen (SLAAC) erst mal gar nicht notwendig, überhaupt DHCP zu verwenden, aber auch wenn man DHCPv6 verwendet, ist der Hostname der VM kein Teil der Anfrage – eine automatische Zuordnung von Hostname zu IPv6-Adresse ist also nicht mehr trivial möglich.

Weiterhin bekommt man im Fall von IPv4 daheim meist nur eine Adresse, die sich ebenfalls regelmäßig ändert. Nach außen hin teilen sich zwangsweise alle VMs die selbe IPv4-Adresse. Notfalls muss ein Proxyserver ggf. Anfragen auf die jeweils zuständigen Hosts verteilen, wenn man von außen auf verschiedene Dienste zugreifen will, die alle den gleichen Port (z.B. 443 für HTTPS) verwenden. Bei IPv6 kann man das auch anders lösen: öffnet man den Port in der Firewall (in meinem Fall in der FritzBox und im pfsense), kann man von außen direkt die VM ansprechen.

Mit dieser Kombination von Schwierigkeiten habe ich mich entschieden, ein verhältnismäßig einfaches System zu programmieren: Auf jeder VM läuft ein Client, der regelmäßig die (öffentlich routbare, also kein ULA) IPv6-Adresse an einen zentralen Server im Netzwerk schickt, der darüber Buch führt und bei Änderungen die notwendigen Updates im DNS von CloudFlare macht.

Setup

Das Setup besteht aus mehreren Komponenten:

  • Das Tool selbst
    • Server: mit Ruby/Sinatra programmiert, liegt auf einer zentralen VM, akzeptiert Aktualisierung der IP-Adresse eines Hosts überHTTP POST-Anfrage.
    • Client: mit Ruby programmiert, läuft auf jeder VM, schickt regelmäßig die aktuelle IP-Adresse der VM an den zentralen Server per HTTP POST, mit Authentifizierung.
    • Worker: mit Ruby programmiert, spricht die CloudFlare-API an und aktualisiert dort die DNS-Einträge.
  • Proxyserver: Zum Beispiel Apache oder Nginx, ist dem Server vorgeschaltet und kümmert sich um Authentifizierung (gegen FreeIPA in meinem Fall) und terminiert SSL.
  • Puppet: Ein Puppet-Modul zur Installation des Tools sowie ein Profil zur Konfiguration der notwendigen Einstellungen drum herum setzen die ganze Sache in meiner Infrastruktur auf.

Automatische Installation

Wie das meiste andere an meinem Setup findet auch die Installation dieses Tools mit Puppet statt. Dazu habe ich ein Modul in der Puppet Forge hochgeladen: fheinle/ddnsv6. Mit Hilfe dieses Moduls ist es recht einfach, sich die Infrastruktur entsprechend zu konfigurieren:

class profiles::ddns {
  $ddns_server         = lookup('profiles::ddns::server',                   String,  'first')
  $ddns_server_ssl     = lookup('profiles::ddns::server_ssl',               Boolean, 'first', true)
  $ddns_server_port    = lookup('profiles::ddns::server_port',              Integer, 'first', 443)
  $ddns_user           = lookup('profiles::ddns::ddns_user',                String,  'first', 'ddns')
  $install_client      = lookup('profiles::ddns::install_client',           Boolean, 'first', true)
  $install_server      = lookup('profiles::ddns::install_server',           Boolean, 'first', false)
  $install_directory   = lookup('profiles::ddns::install_directory',        String,  'first', "/home/${ddns_user}/app")
  $domain_whitelist    = lookup('profiles::ddns::domain_whitelist',         Array,   'unique')
  $cloudflare_user     = lookup('cloudflare_user',                          String,  'first')
  $cloudflare_password = lookup('cloudflare_password',                      String,  'first')
  $client_username     = lookup('profiles::ddns::client_username',          String,  'first')
  $client_password     = lookup('profiles::ddns::client_password',          String,  'first')

  file { 'ddns_home_dir':
    ensure => 'directory',
    path   => "/home/${ddns_user}",
    owner  => $ddns_user,
    mode   => '0700',
  }

  class { '::ddns':
    install_directory => $install_directory,
    install_repo      => $install_repo,
    create_ddns_user  => false
  }

  if $install_client == true {
    class {'::ddns::client':
      ddns_server      => $ddns_server,
      ddns_server_ssl  => $ddns_server_ssl,
      ddns_server_port => $ddns_server_port,
      client_username  => $client_username,
      client_password  => $client_password
    }
  }
  elsif $install_server == true {
  class { '::ddns::server':
      domain_whitelist    => $domain_whitelist,
      cloudflare_user     => $cloudflare_user,
      cloudflare_password => $cloudflare_password
    }
  }
}

Gleich ganz oben werden dabei einige Anfragen an Hiera gestellt, um dort die notwendigen Einstellungen zu beziehen. Diese müssen natürlich entsprechend der Parameter hinterlegt sein. Die meisten Parameter können irgendwo global hinterlegt werden, in einer common.yaml oder so. Die Unterscheidung zwischen client und server wird jedoch auch im Hiera gemacht.

Alle am IPv6-Betrieb beteiligten Hosts brauchen die Klasse ::ddns.

In der Konfiguration des Servers (z.B. über nodes/server.domain.invalid) kann dann profiles::ddns::install_client auf falseund profiles::ddns_install_server auf true gesetzt werden. Analog kann der Client konfiguriert werden, mit profiles::ddns::install_client auf trueund profiles::ddns::install_server auf false.

Benutzung

Das Setup ist mit seinen Cronjobs weitestgehend automatisiert und man muss maximal 10 Minuten warten, wenn ein Präfixwechsel erfolgt ist, bis die neuen IP-Adressen an CloudFlare übertragen wurden. Bei der ersten Installation muss allerdings Puppet zwei mal aufgerufen werden, damit zuerst die Clients ihre Hostnames als Fragmente für die Whitelist hinterlegen und beim zweiten Durchlauf der Server diese Fragmente einsammelt und zur Whitelist zusammenführt. Etwas umständlich, aber so funktionieren exported resources bei Puppet eben.