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 <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Sunil Mohan Adapa 2026-02-26 22:54:51 -08:00 committed by James Valleroy
parent ad9ebe2301
commit 4b24fda3f5
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
7 changed files with 83 additions and 30 deletions

View File

@ -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

View File

@ -36,9 +36,9 @@
<th>{% trans "Public key of this machine:" %}</th>
<td>{{ server.public_key }}</td>
</tr>
<tr class="server-ip-address">
<tr class="server-ip-address-and-network">
<th>{% trans "IP address of this machine:" %}</th>
<td>{{ server.ip_address }}</td>
<td>{{ server.ip_address_and_network }}</td>
</tr>
<tr class="server-default-route">
<th>

View File

@ -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)

View File

@ -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')

View File

@ -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

View File

@ -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']

View File

@ -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)