diff --git a/supervisor/api/network.py b/supervisor/api/network.py index 020ed7acd..6500700ae 100644 --- a/supervisor/api/network.py +++ b/supervisor/api/network.py @@ -10,6 +10,7 @@ import voluptuous as vol from ..const import ( ATTR_ACCESSPOINTS, + ATTR_ADDR_GEN_MODE, ATTR_ADDRESS, ATTR_AUTH, ATTR_CONNECTED, @@ -22,6 +23,7 @@ from ..const import ( ATTR_ID, ATTR_INTERFACE, ATTR_INTERFACES, + ATTR_IP6_PRIVACY, ATTR_IPV4, ATTR_IPV6, ATTR_MAC, @@ -46,7 +48,10 @@ from ..exceptions import APIError, APINotFound, HostNetworkNotFound from ..host.configuration import ( AccessPoint, Interface, + InterfaceAddrGenMode, + InterfaceIp6Privacy, InterfaceMethod, + Ip6Setting, IpConfig, IpSetting, VlanConfig, @@ -68,6 +73,8 @@ _SCHEMA_IPV6_CONFIG = vol.Schema( { vol.Optional(ATTR_ADDRESS): [vol.Coerce(IPv6Interface)], 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_NAMESERVERS): [vol.Coerce(IPv6Address)], } @@ -94,8 +101,8 @@ SCHEMA_UPDATE = vol.Schema( ) -def ipconfig_struct(config: IpConfig, setting: IpSetting) -> dict[str, Any]: - """Return a dict with information about ip configuration.""" +def ip4config_struct(config: IpConfig, setting: IpSetting) -> dict[str, Any]: + """Return a dict with information about IPv4 configuration.""" return { ATTR_METHOD: setting.method, 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]: """Return a dict with information about wifi configuration.""" return { @@ -132,10 +152,10 @@ def interface_struct(interface: Interface) -> dict[str, Any]: ATTR_CONNECTED: interface.connected, ATTR_PRIMARY: interface.primary, 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 else None, - ATTR_IPV6: ipconfig_struct(interface.ipv6, interface.ipv6setting) + ATTR_IPV6: ip6config_struct(interface.ipv6, interface.ipv6setting) if interface.ipv6 and interface.ipv6setting 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(): if key == ATTR_IPV4: interface.ipv4setting = IpSetting( - config.get(ATTR_METHOD, InterfaceMethod.STATIC), - config.get(ATTR_ADDRESS, []), - config.get(ATTR_GATEWAY), - config.get(ATTR_NAMESERVERS, []), + method=config.get(ATTR_METHOD, InterfaceMethod.STATIC), + address=config.get(ATTR_ADDRESS, []), + gateway=config.get(ATTR_GATEWAY), + nameservers=config.get(ATTR_NAMESERVERS, []), ) elif key == ATTR_IPV6: - interface.ipv6setting = IpSetting( - config.get(ATTR_METHOD, InterfaceMethod.STATIC), - config.get(ATTR_ADDRESS, []), - config.get(ATTR_GATEWAY), - config.get(ATTR_NAMESERVERS, []), + interface.ipv6setting = Ip6Setting( + method=config.get(ATTR_METHOD, InterfaceMethod.STATIC), + addr_gen_mode=config.get( + ATTR_ADDR_GEN_MODE, InterfaceAddrGenMode.DEFAULT + ), + 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: interface.wifi = WifiConfig( - config.get(ATTR_MODE, WifiMode.INFRASTRUCTURE), - config.get(ATTR_SSID, ""), - config.get(ATTR_AUTH, AuthMethod.OPEN), - config.get(ATTR_PSK, None), - None, + mode=config.get(ATTR_MODE, WifiMode.INFRASTRUCTURE), + ssid=config.get(ATTR_SSID, ""), + auth=config.get(ATTR_AUTH, AuthMethod.OPEN), + psk=config.get(ATTR_PSK, None), + signal=None, ) elif key == ATTR_ENABLED: interface.enabled = config @@ -277,19 +303,25 @@ class APINetwork(CoreSysAttributes): ipv4_setting = None if ATTR_IPV4 in body: ipv4_setting = IpSetting( - body[ATTR_IPV4].get(ATTR_METHOD, InterfaceMethod.AUTO), - body[ATTR_IPV4].get(ATTR_ADDRESS, []), - body[ATTR_IPV4].get(ATTR_GATEWAY, None), - body[ATTR_IPV4].get(ATTR_NAMESERVERS, []), + method=body[ATTR_IPV4].get(ATTR_METHOD, InterfaceMethod.AUTO), + address=body[ATTR_IPV4].get(ATTR_ADDRESS, []), + gateway=body[ATTR_IPV4].get(ATTR_GATEWAY, None), + nameservers=body[ATTR_IPV4].get(ATTR_NAMESERVERS, []), ) ipv6_setting = None if ATTR_IPV6 in body: - ipv6_setting = IpSetting( - body[ATTR_IPV6].get(ATTR_METHOD, InterfaceMethod.AUTO), - body[ATTR_IPV6].get(ATTR_ADDRESS, []), - body[ATTR_IPV6].get(ATTR_GATEWAY, None), - body[ATTR_IPV6].get(ATTR_NAMESERVERS, []), + ipv6_setting = Ip6Setting( + method=body[ATTR_IPV6].get(ATTR_METHOD, InterfaceMethod.AUTO), + addr_gen_mode=body[ATTR_IPV6].get( + ATTR_ADDR_GEN_MODE, InterfaceAddrGenMode.DEFAULT + ), + 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( diff --git a/supervisor/const.py b/supervisor/const.py index 610dc51e4..a562a3e46 100644 --- a/supervisor/const.py +++ b/supervisor/const.py @@ -97,6 +97,7 @@ ATTR_ADDON = "addon" ATTR_ADDONS = "addons" ATTR_ADDONS_CUSTOM_LIST = "addons_custom_list" ATTR_ADDONS_REPOSITORIES = "addons_repositories" +ATTR_ADDR_GEN_MODE = "addr_gen_mode" ATTR_ADDRESS = "address" ATTR_ADDRESS_DATA = "address-data" ATTR_ADMIN = "admin" @@ -220,6 +221,7 @@ ATTR_INSTALLED = "installed" ATTR_INTERFACE = "interface" ATTR_INTERFACES = "interfaces" ATTR_IP_ADDRESS = "ip_address" +ATTR_IP6_PRIVACY = "ip6_privacy" ATTR_IPV4 = "ipv4" ATTR_IPV6 = "ipv6" ATTR_ISSUES = "issues" diff --git a/supervisor/dbus/const.py b/supervisor/dbus/const.py index 296f516b8..fc324d3b9 100644 --- a/supervisor/dbus/const.py +++ b/supervisor/dbus/const.py @@ -210,6 +210,24 @@ class InterfaceMethod(StrEnum): 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): """Connection type.""" diff --git a/supervisor/dbus/network/configuration.py b/supervisor/dbus/network/configuration.py index b78855f85..066fad41d 100644 --- a/supervisor/dbus/network/configuration.py +++ b/supervisor/dbus/network/configuration.py @@ -77,6 +77,14 @@ class IpProperties: 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) class MatchProperties: """Match properties object for Network Manager.""" diff --git a/supervisor/dbus/network/setting/__init__.py b/supervisor/dbus/network/setting/__init__.py index 707cdf342..8ad1c6ec4 100644 --- a/supervisor/dbus/network/setting/__init__.py +++ b/supervisor/dbus/network/setting/__init__.py @@ -12,6 +12,7 @@ from ...utils import dbus_connected from ..configuration import ( ConnectionProperties, EthernetProperties, + Ip6Properties, IpAddress, IpProperties, MatchProperties, @@ -58,6 +59,8 @@ CONF_ATTR_IPV4_GATEWAY = "gateway" CONF_ATTR_IPV4_DNS = "dns" 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_GATEWAY = "gateway" CONF_ATTR_IPV6_DNS = "dns" @@ -69,6 +72,8 @@ IPV4_6_IGNORE_FIELDS = [ "dns-data", "gateway", "method", + "addr-gen-mode", + "ip6-privacy", ] _LOGGER: logging.Logger = logging.getLogger(__name__) @@ -111,7 +116,7 @@ class NetworkSetting(DBusInterface): self._ethernet: EthernetProperties | None = None self._vlan: VlanProperties | None = None self._ipv4: IpProperties | None = None - self._ipv6: IpProperties | None = None + self._ipv6: Ip6Properties | None = None self._match: MatchProperties | None = None super().__init__() @@ -151,7 +156,7 @@ class NetworkSetting(DBusInterface): return self._ipv4 @property - def ipv6(self) -> IpProperties | None: + def ipv6(self) -> Ip6Properties | None: """Return ipv6 properties if any.""" return self._ipv6 @@ -223,44 +228,52 @@ class NetworkSetting(DBusInterface): # See: https://developer-old.gnome.org/NetworkManager/stable/ch01.html if CONF_ATTR_CONNECTION in data: self._connection = ConnectionProperties( - data[CONF_ATTR_CONNECTION].get(CONF_ATTR_CONNECTION_ID), - data[CONF_ATTR_CONNECTION].get(CONF_ATTR_CONNECTION_UUID), - data[CONF_ATTR_CONNECTION].get(CONF_ATTR_CONNECTION_TYPE), - data[CONF_ATTR_CONNECTION].get(CONF_ATTR_CONNECTION_INTERFACE_NAME), + id=data[CONF_ATTR_CONNECTION].get(CONF_ATTR_CONNECTION_ID), + uuid=data[CONF_ATTR_CONNECTION].get(CONF_ATTR_CONNECTION_UUID), + type=data[CONF_ATTR_CONNECTION].get(CONF_ATTR_CONNECTION_TYPE), + interface_name=data[CONF_ATTR_CONNECTION].get( + CONF_ATTR_CONNECTION_INTERFACE_NAME + ), ) if CONF_ATTR_802_ETHERNET in data: 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: self._wireless = WirelessProperties( - bytes( + ssid=bytes( data[CONF_ATTR_802_WIRELESS].get(CONF_ATTR_802_WIRELESS_SSID, []) ).decode(), - data[CONF_ATTR_802_WIRELESS].get(CONF_ATTR_802_WIRELESS_ASSIGNED_MAC), - data[CONF_ATTR_802_WIRELESS].get(CONF_ATTR_802_WIRELESS_MODE), - data[CONF_ATTR_802_WIRELESS].get(CONF_ATTR_802_WIRELESS_POWERSAVE), + assigned_mac=data[CONF_ATTR_802_WIRELESS].get( + CONF_ATTR_802_WIRELESS_ASSIGNED_MAC + ), + 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: 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 ), - data[CONF_ATTR_802_WIRELESS_SECURITY].get( + key_mgmt=data[CONF_ATTR_802_WIRELESS_SECURITY].get( 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 ), ) if CONF_ATTR_VLAN in data: self._vlan = VlanProperties( - data[CONF_ATTR_VLAN].get(CONF_ATTR_VLAN_ID), - data[CONF_ATTR_VLAN].get(CONF_ATTR_VLAN_PARENT), + id=data[CONF_ATTR_VLAN].get(CONF_ATTR_VLAN_ID), + parent=data[CONF_ATTR_VLAN].get(CONF_ATTR_VLAN_PARENT), ) 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): address_data = [IpAddress(ip["address"], ip["prefix"]) for ip in ips] self._ipv4 = IpProperties( - data[CONF_ATTR_IPV4].get(CONF_ATTR_IPV4_METHOD), - address_data, - data[CONF_ATTR_IPV4].get(CONF_ATTR_IPV4_GATEWAY), - data[CONF_ATTR_IPV4].get(CONF_ATTR_IPV4_DNS), + method=data[CONF_ATTR_IPV4].get(CONF_ATTR_IPV4_METHOD), + address_data=address_data, + gateway=data[CONF_ATTR_IPV4].get(CONF_ATTR_IPV4_GATEWAY), + dns=data[CONF_ATTR_IPV4].get(CONF_ATTR_IPV4_DNS), ) if CONF_ATTR_IPV6 in data: address_data = None if ips := data[CONF_ATTR_IPV6].get(CONF_ATTR_IPV6_ADDRESS_DATA): address_data = [IpAddress(ip["address"], ip["prefix"]) for ip in ips] - self._ipv6 = IpProperties( - data[CONF_ATTR_IPV6].get(CONF_ATTR_IPV6_METHOD), - address_data, - data[CONF_ATTR_IPV6].get(CONF_ATTR_IPV6_GATEWAY), - data[CONF_ATTR_IPV6].get(CONF_ATTR_IPV6_DNS), + self._ipv6 = Ip6Properties( + method=data[CONF_ATTR_IPV6].get(CONF_ATTR_IPV6_METHOD), + addr_gen_mode=data[CONF_ATTR_IPV6].get(CONF_ATTR_IPV6_ADDR_GEN_MODE), + ip6_privacy=data[CONF_ATTR_IPV6].get(CONF_ATTR_IPV6_PRIVACY), + 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: diff --git a/supervisor/dbus/network/setting/generate.py b/supervisor/dbus/network/setting/generate.py index bbd7b50f2..31849989f 100644 --- a/supervisor/dbus/network/setting/generate.py +++ b/supervisor/dbus/network/setting/generate.py @@ -8,8 +8,13 @@ from uuid import uuid4 from dbus_fast import Variant -from ....host.configuration import VlanConfig -from ....host.const import InterfaceMethod, InterfaceType +from ....host.configuration import Ip6Setting, IpSetting, VlanConfig +from ....host.const import ( + InterfaceAddrGenMode, + InterfaceIp6Privacy, + InterfaceMethod, + InterfaceType, +) from .. import NetworkManager from . import ( CONF_ATTR_802_ETHERNET, @@ -36,10 +41,12 @@ from . import ( CONF_ATTR_IPV4_GATEWAY, CONF_ATTR_IPV4_METHOD, CONF_ATTR_IPV6, + CONF_ATTR_IPV6_ADDR_GEN_MODE, CONF_ATTR_IPV6_ADDRESS_DATA, CONF_ATTR_IPV6_DNS, CONF_ATTR_IPV6_GATEWAY, CONF_ATTR_IPV6_METHOD, + CONF_ATTR_IPV6_PRIVACY, CONF_ATTR_MATCH, CONF_ATTR_MATCH_PATH, CONF_ATTR_VLAN, @@ -51,7 +58,7 @@ if TYPE_CHECKING: from ....host.configuration import Interface -def _get_ipv4_connection_settings(ipv4setting) -> dict: +def _get_ipv4_connection_settings(ipv4setting: IpSetting | None) -> dict: ipv4 = {} if not ipv4setting or ipv4setting.method == InterfaceMethod.AUTO: ipv4[CONF_ATTR_IPV4_METHOD] = Variant("s", "auto") @@ -93,10 +100,32 @@ def _get_ipv4_connection_settings(ipv4setting) -> dict: 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 = {} if not ipv6setting or ipv6setting.method == InterfaceMethod.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: ipv6[CONF_ATTR_IPV6_METHOD] = Variant("s", "link-local") 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_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: conn[CONF_ATTR_802_ETHERNET] = { diff --git a/supervisor/host/configuration.py b/supervisor/host/configuration.py index b3ec99dd5..ddf1d950e 100644 --- a/supervisor/host/configuration.py +++ b/supervisor/host/configuration.py @@ -8,11 +8,20 @@ from ..dbus.const import ( ConnectionStateFlags, ConnectionStateType, DeviceType, + InterfaceAddrGenMode as NMInterfaceAddrGenMode, + InterfaceIp6Privacy as NMInterfaceIp6Privacy, InterfaceMethod as NMInterfaceMethod, ) from ..dbus.network.connection import NetworkConnection 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) @@ -46,6 +55,14 @@ class IpSetting: 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) class WifiConfig: """Represent a wifi configuration.""" @@ -79,7 +96,7 @@ class Interface: ipv4: IpConfig | None ipv4setting: IpSetting | None ipv6: IpConfig | None - ipv6setting: IpSetting | None + ipv6setting: Ip6Setting | None wifi: WifiConfig | None vlan: VlanConfig | None @@ -118,8 +135,14 @@ class Interface: ipv4_setting = IpSetting(InterfaceMethod.DISABLED, [], None, []) if inet.settings and inet.settings.ipv6: - ipv6_setting = IpSetting( + ipv6_setting = Ip6Setting( 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=[ IPv6Interface(f"{ip.address}/{ip.prefix}") for ip in inet.settings.ipv6.address_data @@ -134,7 +157,7 @@ class Interface: else [], ) else: - ipv6_setting = IpSetting(InterfaceMethod.DISABLED, [], None, []) + ipv6_setting = Ip6Setting(InterfaceMethod.DISABLED, [], None, []) ipv4_ready = ( bool(inet.connection) @@ -195,6 +218,28 @@ class Interface: 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 def _map_nm_connected(connection: NetworkConnection | None) -> bool: """Map connectivity state.""" diff --git a/supervisor/host/const.py b/supervisor/host/const.py index 9c3a9dc4a..20ac09c2f 100644 --- a/supervisor/host/const.py +++ b/supervisor/host/const.py @@ -15,6 +15,24 @@ class InterfaceMethod(StrEnum): 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): """Configuration of an interface.""" diff --git a/tests/api/test_network.py b/tests/api/test_network.py index e609df410..befe185d2 100644 --- a/tests/api/test_network.py +++ b/tests/api/test_network.py @@ -51,8 +51,10 @@ async def test_api_network_info(api_client: TestClient, coresys: CoreSys): "ready": False, } assert interface["ipv6"] == { + "addr_gen_mode": "default", "address": [], "gateway": None, + "ip6_privacy": "default", "method": "disabled", "nameservers": [], "ready": False, diff --git a/tests/dbus/network/setting/test_generate.py b/tests/dbus/network/setting/test_generate.py index bd6eb0181..df391c959 100644 --- a/tests/dbus/network/setting/test_generate.py +++ b/tests/dbus/network/setting/test_generate.py @@ -5,7 +5,7 @@ from unittest.mock import PropertyMock, patch from supervisor.dbus.network import NetworkManager from supervisor.dbus.network.interface import NetworkInterface 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.network import Interface @@ -57,8 +57,8 @@ async def test_generate_from_vlan(network_manager: NetworkManager): type=InterfaceType.VLAN, ipv4=IpConfig([], None, [], None), ipv4setting=IpSetting(InterfaceMethod.AUTO, [], None, []), - ipv6=None, - ipv6setting=None, + ipv6=IpConfig([], None, [], None), + ipv6setting=Ip6Setting(InterfaceMethod.AUTO, [], None, []), wifi=None, 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 "interface-name" not in connection_payload["connection"] 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 ( diff --git a/tests/dbus/network/setting/test_init.py b/tests/dbus/network/setting/test_init.py index bbe516b47..f1197d993 100644 --- a/tests/dbus/network/setting/test_init.py +++ b/tests/dbus/network/setting/test_init.py @@ -1,14 +1,17 @@ """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.aio.message_bus import MessageBus import pytest +from supervisor.dbus.network import NetworkManager from supervisor.dbus.network.interface import NetworkInterface from supervisor.dbus.network.setting import NetworkSetting 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.network import Interface @@ -48,6 +51,7 @@ async def fixture_dbus_interface( async def test_ethernet_update( dbus_interface: NetworkInterface, connection_settings_service: ConnectionSettingsService, + network_manager: NetworkManager, ): """Test network manager update.""" connection_settings_service.Update.calls.clear() @@ -55,7 +59,7 @@ async def test_ethernet_update( interface = Interface.from_dbus_interface(dbus_interface) conn = get_connection_from_interface( interface, - MagicMock(), + network_manager, name=dbus_interface.settings.connection.id, uuid=dbus_interface.settings.connection.uuid, ) @@ -124,14 +128,16 @@ async def test_ethernet_update( 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.""" interface = Interface.from_dbus_interface(dbus_interface) interface.ipv4setting.method = InterfaceMethod.DISABLED interface.ipv6setting.method = InterfaceMethod.DISABLED conn = get_connection_from_interface( interface, - MagicMock(), + network_manager, name=dbus_interface.settings.connection.id, 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") +@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( connection_settings_service: ConnectionSettingsService, dbus_session_bus: MessageBus ):