diff --git a/.ansible-lint b/.ansible-lint
index 207e16409bd3f5cb54d18bafac698558034ad335..d2b1d24065b7a2d3f4156e4c99d474cfa8c3925d 100644
--- a/.ansible-lint
+++ b/.ansible-lint
@@ -2,3 +2,4 @@ skip_list:
   - 208
   - 301
   - 305
+  - 701
diff --git a/group_vars/all.yml b/group_vars/all.yml
index c6fc5a5ce0afc040f3067bd5ed9bd30bc7ff36c8..5ef1a60c0cd01a997397047d69078e7b5f8348ef 100644
--- a/group_vars/all.yml
+++ b/group_vars/all.yml
@@ -3,6 +3,7 @@ radvd_prefixes:
   - 2001:67c:2d50::/64
 dhcpd_subnet: 10.130.0.0
 dhcpd_netmask: 255.255.0.0
+dhcpd_domain: ffhl.de
 ipv4_subnet: 10.130.0.0/16
 ipv6_subnet: 2001:67c:2d50::/48
 
diff --git a/playbook.yml b/playbook.yml
index 5fa9385ab8160cfce0736745c5b45ddcc87b594b..34fc42e89011e94509c9bc5d9a0c6e62da9f5e8e 100644
--- a/playbook.yml
+++ b/playbook.yml
@@ -3,6 +3,8 @@
   become: yes
   roles:
     - base
+    - role: meshvpn
+      tags: [meshvpn]
     - ffhl_nameserver
 
 - hosts: kaisertor
diff --git a/roles/meshvpn/files/network/10-ffhl-mesh.network b/roles/meshvpn/files/network/10-ffhl-mesh.network
new file mode 100644
index 0000000000000000000000000000000000000000..5e761d0b4b663a34ba8f5345cc419dd19f568c30
--- /dev/null
+++ b/roles/meshvpn/files/network/10-ffhl-mesh.network
@@ -0,0 +1,10 @@
+[Match]
+Name=ffhl_mesh_*
+
+[Network]
+LinkLocalAddressing=no
+IPv6AcceptRA=no
+DHCP=no
+
+[Link]
+ARP=no
diff --git a/roles/meshvpn/files/systemd/update-ffhl-mesh-vpn.service b/roles/meshvpn/files/systemd/update-ffhl-mesh-vpn.service
new file mode 100644
index 0000000000000000000000000000000000000000..2e545d493b469bb65213a5da4691145df2210dcb
--- /dev/null
+++ b/roles/meshvpn/files/systemd/update-ffhl-mesh-vpn.service
@@ -0,0 +1,4 @@
+[Service]
+Type=oneshot
+#WorkingDirectory=/var/local/ffhl-mesh-vpn-peers
+ExecStart=/usr/local/lib/ffhl/update-meshvpn-keys.sh
diff --git a/roles/meshvpn/files/systemd/update-ffhl-mesh-vpn.timer b/roles/meshvpn/files/systemd/update-ffhl-mesh-vpn.timer
new file mode 100644
index 0000000000000000000000000000000000000000..1519f9793d1193cf3c1bc195fe467bc58a943a28
--- /dev/null
+++ b/roles/meshvpn/files/systemd/update-ffhl-mesh-vpn.timer
@@ -0,0 +1,5 @@
+[Timer]
+OnCalendar=*:00/15
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/meshvpn/meta/main.yml b/roles/meshvpn/meta/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4bdca2efd08744f9334fe73ef978e34d6bf0f09b
--- /dev/null
+++ b/roles/meshvpn/meta/main.yml
@@ -0,0 +1,4 @@
+---
+
+dependencies:
+  - role: base
diff --git a/roles/meshvpn/tasks/base.yml b/roles/meshvpn/tasks/base.yml
new file mode 100644
index 0000000000000000000000000000000000000000..533eb0dc2e077c357a803617e81a5934009ef204
--- /dev/null
+++ b/roles/meshvpn/tasks/base.yml
@@ -0,0 +1,46 @@
+---
+
+- name: copy base configs
+  tags: [base, etc, apt]
+  copy:
+    src: etc/
+    dest: /etc
+
+
+- name: install packages
+  apt:
+    autoremove: yes
+    update_cache: yes
+    state: present
+    name:
+      # necessary packets
+      - batctl
+      - fastd
+      - iproute2
+      - isc-dhcp-server
+      - radvd
+
+- name: load batman-adv
+  modprobe:
+    name: batman-adv
+    state: present
+
+
+# install prometheus-fastd-exporter
+- name: install prometheus-fastd-exporter
+  tags: [prometheus-fastd-exporter, fastd]
+  block:
+  - name: download prometheus-fastd-exporter
+    get_url:
+      url: https://freifunk-luebeck.pages.chaotikum.org/prometheus-fastd-exporter/prometheus-fastd-exporter.deb
+      dest: /tmp/prometheus-fastd-exporter.deb
+  - name: install prometheus-fastd-exporter
+    command: dpkg -i --force-confold /tmp/prometheus-fastd-exporter.deb
+  - name: reload systemd
+    systemd:
+      daemon_reload: yes
+  - name: enable prometheus-fastd-exporter
+    systemd:
+      state: restarted
+      enabled: yes
+      name: prometheus-fastd-exporter
diff --git a/roles/meshvpn/tasks/dhcpd.yml b/roles/meshvpn/tasks/dhcpd.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2f47206d5734bfce96030abd6edc72a36d76830c
--- /dev/null
+++ b/roles/meshvpn/tasks/dhcpd.yml
@@ -0,0 +1,17 @@
+---
+
+- name: process dhcpd templates
+  template:
+    src: dhcpd/dhcpd.conf.j2
+    dest: /etc/dhcp/dhcpd.conf
+
+- name: tell dhcpd what interfaces it should listen
+  lineinfile:
+    path: /etc/default/isc-dhcp-server
+    regexp: '^INTERFACESv4='
+    line: INTERFACESv4="ffhl"
+
+- name: restart dhcpd
+  systemd:
+    state: restarted
+    name: isc-dhcp-server.service
diff --git a/roles/meshvpn/tasks/main.yml b/roles/meshvpn/tasks/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2b92f3ff988398d05417114c14831e2bcffc1d71
--- /dev/null
+++ b/roles/meshvpn/tasks/main.yml
@@ -0,0 +1,19 @@
+---
+
+- name: preapare base
+  import_tasks: base.yml
+  tags: [software, base, apt]
+
+- name: copy network configs
+  tags: [networl]
+  import_tasks: network.yml
+
+- name: mesh-vpn
+  tags: [fastd]
+  import_tasks: meshvpn.yml
+
+- import_tasks: radvd.yml
+  tags: [radvd]
+
+- import_tasks: dhcpd.yml
+  tags: [dhcp]
diff --git a/roles/meshvpn/tasks/meshvpn.yml b/roles/meshvpn/tasks/meshvpn.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9681bc780ce3ae651ca34cd897d8be7b26c1ce7c
--- /dev/null
+++ b/roles/meshvpn/tasks/meshvpn.yml
@@ -0,0 +1,77 @@
+---
+- name: create fastd user
+  user:
+    name: fastd
+    system: yes
+    home: /etc/fastd
+
+- name: create fastd config dirs
+  loop: "{{ mesh_vpn_instances }}"
+  file:
+    path: /etc/fastd/{{ item.name }}
+    state: directory
+
+- name: copy fastd config templates
+  loop: "{{ mesh_vpn_instances }}"
+  template:
+    src: mesh-vpn/fastd.conf
+    dest: /etc/fastd/{{ item.name }}/fastd.conf
+    mode: '0640'
+
+
+- name: create fastd-up script
+  loop: "{{ mesh_vpn_instances }}"
+  template:
+    src: mesh-vpn/fastd-up
+    dest: /etc/fastd/{{ item.name }}/fastd-up
+    mode: 0755
+
+
+# configure peers git
+# add update script
+- name: copy update script
+  template:
+    src: mesh-vpn/update-meshvpn-keys.sh
+    dest: /usr/local/lib/ffhl/
+    mode: 0775
+
+- name: install mesh-vpn peer update service
+  copy:
+    src: systemd/
+    dest: /etc/systemd/system/
+    owner: root
+
+
+- name: run meshvpn-keys update script
+  command: /usr/local/lib/ffhl/update-meshvpn-keys.sh
+
+
+- name: enable meshvpn peer update job
+  systemd:
+    daemon_reload: yes
+    enabled: yes
+    state: started
+    name: update-ffhl-mesh-vpn.timer
+
+
+# enable fastd instances
+- name: enable fastd instances
+  loop: "{{ mesh_vpn_instances }}"
+  systemd:
+    enabled: yes
+    daemon-reload: yes
+    state: restarted
+    name: fastd@{{ item.name }}
+
+
+# download public keys to our local machine
+- name: create public key files
+  loop: "{{ mesh_vpn_instances }}"
+  shell:
+    cmd: fastd --show-key -c /etc/fastd/{{ item.name }}/fastd.conf > /etc/fastd/{{ item.name }}/pubkey.key
+
+- name: fetch public keys
+  loop: "{{ mesh_vpn_instances }}"
+  fetch:
+    src: /etc/fastd/{{ item.name }}/pubkey.key
+    dest: artifacts/
diff --git a/roles/meshvpn/tasks/network.yml b/roles/meshvpn/tasks/network.yml
new file mode 100644
index 0000000000000000000000000000000000000000..449ac944e496079c142f7ae591f485976c68a09a
--- /dev/null
+++ b/roles/meshvpn/tasks/network.yml
@@ -0,0 +1,6 @@
+---
+
+- name: copy static network configs
+  copy:
+    src: network/
+    dest: /etc/systemd/network/
diff --git a/roles/meshvpn/tasks/radvd.yml b/roles/meshvpn/tasks/radvd.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0a0d8edad0eeb0840e2e88a9f3af0cb201b2ed63
--- /dev/null
+++ b/roles/meshvpn/tasks/radvd.yml
@@ -0,0 +1,11 @@
+---
+- name: radvd templates
+  template:
+    src: radvd/radvd.conf.j2
+    dest: /etc/radvd.conf
+
+- name: restart radvd
+  systemd:
+    state: restarted
+    enabled: yes
+    name: radvd
diff --git a/roles/meshvpn/templates/dhcpd/dhcpd.conf.j2 b/roles/meshvpn/templates/dhcpd/dhcpd.conf.j2
new file mode 100644
index 0000000000000000000000000000000000000000..8b2c4d2d37d28c8294fd9b51e8e497f6cae45b03
--- /dev/null
+++ b/roles/meshvpn/templates/dhcpd/dhcpd.conf.j2
@@ -0,0 +1,12 @@
+authoritative;
+default-lease-time 600;
+max-lease-time 600;
+
+subnet {{ dhcpd_subnet }} netmask {{ dhcpd_netmask }} {
+    range {{ dhcpd_start }} {{ dhcpd_end }};
+
+    option subnet-mask 255.255.0.0;
+    option domain-name "{{ dhcpd_domain }}";
+    option routers {{ ip4 }};
+    option domain-name-servers {{ ip4 }};
+}
diff --git a/roles/meshvpn/templates/mesh-vpn/fastd-up b/roles/meshvpn/templates/mesh-vpn/fastd-up
new file mode 100644
index 0000000000000000000000000000000000000000..9e1cd336e3dec2699cda28452af15e85347cfe04
--- /dev/null
+++ b/roles/meshvpn/templates/mesh-vpn/fastd-up
@@ -0,0 +1,13 @@
+#!/bin/bash
+set -e
+
+ip link set address {{ item.mac }} dev $INTERFACE
+ip link set up $INTERFACE
+# for some reason it seems that adding the interface too fast
+# to bat0 will cause batman_adv to remove them because they are 'deactivated'
+sleep 5
+echo "Adding interface $INTERFACE to bat0"
+ip link add bat0 type batadv || true
+batctl if add $INTERFACE
+batctl gw_mode server 50000/25000
+batctl network_coding disable
diff --git a/roles/meshvpn/templates/mesh-vpn/fastd.conf b/roles/meshvpn/templates/mesh-vpn/fastd.conf
new file mode 100644
index 0000000000000000000000000000000000000000..9fe293704f7b701c88a997d22538f9510e5a24da
--- /dev/null
+++ b/roles/meshvpn/templates/mesh-vpn/fastd.conf
@@ -0,0 +1,16 @@
+log to syslog level debug;
+bind any:{{ item.port }};
+mtu {{ item.mtu | default('1280')}};
+interface "{{ item.name }}";
+secret "{{ item.secret }}";
+
+user "fastd";
+method "null";
+method "salsa2012+umac";
+hide ip addresses yes;
+hide mac addresses yes;
+status socket "/run/fastd/{{ item.name }}.sock";
+on up "./fastd-up";
+
+peer limit 100;
+include peers from "/var/local/ffhl-meshvpn-peers";
diff --git a/roles/meshvpn/templates/mesh-vpn/update-meshvpn-keys.sh b/roles/meshvpn/templates/mesh-vpn/update-meshvpn-keys.sh
new file mode 100644
index 0000000000000000000000000000000000000000..ae7c5f34f3fc963ea5bc6382d275d9834c541081
--- /dev/null
+++ b/roles/meshvpn/templates/mesh-vpn/update-meshvpn-keys.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+set -e
+
+DIR=$(mktemp -d)
+DEST="/var/local/ffhl-meshvpn-peers"
+REPO="{{ fastd_keys_repo }}"
+
+mkdir -p "$DEST"
+git clone "$REPO" "$DIR"
+git --git-dir="$DIR/.git" --work-tree="$DEST" reset --hard
+
+rm -rf "$DIR"
+
+{% for instance in mesh_vpn_instances %}
+if [ systemctl is-active fastd@{{ instance.name }} ]; then
+	systemctl reload 'fastd@{{ instance.name }}.service'
+fi
+{% endfor %}
diff --git a/roles/meshvpn/templates/radvd/radvd.conf.j2 b/roles/meshvpn/templates/radvd/radvd.conf.j2
new file mode 100644
index 0000000000000000000000000000000000000000..9b67afba56cfbdccf5ed1757978ef984be145220
--- /dev/null
+++ b/roles/meshvpn/templates/radvd/radvd.conf.j2
@@ -0,0 +1,15 @@
+interface ffhl
+{
+	AdvSendAdvert on;
+	IgnoreIfMissing on;
+	MaxRtrAdvInterval 200;
+	AdvDefaultLifetime 900;
+
+{% for prefix in radvd_prefixes %}
+	prefix {{ prefix }} {
+	};
+{% endfor %}
+
+	RDNSS {{ ip6 }} {
+	};
+};