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:
Timotej Lazar 2024-07-30 10:53:57 +02:00
parent 1b26f0738a
commit 3c25cbe88a
8 changed files with 152 additions and 42 deletions

View file

@ -17,7 +17,7 @@ function delKey(key) {
} }
function fetchKeys() { function fetchKeys() {
fetch('list', { fetch(endpoint, {
credentials: 'include' credentials: 'include'
}) })
.then(response => { .then(response => {
@ -26,29 +26,7 @@ function fetchKeys() {
return response.json(); return response.json();
}) })
.then(data => { .then(data => {
const keys = document.querySelector('ul.keys'); update(Object.values(data));
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');
}) })
.catch(error => console.error(error)); .catch(error => console.error(error));
} }

View file

@ -77,14 +77,21 @@ def save_config():
settings = db.read('settings') settings = db.read('settings')
version = settings['version'] = int(settings.get('version') or '0') + 1 version = settings['version'] = int(settings.get('version') or '0') + 1
# Update IP sets with VPN addresses based on AD group membership. # Find networks accessible to VPN users for each AD group.
vpn_groups = set([e['vpn'] for e in ipsets.values() if e.get('vpn')]) vpn_groups = {e['vpn'] for e in ipsets.values() if e.get('vpn')}
group_networks = { group_networks = {
group: [name for name, data in ipsets.items() if data['vpn'] == group] for group in vpn_groups 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(): 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 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') ipsets[network]['ip'].append(f'{ip}/32')
if ip6 := key.get('ip6'): if ip6 := key.get('ip6'):
ipsets[network]['ip6'].append(ip6) ipsets[network]['ip6'].append(ip6)

View file

@ -9,7 +9,8 @@ body {
margin: 1em auto; margin: 1em auto;
} }
code { code {
background-color: #eeeeee; background-color: #f8f8f8;
padding: 0.1em 0.25em;
} }
details { details {
margin: 0.5em 1em; margin: 0.5em 1em;
@ -31,18 +32,22 @@ input:read-only {
border-style: dotted; border-style: dotted;
} }
pre { pre {
background-color: #eeeeee; background-color: #f8f8f8;
border: 1px solid #cccccc; border: 1px solid #cccccc;
padding: 0.5em; padding: 0.5em;
} }
table {
border-spacing: 0 0.1em;
}
th { th {
text-align: left; text-align: left;
} }
th, td { th, td {
padding-right: 1em; padding-right: 1em;
vertical-align: middle;
} }
th { tbody > tr:hover {
border-bottom: 1px solid black; background-color: #f8f8f8;
} }
ul.keys { ul.keys {
margin: 0 0.5em 0.5em; margin: 0 0.5em 0.5em;

View file

@ -3,7 +3,7 @@
<section> <section>
<dl> <dl>
<dt><a href="{{ url_for('vpn.index') }}">VPN</a> <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> <dt><a href="{{ url_for('rules.manage') }}">Pravila</a>
<dd>vklop / izklop pravil za požarni zid <dd>vklop / izklop pravil za požarni zid
</dl> </dl>
@ -20,6 +20,8 @@
<dd>pravila za posredovanje prometa <dd>pravila za posredovanje prometa
<dt><a href="{{ url_for('config.edit', name='netmap') }}">Netmap</a> <dt><a href="{{ url_for('config.edit', name='netmap') }}">Netmap</a>
<dd>statične 1:1 preslikave naslovov za strežniška omrežja <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> <dt><a href="{{ url_for('config.index') }}">Nastavitve</a>
<dd>nastavitve aplikacije FRIwall <dd>nastavitve aplikacije FRIwall
</dl> </dl>

View 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 %}

View file

@ -68,12 +68,37 @@ V nastavitvah lahko dodate ali odstranite vnose <code>AllowedIPs</code>. Ti dolo
<h1>Ključi</h1> <h1>Ključi</h1>
<p> <p>
Če ključa ne uporabljamo, smo ga izgubili ali so nam ga ukradli, ga tukaj odstranimo. Trenutno so registrirani ključi: Č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. <font color="red"><sup></sup></font> Ta ključ uporablja trenutna povezava. Če ga odstranite, bo prekinjena.
</section> </section>
<script type="text/javascript" src="{{ url_for('static', filename='qrcode.js') }}"></script> <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='wireguard.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='vpn.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 %} {% endblock %}

View file

@ -1,5 +1,5 @@
[Interface] [Interface]
# {{ timestamp }} {{ current_user['username'] }} {{ name }} # {{ timestamp }} {{ user }} {{ name }}
# PublicKey = {{ pubkey }} # PublicKey = {{ pubkey }}
PrivateKey = # paste private key here PrivateKey = # paste private key here
Address = {{ ip }}{% if ip6 %}, {{ ip6 }}{% endif %} Address = {{ ip }}{% if ip6 %}, {{ ip6 }}{% endif %}

View file

@ -18,15 +18,36 @@ wgkey_regex = re.compile(r'^[A-Za-z0-9/+=]{44}$')
def index(): def index():
return flask.render_template('vpn/index.html') 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') @blueprint.route('/list')
@flask_login.login_required @flask_login.login_required
def list(): def list():
# Return logged-in user’s keys, marking the key used for current connection (if any). # Return logged-in user’s keys, marking the key used for current connection (if any).
user = flask_login.current_user.get_id() user = flask_login.current_user.get_id()
return flask.jsonify({ return flask.jsonify([
ip: data | {'active': flask.request.remote_addr in (ip, data.get('ip6'))} 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 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',)) @blueprint.route('/new', methods=('POST',))
@flask_login.login_required @flask_login.login_required
@ -76,6 +97,9 @@ def new():
return flask.Response('no more available IP addresses', status=500, mimetype='text/plain') return flask.Response('no more available IP addresses', status=500, mimetype='text/plain')
# Add remaining attributes to new key and update key database. # 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() key['user'] = flask_login.current_user.get_id()
keys[str(ip)] = key keys[str(ip)] = key
db.write('wireguard', keys) db.write('wireguard', keys)
@ -93,6 +117,7 @@ def new():
'ip': str(ip), 'ip': str(ip),
'ip6': key['ip6'], 'ip6': key['ip6'],
'name': key['name'], 'name': key['name'],
'user': key.get('user', 'custom'),
'dns': settings.get('wg_dns', '') if options.get('use-dns', True) else False, 'dns': settings.get('wg_dns', '') if options.get('use-dns', True) else False,
'allowed_nets': settings.get('wg_allowed_nets', ''), 'allowed_nets': settings.get('wg_allowed_nets', ''),
'add_default': options.get('add-default', False), 'add_default': options.get('add-default', False),