Provide more information and automation, e.g. ProxyARP setup

This commit is contained in:
Henri 2021-01-26 22:32:29 +01:00
parent 83f0d61df9
commit f1e48ec8db
5 changed files with 304 additions and 7 deletions

View File

@ -7,7 +7,7 @@ with open('README.md', 'r') as f:
setup_kwargs = { setup_kwargs = {
'name': 'wgfrontend', 'name': 'wgfrontend',
'version': '0.3.2', 'version': '0.4.0',
'author': 'The Towalink Project', 'author': 'The Towalink Project',
'author_email': 'pypi.wgfrontend@towalink.net', 'author_email': 'pypi.wgfrontend@towalink.net',
'description': 'web-based user interface for configuring WireGuard for roadwarriors', 'description': 'web-based user interface for configuring WireGuard for roadwarriors',

View File

@ -105,6 +105,11 @@ class Configuration():
"""The filename incl. path of the config file for the WireGuard interface""" """The filename incl. path of the config file for the WireGuard interface"""
return self.config.get('wg_configfile', '/etc/wireguard/wg_rw.conf') return self.config.get('wg_configfile', '/etc/wireguard/wg_rw.conf')
@property
def wg_interface(self):
"""The name of the WireGuard interface derived from the name of the WireGuard config file"""
return os.path.basename(self.wg_configfile).rpartition('.')[0] # filename without extension
@property @property
def sslcertfile(self): def sslcertfile(self):
"""The filename incl. path for the server certificate""" """The filename incl. path for the server certificate"""

View File

@ -0,0 +1,138 @@
# -*- coding: utf-8 -*-
"""Class for executing commands on the system"""
import logging
import shlex
import subprocess
logger = logging.getLogger(__name__);
class ExecHelper(object):
"""Class for executing commands on the system"""
_os_id = None # cache for storing the detected operating system family identifier
@property
def os_id(self):
if self._os_id is None:
with open('/etc/os-release', 'r') as f:
line = True
while line:
line = f.readline()
parts = line.partition('=')
if parts[0] == 'ID':
self._os_id = parts[2].strip()
break
return self._os_id
def execute(self, command, suppressoutput=False, suppresserrors=False):
"""Execute a command"""
args = shlex.split(command)
nsp = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = nsp.communicate()
if err is not None:
err = err.decode('utf8')
if not suppresserrors and (len(err) > 0):
logger.error(err)
out = out.decode('utf8')
if not suppressoutput and (len(out) > 0):
print(out)
nsp.wait()
return out, err, nsp.returncode
def service_is_active(self, service):
"""Checks whether the given service is active on the system"""
command = f'systemctl status "{service}"'
try:
out, err, ret = self.execute(command, suppressoutput=True, suppresserrors=True)
# ret: (0: active)(3: not active)(4: service not found)
except Exception as e:
logger.error(f'Exception when checking for active service: [{e}]')
return False
return (ret==0)
def start_service(self, service):
"""Starts the given service"""
if self.os_id == 'alpine':
command = f'rc-service "{service}" start'
else:
command = f'systemctl start "{service}"'
try:
out, err, ret = self.execute(command, suppressoutput=True, suppresserrors=True)
except Exception as e:
logger.error(f'Exception when starting service: [{e}]')
def stop_service(self, service):
"""Stops the given service"""
if self.os_id == 'alpine':
command = f'rc-service "{service}" stop'
else:
command = f'systemctl stop "{service}"'
try:
out, err, ret = self.execute(command, suppressoutput=True, suppresserrors=True)
except Exception as e:
logger.error(f'Exception when stopping service: [{e}]')
def reload_service(self, service):
"""Reloads the given service"""
if self.os_id == 'alpine':
command = f'rc-service "{service}" restart'
else:
command = f'systemctl reload "{service}"'
try:
out, err, ret = self.execute(command, suppressoutput=True, suppresserrors=True)
except Exception as e:
logger.error(f'Exception when reloading service: [{e}]')
def restart_service(self, service):
"""Restarts the given service"""
if self.os_id == 'alpine':
command = f'rc-service "{service}" restart'
else:
command = f'systemctl restart "{service}"'
try:
out, err, ret = self.execute(command, suppressoutput=True, suppresserrors=True)
except Exception as e:
logger.error(f'Exception when reloading service: [{e}]')
def enable_service(self, service):
"""Enables the given service"""
if self.os_id == 'alpine':
command = f'rc-update add "{service}"'
else:
command = f'systemctl enable "{service}"'
try:
out, err, ret = self.execute(command, suppressoutput=True, suppresserrors=True)
except Exception as e:
logger.error(f'Exception when enabling service: [{e}]')
def disable_service(self, service):
"""Disable the given service"""
if self.os_id == 'alpine':
command = f'rc-update del "{service}"'
else:
command = f'systemctl disable "{service}"'
try:
out, err, ret = self.execute(command, suppressoutput=True, suppresserrors=True)
except Exception as e:
logger.error(f'Exception when disabling service: [{e}]')
def run_wgquick(self, task, interface):
"""Runs "wg-quick <task> <interface>"""
command = f'wg-quick {task} "{interface}"'
try:
out, err, ret = self.execute(command, suppressoutput=True, suppresserrors=True)
if ret > 0:
logger.error(err)
raise Exception('wg-quick returned an error')
except Exception as e:
logger.error(f'Exception when running wg-quick: [{e}]')
if __name__ == '__main__':
eh = ExecHelper()
print(eh.service_is_active('ssh'))
print(eh.service_is_active('autossh-ssh1'))
print(eh.service_is_active('salt-minion'))

View File

@ -4,13 +4,17 @@
import getpass import getpass
import grp import grp
import ipaddress
import os import os
import pwd import pwd
import string import string
import subprocess
import wgconfig import wgconfig
import wgconfig.wgexec as wgexec import wgconfig.wgexec as wgexec
from . import config from . import config
from . import exechelper
from . import setupenv_alpine
def is_root(): def is_root():
@ -83,6 +87,39 @@ def drop_privileges(uid_name='nobody', gid_name='nogroup'):
os.setgid(gid) os.setgid(gid)
os.setuid(uid) os.setuid(uid)
def get_primary_interface():
"""Returns the name of the network interface having the default route"""
exitcode, interface_name = subprocess.getstatusoutput("ip route | awk '/default/ { print $5 }'")
if exitcode == 0:
return interface_name
else:
return None
def get_primary_interface_addr4():
"""Returns the first IPv4 address of the network interface having the default route"""
interface_name = get_primary_interface()
if interface_name is None:
return None
exitcode, output = subprocess.getstatusoutput(f'ip addr show dev {interface_name}' + "| awk '/inet / { print $2 }'")
if exitcode == 0:
addr4 = output.partition('\n')[0] # get first line
return addr4
else:
return None
def input_yes_no(info, default='Yes'):
"""Queries the user for a yes or no answer"""
while True:
answer = input(info)
if not answer.strip():
answer = default
answer = answer.lower()
if answer in ['1', 'y', 'yes']:
return True
if answer in ['0', 'n', 'no']:
return False
print(' Invalid answer. Please answer yes or no.')
def setup_environment(): def setup_environment():
"""Environment setup assistant""" """Environment setup assistant"""
cfg = config.Configuration() cfg = config.Configuration()
@ -152,22 +189,46 @@ def setup_environment():
print(f'7) WireGuard config file {cfg.wg_configfile} already exists. Ok.') print(f'7) WireGuard config file {cfg.wg_configfile} already exists. Ok.')
else: else:
print(f'7) WireGuard config file {cfg.wg_configfile} does not yet exist. Let\'s create one...') print(f'7) WireGuard config file {cfg.wg_configfile} does not yet exist. Let\'s create one...')
print(' For documentation on possible setups, please refer to')
print(' https://github.com/towalink/wgfrontend/tree/main/doc/network-integration')
print(' Automated configuration is only supported for the ProxyARP setup.')
print(' For this, choose an unused subrange of your local network for WireGuard.')
wg_listenport = input(f'7a) Please specify the listen port of the WireGuard interface [51820]: ') wg_listenport = input(f'7a) Please specify the listen port of the WireGuard interface [51820]: ')
if not wg_listenport.strip(): if not wg_listenport.strip():
wg_listenport = 51820 wg_listenport = 51820
ok = False ok = False
while not ok: while not ok:
endpoint = input(f'7b) Please specify the endpoint hostname (and optionally port) to reach your WireGuard server: ') endpoint = input('7b) Please specify the endpoint hostname (and optionally port) to reach your WireGuard server: ')
if len(endpoint) > 0: if len(endpoint) > 0:
ok = True ok = True
else: else:
print(' You need to enter an endpoint hostname.') print(' You need to enter an endpoint hostname.')
wg_address = input(f'7c) Please specify the IP address of the WireGuard interface incl. prefix length [192.168.0.1/24]: ') ok = False
if not wg_address.strip(): while not ok:
wg_address = '192.168.0.1/24' wg_address = input('7c) Please specify the IP address of the WireGuard interface incl. prefix length [192.168.0.1/24]: ')
wg_networks = input(f'7d) Please specify the network ranges that the clients shall route to the WireGuard server [192.168.0.0/16]: ') if not wg_address.strip():
wg_address = '192.168.0.1/24'
try:
wg_address_obj = ipaddress.ip_interface(wg_address)
ok = True
except e:
print(' Exception: {text}'.format(text=str(e)))
wg_networks = input('7d) Please specify the network ranges that the clients shall route to the WireGuard server [192.168.0.0/16]: ')
if not wg_networks.strip(): if not wg_networks.strip():
wg_networks = '192.168.0.0/16' wg_networks = '192.168.0.0/16'
# Check for ProxyARP setup
proxy_arp_interface = None
eth_address = get_primary_interface_addr4()
if (eth_address is not None) and (len(eth_address) > 0):
eth_address_obj = ipaddress.ip_interface(eth_address)
if wg_address_obj.network.subnet_of(eth_address_obj.network):
interface_name = get_primary_interface()
print(' Setup for ProxyARP detected.')
if input_yes_no(f'7e) Do you want to configure ProxyARP on interface {interface_name} when bringing up the WireGuard interface? [Yes]: '):
proxy_arp_interface = interface_name
else:
print('7e) Please configure your network setup based on the documentation referenced above.')
# Write WireGuard config file
wc = wgconfig.WGConfig(cfg.wg_configfile) wc = wgconfig.WGConfig(cfg.wg_configfile)
wc.initialize_file('# This file has been created and is managed by wgfrontend. Only change manually if you know what you\'re doing.') wc.initialize_file('# This file has been created and is managed by wgfrontend. Only change manually if you know what you\'re doing.')
if not ':' in endpoint: if not ':' in endpoint:
@ -175,8 +236,23 @@ def setup_environment():
wc.add_attr(None, 'ListenPort', wg_listenport, '# Endpoint = ' + endpoint, append_as_line=True) wc.add_attr(None, 'ListenPort', wg_listenport, '# Endpoint = ' + endpoint, append_as_line=True)
wc.add_attr(None, 'PrivateKey', wgexec.generate_privatekey()) wc.add_attr(None, 'PrivateKey', wgexec.generate_privatekey())
wc.add_attr(None, 'Address', wg_address, '# Networks = ' + wg_networks, append_as_line=True) wc.add_attr(None, 'Address', wg_address, '# Networks = ' + wg_networks, append_as_line=True)
if proxy_arp_interface is not None:
wc.add_attr(None, 'PostUp', f'sysctl -w net.ipv4.conf.{proxy_arp_interface}.proxy_arp=1', append_as_line=True)
wc.write_file() wc.write_file()
print(' Config file written. Ok.') print(' Config file written. Ok.')
eh = exechelper.ExecHelper()
if input_yes_no(f'7f) Would you like to activate the WireGuard interface "{cfg.wg_interface}" now? [Yes]: '):
eh.run_wgquick('up', cfg.wg_interface)
if input_yes_no(f'7g) Would you like to activate the WireGuard interface "{cfg.wg_interface}" on boot? [Yes]: '):
if eh.os_id == 'alpine':
setupenv_alpine.start_wginterface_onboot()
else:
eh.enable_service(f'wg-quick@{cfg.wg_interface}')
if input_yes_no(f'7h) Would you like to start wgfrontend on boot? [Yes]: '):
if eh.os_id == 'alpine':
setupenv_alpine.start_wgfrontend_onboot()
else:
print(' Sorry, this can\'t be configured by this assistant on your platform yet.')
print(f'8a) Ensuring list permission of WireGuard config directory {os.path.dirname(cfg.wg_configfile)}.') print(f'8a) Ensuring list permission of WireGuard config directory {os.path.dirname(cfg.wg_configfile)}.')
os.chmod(os.path.dirname(cfg.wg_configfile), 0o711) os.chmod(os.path.dirname(cfg.wg_configfile), 0o711)
print(f'8b) Ensuring ownership of WireGuard config file {cfg.wg_configfile}.') print(f'8b) Ensuring ownership of WireGuard config file {cfg.wg_configfile}.')
@ -186,7 +262,7 @@ def setup_environment():
chown(cfg.user, cfg.sslcertfile) chown(cfg.user, cfg.sslcertfile)
print(f'8d) Ensuring ownership of server private key file {cfg.sslkeyfile} in case it exists.') print(f'8d) Ensuring ownership of server private key file {cfg.sslkeyfile} in case it exists.')
if os.path.exists(cfg.sslkeyfile): if os.path.exists(cfg.sslkeyfile):
chown(cfg.user, cfg.sslkeyfile) chown(cfg.user, cfg.sslkeyfile)
print(f'Attempting to start web frontend...') print(f'Attempting to start web frontend...')
return cfg return cfg

View File

@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
import os
import textwrap
def enable_startscript(service):
"""Start the given service on boot"""
ret = os.system('rc-update add {service}'.format(service=service))
return ret
def get_startupscript_wgfrontend():
"""Get a startup script for wgfrontend"""
template = r'''
#!/sbin/openrc-run
depend() {{
need net
}}
name=$RC_SVCNAME
command="wgfrontend"
command_args=""
pidfile="/run/$RC_SVCNAME.pid"
command_background="yes"
stopsig="SIGTERM"
start_stop_daemon_args="--stdout /var/log/wgfrontend.log --stderr /var/log/wgfrontend.err"
#Let wgfrontend drop privileges so that we keep permission to write to the log files
#command_user="wgfrontend:wgfrontend"
'''
template = textwrap.dedent(template).lstrip()
return template
def get_startupscript_wginterface(interface_name='wg_rw'):
"""Get a startup script for the WireGuard interface used by wgfrontend"""
template = r'''
#!/sbin/openrc-run
depend() {{
need net
}}
start() {{
wg-quick up {interface_name}
}}
stop() {{
wg-quick down {interface_name}
}}
name=$RC_SVCNAME
'''
template = textwrap.dedent(template).lstrip()
return template.format(interface_name=interface_name)
def write_startupscript_wgfrontend():
"""Write a startup script for wgfrontend to /etc/init.d"""
filename = '/etc/init.d/wgfrontend'
with open(filename, 'w') as f:
f.write(get_startupscript_wgfrontend())
os.chmod(filename, 0o700)
def write_startupscript_wginterface():
"""Write a startup script for the WireGuard interface of wgfrontend to /etc/init.d"""
filename = '/etc/init.d/wgfrontend_interface'
with open(filename, 'w') as f:
f.write(get_startupscript_wginterface())
os.chmod(filename, 0o700)
def start_wgfrontend_onboot():
"""Enable the start script of wgfrontend"""
write_startupscript_wgfrontend()
enable_startscript('wgfrontend')
def start_wginterface_onboot():
"""Enable the start script for the WireGuard interface of wgfrontend"""
write_startupscript_wginterface()
enable_startscript('wgfrontend_interface')