bundles/wireguard: use one wireguard connection per peer instead of one for all

This commit is contained in:
Franzi 2021-09-29 19:27:13 +02:00
parent 8110ec508e
commit 902840ee7f
Signed by: kunsi
GPG key ID: 12E3D2136B818350
5 changed files with 110 additions and 94 deletions

View file

@ -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

View file

@ -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

View file

@ -2,14 +2,24 @@ from ipaddress import ip_network
repo.libs.tools.require_bundle(node, 'systemd-networkd') repo.libs.tools.require_bundle(node, 'systemd-networkd')
network = ip_network(node.metadata['wireguard']['my_ip'], strict=False)
files = { 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', 'content_type': 'mako',
'source': 'wg.netdev',
'context': { 'context': {
'network': f'{network.network_address}/{network.prefixlen}', 'endpoint': config.get('endpoint'),
**node.metadata['wireguard'], 'number': number,
'peer': peer,
'port': config['my_port'],
'privatekey': node.metadata.get('wireguard/privatekey'),
'psk': config['psk'],
'pubkey': config['pubkey'],
}, },
'needs': { 'needs': {
'pkg_apt:wireguard', 'pkg_apt:wireguard',
@ -17,15 +27,4 @@ files = {
'triggers': { 'triggers': {
'svc_systemd:systemd-networkd:restart', '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',
} }

View file

@ -102,22 +102,56 @@ def peer_pubkeys(metadata):
@metadata_reactor.provides( @metadata_reactor.provides(
'wireguard/peers', 'wireguard/peers',
) )
def peer_ips_and_endpoints(metadata): def peer_ips_and_ports(metadata):
peers = {} 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: try:
rnode = repo.get_node(peer_name) rnode = repo.get_node(peer_name)
except NoSuchNode: except NoSuchNode:
continue continue
ips = rnode.metadata.get('wireguard/subnets', set()) ip_a, ip_b = repo.libs.s2s.get_subnet_for_connection(repo, *sorted({node.name, peer_name}))
ips.add(rnode.metadata.get('wireguard/my_ip').split('/')[0])
ips = repo.libs.tools.remove_more_specific_subnets(ips) 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] = { peers[rnode.name] = {
'endpoint': '{}:51820'.format(rnode.metadata.get('wireguard/external_hostname', rnode.hostname)), 'my_ip': str(my_ip),
'ips': ips, '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 { return {
@ -133,12 +167,12 @@ def peer_ips_and_endpoints(metadata):
def icinga2(metadata): def icinga2(metadata):
services = {} 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): if config.get('exclude_from_monitoring', False):
continue continue
services[f'WIREGUARD CONNECTION {peer}'] = { 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 { return {
@ -154,63 +188,33 @@ def icinga2(metadata):
'firewall/port_rules', 'firewall/port_rules',
) )
def firewall(metadata): def firewall(metadata):
sources = set(metadata.get('wireguard/restrict-to', set())) ports = {}
for peer_name in metadata.get('wireguard/peers'): for name, config in metadata.get('wireguard/peers').items():
try: try:
rnode = repo.get_node(peer_name) rnode = repo.get_node(name)
except NoSuchNode: # roadwarrior except NoSuchNode: # roadwarrior
continue ports['{}/udp'.format(config['my_port'])] = atomic(set(metadata.get('wireguard/restrict-to', set())))
else: else:
sources.add(peer_name) ports['{}/udp'.format(config['my_port'])] = atomic({name})
return { return {
'firewall': { 'firewall': {
'port_rules': { 'port_rules': ports,
'51820/udp': atomic(sources),
},
}, },
} }
@metadata_reactor.provides( @metadata_reactor.provides(
'interfaces/wg0/ips', 'interfaces',
) )
def interface_ips(metadata): def interface_ips(metadata):
return { interfaces = {}
'interfaces': { for number, (peer, config) in enumerate(sorted(metadata.get('wireguard/peers', {}).items())):
'wg0': { interfaces[f'wg{number}'] = {
'ips': { 'ips': {
metadata.get('wireguard/my_ip'), '{}/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 { return {
'interfaces': { 'interfaces': interfaces,
'wg0': {
'routes': routes,
},
},
} }

21
libs/s2s.py Normal file
View file

@ -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]