diff --git a/neutron/agent/l2/extensions/dhcp/ipv4.py b/neutron/agent/l2/extensions/dhcp/ipv4.py new file mode 100644 index 00000000000..6c2ad74ecd9 --- /dev/null +++ b/neutron/agent/l2/extensions/dhcp/ipv4.py @@ -0,0 +1,213 @@ +# Copyright (c) 2021 China Unicom Cloud Data Co.,Ltd. +# Copyright (c) 2019 - 2020 China Telecom Corporation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import struct + +import netaddr +from neutron_lib import constants +from os_ken.lib import addrconv +from os_ken.lib.packet import dhcp +from os_ken.lib.packet import ethernet +from os_ken.lib.packet import ipv4 +from os_ken.lib.packet import packet +from os_ken.lib.packet import udp +from oslo_config import cfg +from oslo_log import log as logging + +from neutron.agent.l2.extensions.dhcp import base as dhcp_base + +LOG = logging.getLogger(__name__) + +DHCPV4_TYPE_MAP = {dhcp.DHCP_DISCOVER: 'DHCPDISCOVER', + dhcp.DHCP_OFFER: 'DHCPOFFER', + dhcp.DHCP_REQUEST: 'DHCPREQUEST', + dhcp.DHCP_ACK: 'DHCPACK'} +DHCPV4_MSG_TYPE_BYTES_ACK = b'\x05' +DHCPV4_MSG_TYPE_BYTES_OFFER = b'\x02' + +# TODO(liuyulong): Move to os-ken(ryu) someday. +# Align the name prefix with the definitions in +# os_ken.lib.packet.dhcp. +DHCP_BROADCAST_ADDRESS_OPT = 28 + + +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 + + # For some VMs they may need the metadate IP's route, we move + # the destination to gateway IP. + if gateway: + meta_route = self.get_bin_route( + constants.METADATA_V4_CIDR, gateway) + bin_routes += meta_route + + # Subnet routes + for route in routes or []: + bin_routes += self.get_bin_route(route['destination'], + route['nexthop']) + return bin_routes + + def get_dhcp_options(self, port_info, is_ack=False): + fixed_ips = port_info['fixed_ips'] + 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'] + bin_server = addrconv.ipv4.text_to_bin(gateway_ip) + + option_list = [] + + if is_ack: + option_list.append( + dhcp.option(tag=dhcp.DHCP_MESSAGE_TYPE_OPT, + value=DHCPV4_MSG_TYPE_BYTES_ACK)) + else: + option_list.append( + dhcp.option(tag=dhcp.DHCP_MESSAGE_TYPE_OPT, + value=DHCPV4_MSG_TYPE_BYTES_OFFER)) + + option_list.append( + dhcp.option(tag=dhcp.DHCP_SERVER_IDENTIFIER_OPT, + value=bin_server)) + option_list.append( + dhcp.option(tag=dhcp.DHCP_IP_ADDR_LEASE_TIME_OPT, + value=struct.pack( + '!i', cfg.CONF.dhcp_lease_duration))) + + if cfg.CONF.DHCP.renewal_time > 0: + option_list.append( + dhcp.option(tag=dhcp.DHCP_RENEWAL_TIME_OPT, + value=struct.pack( + '!I', cfg.CONF.DHCP.renewal_time))) + if cfg.CONF.DHCP.rebinding_time > 0: + option_list.append( + dhcp.option(tag=dhcp.DHCP_REBINDING_TIME_OPT, + value=struct.pack( + '!I', cfg.CONF.DHCP.rebinding_time))) + + option_list.append( + dhcp.option(tag=dhcp.DHCP_SUBNET_MASK_OPT, + value=addrconv.ipv4.text_to_bin(str(net.netmask)))) + # Option: (28) Broadcast Address + option_list.append( + dhcp.option(tag=DHCP_BROADCAST_ADDRESS_OPT, + value=addrconv.ipv4.text_to_bin(str(net.broadcast)))) + # DNS + if dns_nameservers: + option_list.append( + dhcp.option(tag=dhcp.DHCP_DNS_SERVER_ADDR_OPT, + value=self.get_bin_dns(dns_nameservers))) + if cfg.CONF.dns_domain: + option_list.append( + dhcp.option(tag=dhcp.DHCP_DOMAIN_NAME_OPT, + 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)) + # Static routes + option_list.append( + dhcp.option(tag=dhcp.DHCP_CLASSLESS_ROUTE_OPT, + value=self.get_bin_routes(gateway_ip, + host_routes))) + # MTU + mtu = int(port_info.get('mtu', 0)) + if mtu > 0: + mtu_bin = struct.pack('!H', mtu) + option_list.append( + dhcp.option(tag=dhcp.DHCP_INTERFACE_MTU_OPT, + value=mtu_bin)) + options = dhcp.options(option_list=option_list) + return options + + def get_ret_packet(self, packet_in, port_info, is_ack=False): + ip_info = self.get_port_ip(port_info, + ip_version=constants.IP_VERSION_4) + if not ip_info: + return + ip_addr = ip_info['ip_address'] + gateway_ip = ip_info['gateway_ip'] + + options = self.get_dhcp_options(port_info, is_ack) + if is_ack: + fqdn = 'host-%s' % ip_addr.replace('.', '-').replace(':', '-') + if cfg.CONF.dns_domain: + fqdn = '%s.%s' % (fqdn, cfg.CONF.dns_domain) + domain_name_bin = struct.pack('!%ds' % len(fqdn), + bytes(str(fqdn).encode())) + options.option_list.append( + dhcp.option(tag=dhcp.DHCP_HOST_NAME_OPT, + value=domain_name_bin)) + + header_eth = packet_in.get_protocol(ethernet.ethernet) + header_ipv4 = packet_in.get_protocol(ipv4.ipv4) + header_dhcp = packet_in.get_protocol(dhcp.dhcp) + + ret_pkt = packet.Packet() + ret_pkt.add_protocol(ethernet.ethernet( + ethertype=header_eth.ethertype, + dst=header_eth.src, + src=self.hw_addr)) + ret_pkt.add_protocol( + ipv4.ipv4(dst=header_ipv4.dst, + src=gateway_ip, + proto=header_ipv4.proto)) + ret_pkt.add_protocol(udp.udp(src_port=constants.DHCP_RESPONSE_PORT, + dst_port=constants.DHCP_CLIENT_PORT)) + ret_pkt.add_protocol(dhcp.dhcp(op=dhcp.DHCP_BOOT_REPLY, + chaddr=header_eth.src, + siaddr=gateway_ip, + boot_file=header_dhcp.boot_file, + yiaddr=ip_addr, + xid=header_dhcp.xid, + options=options)) + return ret_pkt + + def assemble_ack(self, pkt, port_info): + ack = self.get_ret_packet(pkt, port_info, is_ack=True) + LOG.debug("DHCP controller DHCPv4 ACK assembled: %s", ack) + return ack + + def assemble_offer(self, pkt, port_info): + offer = self.get_ret_packet(pkt, port_info) + LOG.debug("DHCP controller DHCPv4 offer assemble: %s", offer) + return offer + + def get_state(self, pkt_dhcp): + dhcp_state = ord( + [opt for opt in pkt_dhcp.options.option_list + if opt.tag == dhcp.DHCP_MESSAGE_TYPE_OPT][0].value) + return DHCPV4_TYPE_MAP.get(dhcp_state) + + def handle_dhcp(self, datapath, ofport, pkt, port_info): + pkt_dhcp = pkt.get_protocol(dhcp.dhcp) + dhcp_state = self.get_state(pkt_dhcp) + LOG.debug("DHCP controller DHCPv4 packet received, " + "state: %s, data: %s", + dhcp_state, pkt_dhcp) + if dhcp_state == 'DHCPDISCOVER': + self.packet_out(datapath, ofport, + self.assemble_offer(pkt, port_info)) + elif dhcp_state == 'DHCPREQUEST': + self.packet_out(datapath, ofport, + self.assemble_ack(pkt, port_info)) diff --git a/neutron/conf/plugins/ml2/drivers/ovs_conf.py b/neutron/conf/plugins/ml2/drivers/ovs_conf.py index 2347aedd69e..5ef3b5e210f 100644 --- a/neutron/conf/plugins/ml2/drivers/ovs_conf.py +++ b/neutron/conf/plugins/ml2/drivers/ovs_conf.py @@ -174,10 +174,20 @@ agent_opts = [ "traffic.")), ] +dhcp_opts = [ + cfg.IntOpt('renewal_time', default=0, + help=_("DHCP renewal time T1 (in seconds). If set to 0, it " + "will default to half of the lease time.")), + cfg.IntOpt('rebinding_time', default=0, + help=_("DHCP rebinding time T2 (in seconds). If set to 0, it " + "will default to 7/8 of the lease time.")), +] + def register_ovs_agent_opts(cfg=cfg.CONF): cfg.register_opts(ovs_opts, "OVS") cfg.register_opts(agent_opts, "AGENT") + cfg.register_opts(dhcp_opts, "DHCP") def register_ovs_opts(cfg=cfg.CONF): diff --git a/neutron/tests/unit/agent/l2/extensions/dhcp/test_ipv4.py b/neutron/tests/unit/agent/l2/extensions/dhcp/test_ipv4.py new file mode 100644 index 00000000000..0b5b7e1698c --- /dev/null +++ b/neutron/tests/unit/agent/l2/extensions/dhcp/test_ipv4.py @@ -0,0 +1,124 @@ +# Copyright (c) 2021 China Unicom Cloud Data Co.,Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from unittest import mock + +from os_ken.lib.packet import dhcp +from os_ken.lib.packet import ipv4 + +from neutron.agent.l2.extensions.dhcp import ipv4 as dhcp_ipv4 +from neutron.tests.unit.agent.l2.extensions.dhcp \ + import test_base as dhcp_test_base + + +class DHCPIPv4ResponderTestCase(dhcp_test_base.DHCPResponderBaseTestCase): + + def setUp(self): + super(DHCPIPv4ResponderTestCase, self).setUp() + self.dhcp4_responer = dhcp_ipv4.DHCPIPv4Responder(self.agent_api, + self.ext_api) + self.dhcp4_responer.int_br = self.int_br + + def _compare_option_values(self, expect_options, test_options): + # os_ken dhcp.option class does not have __eq__ method so compare + # one by one + expected = [(option.tag, option.length, option.value) + for option in expect_options] + test = [(option.tag, option.length, option.value) + for option in test_options] + for i in test: + self.assertIn(i, expected) + + def test_handle_dhcp(self): + self.dhcp4_responer.packet_out = mock.Mock() + datapath = mock.Mock() + ofport = 1 + packet_in = self._create_test_dhcp_request_packet() + self.dhcp4_responer.handle_dhcp( + datapath, ofport, packet_in, self.port_info) + self.dhcp4_responer.packet_out.assert_called_once_with( + datapath, ofport, mock.ANY) + + def test_get_state(self): + packet_in = self._create_test_dhcp_request_packet() + dhcp_pkt = packet_in.get_protocol(dhcp.dhcp) + state = self.dhcp4_responer.get_state(dhcp_pkt) + self.assertEqual('DHCPREQUEST', state) + + def test_get_ret_packet(self): + packet_in = self._create_test_dhcp_request_packet() + ret_pkt = self.dhcp4_responer.get_ret_packet( + packet_in, self.port_info, is_ack=False) + + ip_header = ret_pkt.get_protocol(ipv4.ipv4) + self.assertIsNotNone(ip_header) + 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') + 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')], + options_len=0) + offer_options = self.dhcp4_responer.get_dhcp_options(self.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')], + options_len=0) + ack_options = self.dhcp4_responer.get_dhcp_options( + self.port_info, is_ack=True) + self._compare_option_values(expect_ack_options.option_list, + ack_options.option_list) + + 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 ' + b'\x02\x02\x02\x02\xc0\xa8\x01e') + bin_routes = self.dhcp4_responer.get_bin_routes( + self.port_info['fixed_ips'][0]['gateway_ip'], + self.port_info['fixed_ips'][0]['host_routes']) + self.assertEqual(expect_bin_routes, bin_routes)