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

View File

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

View File

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

View File

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

View File

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

View File

@ -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] = {

View File

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

View File

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

View File

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

View File

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

View File

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