diff --git a/bundles/wireguard/files/wg.netdev b/bundles/wireguard/files/wg.netdev new file mode 100644 index 0000000..de9af7f --- /dev/null +++ b/bundles/wireguard/files/wg.netdev @@ -0,0 +1,17 @@ +[NetDev] +Name=wg${number} +Kind=wireguard +Description=WireGuard connection to ${peer} + +[WireGuard] +PrivateKey=${privatekey} +ListenPort=${port} + +[WireGuardPeer] +PublicKey=${pubkey} +AllowedIPs=0.0.0.0/0 +PresharedKey=${psk} +% if endpoint: +Endpoint=${endpoint} +% endif +PersistentKeepalive=30 diff --git a/bundles/wireguard/files/wg0.netdev b/bundles/wireguard/files/wg0.netdev deleted file mode 100644 index 8bc13a0..0000000 --- a/bundles/wireguard/files/wg0.netdev +++ /dev/null @@ -1,25 +0,0 @@ -[NetDev] -Name=wg0 -Kind=wireguard -Description=WireGuard server - -[WireGuard] -PrivateKey=${privatekey} -ListenPort=51820 - -% for peer, config in sorted(peers.items()): -# Peer ${peer} -[WireGuardPeer] -PublicKey=${config['pubkey']} -% if len(peers) == 1: # FIXME -AllowedIPs=${network} -% else: -AllowedIPs=${','.join(sorted(config['ips']))} -% endif -PresharedKey=${config['psk']} -% if 'endpoint' in config: -Endpoint=${config['endpoint']} -% endif -PersistentKeepalive=30 - -% endfor diff --git a/bundles/wireguard/items.py b/bundles/wireguard/items.py index 7f56ed9..a380da7 100644 --- a/bundles/wireguard/items.py +++ b/bundles/wireguard/items.py @@ -2,14 +2,24 @@ from ipaddress import ip_network repo.libs.tools.require_bundle(node, 'systemd-networkd') -network = ip_network(node.metadata['wireguard']['my_ip'], strict=False) - files = { - '/etc/systemd/network/wg0.netdev': { + '/usr/local/share/icinga/plugins/check_wireguard_connected': { + 'mode': '0755', + }, +} + +for number, (peer, config) in enumerate(sorted(node.metadata.get('wireguard/peers', {}).items())): + files[f'/etc/systemd/network/wg{number}.netdev'] = { 'content_type': 'mako', + 'source': 'wg.netdev', 'context': { - 'network': f'{network.network_address}/{network.prefixlen}', - **node.metadata['wireguard'], + 'endpoint': config.get('endpoint'), + 'number': number, + 'peer': peer, + 'port': config['my_port'], + 'privatekey': node.metadata.get('wireguard/privatekey'), + 'psk': config['psk'], + 'pubkey': config['pubkey'], }, 'needs': { 'pkg_apt:wireguard', @@ -17,15 +27,4 @@ files = { 'triggers': { 'svc_systemd:systemd-networkd:restart', }, - }, - '/usr/local/share/icinga/plugins/check_wireguard_connected': { - 'mode': '0755', - }, -} - -if node.has_bundle('pppd'): - files['/etc/ppp/ip-up.d/reconnect-wireguard'] = { - 'source': 'pppd-ip-up', - 'content_type': 'mako', - 'mode': '0755', } diff --git a/bundles/wireguard/metadata.py b/bundles/wireguard/metadata.py index de360e5..885f91d 100644 --- a/bundles/wireguard/metadata.py +++ b/bundles/wireguard/metadata.py @@ -102,22 +102,56 @@ def peer_pubkeys(metadata): @metadata_reactor.provides( 'wireguard/peers', ) -def peer_ips_and_endpoints(metadata): +def peer_ips_and_ports(metadata): peers = {} + base_port = 51820 - for peer_name in metadata.get('wireguard/peers', {}): + for number, peer_name in enumerate(sorted(metadata.get('wireguard/peers', {}).keys())): try: rnode = repo.get_node(peer_name) except NoSuchNode: continue - ips = rnode.metadata.get('wireguard/subnets', set()) - ips.add(rnode.metadata.get('wireguard/my_ip').split('/')[0]) - ips = repo.libs.tools.remove_more_specific_subnets(ips) + ip_a, ip_b = repo.libs.s2s.get_subnet_for_connection(repo, *sorted({node.name, peer_name})) + + if peer_name < node.name: + my_ip = ip_a + their_ip = ip_b + else: + my_ip = ip_b + their_ip = ip_a peers[rnode.name] = { - 'endpoint': '{}:51820'.format(rnode.metadata.get('wireguard/external_hostname', rnode.hostname)), - 'ips': ips, + 'my_ip': str(my_ip), + 'my_port': base_port + number, + 'their_ip': str(their_ip) + } + + return { + 'wireguard': { + 'peers': peers, + }, + } + + +@metadata_reactor.provides( + 'wireguard/peers', +) +def peer_endpoints(metadata): + peers = {} + + for name, config in metadata.get('wireguard/peers', {}).items(): + try: + rnode = repo.get_node(name) + except NoSuchNode: + continue + + + peers[rnode.name] = { + 'endpoint': '{}:{}'.format( + rnode.metadata.get('wireguard/external_hostname', rnode.hostname), + rnode.metadata.get(f'wireguard/peers/{node.name}/my_port', 51820), + ), } return { @@ -133,12 +167,12 @@ def peer_ips_and_endpoints(metadata): def icinga2(metadata): services = {} - for peer, config in metadata.get('wireguard/peers', {}).items(): + for number, (peer, config) in enumerate(sorted(metadata.get('wireguard/peers', {}).items())): if config.get('exclude_from_monitoring', False): continue services[f'WIREGUARD CONNECTION {peer}'] = { - 'command_on_monitored_host': config['pubkey'].format_into('sudo /usr/local/share/icinga/plugins/check_wireguard_connected wg0 {}'), + 'command_on_monitored_host': config['pubkey'].format_into(f'sudo /usr/local/share/icinga/plugins/check_wireguard_connected wg{number} {{}}'), } return { @@ -154,63 +188,33 @@ def icinga2(metadata): 'firewall/port_rules', ) def firewall(metadata): - sources = set(metadata.get('wireguard/restrict-to', set())) - for peer_name in metadata.get('wireguard/peers'): + ports = {} + for name, config in metadata.get('wireguard/peers').items(): try: - rnode = repo.get_node(peer_name) + rnode = repo.get_node(name) except NoSuchNode: # roadwarrior - continue + ports['{}/udp'.format(config['my_port'])] = atomic(set(metadata.get('wireguard/restrict-to', set()))) else: - sources.add(peer_name) + ports['{}/udp'.format(config['my_port'])] = atomic({name}) return { 'firewall': { - 'port_rules': { - '51820/udp': atomic(sources), - }, + 'port_rules': ports, }, } @metadata_reactor.provides( - 'interfaces/wg0/ips', + 'interfaces', ) def interface_ips(metadata): - return { - 'interfaces': { - 'wg0': { - 'ips': { - metadata.get('wireguard/my_ip'), - }, + interfaces = {} + for number, (peer, config) in enumerate(sorted(metadata.get('wireguard/peers', {}).items())): + interfaces[f'wg{number}'] = { + 'ips': { + '{}/31'.format(config['my_ip']), }, - }, - } - - -@metadata_reactor.provides( - 'interfaces/wg0/routes', -) -def routes(metadata): - network = ip_network(metadata.get('wireguard/my_ip'), strict=False) - ips = { - f'{network.network_address}/{network.prefixlen}', - } - routes = {} - - for _, peer_config in metadata.get('wireguard/peers', {}).items(): - for ip in peer_config['ips']: - ips.add(ip) - - if '0.0.0.0/0' in ips: - ips.remove('0.0.0.0/0') - - for ip in repo.libs.tools.remove_more_specific_subnets(ips): - routes[ip] = {} - + } return { - 'interfaces': { - 'wg0': { - 'routes': routes, - }, - }, + 'interfaces': interfaces, } diff --git a/libs/s2s.py b/libs/s2s.py new file mode 100644 index 0000000..1d57128 --- /dev/null +++ b/libs/s2s.py @@ -0,0 +1,21 @@ +from ipaddress import IPv4Network + +AS_NUMBERS = { + # 4290xxxxxx + 'home': 4290000138, + 'htz-cloud': 4290000137, + 'ovh': 4290000001, +} + +def get_subnet_for_connection(repo, peer_a, peer_b): + # XXX this assumes there are never more than 128 nodes which match that expression + nodes = sorted({node.name for node in repo.nodes if node.has_bundle('wireguard')}) + + assert peer_a in nodes + assert peer_b in nodes + + pos_peer_a = nodes.index(peer_a) + pos_peer_b = nodes.index(peer_b) + + vpn_subnet = list(IPv4Network('169.254.0.0/16').subnets(new_prefix=24))[pos_peer_a] + return list(IPv4Network(vpn_subnet).subnets(new_prefix=31))[pos_peer_b]