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
This commit is contained in:
Dai Dang Van
2025-04-17 17:17:53 +07:00
parent a2af9b8baa
commit 2ff5b4a18b
6 changed files with 193 additions and 88 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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