Add the DHCPReponder for IPv4

This patch implements the responder for DHCPv4. The main
work of the responder is to assemble the DHCP packet with
its protocol required options based on the information of
neutron Port/Subnet/Network.

Partially-Implements: bp/distributed-dhcp-for-ml2-ovs
Related-Bug: #1900934

Change-Id: I8b0466263388085bb1949ea37f4788579c46093a
This commit is contained in:
LIU Yulong
2021-01-27 17:09:33 +08:00
parent 493286511d
commit b0022c51fe
3 changed files with 347 additions and 0 deletions
+213
View File
@@ -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))
@@ -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):
@@ -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)