Compare commits

..

No commits in common. "b6c191e2ce75ce82e07feda97e17a217c78c5d95" and "25ee4e8a442bcf828358031ba2d4d53422aa2ac5" have entirely different histories.

10 changed files with 161 additions and 321 deletions

View file

@ -1,45 +0,0 @@
# FRIwall
Web application for managing the main firewall at FRI.
## Operation
The firewall consists of two servers (or “bricks”) in active–backup configuration, using weighted BGP routes for failover. Traffic filtering and VPN are done with nftables and WireGuard. Both are configured exactly the same on both servers. The settings are managed with this application.
On each configuration change, a tarball of relevant files in `/etc` is generated and pushed via SSH to both nodes. This happens for instance each time an IP set or a forwarding rule is modified, or a VPN key is added or removed. Firewall nodes and the current configuration version for each are stored in `nodes.json`.
## IP sets
Names and IP prefixes for physical networks are configured in NetBox and stored in `networks.json`, which is never modified by the application. Custom IP sets used for forwarding rules may be defined at `/ipsets`, as well as NAT addresse and VPN access for all networks. These settings are stored in `ipsets.json` and added to static definitions in `networks.json` when generating firewall configuration.
## Rules
Forwarding rules are configured at `/rules` and toggled at `/rules/manage`. Enabled rules are included directly in the nftables configuration. For each rule, one or more manager groups can be set; users in these AD groups may toggle the rule without admin access. This is used for instance to allow teachers to disable Internet access in classrooms.
## VPN
WireGuard is used for remote access. Domain users can self‐register new keys at `/vpn`. The key database `wireguard.json` is a dictionary of entries like
"10.0.0.26": {
"key": "ABC…XYZ=",
"ip6": "aaaa:bbbb:cccc:dddd:1a::/80",
"time": 1682166836.88937,
"name": "machine1",
"user": "user@domain"
}
IP addresses for new keys are assigned automatically from the subnets defined in `wg_net` and `wg_net6` settings. When generating firewall configuration, each IP is placed in the nftables sets based on its user’s group and settings in `ipsets.json`.
### Custom keys
Administrators can define custom keys with access to specified networks at `/vpn/custom`. These keys are used to connect machines into secure networks where users are not allowed to attach arbitrary devices themselves.
Custom keys are stored in the same database, but with a `networks` entry in place of `user`, for instance
"10.0.0.27": {
"key": "FOO…BAR=",
"ip6": "aaaa:bbbb:cccc:dddd:1b::/80",
"time": 1682166836.88937,
"name": "machine2",
"networks": ["net1", "net2"]
}

View file

@ -25,7 +25,7 @@ def create_app(test_config=None):
'wg_endpoint': '', 'wg_endpoint': '',
'wg_port': '51820', 'wg_port': '51820',
'wg_allowed_nets': '', 'wg_allowed_nets': '',
'wg_dns': '', 'wg_dns': False,
'wg_key': '', 'wg_key': '',
'wg_net': '', 'wg_net': '',
'wg_net6': '', 'wg_net6': '',

View file

@ -1,92 +0,0 @@
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(endpoint, {
credentials: 'include'
})
.then(response => {
if (!response.ok)
throw new Error('fetching keys failed');
return response.json();
})
.then(data => {
update(Object.values(data));
})
.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';
});
});

View file

@ -77,24 +77,17 @@ 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
# Find networks accessible to VPN users for each AD group. # Update IP sets with VPN addresses based on AD group membership.
vpn_groups = {e['vpn'] for e in ipsets.values() if e.get('vpn')} vpn_groups = set([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', ''), ()):
key_networks |= set(group_networks.get(group, ())) for network in 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)
# Create config files. # Create config files.
output = pathlib.Path.home() / 'config' / f'{version}' output = pathlib.Path.home() / 'config' / f'{version}'

View file

@ -9,8 +9,7 @@ body {
margin: 1em auto; margin: 1em auto;
} }
code { code {
background-color: #f8f8f8; background-color: #eeeeee;
padding: 0.1em 0.25em;
} }
details { details {
margin: 0.5em 1em; margin: 0.5em 1em;
@ -32,22 +31,18 @@ input:read-only {
border-style: dotted; border-style: dotted;
} }
pre { pre {
background-color: #f8f8f8; background-color: #eeeeee;
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;
} }
tbody > tr:hover { th {
background-color: #f8f8f8; border-bottom: 1px solid black;
} }
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 oddaljeni dostop <dd>urejanje ključev za WireGuard VPN
<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,8 +20,6 @@
<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

@ -1,68 +0,0 @@
{% 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

@ -2,7 +2,10 @@
{% block content %} {% block content %}
<p> <p>
Za VPN oziroma oddeljano povezavo v omrežje FRI uporabljamo <a href="https://wireguard.com">WireGuard</a>. Za priklop v omrežje ustvarite nov ključ in prenesite izpisano datoteko. Nato sledite napotkom za posamezni sistem. Za VPN oziroma oddeljano povezavo v omrežje FRI uporabljamo <a href="https://wireguard.com">WireGuard</a>. Več informacij o uporabi in nastavitvah VPN najdete v <a href="https://doku.fri.uni-lj.si/vpn">dokumentaciji</a>.
<p>
Za priklop v omrežje spodaj ustvarite nov ključ in prenesite izpisano datoteko. Nato sledite napotkom za posamezni sistem.
<details> <details>
<summary>Windows / Mac</summary> <summary>Windows / Mac</summary>
@ -40,14 +43,14 @@ Na vsaki napravi, ki jo želite povezati v omrežje FRI, ustvarite nov ključ. P
<input id="name" name="name" pattern="[-._A-Za-z0-9 ]*" maxlength="16" placeholder="A-Z a-z 0-9 . _ - " /> <input id="name" name="name" pattern="[-._A-Za-z0-9 ]*" maxlength="16" placeholder="A-Z a-z 0-9 . _ - " />
<button id="submit" type="submit">Ustvari ključ</button> <button id="submit" type="submit">Ustvari ključ</button>
<p> <p>
<input type="checkbox" id="add-default" name="add-default" /> <input type="checkbox" id="add_default" name="add_default" />
<label for="add-default">Uporabi VPN za ves promet</label> <label for="add_default">Uporabi VPN za ves promet</label>
<br> <br>
<input type="checkbox" id="use-dns" name="use-dns" checked /> <input type="checkbox" id="use_dns" name="use_dns" checked />
<label for="use-dns">Uporabi imenske strežnike FRI</label> <label for="use_dns">Uporabi imenske strežnike FRI</label>
<p> <p>
Č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 <a href="https://doku.fri.uni-lj.si/vpn">najdemo v dokumentaciji</a>. Č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.
</form> </form>
<section id="settings" style="display: none;"> <section id="settings" style="display: none;">
@ -68,37 +71,127 @@ 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>
<table class="keys"> <p class="keys" id="active-key-warning" style="margin-top: 0;">
<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> </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"> <script type="text/javascript">
const endpoint = 'list'; function del_key(key) {
function update(keys) { fetch('del', {
const keytab = document.querySelector('table.keys > tbody'); credentials: 'include',
const warning = document.querySelector('p#active-key-warning'); method: 'POST',
keytab.innerHTML = '' headers: { 'Content-Type': 'application/json' },
warning.hidden = true; body: JSON.stringify({ pubkey: key })
for (const key of keys) { })
const row = keytab.insertRow(); .then(response => {
row.insertCell().innerHTML = '<button onclick="delKey(\'' + key.key + '\');"></button>'; if (!response.ok)
row.insertCell().innerHTML = '<code>' + key.key + '</code>'; throw new Error('deleting key failed');
row.insertCell().innerHTML = key.ip; return response.text();
row.insertCell().innerHTML = key.ip6 || ''; })
row.insertCell().innerHTML = key.name + (key.active ? '<font color="red"></font>' : ''); .then(data => {
if (key.active) // reload key list
warning.hidden = false; window.dispatchEvent(new Event('load'));
} })
document.querySelector('section.keys').style.display = (keys.length ? 'unset' : 'none'); .catch(error => console.error(error));
} }
function fetch_keys() {
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.getElementById('active-key-warning');
warning.innerHTML = '';
for (let key of Object.values(data)) {
var a = document.createElement('a');
a.innerText = '✖';
a.href = '';
a.addEventListener('click', event => {
del_key(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.innerHTML = '<font color="red"><sup></sup></font> Ta ključ uporablja trenutna povezava. Če ga odstranite, bo prekinjena.';
}
document.querySelector('section.keys').style.display = (Object.keys(data).length ? 'unset' : 'none');
})
.catch(error => console.error(error));
}
window.addEventListener('load', fetch_keys);
const request = document.getElementById('request');
request.addEventListener('submit', event => {
event.preventDefault();
const name = document.getElementById('name');
const key = wireguard.generateKeypair();
const settings = document.getElementById('settings');
const submit = document.getElementById('submit');
const use_dns = document.getElementById('use_dns');
const add_default = document.getElementById('add_default');
submit.innerHTML = 'Obdelovanje…';
submit.disabled = true;
fetch('new', {
credentials: 'include',
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
pubkey: key.publicKey,
name: name.value,
use_dns: use_dns.checked,
add_default: add_default.checked,
})
})
.then(response => {
if (!response.ok) {
response.text().then(text => {
settings.innerHTML = response.status + ' ' + response.statusText + ': ' + text;
});
} else {
return response.text();
}
})
.then(text => {
var complete = text.replace(/PrivateKey = .*/, "PrivateKey = "+key.privateKey).trim();
document.getElementById("config").innerHTML = complete;
var blob = new Blob([complete], { type: 'text/plain;charset=utf-8' });
const link = document.getElementById('download');
link.download = 'wg-fri.conf';
link.href = window.URL.createObjectURL(blob);
var qr = qrcode(0, 'L');
qr.addData(complete.replace(/#.*\n/g, ''));
qr.make();
document.getElementById('qr').innerHTML = qr.createSvgTag(3);
// reload key list
fetch_keys();
})
.catch(error => {
settings.innerHTML = error;
})
.finally(() => {
request.style.display = 'none';
settings.style.display = 'unset';
});
});
</script> </script>
{% endblock %} {% endblock %}

View file

@ -1,5 +1,5 @@
[Interface] [Interface]
# {{ timestamp }} {{ user }} {{ name }} # {{ timestamp }} {{ current_user['username'] }} {{ 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,46 +18,23 @@ 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).
user = flask_login.current_user.get_id() user = flask_login.current_user.get_id()
return flask.jsonify([ return flask.jsonify(
data | {'ip': ip, 'active': flask.request.remote_addr in (ip, data.get('ip6'))} {k: v | {'active': flask.request.remote_addr in (v.get('ip'), v.get('ip6'))}
for ip, data in db.load('wireguard').items() if data.get('user') == user for k, v in db.load('wireguard').items() if v.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
def new(): def new():
# Each key is assigned a new IPv4 address from the pool settings['wg_net']. # Each key is associated with 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. # Each key gets an 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, # 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. # the key for 10.10.0.10/32 would get 1234:5678:90ab:cdef:a::/80.
def ipv4to6(net4, ip4, net6): def ipv4to6(net4, ip4, net6):
# Calculate the address and prefix length for the IPv6 network that can be assigned to this key. # Calculate the address and prefix length for the assigned IPv6 network.
len4 = (net4.max_prefixlen - net4.prefixlen) len4 = (net4.max_prefixlen - net4.prefixlen)
len6 = (net6.max_prefixlen - net6.prefixlen) len6 = (net6.max_prefixlen - net6.prefixlen)
# Make sure the network address ends at a colon. Wastes some addresses but IPv6. # Make sure the network address ends at a colon. Wastes some addresses but IPv6.
@ -66,42 +43,35 @@ def new():
return ip6 + '/' + str(net6.max_prefixlen - assigned) return ip6 + '/' + str(net6.max_prefixlen - assigned)
pubkey = flask.request.json.get('pubkey', '') pubkey = flask.request.json.get('pubkey', '')
options = flask.request.json.get('options', {})
if not re.match(wgkey_regex, pubkey): if not re.match(wgkey_regex, pubkey):
return flask.Response('invalid key', status=400, mimetype='text/plain') 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') settings = db.load('settings')
server_pubkey = subprocess.run([f'wg pubkey'], input=settings.get('wg_key'), server_pubkey = subprocess.run([f'wg pubkey'], input=settings.get('wg_key'),
text=True, capture_output=True, shell=True).stdout.strip() 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') host = ipaddress.ip_interface(settings.get('wg_net') or '10.0.0.1/24')
key['ip6'] = None ip6 = None
with db.locked(): with db.locked():
# Find a free address for the new key. # Find a free address for the new key.
keys = db.read('wireguard') keys = db.read('wireguard')
for index, ip in enumerate(host.network.hosts(), start=1): for index, ip in enumerate(host.network.hosts(), start=1):
if ip != host.ip and str(ip) not in keys: if ip != host.ip and str(ip) not in keys:
if wg_net6 := settings.get('wg_net6'): if wg_net6 := settings.get('wg_net6'):
key['ip6'] = ipv4to6(host.network, ip, ipaddress.ip_interface(wg_net6).network) ip6 = ipv4to6(host.network, ip, ipaddress.ip_interface(wg_net6).network)
break break
else: else:
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')
now = datetime.datetime.utcnow()
name = re.sub('[^-._A-Za-z0-9]', '', flask.request.json.get('name', ''))
# Add remaining attributes to new key and update key database. keys[str(ip)] = {
if flask_login.current_user.is_admin and flask.request.json.get('networks'): 'key': pubkey,
key['networks'] = flask.request.json.get('networks') 'ip6': str(ip6) if ip6 else None,
else: 'time': now.timestamp(),
key['user'] = flask_login.current_user.get_id() 'user': flask_login.current_user.get_id(),
keys[str(ip)] = key 'name': name,
}
db.write('wireguard', keys) db.write('wireguard', keys)
# Generate a new config archive for firewall nodes. # Generate a new config archive for firewall nodes.
@ -113,14 +83,13 @@ def new():
'port': settings.get('wg_port') or '51820', 'port': settings.get('wg_port') or '51820',
'server_key': server_pubkey, 'server_key': server_pubkey,
'pubkey': pubkey, 'pubkey': pubkey,
'timestamp': now,
'ip': str(ip), 'ip': str(ip),
'ip6': key['ip6'], 'ip6': str(ip6) if ip6 else None,
'name': key['name'], 'timestamp': now,
'user': key.get('user', 'custom'), 'name': name,
'dns': settings.get('wg_dns', '') if options.get('use-dns', True) else False, 'dns': settings.get('wg_dns') if flask.request.json.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': flask.request.json.get('add_default', False),
} }
return flask.render_template('vpn/wg-fri.conf', **args) return flask.render_template('vpn/wg-fri.conf', **args)
@ -133,10 +102,7 @@ def delete():
with db.locked(): with db.locked():
user = flask_login.current_user.get_id() user = flask_login.current_user.get_id()
keys = { keys = {k: v for k, v in db.read('wireguard').items() if v.get('user') != user or v.get('key') != pubkey}
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) db.write('wireguard', keys)
system.run(system.save_config) system.run(system.save_config)