Rework NAT settings

Support static NAT for L2 server networks. Also some other minor
tweaks.
This commit is contained in:
Timotej Lazar 2023-04-25 17:06:31 +02:00
parent 9476a28674
commit 968a2736d2
2 changed files with 56 additions and 31 deletions

View file

@ -34,17 +34,18 @@ def save_config():
settings = db.load('settings') settings = db.load('settings')
groups = db.load('groups') groups = db.load('groups')
# Get users’ group membership from LDAP server. Only query the groups used # For each user build a list of networks they have access to, based on
# by at least one network, and query each group just once. # group membership in AD. Only query groups associated with at least one
user_groups = collections.defaultdict(set) # network, and query each group only once.
user_networks = collections.defaultdict(set)
ldap = ldap3.Connection(ldap3.Server(settings.get('ldap_host'), use_ssl=True), ldap = ldap3.Connection(ldap3.Server(settings.get('ldap_host'), use_ssl=True),
settings.get('ldap_user'), settings.get('ldap_pass'), auto_bind=True) settings.get('ldap_user'), settings.get('ldap_pass'), auto_bind=True)
for group in groups: for group, network in groups.items():
ldap.search(settings.get('ldap_base_dn', ''), ldap.search(settings.get('ldap_base_dn', ''),
f'(distinguishedName={group})', attributes='member') f'(distinguishedName={group})', attributes='member')
if ldap.entries: if ldap.entries:
for user in ldap.entries[0]['member']: for user in ldap.entries[0]['member']:
user_groups[user].add(group) user_networks[user].add(network)
# Now read the settings again and lock the database while generating # Now read the settings again and lock the database while generating
# config files, then increment version before unlocking. # config files, then increment version before unlocking.
@ -52,13 +53,26 @@ def save_config():
settings = db.read('settings') settings = db.read('settings')
version = settings['version'] = int(settings.get('version', 0)) + 1 version = settings['version'] = int(settings.get('version', 0)) + 1
# Populate IP sets. # Populate IP sets and translation maps for NAT.
wireguard = db.load('wireguard')
ipsets = collections.defaultdict(set) ipsets = collections.defaultdict(set)
networks = db.load('networks')
nat = {}
netmap = {}
for name, network in networks.items():
for ip in network.get('ip', ()):
ipsets[name].add(ip)
if 'nat' in network:
nat[ip] = network['nat']
for ip6 in network.get('ip6', ()):
ipsets[f'{name}6'].update(ip6)
netmap.update(network.get('netmap', {}))
wireguard = db.load('wireguard')
for ip, key in wireguard.items(): for ip, key in wireguard.items():
for group in user_groups.get(key.get('user', ''), ()): for network in user_networks.get(key.get('user', ''), ()):
for name in groups[group]: ipsets[network].add(f'{ip}/32')
ipsets[name].add(f'{ip}/32') if 'ip6' in key:
ipsets[f'{network}6'].add(f'{key["ip6"]}/128')
# Create config files. # Create config files.
output = pathlib.Path.home() / 'config' / f'{version}' output = pathlib.Path.home() / 'config' / f'{version}'
@ -68,44 +82,54 @@ def save_config():
# Add registered VPN addresses for each network based on # Add registered VPN addresses for each network based on
# LDAP group membership. # LDAP group membership.
with open(f'{output}/etc/nftables.d/sets-vpn.nft', 'w', encoding='utf-8') as f: with open(f'{output}/etc/nftables.d/sets.nft', 'w', encoding='utf-8') as f:
def format_set(name, ips): def format_set(name, ips):
return f'''\ return f'''\
set {name} {{ set {name} {{
typeof ip daddr; flags interval type ipv4_addr; flags interval
elements = {{ {', '.join(ips)} }} elements = {{ {', '.join(ips)} }}
}}''' }}'''
for name, ips in ipsets.items(): for name, ips in ipsets.items():
if not name.endswith('6'):
print(format_set(name, ips), file=f) print(format_set(name, ips), file=f)
# Print NAT (dynamic and 1:1) rules.
with open(f'{output}/etc/nftables.d/nat.nft', 'w', encoding='utf-8') as f:
def format_map(name, elements):
lines = ',\n'.join(f'{a}: {b}' for a, b in elements)
return f'''\
map {name} {{
type ipv4_addr : interval ipv4_addr; flags interval
elements = {{
{lines}
}}
}}
'''
if nat:
print(format_map('nat', ((private, public) for private, public in nat.items())), file=f)
if netmap:
print(format_map('netmap-out', ((private, public) for private, public in netmap.items())), file=f)
print(format_map('netmap-in', ((public, private) for private, public in netmap.items())), file=f)
# Print forwarding rules. # Print forwarding rules.
with open(f'{output}/etc/nftables.d/forward.nft', 'w', encoding='utf-8') as f: with open(f'{output}/etc/nftables.d/forward.nft', 'w', encoding='utf-8') as f:
def format_forward(src, dst): for forward in db.load('forwards'):
rule = 'iifname @ifaces_inside oifname @ifaces_inside' print(forward, file=f)
if src:
rule += f' ip saddr @{src}'
if dst:
rule += f' ip daddr @{dst}'
return rule + ' accept'
for src, dst in db.load('forwards'):
print(format_forward(src, dst), file=f)
# Print wireguard config. # Print wireguard config.
with open(f'{output}/etc/wireguard/wg.conf', 'w', encoding='utf-8') as f: with open(f'{output}/etc/wireguard/wg.conf', 'w', encoding='utf-8') as f:
def format_wg_peer(ip, data):
return f'''\
# {data.get('user')}
[Peer]
PublicKey = {data.get('key')}
AllowedIPs = {ip}
'''
print(f'''\ print(f'''\
[Interface] [Interface]
ListenPort = {settings.get('wg_port', 51820)} ListenPort = {settings.get('wg_port', 51820)}
PrivateKey = {settings.get('wg_key')} PrivateKey = {settings.get('wg_key')}
''', file=f) ''', file=f)
for ip, key in wireguard.items(): for ip, data in wireguard.items():
print(format_wg_peer(ip, key), file=f) print(f'''\
# {data.get('user')}
[Peer]
PublicKey = {data.get('key')}
AllowedIPs = {ip}
''', file=f)
# Make a config archive in a temporary place, so we don’t send # Make a config archive in a temporary place, so we don’t send
# incomplete tars. # incomplete tars.

View file

@ -4,6 +4,7 @@
<ul> <ul>
{% if current_user.is_admin %} {% if current_user.is_admin %}
<li><a href="{{ url_for('config.index') }}">Nastavitve <li><a href="{{ url_for('config.index') }}">Nastavitve
<li><a href="{{ url_for('config.edit', name='networks') }}">Omrežja
<li><a href="{{ url_for('config.edit', name='groups') }}">Skupine <li><a href="{{ url_for('config.edit', name='groups') }}">Skupine
<li><a href="{{ url_for('config.edit', name='forwards') }}">Luknje <li><a href="{{ url_for('config.edit', name='forwards') }}">Luknje
{% endif %} {% endif %}