Add IPv6 address generation mode & privacy extensions (#5892)

* feat: Add IPv6 address generation mode & privacy extensions

Signed-off-by: David Rapan <david@rapan.cz>

* Use NetworkManager fixture for settings init tests

This fixes the test by since the extended implementation now can read
the version of NetworkManager.

* Add pytest for addr_gen_mode

---------

Signed-off-by: David Rapan <david@rapan.cz>
Co-authored-by: Stefan Agner <stefan@agner.ch>
This commit is contained in:
David Rapan 2025-05-20 17:03:08 +02:00 committed by GitHub
parent 6e6fe5ba39
commit 3b575eedba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 274 additions and 68 deletions

View File

@ -10,6 +10,7 @@ import voluptuous as vol
from ..const import ( from ..const import (
ATTR_ACCESSPOINTS, ATTR_ACCESSPOINTS,
ATTR_ADDR_GEN_MODE,
ATTR_ADDRESS, ATTR_ADDRESS,
ATTR_AUTH, ATTR_AUTH,
ATTR_CONNECTED, ATTR_CONNECTED,
@ -22,6 +23,7 @@ from ..const import (
ATTR_ID, ATTR_ID,
ATTR_INTERFACE, ATTR_INTERFACE,
ATTR_INTERFACES, ATTR_INTERFACES,
ATTR_IP6_PRIVACY,
ATTR_IPV4, ATTR_IPV4,
ATTR_IPV6, ATTR_IPV6,
ATTR_MAC, ATTR_MAC,
@ -46,7 +48,10 @@ from ..exceptions import APIError, APINotFound, HostNetworkNotFound
from ..host.configuration import ( from ..host.configuration import (
AccessPoint, AccessPoint,
Interface, Interface,
InterfaceAddrGenMode,
InterfaceIp6Privacy,
InterfaceMethod, InterfaceMethod,
Ip6Setting,
IpConfig, IpConfig,
IpSetting, IpSetting,
VlanConfig, VlanConfig,
@ -68,6 +73,8 @@ _SCHEMA_IPV6_CONFIG = vol.Schema(
{ {
vol.Optional(ATTR_ADDRESS): [vol.Coerce(IPv6Interface)], vol.Optional(ATTR_ADDRESS): [vol.Coerce(IPv6Interface)],
vol.Optional(ATTR_METHOD): vol.Coerce(InterfaceMethod), vol.Optional(ATTR_METHOD): vol.Coerce(InterfaceMethod),
vol.Optional(ATTR_ADDR_GEN_MODE): vol.Coerce(InterfaceAddrGenMode),
vol.Optional(ATTR_IP6_PRIVACY): vol.Coerce(InterfaceIp6Privacy),
vol.Optional(ATTR_GATEWAY): vol.Coerce(IPv6Address), vol.Optional(ATTR_GATEWAY): vol.Coerce(IPv6Address),
vol.Optional(ATTR_NAMESERVERS): [vol.Coerce(IPv6Address)], vol.Optional(ATTR_NAMESERVERS): [vol.Coerce(IPv6Address)],
} }
@ -94,8 +101,8 @@ SCHEMA_UPDATE = vol.Schema(
) )
def ipconfig_struct(config: IpConfig, setting: IpSetting) -> dict[str, Any]: def ip4config_struct(config: IpConfig, setting: IpSetting) -> dict[str, Any]:
"""Return a dict with information about ip configuration.""" """Return a dict with information about IPv4 configuration."""
return { return {
ATTR_METHOD: setting.method, ATTR_METHOD: setting.method,
ATTR_ADDRESS: [address.with_prefixlen for address in config.address], ATTR_ADDRESS: [address.with_prefixlen for address in config.address],
@ -105,6 +112,19 @@ def ipconfig_struct(config: IpConfig, setting: IpSetting) -> dict[str, Any]:
} }
def ip6config_struct(config: IpConfig, setting: Ip6Setting) -> dict[str, Any]:
"""Return a dict with information about IPv6 configuration."""
return {
ATTR_METHOD: setting.method,
ATTR_ADDR_GEN_MODE: setting.addr_gen_mode,
ATTR_IP6_PRIVACY: setting.ip6_privacy,
ATTR_ADDRESS: [address.with_prefixlen for address in config.address],
ATTR_NAMESERVERS: [str(address) for address in config.nameservers],
ATTR_GATEWAY: str(config.gateway) if config.gateway else None,
ATTR_READY: config.ready,
}
def wifi_struct(config: WifiConfig) -> dict[str, Any]: def wifi_struct(config: WifiConfig) -> dict[str, Any]:
"""Return a dict with information about wifi configuration.""" """Return a dict with information about wifi configuration."""
return { return {
@ -132,10 +152,10 @@ def interface_struct(interface: Interface) -> dict[str, Any]:
ATTR_CONNECTED: interface.connected, ATTR_CONNECTED: interface.connected,
ATTR_PRIMARY: interface.primary, ATTR_PRIMARY: interface.primary,
ATTR_MAC: interface.mac, ATTR_MAC: interface.mac,
ATTR_IPV4: ipconfig_struct(interface.ipv4, interface.ipv4setting) ATTR_IPV4: ip4config_struct(interface.ipv4, interface.ipv4setting)
if interface.ipv4 and interface.ipv4setting if interface.ipv4 and interface.ipv4setting
else None, else None,
ATTR_IPV6: ipconfig_struct(interface.ipv6, interface.ipv6setting) ATTR_IPV6: ip6config_struct(interface.ipv6, interface.ipv6setting)
if interface.ipv6 and interface.ipv6setting if interface.ipv6 and interface.ipv6setting
else None, else None,
ATTR_WIFI: wifi_struct(interface.wifi) if interface.wifi else None, ATTR_WIFI: wifi_struct(interface.wifi) if interface.wifi else None,
@ -212,25 +232,31 @@ class APINetwork(CoreSysAttributes):
for key, config in body.items(): for key, config in body.items():
if key == ATTR_IPV4: if key == ATTR_IPV4:
interface.ipv4setting = IpSetting( interface.ipv4setting = IpSetting(
config.get(ATTR_METHOD, InterfaceMethod.STATIC), method=config.get(ATTR_METHOD, InterfaceMethod.STATIC),
config.get(ATTR_ADDRESS, []), address=config.get(ATTR_ADDRESS, []),
config.get(ATTR_GATEWAY), gateway=config.get(ATTR_GATEWAY),
config.get(ATTR_NAMESERVERS, []), nameservers=config.get(ATTR_NAMESERVERS, []),
) )
elif key == ATTR_IPV6: elif key == ATTR_IPV6:
interface.ipv6setting = IpSetting( interface.ipv6setting = Ip6Setting(
config.get(ATTR_METHOD, InterfaceMethod.STATIC), method=config.get(ATTR_METHOD, InterfaceMethod.STATIC),
config.get(ATTR_ADDRESS, []), addr_gen_mode=config.get(
config.get(ATTR_GATEWAY), ATTR_ADDR_GEN_MODE, InterfaceAddrGenMode.DEFAULT
config.get(ATTR_NAMESERVERS, []), ),
ip6_privacy=config.get(
ATTR_IP6_PRIVACY, InterfaceIp6Privacy.DEFAULT
),
address=config.get(ATTR_ADDRESS, []),
gateway=config.get(ATTR_GATEWAY),
nameservers=config.get(ATTR_NAMESERVERS, []),
) )
elif key == ATTR_WIFI: elif key == ATTR_WIFI:
interface.wifi = WifiConfig( interface.wifi = WifiConfig(
config.get(ATTR_MODE, WifiMode.INFRASTRUCTURE), mode=config.get(ATTR_MODE, WifiMode.INFRASTRUCTURE),
config.get(ATTR_SSID, ""), ssid=config.get(ATTR_SSID, ""),
config.get(ATTR_AUTH, AuthMethod.OPEN), auth=config.get(ATTR_AUTH, AuthMethod.OPEN),
config.get(ATTR_PSK, None), psk=config.get(ATTR_PSK, None),
None, signal=None,
) )
elif key == ATTR_ENABLED: elif key == ATTR_ENABLED:
interface.enabled = config interface.enabled = config
@ -277,19 +303,25 @@ class APINetwork(CoreSysAttributes):
ipv4_setting = None ipv4_setting = None
if ATTR_IPV4 in body: if ATTR_IPV4 in body:
ipv4_setting = IpSetting( ipv4_setting = IpSetting(
body[ATTR_IPV4].get(ATTR_METHOD, InterfaceMethod.AUTO), method=body[ATTR_IPV4].get(ATTR_METHOD, InterfaceMethod.AUTO),
body[ATTR_IPV4].get(ATTR_ADDRESS, []), address=body[ATTR_IPV4].get(ATTR_ADDRESS, []),
body[ATTR_IPV4].get(ATTR_GATEWAY, None), gateway=body[ATTR_IPV4].get(ATTR_GATEWAY, None),
body[ATTR_IPV4].get(ATTR_NAMESERVERS, []), nameservers=body[ATTR_IPV4].get(ATTR_NAMESERVERS, []),
) )
ipv6_setting = None ipv6_setting = None
if ATTR_IPV6 in body: if ATTR_IPV6 in body:
ipv6_setting = IpSetting( ipv6_setting = Ip6Setting(
body[ATTR_IPV6].get(ATTR_METHOD, InterfaceMethod.AUTO), method=body[ATTR_IPV6].get(ATTR_METHOD, InterfaceMethod.AUTO),
body[ATTR_IPV6].get(ATTR_ADDRESS, []), addr_gen_mode=body[ATTR_IPV6].get(
body[ATTR_IPV6].get(ATTR_GATEWAY, None), ATTR_ADDR_GEN_MODE, InterfaceAddrGenMode.DEFAULT
body[ATTR_IPV6].get(ATTR_NAMESERVERS, []), ),
ip6_privacy=body[ATTR_IPV6].get(
ATTR_IP6_PRIVACY, InterfaceIp6Privacy.DEFAULT
),
address=body[ATTR_IPV6].get(ATTR_ADDRESS, []),
gateway=body[ATTR_IPV6].get(ATTR_GATEWAY, None),
nameservers=body[ATTR_IPV6].get(ATTR_NAMESERVERS, []),
) )
vlan_interface = Interface( vlan_interface = Interface(

View File

@ -97,6 +97,7 @@ ATTR_ADDON = "addon"
ATTR_ADDONS = "addons" ATTR_ADDONS = "addons"
ATTR_ADDONS_CUSTOM_LIST = "addons_custom_list" ATTR_ADDONS_CUSTOM_LIST = "addons_custom_list"
ATTR_ADDONS_REPOSITORIES = "addons_repositories" ATTR_ADDONS_REPOSITORIES = "addons_repositories"
ATTR_ADDR_GEN_MODE = "addr_gen_mode"
ATTR_ADDRESS = "address" ATTR_ADDRESS = "address"
ATTR_ADDRESS_DATA = "address-data" ATTR_ADDRESS_DATA = "address-data"
ATTR_ADMIN = "admin" ATTR_ADMIN = "admin"
@ -220,6 +221,7 @@ ATTR_INSTALLED = "installed"
ATTR_INTERFACE = "interface" ATTR_INTERFACE = "interface"
ATTR_INTERFACES = "interfaces" ATTR_INTERFACES = "interfaces"
ATTR_IP_ADDRESS = "ip_address" ATTR_IP_ADDRESS = "ip_address"
ATTR_IP6_PRIVACY = "ip6_privacy"
ATTR_IPV4 = "ipv4" ATTR_IPV4 = "ipv4"
ATTR_IPV6 = "ipv6" ATTR_IPV6 = "ipv6"
ATTR_ISSUES = "issues" ATTR_ISSUES = "issues"

View File

@ -210,6 +210,24 @@ class InterfaceMethod(StrEnum):
LINK_LOCAL = "link-local" LINK_LOCAL = "link-local"
class InterfaceAddrGenMode(IntEnum):
"""Interface addr_gen_mode."""
EUI64 = 0
STABLE_PRIVACY = 1
DEFAULT_OR_EUI64 = 2
DEFAULT = 3
class InterfaceIp6Privacy(IntEnum):
"""Interface ip6_privacy."""
DEFAULT = -1
DISABLED = 0
ENABLED_PREFER_PUBLIC = 1
ENABLED = 2
class ConnectionType(StrEnum): class ConnectionType(StrEnum):
"""Connection type.""" """Connection type."""

View File

@ -77,6 +77,14 @@ class IpProperties:
dns: list[bytes | int] | None dns: list[bytes | int] | None
@dataclass(slots=True)
class Ip6Properties(IpProperties):
"""IPv6 properties object for Network Manager."""
addr_gen_mode: int
ip6_privacy: int
@dataclass(slots=True) @dataclass(slots=True)
class MatchProperties: class MatchProperties:
"""Match properties object for Network Manager.""" """Match properties object for Network Manager."""

View File

@ -12,6 +12,7 @@ from ...utils import dbus_connected
from ..configuration import ( from ..configuration import (
ConnectionProperties, ConnectionProperties,
EthernetProperties, EthernetProperties,
Ip6Properties,
IpAddress, IpAddress,
IpProperties, IpProperties,
MatchProperties, MatchProperties,
@ -58,6 +59,8 @@ CONF_ATTR_IPV4_GATEWAY = "gateway"
CONF_ATTR_IPV4_DNS = "dns" CONF_ATTR_IPV4_DNS = "dns"
CONF_ATTR_IPV6_METHOD = "method" CONF_ATTR_IPV6_METHOD = "method"
CONF_ATTR_IPV6_ADDR_GEN_MODE = "addr-gen-mode"
CONF_ATTR_IPV6_PRIVACY = "ip6-privacy"
CONF_ATTR_IPV6_ADDRESS_DATA = "address-data" CONF_ATTR_IPV6_ADDRESS_DATA = "address-data"
CONF_ATTR_IPV6_GATEWAY = "gateway" CONF_ATTR_IPV6_GATEWAY = "gateway"
CONF_ATTR_IPV6_DNS = "dns" CONF_ATTR_IPV6_DNS = "dns"
@ -69,6 +72,8 @@ IPV4_6_IGNORE_FIELDS = [
"dns-data", "dns-data",
"gateway", "gateway",
"method", "method",
"addr-gen-mode",
"ip6-privacy",
] ]
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -111,7 +116,7 @@ class NetworkSetting(DBusInterface):
self._ethernet: EthernetProperties | None = None self._ethernet: EthernetProperties | None = None
self._vlan: VlanProperties | None = None self._vlan: VlanProperties | None = None
self._ipv4: IpProperties | None = None self._ipv4: IpProperties | None = None
self._ipv6: IpProperties | None = None self._ipv6: Ip6Properties | None = None
self._match: MatchProperties | None = None self._match: MatchProperties | None = None
super().__init__() super().__init__()
@ -151,7 +156,7 @@ class NetworkSetting(DBusInterface):
return self._ipv4 return self._ipv4
@property @property
def ipv6(self) -> IpProperties | None: def ipv6(self) -> Ip6Properties | None:
"""Return ipv6 properties if any.""" """Return ipv6 properties if any."""
return self._ipv6 return self._ipv6
@ -223,44 +228,52 @@ class NetworkSetting(DBusInterface):
# See: https://developer-old.gnome.org/NetworkManager/stable/ch01.html # See: https://developer-old.gnome.org/NetworkManager/stable/ch01.html
if CONF_ATTR_CONNECTION in data: if CONF_ATTR_CONNECTION in data:
self._connection = ConnectionProperties( self._connection = ConnectionProperties(
data[CONF_ATTR_CONNECTION].get(CONF_ATTR_CONNECTION_ID), id=data[CONF_ATTR_CONNECTION].get(CONF_ATTR_CONNECTION_ID),
data[CONF_ATTR_CONNECTION].get(CONF_ATTR_CONNECTION_UUID), uuid=data[CONF_ATTR_CONNECTION].get(CONF_ATTR_CONNECTION_UUID),
data[CONF_ATTR_CONNECTION].get(CONF_ATTR_CONNECTION_TYPE), type=data[CONF_ATTR_CONNECTION].get(CONF_ATTR_CONNECTION_TYPE),
data[CONF_ATTR_CONNECTION].get(CONF_ATTR_CONNECTION_INTERFACE_NAME), interface_name=data[CONF_ATTR_CONNECTION].get(
CONF_ATTR_CONNECTION_INTERFACE_NAME
),
) )
if CONF_ATTR_802_ETHERNET in data: if CONF_ATTR_802_ETHERNET in data:
self._ethernet = EthernetProperties( self._ethernet = EthernetProperties(
data[CONF_ATTR_802_ETHERNET].get(CONF_ATTR_802_ETHERNET_ASSIGNED_MAC), assigned_mac=data[CONF_ATTR_802_ETHERNET].get(
CONF_ATTR_802_ETHERNET_ASSIGNED_MAC
),
) )
if CONF_ATTR_802_WIRELESS in data: if CONF_ATTR_802_WIRELESS in data:
self._wireless = WirelessProperties( self._wireless = WirelessProperties(
bytes( ssid=bytes(
data[CONF_ATTR_802_WIRELESS].get(CONF_ATTR_802_WIRELESS_SSID, []) data[CONF_ATTR_802_WIRELESS].get(CONF_ATTR_802_WIRELESS_SSID, [])
).decode(), ).decode(),
data[CONF_ATTR_802_WIRELESS].get(CONF_ATTR_802_WIRELESS_ASSIGNED_MAC), assigned_mac=data[CONF_ATTR_802_WIRELESS].get(
data[CONF_ATTR_802_WIRELESS].get(CONF_ATTR_802_WIRELESS_MODE), CONF_ATTR_802_WIRELESS_ASSIGNED_MAC
data[CONF_ATTR_802_WIRELESS].get(CONF_ATTR_802_WIRELESS_POWERSAVE), ),
mode=data[CONF_ATTR_802_WIRELESS].get(CONF_ATTR_802_WIRELESS_MODE),
powersave=data[CONF_ATTR_802_WIRELESS].get(
CONF_ATTR_802_WIRELESS_POWERSAVE
),
) )
if CONF_ATTR_802_WIRELESS_SECURITY in data: if CONF_ATTR_802_WIRELESS_SECURITY in data:
self._wireless_security = WirelessSecurityProperties( self._wireless_security = WirelessSecurityProperties(
data[CONF_ATTR_802_WIRELESS_SECURITY].get( auth_alg=data[CONF_ATTR_802_WIRELESS_SECURITY].get(
CONF_ATTR_802_WIRELESS_SECURITY_AUTH_ALG CONF_ATTR_802_WIRELESS_SECURITY_AUTH_ALG
), ),
data[CONF_ATTR_802_WIRELESS_SECURITY].get( key_mgmt=data[CONF_ATTR_802_WIRELESS_SECURITY].get(
CONF_ATTR_802_WIRELESS_SECURITY_KEY_MGMT CONF_ATTR_802_WIRELESS_SECURITY_KEY_MGMT
), ),
data[CONF_ATTR_802_WIRELESS_SECURITY].get( psk=data[CONF_ATTR_802_WIRELESS_SECURITY].get(
CONF_ATTR_802_WIRELESS_SECURITY_PSK CONF_ATTR_802_WIRELESS_SECURITY_PSK
), ),
) )
if CONF_ATTR_VLAN in data: if CONF_ATTR_VLAN in data:
self._vlan = VlanProperties( self._vlan = VlanProperties(
data[CONF_ATTR_VLAN].get(CONF_ATTR_VLAN_ID), id=data[CONF_ATTR_VLAN].get(CONF_ATTR_VLAN_ID),
data[CONF_ATTR_VLAN].get(CONF_ATTR_VLAN_PARENT), parent=data[CONF_ATTR_VLAN].get(CONF_ATTR_VLAN_PARENT),
) )
if CONF_ATTR_IPV4 in data: if CONF_ATTR_IPV4 in data:
@ -268,21 +281,23 @@ class NetworkSetting(DBusInterface):
if ips := data[CONF_ATTR_IPV4].get(CONF_ATTR_IPV4_ADDRESS_DATA): if ips := data[CONF_ATTR_IPV4].get(CONF_ATTR_IPV4_ADDRESS_DATA):
address_data = [IpAddress(ip["address"], ip["prefix"]) for ip in ips] address_data = [IpAddress(ip["address"], ip["prefix"]) for ip in ips]
self._ipv4 = IpProperties( self._ipv4 = IpProperties(
data[CONF_ATTR_IPV4].get(CONF_ATTR_IPV4_METHOD), method=data[CONF_ATTR_IPV4].get(CONF_ATTR_IPV4_METHOD),
address_data, address_data=address_data,
data[CONF_ATTR_IPV4].get(CONF_ATTR_IPV4_GATEWAY), gateway=data[CONF_ATTR_IPV4].get(CONF_ATTR_IPV4_GATEWAY),
data[CONF_ATTR_IPV4].get(CONF_ATTR_IPV4_DNS), dns=data[CONF_ATTR_IPV4].get(CONF_ATTR_IPV4_DNS),
) )
if CONF_ATTR_IPV6 in data: if CONF_ATTR_IPV6 in data:
address_data = None address_data = None
if ips := data[CONF_ATTR_IPV6].get(CONF_ATTR_IPV6_ADDRESS_DATA): if ips := data[CONF_ATTR_IPV6].get(CONF_ATTR_IPV6_ADDRESS_DATA):
address_data = [IpAddress(ip["address"], ip["prefix"]) for ip in ips] address_data = [IpAddress(ip["address"], ip["prefix"]) for ip in ips]
self._ipv6 = IpProperties( self._ipv6 = Ip6Properties(
data[CONF_ATTR_IPV6].get(CONF_ATTR_IPV6_METHOD), method=data[CONF_ATTR_IPV6].get(CONF_ATTR_IPV6_METHOD),
address_data, addr_gen_mode=data[CONF_ATTR_IPV6].get(CONF_ATTR_IPV6_ADDR_GEN_MODE),
data[CONF_ATTR_IPV6].get(CONF_ATTR_IPV6_GATEWAY), ip6_privacy=data[CONF_ATTR_IPV6].get(CONF_ATTR_IPV6_PRIVACY),
data[CONF_ATTR_IPV6].get(CONF_ATTR_IPV6_DNS), address_data=address_data,
gateway=data[CONF_ATTR_IPV6].get(CONF_ATTR_IPV6_GATEWAY),
dns=data[CONF_ATTR_IPV6].get(CONF_ATTR_IPV6_DNS),
) )
if CONF_ATTR_MATCH in data: if CONF_ATTR_MATCH in data:

View File

@ -8,8 +8,13 @@ from uuid import uuid4
from dbus_fast import Variant from dbus_fast import Variant
from ....host.configuration import VlanConfig from ....host.configuration import Ip6Setting, IpSetting, VlanConfig
from ....host.const import InterfaceMethod, InterfaceType from ....host.const import (
InterfaceAddrGenMode,
InterfaceIp6Privacy,
InterfaceMethod,
InterfaceType,
)
from .. import NetworkManager from .. import NetworkManager
from . import ( from . import (
CONF_ATTR_802_ETHERNET, CONF_ATTR_802_ETHERNET,
@ -36,10 +41,12 @@ from . import (
CONF_ATTR_IPV4_GATEWAY, CONF_ATTR_IPV4_GATEWAY,
CONF_ATTR_IPV4_METHOD, CONF_ATTR_IPV4_METHOD,
CONF_ATTR_IPV6, CONF_ATTR_IPV6,
CONF_ATTR_IPV6_ADDR_GEN_MODE,
CONF_ATTR_IPV6_ADDRESS_DATA, CONF_ATTR_IPV6_ADDRESS_DATA,
CONF_ATTR_IPV6_DNS, CONF_ATTR_IPV6_DNS,
CONF_ATTR_IPV6_GATEWAY, CONF_ATTR_IPV6_GATEWAY,
CONF_ATTR_IPV6_METHOD, CONF_ATTR_IPV6_METHOD,
CONF_ATTR_IPV6_PRIVACY,
CONF_ATTR_MATCH, CONF_ATTR_MATCH,
CONF_ATTR_MATCH_PATH, CONF_ATTR_MATCH_PATH,
CONF_ATTR_VLAN, CONF_ATTR_VLAN,
@ -51,7 +58,7 @@ if TYPE_CHECKING:
from ....host.configuration import Interface from ....host.configuration import Interface
def _get_ipv4_connection_settings(ipv4setting) -> dict: def _get_ipv4_connection_settings(ipv4setting: IpSetting | None) -> dict:
ipv4 = {} ipv4 = {}
if not ipv4setting or ipv4setting.method == InterfaceMethod.AUTO: if not ipv4setting or ipv4setting.method == InterfaceMethod.AUTO:
ipv4[CONF_ATTR_IPV4_METHOD] = Variant("s", "auto") ipv4[CONF_ATTR_IPV4_METHOD] = Variant("s", "auto")
@ -93,10 +100,32 @@ def _get_ipv4_connection_settings(ipv4setting) -> dict:
return ipv4 return ipv4
def _get_ipv6_connection_settings(ipv6setting) -> dict: def _get_ipv6_connection_settings(
ipv6setting: Ip6Setting | None, support_addr_gen_mode_defaults: bool = False
) -> dict:
ipv6 = {} ipv6 = {}
if not ipv6setting or ipv6setting.method == InterfaceMethod.AUTO: if not ipv6setting or ipv6setting.method == InterfaceMethod.AUTO:
ipv6[CONF_ATTR_IPV6_METHOD] = Variant("s", "auto") ipv6[CONF_ATTR_IPV6_METHOD] = Variant("s", "auto")
if ipv6setting:
if ipv6setting.addr_gen_mode == InterfaceAddrGenMode.EUI64:
ipv6[CONF_ATTR_IPV6_ADDR_GEN_MODE] = Variant("i", 0)
elif (
not support_addr_gen_mode_defaults
or ipv6setting.addr_gen_mode == InterfaceAddrGenMode.STABLE_PRIVACY
):
ipv6[CONF_ATTR_IPV6_ADDR_GEN_MODE] = Variant("i", 1)
elif ipv6setting.addr_gen_mode == InterfaceAddrGenMode.DEFAULT_OR_EUI64:
ipv6[CONF_ATTR_IPV6_ADDR_GEN_MODE] = Variant("i", 2)
else:
ipv6[CONF_ATTR_IPV6_ADDR_GEN_MODE] = Variant("i", 3)
if ipv6setting.ip6_privacy == InterfaceIp6Privacy.DISABLED:
ipv6[CONF_ATTR_IPV6_PRIVACY] = Variant("i", 0)
elif ipv6setting.ip6_privacy == InterfaceIp6Privacy.ENABLED_PREFER_PUBLIC:
ipv6[CONF_ATTR_IPV6_PRIVACY] = Variant("i", 1)
elif ipv6setting.ip6_privacy == InterfaceIp6Privacy.ENABLED:
ipv6[CONF_ATTR_IPV6_PRIVACY] = Variant("i", 2)
else:
ipv6[CONF_ATTR_IPV6_PRIVACY] = Variant("i", -1)
elif ipv6setting.method == InterfaceMethod.DISABLED: elif ipv6setting.method == InterfaceMethod.DISABLED:
ipv6[CONF_ATTR_IPV6_METHOD] = Variant("s", "link-local") ipv6[CONF_ATTR_IPV6_METHOD] = Variant("s", "link-local")
elif ipv6setting.method == InterfaceMethod.STATIC: elif ipv6setting.method == InterfaceMethod.STATIC:
@ -183,7 +212,9 @@ def get_connection_from_interface(
conn[CONF_ATTR_IPV4] = _get_ipv4_connection_settings(interface.ipv4setting) conn[CONF_ATTR_IPV4] = _get_ipv4_connection_settings(interface.ipv4setting)
conn[CONF_ATTR_IPV6] = _get_ipv6_connection_settings(interface.ipv6setting) conn[CONF_ATTR_IPV6] = _get_ipv6_connection_settings(
interface.ipv6setting, network_manager.version >= "1.40.0"
)
if interface.type == InterfaceType.ETHERNET: if interface.type == InterfaceType.ETHERNET:
conn[CONF_ATTR_802_ETHERNET] = { conn[CONF_ATTR_802_ETHERNET] = {

View File

@ -8,11 +8,20 @@ from ..dbus.const import (
ConnectionStateFlags, ConnectionStateFlags,
ConnectionStateType, ConnectionStateType,
DeviceType, DeviceType,
InterfaceAddrGenMode as NMInterfaceAddrGenMode,
InterfaceIp6Privacy as NMInterfaceIp6Privacy,
InterfaceMethod as NMInterfaceMethod, InterfaceMethod as NMInterfaceMethod,
) )
from ..dbus.network.connection import NetworkConnection from ..dbus.network.connection import NetworkConnection
from ..dbus.network.interface import NetworkInterface from ..dbus.network.interface import NetworkInterface
from .const import AuthMethod, InterfaceMethod, InterfaceType, WifiMode from .const import (
AuthMethod,
InterfaceAddrGenMode,
InterfaceIp6Privacy,
InterfaceMethod,
InterfaceType,
WifiMode,
)
@dataclass(slots=True) @dataclass(slots=True)
@ -46,6 +55,14 @@ class IpSetting:
nameservers: list[IPv4Address | IPv6Address] nameservers: list[IPv4Address | IPv6Address]
@dataclass(slots=True)
class Ip6Setting(IpSetting):
"""Represent a user IPv6 setting."""
addr_gen_mode: InterfaceAddrGenMode = InterfaceAddrGenMode.DEFAULT
ip6_privacy: InterfaceIp6Privacy = InterfaceIp6Privacy.DEFAULT
@dataclass(slots=True) @dataclass(slots=True)
class WifiConfig: class WifiConfig:
"""Represent a wifi configuration.""" """Represent a wifi configuration."""
@ -79,7 +96,7 @@ class Interface:
ipv4: IpConfig | None ipv4: IpConfig | None
ipv4setting: IpSetting | None ipv4setting: IpSetting | None
ipv6: IpConfig | None ipv6: IpConfig | None
ipv6setting: IpSetting | None ipv6setting: Ip6Setting | None
wifi: WifiConfig | None wifi: WifiConfig | None
vlan: VlanConfig | None vlan: VlanConfig | None
@ -118,8 +135,14 @@ class Interface:
ipv4_setting = IpSetting(InterfaceMethod.DISABLED, [], None, []) ipv4_setting = IpSetting(InterfaceMethod.DISABLED, [], None, [])
if inet.settings and inet.settings.ipv6: if inet.settings and inet.settings.ipv6:
ipv6_setting = IpSetting( ipv6_setting = Ip6Setting(
method=Interface._map_nm_method(inet.settings.ipv6.method), method=Interface._map_nm_method(inet.settings.ipv6.method),
addr_gen_mode=Interface._map_nm_addr_gen_mode(
inet.settings.ipv6.addr_gen_mode
),
ip6_privacy=Interface._map_nm_ip6_privacy(
inet.settings.ipv6.ip6_privacy
),
address=[ address=[
IPv6Interface(f"{ip.address}/{ip.prefix}") IPv6Interface(f"{ip.address}/{ip.prefix}")
for ip in inet.settings.ipv6.address_data for ip in inet.settings.ipv6.address_data
@ -134,7 +157,7 @@ class Interface:
else [], else [],
) )
else: else:
ipv6_setting = IpSetting(InterfaceMethod.DISABLED, [], None, []) ipv6_setting = Ip6Setting(InterfaceMethod.DISABLED, [], None, [])
ipv4_ready = ( ipv4_ready = (
bool(inet.connection) bool(inet.connection)
@ -195,6 +218,28 @@ class Interface:
return mapping.get(method, InterfaceMethod.DISABLED) return mapping.get(method, InterfaceMethod.DISABLED)
@staticmethod
def _map_nm_addr_gen_mode(addr_gen_mode: int) -> InterfaceAddrGenMode:
"""Map IPv6 interface addr_gen_mode."""
mapping = {
NMInterfaceAddrGenMode.EUI64: InterfaceAddrGenMode.EUI64,
NMInterfaceAddrGenMode.STABLE_PRIVACY: InterfaceAddrGenMode.STABLE_PRIVACY,
NMInterfaceAddrGenMode.DEFAULT_OR_EUI64: InterfaceAddrGenMode.DEFAULT_OR_EUI64,
}
return mapping.get(addr_gen_mode, InterfaceAddrGenMode.DEFAULT)
@staticmethod
def _map_nm_ip6_privacy(ip6_privacy: int) -> InterfaceIp6Privacy:
"""Map IPv6 interface ip6_privacy."""
mapping = {
NMInterfaceIp6Privacy.DISABLED: InterfaceIp6Privacy.DISABLED,
NMInterfaceIp6Privacy.ENABLED_PREFER_PUBLIC: InterfaceIp6Privacy.ENABLED_PREFER_PUBLIC,
NMInterfaceIp6Privacy.ENABLED: InterfaceIp6Privacy.ENABLED,
}
return mapping.get(ip6_privacy, InterfaceIp6Privacy.DEFAULT)
@staticmethod @staticmethod
def _map_nm_connected(connection: NetworkConnection | None) -> bool: def _map_nm_connected(connection: NetworkConnection | None) -> bool:
"""Map connectivity state.""" """Map connectivity state."""

View File

@ -15,6 +15,24 @@ class InterfaceMethod(StrEnum):
AUTO = "auto" AUTO = "auto"
class InterfaceAddrGenMode(StrEnum):
"""Configuration of an interface."""
EUI64 = "eui64"
STABLE_PRIVACY = "stable-privacy"
DEFAULT_OR_EUI64 = "default-or-eui64"
DEFAULT = "default"
class InterfaceIp6Privacy(StrEnum):
"""Configuration of an interface."""
DEFAULT = "default"
DISABLED = "disabled"
ENABLED_PREFER_PUBLIC = "enabled-prefer-public"
ENABLED = "enabled"
class InterfaceType(StrEnum): class InterfaceType(StrEnum):
"""Configuration of an interface.""" """Configuration of an interface."""

View File

@ -51,8 +51,10 @@ async def test_api_network_info(api_client: TestClient, coresys: CoreSys):
"ready": False, "ready": False,
} }
assert interface["ipv6"] == { assert interface["ipv6"] == {
"addr_gen_mode": "default",
"address": [], "address": [],
"gateway": None, "gateway": None,
"ip6_privacy": "default",
"method": "disabled", "method": "disabled",
"nameservers": [], "nameservers": [],
"ready": False, "ready": False,

View File

@ -5,7 +5,7 @@ from unittest.mock import PropertyMock, patch
from supervisor.dbus.network import NetworkManager from supervisor.dbus.network import NetworkManager
from supervisor.dbus.network.interface import NetworkInterface from supervisor.dbus.network.interface import NetworkInterface
from supervisor.dbus.network.setting.generate import get_connection_from_interface from supervisor.dbus.network.setting.generate import get_connection_from_interface
from supervisor.host.configuration import IpConfig, IpSetting, VlanConfig from supervisor.host.configuration import Ip6Setting, IpConfig, IpSetting, VlanConfig
from supervisor.host.const import InterfaceMethod, InterfaceType from supervisor.host.const import InterfaceMethod, InterfaceType
from supervisor.host.network import Interface from supervisor.host.network import Interface
@ -57,8 +57,8 @@ async def test_generate_from_vlan(network_manager: NetworkManager):
type=InterfaceType.VLAN, type=InterfaceType.VLAN,
ipv4=IpConfig([], None, [], None), ipv4=IpConfig([], None, [], None),
ipv4setting=IpSetting(InterfaceMethod.AUTO, [], None, []), ipv4setting=IpSetting(InterfaceMethod.AUTO, [], None, []),
ipv6=None, ipv6=IpConfig([], None, [], None),
ipv6setting=None, ipv6setting=Ip6Setting(InterfaceMethod.AUTO, [], None, []),
wifi=None, wifi=None,
vlan=VlanConfig(1, "eth0"), vlan=VlanConfig(1, "eth0"),
) )
@ -70,6 +70,8 @@ async def test_generate_from_vlan(network_manager: NetworkManager):
assert "match" not in connection_payload["connection"] assert "match" not in connection_payload["connection"]
assert "interface-name" not in connection_payload["connection"] assert "interface-name" not in connection_payload["connection"]
assert connection_payload["ipv4"]["method"].value == "auto" assert connection_payload["ipv4"]["method"].value == "auto"
assert connection_payload["ipv6"]["addr-gen-mode"].value == 1
assert connection_payload["ipv6"]["ip6-privacy"].value == -1
assert connection_payload["vlan"]["id"].value == 1 assert connection_payload["vlan"]["id"].value == 1
assert ( assert (

View File

@ -1,14 +1,17 @@
"""Test Network Manager Connection object.""" """Test Network Manager Connection object."""
from unittest.mock import MagicMock from unittest.mock import MagicMock, PropertyMock
from awesomeversion import AwesomeVersion
from dbus_fast import Variant from dbus_fast import Variant
from dbus_fast.aio.message_bus import MessageBus from dbus_fast.aio.message_bus import MessageBus
import pytest import pytest
from supervisor.dbus.network import NetworkManager
from supervisor.dbus.network.interface import NetworkInterface from supervisor.dbus.network.interface import NetworkInterface
from supervisor.dbus.network.setting import NetworkSetting from supervisor.dbus.network.setting import NetworkSetting
from supervisor.dbus.network.setting.generate import get_connection_from_interface from supervisor.dbus.network.setting.generate import get_connection_from_interface
from supervisor.host.configuration import Ip6Setting
from supervisor.host.const import InterfaceMethod from supervisor.host.const import InterfaceMethod
from supervisor.host.network import Interface from supervisor.host.network import Interface
@ -48,6 +51,7 @@ async def fixture_dbus_interface(
async def test_ethernet_update( async def test_ethernet_update(
dbus_interface: NetworkInterface, dbus_interface: NetworkInterface,
connection_settings_service: ConnectionSettingsService, connection_settings_service: ConnectionSettingsService,
network_manager: NetworkManager,
): ):
"""Test network manager update.""" """Test network manager update."""
connection_settings_service.Update.calls.clear() connection_settings_service.Update.calls.clear()
@ -55,7 +59,7 @@ async def test_ethernet_update(
interface = Interface.from_dbus_interface(dbus_interface) interface = Interface.from_dbus_interface(dbus_interface)
conn = get_connection_from_interface( conn = get_connection_from_interface(
interface, interface,
MagicMock(), network_manager,
name=dbus_interface.settings.connection.id, name=dbus_interface.settings.connection.id,
uuid=dbus_interface.settings.connection.uuid, uuid=dbus_interface.settings.connection.uuid,
) )
@ -124,14 +128,16 @@ async def test_ethernet_update(
assert "802-11-wireless-security" not in settings assert "802-11-wireless-security" not in settings
async def test_ipv6_disabled_is_link_local(dbus_interface: NetworkInterface): async def test_ipv6_disabled_is_link_local(
dbus_interface: NetworkInterface, network_manager: NetworkManager
):
"""Test disabled equals link local for ipv6.""" """Test disabled equals link local for ipv6."""
interface = Interface.from_dbus_interface(dbus_interface) interface = Interface.from_dbus_interface(dbus_interface)
interface.ipv4setting.method = InterfaceMethod.DISABLED interface.ipv4setting.method = InterfaceMethod.DISABLED
interface.ipv6setting.method = InterfaceMethod.DISABLED interface.ipv6setting.method = InterfaceMethod.DISABLED
conn = get_connection_from_interface( conn = get_connection_from_interface(
interface, interface,
MagicMock(), network_manager,
name=dbus_interface.settings.connection.id, name=dbus_interface.settings.connection.id,
uuid=dbus_interface.settings.connection.uuid, uuid=dbus_interface.settings.connection.uuid,
) )
@ -140,6 +146,33 @@ async def test_ipv6_disabled_is_link_local(dbus_interface: NetworkInterface):
assert conn["ipv6"]["method"] == Variant("s", "link-local") assert conn["ipv6"]["method"] == Variant("s", "link-local")
@pytest.mark.parametrize(
["version", "addr_gen_mode"],
[
("1.38.0", 1),
("1.40.0", 3),
],
)
async def test_ipv6_addr_gen_mode(
dbus_interface: NetworkInterface, version: str, addr_gen_mode: int
):
"""Test addr_gen_mode with various NetworkManager versions."""
interface = Interface.from_dbus_interface(dbus_interface)
interface.ipv6setting = Ip6Setting(InterfaceMethod.AUTO, [], None, [])
network_manager = MagicMock()
type(network_manager).version = PropertyMock(return_value=AwesomeVersion(version))
conn = get_connection_from_interface(
interface,
network_manager,
name=dbus_interface.settings.connection.id,
uuid=dbus_interface.settings.connection.uuid,
)
assert conn["ipv6"]["method"] == Variant("s", "auto")
assert conn["ipv6"]["addr-gen-mode"] == Variant("i", addr_gen_mode)
async def test_watching_updated_signal( async def test_watching_updated_signal(
connection_settings_service: ConnectionSettingsService, dbus_session_bus: MessageBus connection_settings_service: ConnectionSettingsService, dbus_session_bus: MessageBus
): ):