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 = {
'name': 'wgfrontend',
'version': '0.3.2',
'version': '0.4.0',
'author': 'The Towalink Project',
'author_email': 'pypi.wgfrontend@towalink.net',
'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"""
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
def sslcertfile(self):
"""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 grp
import ipaddress
import os
import pwd
import string
import subprocess
import wgconfig
import wgconfig.wgexec as wgexec
from . import config
from . import exechelper
from . import setupenv_alpine
def is_root():
@ -83,6 +87,39 @@ def drop_privileges(uid_name='nobody', gid_name='nogroup'):
os.setgid(gid)
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():
"""Environment setup assistant"""
cfg = config.Configuration()
@ -152,22 +189,46 @@ def setup_environment():
print(f'7) WireGuard config file {cfg.wg_configfile} already exists. Ok.')
else:
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]: ')
if not wg_listenport.strip():
wg_listenport = 51820
ok = False
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:
ok = True
else:
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]: ')
if not wg_address.strip():
wg_address = '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]: ')
ok = False
while not ok:
wg_address = input('7c) Please specify the IP address of the WireGuard interface incl. prefix length [192.168.0.1/24]: ')
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():
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.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:
@ -175,8 +236,23 @@ def setup_environment():
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, '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()
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)}.')
os.chmod(os.path.dirname(cfg.wg_configfile), 0o711)
print(f'8b) Ensuring ownership of WireGuard config file {cfg.wg_configfile}.')
@ -186,7 +262,7 @@ def setup_environment():
chown(cfg.user, cfg.sslcertfile)
print(f'8d) Ensuring ownership of server private key file {cfg.sslkeyfile} in case it exists.')
if os.path.exists(cfg.sslkeyfile):
chown(cfg.user, cfg.sslkeyfile)
chown(cfg.user, cfg.sslkeyfile)
print(f'Attempting to start web frontend...')
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')