diff --git a/web/static/vpn.js b/web/static/vpn.js
new file mode 100644
index 0000000..c8e66af
--- /dev/null
+++ b/web/static/vpn.js
@@ -0,0 +1,114 @@
+function delKey(key) {
+ fetch('del', {
+ credentials: 'include',
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ pubkey: key })
+ })
+ .then(response => {
+ if (!response.ok)
+ throw new Error('deleting key failed');
+ return response.text();
+ })
+ .then(data => {
+ fetchKeys();
+ })
+ .catch(error => console.error(error));
+}
+
+function fetchKeys() {
+ fetch('list', {
+ credentials: 'include'
+ })
+ .then(response => {
+ if (!response.ok)
+ throw new Error('fetching keys failed');
+ return response.json();
+ })
+ .then(data => {
+ const keys = document.querySelector('ul.keys');
+ keys.innerHTML = '';
+ const warning = document.querySelector('p#active-key-warning');
+ warning.hidden = true;
+
+ for (let key of Object.values(data)) {
+ var a = document.createElement('a');
+ a.innerText = '✖';
+ a.href = '';
+ a.addEventListener('click', event => {
+ delKey(key.key);
+ event.preventDefault();
+ });
+ var li = document.createElement('li');
+ li.innerHTML = ' ' + (new Date(key.time*1000).toISOString().split('T')[0]) +
+ ' ' + key.key + '
' + key.name +
+ (key.active ? '★ ' : '');
+ li.prepend(a);
+ keys.appendChild(li);
+ if (key.active)
+ warning.hidden = false;
+ }
+ document.querySelector('section.keys').style.display = (Object.keys(data).length ? 'unset' : 'none');
+ })
+ .catch(error => console.error(error));
+}
+
+window.addEventListener('load', fetchKeys);
+
+const form = document.querySelector('form#request');
+form.addEventListener('submit', event => {
+ event.preventDefault();
+
+ const inputs = Array.from(document.querySelectorAll('form input'));
+ const settings = document.querySelector('section#settings');
+ const submit = document.querySelector('button#submit');
+
+ const key = wireguard.generateKeypair();
+ const options = Object.fromEntries(inputs.map(e => [e.name, e.type === 'checkbox' ? e.checked : e.value]));
+ const networks = Array.from(document.querySelectorAll('select#networks option:checked')).map(e => e.value);
+
+ submit.innerHTML = 'Obdelovanje…';
+ submit.disabled = true;
+ fetch('new', {
+ credentials: 'include',
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ pubkey: key.publicKey,
+ options: options,
+ networks: networks,
+ })
+ })
+ .then(response => {
+ if (!response.ok) {
+ response.text().then(text => {
+ settings.innerHTML = response.status + ' ' + response.statusText + ': ' + text;
+ });
+ } else {
+ return response.text();
+ }
+ })
+ .then(text => {
+ const config = text.replace(/PrivateKey = .*/, "PrivateKey = "+key.privateKey).trim();
+ document.querySelector('code#config').innerHTML = config;
+
+ const blob = new Blob([config], { type: 'text/plain;charset=utf-8' });
+ const link = document.getElementById('download');
+ link.download = 'wg-fri.conf';
+ link.href = window.URL.createObjectURL(blob);
+
+ const qr = qrcode(0, 'L');
+ qr.addData(config.replace(/#.*\n/g, ''));
+ qr.make();
+ document.getElementById('qr').innerHTML = qr.createSvgTag(3);
+
+ fetchKeys();
+ })
+ .catch(error => {
+ settings.innerHTML = error;
+ })
+ .finally(() => {
+ form.style.display = 'none';
+ settings.style.display = 'unset';
+ });
+});
diff --git a/web/templates/vpn/index.html b/web/templates/vpn/index.html
index 473704d..c78a323 100644
--- a/web/templates/vpn/index.html
+++ b/web/templates/vpn/index.html
@@ -2,10 +2,7 @@
{% block content %}
-Za VPN oziroma oddeljano povezavo v omrežje FRI uporabljamo WireGuard. Več informacij o uporabi in nastavitvah VPN najdete v dokumentaciji. - -
-Za priklop v omrežje spodaj ustvarite nov ključ in prenesite izpisano datoteko. Nato sledite napotkom za posamezni sistem.
+Za VPN oziroma oddeljano povezavo v omrežje FRI uporabljamo WireGuard. Za priklop v omrežje ustvarite nov ključ in prenesite izpisano datoteko. Nato sledite napotkom za posamezni sistem.
-
-
+
+
-Če vklopite prvo opcijo, bo vaš računalnik čez VPN usmerjal ves promet. Če izklopite drugo opcijo, bodo nekateri strežniki dostopni le prek naslova IP. Če ste v dvomih, pustite privzete nastavitve.
+Če vklopite prvo opcijo, bo vaš računalnik čez VPN usmerjal ves promet. Če izklopite drugo opcijo, bodo nekateri strežniki dostopni le prek naslova IP. Če ste v dvomih, pustite privzete nastavitve. Več informacij o uporabi in nastavitvah VPN najdemo v dokumentaciji.
-
+
{% endblock %}
diff --git a/web/vpn.py b/web/vpn.py
index ce82fe4..46f0431 100644
--- a/web/vpn.py
+++ b/web/vpn.py
@@ -21,20 +21,22 @@ def index():
@blueprint.route('/list')
@flask_login.login_required
def list():
+ # Return logged-in user’s keys, marking the key used for current connection (if any).
user = flask_login.current_user.get_id()
- return flask.jsonify(
- {k: v | {'active': flask.request.remote_addr in (v.get('ip'), v.get('ip6'))}
- for k, v in db.load('wireguard').items() if v.get('user') == user})
+ return flask.jsonify({
+ ip: data | {'active': flask.request.remote_addr in (ip, data.get('ip6'))}
+ for ip, data in db.load('wireguard').items() if data.get('user') == user
+ })
@blueprint.route('/new', methods=('POST',))
@flask_login.login_required
def new():
- # Each key is associated with a new IPv4 address from the pool settings['wg_net'].
- # Each key gets an IPv6 subnet depending on the amount of surplus addresses available.
+ # Each key is assigned a new IPv4 address from the pool settings['wg_net'].
+ # Each key is then assigned a corresponding IPv6 subnet, depending on the amount of surplus addresses available.
# For wg_net 10.10.0.0/18 and wg_net6 1234:5678:90ab:cdef::/64,
# the key for 10.10.0.10/32 would get 1234:5678:90ab:cdef:a::/80.
def ipv4to6(net4, ip4, net6):
- # Calculate the address and prefix length for the assigned IPv6 network.
+ # Calculate the address and prefix length for the IPv6 network that can be assigned to this key.
len4 = (net4.max_prefixlen - net4.prefixlen)
len6 = (net6.max_prefixlen - net6.prefixlen)
# Make sure the network address ends at a colon. Wastes some addresses but IPv6.
@@ -43,35 +45,39 @@ def new():
return ip6 + '/' + str(net6.max_prefixlen - assigned)
pubkey = flask.request.json.get('pubkey', '')
+ options = flask.request.json.get('options', {})
if not re.match(wgkey_regex, pubkey):
return flask.Response('invalid key', status=400, mimetype='text/plain')
+ # Read server’s private key and find the corresponding public key.
settings = db.load('settings')
server_pubkey = subprocess.run([f'wg pubkey'], input=settings.get('wg_key'),
text=True, capture_output=True, shell=True).stdout.strip()
+ now = datetime.datetime.utcnow()
+ key = {
+ 'key': pubkey,
+ 'time': now.timestamp(),
+ 'name': re.sub('[^-._A-Za-z0-9]', '', options.get('name', '')),
+ }
+
+ # Determine IPv4 and IPv6 addresses for the new key.
host = ipaddress.ip_interface(settings.get('wg_net') or '10.0.0.1/24')
- ip6 = None
+ key['ip6'] = None
with db.locked():
# Find a free address for the new key.
keys = db.read('wireguard')
for index, ip in enumerate(host.network.hosts(), start=1):
if ip != host.ip and str(ip) not in keys:
if wg_net6 := settings.get('wg_net6'):
- ip6 = ipv4to6(host.network, ip, ipaddress.ip_interface(wg_net6).network)
+ key['ip6'] = ipv4to6(host.network, ip, ipaddress.ip_interface(wg_net6).network)
break
else:
return flask.Response('no more available IP addresses', status=500, mimetype='text/plain')
- now = datetime.datetime.utcnow()
- name = re.sub('[^-._A-Za-z0-9]', '', flask.request.json.get('name', ''))
- keys[str(ip)] = {
- 'key': pubkey,
- 'ip6': str(ip6) if ip6 else None,
- 'time': now.timestamp(),
- 'user': flask_login.current_user.get_id(),
- 'name': name,
- }
+ # Add remaining attributes to new key and update key database.
+ key['user'] = flask_login.current_user.get_id()
+ keys[str(ip)] = key
db.write('wireguard', keys)
# Generate a new config archive for firewall nodes.
@@ -83,13 +89,13 @@ def new():
'port': settings.get('wg_port') or '51820',
'server_key': server_pubkey,
'pubkey': pubkey,
- 'ip': str(ip),
- 'ip6': str(ip6) if ip6 else None,
'timestamp': now,
- 'name': name,
- 'dns': settings.get('wg_dns') if flask.request.json.get('use_dns', True) else False,
+ 'ip': str(ip),
+ 'ip6': key['ip6'],
+ 'name': key['name'],
+ 'dns': settings.get('wg_dns', '') if options.get('use-dns', True) else False,
'allowed_nets': settings.get('wg_allowed_nets', ''),
- 'add_default': flask.request.json.get('add_default', False),
+ 'add_default': options.get('add-default', False),
}
return flask.render_template('vpn/wg-fri.conf', **args)
@@ -102,7 +108,10 @@ def delete():
with db.locked():
user = flask_login.current_user.get_id()
- keys = {k: v for k, v in db.read('wireguard').items() if v.get('user') != user or v.get('key') != pubkey}
+ keys = {
+ ip: data for ip, data in db.read('wireguard').items()
+ if not (data.get('key') == pubkey and (data.get('user') == user or flask_login.current_user.is_admin))
+ }
db.write('wireguard', keys)
system.run(system.save_config)
Windows / Mac
@@ -43,14 +40,14 @@ Na vsaki napravi, ki jo želite povezati v omrežje FRI, ustvarite nov ključ. P
-
-
+
+