wireguard: Only use network manager for connections to servers

- Don't create network link. This don't persist across reboots and it is the job
  of Network Manager.

- Move NM settings code to regular plinth process instead of superuser.
  Permission for managing NM connections from the service daemon is granted by
  PolKit.

- Use interface name to identify the connection as it seems to be simply to do
  so than the public key. Public key is not easy to retrieve from NM connection.

- Merge code for adding and editing the connection to avoid repetition.

- Add icon to the edit button.

- Throw 404 error when incorrect client is specified.

- Fix issue with storing preshared key.

- Show formatting date in case of last connected time.

- Show formatted sizes for data transmitted.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Sunil Mohan Adapa 2019-11-01 15:53:56 -07:00 committed by James Valleroy
parent b96c5e5433
commit 71c7ab4a9d
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
11 changed files with 258 additions and 253 deletions

View File

@ -24,9 +24,6 @@ import json
import os
import pathlib
import subprocess
import sys
from plinth import network
PUBLIC_KEY_HELP = 'Public key for the client'
@ -57,17 +54,6 @@ def parse_arguments():
help='Remove a client')
remove_client.add_argument('publickey', help=PUBLIC_KEY_HELP)
add_server = subparsers.add_parser('add-server', help='Add a server')
add_server.add_argument('--endpoint', required=True,
help='Server endpoint')
add_server.add_argument('--client-ip', required=True,
help='Client IP address provided by server')
add_server.add_argument('--public-key', required=True,
help='Public key of the server')
add_server.add_argument(
'--all-outgoing', action='store_true',
help='Use this connection to send all outgoing traffic')
modify_server = subparsers.add_parser('modify-server',
help='Modify a server')
modify_server.add_argument('--endpoint', required=True,
@ -185,129 +171,6 @@ def subcommand_remove_client(arguments):
check=True)
def _find_next_interface():
"""Find next unused wireguard interface name."""
output = subprocess.check_output(['wg', 'show',
'interfaces']).decode().strip()
interfaces = output.split()
interface_num = 1
new_interface_name = 'wg1'
while new_interface_name in interfaces:
interface_num += 1
new_interface_name = 'wg' + str(interface_num)
return new_interface_name
def _get_connection_settings(name, interface, endpoint, client_ip, public_key,
client_private_key, pre_shared_key):
"""Return settings for Network Manager connection."""
if not client_private_key:
with PRIVATE_KEY_PATH.open() as private_key_file:
client_private_key = private_key_file.read().strip()
return {
'common': {
'name': name,
'type': 'wireguard',
'interface': interface,
'zone': 'internal',
},
'ipv4': {
'method': 'manual',
'address': client_ip,
'netmask': '',
'gateway': '',
'dns': '',
'second_dns': '',
},
'wireguard': {
'private_key': client_private_key,
'peer_endpoint': endpoint,
'peer_public_key': public_key,
'preshared_key': pre_shared_key,
},
}
def subcommand_add_server(arguments):
"""Add a server."""
secret_args = json.loads(sys.stdin.read() or '{}')
new_interface_name = _find_next_interface()
subprocess.run(
['ip', 'link', 'add', 'dev', new_interface_name, 'type', 'wireguard'],
check=True)
connection_name = 'WireGuard-' + new_interface_name
settings = _get_connection_settings(connection_name, new_interface_name,
arguments.endpoint,
arguments.client_ip,
arguments.public_key,
secret_args.get('client_private_key'),
secret_args.get('pre_shared_key'))
network.add_connection(settings)
def subcommand_modify_server(arguments):
"""Modify a server."""
secret_args = json.loads(sys.stdin.read() or '{}')
interfaces = _get_info()
interfaces.pop(SERVER_INTERFACE, None)
interface_to_modify = None
for interface in interfaces.values():
if interface['peers']:
peer = interface['peers'][0]
if peer['public_key'] == arguments.public_key:
interface_to_modify = interface['interface_name']
if interface_to_modify:
connection = network.get_connection_by_interface_name(
interface_to_modify)
settings = _get_connection_settings(
'WireGuard-' + interface_to_modify, interface_to_modify,
arguments.endpoint, arguments.client_ip, arguments.public_key,
secret_args.get('client_private_key'),
secret_args.get('pre_shared_key'))
if connection:
network.edit_connection(connection, settings)
else:
# XXX: raise error?
network.add_connection(settings)
else:
raise InterfaceNotFoundError(
'Interface with peer %s not found' % arguments.public_key)
def subcommand_remove_server(arguments):
"""Remove a server."""
interfaces = _get_info()
interfaces.pop(SERVER_INTERFACE, None)
interface_to_remove = None
for interface in interfaces.values():
if interface['peers']:
peer = interface['peers'][0]
if peer['public_key'] == arguments.publickey:
interface_to_remove = interface['interface_name']
if interface_to_remove:
connection = network.get_connection_by_interface_name(
interface_to_remove)
if connection:
network.delete_connection(connection.get_uuid())
subprocess.run(['ip', 'link', 'delete', interface_to_remove],
check=True)
else:
raise InterfaceNotFoundError(
'Interface with peer %s not found' % arguments.publickey)
def main():
"""Parse arguments and perform all duties."""
arguments = parse_arguments()

View File

@ -18,6 +18,7 @@
FreedomBox app for wireguard.
"""
import datetime
import json
from django.urls import reverse_lazy
@ -27,10 +28,12 @@ from plinth import actions
from plinth import app as app_module
from plinth import cfg, frontpage, menu
from plinth.modules.firewall.components import Firewall
from plinth.utils import format_lazy
from plinth.utils import format_lazy, import_from_gi
from .manifest import clients # noqa, pylint: disable=unused-import
nm = import_from_gi('NM', '1.0')
version = 1
managed_packages = ['wireguard']
@ -116,15 +119,74 @@ def get_public_key():
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'])
info = json.loads(output)
my_server_info = info.pop(SERVER_INTERFACE, {})
my_client_servers = []
for interface in info.values():
if interface['peers']:
my_client_servers.append(interface['peers'][0])
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': {

View File

@ -31,30 +31,55 @@ class AddClientForm(forms.Form):
class AddServerForm(forms.Form):
"""Form to add server."""
endpoint = forms.CharField(
peer_endpoint = forms.CharField(
label=_('Endpoint'), strip=True,
help_text=_('Server endpoint with the form "ip:port".'))
client_ip_address = forms.CharField(
peer_public_key = forms.CharField(
label=_('Public key of the server'), strip=True,
help_text=_('Public key of the server.'))
ip_address = forms.CharField(
label=_('Client IP address provided by server'), strip=True,
help_text=_('IP address assigned to the client on the VPN after '
'connecting to the endpoint.'))
public_key = forms.CharField(
label=_('Public key of the server'), strip=True,
help_text=_('Public key of the server.'))
client_private_key = forms.CharField(
private_key = forms.CharField(
label=_('Private key of the client'), strip=True,
help_text=_('Optional. A new key is generated if left blank.'),
required=False)
pre_shared_key = forms.CharField(
preshared_key = forms.CharField(
label=_('Pre-shared key'), strip=True, required=False,
help_text=_('Optional. A shared secret key provided by the server to '
'add an additional layer of encryption.'))
all_outgoing_traffic = forms.BooleanField(
default_route = forms.BooleanField(
label=_('Use this connection to send all outgoing traffic'),
required=False,
help_text=_('Use this connection to send all outgoing traffic.'))
def get_settings(self):
"""Return NM settings dict from cleaned data."""
settings = {
'common': {
'type': 'wireguard',
'zone': 'internal',
},
'ipv4': {
'method': 'manual',
'address': self.cleaned_data['ip_address'],
'netmask': '',
'gateway': '',
'dns': '',
'second_dns': '',
},
'wireguard': {
'peer_endpoint': self.cleaned_data['peer_endpoint'],
'peer_public_key': self.cleaned_data['peer_public_key'],
'private_key': self.cleaned_data['private_key'],
'preshared_key': self.cleaned_data['preshared_key'],
'default_route': self.cleaned_data['default_route'],
}
}
return settings

View File

@ -43,7 +43,6 @@
<td>{{ peer.latest_handshake }}</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="3">
@ -72,18 +71,17 @@
<th>{% trans "Last Connected Time" %}</th>
</tr>
{% if client_peers %}
{% for peer in client_peers %}
{% for interface, server in client_peers.items %}
<tr>
<td>{{ peer.endpoint }}</td>
<td>{{ server.peers.0.endpoint }}</td>
<td>
<a href="{% url 'wireguard:show-server' peer.public_key|urlencode:'' %}">
{{ peer.public_key }}
<a href="{% url 'wireguard:show-server' interface %}">
{{ server.peers.0.public_key }}
</a>
</td>
<td>{{ peer.latest_handshake }}</td>
<td>{{ server.peers.0.status.latest_handshake }}</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="4">

View File

@ -28,9 +28,18 @@
<p>
{% trans "Are you sure that you want to delete this server?" %}
</p>
<p>
<b>{{ public_key }}</b>
</p>
<table class="table table-bordered table-condensed table striped">
<tbody>
<tr>
<th>{% trans "Endpoint" %}</th>
<td>{{ peer_endpoint }}</td>
</tr>
<tr>
<th>{% trans "Public Key" %}</th>
<td>{{ peer_public_key }}</td>
</tr>
</tbody>
</table>
<form class="form" method="post">
{% csrf_token %}

View File

@ -32,8 +32,8 @@
<h4>{% trans "Status" %}</h4>
<p>{% trans "Client Public Key:" %} {{ client.public_key }}</p>
<p>{% trans "Data transmitted:" %} {{ client.transfer_tx }}</p>
<p>{% trans "Data received:" %} {{ client.transfer_rx }}</p>
<p>{% trans "Data transmitted:" %} {{ client.transfer_tx|filesizeformat }}</p>
<p>{% trans "Data received:" %} {{ client.transfer_rx|filesizeformat }}</p>
<p>{% trans "Latest handshake:" %} {{ client.latest_handshake }}</p>
<p>

View File

@ -24,21 +24,43 @@
<h3>{{ title }}</h3>
<h4>{% trans "Server Information" %}</h4>
<p>{% trans "Endpoint:" %} {{ server.endpoint }}</p>
<p>{% trans "Public Key:" %} {{ server.public_key }}</p>
<p>{% trans "Pre-shared key:" %} {{ server.preshared_key }}</p>
<p>{% trans "Data transmitted:" %} {{ server.transfer_tx }}</p>
<p>{% trans "Data received:" %} {{ server.transfer_rx }}</p>
<p>{% trans "Latest handshake:" %} {{ server.latest_handshake }}</p>
<table class="table table-bordered table-condensed table-striped">
<tbody>
<tr>
<th>{% trans "Endpoint:" %}</th>
<td>{{ server.peers.0.endpoint }}</td>
</tr>
<tr>
<th>{% trans "Public Key:" %}</th>
<td>{{ server.peers.0.public_key }}</td>
</tr>
<tr>
<th>{% trans "Pre-shared key:" %}</th>
<td>{{ server.peers.0.preshared_key }}</td>
</tr>
<tr>
<th>{% trans "Data transmitted:" %}</th>
<td>{{ server.peers.0.status.transfer_tx|filesizeformat }}</td>
</tr>
<tr>
<th>{% trans "Data received:" %}</th>
<td>{{ server.peers.0.status.transfer_rx|filesizeformat }}</td>
</tr>
<tr>
<th>{% trans "Latest handshake:" %}</th>
<td>{{ server.peers.0.status.latest_handshake }}</td>
</tr>
</tbody>
</table>
<p>
<a class="btn btn-default"
href="{% url 'wireguard:edit-server' server.public_key|urlencode:'' %}">
href="{% url 'wireguard:edit-server' interface %}">
<span class="fa fa-pencil-square-o" aria-hidden="true"></span>
{% trans "Edit Server" %}
</a>
<a class="btn btn-default"
href="{% url 'wireguard:delete-server' server.public_key|urlencode:'' %}">
href="{% url 'wireguard:delete-server' interface %}">
<span class="fa fa-trash-o" aria-hidden="true"></span>
{% trans "Delete Server" %}
</a>

View File

@ -34,10 +34,10 @@ urlpatterns = [
views.DeleteClientView.as_view(), name='delete-client'),
url(r'^apps/wireguard/server/add/$', views.AddServerView.as_view(),
name='add-server'),
url(r'^apps/wireguard/server/(?P<public_key>[^/]+)/show/$',
url(r'^apps/wireguard/server/(?P<interface>wg[0-9]+)/show/$',
views.ShowServerView.as_view(), name='show-server'),
url(r'^apps/wireguard/server/(?P<public_key>[^/]+)/edit/$',
url(r'^apps/wireguard/server/(?P<interface>wg[0-9]+)/edit/$',
views.EditServerView.as_view(), name='edit-server'),
url(r'^apps/wireguard/server/(?P<public_key>[^/]+)/delete/$',
url(r'^apps/wireguard/server/(?P<interface>wg[0-9]+)/delete/$',
views.DeleteServerView.as_view(), name='delete-server'),
]

View File

@ -0,0 +1,45 @@
#
# This file is part of FreedomBox.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
Utilities for managing WireGuard.
"""
import subprocess
from plinth import network
def find_next_interface():
"""Find next unused wireguard interface name."""
output = subprocess.check_output(['wg', 'show',
'interfaces']).decode().strip()
interfaces = output.split()
interface_num = 1
new_interface_name = 'wg1'
while new_interface_name in interfaces:
interface_num += 1
new_interface_name = 'wg' + str(interface_num)
return new_interface_name
def add_server(settings):
"""Add a server."""
interface_name = find_next_interface()
settings['common']['name'] = 'WireGuard-' + interface_name
settings['common']['interface'] = interface_name
network.add_connection(settings)

View File

@ -18,22 +18,21 @@
Views for WireGuard application.
"""
import json
import tempfile
import urllib.parse
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.http import Http404
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import FormView, TemplateView
import plinth.modules.wireguard as wireguard
from plinth import actions
from plinth import actions, network
from plinth.views import AppView
from . import forms
from . import forms, utils
class WireguardView(AppView):
@ -159,29 +158,7 @@ class AddServerView(SuccessMessageMixin, FormView):
def form_valid(self, form):
"""Add the server."""
endpoint = form.cleaned_data.get('endpoint')
client_ip_address = form.cleaned_data.get('client_ip_address')
public_key = form.cleaned_data.get('public_key')
client_private_key = form.cleaned_data.get('client_private_key')
pre_shared_key = form.cleaned_data.get('pre_shared_key')
all_outgoing_traffic = form.cleaned_data.get('all_outgoing_traffic')
args = [
'add-server', '--endpoint', endpoint, '--client-ip',
client_ip_address, '--public-key', public_key
]
secret_args = {}
if client_private_key:
secret_args['client_private_key'] = client_private_key
if pre_shared_key:
secret_args['pre_shared_key'] = pre_shared_key
if all_outgoing_traffic:
args.append('--all-outgoing')
actions.superuser_run('wireguard', args,
input=json.dumps(secret_args).encode())
utils.add_server(form.get_settings())
return super().form_valid(form)
@ -192,15 +169,16 @@ class ShowServerView(SuccessMessageMixin, TemplateView):
def get_context_data(self, **kwargs):
"""Return additional context data for rendering the template."""
context = super().get_context_data(**kwargs)
context['title'] = _('Show Server')
context['title'] = _('Server Information')
public_key = urllib.parse.unquote(self.kwargs['public_key'])
interface = self.kwargs['interface']
info = wireguard.get_info()
context.update(info)
for server in info['my_client']['servers']:
if server['public_key'] == public_key:
context['server'] = server
server = info['my_client']['servers'].get(interface)
if not server:
raise Http404
context['interface'] = interface
context['server'] = server
return context
@ -220,48 +198,31 @@ class EditServerView(SuccessMessageMixin, FormView):
def get_initial(self):
"""Get initial form data."""
initial = super().get_initial()
public_key = urllib.parse.unquote(self.kwargs['public_key'])
info = wireguard.get_info()
for server in info['my_client']['servers']:
if server['public_key'] == public_key:
initial['endpoint'] = server['endpoint']
initial['client_ip_address'] = ''
initial['public_key'] = server['public_key']
pre_shared_key = server['preshared_key']
if pre_shared_key == '(none)':
initial['pre_shared_key'] = ''
else:
initial['pre_shared_key'] = server['preshared_key']
interface = self.kwargs['interface']
info = wireguard.get_nm_info()
server = info.get(interface)
if not server:
raise Http404
initial['all_outgoing_traffic'] = False
initial['ip_address'] = server.get('ip_address')
if server['peers']:
peer = server['peers'][0]
initial['peer_endpoint'] = peer['endpoint']
initial['peer_public_key'] = peer['public_key']
initial['private_key'] = server['private_key']
initial['preshared_key'] = peer['preshared_key']
initial['default_route'] = server['default_route']
return initial
def form_valid(self, form):
"""Update the server."""
endpoint = form.cleaned_data.get('endpoint')
client_ip_address = form.cleaned_data.get('client_ip_address')
public_key = form.cleaned_data.get('public_key')
client_private_key = form.client_data.get('client_private_key')
pre_shared_key = form.cleaned_data.get('pre_shared_key')
all_outgoing_traffic = form.cleaned_data.get('all_outgoing_traffic')
args = [
'modify-server', '--endpoint', endpoint, '--client-ip',
client_ip_address, '--public-key', public_key
]
secret_args = {}
if client_private_key:
secret_args['client_private_key'] = client_private_key
if pre_shared_key:
secret_args['pre_shared_key'] = pre_shared_key
if all_outgoing_traffic:
args.append('--all-outgoing')
actions.superuser_run('wireguard', args,
input=json.dumps(secret_args).encode())
settings = form.get_settings()
interface = self.kwargs['interface']
settings['common']['interface'] = interface
settings['common']['name'] = 'WireGuard-' + interface
connection = network.get_connection_by_interface_name(interface)
network.edit_connection(connection, settings)
return super().form_valid(form)
@ -273,12 +234,24 @@ class DeleteServerView(SuccessMessageMixin, TemplateView):
"""Return additional context data for rendering the template."""
context = super().get_context_data(**kwargs)
context['title'] = _('Delete Server')
context['public_key'] = urllib.parse.unquote(self.kwargs['public_key'])
interface = self.kwargs['interface']
info = wireguard.get_nm_info()
server = info.get(interface)
if not server:
raise Http404
context['interface'] = interface
if server['peers']:
peer = server['peers'][0]
context['peer_endpoint'] = peer['endpoint']
context['peer_public_key'] = peer['public_key']
return context
def post(self, request, public_key):
def post(self, request, interface):
"""Delete the server."""
public_key = urllib.parse.unquote(public_key)
actions.superuser_run('wireguard', ['remove-server', public_key])
connection = network.get_connection_by_interface_name(interface)
network.delete_connection(connection.get_uuid())
messages.success(request, _('Server deleted.'))
return redirect('wireguard:index')

View File

@ -470,17 +470,25 @@ def _update_wireless_settings(connection, wireless):
def _update_wireguard_settings(connection, wireguard):
"""Create/edit WireGuard settings for network manager connections."""
settings = nm.SettingWireGuard.new()
connection.add_setting(settings)
settings = connection.get_setting_by_name('wireguard')
if not settings:
settings = nm.SettingWireGuard.new()
connection.add_setting(settings)
settings.set_property(nm.SETTING_WIREGUARD_PRIVATE_KEY,
wireguard['private_key'])
peer = nm.WireGuardPeer.new()
peer.set_endpoint(wireguard['peer_endpoint'], False)
peer.set_public_key(wireguard['peer_public_key'], False)
peer.set_preshared_key(wireguard['preshared_key'], 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)