Always combine IP set data with static network definitions from NetBox

Before we relied on the combined data being present in ipsets.json
when generating a new config, but ipsets.json is only updated through
the form at /ipsets. So submitting any other form after changing
NetBox definitions might crash when trying to find an entry from
networks.json in ipsets.json.

Now we introduce a helper functon to always read both files and
combine the prefixes fron networks.json with ipsets.json. This way it
is not necessary to save a new ipsets.json before other changes.

Also don’t crash when enumerating networks for each VPN group.
This commit is contained in:
Timotej Lazar 2024-08-14 11:25:07 +02:00
parent b6c191e2ce
commit 048195c45c
5 changed files with 41 additions and 26 deletions

View file

@ -18,6 +18,7 @@ import flask.cli
import ldap3
from . import db
from . import ipsets
def init_app(app):
app.cli.add_command(generate)
@ -49,7 +50,6 @@ def save_config():
# Just load required settings here but keep the database unlocked
# while we load group memberships from LDAP.
with db.locked():
ipsets = db.read('ipsets')
settings = db.read('settings')
# Build LDAP query for users and groups.
@ -72,15 +72,15 @@ def save_config():
# Now read the settings again while keeping the database locked until
# config files are generated, and increment version before unlocking.
with db.locked():
ipsets = db.read('ipsets')
sets = ipsets.read()
wireguard = db.read('wireguard')
settings = db.read('settings')
version = settings['version'] = int(settings.get('version') or '0') + 1
# Find networks accessible to VPN users for each AD group.
vpn_groups = {e['vpn'] for e in ipsets.values() if e.get('vpn')}
vpn_groups = {e['vpn'] for e in sets.values() if e.get('vpn')}
group_networks = {
group: [name for name, data in ipsets.items() if data['vpn'] == group] for group in vpn_groups
group: [name for name, data in sets.items() if data.get('vpn') == group] for group in vpn_groups
}
# Add VPN addresses to IP sets.
@ -92,9 +92,9 @@ def save_config():
for group in user_groups.get(key.get('user', ''), ()):
key_networks |= set(group_networks.get(group, ()))
for network in key_networks:
ipsets[network]['ip'].append(f'{ip}/32')
sets[network]['ip'].append(f'{ip}/32')
if ip6 := key.get('ip6'):
ipsets[network]['ip6'].append(ip6)
sets[network]['ip6'].append(ip6)
# Create config files.
output = pathlib.Path.home() / 'config' / f'{version}'
@ -113,7 +113,7 @@ def save_config():
def make_set(ips):
# return "elements = { ip1, ip2, … }", prefixed with "# " if no ips
return f'{"" if ips else "# "}elements = {{ {", ".join(ips)} }}'
for name, data in ipsets.items():
for name, data in sets.items():
f.write(nft_set.format(name=name, ips=make_set(data.get('ip', ()))))
f.write(nft_set6.format(name=name, ips=make_set(data.get('ip6', ()))))
f.write('\n')
@ -132,14 +132,14 @@ def save_config():
# Print dynamic NAT rules.
with open(output / 'etc/nftables.d/nat.nft', 'w', encoding='utf-8') as f:
nft_nat = 'iif @inside oif @outside ip saddr @{name} snat to {nat}\n'
for name, data in ipsets.items():
for name, data in sets.items():
if nat := data.get('nat'):
f.write(nft_nat.format(name=name, nat=nat))
# Print forwarding rules.
with open(output / 'etc/nftables.d/forward.nft', 'w', encoding='utf-8') as f:
# Forwarding rules for VPN users.
if vpn_networks := sorted(name for name, data in ipsets.items() if data.get('vpn')):
if vpn_networks := sorted(name for name, data in sets.items() if data.get('vpn')):
nft_forward = 'iif @inside oif @inside ip saddr @{name} ip daddr @{name} accept\n'
nft_forward6 = 'iif @inside oif @inside ip6 saddr @{name}/6 ip6 daddr @{name}/6 accept\n'
f.write('# forward from the VPN interface to physical networks and back\n')