Skip to content

Commit

Permalink
state_diff: improve analysis of RA/LL addresses
Browse files Browse the repository at this point in the history
When IPv6 link-local and RA (which depends on link-local) are enabled,
the system will receive IP addresses, nameservers and routes dynamically
via RA. These addresses are not obviously identified as dynamic in the
system. networkd probably does have this information but it's not fully
exposed apparently.

Here I try to improve the comparison of these addresses by checking if
they are 1) a link-local address and 2) part of the same network as the
route received via RA.

Without this, status --diff will show the RA IP addresses and link-local
DNS nameserver as a diff.
  • Loading branch information
daniloegea committed May 31, 2024
1 parent 65156af commit 2f63fca
Show file tree
Hide file tree
Showing 2 changed files with 375 additions and 10 deletions.
65 changes: 55 additions & 10 deletions netplan_cli/cli/state_diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def _analyze_ip_addresses(self, config: dict, iface: dict) -> None:
flags = addr_data.get('flags', [])

# Select only static IPs
if not {'dhcp', 'dynamic', 'link'}.intersection(flags):
if not {'dhcp', 'dynamic', 'link', 'ra'}.intersection(flags):
system_ips.add(addr)

# Handle the link local address
Expand All @@ -168,6 +168,14 @@ def _analyze_ip_addresses(self, config: dict, iface: dict) -> None:
if 'dhcp' in flags:
missing_dhcp6_address = False

# Handle RA addresses
# We only consider the presence of addresses with the 'ra' flag to be a diff
# if 'accept_ra' is set to false.
# When it's unset, networkd will enable it by default.
accept_ra = config.get('netplan_state', {}).get('accept_ra')
if 'ra' in flags and accept_ra is False:
system_ips.add(addr)

present_only_in_netplan = netplan_ips.difference(system_ips)
present_only_in_system = system_ips.difference(netplan_ips)

Expand Down Expand Up @@ -246,13 +254,36 @@ def _analyze_nameservers(self, config: dict, iface: dict) -> None:
# Filter out dynamically assigned DNS data
# Here we implement some heuristics to try to filter out dynamic DNS configuration
#
# If the nameserver address is the same as a RA route we assume it's dynamic
# If ipv6 link-local is enabled, filter out DNS IPs that are link local
dynamic_ns = set()
accept_ra_enabled = config.get('netplan_state', {}).get('accept_ra') is not False
link_local_enabled = 'ipv6' in config.get('netplan_state', {}).get('link_local', [])
link_local_ns = {ns for ns in system_nameservers if ipaddress.ip_interface(ns).is_link_local}
dynamic_ns.update(link_local_ns)

if link_local_enabled and accept_ra_enabled:
system_nameservers = system_nameservers - link_local_ns

# If RA is enabled, filter out DNS IPs that are part of the RA network
system_routes = config.get('system_state', {}).get('routes', [])
ra_routes = [r.via for r in system_routes if r.protocol == 'ra' and r.via]
system_nameservers = {ns for ns in system_nameservers if ns not in ra_routes}
ra_routes = [ipaddress.ip_interface(r.to).network for r in system_routes
if r.protocol == 'ra' and r.to != 'default']
filtered = set()
for ns in system_nameservers:
if isinstance(ipaddress.ip_interface(ns), ipaddress.IPv6Address):
network = ipaddress.ip_interface(f'{ns}/64').network
if network not in ra_routes:
filtered.add(ns)
else:
dynamic_ns.add(ns)
else:
filtered.add(ns)
if link_local_enabled and accept_ra_enabled:
system_nameservers = filtered

# If the netplan configuration has DHCP enabled and an empty list of nameservers
# we assume it's dynamic.
# we assume it's dynamic. So we don't take these addresses into account when comparing
# them, except for the ones we already identified as RA/LL.
# Note: Some useful information can be found in /var/run/systemd/netif/leases/
# but the lease files have a comment saying they shouldn't be parsed.
# There is a feature request to expose more DHCP information via the DBus API
Expand All @@ -262,8 +293,14 @@ def _analyze_nameservers(self, config: dict, iface: dict) -> None:
system_nameservers = {ns for ns in system_nameservers
if not isinstance(ipaddress.ip_address(ns), ipaddress.IPv4Address)}
if config.get('netplan_state', {}).get('dhcp6'):
system_nameservers = {ns for ns in system_nameservers
if not isinstance(ipaddress.ip_address(ns), ipaddress.IPv6Address)}
filtered = set()
for ns in system_nameservers:
if isinstance(ipaddress.ip_address(ns), ipaddress.IPv6Address):
# Keep addresses that were flagged as RA/LL
if ns in dynamic_ns:
filtered.add(ns)

system_nameservers = filtered

present_only_in_netplan = netplan_nameservers.difference(system_nameservers)
present_only_in_system = system_nameservers.difference(netplan_nameservers)
Expand Down Expand Up @@ -525,8 +562,13 @@ def _filter_system_routes(self, system_routes: AbstractSet[NetplanRoute], system
if route.scope == 'link' and route.to != 'default' and not ipaddress.ip_interface(route.to).is_link_local:
continue

# Filter out routes installed by DHCP or RA
if route.protocol == 'dhcp' or route.protocol == 'ra':
# Filter out routes installed by DHCP
if route.protocol == 'dhcp':
continue

# Filter out routes installed by RA if accept_ra is not false
accept_ra = config.get('netplan_state', {}).get('accept_ra')
if route.protocol == 'ra' and accept_ra is not False:
continue

# Filter out Link Local routes
Expand All @@ -549,7 +591,7 @@ def _filter_system_routes(self, system_routes: AbstractSet[NetplanRoute], system
continue

# Filter IPv6 local routes
if route.family == 10 and (route.to in local_networks or route.to in addresses):
if route.family == 10 and route.protocol != 'ra' and (route.to in local_networks or route.to in addresses):
continue

routes.add(route)
Expand All @@ -571,6 +613,9 @@ def _get_netplan_interfaces(self) -> dict:

iface_ref['link_local'] = config.link_local

if config.accept_ra is not None:
iface_ref['accept_ra'] = config.accept_ra

addresses = [addr for addr in config.addresses]
if addresses:
iface_ref['addresses'] = {}
Expand Down
Loading

0 comments on commit 2f63fca

Please sign in to comment.