wireguard: Enable FB to connect to a server using IPv6

This MR enables FreedomBox to connect as a "client" to a WireGuard
"server" using IPv6.

- Validate IPv4/6 with ip_interface
- Created helper functions to build NM settings for IPv4/6
- Modify get_settings to include settings for either IP version 4 or 6
- Created helper function to get NM address info
- Modify get_nm_info to work with IPv4 and IPv6
- Modified tests to use validate_ip_address_with_network
- Added IPv6 valid and invalid patterns to tests

Tested:

- IPv4 works unchanged
- IPv6 parsing + NM settings generation works
- IPv6 display in Show Server UI

Not tested:

- Needs IPv6 WireGuard server for full connectivity test

Closes: #1762

Signed-off-by: Frederico Gomes <fredericojfgomes@gmail.com>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Frederico Gomes 2026-04-30 19:10:16 +01:00 committed by James Valleroy
parent 9fd7a3b3af
commit ad1b420397
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
3 changed files with 79 additions and 36 deletions

View File

@ -55,14 +55,12 @@ 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."""
def validate_ip_address_with_network(value: str):
"""Check that value is a valid IP 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.'))
ipaddress.ip_interface(value)
except ValueError:
raise ValidationError(_('Not a valid IP address.'))
class AddClientForm(forms.Form):
@ -98,10 +96,11 @@ class AddServerForm(forms.Form):
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. '
'operator. Example: 192.168.0.10 or '
'2a03:7c80:4b2c:91a2:5d41:ffee:9b82:7c17. 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])
validators=[validate_ip_address_with_network])
private_key = forms.CharField(
label=_('Private key of this machine'), strip=True, help_text=_(
@ -125,31 +124,44 @@ class AddServerForm(forms.Form):
'Typically checked for a VPN service through which all traffic '
'is sent.'))
def _build_ipv4_settings(self, iface) -> dict:
"""Build IPv4 NM settings from interfaces."""
return {
'method': 'manual',
'address': str(iface.ip),
'netmask': str(iface.netmask),
'gateway': '',
'dns': '',
'second_dns': '',
}
def _build_ipv6_settings(self, iface) -> dict:
"""Build IPv6 NM settings from interfaces."""
return {
'method': 'manual',
'address': str(iface.ip),
'prefix': iface.network.prefixlen,
'gateway': '',
'dns': '',
'second_dns': '',
}
def get_settings(self) -> dict[str, dict]:
"""Return NM settings dict from cleaned data."""
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
ip_interface = ipaddress.ip_interface(
self.cleaned_data['ip_address_and_network']
)
if self.cleaned_data['default_route']:
allowed_ips = ['0.0.0.0/0', '::/0']
else:
allowed_ips = [f'{ip_address}/{prefixlen}']
allowed_ips = [str(ip_interface)]
settings = {
'common': {
'type': 'wireguard',
'zone': 'external',
},
'ipv4': {
'method': 'manual',
'address': ip_address,
'netmask': str(ip_address_and_network.netmask),
'gateway': '',
'dns': '',
'second_dns': '',
},
'wireguard': {
'peer_endpoint': self.cleaned_data['peer_endpoint'],
'peer_public_key': self.cleaned_data['peer_public_key'],
@ -158,4 +170,10 @@ class AddServerForm(forms.Form):
'allowed_ips': allowed_ips,
}
}
if ip_interface.version == 4:
settings['ipv4'] = self._build_ipv4_settings(ip_interface)
else:
settings['ipv6'] = self._build_ipv6_settings(ip_interface)
return settings

View File

@ -7,7 +7,7 @@ import pytest
from django.core.exceptions import ValidationError
from plinth.modules.wireguard.forms import (validate_endpoint,
validate_ipv4_address_with_network,
validate_ip_address_with_network,
validate_key)
@ -73,22 +73,33 @@ def test_validate_endpoint_invalid_patterns(endpoint):
'1.2.3.4/24',
'1.2.3.4/255.255.255.0',
'1.2.3.4/0.0.0.255',
'::1',
'2001:db8::1',
'2001:db8::1/64',
'fe80::1/64',
'::/0',
])
def test_validate_ipv4_address_with_network_valid_patterns(value):
def test_validate_ip_address_with_network_valid_patterns(value):
"""Test validating IPv4 address with network works for valid values."""
validate_ipv4_address_with_network(value)
validate_ip_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',
'2001:db8::1/',
'2001:db8::1/129',
'2001:db8::1/x',
'2001:db8::1/-1',
'2001:db8::1/255.255.255.0',
'2001:db8::1::1',
'12345::1',
])
def test_validate_ipv4_address_with_network_invalid_patterns(value):
def test_validate_ip_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)
validate_ip_address_with_network(value)

View File

@ -19,6 +19,18 @@ IP_TEMPLATE = '10.84.0.{}'
logger = logging.getLogger(__name__)
def _get_nm_address_info(settings_ipv4, settings_ipv6) -> tuple[str, str]:
"""Extract IP address info from NM IPv4/IPv6 settings."""
for settings in [settings_ipv4, settings_ipv6]:
if settings and settings.get_num_addresses():
nm_address = settings.get_address(0)
address = nm_address.get_address()
prefix = str(nm_address.get_prefix())
return address, address + '/' + prefix
return '', ''
def get_nm_info():
"""Get information from network manager."""
setting_name = nm.SETTING_WIREGUARD_SETTING_NAME
@ -64,12 +76,14 @@ def get_nm_info():
info['default_route'] = True
settings_ipv4 = connection.get_setting_ip4_config()
if settings_ipv4 and settings_ipv4.get_num_addresses():
nm_address = settings_ipv4.get_address(0)
address = nm_address.get_address()
prefix = str(nm_address.get_prefix())
info['ip_address'] = address
info['ip_address_and_network'] = address + '/' + prefix
settings_ipv6 = connection.get_setting_ip6_config()
ip_address, ip_address_and_network = _get_nm_address_info(
settings_ipv4, settings_ipv6
)
info['ip_address'] = ip_address
info['ip_address_and_network'] = ip_address_and_network
connections[info['interface']] = info