mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-09-19 04:59:01 +00:00
wireguard: Handle client connections through network manager
- Any changes done directly using 'wg' command need to be redone after a reboot and disable/enable sequence. Let that duty be handled by network manager. - Handle (none) values for keys and 0 values for latest handshake from 'wg' dump command output. - Don't store public/private keys for wireguard in /var/lib. Let Network Manager deal with the storage of secrets. - Create client connections in the 'external' zone. - Show allowed IPs for each client in the main page. - Show server connection public key only for clients. We use different key pairs when connecting to each of the servers. - Separate out configuration information and status information in the show page. - Allocate IP addresses to each of the clients. Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
parent
71c7ab4a9d
commit
6e1b0a3642
@ -21,99 +21,23 @@ Configuration helper for WireGuard.
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import pathlib
|
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
PUBLIC_KEY_HELP = 'Public key for the client'
|
|
||||||
|
|
||||||
SERVER_INTERFACE = 'wg0'
|
SERVER_INTERFACE = 'wg0'
|
||||||
|
|
||||||
KEY_FOLDER = pathlib.Path('/var/lib/freedombox/wireguard')
|
|
||||||
PRIVATE_KEY_PATH = KEY_FOLDER / 'privatekey'
|
|
||||||
PUBLIC_KEY_PATH = KEY_FOLDER / 'publickey'
|
|
||||||
|
|
||||||
|
|
||||||
class InterfaceNotFoundError(Exception):
|
|
||||||
"""Exception raised when no matching interface is found."""
|
|
||||||
|
|
||||||
|
|
||||||
def parse_arguments():
|
def parse_arguments():
|
||||||
"""Return parsed command line arguments as dictionary."""
|
"""Return parsed command line arguments as dictionary."""
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
|
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
|
||||||
|
|
||||||
subparsers.add_parser('setup', help='Setup WireGuard')
|
|
||||||
subparsers.add_parser('get-info',
|
subparsers.add_parser('get-info',
|
||||||
help='Get info for each configured interface')
|
help='Get info for each configured interface')
|
||||||
|
|
||||||
add_client = subparsers.add_parser('add-client', help='Add a client')
|
|
||||||
add_client.add_argument('publickey', help=PUBLIC_KEY_HELP)
|
|
||||||
|
|
||||||
remove_client = subparsers.add_parser('remove-client',
|
|
||||||
help='Remove a client')
|
|
||||||
remove_client.add_argument('publickey', help=PUBLIC_KEY_HELP)
|
|
||||||
|
|
||||||
modify_server = subparsers.add_parser('modify-server',
|
|
||||||
help='Modify a server')
|
|
||||||
modify_server.add_argument('--endpoint', required=True,
|
|
||||||
help='Server endpoint')
|
|
||||||
modify_server.add_argument('--client-ip', required=True,
|
|
||||||
help='Client IP address provided by server')
|
|
||||||
modify_server.add_argument('--public-key', required=True,
|
|
||||||
help='Public key of the server')
|
|
||||||
modify_server.add_argument('--pre-shared-key', help='Pre-shared key')
|
|
||||||
modify_server.add_argument(
|
|
||||||
'--all-outgoing', action='store_true',
|
|
||||||
help='Use this connection to send all outgoing traffic')
|
|
||||||
|
|
||||||
remove_server = subparsers.add_parser('remove-server',
|
|
||||||
help='Remove a server')
|
|
||||||
remove_server.add_argument('publickey', help=PUBLIC_KEY_HELP)
|
|
||||||
|
|
||||||
subparsers.required = True
|
subparsers.required = True
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
def _generate_key_pair():
|
|
||||||
"""Generate private/public key pair."""
|
|
||||||
private_key = subprocess.check_output(['wg', 'genkey'])
|
|
||||||
public_key = subprocess.check_output(['wg', 'pubkey'], input=private_key)
|
|
||||||
KEY_FOLDER.mkdir(parents=True, exist_ok=True)
|
|
||||||
with PUBLIC_KEY_PATH.open(mode='wb') as public_key_file:
|
|
||||||
public_key_file.write(public_key)
|
|
||||||
|
|
||||||
old_umask = os.umask(0o077)
|
|
||||||
try:
|
|
||||||
with PRIVATE_KEY_PATH.open(mode='wb') as private_key_file:
|
|
||||||
private_key_file.write(private_key)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
os.umask(old_umask)
|
|
||||||
|
|
||||||
|
|
||||||
def subcommand_setup(_):
|
|
||||||
"""Setup WireGuard."""
|
|
||||||
# Create interface.
|
|
||||||
try:
|
|
||||||
subprocess.run(['ip', 'link', 'show', SERVER_INTERFACE],
|
|
||||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
||||||
check=True)
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
subprocess.run([
|
|
||||||
'ip', 'link', 'add', 'dev', SERVER_INTERFACE, 'type', 'wireguard'
|
|
||||||
], check=True)
|
|
||||||
|
|
||||||
if not (PUBLIC_KEY_PATH.exists() and PRIVATE_KEY_PATH.exists()):
|
|
||||||
_generate_key_pair()
|
|
||||||
|
|
||||||
# Configure interface.
|
|
||||||
subprocess.run([
|
|
||||||
'wg', 'set', SERVER_INTERFACE, 'listen-port', '51820', 'private-key',
|
|
||||||
str(PRIVATE_KEY_PATH)
|
|
||||||
], check=True)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_info():
|
def _get_info():
|
||||||
"""Return info for each configured interface."""
|
"""Return info for each configured interface."""
|
||||||
output = subprocess.check_output(['wg', 'show', 'all',
|
output = subprocess.check_output(['wg', 'show', 'all',
|
||||||
@ -125,14 +49,16 @@ def _get_info():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
fields = line.split()
|
fields = line.split()
|
||||||
|
fields = [field if field != '(none)' else None for field in fields]
|
||||||
interface_name = fields[0]
|
interface_name = fields[0]
|
||||||
if interface_name in interfaces:
|
if interface_name in interfaces:
|
||||||
|
latest_handshake = int(fields[5]) if int(fields[5]) else None
|
||||||
peer = {
|
peer = {
|
||||||
'public_key': fields[1],
|
'public_key': fields[1],
|
||||||
'preshared_key': fields[2],
|
'preshared_key': fields[2],
|
||||||
'endpoint': fields[3],
|
'endpoint': fields[3],
|
||||||
'allowed_ips': fields[4],
|
'allowed_ips': fields[4],
|
||||||
'latest_handshake': fields[5],
|
'latest_handshake': latest_handshake,
|
||||||
'transfer_rx': fields[6],
|
'transfer_rx': fields[6],
|
||||||
'transfer_tx': fields[7],
|
'transfer_tx': fields[7],
|
||||||
'persistent_keepalive': fields[8],
|
'persistent_keepalive': fields[8],
|
||||||
@ -157,20 +83,6 @@ def subcommand_get_info(_):
|
|||||||
print(json.dumps(_get_info()))
|
print(json.dumps(_get_info()))
|
||||||
|
|
||||||
|
|
||||||
def subcommand_add_client(arguments):
|
|
||||||
"""Add a client."""
|
|
||||||
subprocess.run(
|
|
||||||
['wg', 'set', SERVER_INTERFACE, 'peer', arguments.publickey],
|
|
||||||
check=True)
|
|
||||||
|
|
||||||
|
|
||||||
def subcommand_remove_client(arguments):
|
|
||||||
"""Remove a client."""
|
|
||||||
subprocess.run(
|
|
||||||
['wg', 'set', SERVER_INTERFACE, 'peer', arguments.publickey, 'remove'],
|
|
||||||
check=True)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Parse arguments and perform all duties."""
|
"""Parse arguments and perform all duties."""
|
||||||
arguments = parse_arguments()
|
arguments = parse_arguments()
|
||||||
|
|||||||
@ -18,13 +18,9 @@
|
|||||||
FreedomBox app for wireguard.
|
FreedomBox app for wireguard.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import datetime
|
|
||||||
import json
|
|
||||||
|
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from plinth import actions
|
|
||||||
from plinth import app as app_module
|
from plinth import app as app_module
|
||||||
from plinth import cfg, frontpage, menu
|
from plinth import cfg, frontpage, menu
|
||||||
from plinth.modules.firewall.components import Firewall
|
from plinth.modules.firewall.components import Firewall
|
||||||
@ -102,98 +98,4 @@ def init():
|
|||||||
def setup(helper, old_version=None):
|
def setup(helper, old_version=None):
|
||||||
"""Install and configure the module."""
|
"""Install and configure the module."""
|
||||||
helper.install(managed_packages)
|
helper.install(managed_packages)
|
||||||
helper.call('post', actions.superuser_run, 'wireguard', ['setup'])
|
|
||||||
helper.call('post', app.enable)
|
helper.call('post', app.enable)
|
||||||
|
|
||||||
|
|
||||||
def get_public_key():
|
|
||||||
"""Return this box's public key."""
|
|
||||||
public_key_path = '/var/lib/freedombox/wireguard/publickey'
|
|
||||||
try:
|
|
||||||
with open(public_key_path) as public_key_file:
|
|
||||||
public_key = public_key_file.read().strip()
|
|
||||||
|
|
||||||
except FileNotFoundError:
|
|
||||||
public_key = None
|
|
||||||
|
|
||||||
return public_key
|
|
||||||
|
|
||||||
|
|
||||||
def get_nm_info():
|
|
||||||
"""Get information from network manager."""
|
|
||||||
client = nm.Client.new(None)
|
|
||||||
|
|
||||||
connections = {}
|
|
||||||
for connection in client.get_connections():
|
|
||||||
if connection.get_connection_type() != 'wireguard':
|
|
||||||
continue
|
|
||||||
|
|
||||||
settings = connection.get_setting_by_name('wireguard')
|
|
||||||
secrets = connection.get_secrets('wireguard')
|
|
||||||
connection.update_secrets('wireguard', secrets)
|
|
||||||
|
|
||||||
info = {}
|
|
||||||
info['interface'] = connection.get_interface_name()
|
|
||||||
info['private_key'] = settings.get_private_key()
|
|
||||||
info['listen_port'] = settings.get_listen_port()
|
|
||||||
info['fwmark'] = settings.get_fwmark()
|
|
||||||
info['mtu'] = settings.get_mtu()
|
|
||||||
info['default_route'] = settings.get_ip4_auto_default_route()
|
|
||||||
info['peers'] = []
|
|
||||||
for peer_index in range(settings.get_peers_len()):
|
|
||||||
peer = settings.get_peer(peer_index)
|
|
||||||
peer_info = {
|
|
||||||
'endpoint': peer.get_endpoint(),
|
|
||||||
'public_key': peer.get_public_key(),
|
|
||||||
'preshared_key': peer.get_preshared_key(),
|
|
||||||
'persistent_keepalive': peer.get_persistent_keepalive(),
|
|
||||||
'allowed_ips': []
|
|
||||||
}
|
|
||||||
for index in range(peer.get_allowed_ips_len()):
|
|
||||||
allowed_ip = peer.get_allowed_ip(index, None)
|
|
||||||
peer_info['allowed_ips'].append(allowed_ip)
|
|
||||||
|
|
||||||
info['peers'].append(peer_info)
|
|
||||||
|
|
||||||
settings_ipv4 = connection.get_setting_ip4_config()
|
|
||||||
if settings_ipv4 and settings_ipv4.get_num_addresses():
|
|
||||||
info['ip_address'] = settings_ipv4.get_address(0).get_address()
|
|
||||||
|
|
||||||
connections[info['interface']] = info
|
|
||||||
|
|
||||||
return connections
|
|
||||||
|
|
||||||
|
|
||||||
def get_info():
|
|
||||||
"""Return server and clients info."""
|
|
||||||
output = actions.superuser_run('wireguard', ['get-info'])
|
|
||||||
status = json.loads(output)
|
|
||||||
|
|
||||||
nm_info = get_nm_info()
|
|
||||||
|
|
||||||
my_server_info = status.pop(SERVER_INTERFACE, {})
|
|
||||||
my_client_servers = {}
|
|
||||||
|
|
||||||
for interface, info in nm_info.items():
|
|
||||||
my_client_servers[interface] = info
|
|
||||||
|
|
||||||
if interface not in status:
|
|
||||||
continue
|
|
||||||
|
|
||||||
for info_peer in info['peers']:
|
|
||||||
for status_peer in status[interface]['peers']:
|
|
||||||
if info_peer['public_key'] == status_peer['public_key']:
|
|
||||||
info_peer['status'] = status_peer
|
|
||||||
status_peer['latest_handshake'] = \
|
|
||||||
datetime.datetime.fromtimestamp(
|
|
||||||
int(status_peer['latest_handshake']))
|
|
||||||
|
|
||||||
return {
|
|
||||||
'my_server': {
|
|
||||||
'public_key': my_server_info.get('public_key'),
|
|
||||||
'clients': my_server_info.get('peers'),
|
|
||||||
},
|
|
||||||
'my_client': {
|
|
||||||
'servers': my_client_servers,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|||||||
@ -64,7 +64,7 @@ class AddServerForm(forms.Form):
|
|||||||
settings = {
|
settings = {
|
||||||
'common': {
|
'common': {
|
||||||
'type': 'wireguard',
|
'type': 'wireguard',
|
||||||
'zone': 'internal',
|
'zone': 'external',
|
||||||
},
|
},
|
||||||
'ipv4': {
|
'ipv4': {
|
||||||
'method': 'manual',
|
'method': 'manual',
|
||||||
|
|||||||
@ -30,18 +30,22 @@
|
|||||||
id="server-peers-list">
|
id="server-peers-list">
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Public Key" %}</th>
|
<th>{% trans "Public Key" %}</th>
|
||||||
|
<th>{% trans "Allowed IPs" %}</th>
|
||||||
<th>{% trans "Last Connected Time" %}</th>
|
<th>{% trans "Last Connected Time" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
{% if server_peers %}
|
{% if server.peers %}
|
||||||
{% for peer in server_peers %}
|
{% for peer in server.peers.values %}
|
||||||
<tr>
|
{% if peer.public_key %}
|
||||||
<td>
|
<tr>
|
||||||
<a href="{% url 'wireguard:show-client' peer.public_key|urlencode:'' %}">
|
<td>
|
||||||
{{ peer.public_key }}
|
<a href="{% url 'wireguard:show-client' peer.public_key|urlencode:'' %}">
|
||||||
</a>
|
{{ peer.public_key }}
|
||||||
</td>
|
</a>
|
||||||
<td>{{ peer.latest_handshake }}</td>
|
</td>
|
||||||
</tr>
|
<td>{{ peer.allowed_ips|join:", " }}</td>
|
||||||
|
<td>{{ peer.status.latest_handshake|default:'' }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr>
|
<tr>
|
||||||
@ -54,6 +58,13 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{% blocktrans %}
|
||||||
|
Public key for this {{ box_name }}:
|
||||||
|
{% endblocktrans %}
|
||||||
|
<pre>{{ server.public_key }}</pre>
|
||||||
|
</p>
|
||||||
|
|
||||||
<a title="{% trans 'Add a new peer' %}"
|
<a title="{% trans 'Add a new peer' %}"
|
||||||
role="button" class="btn btn-default"
|
role="button" class="btn btn-default"
|
||||||
href="{% url 'wireguard:add-client' %}">
|
href="{% url 'wireguard:add-client' %}">
|
||||||
@ -72,15 +83,19 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{% if client_peers %}
|
{% if client_peers %}
|
||||||
{% for interface, server in client_peers.items %}
|
{% for interface, server in client_peers.items %}
|
||||||
<tr>
|
{% for peer in server.peers.values %}
|
||||||
<td>{{ server.peers.0.endpoint }}</td>
|
{% if forloop.first %}
|
||||||
<td>
|
<tr>
|
||||||
<a href="{% url 'wireguard:show-server' interface %}">
|
<td>{{ peer.endpoint }}</td>
|
||||||
{{ server.peers.0.public_key }}
|
<td>
|
||||||
</a>
|
<a href="{% url 'wireguard:show-server' interface %}">
|
||||||
</td>
|
{{ peer.public_key }}
|
||||||
<td>{{ server.peers.0.status.latest_handshake }}</td>
|
</a>
|
||||||
</tr>
|
</td>
|
||||||
|
<td>{{ peer.status.latest_handshake|default:'' }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr>
|
<tr>
|
||||||
@ -100,16 +115,6 @@
|
|||||||
{% trans "Add Server" %}
|
{% trans "Add Server" %}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<h3>{% trans "Connection Information" %}</h3>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
{% blocktrans %}
|
|
||||||
Public key for this {{ box_name }}:
|
|
||||||
{% endblocktrans %}
|
|
||||||
|
|
||||||
<pre>{{ public_key }}</pre>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -22,23 +22,58 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<h3>{{ title }}</h3>
|
<h3>{% trans "Connection Information" %}</h3>
|
||||||
|
<table class="table table-bordered table-condensed table-striped">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Client public key:" %}</th>
|
||||||
|
<td>{{ client.public_key }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "IP address to use:" %}</th>
|
||||||
|
<td>{{ client.allowed_ips|join:", " }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Pre-shared key:" %}</th>
|
||||||
|
<td>{{ client.preshared_key }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Server endpoints:" %}</th>
|
||||||
|
<td>
|
||||||
|
{% for endpoint in endpoints %}
|
||||||
|
<div>{{ endpoint }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Server's public key:" %}</th>
|
||||||
|
<td>{{ server.public_key }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
<h4>{% trans "Connection Information" %}</h4>
|
<h3>{% trans "Status" %}</h3>
|
||||||
<p>{% trans "IP address to use:" %}</p>
|
<table class="table table-bordered table-condensed table-striped">
|
||||||
<p>{% trans "Server endpoints:" %}</p>
|
<tbody>
|
||||||
<p>{% trans "Server's public key:" %} {{ my_server.public_key }}</p>
|
<tr>
|
||||||
<p>{% trans "Pre-shared key:" %}</p>
|
<th>{% trans "Data transmitted:" %}</th>
|
||||||
|
<td>{{ client.status.transfer_tx|filesizeformat }}</td>
|
||||||
<h4>{% trans "Status" %}</h4>
|
</tr>
|
||||||
<p>{% trans "Client Public Key:" %} {{ client.public_key }}</p>
|
<tr>
|
||||||
<p>{% trans "Data transmitted:" %} {{ client.transfer_tx|filesizeformat }}</p>
|
<th>{% trans "Data received:" %}</th>
|
||||||
<p>{% trans "Data received:" %} {{ client.transfer_rx|filesizeformat }}</p>
|
<td>{{ client.status.transfer_rx|filesizeformat }}</td>
|
||||||
<p>{% trans "Latest handshake:" %} {{ client.latest_handshake }}</p>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans "Latest handshake:" %}</th>
|
||||||
|
<td>{{ client.status.latest_handshake|default:'' }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a class="btn btn-default"
|
<a class="btn btn-default"
|
||||||
href="{% url 'wireguard:edit-client' client.public_key|urlencode:'' %}">
|
href="{% url 'wireguard:edit-client' client.public_key|urlencode:'' %}">
|
||||||
|
<span class="fa fa-pencil-square-o" aria-hidden="true"></span>
|
||||||
{% trans "Edit Client" %}
|
{% trans "Edit Client" %}
|
||||||
</a>
|
</a>
|
||||||
<a class="btn btn-default"
|
<a class="btn btn-default"
|
||||||
|
|||||||
@ -26,30 +26,34 @@
|
|||||||
|
|
||||||
<table class="table table-bordered table-condensed table-striped">
|
<table class="table table-bordered table-condensed table-striped">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
{% for peer in server.peers.values %}
|
||||||
<th>{% trans "Endpoint:" %}</th>
|
{% if forloop.first %}
|
||||||
<td>{{ server.peers.0.endpoint }}</td>
|
<tr>
|
||||||
</tr>
|
<th>{% trans "Endpoint:" %}</th>
|
||||||
<tr>
|
<td>{{ peer.endpoint }}</td>
|
||||||
<th>{% trans "Public Key:" %}</th>
|
</tr>
|
||||||
<td>{{ server.peers.0.public_key }}</td>
|
<tr>
|
||||||
</tr>
|
<th>{% trans "Public Key:" %}</th>
|
||||||
<tr>
|
<td>{{ peer.public_key }}</td>
|
||||||
<th>{% trans "Pre-shared key:" %}</th>
|
</tr>
|
||||||
<td>{{ server.peers.0.preshared_key }}</td>
|
<tr>
|
||||||
</tr>
|
<th>{% trans "Pre-shared key:" %}</th>
|
||||||
<tr>
|
<td>{{ peer.preshared_key }}</td>
|
||||||
<th>{% trans "Data transmitted:" %}</th>
|
</tr>
|
||||||
<td>{{ server.peers.0.status.transfer_tx|filesizeformat }}</td>
|
<tr>
|
||||||
</tr>
|
<th>{% trans "Data transmitted:" %}</th>
|
||||||
<tr>
|
<td>{{ peer.status.transfer_tx|filesizeformat }}</td>
|
||||||
<th>{% trans "Data received:" %}</th>
|
</tr>
|
||||||
<td>{{ server.peers.0.status.transfer_rx|filesizeformat }}</td>
|
<tr>
|
||||||
</tr>
|
<th>{% trans "Data received:" %}</th>
|
||||||
<tr>
|
<td>{{ peer.status.transfer_rx|filesizeformat }}</td>
|
||||||
<th>{% trans "Latest handshake:" %}</th>
|
</tr>
|
||||||
<td>{{ server.peers.0.status.latest_handshake }}</td>
|
<tr>
|
||||||
</tr>
|
<th>{% trans "Latest handshake:" %}</th>
|
||||||
|
<td>{{ peer.status.latest_handshake|default:'' }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|||||||
@ -18,9 +18,100 @@
|
|||||||
Utilities for managing WireGuard.
|
Utilities for managing WireGuard.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import time
|
||||||
|
|
||||||
from plinth import network
|
from plinth import actions, network
|
||||||
|
from plinth.utils import import_from_gi
|
||||||
|
|
||||||
|
nm = import_from_gi('NM', '1.0')
|
||||||
|
|
||||||
|
IP_TEMPLATE = '10.84.0.{}'
|
||||||
|
WIREGUARD_SETTING = nm.SETTING_WIREGUARD_SETTING_NAME
|
||||||
|
|
||||||
|
|
||||||
|
def get_nm_info():
|
||||||
|
"""Get information from network manager."""
|
||||||
|
client = network.get_nm_client()
|
||||||
|
|
||||||
|
connections = {}
|
||||||
|
for connection in client.get_connections():
|
||||||
|
if connection.get_connection_type() != WIREGUARD_SETTING:
|
||||||
|
continue
|
||||||
|
|
||||||
|
settings = connection.get_setting_by_name(WIREGUARD_SETTING)
|
||||||
|
secrets = connection.get_secrets(WIREGUARD_SETTING)
|
||||||
|
connection.update_secrets(WIREGUARD_SETTING, secrets)
|
||||||
|
|
||||||
|
info = {}
|
||||||
|
info['interface'] = connection.get_interface_name()
|
||||||
|
info['private_key'] = settings.get_private_key()
|
||||||
|
info['public_key'] = None
|
||||||
|
info['listen_port'] = settings.get_listen_port()
|
||||||
|
info['fwmark'] = settings.get_fwmark()
|
||||||
|
info['mtu'] = settings.get_mtu()
|
||||||
|
info['default_route'] = settings.get_ip4_auto_default_route()
|
||||||
|
info['peers'] = {}
|
||||||
|
for peer_index in range(settings.get_peers_len()):
|
||||||
|
peer = settings.get_peer(peer_index)
|
||||||
|
peer_info = {
|
||||||
|
'endpoint': peer.get_endpoint(),
|
||||||
|
'public_key': peer.get_public_key(),
|
||||||
|
'preshared_key': peer.get_preshared_key(),
|
||||||
|
'persistent_keepalive': peer.get_persistent_keepalive(),
|
||||||
|
'allowed_ips': []
|
||||||
|
}
|
||||||
|
for index in range(peer.get_allowed_ips_len()):
|
||||||
|
allowed_ip = peer.get_allowed_ip(index, None)
|
||||||
|
peer_info['allowed_ips'].append(allowed_ip)
|
||||||
|
|
||||||
|
info['peers'][peer_info['public_key']] = peer_info
|
||||||
|
|
||||||
|
settings_ipv4 = connection.get_setting_ip4_config()
|
||||||
|
if settings_ipv4 and settings_ipv4.get_num_addresses():
|
||||||
|
info['ip_address'] = settings_ipv4.get_address(0).get_address()
|
||||||
|
|
||||||
|
connections[info['interface']] = info
|
||||||
|
|
||||||
|
return connections
|
||||||
|
|
||||||
|
|
||||||
|
def get_info():
|
||||||
|
"""Return server and clients info."""
|
||||||
|
output = actions.superuser_run('wireguard', ['get-info'])
|
||||||
|
status = json.loads(output)
|
||||||
|
|
||||||
|
nm_info = get_nm_info()
|
||||||
|
|
||||||
|
my_server_info = None
|
||||||
|
my_client_servers = {}
|
||||||
|
for interface, info in nm_info.items():
|
||||||
|
if interface == 'wg0':
|
||||||
|
my_server_info = info
|
||||||
|
else:
|
||||||
|
my_client_servers[interface] = info
|
||||||
|
|
||||||
|
if interface not in status:
|
||||||
|
continue
|
||||||
|
|
||||||
|
info['public_key'] = status[interface]['public_key']
|
||||||
|
for status_peer in status[interface]['peers']:
|
||||||
|
if status_peer['latest_handshake']:
|
||||||
|
status_peer['latest_handshake'] = \
|
||||||
|
datetime.datetime.fromtimestamp(
|
||||||
|
status_peer['latest_handshake'])
|
||||||
|
public_key = status_peer['public_key']
|
||||||
|
info_peer = info['peers'].setdefault(public_key, {})
|
||||||
|
info_peer['status'] = status_peer
|
||||||
|
|
||||||
|
return {
|
||||||
|
'my_server': my_server_info,
|
||||||
|
'my_client': {
|
||||||
|
'servers': my_client_servers,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def find_next_interface():
|
def find_next_interface():
|
||||||
@ -38,8 +129,106 @@ def find_next_interface():
|
|||||||
|
|
||||||
|
|
||||||
def add_server(settings):
|
def add_server(settings):
|
||||||
"""Add a server."""
|
"""Add information for connecting to a server."""
|
||||||
interface_name = find_next_interface()
|
interface_name = find_next_interface()
|
||||||
settings['common']['name'] = 'WireGuard-' + interface_name
|
settings['common']['name'] = 'WireGuard-Client-' + interface_name
|
||||||
settings['common']['interface'] = interface_name
|
settings['common']['interface'] = interface_name
|
||||||
network.add_connection(settings)
|
network.add_connection(settings)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_server():
|
||||||
|
"""Setup a server connection that clients can connect to."""
|
||||||
|
process = subprocess.run(['wg', 'genkey'], check=True, capture_output=True)
|
||||||
|
private_key = process.stdout.decode().strip()
|
||||||
|
settings = {
|
||||||
|
'common': {
|
||||||
|
'name': 'WireGuard-Server-wg0',
|
||||||
|
'type': WIREGUARD_SETTING,
|
||||||
|
'zone': 'internal',
|
||||||
|
'interface': 'wg0'
|
||||||
|
},
|
||||||
|
'ipv4': {
|
||||||
|
'method': 'manual',
|
||||||
|
'address': IP_TEMPLATE.format(1),
|
||||||
|
'netmask': '255.255.255.0',
|
||||||
|
'gateway': '',
|
||||||
|
'dns': '',
|
||||||
|
'second_dns': '',
|
||||||
|
},
|
||||||
|
'wireguard': {
|
||||||
|
'private_key': private_key,
|
||||||
|
'listen_port': 51820,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
network.add_connection(settings)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_next_available_ip_address(settings):
|
||||||
|
"""Get the next available IP address to allocate to a client."""
|
||||||
|
allocated_ips = set()
|
||||||
|
for peer_index in range(settings.get_peers_len()):
|
||||||
|
peer = settings.get_peer(peer_index)
|
||||||
|
for ip_index in range(peer.get_allowed_ips_len()):
|
||||||
|
allowed_ip = peer.get_allowed_ip(ip_index)
|
||||||
|
# We assume these are simple IP addresses but they can be subnets.
|
||||||
|
allocated_ips.add(allowed_ip)
|
||||||
|
|
||||||
|
for index in range(2, 254):
|
||||||
|
ip_address = IP_TEMPLATE.format(index)
|
||||||
|
if ip_address not in allocated_ips:
|
||||||
|
return ip_address
|
||||||
|
|
||||||
|
raise IndexError('Reached client limit')
|
||||||
|
|
||||||
|
|
||||||
|
def _server_connection():
|
||||||
|
"""Return a server connection. Create one if necessary."""
|
||||||
|
connection = network.get_connection_by_interface_name('wg0')
|
||||||
|
if not connection:
|
||||||
|
setup_server()
|
||||||
|
|
||||||
|
for _ in range(10):
|
||||||
|
# XXX: Improve this waiting by doing a synchronous D-Bus operation to
|
||||||
|
# add network manager connection instead.
|
||||||
|
time.sleep(1)
|
||||||
|
connection = network.get_connection_by_interface_name('wg0')
|
||||||
|
if connection:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not connection:
|
||||||
|
raise RuntimeError('Unable to create a server connection.')
|
||||||
|
|
||||||
|
# Retrieve secrets so that when the connection is changed, secrets are
|
||||||
|
# preserved properly.
|
||||||
|
secrets = connection.get_secrets(WIREGUARD_SETTING)
|
||||||
|
connection.update_secrets(WIREGUARD_SETTING, secrets)
|
||||||
|
|
||||||
|
return connection
|
||||||
|
|
||||||
|
|
||||||
|
def add_client(public_key):
|
||||||
|
"""Add a permission for a client to connect our server."""
|
||||||
|
connection = _server_connection()
|
||||||
|
settings = connection.get_setting_by_name(WIREGUARD_SETTING)
|
||||||
|
peer, _ = settings.get_peer_by_public_key(public_key)
|
||||||
|
if peer:
|
||||||
|
raise ValueError('Peer with public key already exists')
|
||||||
|
|
||||||
|
peer = nm.WireGuardPeer.new()
|
||||||
|
peer.set_public_key(public_key, False)
|
||||||
|
peer.set_persistent_keepalive(25) # To keep NAT 'connections' alive
|
||||||
|
peer.append_allowed_ip(_get_next_available_ip_address(settings), False)
|
||||||
|
settings.append_peer(peer)
|
||||||
|
connection.commit_changes(True)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_client(public_key):
|
||||||
|
"""Remove permission for a client to connect our server."""
|
||||||
|
connection = _server_connection()
|
||||||
|
settings = connection.get_setting_by_name(WIREGUARD_SETTING)
|
||||||
|
peer, peer_index = settings.get_peer_by_public_key(public_key)
|
||||||
|
if not peer:
|
||||||
|
raise KeyError('Client not found')
|
||||||
|
|
||||||
|
settings.remove_peer(peer_index)
|
||||||
|
connection.commit_changes(True)
|
||||||
|
|||||||
@ -29,7 +29,8 @@ from django.utils.translation import ugettext as _
|
|||||||
from django.views.generic import FormView, TemplateView
|
from django.views.generic import FormView, TemplateView
|
||||||
|
|
||||||
import plinth.modules.wireguard as wireguard
|
import plinth.modules.wireguard as wireguard
|
||||||
from plinth import actions, network
|
from plinth import network
|
||||||
|
from plinth.modules.names.components import DomainName
|
||||||
from plinth.views import AppView
|
from plinth.views import AppView
|
||||||
|
|
||||||
from . import forms, utils
|
from . import forms, utils
|
||||||
@ -49,9 +50,8 @@ class WireguardView(AppView):
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""Return additional context for rendering the template."""
|
"""Return additional context for rendering the template."""
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['public_key'] = wireguard.get_public_key()
|
info = utils.get_info()
|
||||||
info = wireguard.get_info()
|
context['server'] = info['my_server']
|
||||||
context['server_peers'] = info['my_server']['clients']
|
|
||||||
context['client_peers'] = info['my_client']['servers']
|
context['client_peers'] = info['my_client']['servers']
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@ -72,7 +72,13 @@ class AddClientView(SuccessMessageMixin, FormView):
|
|||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
"""Add the client."""
|
"""Add the client."""
|
||||||
public_key = form.cleaned_data.get('public_key')
|
public_key = form.cleaned_data.get('public_key')
|
||||||
actions.superuser_run('wireguard', ['add-client', public_key])
|
try:
|
||||||
|
utils.add_client(public_key)
|
||||||
|
except ValueError:
|
||||||
|
messages.warning(self.request,
|
||||||
|
_('Client with public key already exists'))
|
||||||
|
return redirect('wireguard:index')
|
||||||
|
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
@ -86,12 +92,17 @@ class ShowClientView(SuccessMessageMixin, TemplateView):
|
|||||||
context['title'] = _('Show Client')
|
context['title'] = _('Show Client')
|
||||||
|
|
||||||
public_key = urllib.parse.unquote(self.kwargs['public_key'])
|
public_key = urllib.parse.unquote(self.kwargs['public_key'])
|
||||||
info = wireguard.get_info()
|
server_info = utils.get_info()['my_server']
|
||||||
context.update(info)
|
if not server_info or public_key not in server_info['peers']:
|
||||||
for client in info['my_server']['clients']:
|
raise Http404
|
||||||
if client['public_key'] == public_key:
|
|
||||||
context['client'] = client
|
|
||||||
|
|
||||||
|
domains = DomainName.list_names(filter_for_service='wireguard')
|
||||||
|
context['server'] = server_info
|
||||||
|
context['client'] = server_info['peers'][public_key]
|
||||||
|
context['endpoints'] = [
|
||||||
|
domain + ':' + str(server_info['listen_port'])
|
||||||
|
for domain in domains
|
||||||
|
]
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@ -117,10 +128,17 @@ class EditClientView(SuccessMessageMixin, FormView):
|
|||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
"""Update the client."""
|
"""Update the client."""
|
||||||
old_public_key = form.initial['public_key']
|
old_public_key = form.initial['public_key']
|
||||||
actions.superuser_run('wireguard', ['remove-client', old_public_key])
|
|
||||||
|
|
||||||
public_key = form.cleaned_data.get('public_key')
|
public_key = form.cleaned_data.get('public_key')
|
||||||
actions.superuser_run('wireguard', ['add-client', public_key])
|
|
||||||
|
if old_public_key != public_key:
|
||||||
|
try:
|
||||||
|
utils.add_client(public_key)
|
||||||
|
except ValueError:
|
||||||
|
messages.warning(self.request,
|
||||||
|
_('Client with public key already exists'))
|
||||||
|
|
||||||
|
utils.remove_client(old_public_key)
|
||||||
|
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
@ -138,8 +156,12 @@ class DeleteClientView(SuccessMessageMixin, TemplateView):
|
|||||||
def post(self, request, public_key):
|
def post(self, request, public_key):
|
||||||
"""Delete the client."""
|
"""Delete the client."""
|
||||||
public_key = urllib.parse.unquote(public_key)
|
public_key = urllib.parse.unquote(public_key)
|
||||||
actions.superuser_run('wireguard', ['remove-client', public_key])
|
try:
|
||||||
messages.success(request, _('Client deleted.'))
|
utils.remove_client(public_key)
|
||||||
|
messages.success(request, _('Client deleted.'))
|
||||||
|
except KeyError:
|
||||||
|
messages.error(request, _('Client not found'))
|
||||||
|
|
||||||
return redirect('wireguard:index')
|
return redirect('wireguard:index')
|
||||||
|
|
||||||
|
|
||||||
@ -172,7 +194,7 @@ class ShowServerView(SuccessMessageMixin, TemplateView):
|
|||||||
context['title'] = _('Server Information')
|
context['title'] = _('Server Information')
|
||||||
|
|
||||||
interface = self.kwargs['interface']
|
interface = self.kwargs['interface']
|
||||||
info = wireguard.get_info()
|
info = utils.get_info()
|
||||||
server = info['my_client']['servers'].get(interface)
|
server = info['my_client']['servers'].get(interface)
|
||||||
if not server:
|
if not server:
|
||||||
raise Http404
|
raise Http404
|
||||||
@ -199,14 +221,14 @@ class EditServerView(SuccessMessageMixin, FormView):
|
|||||||
"""Get initial form data."""
|
"""Get initial form data."""
|
||||||
initial = super().get_initial()
|
initial = super().get_initial()
|
||||||
interface = self.kwargs['interface']
|
interface = self.kwargs['interface']
|
||||||
info = wireguard.get_nm_info()
|
info = utils.get_nm_info()
|
||||||
server = info.get(interface)
|
server = info.get(interface)
|
||||||
if not server:
|
if not server:
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
initial['ip_address'] = server.get('ip_address')
|
initial['ip_address'] = server.get('ip_address')
|
||||||
if server['peers']:
|
if server['peers']:
|
||||||
peer = server['peers'][0]
|
peer = next(peer for peer in server['peers'].values())
|
||||||
initial['peer_endpoint'] = peer['endpoint']
|
initial['peer_endpoint'] = peer['endpoint']
|
||||||
initial['peer_public_key'] = peer['public_key']
|
initial['peer_public_key'] = peer['public_key']
|
||||||
initial['private_key'] = server['private_key']
|
initial['private_key'] = server['private_key']
|
||||||
@ -220,7 +242,7 @@ class EditServerView(SuccessMessageMixin, FormView):
|
|||||||
settings = form.get_settings()
|
settings = form.get_settings()
|
||||||
interface = self.kwargs['interface']
|
interface = self.kwargs['interface']
|
||||||
settings['common']['interface'] = interface
|
settings['common']['interface'] = interface
|
||||||
settings['common']['name'] = 'WireGuard-' + interface
|
settings['common']['name'] = 'WireGuard-Client-' + interface
|
||||||
connection = network.get_connection_by_interface_name(interface)
|
connection = network.get_connection_by_interface_name(interface)
|
||||||
network.edit_connection(connection, settings)
|
network.edit_connection(connection, settings)
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
@ -236,14 +258,14 @@ class DeleteServerView(SuccessMessageMixin, TemplateView):
|
|||||||
context['title'] = _('Delete Server')
|
context['title'] = _('Delete Server')
|
||||||
|
|
||||||
interface = self.kwargs['interface']
|
interface = self.kwargs['interface']
|
||||||
info = wireguard.get_nm_info()
|
info = utils.get_nm_info()
|
||||||
server = info.get(interface)
|
server = info.get(interface)
|
||||||
if not server:
|
if not server:
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
context['interface'] = interface
|
context['interface'] = interface
|
||||||
if server['peers']:
|
if server['peers']:
|
||||||
peer = server['peers'][0]
|
peer = next(peer for peer in server['peers'].values())
|
||||||
context['peer_endpoint'] = peer['endpoint']
|
context['peer_endpoint'] = peer['endpoint']
|
||||||
context['peer_public_key'] = peer['public_key']
|
context['peer_public_key'] = peer['public_key']
|
||||||
|
|
||||||
|
|||||||
@ -477,19 +477,27 @@ def _update_wireguard_settings(connection, wireguard):
|
|||||||
|
|
||||||
settings.set_property(nm.SETTING_WIREGUARD_PRIVATE_KEY,
|
settings.set_property(nm.SETTING_WIREGUARD_PRIVATE_KEY,
|
||||||
wireguard['private_key'])
|
wireguard['private_key'])
|
||||||
peer = nm.WireGuardPeer.new()
|
if 'listen_port' in wireguard:
|
||||||
peer.set_endpoint(wireguard['peer_endpoint'], False)
|
settings.set_property(nm.SETTING_WIREGUARD_LISTEN_PORT,
|
||||||
peer.set_public_key(wireguard['peer_public_key'], False)
|
wireguard['listen_port'])
|
||||||
if wireguard['preshared_key']:
|
|
||||||
# Flag NONE means that NM should store and retain the secret.
|
|
||||||
# Default seems to be NOT_REQUIRED in this case.
|
|
||||||
peer.set_preshared_key_flags(nm.SettingSecretFlags.NONE)
|
|
||||||
peer.set_preshared_key(wireguard['preshared_key'], False)
|
|
||||||
|
|
||||||
peer.append_allowed_ip('0.0.0.0/0', False)
|
if 'peer_public_key' in wireguard:
|
||||||
peer.append_allowed_ip('::/0', False)
|
peer = nm.WireGuardPeer.new()
|
||||||
settings.clear_peers()
|
peer.set_public_key(wireguard['peer_public_key'], False)
|
||||||
settings.append_peer(peer)
|
|
||||||
|
if 'peer_endpoint' in wireguard:
|
||||||
|
peer.set_endpoint(wireguard['peer_endpoint'], False)
|
||||||
|
|
||||||
|
if wireguard['preshared_key']:
|
||||||
|
# Flag NONE means that NM should store and retain the secret.
|
||||||
|
# Default seems to be NOT_REQUIRED in this case.
|
||||||
|
peer.set_preshared_key_flags(nm.SettingSecretFlags.NONE)
|
||||||
|
peer.set_preshared_key(wireguard['preshared_key'], False)
|
||||||
|
|
||||||
|
peer.append_allowed_ip('0.0.0.0/0', False)
|
||||||
|
peer.append_allowed_ip('::/0', False)
|
||||||
|
settings.clear_peers()
|
||||||
|
settings.append_peer(peer)
|
||||||
|
|
||||||
|
|
||||||
def _update_settings(connection, connection_uuid, settings):
|
def _update_settings(connection, connection_uuid, settings):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user