From f16d2016b7f87c7a08fe773b9c006c0e6bb4d45e Mon Sep 17 00:00:00 2001 From: Max Makarov Date: Mon, 6 Apr 2026 20:56:21 +0000 Subject: [PATCH] Filter default gateway routes by address family in dual-stack config When a network adapter has both IPv4 and IPv6 addresses with separate default gateways, _process_networks picks the first route with prefixlen == 0 regardless of address family. This causes IPv6 addresses to receive the IPv4 gateway (e.g., 169.254.0.1 instead of fe80::1), and the IPv6 default route is never configured. Filter the default gateway route candidates by matching the route's address family (0.0.0.0/0 vs ::/0) against the network address being configured. Signed-off-by: Max Makarov --- cloudbaseinit/plugins/common/networkconfig.py | 6 +- .../plugins/common/test_networkconfig.py | 65 ++++++++++++++++++- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/cloudbaseinit/plugins/common/networkconfig.py b/cloudbaseinit/plugins/common/networkconfig.py index 768212aa..78a372a1 100644 --- a/cloudbaseinit/plugins/common/networkconfig.py +++ b/cloudbaseinit/plugins/common/networkconfig.py @@ -263,9 +263,13 @@ def _process_networks(osutils, network_details): ip_address, prefix_len = net.address_cidr.split("/") gateway = None + is_ipv6 = netaddr.valid_ipv6(ip_address) + expected_version = 6 if is_ipv6 else 4 default_gw_route = [ r for r in net.routes if - netaddr.IPNetwork(r.network_cidr).prefixlen == 0] + netaddr.IPNetwork(r.network_cidr).prefixlen == 0 and + netaddr.IPNetwork(r.network_cidr).version == + expected_version] if default_gw_route: gateway = default_gw_route[0].gateway diff --git a/cloudbaseinit/tests/plugins/common/test_networkconfig.py b/cloudbaseinit/tests/plugins/common/test_networkconfig.py index ceebe8ce..633fe61a 100644 --- a/cloudbaseinit/tests/plugins/common/test_networkconfig.py +++ b/cloudbaseinit/tests/plugins/common/test_networkconfig.py @@ -458,9 +458,14 @@ def _test_execute_network_details_v2(self, mock_get_os_utils, any_order=False) ip_address, prefix_len = mock.sentinel.address_cidr1.split("/") + # When address is IPv6 but gateway is IPv4-only, gateway should + # be None because the address family filter excludes it. + expected_gateway = mock.sentinel.gateway1 + if ":" in ip_address: + expected_gateway = None mock_os_utils.set_static_network_config.assert_called_once_with( mock.sentinel.link_id1, ip_address, prefix_len, - mock.sentinel.gateway1, expected_dns_list) + expected_gateway, expected_dns_list) def test_execute_network_details_v2(self): self._test_execute_network_details_v2() @@ -473,3 +478,61 @@ def test_execute_network_details_v2_ipv4_dns_list(self): def test_execute_network_details_v2_ipv6_dns_list(self): self._test_execute_network_details_v2(both_ipv6_dns_list=True) + + @mock.patch("cloudbaseinit.osutils.factory.get_os_utils") + def test_execute_network_details_v2_dual_stack_gateways( + self, mock_get_os_utils): + """IPv4 address gets IPv4 gateway, IPv6 address gets IPv6 gateway.""" + link1 = network_model.Link( + id="eth0", + name="eth0", + type=network_model.LINK_TYPE_PHYSICAL, + enabled=True, + mac_address=u"00:00:00:00:00:01", + mtu=None, + bond=None, + vlan_link=None, + vlan_id=None) + + route_v4 = network_model.Route( + network_cidr=u"0.0.0.0/0", + gateway=u"169.254.0.1") + route_v6 = network_model.Route( + network_cidr=u"::/0", + gateway=u"fe80::1") + + network_v4 = network_model.Network( + link="eth0", + address_cidr=u"10.0.0.1/32", + dns_nameservers=["1.1.1.1"], + routes=[route_v4, route_v6]) + network_v6 = network_model.Network( + link="eth0", + address_cidr=u"2001:db8::1/96", + dns_nameservers=["1.1.1.1"], + routes=[route_v4, route_v6]) + + network_details = network_model.NetworkDetailsV2( + links=[link1], + networks=[network_v4, network_v6], + services=[]) + + service = mock.Mock() + service.get_network_details_v2.return_value = network_details + + mock_os_utils = mock.Mock() + mock_get_os_utils.return_value = mock_os_utils + mock_os_utils.get_network_adapter_name_by_mac_address.return_value = \ + "eth0" + + plugin = networkconfig.NetworkConfigPlugin() + plugin.execute(service, {}) + + calls = mock_os_utils.set_static_network_config.call_args_list + self.assertEqual(len(calls), 2) + # IPv4 address should get IPv4 gateway + self.assertEqual(calls[0], mock.call( + "eth0", "10.0.0.1", "32", "169.254.0.1", ["1.1.1.1"])) + # IPv6 address should get IPv6 gateway + self.assertEqual(calls[1], mock.call( + "eth0", "2001:db8::1", "96", "fe80::1", ["1.1.1.1"]))