From 2ff5b4a18bea01a78b069540108709ce8909d95a Mon Sep 17 00:00:00 2001 From: Dai Dang Van Date: Thu, 17 Apr 2025 17:17:53 +0700 Subject: [PATCH] Fix DHCP extension if subnet has no gateway_ip The subnet gateway_ip can be none, so the DHCP agent extension will get failed to assemble the response packet. This patch adds default gateway IP address for DHCP, for IPv4 it is 169.254.169.254, for IPv6 it is fe80::a9fe:a9fe. Partially-Implements: bp/distributed-dhcp-for-ml2-ovs Related-Bug: #2107552 Change-Id: I709b73c242285930c4ade5bb9e6f167495b90d80 --- neutron/agent/l2/extensions/dhcp/extension.py | 8 +- neutron/agent/l2/extensions/dhcp/ipv4.py | 21 ++-- neutron/agent/l2/extensions/dhcp/ipv6.py | 6 +- .../agent/l2/extensions/dhcp/test_base.py | 101 ++++++++++++------ .../agent/l2/extensions/dhcp/test_ipv4.py | 97 +++++++++++------ .../agent/l2/extensions/dhcp/test_ipv6.py | 48 +++++++-- 6 files changed, 193 insertions(+), 88 deletions(-) diff --git a/neutron/agent/l2/extensions/dhcp/extension.py b/neutron/agent/l2/extensions/dhcp/extension.py index 14ba3343b38..ac14388ec10 100644 --- a/neutron/agent/l2/extensions/dhcp/extension.py +++ b/neutron/agent/l2/extensions/dhcp/extension.py @@ -26,6 +26,10 @@ from neutron.agent.l2.extensions.dhcp import ipv6 from neutron.api.rpc.callbacks import resources LOG = logging.getLogger(__name__) +LINK_LOCAL_GATEWAY = { + constants.IP_VERSION_4: constants.METADATA_V4_IP, + constants.IP_VERSION_6: constants.METADATA_V6_IP +} class DHCPExtensionPortInfoAPI: @@ -64,7 +68,9 @@ class DHCPExtensionPortInfoAPI: 'cidr': subnet.cidr, 'host_routes': subnet.host_routes, 'dns_nameservers': subnet.dns_nameservers, - 'gateway_ip': subnet.gateway_ip} + 'gateway_ip': subnet.gateway_ip or LINK_LOCAL_GATEWAY[ + subnet.ip_version + ]} fixed_ips.append(info) net = self.cache_api.get_resource_by_id( resources.NETWORK, port_obj.network_id) diff --git a/neutron/agent/l2/extensions/dhcp/ipv4.py b/neutron/agent/l2/extensions/dhcp/ipv4.py index aeb7a20a81f..3e6719421ba 100644 --- a/neutron/agent/l2/extensions/dhcp/ipv4.py +++ b/neutron/agent/l2/extensions/dhcp/ipv4.py @@ -49,13 +49,13 @@ class DHCPIPv4Responder(dhcp_base.DHCPResponderBase): def get_bin_routes(self, gateway=None, routes=None): bin_routes = b'' - # Default routes - default_route = self.get_bin_route(constants.IPv4_ANY, gateway) - bin_routes += default_route + if gateway and gateway != constants.METADATA_V4_IP: + # Default routes + default_route = self.get_bin_route(constants.IPv4_ANY, gateway) + bin_routes += default_route - # For some VMs they may need the metadata IP's route, we move - # the destination to gateway IP. - if gateway: + # For some VMs they may need the metadata IP's route, we move + # the destination to gateway IP. meta_route = self.get_bin_route( constants.METADATA_V4_CIDR, gateway) bin_routes += meta_route @@ -71,7 +71,7 @@ class DHCPIPv4Responder(dhcp_base.DHCPResponderBase): net = netaddr.IPNetwork(fixed_ips[0]['cidr']) dns_nameservers = fixed_ips[0]['dns_nameservers'] host_routes = fixed_ips[0]['host_routes'] - gateway_ip = fixed_ips[0]['gateway_ip'] + gateway_ip = str(fixed_ips[0]['gateway_ip']) bin_server = addrconv.ipv4.text_to_bin(gateway_ip) option_list = [] @@ -122,9 +122,10 @@ class DHCPIPv4Responder(dhcp_base.DHCPResponderBase): value=struct.pack( '!%ds' % len(cfg.CONF.dns_domain), str.encode(cfg.CONF.dns_domain)))) - option_list.append( - dhcp.option(tag=dhcp.DHCP_GATEWAY_ADDR_OPT, - value=bin_server)) + if gateway_ip != constants.METADATA_V4_IP: + option_list.append( + dhcp.option(tag=dhcp.DHCP_GATEWAY_ADDR_OPT, + value=bin_server)) # Static routes option_list.append( dhcp.option(tag=dhcp.DHCP_CLASSLESS_ROUTE_OPT, diff --git a/neutron/agent/l2/extensions/dhcp/ipv6.py b/neutron/agent/l2/extensions/dhcp/ipv6.py index cd756df1322..118218f6bb8 100644 --- a/neutron/agent/l2/extensions/dhcp/ipv6.py +++ b/neutron/agent/l2/extensions/dhcp/ipv6.py @@ -147,7 +147,7 @@ class DHCPIPv6Responder(dhcp_base.DHCPResponderBase): def get_dhcp_options(self, mac, ip_info, req_options, req_type): ip_addr = ip_info['ip_address'] - gateway_ip = ip_info['gateway_ip'] + gateway_ip = str(ip_info['gateway_ip']) dns_nameservers = ip_info['dns_nameservers'] option_list = [] @@ -210,9 +210,9 @@ class DHCPIPv6Responder(dhcp_base.DHCPResponderBase): dhcp6.option( code=DHCPV6_OPTION_DNS_RECURSIVE_NS, data=domain_serach, length=len(domain_serach))) - else: + elif gateway_ip != constants.METADATA_V6_IP: # use gateway as the default DNS server address - domain_serach = addrconv.ipv6.text_to_bin(str(gateway_ip)) + domain_serach = addrconv.ipv6.text_to_bin(gateway_ip) option_list.append( dhcp6.option( code=DHCPV6_OPTION_DNS_RECURSIVE_NS, diff --git a/neutron/tests/unit/agent/l2/extensions/dhcp/test_base.py b/neutron/tests/unit/agent/l2/extensions/dhcp/test_base.py index ac50c6d3d81..7eedf456693 100644 --- a/neutron/tests/unit/agent/l2/extensions/dhcp/test_base.py +++ b/neutron/tests/unit/agent/l2/extensions/dhcp/test_base.py @@ -61,41 +61,81 @@ class FakeMsg: self.data = packet.data +IPV4_INFO = { + 'version': 4, + 'host_routes': [ + subnet_obj.Route( + destination=netaddr.IPNetwork('1.1.1.0/24'), + nexthop='192.168.1.100', + subnet_id='daed3c3d-d95a-48a8-a8b1-17d408cd760f' + ), + subnet_obj.Route( + destination=netaddr.IPNetwork('2.2.2.2/32'), + nexthop='192.168.1.101', + subnet_id='daed3c3d-d95a-48a8-a8b1-17d408cd760f' + ) + ], + 'subnet_id': 'daed3c3d-d95a-48a8-a8b1-17d408cd760f', + 'dns_nameservers': [ + subnet_obj.DNSNameServer( + address='8.8.8.8', + order=0, + subnet_id='daed3c3d-d95a-48a8-a8b1-17d408cd760f' + ), + subnet_obj.DNSNameServer( + address='8.8.4.4', + order=1, + subnet_id='daed3c3d-d95a-48a8-a8b1-17d408cd760f' + ) + ], + 'cidr': net_utils.AuthenticIPNetwork('192.168.111.0/24'), + 'ip_address': '192.168.111.45', + 'gateway_ip': netaddr.IPAddress('192.168.111.1') +} + + +IPV6_INFO = { + 'version': 6, + 'host_routes': [], + 'subnet_id': 'bd013460-b05f-4927-a4c6-5127584b2487', + 'dns_nameservers': [], + 'cidr': net_utils.AuthenticIPNetwork('fda7:a5cc:3460:1::/64'), + 'ip_address': 'fda7:a5cc:3460:1::bf', + 'gateway_ip': netaddr.IPAddress('fda7:a5cc:3460:1::1') +} + + PORT_INFO = { 'device_owner': 'compute:nova', 'admin_state_up': True, 'network_id': 'd666ccb3-69e9-46cb-b157-bb3741d87d5a', 'fixed_ips': [ - {'version': 4, - 'host_routes': [ - subnet_obj.Route( - destination=netaddr.IPNetwork('1.1.1.0/24'), - nexthop='192.168.1.100', - subnet_id='daed3c3d-d95a-48a8-a8b1-17d408cd760f'), - subnet_obj.Route( - destination=netaddr.IPNetwork('2.2.2.2/32'), - nexthop='192.168.1.101', - subnet_id='daed3c3d-d95a-48a8-a8b1-17d408cd760f')], - 'subnet_id': 'daed3c3d-d95a-48a8-a8b1-17d408cd760f', - 'dns_nameservers': [ - subnet_obj.DNSNameServer( - address='8.8.8.8', - order=0, - subnet_id='daed3c3d-d95a-48a8-a8b1-17d408cd760f'), - subnet_obj.DNSNameServer( - address='8.8.4.4', - order=1, - subnet_id='daed3c3d-d95a-48a8-a8b1-17d408cd760f')], - 'cidr': net_utils.AuthenticIPNetwork('192.168.111.0/24'), - 'ip_address': '192.168.111.45', - 'gateway_ip': netaddr.IPAddress('192.168.111.1')}, - {'version': 6, - 'host_routes': [], - 'subnet_id': 'bd013460-b05f-4927-a4c6-5127584b2487', - 'dns_nameservers': [], - 'cidr': net_utils.AuthenticIPNetwork('fda7:a5cc:3460:1::/64'), - 'ip_address': 'fda7:a5cc:3460:1::bf', - 'gateway_ip': netaddr.IPAddress('fda7:a5cc:3460:1::1')} + IPV4_INFO, + IPV6_INFO + ], + 'mac_address': '00:01:02:03:04:05', + 'port_id': '9a0e1889-f05f-43c7-a319-e1a723ed1587', + 'mtu': 1450 +} + + +IPV4_INFO_NO_GATEWAY = { + **IPV4_INFO, + 'gateway_ip': netaddr.IPAddress(constants.METADATA_V4_IP) +} +IPV6_INFO_NO_GATEWAY = { + **IPV6_INFO, + 'gateway_ip': netaddr.IPAddress(constants.METADATA_V6_IP) +} + + +NO_GATEWAY_PORT_INFO = { + 'device_owner': 'compute:nova', + 'admin_state_up': True, + 'network_id': 'd666ccb3-69e9-46cb-b157-bb3741d87d5a', + 'fixed_ips': [ + IPV4_INFO_NO_GATEWAY, + IPV6_INFO_NO_GATEWAY ], 'mac_address': '00:01:02:03:04:05', 'port_id': '9a0e1889-f05f-43c7-a319-e1a723ed1587', @@ -128,6 +168,7 @@ class DHCPResponderBaseTestCase(base.BaseTestCase): self.base_responer.handle_dhcp = mock.Mock() self.port_info = PORT_INFO + self.no_gateway_port_info = NO_GATEWAY_PORT_INFO def _create_test_dhcp_request_packet(self): option_list = [] diff --git a/neutron/tests/unit/agent/l2/extensions/dhcp/test_ipv4.py b/neutron/tests/unit/agent/l2/extensions/dhcp/test_ipv4.py index fa4b8bcfe37..13453dc9694 100644 --- a/neutron/tests/unit/agent/l2/extensions/dhcp/test_ipv4.py +++ b/neutron/tests/unit/agent/l2/extensions/dhcp/test_ipv4.py @@ -67,53 +67,70 @@ class DHCPIPv4ResponderTestCase(dhcp_test_base.DHCPResponderBaseTestCase): dhcp_pkt = ret_pkt.get_protocols(dhcp.dhcp) self.assertIsNotNone(dhcp_pkt) - def test_get_dhcp_options(self): - expect_bin_routes = (b'\x00\xc0\xa8o\x01 \xa9\xfe\xa9\xfe\xc0\xa8o\x01' - b'\x18\x01\x01\x01\xc0\xa8\x01d ' - b'\x02\x02\x02\x02\xc0\xa8\x01e') + def _test_get_dhcp_options(self, port_info, has_gateway_ip=False): + expect_bin_routes = ( + b'\x18\x01\x01\x01\xc0\xa8\x01d ' + b'\x02\x02\x02\x02\xc0\xa8\x01e' + ) + offer_option_list = [ + dhcp.option(length=0, tag=53, value=b'\x02'), + dhcp.option(length=0, tag=54, value=b'\xa9\xfe\xa9\xfe'), + dhcp.option(length=0, tag=51, value=b'\x00\x01Q\x80'), + dhcp.option(length=0, tag=1, value=b'\xff\xff\xff\x00'), + dhcp.option(length=0, tag=28, value=b'\xc0\xa8o\xff'), + dhcp.option(length=0, tag=6, + value=b'\x08\x08\x08\x08\x08\x08\x04\x04'), + dhcp.option(length=0, tag=15, value=b'openstacklocal'), + dhcp.option(length=0, tag=121, value=expect_bin_routes), + dhcp.option(length=0, tag=26, value=b'\x05\xaa') + ] + ack_option_list = list(offer_option_list) + ack_option_list[0] = dhcp.option(length=0, tag=53, value=b'\x05') + if has_gateway_ip: + expect_bin_routes = ( + b'\x00\xc0\xa8o\x01 \xa9\xfe\xa9\xfe\xc0\xa8o\x01' + b'\x18\x01\x01\x01\xc0\xa8\x01d ' + b'\x02\x02\x02\x02\xc0\xa8\x01e' + ) + offer_option_list[1] = dhcp.option( + length=0, tag=54, value=b'\xc0\xa8o\x01' + ) + ack_option_list[1] = dhcp.option( + length=0, tag=54, value=b'\xc0\xa8o\x01' + ) + offer_option_list[7] = dhcp.option( + length=0, tag=121, value=expect_bin_routes + ) + ack_option_list[7] = dhcp.option( + length=0, tag=121, value=expect_bin_routes + ) + offer_option_list.append( + dhcp.option(length=0, tag=3, value=b'\xc0\xa8o\x01') + ) + ack_option_list.append( + dhcp.option(length=0, tag=3, value=b'\xc0\xa8o\x01') + ) + expect_offer_options = dhcp.options( magic_cookie='99.130.83.99', - option_list=[ - dhcp.option(length=0, tag=53, value=b'\x02'), - dhcp.option(length=0, tag=54, value=b'\xc0\xa8o\x01'), - dhcp.option(length=0, tag=51, value=b'\x00\x01Q\x80'), - dhcp.option(length=0, tag=1, value=b'\xff\xff\xff\x00'), - dhcp.option(length=0, tag=28, value=b'\xc0\xa8o\xff'), - dhcp.option(length=0, tag=6, - value=b'\x08\x08\x08\x08\x08\x08\x04\x04'), - dhcp.option(length=0, tag=15, value=b'openstacklocal'), - dhcp.option(length=0, tag=3, value=b'\xc0\xa8o\x01'), - dhcp.option( - length=0, tag=121, - value=expect_bin_routes), - dhcp.option(length=0, tag=26, value=b'\x05\xaa')], + option_list=offer_option_list, options_len=0) - offer_options = self.dhcp4_responer.get_dhcp_options(self.port_info) + offer_options = self.dhcp4_responer.get_dhcp_options(port_info) self._compare_option_values(expect_offer_options.option_list, offer_options.option_list) expect_ack_options = dhcp.options( magic_cookie='99.130.83.99', - option_list=[ - dhcp.option(length=0, tag=53, value=b'\x05'), - dhcp.option(length=0, tag=54, value=b'\xc0\xa8o\x01'), - dhcp.option(length=0, tag=51, value=b'\x00\x01Q\x80'), - dhcp.option(length=0, tag=1, value=b'\xff\xff\xff\x00'), - dhcp.option(length=0, tag=28, value=b'\xc0\xa8o\xff'), - dhcp.option(length=0, tag=6, - value=b'\x08\x08\x08\x08\x08\x08\x04\x04'), - dhcp.option(length=0, tag=15, value=b'openstacklocal'), - dhcp.option(length=0, tag=3, value=b'\xc0\xa8o\x01'), - dhcp.option( - length=0, tag=121, - value=expect_bin_routes), - dhcp.option(length=0, tag=26, value=b'\x05\xaa')], + option_list=ack_option_list, options_len=0) ack_options = self.dhcp4_responer.get_dhcp_options( - self.port_info, is_ack=True) + port_info, is_ack=True) self._compare_option_values(expect_ack_options.option_list, ack_options.option_list) + def test_get_dhcp_options(self): + self._test_get_dhcp_options(self.port_info, has_gateway_ip=True) + def test_get_bin_routes(self): expect_bin_routes = (b'\x00\xc0\xa8o\x01 \xa9\xfe\xa9\xfe\xc0\xa8o\x01' b'\x18\x01\x01\x01\xc0\xa8\x01d ' @@ -122,3 +139,15 @@ class DHCPIPv4ResponderTestCase(dhcp_test_base.DHCPResponderBaseTestCase): self.port_info['fixed_ips'][0]['gateway_ip'], self.port_info['fixed_ips'][0]['host_routes']) self.assertEqual(expect_bin_routes, bin_routes) + + def test_get_dhcp_options_no_gateway(self): + self._test_get_dhcp_options( + self.no_gateway_port_info, has_gateway_ip=False + ) + + def test_get_bin_routes_no_gateway(self): + expect_bin_routes = (b'\x18\x01\x01\x01\xc0\xa8\x01d ' + b'\x02\x02\x02\x02\xc0\xa8\x01e') + bin_routes = self.dhcp4_responer.get_bin_routes( + routes=self.port_info['fixed_ips'][0]['host_routes']) + self.assertEqual(expect_bin_routes, bin_routes) diff --git a/neutron/tests/unit/agent/l2/extensions/dhcp/test_ipv6.py b/neutron/tests/unit/agent/l2/extensions/dhcp/test_ipv6.py index 5c42a5f7bc1..eb4ed6fb565 100644 --- a/neutron/tests/unit/agent/l2/extensions/dhcp/test_ipv6.py +++ b/neutron/tests/unit/agent/l2/extensions/dhcp/test_ipv6.py @@ -114,14 +114,27 @@ class DHCPIPv6ResponderTestCase(dhcp_test_base.DHCPResponderBaseTestCase): self.assertEqual(expect_status_code, status_code) def test_get_dhcp_options(self): - self._test_get_dhcp_options() + self._test_get_dhcp_options(self.port_info, has_gateway_ip=True) def test_get_dhcp_options_zero_time(self): - self._test_get_dhcp_options(zero_time=True) + self._test_get_dhcp_options( + self.port_info, has_gateway_ip=True, zero_time=True + ) - def _test_get_dhcp_options(self, zero_time=False): - ip_info = self.dhcp6_responer.get_port_ip(self.port_info, ip_version=6) - mac = self.port_info['mac_address'] + def test_get_dhcp_options_no_gateway(self): + self._test_get_dhcp_options( + self.no_gateway_port_info, has_gateway_ip=False + ) + + def test_get_dhcp_options_zero_time_no_gateway(self): + self._test_get_dhcp_options( + self.no_gateway_port_info, has_gateway_ip=False, zero_time=True + ) + + def _test_get_dhcp_options(self, port_info,has_gateway_ip=False, + zero_time=False): + ip_info = self.dhcp6_responer.get_port_ip(port_info, ip_version=6) + mac = port_info['mac_address'] option_list = [ dhcp6.option( @@ -135,11 +148,6 @@ class DHCPIPv6ResponderTestCase(dhcp_test_base.DHCPResponderBaseTestCase): dhcp6.option(code=13, data=b'\x00\x00success', length=9), - dhcp6.option( - code=23, - data=(b'\xfd\xa7\xa5\xcc4`\x00\x01\x00' - b'\x00\x00\x00\x00\x00\x00\x01'), - length=16), dhcp6.option( code=24, data=b'\x0eopenstacklocal\x00', @@ -148,6 +156,26 @@ class DHCPIPv6ResponderTestCase(dhcp_test_base.DHCPResponderBaseTestCase): code=39, data=b'\x03(host-fda7-a5cc-3460-1--bf.openstacklocal', length=42)] + + if has_gateway_ip: + option_list.append( + dhcp6.option( + code=23, + data=(b'\xfd\xa7\xa5\xcc4`\x00\x01\x00' + b'\x00\x00\x00\x00\x00\x00\x01'), + length=16 + ) + ) + else: + option_list.append( + dhcp6.option( + code=23, + data=(b'\xfe\x80\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\xa9\xfe\xa9\xfe'), + length=16 + ) + ) + if zero_time: option_list.append( dhcp6.option(