Skip to content
Snippets Groups Projects
Unverified Commit 99148c81 authored by Nico's avatar Nico
Browse files

add intermediate version of BGP backbone

This adds (incomplete) support for configuring the BGP backbone that
could until now only be deployed locally using shell scripts.

The idea is to share the CSV-based config files between the shell
scripts and the new Ansible-based approach to allow peaceful coexistence
of both deployment variants. Note that the Ansible-based approach
verifies input data much stricter than the shell scripts to prevent
common user errors, so not everything will work.

For Ansible to find the config CSVs, a symlink "backbone-conf" to the
conf directory of the backbone repo[1] is expected in the ansible root
directory.

Currently, the ansible-based approach can only deploy Wireguard tunnels,
but will be extended to also configure Bird2 sessions.

[1] https://github.com/freifunk-stuttgart/backbone
parent aea863aa
Branches
No related tags found
No related merge requests found
...@@ -2,3 +2,5 @@ ...@@ -2,3 +2,5 @@
.history .history
ansible.out ansible.out
**.swp **.swp
**/__pycache__/
**.pyc
---
- hosts: backbone
roles:
- backbone
...@@ -3,4 +3,7 @@ backbone: ...@@ -3,4 +3,7 @@ backbone:
hosts: hosts:
nrb-backbonetest.freifunk-stuttgart.de: nrb-backbonetest.freifunk-stuttgart.de:
ansible_ssh_host: ffs-backbonetest 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: gw09n03.gw.freifunk-stuttgart.de:
---
- 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
---
- name: Deploy wireguard tunnel
include_tasks: wireguard_tunnel.yml
loop: "{{ local_line.tunnels }}"
loop_control:
loop_var: tunnel
---
- 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: ""
[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 }}
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()}}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment