Implement VPN + SSH configuration
This commit is contained in:
parent
d863fe6675
commit
bba7e0383c
@ -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
|
||||
|
@ -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])
|
||||
|
@ -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'
|
||||
|
99
usr/lib/python3.6/vmmgr/remote.py
Normal file
99
usr/lib/python3.6/vmmgr/remote.py
Normal file
@ -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)
|
@ -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
|
||||
'''
|
||||
|
@ -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'])
|
||||
|
@ -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')
|
||||
|
@ -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 href="{}">{}</a> 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):
|
||||
|
@ -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 {
|
||||
|
@ -27,6 +27,7 @@
|
||||
{% if session.admin %}
|
||||
<li><a href="/setup-host">Nastavení hostitele</a></li>
|
||||
<li><a href="/setup-apps">Nastavení aplikací</a></li>
|
||||
<li><a href="/setup-remote">Nastavení vzdáleného přístupu</a></li>
|
||||
<li><a href="/logout">Odhlášení</a></li>
|
||||
{% else %}
|
||||
<li><a href="/login">Přihlášení</a></li>
|
||||
|
72
usr/share/vmmgr/templates/setup-remote.html
Normal file
72
usr/share/vmmgr/templates/setup-remote.html
Normal file
@ -0,0 +1,72 @@
|
||||
{% extends 'layout.html' %}
|
||||
{% block title %}Nastavení vzdáleného přístupu{% endblock %}
|
||||
{% block body %}
|
||||
<div class="setup-box">
|
||||
<h2>SSH klíče</h2>
|
||||
<p>Obsah souboru <em>authorized_hosts</em> uživatele root.</p>
|
||||
<form id="update-ssh-keys" action="/update-ssh-keys" method="post">
|
||||
<textarea name="ssh-keys" id="ssh-keys">{{ authorized_keys }}</textarea>
|
||||
<input type="submit" value="Nastavit klíče">
|
||||
{% if message and message[0] == 'ssh' %}
|
||||
<p class="{{ message[1] }}">{{ message[2] }}</p>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="setup-box">
|
||||
<h2>WireGuard VPN</h2>
|
||||
<p>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.</p>
|
||||
<form id="update-vpn" action="/update-vpn" method="post">
|
||||
<table>
|
||||
<tr>
|
||||
<td>Naslouchací port</td>
|
||||
<td><input type="text" name="vpn-port" id="vpn-port" value="{{ wg_conf['port'] }}"></td>
|
||||
<td class="remark">Port na kterém WireGuard rozhraní tohoto virtuálního stroje naslouchá příchozím spojením.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>VPN IP adresa</td>
|
||||
<td>172.17.255.<input type="text" name="vpn-lip" id="vpn-lip" value="{{ wg_conf['ip'] }}"></td>
|
||||
<td class="remark">Virtuální IP adresa WireGuard rozhraní tohoto virtuálního stroje dostupná pouze v rámci VPN.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Partneři</td>
|
||||
<td colspan="2">
|
||||
<textarea name="vpn-peers" id="vpn-peers">{{ wg_conf['peers'] }}</textarea>
|
||||
<p class="remark">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.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td colspan="2">
|
||||
<input type="submit" value="Nastavit VPN">
|
||||
<div id="vpn-message"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Váš veřejný klíč</td>
|
||||
<td colspan="2"><input type="text" id="vpn-pubkey" value="{{ wg_conf['pubkey'] }}" readonly> <input type="submit" formaction="/generate-vpn-key" value="Generovat nový klíč"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Stav VPN</td>
|
||||
<td colspan="2" class="{{ wg_conf['status'][0] }}">{{ wg_conf['status'][1] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td colspan="2">
|
||||
{% if wg_conf['running'] %}
|
||||
<input type="submit" formaction="/stop-vpn" value="Zastavit VPN">
|
||||
{% else %}
|
||||
<input type="submit" formaction="/start-vpn" value="Spustit VPN">
|
||||
{% endif %}
|
||||
{% if message and message[0] == 'vpn' %}
|
||||
<p class="{{ message[1] }}">{{ message[2] }}</p>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
Loading…
Reference in New Issue
Block a user