diff --git a/usr/lib/python3.6/vmmgr/crypto.py b/usr/lib/python3.6/vmmgr/crypto.py index 5236d74..b048bcc 100644 --- a/usr/lib/python3.6/vmmgr/crypto.py +++ b/usr/lib/python3.6/vmmgr/crypto.py @@ -56,7 +56,7 @@ def create_selfsigned_cert(domain): f.write(cert.public_bytes(serialization.Encoding.PEM)) with open(CERT_KEY_FILE, 'wb') as f: f.write(private_key.private_bytes(serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption())) - os.chmod(CERT_KEY_FILE, 0o640) + os.chmod(CERT_KEY_FILE, 0o600) def get_cert_info(): # Gather certificate data important for setup-host diff --git a/usr/lib/python3.6/vmmgr/lxcmgr.py b/usr/lib/python3.6/vmmgr/lxcmgr.py index de02d81..95a7e8d 100644 --- a/usr/lib/python3.6/vmmgr/lxcmgr.py +++ b/usr/lib/python3.6/vmmgr/lxcmgr.py @@ -56,7 +56,7 @@ def update_hosts_lease(app, is_request): # If this call is a request for lease, find the first unassigned IP if is_request: used_ips = [l[0] for l in leases] - for i in range(2, 65534): + for i in range(2, 65278): # Reserve last /24 subnet for VPN ip = '172.17.{}.{}'. format(i // 256, i % 256) if ip not in used_ips: leases.append([ip, app]) diff --git a/usr/lib/python3.6/vmmgr/paths.py b/usr/lib/python3.6/vmmgr/paths.py index 1c11de2..3b921f5 100644 --- a/usr/lib/python3.6/vmmgr/paths.py +++ b/usr/lib/python3.6/vmmgr/paths.py @@ -23,8 +23,11 @@ LXC_ROOT = '/var/lib/lxc' ISSUE_FILE = '/etc/issue' NGINX_DIR = '/etc/nginx/conf.d' -# Remote support -WIREGUARD_FILE = '/etc/wireguard/wg0.conf' +# Remote access +AUTHORIZED_KEYS = '/root/.ssh/authorized_keys' +INTERFACES_FILE = '/etc/network/interfaces' +WG_CONF_FILE = '/etc/wireguard/wg0.conf' +WG_CONF_FILE_DISABLED = '/etc/wireguard/wg0.conf.disabled' # URLs MYIP_URL = 'https://tools.dasm.cz/myip.php' diff --git a/usr/lib/python3.6/vmmgr/remote.py b/usr/lib/python3.6/vmmgr/remote.py new file mode 100644 index 0000000..5d5208b --- /dev/null +++ b/usr/lib/python3.6/vmmgr/remote.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- + +import configparser +import os +import subprocess + +from . import templates +from .paths import AUTHORIZED_KEYS, INTERFACES_FILE, WG_CONF_FILE, WG_CONF_FILE_DISABLED + +def get_authorized_keys(): + # Fetches content of root's authorized_files + if not os.path.exists(AUTHORIZED_KEYS): + return '' + with open(AUTHORIZED_KEYS) as f: + return f.read() + +def set_authorized_keys(keys): + # Saves content of root's authorized_files + with open(AUTHORIZED_KEYS, 'w') as f: + f.write(keys) + +def is_wireguard_running(): + # Returns status of wg0 interface (inferred from existence of its config file) + return os.path.exists(WG_CONF_FILE) + +def regenerate_wireguard_key(): + # Regenerates WireGuard key pair for wg0 interface + was_running = is_wireguard_running() + if was_running: + stop_wireguard() + privkey = subprocess.run(['wg', 'genkey'], stdout=subprocess.PIPE).stdout.strip().decode() + with open(WG_CONF_FILE_DISABLED) as f: + conf_lines = f.readlines() + conf_lines[2] = 'PrivateKey = {}\n'.format(privkey) + with open(WG_CONF_FILE_DISABLED, 'w') as f: + f.writelines(conf_lines) + if was_running: + start_wireguard() + return privkey + +def get_wireguard_conf(): + # Returns dictionary with WireGuard wg0 interface VPN IP, public key, listen port and peer info + result = {'running': is_wireguard_running()} + # IP + with open(INTERFACES_FILE) as f: + for line in f.read().splitlines(): + if '172.17.255' in line: + result['ip'] = line.split('.')[-1] + break + # Listen port and peers + with open(WG_CONF_FILE if result['running'] else WG_CONF_FILE_DISABLED) as f: + conf_lines = f.read().splitlines() + result['port'] = conf_lines[1].split()[2] + result['peers'] = '\n'.join(conf_lines[4:]) + # Public key + privkey = conf_lines[2].split()[2] + if privkey == 'None': + privkey = regenerate_wireguard_key() + p = subprocess.Popen(['wg', 'pubkey'], stdin=subprocess.PIPE, stdout=subprocess.PIPE) + result['pubkey'] = p.communicate(privkey.encode())[0].strip().decode() + return result + +def set_wireguard_conf(ip, port, peers): + # Sets WireGuard wg0 interface VPN IP, listen port and peer info + was_running = is_wireguard_running() + if was_running: + stop_wireguard() + # IP + interface_lines = [] + with open(INTERFACES_FILE) as f: + for line in f.readlines(): + if '172.17.255' in line: + line = ' address 172.17.255.{}\n'.format(ip) + interface_lines.append(line) + with open(INTERFACES_FILE, 'w') as f: + f.writelines(interface_lines) + # Recreate config (listen port and peers) + with open(WG_CONF_FILE_DISABLED) as f: + conf_lines = f.readlines()[:4] + conf_lines[1] = 'ListenPort = {}\n'.format(port) + with open(WG_CONF_FILE_DISABLED, 'w') as f: + f.writelines(conf_lines) + f.write(peers) + if was_running: + start_wireguard() + +def start_wireguard(): + # Sets up WireGuard interface + if not os.path.exists(WG_CONF_FILE): + os.rename(WG_CONF_FILE_DISABLED, WG_CONF_FILE) + else: + subprocess.run(['ifdown', 'wg0']) + subprocess.run(['ifup', 'wg0']) + +def stop_wireguard(): + # Tears down WireGuard interface + subprocess.run(['ifdown', 'wg0']) + if os.path.exists(WG_CONF_FILE): + os.rename(WG_CONF_FILE, WG_CONF_FILE_DISABLED) diff --git a/usr/lib/python3.6/vmmgr/templates.py b/usr/lib/python3.6/vmmgr/templates.py index 6260b54..b87c179 100644 --- a/usr/lib/python3.6/vmmgr/templates.py +++ b/usr/lib/python3.6/vmmgr/templates.py @@ -112,15 +112,3 @@ ISSUE = ''' - \x1b[1m{url}\x1b[0m - \x1b[1m{ip}\x1b[0m\x1b[?1c ''' - -WIREGUARD = ''' -[Interface] -ListenPort = 51820 -PrivateKey = {privkey} - -[Peer] -PublicKey = {pubkey} -AllowedIPs = 172.18.0.1/32 -Endpoint = {endpoint} -PersistentKeepalive = 15 -''' diff --git a/usr/lib/python3.6/vmmgr/vmmgr.py b/usr/lib/python3.6/vmmgr/vmmgr.py index 72ad2ad..7aaf01e 100644 --- a/usr/lib/python3.6/vmmgr/vmmgr.py +++ b/usr/lib/python3.6/vmmgr/vmmgr.py @@ -10,7 +10,7 @@ import urllib from . import crypto from . import templates from . import net -from .paths import ACME_CRON, ACME_DIR, ISSUE_FILE, NGINX_DIR, RELOAD_URL, WIREGUARD_FILE +from .paths import ACME_CRON, ACME_DIR, ISSUE_FILE, NGINX_DIR, RELOAD_URL class VMMgr: def __init__(self, conf): @@ -134,7 +134,7 @@ class VMMgr: # Copy certificate files shutil.copyfile(public_file, crypto.CERT_PUB_FILE) shutil.copyfile(private_file, crypto.CERT_KEY_FILE) - os.chmod(crypto.CERT_KEY_FILE, 0o640) + os.chmod(crypto.CERT_KEY_FILE, 0o600) # Reload nginx self.reload_nginx() @@ -143,15 +143,3 @@ class VMMgr: def reboot_vm(self): subprocess.run(['/sbin/reboot']) - - def enable_remote_support(self, pubkey, endpoint): - # Sets up wireguard interface - privkey = subprocess.run(['wg', 'genkey']) - with open(WIREGUARD_FILE, 'w') as f: - f.write(templates.WIREGUARD.format(privkey=privkey, pubkey=pubkey, endpoint=endpoint)) - subprocess.check_output(['ip', 'link', 'set', 'wg0', 'up']) - - def disable_remote_support(self): - # Tears down wireguard settings - os.unlink(WIREGUARD_FILE) - subprocess.check_output(['ip', 'link', 'set', 'wg0', 'down']) diff --git a/usr/lib/python3.6/vmmgr/wsgiapp.py b/usr/lib/python3.6/vmmgr/wsgiapp.py index d03c6b8..058f195 100644 --- a/usr/lib/python3.6/vmmgr/wsgiapp.py +++ b/usr/lib/python3.6/vmmgr/wsgiapp.py @@ -13,6 +13,7 @@ from cryptography.exceptions import InvalidSignature from . import crypto from . import net +from . import remote from . import validator from .actionqueue import ActionQueue from .appmgr import AppMgr @@ -38,12 +39,14 @@ class WSGIApp: Rule('/login', methods=['POST'], endpoint='login_action'), Rule('/setup-host', redirect_to='/login?redir=setup-host'), Rule('/setup-apps', redirect_to='/login?redir=setup-apps'), + Rule('/setup-remote', redirect_to='/login?redir=setup-remote'), )) self.admin_url_map = Map(( Rule('/', endpoint='portal_view'), Rule('/logout', endpoint='logout_action'), Rule('/setup-host', endpoint='setup_host_view'), Rule('/setup-apps', endpoint='setup_apps_view'), + Rule('/setup-remote', endpoint='setup_remote_view'), Rule('/update-host', endpoint='update_host_action'), Rule('/verify-dns', endpoint='verify_dns_action'), Rule('/verify-https', endpoint='verify_http_action', defaults={'proto': 'https'}), @@ -62,6 +65,11 @@ class WSGIApp: Rule('/update-password', endpoint='update_password_action'), Rule('/shutdown-vm', endpoint='shutdown_vm_action'), Rule('/reboot-vm', endpoint='reboot_vm_action'), + Rule('/update-ssh-keys', endpoint='update_ssh_keys_action'), + Rule('/update-vpn', endpoint='update_vpn_action'), + Rule('/generate-vpn-key', endpoint='generate_vpn_key_action'), + Rule('/start-vpn', endpoint='start_vpn_action'), + Rule('/stop-vpn', endpoint='stop_vpn_action'), )) self.localhost_url_map = Map(( Rule('/reload-config', endpoint='reload_config_action'), @@ -248,14 +256,22 @@ class WSGIApp: app_data[app] = {'title': title, 'visible': visible, 'installed': installed, 'autostarted': autostarted, 'status': status, 'actions': actions} return self.render_template('setup-apps-table.html', request, app_data=app_data) + def setup_remote_view(self, request): + # Remote access setup view + authorized_keys = remote.get_authorized_keys() + wg_conf = remote.get_wireguard_conf() + wg_conf['status'] = ('info', request.session.lang.status_started()) if wg_conf['running'] else ('error', request.session.lang.status_stopped()) + message = self.get_session_message(request) + return self.render_html('setup-remote.html', request, authorized_keys=authorized_keys, wg_conf=wg_conf, message=message) + def update_host_action(self, request): # Update domain and port, then restart nginx domain = request.form['domain'] port = request.form['port'] if not validator.is_valid_domain(domain): - return self.render_json({'error': request.session.lang.invalid_domain(domain)}) + return self.render_json({'error': request.session.lang.invalid_domain()}) if not validator.is_valid_port(port): - return self.render_json({'error': request.session.lang.invalid_port(port)}) + return self.render_json({'error': request.session.lang.invalid_port()}) self.vmmgr.update_host(domain, port) url = '{}/setup-host'.format(net.compile_url(net.get_local_ip(), port)) response = self.render_json({'ok': request.session.lang.host_updated(url, url)}) @@ -410,3 +426,39 @@ class WSGIApp: def is_app_visible(self, app): return app in self.conf['apps'] and self.conf['apps'][app]['visible'] and self.appmgr.is_service_started(app) + + def update_ssh_keys_action(self, request): + # Update authorized_keys file + remote.set_authorized_keys(request.form['ssh-keys'].replace('\r', '')) + request.session['msg'] = 'ssh:info:{}'.format(request.session.lang.ssh_keys_installed()) + return redirect('/setup-remote') + + def update_vpn_action(self, request): + # Update WireGuard VPN listen port, virtual IP and peer list + ip = request.form['vpn-lip'] + if not ip.isdigit() or not 0 < int(ip) < 255: + request.session['msg'] = 'vpn:error:{}'.format(request.session.lang.invalid_ip()) + return redirect('/setup-remote') + port = request.form['vpn-port'] + if not port.isdigit() or not 0 < int(port) < 65536: + request.session['msg'] = 'vpn:error:{}'.format(request.session.lang.invalid_port()) + return redirect('/setup-remote') + peers = request.form['vpn-peers'].replace('\r', '') + remote.set_wireguard_conf(ip, port, peers) + request.session['msg'] = 'vpn:info:{}'.format(request.session.lang.vpn_updated()) + return redirect('/setup-remote') + + def generate_vpn_key_action(self, request): + # Regenerate WireGuard key pair + remote.regenerate_wireguard_key() + return redirect('/setup-remote') + + def start_vpn_action(self, request): + # Start WireGuard VPN + remote.start_wireguard() + return redirect('/setup-remote') + + def stop_vpn_action(self, request): + # Stop WireGuard VPN + remote.stop_wireguard() + return redirect('/setup-remote') diff --git a/usr/lib/python3.6/vmmgr/wsgilang.py b/usr/lib/python3.6/vmmgr/wsgilang.py index 3dbadbe..010feb7 100644 --- a/usr/lib/python3.6/vmmgr/wsgilang.py +++ b/usr/lib/python3.6/vmmgr/wsgilang.py @@ -2,8 +2,9 @@ class WSGILang: lang = { - 'invalid_domain': 'Zadaný doménový název "{}" není platný.', - 'invalid_port': 'Zadaný port "{}" není platný.', + 'invalid_domain': 'Zadaný doménový název není platný.', + 'invalid_port': 'Zadaný port není platný.', + 'invalid_ip': 'Zadaná IP adresa není platná.', 'host_updated': 'Nastavení hostitele bylo úspěšně změněno. Přejděte na URL {} a pokračujte následujícími kroky.', 'dns_record_does_not_exist': 'DNS záznam pro název "{}" neexistuje.', 'dns_record_mismatch': 'DNS záznam pro název "{}" směřuje na IP {} místo očekávané {}.', @@ -31,7 +32,7 @@ class WSGILang: 'bad_password': 'Nesprávné heslo', 'password_mismatch': 'Zadaná hesla se neshodují', 'password_empty': 'Nové heslo nesmí být prázdné', - 'password_changed': 'Heslo úspěšně změněno', + 'password_changed': 'Heslo bylo úspěšně změněno', 'reboot_initiated': 'Příkaz odeslán. Vyčkejte na restartování virtuálního stroje.', 'shutdown_initiated': 'Příkaz odeslán. Vyčkejte na vypnutí virtuálního stroje.', 'status_queued': 've frontě', @@ -49,6 +50,8 @@ class WSGILang: 'action_stop': 'Zastavit', 'action_install': 'Instalovat', 'action_uninstall': 'Odinstalovat', + 'ssh_keys_installed': 'SSH klíče byly úspěšně změněny.', + 'vpn_updated': 'Nastavení VPN bylo úspěšně změněno.', } def __getattr__(self, key): diff --git a/usr/share/vmmgr/static/css/style.css b/usr/share/vmmgr/static/css/style.css index 7ec9407..d9b2cc5 100644 --- a/usr/share/vmmgr/static/css/style.css +++ b/usr/share/vmmgr/static/css/style.css @@ -147,12 +147,32 @@ header p, white-space: nowrap; } -.setup-box td.remark { +.setup-box .remark { color: #999; font-size: 80%; font-style: italic; line-height: 125%; padding-top: 5px; + margin-top: 0px; +} + +.setup-box #ssh-keys { + min-height: 100px; + width: 99%; +} + +.setup-box #vpn-peers { + min-height: 180px; + width: 100%; +} + +.setup-box #vpn-lip { + width: 40px; +} + +.setup-box #vpn-pubkey { + width: 360px; + font-family: monospace; } #app-manager { diff --git a/usr/share/vmmgr/templates/layout.html b/usr/share/vmmgr/templates/layout.html index a6ceee6..5e7df79 100644 --- a/usr/share/vmmgr/templates/layout.html +++ b/usr/share/vmmgr/templates/layout.html @@ -27,6 +27,7 @@ {% if session.admin %}
  • Nastavení hostitele
  • Nastavení aplikací
  • +
  • Nastavení vzdáleného přístupu
  • Odhlášení
  • {% else %}
  • Přihlášení
  • diff --git a/usr/share/vmmgr/templates/setup-remote.html b/usr/share/vmmgr/templates/setup-remote.html new file mode 100644 index 0000000..78eac54 --- /dev/null +++ b/usr/share/vmmgr/templates/setup-remote.html @@ -0,0 +1,72 @@ +{% extends 'layout.html' %} +{% block title %}Nastavení vzdáleného přístupu{% endblock %} +{% block body %} +
    +

    SSH klíče

    +

    Obsah souboru authorized_hosts uživatele root.

    +
    + + + {% if message and message[0] == 'ssh' %} +

    {{ message[2] }}

    + {% endif %} +
    +
    + +
    +

    WireGuard VPN

    +

    Pro vzdálený administrátorský přístup nebo propojení dvou virtuálních strojů v sítích, kde není možno nastavit překlad síťových adres, je možno využít PPP VPN protokol WireGuard.

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Naslouchací portPort na kterém WireGuard rozhraní tohoto virtuálního stroje naslouchá příchozím spojením.
    VPN IP adresa172.17.255.Virtuální IP adresa WireGuard rozhraní tohoto virtuálního stroje dostupná pouze v rámci VPN.
    Partneři + +

    Seznam WireGuard partnerů ve formátu konfiguračního souboru WireGuard. Pro více informací a příklady užití vizte stránku WireGuard VPN v technické dokumentaci.

    +
      + +
    +
     
    Váš veřejný klíč
    Stav VPN{{ wg_conf['status'][1] }}
      + {% if wg_conf['running'] %} + + {% else %} + + {% endif %} + {% if message and message[0] == 'vpn' %} +

    {{ message[2] }}

    + {% endif %} +
    +
    +
    +{% endblock %}