diff --git a/.gitignore b/.gitignore
index 83931635ab9f3fb9945e04bdef2e5740559d5398..35b9f4e6c585457e08e665220bbc41118704bd4d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,5 @@
 .history
 ansible.out
 **.swp
+**/__pycache__/
+**.pyc
diff --git a/backbone.yml b/backbone.yml
new file mode 100644
index 0000000000000000000000000000000000000000..36b80a54515563c757aee359162bf3633ed56f83
--- /dev/null
+++ b/backbone.yml
@@ -0,0 +1,4 @@
+---
+- hosts: backbone
+  roles:
+    - backbone
diff --git a/inventory/backbone b/inventory/backbone
index 4a70346766f5f59364caf6f9efe1ad33b100b234..f18c0888df6020dbbd802ea0ef639e7dd673215e 100644
--- a/inventory/backbone
+++ b/inventory/backbone
@@ -3,4 +3,7 @@ backbone:
   hosts:
     nrb-backbonetest.freifunk-stuttgart.de:
       ansible_ssh_host: ffs-backbonetest
+    nrb-backbonetest2.freifunk-stuttgart.de:
+      ansible_ssh_host: 192.168.122.3
+      ansible_ssh_user: root
     gw09n03.gw.freifunk-stuttgart.de:
diff --git a/roles/backbone/tasks/main.yml b/roles/backbone/tasks/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2ffb0ba50127f8afa27106b434901251f94078bf
--- /dev/null
+++ b/roles/backbone/tasks/main.yml
@@ -0,0 +1,14 @@
+---
+- name: Install required packages
+  apt:
+    name:
+      - bird2
+      - wireguard-tools
+    state: present
+
+- name: Deploy lines
+  include_tasks: wireguard_line.yml
+  loop: "{{ bb_bgp_peers[ansible_facts['hostname']].lines }}"
+  loop_control:
+    loop_var: local_line
+  
diff --git a/roles/backbone/tasks/wireguard_line.yml b/roles/backbone/tasks/wireguard_line.yml
new file mode 100644
index 0000000000000000000000000000000000000000..06de9b19333140fb060c4a97b4f12b6258c91408
--- /dev/null
+++ b/roles/backbone/tasks/wireguard_line.yml
@@ -0,0 +1,6 @@
+---
+- name: Deploy wireguard tunnel
+  include_tasks: wireguard_tunnel.yml
+  loop: "{{ local_line.tunnels }}"
+  loop_control:
+    loop_var: tunnel
diff --git a/roles/backbone/tasks/wireguard_tunnel.yml b/roles/backbone/tasks/wireguard_tunnel.yml
new file mode 100644
index 0000000000000000000000000000000000000000..51cda9db9d82db69468d05fbf9215cc3a9ffa5bc
--- /dev/null
+++ b/roles/backbone/tasks/wireguard_tunnel.yml
@@ -0,0 +1,46 @@
+---
+- ansible.builtin.set_fact:
+    bb_bgp_remote_line_name: "{{ tunnel.line_b if bb_bgp_lines[tunnel.line_a].hostname == ansible_facts['hostname'] else tunnel.line_a }}"
+    bb_bgp_local_line_name: "{{ tunnel.line_a if bb_bgp_lines[tunnel.line_a].hostname == ansible_facts['hostname'] else tunnel.line_b }}"
+
+# this is seperate from the above because it depends on a fact set above
+- ansible.builtin.set_fact:
+    bb_bgp_remote_line: "{{ bb_bgp_lines[bb_bgp_remote_line_name] }}"
+    bb_bgp_local_line: "{{ bb_bgp_lines[bb_bgp_local_line_name] }}"
+
+# this is seperate from the above because it depends on a fact set above
+- ansible.builtin.set_fact:
+    bb_bgp_remote_host: "{{ bb_bgp_peers[bb_bgp_remote_line.hostname] }}"
+    bb_bgp_local_host: "{{ bb_bgp_peers[bb_bgp_local_line.hostname] }}"
+
+- ansible.builtin.debug:
+    msg: "{{ bb_bgp_local_line }}"
+- ansible.builtin.debug:
+    msg: "{{ bb_bgp_remote_line }}"
+
+- name: Deploy netdev file for wireguard tunnel
+  ansible.builtin.template:
+    src: wg-tunnel.netdev.j2
+    dest: "/etc/systemd/network/{{ bb_bgp_remote_line_name }}.netdev"
+    mode: 0750
+    owner: root
+    group: systemd-network
+  vars:
+    local_port: "{{ 12000 + (tunnel.idx|int - 1) * 1000 + bb_bgp_remote_host.ip4offset|int }}"
+    remote_port: "{{ 12000 + (tunnel.idx|int - 1) * 1000 + bb_bgp_local_host.ip4offset|int }}"
+
+- name: Deploy network file for wireguard tunnel
+  ansible.builtin.template:
+    src: wg-tunnel.network.j2
+    dest: "/etc/systemd/network/{{ bb_bgp_remote_line_name }}.network"
+    mode: 0750
+    owner: root
+    group: systemd-network
+
+- ansible.builtin.set_fact:
+    bb_bgp_remote_line_name: ""
+    bb_bgp_local_line_name: ""
+    bb_bgp_remote_line: ""
+    bb_bgp_local_line: ""
+    bb_bgp_remote_host: ""
+    bb_bgp_local_host: ""
diff --git a/roles/backbone/templates/wg-tunnel.netdev.j2 b/roles/backbone/templates/wg-tunnel.netdev.j2
new file mode 100644
index 0000000000000000000000000000000000000000..edcccb9e27069aa17ea56c1aea8c930aae586b12
--- /dev/null
+++ b/roles/backbone/templates/wg-tunnel.netdev.j2
@@ -0,0 +1,15 @@
+[NetDev]
+Name={{ bb_bgp_remote_line_name }}
+Kind=wireguard
+MTUBytes=1280
+
+[WireGuard]
+ListenPort={{ local_port }}
+PrivateKeyFile=/etc/wireguard/wg-private.key
+
+[WireGuardPeer]
+PublicKey={{ bb_bgp_remote_host.wg_pubkey }}
+AllowedIPs=0.0.0.0/0, ::/0
+Endpoint={{ bb_bgp_remote_line.ip }}:{{ remote_port }}
+
+
diff --git a/roles/backbone/vars_plugins/backbone_csv_vars.py b/roles/backbone/vars_plugins/backbone_csv_vars.py
new file mode 100644
index 0000000000000000000000000000000000000000..201b38b8e2bb36ee1a61dc547874338fb89429ba
--- /dev/null
+++ b/roles/backbone/vars_plugins/backbone_csv_vars.py
@@ -0,0 +1,158 @@
+import os.path
+import pathlib
+import csv
+from dataclasses import dataclass, asdict
+from ansible.plugins.vars import BaseVarsPlugin
+
+@dataclass
+class Tunnel:
+    """
+    A wireguard tunnel connects two lines with each other.
+    """
+    line_a: str
+    line_b: str
+    idx: int
+
+@dataclass
+class Line:
+    """
+    A line over which a wireguard tunnel can be established to a BgpPeer.
+    """
+    hostname: str
+    linename: str
+    ip: str
+    tunnels: list[Tunnel]
+
+@dataclass
+class BgpPeer:
+    """
+    A router speaking BGP. Consists of multile @Line.
+    """
+    hostname: str
+    asn: str
+    routerid: str
+    lines: list[Line]
+    wg_pubkey: str
+    ip4offset: int
+    ip6offset: int
+
+class VarsModule(BaseVarsPlugin):
+    def get_vars(self, loader, path, entities):
+        if not "all" in map(lambda e: e.name, entities):
+            return {}
+
+        self._path_backbone_conf = pathlib.Path(path) / "backbone-conf"
+
+        if self._path_backbone_conf.is_dir():
+            return self.parse_backbone_conf()
+        return {}
+
+    def _make_csv_reader(self, fp):
+        return csv.reader(fp, 
+                          delimiter=';', 
+                          skipinitialspace=True)
+
+    def _csv_iterator(self, fp):
+        csv = self._make_csv_reader(fp)
+        while True:
+            try:
+                line = list(map(lambda s: s.strip(), next(csv)))
+            except StopIteration:
+                return
+            if line[0].startswith("#"):
+                continue
+            yield line
+
+    def _parse_host_keyvalue(self, csvfilename, name):
+        output = {}
+        csv_path = self._path_backbone_conf / csvfilename
+        with csv_path.open() as input_fp:
+            for item in self._csv_iterator(input_fp):
+                if item[0] in output:
+                    raise ValueError(f"Duplicate {name} for {item[0]}")
+                output[item[0]] = item[1]
+
+        return output
+
+    def parse_backbone_conf(self):
+        wg_pubkeys = self._parse_host_keyvalue("wireguard-pubkeys", "wireguard pubkey")
+        ip4offsets = self._parse_host_keyvalue("ipoffsets", "ip4offset")
+        ip6offsets = self._parse_host_keyvalue("ip6offsets", "ip6offset")
+
+        bgp_peers = {}
+        bgp_lines_path = self._path_backbone_conf / "bgp_lines"
+        with bgp_lines_path.open() as bgp_lines_fp:
+            for bgp_line_csv in self._csv_iterator(bgp_lines_fp):
+                hostname = bgp_line_csv[0]
+                try:
+                    wg_pubkey = wg_pubkeys[hostname]
+                except KeyError:
+                    print(f"WARNING: Missing wireguard pubkey for {hostname}")
+                try:
+                    ip4offset = ip4offsets[hostname]
+                except KeyError:
+                    print(f"WARNING: Missing ip4offset for {hostname}")
+                try:
+                    ip6offset = ip6offsets[hostname]
+                except KeyError:
+                    print(f"WARNING: Missing ip6offset for {hostname}")
+                bgp_peer = BgpPeer(hostname=hostname,
+                                   asn=bgp_line_csv[2],
+                                   routerid=bgp_line_csv[3],
+                                   wg_pubkey=wg_pubkey,
+                                   ip4offset=ip4offset,
+                                   ip6offset=ip6offset,
+                                   lines=[])
+                if bgp_peer.hostname in bgp_peers:
+                    other_bgp_peer = bgp_peers[bgp_peer.hostname]
+                    if other_bgp_peer.asn != bgp_peer.asn:
+                        raise ValueError(f"Inconsistent ASN for {bgp_peer.hostname} in bgp_lines {other_bgp_peer.asn} != {bgp_peer.asn}")
+                    if other_bgp_peer.routerid != bgp_peer.routerid:
+                        raise ValueError(f"Inconsistent ASN for {bgp_peer.hostname} in bgp_lines {other_bgp_peer.routerid} != {bgp_peer.routerid}")
+                    continue
+                bgp_peers[bgp_peer.hostname] = bgp_peer
+
+        lines = {}
+        lines_path = self._path_backbone_conf / "lines"
+        with lines_path.open() as lines_fp:
+            for line_csv in self._csv_iterator(lines_fp):
+                line = Line(hostname=line_csv[0],
+                            linename=line_csv[1],
+                            ip=line_csv[3],
+                            tunnels=[])
+                if line.linename in lines:
+                    raise ValueError(f"Linename {line.linename} already exists")
+                try:
+                    bgp_peers[line.hostname].lines.append(line)
+                except KeyError:
+                    raise ValueError(f"Could not find hostname {line.hostname} for line {line.linename} in bgp_peers")
+                lines[line.linename] = line
+
+        tunnels_path = self._path_backbone_conf / "tunnels"
+        tunnels_next_idx = {}
+        with tunnels_path.open() as tunnels_fp:
+            for tunnel_csv in self._csv_iterator(tunnels_fp):
+                tunnel_csv = list(map(lambda s: s.strip(), tunnel_csv))
+                if tunnel_csv[0].startswith("#"):
+                    continue
+                line_a = tunnel_csv[0] if tunnel_csv[0] < tunnel_csv[1] else tunnel_csv[1]
+                line_b = tunnel_csv[1] if tunnel_csv[0] < tunnel_csv[1] else tunnel_csv[0]
+                hostname_a = lines[line_a].hostname
+                hostname_b = lines[line_b].hostname
+                tunnel_key = f"{hostname_a}-{hostname_b}"
+                try:
+                    tunnels_next_idx[tunnel_key] += 1
+                except KeyError:
+                    tunnels_next_idx[tunnel_key] = 1
+                
+                tunnel = Tunnel(line_a=line_a,
+                                line_b=line_b,
+                                idx=tunnels_next_idx[tunnel_key])
+                try:
+                    lines[line_a].tunnels.append(tunnel)
+                    lines[line_b].tunnels.append(tunnel)
+                except KeyError:
+                    raise ValueError(f"Line {linea} or {lineb} do not exist")
+
+        return {"bb_bgp_peers": {peername: asdict(peer) for peername, peer in bgp_peers.items()},
+                "bb_bgp_lines": {linename: asdict(line) for linename, line in lines.items()}}