Folgende Problemstellung sei gegeben:

Man hat ein Image für eine Debian-Instalation, das man auf einem Rechner (Dabei ist es egal, ob echt oder virtuell) aufgespielt hat, kein Netzwerk definiert, man kennt aber die MAC-Adresse des Netzwerk-Interfaces und will jetzt das Netzwerk konfigurieren.

Die Lösung ist relativ einfach, da man über die MAC-Adresse die IPv6-Link-Local-Adresse berechnen kann, mit der man sich dann zum sshd des Rechners verbinden kann.

Ich zeige nun wie ich das Ganze über Ansible gelöst habe.

Meine Ordnerstruktur für mein Ansible-Setup sieht so aus:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
. ansible.cfg
. hosts
/ host_vars
|--> . xmpp.int.datenknoten.me
/ playbooks
|--> . network.yml
|--> / filter_plugins
|----> . conv_mac2ll.py
/ templates
|--> network
|----> . interfaces
|----> . resolv.conf
|----> . sysctl.conf

Nun werde ich erklären, was die einzelnen Dateien tun.

Die Datei hosts sieht für dieses Beispiel so aus:

1
2
[linux_guests]
xmpp.int.datenknoten.me

In dem Ordner host_vars gibt es für jeden Host eine Datei. Für den Rechner xmpp.int.datenknoten.me sieht die Datei so aus:

1
2
3
---
id: 4
mac: 52:54:00:b8:e9:a1

Das Feld id enthält eine Zahl, die für die Berechnung der IPv4 NAT Adresse und der globalen IPv6 Adresse benutzt wird. Das Feld mac ist die MAC-Adresse des Rechners. Diese Adresse wird für die Berechnung der IPv6-Link-Local-Adresse benötigt.

Die Datei conv_mac2ll.py enthält ein Python-Skript, welches die eigentliche Berechnung vornimmt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/usr/local/bin/python

def mac2ll(mac):
    mac = mac.split(":")
    mac.insert(3,'fe')
    mac.insert(3,'ff')
    mac[0] = str(int(mac[0]) ^ 6)
    return "fe80::%s:%s:%s:%s" % ("".join(mac[0:2]),"".join(mac[2:4]),"".join(mac[4:6]),"".join(mac[6:8]))


class FilterModule(object):
    ''' Ansible network jinja2 filters '''
    def filters(self):
        return {
            'mac2ll': mac2ll
            }

Mein eigentliches Playbook für den Netzwerkkram ist dadurch sehr übersichtlich:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
---
- hosts: linux_guests
  vars:
    ansible_ssh_host: "{{ mac|mac2ll }}%vtnet0"
  tasks:
    - name: write sysctl.conf to the disk
      copy: src=../templates/network/sysctl.conf dest=/etc/sysctl.conf
    - name: write resolv.conf to the disk
      copy: src=../templates/network/resolv.conf dest=/etc/resolv.conf
    - name: write /etc/network/interfaces
      template: src=../templates/network/interfaces dest=/etc/network/interfaces
    - name: restart networking
      shell: "/sbin/ifdown eth0; /sbin/ifup eth0"

Da ich die Playbooks auf einer FreeBSD Kiste ausführe, heißt das Netzwerk-Interface hier vtnet0. Unter Linux heißt dieses aller Wahrscheinlichkeit nach eth0.

In der sysctl.conf deaktiviere ich das Router Advertisement Protocol, da ich alles hart verdrahte:

1
2
net.ipv6.conf.all.accept_ra=0
net.ipv6.conf.all.autoconf=0

Die Namensserver-Konfiguration ist jetzt auch nicht weltbewegend:

1
2
nameserver 192.168.122.9
search int.datenknoten.me

Und zum Schluss das Wichtigste, die Netzwerkkonfiguration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
auto lo
iface lo inet loopback

# The primary network interface
auto  eth0
iface eth0 inet static
  address   192.168.122.{{ id }}
  broadcast 192.168.122.255
  netmask   255.255.255.0
  gateway   192.168.122.1

iface eth0 inet6 static
  address 2a01:4f8:200:2265:3:100::{{ id }}
  netmask 112
  gateway fe80::5054:ff:fe9f:c3e4

Ziel dieses Artikel ist es, zu zeigen wie mein OpenVPN-Setup funktioniert, wie ich meine Clients konfiguriere und was das für Vorteile bringt.

Fangen wir mal mit dem ‘Warum’ an. Mein initiales Anliegen war in das virtuelle Netzwerk meines libvirt/KVM-Hostes einzusteigen, damit ich die virtuellen Maschinen besser verwalten kann. Ich habe zuerst an IPSec versucht, bin da aber an der harschen Internet-Realität gescheitert. So hat mein Internet-Anbieter Kabel Deutschland fragmentierte UDP-Pakete klammheimlich verworfen. Hat mit mich einige Zeit gekostet, das rauszufinden. Aber dank Netalyzr findet man solche Sachen dann doch relativ schnell. An dieser Stelle muss ich mal eine Lanze für Netalyzr brechen. Wenn ihr in ein neues Netz kommt, führt den Test einmal aus, um euch der Limitationen des Netzes bewusst zu werden. Dann habe ich mich an OpenVPN versucht und alles funktionierte ohne mit der Wimper zu zucken.

Bevor ich jetzt zu der Konfiguration komme, noch ein paar Takte zu meiner Infrastruktur.

Infrastruktur

Ich habe einen Server bei Hetzner gemietet, auf dem ich mehrere virtuelle Maschinen mit unterschiedichen Diensten betreibr. Da ich nur eine IPv4-Addresse habe und aus Kostengründen nichts ändern will, habe ich ein privates LAN mit NAT für die ganzen virtuellen Rechner eingerichtet. Das IPv4-Lan hat den Prefix 192.168.122.0/24, dieser Prefix sollte natürlich auch über das VPN erreichbar sein. Daneben habe ich von Hetzner einen nativen IPv6 Zugang erhalten und habe den Prefix 2a01:4f8:200:2265::/64. Was ich im Verlauf des Einrichtens gelernt habe ist: Wenn man verschiedene Unterinfrastrukturen hat, sollte man diese auch mit eigenen Prefixen beglücken. So habe ich den /64 in mehrere /112-Netze aufgeteilt. Die virtuellen Server haben den Prefix 2a01:4f8:200:2265:3::/112 und das VPN-Netz hat 2a01:4f8:200:2265:4::/112. Was ich in diesem Kontext auch gelernt habe, das 2000::/3 der komplette öffentlich routbare Teil von IPv6 ist. Zum Schluss sei noch gesagt, dass ich einen LDAP-Verzeichnis betreibe, in welchem ich Benutzer verwalte. Das sollte auch an das OpenVPN angebunden werden. Soviel zur Infrastruktur, jetzt zur Konfiguration:

Server Konfiguration

Meine Server Konfiguration sieht so aus:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Crypto konfigurieren
ca /etc/ipsec.d/cacerts/cacert.pem
cert /etc/ipsec.d/certs/node2.datenknoten.me.pem
key /etc/ipsec.d/private/node2.datenknoten.me.pem
dh /etc/ipsec.d/dh4096.pem

# Netzwerk aufsetzen
server 10.8.0.0 255.255.255.0
server-ipv6 2a01:4f8:200:2265:4::1/112
push "route 192.168.122.0 255.255.255.0"
push "redirect-gateway def1 bypass-dhcp"
push "route-ipv6 2000::/3"
push "dhcp-option DNS 192.168.122.9"

# Einstellungen
keepalive 10 120
comp-lzo
persist-key
persist-tun
verb 3
cipher AES-256-CBC
port 1194
proto tcp
dev tun

# LDAP aktivieren
plugin /usr/lib/openvpn/openvpn-auth-ldap.so /etc/openvpn/auth-ldap.conf

Die LDAP-Konfiguration sieht so aus:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<LDAP>
        URL             ldaps://ldap.datenknoten.me
        BindDN          cn=systemuser,ou=users,dc=datenknoten,dc=me
        Password        tolles passwort
        Timeout         15
        TLSEnable       no
        FollowReferrals yes
</LDAP>

<Authorization>
        BaseDN          "ou=users,dc=datenknoten,dc=me"
        SearchFilter    "(uid=%u)"
        RequireGroup    false
        <Group>
                BaseDN          "ou=groups,dc=datenknoten,dc=me"
                SearchFilter    "cn=vpnusers"
                MemberAttribute memberUid
        </Group>
</Authorization>

Hier ist anzufügen, dass die Limitierung auf die Gruppe vpnusers irgendwie nie geklappt hat. Für sachdienliche Hinweise wäre ich sehr dankbar.

Als nächstes muss auf dem Server noch die Firewall eingerichtet werden. Ich benutze das Programm „Ferm”, um meine IPTables-Regeln zu verwalten. Entsprechend sieht mein Script so aus:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# -*- shell-script -*-
#
#  Configuration file for ferm(1).
#

@def $DEV_WORLD = eth0;
@def $DEV_DMZ = virbr1;

@def $HOST_STATIC = 144.76.154.114;


@def $DEV_PRIVATE = virbr1;
@def $NET_PRIVATE = 192.168.122.0/24;

@def $DEV_VPN = tun0;
@def $NET_VPN = 10.8.0.0/24;

# convenience function which creates both the nat/DNAT and the filter/FORWARD
# rule
@def &FORWARD_TCP($proto, $port, $dest) = {
    # interface (lo $DEV_WORLD $DEV_VPN $DEV_PRIVATE)
    table filter chain FORWARD outerface $DEV_DMZ daddr $dest proto $proto dport $port ACCEPT;
    table nat chain PREROUTING daddr $HOST_STATIC proto $proto dport $port DNAT to $dest;
}

table filter {
    chain INPUT {
        policy ACCEPT;

        mod state state INVALID DROP;
        mod state state (ESTABLISHED RELATED) ACCEPT;

        interface $DEV_DMZ proto (tcp udp) dport (53 67) ACCEPT;
    }
    chain OUTPUT {
        policy ACCEPT;
    }
    chain FORWARD {
        policy ACCEPT;
        mod state state INVALID DROP;
        mod state state (ESTABLISHED RELATED) ACCEPT;
        interface $DEV_PRIVATE ACCEPT;
        interface $DEV_VPN mod conntrack ctstate NEW ACCEPT;
        mod conntrack ctstate (ESTABLISHED RELATED) ACCEPT;
    }
}

table nat {
    chain POSTROUTING {
        # masquerade private IP addresses
        saddr ($NET_PRIVATE $NET_VPN) outerface $DEV_WORLD MASQUERADE;
    }
}

domain ip6 {
    table filter {
        chain INPUT {
            policy ACCEPT;
        }
        chain OUTPUT {
            policy ACCEPT;
        }
        chain FORWARD {
            policy ACCEPT;
            interface ($DEV_WORLD $DEV_VPN) outerface $DEV_DMZ daddr 2a01:4f8:200:2265::/64 ACCEPT;
            outerface ($DEV_WORLD $DEV_VPN) interface $DEV_DMZ saddr 2a01:4f8:200:2265::/64 ACCEPT;
            interface $DEV_DMZ outerface $DEV_DMZ ACCEPT;
            interface ($DEV_WORLD $DEV_VPN) outerface $DEV_DMZ REJECT reject-with icmp6-port-unreachable;
            outerface ($DEV_WORLD $DEV_VPN) interface $DEV_DMZ REJECT reject-with icmp6-port-unreachable;

        }
    }
}

&FORWARD_TCP(tcp, (80 443), 192.168.122.2);

Es empfiehlt sich natürlich, sich etwas mit der Materie auseinanderzusetzen, damit man versteht, was ich hier schreibe. Vieles, was in diesen Konfigurations-Dateien steht, hat sich über die Jahre so entwickelt.

Client Konfiguration (Linux)

Unter Linux ist bis auf einen Punkt eigentlich alles sehr entspannt. Das Problem ist, dass der DNS-Server, den ich bereitstelle, nicht übernommen wird. Dafür gibt es eine Lösung und jetzt erstmal die Config:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
client
dev tun
proto tcp
remote 144.76.154.114
resolv-retry infinite
nobind
persist-key
persist-tun
ca dk-ca.crt
cert manjaro.crt
key manjaro.key
verb 3
cipher AES-256-CBC
auth SHA1
reneg-sec 0
route-delay 4
comp-lzo no
auth-user-pass
script-security 2
up /home/hana/openvpn/datenknoten/update-dns
down /home/hana/openvpn/datenknoten/update-dns

2 Anmerkungen: Zum einen sei hier der Eintrag auth-user-pass hervorzuheben, der den Client auffordert sich Zugangsdaten vom Benutzer zu erfragen. Zum anderen die letzten 3 Zeilen. Diese sorgen nämlich mittels einem kleinen Skript, welches openresolv aufruft, dafür, dass die mitgelieferten DNS-Server in die Datei /etc/resolv.conf eingetragen werden. Hier das Skript:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#!/bin/bash
#
# Parses DHCP options from openvpn to update resolv.conf
# To use set as 'up' and 'down' script in your openvpn *.conf:
# up /etc/openvpn/update-resolv-conf
# down /etc/openvpn/update-resolv-conf
#
# Used snippets of resolvconf script by Thomas Hood <jdthood@yahoo.co.uk>
# and Chris Hanson
# Licensed under the GNU GPL.  See /usr/share/common-licenses/GPL.
# 07/2013 colin@daedrum.net Fixed intet name
# 05/2006 chlauber@bnc.ch
#
# Example envs set from openvpn:
# foreign_option_1='dhcp-option DNS 193.43.27.132'
# foreign_option_2='dhcp-option DNS 193.43.27.133'
# foreign_option_3='dhcp-option DOMAIN be.bnc.ch'
# foreign_option_4='dhcp-option DOMAIN-SEARCH bnc.local'

## You might need to set the path manually here, i.e.
RESOLVCONF=/sbin/resolvconf

case $script_type in

up)
  for optionname in ${!foreign_option_*} ; do
    option="${!optionname}"
    echo $option
    part1=$(echo "$option" | cut -d " " -f 1)
    if [ "$part1" == "dhcp-option" ] ; then
      part2=$(echo "$option" | cut -d " " -f 2)
      part3=$(echo "$option" | cut -d " " -f 3)
      if [ "$part2" == "DNS" ] ; then
        IF_DNS_NAMESERVERS="$IF_DNS_NAMESERVERS $part3"
      fi
      if [[ "$part2" == "DOMAIN" || "$part2" == "DOMAIN-SEARCH" ]] ; then
        IF_DNS_SEARCH="$IF_DNS_SEARCH $part3"
      fi
    fi
  done
  R=""
  if [ "$IF_DNS_SEARCH" ]; then
    R="search "
    for DS in $IF_DNS_SEARCH ; do
      R="${R} $DS"
    done
  R="${R}
"
  fi

  for NS in $IF_DNS_NAMESERVERS ; do
    R="${R}nameserver $NS
"
  done
  #echo -n "$R" | $RESOLVCONF -p -a "${dev}"
  echo -n "$R" | $RESOLVCONF -a "${dev}.inet"
  ;;
down)
  $RESOLVCONF -d "${dev}.inet"
  ;;
esac

Client Konfiguration (Android)

Unter Android benutze ich OpenVPN for android, welches es auch im F-Droid Store gibt.

Das Einrichten ist einfach und es gibt nichts zu beachten. Als ich das VPN eingerichtet habe, habe ich einen Bug in dem von mir benutzen XMPP-Client Conversations gefunden, weil dieser, bzw. die darunterliegende DNS-Bibliothek die DNS-Server nicht richtig bestimmen konnte (Ist inzwischen behoben).

Fazit

Insgesamt bin ich mit dem Setup sehr zufrieden, vorallem weil es kaputte Netze brauchbar macht, da ich durch das VPN ein zensurfreies Netz, IPv6, einen DNSSEC fähigen Resolver bekomme. Bei Fragen könnt ihr mich entweder per E-Mail, im Chat des Krautspaces kontaktieren oder hinterlasst einen Kommentar am Ende.