330 lines
7.9 KiB
Python
330 lines
7.9 KiB
Python
from ipaddress import ip_network
|
|
from re import sub
|
|
|
|
from bundlewrap.exceptions import NoSuchNode
|
|
from bundlewrap.metadata import atomic
|
|
|
|
defaults = {
|
|
'apt': {
|
|
'packages': {
|
|
'wireguard': {},
|
|
},
|
|
'repos': {
|
|
'backports': {
|
|
'install_gpg_key': False, # default debian signing key
|
|
'items': {
|
|
'deb http://deb.debian.org/debian {os_release}-backports main',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
'wireguard': {
|
|
'privatekey': repo.libs.keys.gen_privkey(repo, f'{node.name} wireguard privatekey'),
|
|
},
|
|
}
|
|
|
|
if node.has_bundle('telegraf'):
|
|
defaults['telegraf'] = {
|
|
'input_plugins': {
|
|
'builtin': {
|
|
'wireguard': [{}],
|
|
},
|
|
},
|
|
'additional_capabilities': {
|
|
'CAP_NET_ADMIN',
|
|
},
|
|
}
|
|
|
|
|
|
@metadata_reactor.provides(
|
|
'wireguard/peers',
|
|
)
|
|
def peers_auto_full_mesh(metadata):
|
|
peers = {}
|
|
|
|
if node.name not in repo.libs.s2s.WG_AUTOGEN_NODES:
|
|
return {}
|
|
|
|
for rnode in repo.libs.s2s.WG_AUTOGEN_NODES:
|
|
if rnode is None or rnode == node.name:
|
|
continue
|
|
|
|
try:
|
|
rnode = repo.get_node(rnode)
|
|
except NoSuchNode:
|
|
continue
|
|
|
|
if rnode.dummy:
|
|
continue
|
|
|
|
peers[rnode.name] = {}
|
|
|
|
return {
|
|
'wireguard': {
|
|
'peers': peers,
|
|
},
|
|
}
|
|
|
|
|
|
@metadata_reactor.provides(
|
|
'wireguard/peers',
|
|
)
|
|
def peer_psks(metadata):
|
|
peers = {}
|
|
|
|
for peer_name in metadata.get('wireguard/peers', {}):
|
|
peers[peer_name] = {
|
|
'iface': sub('[^a-z0-9-_]+', '_', peer_name)[:12],
|
|
}
|
|
|
|
if node.name < peer_name:
|
|
peers[peer_name]['psk'] = repo.vault.random_bytes_as_base64_for(f'{node.name} wireguard {peer_name}')
|
|
else:
|
|
peers[peer_name]['psk'] = repo.vault.random_bytes_as_base64_for(f'{peer_name} wireguard {node.name}')
|
|
|
|
return {
|
|
'wireguard': {
|
|
'peers': peers,
|
|
},
|
|
}
|
|
|
|
|
|
@metadata_reactor.provides(
|
|
'wireguard/peers',
|
|
)
|
|
def peer_pubkeys(metadata):
|
|
peers = {}
|
|
|
|
for peer_name in metadata.get('wireguard/peers', {}):
|
|
try:
|
|
rnode = repo.get_node(peer_name)
|
|
except NoSuchNode:
|
|
continue
|
|
|
|
peers[peer_name] = {
|
|
'pubkey': repo.libs.keys.get_pubkey_from_privkey(
|
|
repo,
|
|
f'{rnode.name} wireguard pubkey',
|
|
rnode.metadata.get('wireguard/privatekey'),
|
|
),
|
|
}
|
|
|
|
return {
|
|
'wireguard': {
|
|
'peers': peers,
|
|
},
|
|
}
|
|
|
|
|
|
@metadata_reactor.provides(
|
|
'wireguard/peers',
|
|
)
|
|
def peer_ips_and_ports(metadata):
|
|
if node.name not in repo.libs.s2s.WG_AUTOGEN_NODES:
|
|
raise DoNotRunAgain
|
|
|
|
peers = {}
|
|
base_port = 51820
|
|
|
|
for number, peer_name in enumerate(sorted(metadata.get('wireguard/peers', {}).keys())):
|
|
try:
|
|
rnode = repo.get_node(peer_name)
|
|
except NoSuchNode:
|
|
continue
|
|
|
|
if rnode.name not in repo.libs.s2s.WG_AUTOGEN_NODES:
|
|
continue
|
|
|
|
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] = {
|
|
'my_ip': str(my_ip),
|
|
'my_port': base_port + repo.libs.s2s.WG_AUTOGEN_NODES.index(rnode.name),
|
|
'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 {
|
|
'wireguard': {
|
|
'peers': peers,
|
|
},
|
|
}
|
|
|
|
|
|
@metadata_reactor.provides(
|
|
'icinga2_api/wireguard/services',
|
|
)
|
|
def icinga2(metadata):
|
|
services = {}
|
|
|
|
for peer, config in 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(f'sudo /usr/local/share/icinga/plugins/check_wireguard_connected wg_{config["iface"]} {{}}'),
|
|
}
|
|
|
|
return {
|
|
'icinga2_api': {
|
|
'wireguard': {
|
|
'services': services,
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
@metadata_reactor.provides(
|
|
'firewall/port_rules',
|
|
)
|
|
def firewall(metadata):
|
|
ports = {}
|
|
for name, config in metadata.get('wireguard/peers').items():
|
|
try:
|
|
rnode = repo.get_node(name)
|
|
except NoSuchNode: # roadwarrior
|
|
ports['{}/udp'.format(config['my_port'])] = atomic(set(metadata.get('wireguard/restrict-to', set())))
|
|
else:
|
|
ports['{}/udp'.format(config['my_port'])] = atomic({name})
|
|
|
|
return {
|
|
'firewall': {
|
|
'port_rules': ports,
|
|
},
|
|
}
|
|
|
|
|
|
@metadata_reactor.provides(
|
|
'interfaces',
|
|
)
|
|
def interface_ips(metadata):
|
|
interfaces = {}
|
|
snat_ip = metadata.get('wireguard/snat_ip', None)
|
|
|
|
for peer, config in sorted(metadata.get('wireguard/peers', {}).items()):
|
|
routes = {}
|
|
|
|
if '/' in config['my_ip']:
|
|
my_ip = config['my_ip']
|
|
else:
|
|
my_ip = '{}/31'.format(config['my_ip'])
|
|
|
|
ips = {my_ip}
|
|
if snat_ip:
|
|
ips.add(snat_ip)
|
|
|
|
their_ip = config['their_ip']
|
|
if '/' in their_ip:
|
|
their_ip = their_ip.split('/')[0]
|
|
|
|
for route in config.get('routes', set()):
|
|
routes[route] = {'via': their_ip}
|
|
|
|
interfaces[f'wg_{config["iface"]}'] = {
|
|
'activation_policy': 'up' if config.get('auto_connection', True) else 'manual',
|
|
'ips': ips,
|
|
'routes': routes,
|
|
}
|
|
return {
|
|
'interfaces': interfaces,
|
|
}
|
|
|
|
|
|
@metadata_reactor.provides(
|
|
'nftables/rules/10-wireguard',
|
|
)
|
|
def snat(metadata):
|
|
if not node.has_bundle('nftables') or node.os == 'arch':
|
|
raise DoNotRunAgain
|
|
|
|
snat_ip = metadata.get('wireguard/snat_ip', None)
|
|
|
|
rules = set()
|
|
for peer, config in sorted(metadata.get('wireguard/peers', {}).items()):
|
|
rules.add(f'inet filter forward iifname wg_{config["iface"]} accept')
|
|
rules.add(f'inet filter forward oifname wg_{config["iface"]} accept')
|
|
|
|
if snat_ip:
|
|
rules.add('nat postrouting ip saddr {} ip daddr != {} snat to {}'.format(
|
|
config['my_ip'],
|
|
config['their_ip'],
|
|
snat_ip,
|
|
))
|
|
|
|
return {
|
|
'nftables': {
|
|
'rules': {
|
|
'10-wireguard': sorted(rules),
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
@metadata_reactor.provides(
|
|
'wireguard/health_checks',
|
|
'systemd-timers/timers/wg-health-check',
|
|
)
|
|
def health_checks(metadata):
|
|
checks = {}
|
|
|
|
for peer, config in metadata.get('wireguard/peers', {}).items():
|
|
if (
|
|
config.get('exclude_from_monitoring', False)
|
|
or not config.get('auto_connection', True)
|
|
or 'endpoint' not in config
|
|
):
|
|
continue
|
|
|
|
checks[peer] = config['their_ip']
|
|
|
|
if checks:
|
|
timer = {
|
|
'wg-health-check': {
|
|
'command': '/usr/local/bin/wg_health_check',
|
|
'when': 'minutely',
|
|
},
|
|
}
|
|
else:
|
|
timer = {}
|
|
|
|
return {
|
|
'systemd-timers': {
|
|
'timers': timer,
|
|
},
|
|
'wireguard': {
|
|
'health_checks': checks,
|
|
},
|
|
}
|