From aa66a9135c996fba90378e25567e64a475e1e0aa Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Mon, 16 Sep 2019 20:05:35 -0400 Subject: [PATCH] wireguard: Add views for editing and deleting clients and servers Signed-off-by: James Valleroy Reviewed-by: Sunil Mohan Adapa --- actions/wireguard | 71 +++++++++++- plinth/modules/wireguard/__init__.py | 7 +- .../wireguard/templates/wireguard.html | 8 -- .../templates/wireguard_delete_server.html | 42 ++++++++ .../templates/wireguard_edit_client.html | 37 +++++++ .../templates/wireguard_edit_server.html | 37 +++++++ .../templates/wireguard_show_client.html | 12 +++ .../templates/wireguard_show_server.html | 12 +++ plinth/modules/wireguard/urls.py | 6 ++ plinth/modules/wireguard/views.py | 101 ++++++++++++++++++ 10 files changed, 318 insertions(+), 15 deletions(-) create mode 100644 plinth/modules/wireguard/templates/wireguard_delete_server.html create mode 100644 plinth/modules/wireguard/templates/wireguard_edit_client.html create mode 100644 plinth/modules/wireguard/templates/wireguard_edit_server.html diff --git a/actions/wireguard b/actions/wireguard index 76fc529ce..1231bf1c8 100755 --- a/actions/wireguard +++ b/actions/wireguard @@ -34,6 +34,10 @@ 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(): """Return parsed command line arguments as dictionary.""" parser = argparse.ArgumentParser() @@ -62,6 +66,19 @@ def parse_arguments(): '--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, + 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) @@ -216,13 +233,57 @@ def subcommand_add_server(arguments): subprocess.run(args, check=True) +def subcommand_modify_server(arguments): + """Modify a server.""" + interfaces = _get_info() + interfaces.pop(SERVER_INTERFACE) + 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: + args = ['wg', 'set', interface_to_modify, 'peer', arguments.public_key] + if arguments.pre_shared_key: + args += ['preshared-key', arguments.pre_shared_key] + + args += ['endpoint', arguments.endpoint] + subprocess.run(args, check=True) + + subprocess.run(['nmcli', 'con', 'modify', + 'WireGuard-' + interface_to_modify, + 'ipv4.method', 'manual', + 'ipv4.addresses', arguments.client_ip + '/24'], + check=True) + + else: + raise InterfaceNotFoundError( + 'Interface with peer %s not found' % arguments.publickey) + + def subcommand_remove_server(arguments): """Remove a server.""" - # XXX: fix this - subprocess.run( - ['wg', 'set', SERVER_INTERFACE, 'peer', arguments.publickey, 'remove'], - check=True) - # TODO: also delete NM connection + interfaces = _get_info() + interfaces.pop(SERVER_INTERFACE) + 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: + subprocess.run( + ['nmcli', 'con', 'delete', 'WireGuard-' + interface_to_remove], + check=True) + subprocess.run(['ip', 'link', 'delete', interface_to_remove], + check=True) + + else: + raise InterfaceNotFoundError( + 'Interface with peer %s not found' % arguments.publickey) def main(): diff --git a/plinth/modules/wireguard/__init__.py b/plinth/modules/wireguard/__init__.py index 9f830e77c..fc02e46ad 100644 --- a/plinth/modules/wireguard/__init__.py +++ b/plinth/modules/wireguard/__init__.py @@ -122,8 +122,11 @@ def get_info(): output = actions.superuser_run('wireguard', ['get-info']) info = json.loads(output) my_server_info = info.pop(SERVER_INTERFACE) - my_client_servers = [interface['peers'] and interface['peers'][0] or {} - for interface in info.values()] + my_client_servers = [] + for interface in info.values(): + if interface['peers']: + my_client_servers.append(interface['peers'][0]) + return { 'my_server': { 'public_key': my_server_info['public_key'], diff --git a/plinth/modules/wireguard/templates/wireguard.html b/plinth/modules/wireguard/templates/wireguard.html index 13a2132ad..7734d7f47 100644 --- a/plinth/modules/wireguard/templates/wireguard.html +++ b/plinth/modules/wireguard/templates/wireguard.html @@ -31,7 +31,6 @@ {% trans "Public Key" %} {% trans "Last Connected Time" %} - {% trans "Delete" %} {% if server_peers %} {% for peer in server_peers %} @@ -42,11 +41,6 @@ {{ peer.latest_handshake }} - - - {% endfor %} @@ -76,7 +70,6 @@ {% trans "Endpoint" %} {% trans "Public Key" %} {% trans "Last Connected Time" %} - {% trans "Edit" %} {% if client_peers %} {% for peer in client_peers %} @@ -88,7 +81,6 @@ {{ peer.latest_handshake }} - Edit {% endfor %} diff --git a/plinth/modules/wireguard/templates/wireguard_delete_server.html b/plinth/modules/wireguard/templates/wireguard_delete_server.html new file mode 100644 index 000000000..1fb708300 --- /dev/null +++ b/plinth/modules/wireguard/templates/wireguard_delete_server.html @@ -0,0 +1,42 @@ +{% extends "base.html" %} +{% comment %} +# +# 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 . +# +{% endcomment %} + +{% load bootstrap %} +{% load i18n %} + +{% block content %} + +

{{ title }}

+ +

+ {% trans "Are you sure that you want to delete this server?" %} +

+

+ {{ public_key }} +

+ +
+ {% csrf_token %} + + +
+ +{% endblock %} diff --git a/plinth/modules/wireguard/templates/wireguard_edit_client.html b/plinth/modules/wireguard/templates/wireguard_edit_client.html new file mode 100644 index 000000000..93da05b41 --- /dev/null +++ b/plinth/modules/wireguard/templates/wireguard_edit_client.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} +{% comment %} +# +# 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 . +# +{% endcomment %} + +{% load bootstrap %} +{% load i18n %} + +{% block content %} + +

{{ title }}

+ +
+ {% csrf_token %} + + {{ form|bootstrap }} + + +
+ +{% endblock %} diff --git a/plinth/modules/wireguard/templates/wireguard_edit_server.html b/plinth/modules/wireguard/templates/wireguard_edit_server.html new file mode 100644 index 000000000..b2f1a6e6c --- /dev/null +++ b/plinth/modules/wireguard/templates/wireguard_edit_server.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} +{% comment %} +# +# 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 . +# +{% endcomment %} + +{% load bootstrap %} +{% load i18n %} + +{% block content %} + +

{{ title }}

+ +
+ {% csrf_token %} + + {{ form|bootstrap }} + + +
+ +{% endblock %} diff --git a/plinth/modules/wireguard/templates/wireguard_show_client.html b/plinth/modules/wireguard/templates/wireguard_show_client.html index e07238ca9..32718245d 100644 --- a/plinth/modules/wireguard/templates/wireguard_show_client.html +++ b/plinth/modules/wireguard/templates/wireguard_show_client.html @@ -36,4 +36,16 @@

{% trans "Data received:" %} {{ client.transfer_rx }}

{% trans "Latest handshake:" %} {{ client.latest_handshake }}

+

+ + {% trans "Edit Client" %} + + + + {% trans "Delete Client" %} + +

+ {% endblock %} diff --git a/plinth/modules/wireguard/templates/wireguard_show_server.html b/plinth/modules/wireguard/templates/wireguard_show_server.html index 9ad01eaed..95997a429 100644 --- a/plinth/modules/wireguard/templates/wireguard_show_server.html +++ b/plinth/modules/wireguard/templates/wireguard_show_server.html @@ -32,4 +32,16 @@

{% trans "Data received:" %} {{ server.transfer_rx }}

{% trans "Latest handshake:" %} {{ server.latest_handshake }}

+

+ + {% trans "Edit Server" %} + + + + {% trans "Delete Server" %} + +

+ {% endblock %} diff --git a/plinth/modules/wireguard/urls.py b/plinth/modules/wireguard/urls.py index fa2a1f159..20c9c11d6 100644 --- a/plinth/modules/wireguard/urls.py +++ b/plinth/modules/wireguard/urls.py @@ -28,10 +28,16 @@ urlpatterns = [ name='add-client'), url(r'^apps/wireguard/client/(?P[^/]+)/show/$', views.ShowClientView.as_view(), name='show-client'), + url(r'^apps/wireguard/client/(?P[^/]+)/edit/$', + views.EditClientView.as_view(), name='edit-client'), url(r'^apps/wireguard/client/(?P[^/]+)/delete/$', 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[^/]+)/show/$', views.ShowServerView.as_view(), name='show-server'), + url(r'^apps/wireguard/server/(?P[^/]+)/edit/$', + views.EditServerView.as_view(), name='edit-server'), + url(r'^apps/wireguard/server/(?P[^/]+)/delete/$', + views.DeleteServerView.as_view(), name='delete-server'), ] diff --git a/plinth/modules/wireguard/views.py b/plinth/modules/wireguard/views.py index 61e89ecfd..c919a5b88 100644 --- a/plinth/modules/wireguard/views.py +++ b/plinth/modules/wireguard/views.py @@ -94,6 +94,35 @@ class ShowClientView(SuccessMessageMixin, TemplateView): return context +class EditClientView(SuccessMessageMixin, FormView): + """View to modify a client.""" + form_class = forms.AddClientForm + template_name = 'wireguard_edit_client.html' + success_url = reverse_lazy('wireguard:index') + success_message = _('Updated client.') + + def get_context_data(self, **kwargs): + """Return additional context for rendering the template.""" + context = super().get_context_data(**kwargs) + context['title'] = _('Modify Client') + return context + + def get_initial(self): + """Get initial form data.""" + initial = super().get_initial() + initial['public_key'] = urllib.parse.unquote(self.kwargs['public_key']) + return initial + + def form_valid(self, form): + """Update the client.""" + old_public_key = form.initial['public_key'] + actions.superuser_run('wireguard', ['remove-client', old_public_key]) + + public_key = form.cleaned_data.get('public_key') + actions.superuser_run('wireguard', ['add-client', public_key]) + return super().form_valid(form) + + class DeleteClientView(SuccessMessageMixin, TemplateView): """View to delete a client.""" template_name = 'wireguard_delete_client.html' @@ -163,3 +192,75 @@ class ShowServerView(SuccessMessageMixin, TemplateView): context['server'] = server return context + + +class EditServerView(SuccessMessageMixin, FormView): + """View to modify a server.""" + form_class = forms.AddServerForm + template_name = 'wireguard_edit_server.html' + success_url = reverse_lazy('wireguard:index') + success_message = _('Updated server.') + + def get_context_data(self, **kwargs): + """Return additional context for rendering the template.""" + context = super().get_context_data(**kwargs) + context['title'] = _('Modify Server') + return context + + 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'] + + initial['all_outgoing_traffic'] = False + + 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') + 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] + if pre_shared_key: + # TODO: pass pre-shared key through stdin + args += ['--pre-shared-key', pre_shared_key] + + if all_outgoing_traffic: + args.append('--all-outgoing') + + actions.superuser_run('wireguard', args) + return super().form_valid(form) + + +class DeleteServerView(SuccessMessageMixin, TemplateView): + """View to delete a server.""" + template_name = 'wireguard_delete_server.html' + + def get_context_data(self, **kwargs): + """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']) + return context + + def post(self, request, public_key): + """Delete the server.""" + public_key = urllib.parse.unquote(public_key) + actions.superuser_run('wireguard', ['remove-server', public_key]) + messages.success(request, _('Server deleted.')) + return redirect('wireguard:index')