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:
@@ -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)
|
||||
Reference in New Issue
Block a user