wip
This commit is contained in:
parent
c51d226f89
commit
49d2572998
12 changed files with 550 additions and 4 deletions
3
bundles/systemd-networkd/files/resolv.conf
Normal file
3
bundles/systemd-networkd/files/resolv.conf
Normal file
|
@ -0,0 +1,3 @@
|
|||
% for nameserver in sorted(node.metadata.get('nameservers', {'9.9.9.10', '2620:fe::10'})):
|
||||
nameserver ${nameserver}
|
||||
% endfor
|
50
bundles/systemd-networkd/files/template-iface-nodhcp.network
Normal file
50
bundles/systemd-networkd/files/template-iface-nodhcp.network
Normal file
|
@ -0,0 +1,50 @@
|
|||
<%
|
||||
from ipaddress import ip_network
|
||||
%>\
|
||||
[Match]
|
||||
Name=${interface}
|
||||
|
||||
% for addr in sorted(config.get('ips', set())):
|
||||
[Address]
|
||||
<%
|
||||
if '/' in addr:
|
||||
ip, prefix = addr.split('/')
|
||||
else:
|
||||
ip = addr
|
||||
prefix = '32'
|
||||
%>\
|
||||
Address=${ip}/${prefix}
|
||||
|
||||
% endfor
|
||||
% for route, rconfig in sorted(config.get('routes', {}).items()):
|
||||
[Route]
|
||||
% if 'via' in rconfig:
|
||||
Gateway=${rconfig['via']}
|
||||
% endif
|
||||
Destination=${route}
|
||||
GatewayOnlink=yes
|
||||
|
||||
% endfor
|
||||
% if 'gateway4' in config:
|
||||
[Route]
|
||||
Gateway=${config['gateway4']}
|
||||
GatewayOnlink=yes
|
||||
|
||||
% endif
|
||||
% if 'gateway6' in config:
|
||||
[Route]
|
||||
Gateway=${config['gateway6']}
|
||||
GatewayOnlink=yes
|
||||
|
||||
% endif
|
||||
[Network]
|
||||
DHCP=no
|
||||
IPv6AcceptRA=no
|
||||
|
||||
% if config.get('forwarding', False):
|
||||
IPForward=yes
|
||||
%endif
|
||||
|
||||
% for vlan in sorted(config.get('vlans', set())):
|
||||
VLAN=${interface}.${vlan}
|
||||
% endfor
|
142
bundles/systemd-networkd/items.py
Normal file
142
bundles/systemd-networkd/items.py
Normal file
|
@ -0,0 +1,142 @@
|
|||
assert node.has_bundle('systemd')
|
||||
|
||||
from bundlewrap.exceptions import BundleError
|
||||
|
||||
|
||||
files = {
|
||||
'/etc/network/interfaces': {
|
||||
'delete': True,
|
||||
},
|
||||
}
|
||||
|
||||
if node.metadata.get('systemd-networkd/enable-resolved', False):
|
||||
symlinks['/etc/resolv.conf'] = {
|
||||
'target': '/run/systemd/resolve/stub-resolv.conf',
|
||||
}
|
||||
svc_systemd['systemd-resolved'] = {}
|
||||
else:
|
||||
files['/etc/resolv.conf'] = {
|
||||
'content_type': 'mako',
|
||||
}
|
||||
|
||||
|
||||
directories = {
|
||||
'/etc/systemd/network': {
|
||||
'purge': True,
|
||||
},
|
||||
}
|
||||
|
||||
mac_host_prefix = '%04x' % (node.magic_number % 65534)
|
||||
generated_mac = f'52:54:00:{mac_host_prefix[0:2]}:{mac_host_prefix[2:4]}:{{}}'
|
||||
|
||||
# Don't use .get() here. We might end up with a node without a network
|
||||
# config!
|
||||
for interface, config in node.metadata['interfaces'].items():
|
||||
if config.get('dhcp', False):
|
||||
if 'vlans' in config:
|
||||
raise BundleError(f'{node.name} interface {interface} cannot use vlans and dhcp!')
|
||||
template = 'template-iface-dhcp.network'
|
||||
else:
|
||||
template = 'template-iface-nodhcp.network'
|
||||
|
||||
if '.' in interface:
|
||||
vlan_id = int(interface.split('.')[1])
|
||||
vlan_hex = '%02x' % (vlan_id % 255)
|
||||
files['/etc/systemd/network/60-iface-{}.netdev'.format(interface)] = {
|
||||
'source': 'template-iface-vlan.netdev',
|
||||
'content_type': 'mako',
|
||||
'context': {
|
||||
'interface': interface,
|
||||
'vlan': vlan_id,
|
||||
'mac': generated_mac.format(vlan_hex)
|
||||
},
|
||||
'needed_by': {
|
||||
'svc_systemd:systemd-networkd',
|
||||
},
|
||||
'triggers': {
|
||||
'svc_systemd:systemd-networkd:restart',
|
||||
},
|
||||
}
|
||||
weight = 61
|
||||
else:
|
||||
weight = 50
|
||||
|
||||
if not config.get('ignore', False):
|
||||
files['/etc/systemd/network/{}-iface-{}.network'.format(weight, interface)] = {
|
||||
'source': template,
|
||||
'content_type': 'mako',
|
||||
'context': {
|
||||
'interface': interface,
|
||||
'config': config,
|
||||
},
|
||||
'needed_by': {
|
||||
'svc_systemd:systemd-networkd',
|
||||
},
|
||||
'triggers': {
|
||||
'svc_systemd:systemd-networkd:restart',
|
||||
},
|
||||
}
|
||||
|
||||
for bond, config in node.metadata.get('systemd-networkd/bonds', {}).items():
|
||||
files['/etc/systemd/network/20-bond-{}.netdev'.format(bond)] = {
|
||||
'source': 'template-bond.netdev',
|
||||
'content_type': 'mako',
|
||||
'context': {
|
||||
'bond': bond,
|
||||
'mode': config.get('mode', '802.3ad'),
|
||||
'prio': config.get('priority', '32768'),
|
||||
},
|
||||
'needed_by': {
|
||||
'svc_systemd:systemd-networkd',
|
||||
},
|
||||
'triggers': {
|
||||
'svc_systemd:systemd-networkd:restart',
|
||||
},
|
||||
}
|
||||
files['/etc/systemd/network/21-bond-{}.network'.format(bond)] = {
|
||||
'source': 'template-bond.network',
|
||||
'content_type': 'mako',
|
||||
'context': {
|
||||
'bond': bond,
|
||||
'match': config['match'],
|
||||
},
|
||||
'needed_by': {
|
||||
'svc_systemd:systemd-networkd',
|
||||
},
|
||||
'triggers': {
|
||||
'svc_systemd:systemd-networkd:restart',
|
||||
},
|
||||
}
|
||||
|
||||
for brname, config in node.metadata.get('systemd-networkd/bridges', {}).items():
|
||||
files['/etc/systemd/network/30-bridge-{}.netdev'.format(brname)] = {
|
||||
'source': 'template-bridge.netdev',
|
||||
'content_type': 'mako',
|
||||
'context': {
|
||||
'bridge': brname,
|
||||
},
|
||||
'needed_by': {
|
||||
'svc_systemd:systemd-networkd',
|
||||
},
|
||||
'triggers': {
|
||||
'svc_systemd:systemd-networkd:restart',
|
||||
},
|
||||
}
|
||||
files['/etc/systemd/network/31-bridge-{}.network'.format(brname)] = {
|
||||
'source': 'template-bridge.network',
|
||||
'content_type': 'mako',
|
||||
'context': {
|
||||
'bridge': brname,
|
||||
'match': config['match'],
|
||||
},
|
||||
'needed_by': {
|
||||
'svc_systemd:systemd-networkd',
|
||||
},
|
||||
'triggers': {
|
||||
'svc_systemd:systemd-networkd:restart',
|
||||
},
|
||||
}
|
||||
|
||||
svc_systemd = {
|
||||
'systemd-networkd': {},
|
||||
}
|
29
bundles/systemd-networkd/metadata.py
Normal file
29
bundles/systemd-networkd/metadata.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
defaults = {
|
||||
'apt': {
|
||||
'packages': {
|
||||
'resolvconf': {
|
||||
'installed': False,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@metadata_reactor.provides(
|
||||
'interfaces',
|
||||
)
|
||||
def add_vlan_infos_to_interface(metadata):
|
||||
interfaces = {}
|
||||
|
||||
for iface in metadata.get('interfaces', {}):
|
||||
if not '.' in iface:
|
||||
continue
|
||||
|
||||
interface,vlan = iface.split('.')
|
||||
|
||||
interfaces.setdefault(interface, {}).setdefault('vlans', set())
|
||||
interfaces[interface]['vlans'].add(vlan)
|
||||
|
||||
return {
|
||||
'interfaces': interfaces,
|
||||
}
|
25
bundles/wireguard/files/wg0.netdev
Normal file
25
bundles/wireguard/files/wg0.netdev
Normal file
|
@ -0,0 +1,25 @@
|
|||
[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
|
21
bundles/wireguard/items.py
Normal file
21
bundles/wireguard/items.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
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': {
|
||||
'content_type': 'mako',
|
||||
'context': {
|
||||
'network': f'{network.network_address}/{network.prefixlen}',
|
||||
**node.metadata['wireguard'],
|
||||
},
|
||||
'needs': {
|
||||
'pkg_apt:wireguard',
|
||||
},
|
||||
'triggers': {
|
||||
'svc_systemd:systemd-networkd:restart',
|
||||
},
|
||||
},
|
||||
}
|
140
bundles/wireguard/metadata.py
Normal file
140
bundles/wireguard/metadata.py
Normal file
|
@ -0,0 +1,140 @@
|
|||
from ipaddress import ip_network
|
||||
|
||||
from bundlewrap.exceptions import NoSuchNode
|
||||
from bundlewrap.metadata import atomic
|
||||
|
||||
|
||||
defaults = {
|
||||
'apt': {
|
||||
'packages': {
|
||||
'wireguard': {},
|
||||
},
|
||||
},
|
||||
'wireguard': {
|
||||
'privatekey': repo.libs.keys.gen_privkey(repo, f'{node.name} wireguard privatekey'),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@metadata_reactor.provides(
|
||||
'wireguard/peers',
|
||||
)
|
||||
def peer_psks(metadata):
|
||||
peers = {}
|
||||
|
||||
for peer_name in metadata.get('wireguard/peers', {}):
|
||||
peers[peer_name] = {}
|
||||
|
||||
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_endpoints(metadata):
|
||||
peers = {}
|
||||
|
||||
for peer_name in metadata.get('wireguard/peers', {}):
|
||||
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)
|
||||
|
||||
peers[rnode.name] = {
|
||||
'endpoint': '{}:51820'.format(rnode.metadata.get('wireguard/external_hostname', rnode.hostname)),
|
||||
'ips': ips,
|
||||
}
|
||||
|
||||
return {
|
||||
'wireguard': {
|
||||
'peers': peers,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@metadata_reactor.provides(
|
||||
'interfaces/wg0/ips',
|
||||
)
|
||||
def interface_ips(metadata):
|
||||
return {
|
||||
'interfaces': {
|
||||
'wg0': {
|
||||
'ips': {
|
||||
metadata.get('wireguard/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,
|
||||
},
|
||||
},
|
||||
}
|
|
@ -5,6 +5,7 @@
|
|||
'bundles': [
|
||||
'apt',
|
||||
'systemd',
|
||||
'systemd-networkd',
|
||||
],
|
||||
'os': 'debian',
|
||||
'pip_command': 'pip3',
|
||||
|
|
15
libs/keys.py
Normal file
15
libs/keys.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
import base64
|
||||
from nacl.public import PrivateKey
|
||||
from nacl.encoding import Base64Encoder
|
||||
from bundlewrap.utils import Fault
|
||||
|
||||
def gen_privkey(repo, identifier):
|
||||
return repo.vault.random_bytes_as_base64_for(identifier)
|
||||
|
||||
def get_pubkey_from_privkey(repo, identifier, privkey):
|
||||
# FIXME this assumes the privkey is always a base64 encoded string
|
||||
def derive_pubkey():
|
||||
pub_key = PrivateKey(base64.b64decode(str(privkey))).public_key
|
||||
return pub_key.encode(encoder=Base64Encoder).decode('ascii')
|
||||
|
||||
return Fault(f'pubkey from privkey {identifier}', derive_pubkey)
|
88
libs/tools.py
Normal file
88
libs/tools.py
Normal file
|
@ -0,0 +1,88 @@
|
|||
from ipaddress import ip_address, ip_network, IPv4Address, IPv4Network
|
||||
|
||||
from bundlewrap.exceptions import NoSuchGroup, NoSuchNode, BundleError
|
||||
from bundlewrap.utils.text import bold, red
|
||||
from bundlewrap.utils.ui import io
|
||||
|
||||
def resolve_identifier(repo, identifier):
|
||||
"""
|
||||
Try to resolve an identifier (group or node). Return a set of ip
|
||||
addresses valid for this identifier.
|
||||
"""
|
||||
try:
|
||||
nodes = {repo.get_node(identifier)}
|
||||
except NoSuchNode:
|
||||
try:
|
||||
nodes = repo.nodes_in_group(identifier)
|
||||
except NoSuchGroup:
|
||||
try:
|
||||
ip = ip_network(identifier)
|
||||
|
||||
if isinstance(ip, IPv4Network):
|
||||
return {'ipv4': {ip}, 'ipv6': set()}
|
||||
else:
|
||||
return {'ipv4': set(), 'ipv6': {ip}}
|
||||
except Exception as e:
|
||||
io.stderr('{x} {t} Exception while resolving "{i}": {e}'.format(
|
||||
x=red('✘'),
|
||||
t=bold('libs.tools.resolve_identifier'),
|
||||
i=identifier,
|
||||
e=str(e),
|
||||
))
|
||||
raise
|
||||
|
||||
found_ips = set()
|
||||
for node in nodes:
|
||||
for interface, config in node.metadata.get('interfaces', {}).items():
|
||||
for ip in config.get('ips', set()):
|
||||
if '/' in ip:
|
||||
found_ips.add(ip_address(ip.split('/')[0]))
|
||||
else:
|
||||
found_ips.add(ip_address(ip))
|
||||
|
||||
if node.metadata.get('external_ipv4', None):
|
||||
found_ips.add(ip_address(node.metadata.get('external_ipv4')))
|
||||
|
||||
ip_dict = {
|
||||
'ipv4': set(),
|
||||
'ipv6': set(),
|
||||
}
|
||||
|
||||
for ip in found_ips:
|
||||
if isinstance(ip, IPv4Address):
|
||||
ip_dict['ipv4'].add(ip)
|
||||
else:
|
||||
ip_dict['ipv6'].add(ip)
|
||||
|
||||
return ip_dict
|
||||
|
||||
|
||||
def remove_more_specific_subnets(input_subnets) -> list:
|
||||
final_subnets = []
|
||||
|
||||
for subnet in sorted(input_subnets):
|
||||
source = ip_network(subnet)
|
||||
|
||||
if not source in final_subnets:
|
||||
subnet_found = False
|
||||
|
||||
for dest_subnet in final_subnets:
|
||||
if source.subnet_of(dest_subnet):
|
||||
subnet_found = True
|
||||
|
||||
if not subnet_found:
|
||||
final_subnets.append(source)
|
||||
|
||||
out = []
|
||||
for net in final_subnets:
|
||||
out.append(str(net))
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def require_bundle(node, bundle, hint=''):
|
||||
# It's considered bad style to use assert statements outside of tests.
|
||||
# That's why this little helper function exists, so we have an easy
|
||||
# way of defining bundle requirements in other bundles.
|
||||
if not node.has_bundle(bundle):
|
||||
raise BundleError(f'{node.name} requires bundle {bundle}, but wasn\'t found! {hint}')
|
|
@ -1,17 +1,32 @@
|
|||
{
|
||||
'hostname': '10.0.0.2',
|
||||
'bundles': [
|
||||
'gitea',
|
||||
'postgresql',
|
||||
],
|
||||
'groups': [
|
||||
'debian-10',
|
||||
],
|
||||
'bundles': [
|
||||
'gitea',
|
||||
'postgresql',
|
||||
'wireguard',
|
||||
],
|
||||
'metadata': {
|
||||
'interfaces': {
|
||||
'enp1s0f0': {
|
||||
'ips': {
|
||||
'10.0.0.2/24',
|
||||
},
|
||||
'gateway4': '10.0.0.1',
|
||||
},
|
||||
},
|
||||
'gitea': {
|
||||
'version': '1.14.2',
|
||||
'sha256': '0d11d87ce60d5d98e22fc52f2c8c6ba2b54b14f9c26c767a46bf102c381ad128',
|
||||
'domain': 'git.sublimity.de',
|
||||
},
|
||||
'wireguard': {
|
||||
'my_ip': '172.19.136.1/22',
|
||||
'peers': {
|
||||
'htz.mails': {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -6,9 +6,20 @@
|
|||
'webserver',
|
||||
],
|
||||
'bundles': [
|
||||
'wireguard',
|
||||
'zfs',
|
||||
],
|
||||
'metadata': {
|
||||
'interfaces': {
|
||||
'eth0': {
|
||||
'ips': {
|
||||
'162.55.188.157',
|
||||
'2a01:4f8:1c1c:4121::/64',
|
||||
},
|
||||
'gateway4': '172.31.1.1',
|
||||
'gateway6': 'fe80::1',
|
||||
},
|
||||
},
|
||||
'nginx': {
|
||||
'vhosts': {
|
||||
'nextcloud': {
|
||||
|
@ -38,5 +49,11 @@
|
|||
},
|
||||
},
|
||||
},
|
||||
'wireguard': {
|
||||
'my_ip': '172.19.136.2/22',
|
||||
'peers': {
|
||||
'home.server': {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue