117 lines
4.9 KiB
Python
Executable File
117 lines
4.9 KiB
Python
Executable File
#!/usr/bin/python3
|
|
"""Write a candidate vpn-router.json to the path given on the command line.
|
|
|
|
Called from postinst, which hands the result to ucf(1). ucf compares it with
|
|
/etc/vpn-router/vpn-router.json and decides what to do about local changes, so
|
|
this script never writes to /etc and never has to worry about destroying an
|
|
administrator's edits.
|
|
|
|
The candidate is the current /etc/vpn-router/vpn-router.json as it is, or the
|
|
shipped defaults when there is none, with every debconf answer that was given
|
|
written over it. Without an answer that changes it, the candidate is the current
|
|
file byte for byte, so
|
|
an upgrade or a reconfiguration that changes nothing gives ucf nothing to ask;
|
|
defaults the file leaves out are applied by vpnrouter.load_config() when the
|
|
file is read. An empty answer means "leave alone", which is why clearing the
|
|
pre-shared key from the debconf database after installation does not blank it
|
|
on the next dpkg-reconfigure.
|
|
|
|
Answers arrive as VPN_ROUTER_* environment variables and are converted to the
|
|
type of the setting they fill: comma-separated lists become arrays, "true" and
|
|
"false" become booleans and digits become integers. The site-to-site answers
|
|
fill the connection vpnrouter.FIRST_CONNECTION of site_to_site; the local
|
|
subnets fill both its local_cidrs and local.cidrs.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
|
|
|
|
import vpnrouter
|
|
|
|
FIRST = ('site_to_site', vpnrouter.FIRST_CONNECTION)
|
|
|
|
# environment variable -> the settings it fills, each (section, option) or (site_to_site, name, option)
|
|
ANSWERS = {
|
|
'VPN_ROUTER_PLATFORM': (('general', 'platform'),),
|
|
'VPN_ROUTER_MODE': (('general', 'mode'),),
|
|
'VPN_ROUTER_INT_ADDR': (('local', 'int_addr'),),
|
|
'VPN_ROUTER_EXTERNAL_INTERFACE': (('interfaces', 'external'),),
|
|
'VPN_ROUTER_INTERNAL_INTERFACE': (('interfaces', 'internal'),),
|
|
'VPN_ROUTER_LOCAL_FQDN': (('wan', 'local_fqdn'),),
|
|
'VPN_ROUTER_LOCAL_ID_MODE': ((*FIRST, 'local_id_mode'),),
|
|
'VPN_ROUTER_LOCAL_CIDRS': (('local', 'cidrs'), (*FIRST, 'local_cidrs')),
|
|
'VPN_ROUTER_INT_GATEWAY_IP': (('local', 'int_gateway_ip'),),
|
|
'VPN_ROUTER_REMOTE_ADDRS': ((*FIRST, 'addrs'),),
|
|
'VPN_ROUTER_REMOTE_ID': ((*FIRST, 'id'),),
|
|
'VPN_ROUTER_REMOTE_CIDRS': ((*FIRST, 'cidrs'),),
|
|
'VPN_ROUTER_PSK': ((*FIRST, 'psk'),),
|
|
'VPN_ROUTER_P2S_ENABLED': (('p2s', 'enabled'),),
|
|
'VPN_ROUTER_P2S_ADDRESS_POOL': (('p2s', 'address_pool'),),
|
|
'VPN_ROUTER_PKI_ROOT_NAME': (('pki', 'root_name'),),
|
|
'VPN_ROUTER_WG_ENABLED': (('wireguard', 'enabled'),),
|
|
'VPN_ROUTER_WG_ADDRESS': (('wireguard', 'address'),),
|
|
'VPN_ROUTER_WG_LISTEN_PORT': (('wireguard', 'listen_port'),),
|
|
}
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) != 2:
|
|
print('usage: generate-config OUTPUT', file=sys.stderr)
|
|
return 2
|
|
output = sys.argv[1]
|
|
|
|
try:
|
|
current = vpnrouter.CONFIG_FILE.read_text(encoding='utf-8')
|
|
except FileNotFoundError:
|
|
current = None
|
|
if current is None:
|
|
cfg = vpnrouter.load_config()
|
|
else:
|
|
try:
|
|
cfg = json.loads(current)
|
|
except ValueError as exc:
|
|
vpnrouter.fail(f'cannot parse {vpnrouter.CONFIG_FILE}: {exc}')
|
|
if not isinstance(cfg, dict):
|
|
vpnrouter.fail(f'cannot parse {vpnrouter.CONFIG_FILE}: the top level is not an object')
|
|
|
|
answered = False
|
|
for variable, paths in ANSWERS.items():
|
|
answer = os.environ.get(variable)
|
|
if not answer:
|
|
continue
|
|
for path in paths:
|
|
# A value that does not convert is kept as a string for validate() to report.
|
|
default = vpnrouter.CONNECTION[path[2]] if path[0] == 'site_to_site' else vpnrouter.SCHEMA[path[0]][path[1]]
|
|
value = answer
|
|
if isinstance(default, bool):
|
|
value = value == 'true'
|
|
elif isinstance(default, int):
|
|
value = int(value) if value.isdigit() else value
|
|
elif isinstance(default, list):
|
|
value = [item.strip() for item in value.split(',') if item.strip()]
|
|
target = cfg
|
|
for key in path[:-1]:
|
|
if not isinstance(target.setdefault(key, {}), dict):
|
|
vpnrouter.fail(f'cannot parse {vpnrouter.CONFIG_FILE}: {".".join(path[:-1])} is not an object')
|
|
target = target[key]
|
|
if target.get(path[-1]) != value:
|
|
target[path[-1]] = value
|
|
answered = True
|
|
|
|
# Without a change the candidate is the current file byte for byte, as ucf compares checksums.
|
|
with open(output, 'w', encoding='utf-8') as handle:
|
|
if current is not None and not answered:
|
|
handle.write(current)
|
|
else:
|
|
json.dump(cfg, handle, indent=2)
|
|
handle.write('\n')
|
|
os.chmod(output, 0o600)
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|