Support custom DNS configuration on auto

So far, when the network setting is set to auto, we don't allow a
custom DNS configuration. However, it is a perfectly valid use case to
provide custom DNS configuration while using automatic IP configuration
(e.g. DHCP). Especially when the DHCP server doesn't provide a DNS
server. In this case, systemd-resolved (at least on Home Assistant OS)
falls back to Cloudflare. This might not what the user wants. Customze
the DNS setting allows to set a custom fallback DNS server or override
a potential non-working DHCP provided one.
This commit is contained in:
Stefan Agner 2024-08-20 16:32:40 +00:00
parent 8ab396d77c
commit 1e9a3d7d0f
No known key found for this signature in database
GPG Key ID: 22D95D15D3A36E95
6 changed files with 76 additions and 28 deletions

View File

@ -29,6 +29,7 @@ from ..const import (
ATTR_METHOD, ATTR_METHOD,
ATTR_MODE, ATTR_MODE,
ATTR_NAMESERVERS, ATTR_NAMESERVERS,
ATTR_NAMESERVERS_AUTO,
ATTR_PARENT, ATTR_PARENT,
ATTR_PRIMARY, ATTR_PRIMARY,
ATTR_PSK, ATTR_PSK,
@ -90,6 +91,7 @@ def ipconfig_struct(config: IpConfig) -> dict[str, Any]:
return { return {
ATTR_METHOD: config.method, ATTR_METHOD: config.method,
ATTR_ADDRESS: [address.with_prefixlen for address in config.address], ATTR_ADDRESS: [address.with_prefixlen for address in config.address],
ATTR_NAMESERVERS_AUTO: config.nameservers_auto,
ATTR_NAMESERVERS: [str(address) for address in config.nameservers], ATTR_NAMESERVERS: [str(address) for address in config.nameservers],
ATTR_GATEWAY: str(config.gateway) if config.gateway else None, ATTR_GATEWAY: str(config.gateway) if config.gateway else None,
ATTR_READY: config.ready, ATTR_READY: config.ready,
@ -200,15 +202,19 @@ class APINetwork(CoreSysAttributes):
if key == ATTR_IPV4: if key == ATTR_IPV4:
interface.ipv4 = replace( interface.ipv4 = replace(
interface.ipv4 interface.ipv4
or IpConfig(InterfaceMethod.STATIC, [], None, [], None), or IpConfig(InterfaceMethod.STATIC, [], None, False, [], None),
**config, **config,
) )
if interface.ipv4.method == InterfaceMethod.AUTO:
interface.ipv4.nameservers_auto = ATTR_NAMESERVERS not in config
elif key == ATTR_IPV6: elif key == ATTR_IPV6:
interface.ipv6 = replace( interface.ipv6 = replace(
interface.ipv6 interface.ipv6
or IpConfig(InterfaceMethod.STATIC, [], None, [], None), or IpConfig(InterfaceMethod.STATIC, [], None, False, [], None),
**config, **config,
) )
if interface.ipv6.method == InterfaceMethod.AUTO:
interface.ipv6.nameservers_auto = ATTR_NAMESERVERS not in config
elif key == ATTR_WIFI: elif key == ATTR_WIFI:
interface.wifi = replace( interface.wifi = replace(
interface.wifi interface.wifi

View File

@ -244,6 +244,7 @@ ATTR_MODE = "mode"
ATTR_MULTICAST = "multicast" ATTR_MULTICAST = "multicast"
ATTR_NAME = "name" ATTR_NAME = "name"
ATTR_NAMESERVERS = "nameservers" ATTR_NAMESERVERS = "nameservers"
ATTR_NAMESERVERS_AUTO = "nameservers_auto"
ATTR_NETWORK = "network" ATTR_NETWORK = "network"
ATTR_NETWORK_DESCRIPTION = "network_description" ATTR_NETWORK_DESCRIPTION = "network_description"
ATTR_NETWORK_RX = "network_rx" ATTR_NETWORK_RX = "network_rx"

View File

@ -64,6 +64,7 @@ class IpProperties:
"""IP properties object for Network Manager.""" """IP properties object for Network Manager."""
method: str | None method: str | None
dns: list[str] | None
@dataclass(slots=True) @dataclass(slots=True)

View File

@ -6,7 +6,7 @@ from typing import Any
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
from ....const import ATTR_METHOD, ATTR_MODE, ATTR_PSK, ATTR_SSID from ....const import ATTR_DNS, ATTR_METHOD, ATTR_MODE, ATTR_PSK, ATTR_SSID
from ...const import DBUS_NAME_NM from ...const import DBUS_NAME_NM
from ...interface import DBusInterface from ...interface import DBusInterface
from ...utils import dbus_connected from ...utils import dbus_connected
@ -75,7 +75,7 @@ def _merge_settings_attribute(
class NetworkSetting(DBusInterface): class NetworkSetting(DBusInterface):
"""Network connection setting object for Network Manager. """Network connection setting object for Network Manager.
https://developer.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.Settings.Connection.html https://networkmanager.dev/docs/api/1.48.0/gdbus-org.freedesktop.NetworkManager.Settings.Connection.html
""" """
bus_name: str = DBUS_NAME_NM bus_name: str = DBUS_NAME_NM
@ -229,11 +229,13 @@ class NetworkSetting(DBusInterface):
if CONF_ATTR_IPV4 in data: if CONF_ATTR_IPV4 in data:
self._ipv4 = IpProperties( self._ipv4 = IpProperties(
data[CONF_ATTR_IPV4].get(ATTR_METHOD), data[CONF_ATTR_IPV4].get(ATTR_METHOD),
data[CONF_ATTR_IPV4].get(ATTR_DNS),
) )
if CONF_ATTR_IPV6 in data: if CONF_ATTR_IPV6 in data:
self._ipv6 = IpProperties( self._ipv6 = IpProperties(
data[CONF_ATTR_IPV6].get(ATTR_METHOD), data[CONF_ATTR_IPV6].get(ATTR_METHOD),
data[CONF_ATTR_IPV6].get(ATTR_DNS),
) )
if CONF_ATTR_MATCH in data: if CONF_ATTR_MATCH in data:

View File

@ -76,13 +76,6 @@ def get_connection_from_interface(
ipv4["method"] = Variant("s", "disabled") ipv4["method"] = Variant("s", "disabled")
else: else:
ipv4["method"] = Variant("s", "manual") ipv4["method"] = Variant("s", "manual")
ipv4["dns"] = Variant(
"au",
[
socket.htonl(int(ip_address))
for ip_address in interface.ipv4.nameservers
],
)
adressdata = [] adressdata = []
for address in interface.ipv4.address: for address in interface.ipv4.address:
@ -96,6 +89,18 @@ def get_connection_from_interface(
ipv4["address-data"] = Variant("aa{sv}", adressdata) ipv4["address-data"] = Variant("aa{sv}", adressdata)
ipv4["gateway"] = Variant("s", str(interface.ipv4.gateway)) ipv4["gateway"] = Variant("s", str(interface.ipv4.gateway))
if (
interface.ipv4.method == InterfaceMethod.AUTO
and not interface.ipv4.nameservers_auto
) or interface.ipv4.method == InterfaceMethod.STATIC:
ipv4["dns"] = Variant(
"au",
[
socket.htonl(int(ip_address))
for ip_address in interface.ipv4.nameservers
],
)
conn[CONF_ATTR_IPV4] = ipv4 conn[CONF_ATTR_IPV4] = ipv4
ipv6 = {} ipv6 = {}
@ -105,9 +110,6 @@ def get_connection_from_interface(
ipv6["method"] = Variant("s", "link-local") ipv6["method"] = Variant("s", "link-local")
else: else:
ipv6["method"] = Variant("s", "manual") ipv6["method"] = Variant("s", "manual")
ipv6["dns"] = Variant(
"aay", [ip_address.packed for ip_address in interface.ipv6.nameservers]
)
adressdata = [] adressdata = []
for address in interface.ipv6.address: for address in interface.ipv6.address:
@ -121,6 +123,14 @@ def get_connection_from_interface(
ipv6["address-data"] = Variant("aa{sv}", adressdata) ipv6["address-data"] = Variant("aa{sv}", adressdata)
ipv6["gateway"] = Variant("s", str(interface.ipv6.gateway)) ipv6["gateway"] = Variant("s", str(interface.ipv6.gateway))
if (
interface.ipv6.method == InterfaceMethod.AUTO
and not interface.ipv6.nameservers_auto
) or interface.ipv6.method == InterfaceMethod.STATIC:
ipv6["dns"] = Variant(
"aay", [ip_address.packed for ip_address in interface.ipv6.nameservers]
)
conn[CONF_ATTR_IPV6] = ipv6 conn[CONF_ATTR_IPV6] = ipv6
if interface.type == InterfaceType.ETHERNET: if interface.type == InterfaceType.ETHERNET:

View File

@ -2,6 +2,7 @@
from dataclasses import dataclass from dataclasses import dataclass
from ipaddress import IPv4Address, IPv4Interface, IPv6Address, IPv6Interface from ipaddress import IPv4Address, IPv4Interface, IPv6Address, IPv6Interface
import socket
from ..dbus.const import ( from ..dbus.const import (
ConnectionStateFlags, ConnectionStateFlags,
@ -32,6 +33,7 @@ class IpConfig:
method: InterfaceMethod method: InterfaceMethod
address: list[IPv4Interface | IPv6Interface] address: list[IPv4Interface | IPv6Interface]
gateway: IPv4Address | IPv6Address | None gateway: IPv4Address | IPv6Address | None
nameservers_auto: bool
nameservers: list[IPv4Address | IPv6Address] nameservers: list[IPv4Address | IPv6Address]
ready: bool | None ready: bool | None
@ -102,6 +104,30 @@ class Interface:
bool(inet.connection) bool(inet.connection)
and ConnectionStateFlags.IP6_READY in inet.connection.state_flags and ConnectionStateFlags.IP6_READY in inet.connection.state_flags
) )
# Use Nameserver from configuration if present (means manual DNS override)
if inet.settings.ipv4.dns:
ipv4_nameservers_auto = False
ipv4_nameservers = [
IPv4Address(socket.ntohl(ip)) for ip in inet.settings.ipv4.dns
]
elif inet.connection.ipv4.nameservers:
ipv4_nameservers_auto = True
ipv4_nameservers = inet.connection.ipv4.nameservers
else:
ipv4_nameservers_auto = True
ipv4_nameservers = []
if inet.settings.ipv6.dns:
ipv6_nameservers_auto = False
ipv6_nameservers = [IPv6Address(bytes(ip)) for ip in inet.settings.ipv6.dns]
elif inet.connection.ipv6.nameservers:
ipv6_nameservers_auto = True
ipv6_nameservers = inet.connection.ipv6.nameservers
else:
ipv6_nameservers_auto = True
ipv6_nameservers = []
return Interface( return Interface(
inet.name, inet.name,
inet.hw_address, inet.hw_address,
@ -111,27 +137,29 @@ class Interface:
inet.primary, inet.primary,
Interface._map_nm_type(inet.type), Interface._map_nm_type(inet.type),
IpConfig( IpConfig(
ipv4_method, method=ipv4_method,
inet.connection.ipv4.address if inet.connection.ipv4.address else [], address=inet.connection.ipv4.address
inet.connection.ipv4.gateway, if inet.connection.ipv4.address
inet.connection.ipv4.nameservers
if inet.connection.ipv4.nameservers
else [], else [],
ipv4_ready, gateway=inet.connection.ipv4.gateway,
nameservers_auto=ipv4_nameservers_auto,
nameservers=ipv4_nameservers,
ready=ipv4_ready,
) )
if inet.connection and inet.connection.ipv4 if inet.connection and inet.connection.ipv4
else IpConfig(ipv4_method, [], None, [], ipv4_ready), else IpConfig(ipv4_method, [], None, True, [], ipv4_ready),
IpConfig( IpConfig(
ipv6_method, method=ipv6_method,
inet.connection.ipv6.address if inet.connection.ipv6.address else [], address=inet.connection.ipv6.address
inet.connection.ipv6.gateway, if inet.connection.ipv6.address
inet.connection.ipv6.nameservers
if inet.connection.ipv6.nameservers
else [], else [],
ipv6_ready, gateway=inet.connection.ipv6.gateway,
nameservers_auto=ipv6_nameservers_auto,
nameservers=ipv6_nameservers,
ready=ipv6_ready,
) )
if inet.connection and inet.connection.ipv6 if inet.connection and inet.connection.ipv6
else IpConfig(ipv6_method, [], None, [], ipv6_ready), else IpConfig(ipv6_method, [], None, True, [], ipv6_ready),
Interface._map_nm_wifi(inet), Interface._map_nm_wifi(inet),
Interface._map_nm_vlan(inet), Interface._map_nm_vlan(inet),
) )