From 4b24fda3f5f77ea01229c828ec66ec190aa7013a Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Thu, 26 Feb 2026 22:54:51 -0800 Subject: [PATCH] wireguard: Accept/use netmask with IP address for server connection - Currently, the value is hard-coded as /24. Instead take this as input and use that value. Tests: - Entering invalid IPv4 address results in 'Enter a valid IPv4 address' error message during form submission. - Entering invalid prefix such as /33 results in 'Enter a valid network prefix or net mask.' error during form submission. - Both /32 and /255.255.255.255 formats are accepted. - The description text for the form field 'IP address' is as expected. - Changing the value of default route and IP address + netmask reflects in the status page. Correct values is shown in the edit server and server status page. - Not providing a netmask results in /32 being assigned. - Unit and functional tests for wireguard pass. There are some intermittent failures with functional tests that are unrelated to the patch. - Setting the /32 prefix results in correct routing table as shown by 'ip route show table all'. No default routes are network routes are present. 'traceroute 1.1.1.1' shows route taken via regular network. - Setting the /24 prefix results in correct routing table. No default routes are present. However, for the /24 network a route is present with device wg1. 'traceroute 1.1.1.1' shows route taken via regular network. - Enabling the default route results in correct routing table. Default route is shown for device wg1 with high priority. 'traceroute 1.1.1.1' shows route taken via WireGuard network. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/modules/wireguard/forms.py | 43 ++++++++++++++----- .../templates/wireguard_show_server.html | 4 +- plinth/modules/wireguard/tests/test_forms.py | 32 +++++++++++++- .../wireguard/tests/test_functional.py | 16 ++++--- plinth/modules/wireguard/utils.py | 4 +- plinth/modules/wireguard/views.py | 6 +-- plinth/network.py | 8 +--- 7 files changed, 83 insertions(+), 30 deletions(-) diff --git a/plinth/modules/wireguard/forms.py b/plinth/modules/wireguard/forms.py index 87450ac32..cccc37ad8 100644 --- a/plinth/modules/wireguard/forms.py +++ b/plinth/modules/wireguard/forms.py @@ -5,10 +5,10 @@ Forms for wireguard module. import base64 import binascii +import ipaddress from django import forms from django.core.exceptions import ValidationError -from django.core.validators import validate_ipv4_address from django.utils.translation import gettext_lazy as _ KEY_LENGTH = 32 @@ -55,6 +55,16 @@ def validate_endpoint(endpoint): raise ValidationError('Invalid endpoint.') +def validate_ipv4_address_with_network(value: str): + """Check that value is a valid IPv4 address with an optional network.""" + try: + ipaddress.IPv4Interface(value) + except ipaddress.AddressValueError: + raise ValidationError(_('Enter a valid IPv4 address.')) + except ipaddress.NetmaskValueError: + raise ValidationError(_('Enter a valid network prefix or net mask.')) + + class AddClientForm(forms.Form): """Form to add client.""" public_key = forms.CharField( @@ -78,12 +88,15 @@ class AddServerForm(forms.Form): 'Example: MConEJFIg6+DFHg2J1nn9SNLOSE9KR0ysdPgmPjibEs= .'), validators=[validate_key]) - ip_address = forms.CharField( + ip_address_and_network = forms.CharField( label=_('Client IP address provided by server'), strip=True, - help_text=_('IP address assigned to this machine on the VPN after ' - 'connecting to the endpoint. This value is usually ' - 'provided by the server operator. Example: 192.168.0.10.'), - validators=[validate_ipv4_address]) + help_text=_( + 'IP address assigned to this machine on the VPN after connecting ' + 'to the endpoint. This value is usually provided by the server ' + 'operator. Example: 192.168.0.10. You can also specify the ' + 'network. This will allow reaching machines in the network. ' + 'Examples: 10.68.12.43/24 or 10.68.12.43/255.255.255.0.'), + validators=[validate_ipv4_address_with_network]) private_key = forms.CharField( label=_('Private key of this machine'), strip=True, help_text=_( @@ -107,9 +120,18 @@ class AddServerForm(forms.Form): 'Typically checked for a VPN service through which all traffic ' 'is sent.')) - def get_settings(self): + def get_settings(self) -> dict[str, dict]: """Return NM settings dict from cleaned data.""" - ip_address = self.cleaned_data['ip_address'] + ip_address_and_network = self.cleaned_data['ip_address_and_network'] + ip_address_and_network = ipaddress.IPv4Interface( + ip_address_and_network) + ip_address = str(ip_address_and_network.ip) + prefixlen = ip_address_and_network.network.prefixlen + if self.cleaned_data['default_route']: + allowed_ips = ['0.0.0.0/0', '::/0'] + else: + allowed_ips = [f'{ip_address}/{prefixlen}'] + settings = { 'common': { 'type': 'wireguard', @@ -118,7 +140,7 @@ class AddServerForm(forms.Form): 'ipv4': { 'method': 'manual', 'address': ip_address, - 'netmask': '255.255.255.0', + 'netmask': str(ip_address_and_network.netmask), 'gateway': '', 'dns': '', 'second_dns': '', @@ -126,10 +148,9 @@ class AddServerForm(forms.Form): 'wireguard': { 'peer_endpoint': self.cleaned_data['peer_endpoint'], 'peer_public_key': self.cleaned_data['peer_public_key'], - 'ip_address': ip_address, 'private_key': self.cleaned_data['private_key'], 'preshared_key': self.cleaned_data['preshared_key'], - 'default_route': self.cleaned_data['default_route'], + 'allowed_ips': allowed_ips, } } return settings diff --git a/plinth/modules/wireguard/templates/wireguard_show_server.html b/plinth/modules/wireguard/templates/wireguard_show_server.html index 4f886ccfb..f0f306c95 100644 --- a/plinth/modules/wireguard/templates/wireguard_show_server.html +++ b/plinth/modules/wireguard/templates/wireguard_show_server.html @@ -36,9 +36,9 @@ {% trans "Public key of this machine:" %} {{ server.public_key }} - + {% trans "IP address of this machine:" %} - {{ server.ip_address }} + {{ server.ip_address_and_network }} diff --git a/plinth/modules/wireguard/tests/test_forms.py b/plinth/modules/wireguard/tests/test_forms.py index 3e4c33d9b..780cb8d4a 100644 --- a/plinth/modules/wireguard/tests/test_forms.py +++ b/plinth/modules/wireguard/tests/test_forms.py @@ -6,7 +6,9 @@ Tests for wireguard module forms. import pytest from django.core.exceptions import ValidationError -from plinth.modules.wireguard.forms import validate_endpoint, validate_key +from plinth.modules.wireguard.forms import (validate_endpoint, + validate_ipv4_address_with_network, + validate_key) @pytest.mark.parametrize('key', [ @@ -62,3 +64,31 @@ def test_validate_endpoint_invalid_patterns(endpoint): """Test that invalid wireguard endpoint patterns are rejected.""" with pytest.raises(ValidationError): validate_endpoint(endpoint) + + +@pytest.mark.parametrize('value', [ + '1.2.3.4', + '1.2.3.4/0', + '1.2.3.4/32', + '1.2.3.4/24', + '1.2.3.4/255.255.255.0', + '1.2.3.4/0.0.0.255', +]) +def test_validate_ipv4_address_with_network_valid_patterns(value): + """Test validating IPv4 address with network works for valid values.""" + validate_ipv4_address_with_network(value) + + +@pytest.mark.parametrize('value', [ + '::1', + '1.2.3.4/', + 'invalid-ip/24', + '1.2.3.4/x', + '1.2.3.4/-1', + '1.2.3.4/33', + '1.2.3.4/9.8.7.6', +]) +def test_validate_ipv4_address_with_network_invalid_patterns(value): + """Test validating IPv4 address with network works for invalid values.""" + with pytest.raises(ValidationError): + validate_ipv4_address_with_network(value) diff --git a/plinth/modules/wireguard/tests/test_functional.py b/plinth/modules/wireguard/tests/test_functional.py index 02f60a36d..bc2714440 100644 --- a/plinth/modules/wireguard/tests/test_functional.py +++ b/plinth/modules/wireguard/tests/test_functional.py @@ -23,14 +23,14 @@ class TestWireguardApp(functional.BaseAppTests): { 'peer_endpoint': 'wg1.example.org:1234', 'peer_public_key': 'HBCqZk4B93N6q19zNleJkAVs+PEfWAPgPpKnrhL/CVw=', - 'ip_address': '10.0.0.2', + 'ip_address_and_network': '10.0.0.2/32', 'private_key': '', 'preshared_key': '' }, { 'peer_endpoint': 'wg2.example.org:5678', 'peer_public_key': 'Z/iHo0vaeSN8Ykk5KwhQ819MMU5nyzD7y7xFFthlxXI=', - 'ip_address': '192.168.0.2', + 'ip_address_and_network': '192.168.0.2/24', 'private_key': 'QC2xEZMn3bgNsSVFrU51+ALSUiUaWg6gRWigh3EeVm0=', 'preshared_key': 'AHxZ4Rr8Ij4L1aq+ceusSIgBfluqiI9Vb5I2UtQFanI=' }, @@ -70,7 +70,8 @@ class TestWireguardApp(functional.BaseAppTests): # Start the server on FreedomBox, if needed. start_server_button = browser.find_by_css('.btn-start-server') if start_server_button: - start_server_button.first.click() + with functional.wait_for_page_update(browser): + start_server_button.first.click() browser.find_by_css('.btn-add-client').first.click() browser.find_by_id('id_public_key').fill(key) @@ -135,7 +136,8 @@ class TestWireguardApp(functional.BaseAppTests): href.first.click() assert get_value('peer-endpoint') == config['peer_endpoint'] assert get_value('peer-public-key') == config['peer_public_key'] - assert get_value('server-ip-address') == config['ip_address'] + assert get_value('server-ip-address-and-network' + ) == config['ip_address_and_network'] assert get_value('peer-preshared-key') == (config['preshared_key'] or 'None') @@ -147,7 +149,8 @@ class TestWireguardApp(functional.BaseAppTests): browser.find_by_id('id_peer_endpoint').fill(config['peer_endpoint']) browser.find_by_id('id_peer_public_key').fill( config['peer_public_key']) - browser.find_by_id('id_ip_address').fill(config['ip_address']) + browser.find_by_id('id_ip_address_and_network').fill( + config['ip_address_and_network']) browser.find_by_id('id_private_key').fill(config['private_key']) browser.find_by_id('id_preshared_key').fill(config['preshared_key']) functional.submit(browser, form_class='form-add-server') @@ -161,7 +164,8 @@ class TestWireguardApp(functional.BaseAppTests): browser.find_by_id('id_peer_endpoint').fill(config2['peer_endpoint']) browser.find_by_id('id_peer_public_key').fill( config2['peer_public_key']) - browser.find_by_id('id_ip_address').fill(config2['ip_address']) + browser.find_by_id('id_ip_address_and_network').fill( + config2['ip_address_and_network']) browser.find_by_id('id_private_key').fill(config2['private_key']) browser.find_by_id('id_preshared_key').fill(config2['preshared_key']) functional.submit(browser, form_class='form-edit-server') diff --git a/plinth/modules/wireguard/utils.py b/plinth/modules/wireguard/utils.py index c7915bac3..5917ade72 100644 --- a/plinth/modules/wireguard/utils.py +++ b/plinth/modules/wireguard/utils.py @@ -65,7 +65,9 @@ def get_nm_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() + address = settings_ipv4.get_address(0) + info['ip_address_and_network'] = (address.get_address() + '/' + + str(address.get_prefix())) connections[info['interface']] = info diff --git a/plinth/modules/wireguard/views.py b/plinth/modules/wireguard/views.py index 545acfbc1..e561bd245 100644 --- a/plinth/modules/wireguard/views.py +++ b/plinth/modules/wireguard/views.py @@ -101,8 +101,7 @@ class ShowClientView(SuccessMessageMixin, TemplateView): context['client'] = server_info['peers'][public_key] context['endpoints'] = [ domain + ':' + str(server_info['listen_port']) - for domain in domains - if not domain.endswith('.local') + for domain in domains if not domain.endswith('.local') ] return context @@ -227,7 +226,8 @@ class EditServerView(SuccessMessageMixin, FormView): if not server: raise Http404 - initial['ip_address'] = server.get('ip_address') + initial['ip_address_and_network'] = server.get( + 'ip_address_and_network') if server['peers']: peer = next(peer for peer in server['peers'].values()) initial['peer_endpoint'] = peer['endpoint'] diff --git a/plinth/network.py b/plinth/network.py index 873bcd5bb..657d73363 100644 --- a/plinth/network.py +++ b/plinth/network.py @@ -507,12 +507,8 @@ def _update_wireguard_settings(connection, wireguard): peer.set_preshared_key_flags(nm.SettingSecretFlags.NONE) peer.set_preshared_key(wireguard['preshared_key'], False) - if wireguard['default_route']: - peer.append_allowed_ip('0.0.0.0/0', False) - peer.append_allowed_ip('::/0', False) - else: - ip_addr = wireguard['ip_address'] - peer.append_allowed_ip(f'{ip_addr}/24', False) + for allowed_ip in wireguard['allowed_ips']: + peer.append_allowed_ip(allowed_ip, False) settings.clear_peers() settings.append_peer(peer)