vpn: add support for custom keys
Custom keys are created by admin and specify networks directly, bypassing AD permissions. They are intended to join managed devices into networks where users are not allowed to create keys themselves. Also comprehend a set directly.
This commit is contained in:
parent
1b26f0738a
commit
3c25cbe88a
|
@ -17,7 +17,7 @@ function delKey(key) {
|
|||
}
|
||||
|
||||
function fetchKeys() {
|
||||
fetch('list', {
|
||||
fetch(endpoint, {
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(response => {
|
||||
|
@ -26,29 +26,7 @@ function fetchKeys() {
|
|||
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]) +
|
||||
' <code>' + key.key + '</code> ' + key.name +
|
||||
(key.active ? '<font color="red"><sup>★</sup></font> ' : '');
|
||||
li.prepend(a);
|
||||
keys.appendChild(li);
|
||||
if (key.active)
|
||||
warning.hidden = false;
|
||||
}
|
||||
document.querySelector('section.keys').style.display = (Object.keys(data).length ? 'unset' : 'none');
|
||||
update(Object.values(data));
|
||||
})
|
||||
.catch(error => console.error(error));
|
||||
}
|
||||
|
|
|
@ -77,14 +77,21 @@ def save_config():
|
|||
settings = db.read('settings')
|
||||
version = settings['version'] = int(settings.get('version') or '0') + 1
|
||||
|
||||
# Update IP sets with VPN addresses based on AD group membership.
|
||||
vpn_groups = set([e['vpn'] for e in ipsets.values() if e.get('vpn')])
|
||||
# Find networks accessible to VPN users for each AD group.
|
||||
vpn_groups = {e['vpn'] for e in ipsets.values() if e.get('vpn')}
|
||||
group_networks = {
|
||||
group: [name for name, data in ipsets.items() if data['vpn'] == group] for group in vpn_groups
|
||||
}
|
||||
|
||||
# Add VPN addresses to IP sets.
|
||||
for ip, key in wireguard.items():
|
||||
# Find all networks this IP should belong to:
|
||||
# - manually specified networks for custom keys,
|
||||
# - networks accessible to any of the user’s groups.
|
||||
key_networks = set(key.get('networks', ()))
|
||||
for group in user_groups.get(key.get('user', ''), ()):
|
||||
for network in group_networks.get(group, ()):
|
||||
key_networks |= set(group_networks.get(group, ()))
|
||||
for network in key_networks:
|
||||
ipsets[network]['ip'].append(f'{ip}/32')
|
||||
if ip6 := key.get('ip6'):
|
||||
ipsets[network]['ip6'].append(ip6)
|
||||
|
|
|
@ -9,7 +9,8 @@ body {
|
|||
margin: 1em auto;
|
||||
}
|
||||
code {
|
||||
background-color: #eeeeee;
|
||||
background-color: #f8f8f8;
|
||||
padding: 0.1em 0.25em;
|
||||
}
|
||||
details {
|
||||
margin: 0.5em 1em;
|
||||
|
@ -31,18 +32,22 @@ input:read-only {
|
|||
border-style: dotted;
|
||||
}
|
||||
pre {
|
||||
background-color: #eeeeee;
|
||||
background-color: #f8f8f8;
|
||||
border: 1px solid #cccccc;
|
||||
padding: 0.5em;
|
||||
}
|
||||
table {
|
||||
border-spacing: 0 0.1em;
|
||||
}
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
th, td {
|
||||
padding-right: 1em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
th {
|
||||
border-bottom: 1px solid black;
|
||||
tbody > tr:hover {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
ul.keys {
|
||||
margin: 0 0.5em 0.5em;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<section>
|
||||
<dl>
|
||||
<dt><a href="{{ url_for('vpn.index') }}">VPN</a>
|
||||
<dd>urejanje ključev za WireGuard VPN
|
||||
<dd>urejanje ključev za oddaljeni dostop
|
||||
<dt><a href="{{ url_for('rules.manage') }}">Pravila</a>
|
||||
<dd>vklop / izklop pravil za požarni zid
|
||||
</dl>
|
||||
|
@ -20,6 +20,8 @@
|
|||
<dd>pravila za posredovanje prometa
|
||||
<dt><a href="{{ url_for('config.edit', name='netmap') }}">Netmap</a>
|
||||
<dd>statične 1:1 preslikave naslovov za strežniška omrežja
|
||||
<dt><a href="{{ url_for('vpn.custom') }}">VPN po meri</a>
|
||||
<dd>urejanje ključev za oddaljeni dostop do posebnih omrežij
|
||||
<dt><a href="{{ url_for('config.index') }}">Nastavitve</a>
|
||||
<dd>nastavitve aplikacije FRIwall
|
||||
</dl>
|
||||
|
|
68
web/templates/vpn/custom.html
Normal file
68
web/templates/vpn/custom.html
Normal file
|
@ -0,0 +1,68 @@
|
|||
{% extends 'base.html' %}
|
||||
{% block header %}
|
||||
<style>
|
||||
td > input {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<p>
|
||||
Urejate ključe WireGuard s posebnimi dostopi.
|
||||
|
||||
<table class="keys">
|
||||
<thead>
|
||||
<th><th>Ključ<th>IP<th>IPv6<th>Naprava<th>Omrežja
|
||||
<tbody>
|
||||
</table>
|
||||
|
||||
<section>
|
||||
<h1>Nov ključ</h1>
|
||||
<form id="request">
|
||||
<p>
|
||||
<label for="name">Ime naprave</label><br>
|
||||
<input type="text" id="name" name="name" pattern="[-._A-Za-z0-9 ]*" maxlength="32" placeholder="A-Z a-z 0-9 . _ - " />
|
||||
<p>
|
||||
<label for="networks">Omrežja</label><br>
|
||||
<select id="networks" name="networks" multiple style="width: 20em;">
|
||||
{% for network in ipsets %}
|
||||
<option>{{ network }}
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p>
|
||||
<button id="submit" type="submit">Ustvari ključ</button>
|
||||
</form>
|
||||
|
||||
<section id="settings" style="display: none;">
|
||||
<p>
|
||||
Nastavitve za povezavo so izpisane spodaj. Za nov ključ osvežite to stran.
|
||||
|
||||
<section style="display: flex; align-items: center;">
|
||||
<pre style="flex-grow: 3; margin: 0;"><a id="download" href="" style="float: right; padding: 0.5em;">Prenesi</a><code id="config"></code></pre>
|
||||
<div id="qr" style="flex-grow: 1; text-align: center;"></div>
|
||||
</section>
|
||||
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='qrcode.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='wireguard.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='vpn.js') }}"></script>
|
||||
<script type="text/javascript">
|
||||
const endpoint = 'list-custom';
|
||||
function update(keys) {
|
||||
const keytab = document.querySelector('table.keys > tbody');
|
||||
keytab.innerHTML = ''
|
||||
for (const key of keys) {
|
||||
const row = keytab.insertRow();
|
||||
row.insertCell().innerHTML = '<button onclick="delKey(\'' + key.key + '\');">✖</button>';
|
||||
row.insertCell().innerHTML = '<code>' + key.key + '</code>';
|
||||
row.insertCell().innerHTML = key.ip;
|
||||
row.insertCell().innerHTML = key.ip6 || '';
|
||||
row.insertCell().innerHTML = key.name;
|
||||
row.insertCell().innerHTML = key.networks;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -68,12 +68,37 @@ V nastavitvah lahko dodate ali odstranite vnose <code>AllowedIPs</code>. Ti dolo
|
|||
<h1>Ključi</h1>
|
||||
<p>
|
||||
Če ključa ne uporabljamo, smo ga izgubili ali so nam ga ukradli, ga tukaj odstranimo. Trenutno so registrirani ključi:
|
||||
<ul class="keys" style="list-style: none;"></ul>
|
||||
<p class="keys" id="active-key-warning" style="margin-top: 0;">
|
||||
|
||||
<table class="keys">
|
||||
<thead><th><th>Ključ<th>IP<th>IPv6<th>Naprava
|
||||
<tbody>
|
||||
</table>
|
||||
|
||||
<p class="keys" id="active-key-warning">
|
||||
<font color="red"><sup>★</sup></font> Ta ključ uporablja trenutna povezava. Če ga odstranite, bo prekinjena.
|
||||
</section>
|
||||
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='qrcode.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='wireguard.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='vpn.js') }}"></script>
|
||||
<script type="text/javascript">
|
||||
const endpoint = 'list';
|
||||
function update(keys) {
|
||||
const keytab = document.querySelector('table.keys > tbody');
|
||||
const warning = document.querySelector('p#active-key-warning');
|
||||
keytab.innerHTML = ''
|
||||
warning.hidden = true;
|
||||
for (const key of keys) {
|
||||
const row = keytab.insertRow();
|
||||
row.insertCell().innerHTML = '<button onclick="delKey(\'' + key.key + '\');">✖</button>';
|
||||
row.insertCell().innerHTML = '<code>' + key.key + '</code>';
|
||||
row.insertCell().innerHTML = key.ip;
|
||||
row.insertCell().innerHTML = key.ip6 || '';
|
||||
row.insertCell().innerHTML = key.name + (key.active ? '<font color="red">★</font>' : '');
|
||||
if (key.active)
|
||||
warning.hidden = false;
|
||||
}
|
||||
document.querySelector('section.keys').style.display = (keys.length ? 'unset' : 'none');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[Interface]
|
||||
# {{ timestamp }} {{ current_user['username'] }} {{ name }}
|
||||
# {{ timestamp }} {{ user }} {{ name }}
|
||||
# PublicKey = {{ pubkey }}
|
||||
PrivateKey = # paste private key here
|
||||
Address = {{ ip }}{% if ip6 %}, {{ ip6 }}{% endif %}
|
||||
|
|
31
web/vpn.py
31
web/vpn.py
|
@ -18,15 +18,36 @@ wgkey_regex = re.compile(r'^[A-Za-z0-9/+=]{44}$')
|
|||
def index():
|
||||
return flask.render_template('vpn/index.html')
|
||||
|
||||
@blueprint.route('/custom')
|
||||
@flask_login.login_required
|
||||
def custom():
|
||||
if not flask_login.current_user.is_admin:
|
||||
return flask.Response('forbidden', status=403, mimetype='text/plain')
|
||||
with db.locked():
|
||||
keys = {ip: data for ip, data in db.read('wireguard').items() if data.get('networks') and not data.get('user')}
|
||||
ipsets = db.read('networks') | db.read('ipsets')
|
||||
return flask.render_template('vpn/custom.html', keys=keys, ipsets=ipsets.keys())
|
||||
|
||||
@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({
|
||||
ip: data | {'active': flask.request.remote_addr in (ip, data.get('ip6'))}
|
||||
return flask.jsonify([
|
||||
data | {'ip': ip, '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('/list-custom')
|
||||
@flask_login.login_required
|
||||
def list_custom():
|
||||
# Return all custom keys.
|
||||
if not flask_login.current_user.is_admin:
|
||||
return flask.Response('forbidden', status=403, mimetype='text/plain')
|
||||
return flask.jsonify([
|
||||
data | {'ip': ip, 'active': flask.request.remote_addr in (ip, data.get('ip6'))}
|
||||
for ip, data in db.load('wireguard').items() if data.get('networks') and not data.get('user')
|
||||
])
|
||||
|
||||
@blueprint.route('/new', methods=('POST',))
|
||||
@flask_login.login_required
|
||||
|
@ -76,6 +97,9 @@ def new():
|
|||
return flask.Response('no more available IP addresses', status=500, mimetype='text/plain')
|
||||
|
||||
# Add remaining attributes to new key and update key database.
|
||||
if flask_login.current_user.is_admin and flask.request.json.get('networks'):
|
||||
key['networks'] = flask.request.json.get('networks')
|
||||
else:
|
||||
key['user'] = flask_login.current_user.get_id()
|
||||
keys[str(ip)] = key
|
||||
db.write('wireguard', keys)
|
||||
|
@ -93,6 +117,7 @@ def new():
|
|||
'ip': str(ip),
|
||||
'ip6': key['ip6'],
|
||||
'name': key['name'],
|
||||
'user': key.get('user', 'custom'),
|
||||
'dns': settings.get('wg_dns', '') if options.get('use-dns', True) else False,
|
||||
'allowed_nets': settings.get('wg_allowed_nets', ''),
|
||||
'add_default': options.get('add-default', False),
|
||||
|
|
Loading…
Reference in a new issue