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()}}