dragonflow/dragonflow/controller/apps/dhcp.py

541 lines
21 KiB
Python

# Copyright (c) 2015 Huawei Tech. 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.
import collections
import copy
import functools
import math
import struct
from neutron.conf import common as common_config
from neutron_lib import constants as n_const
from oslo_log import log
from ryu.lib import addrconv
from ryu.lib.packet import dhcp
from ryu.lib.packet import ethernet
from ryu.lib.packet import ipv4
from ryu.lib.packet import packet as ryu_packet
from ryu.lib.packet import udp
from ryu.ofproto import ether
from dragonflow.common import utils as df_utils
from dragonflow import conf as cfg
from dragonflow.controller.common import arp_responder
from dragonflow.controller.common import constants as const
from dragonflow.controller.common import icmp_responder
from dragonflow.controller import df_base_app
from dragonflow.db.models import constants as model_constants
from dragonflow.db.models import host_route
from dragonflow.db.models import l2
LOG = log.getLogger(__name__)
class DHCPApp(df_base_app.DFlowApp):
def __init__(self, *args, **kwargs):
super(DHCPApp, self).__init__(*args, **kwargs)
self.idle_timeout = 30
self.hard_timeout = 0
cfg.CONF.register_opts(common_config.core_opts)
self.conf = cfg.CONF.df_dhcp_app
self.global_dns_list = self.conf.df_dns_servers
self.lease_time = cfg.CONF.dhcp_lease_duration
self.domain_name = cfg.CONF.dns_domain
self.block_hard_timeout = self.conf.df_dhcp_block_time_in_sec
self.default_interface_mtu = self.conf.df_default_network_device_mtu
self._port_rate_limiters = collections.defaultdict(
functools.partial(df_utils.RateLimiter,
max_rate=self.conf.df_dhcp_max_rate_per_sec,
time_unit=1))
self.api.register_table_handler(const.DHCP_TABLE,
self.packet_in_handler)
self._dhcp_ip_by_subnet = {}
def _get_dhcp_port_by_network(self, network_unique_key):
lswitch = self.db_store.get_one(l2.LogicalSwitch(
unique_key=network_unique_key),
index=l2.LogicalSwitch.get_index('unique_key'))
return self.db_store.get_one(
l2.LogicalPort(
device_owner=n_const.DEVICE_OWNER_DHCP,
lswitch=lswitch
),
index=l2.LogicalPort.get_index('switch,owner')
)
def switch_features_handler(self, ev):
self._install_dhcp_packet_match_flow()
self.add_flow_go_to_table(const.DHCP_TABLE,
const.PRIORITY_DEFAULT,
const.L2_LOOKUP_TABLE)
self._port_rate_limiters.clear()
def _check_port_limit(self, lport):
port_rate_limiter = self._port_rate_limiters[lport.id]
return port_rate_limiter()
def packet_in_handler(self, event):
msg = event.msg
pkt = ryu_packet.Packet(msg.data)
pkt_ip = pkt.get_protocol(ipv4.ipv4)
if not pkt_ip:
LOG.error("No support for non IPv4 protocol")
return
unique_key = msg.match.get('reg6')
lport = self.db_store.get_one(
l2.LogicalPort(unique_key=unique_key),
index=l2.LogicalPort.get_index('unique_key'),
)
network_key = msg.match.get('metadata')
dhcp_lport = self._get_dhcp_port_by_network(network_key)
if not dhcp_lport:
LOG.error("No DHCP port for network {}".format(str(network_key)))
return
if self._check_port_limit(lport):
self._block_port_dhcp_traffic(unique_key, lport)
LOG.warning("pass rate limit for %(port_id)s blocking DHCP "
"traffic for %(time)s sec",
{'port_id': lport.id,
'time': self.block_hard_timeout})
return
if not self.db_store.get_one(lport):
LOG.error("Port %s no longer found.", lport.id)
return
try:
self._handle_dhcp_request(pkt, lport, dhcp_lport)
except Exception:
LOG.exception("Unable to handle packet %s", msg)
def _handle_dhcp_request(self, packet, lport, dhcp_port):
dhcp_packet = packet.get_protocol(dhcp.dhcp)
dhcp_message_type = self._get_dhcp_message_type_opt(dhcp_packet)
send_packet = None
if dhcp_message_type == dhcp.DHCP_DISCOVER:
send_packet = self._create_dhcp_response(
packet,
dhcp_packet,
dhcp.DHCP_OFFER,
lport,
dhcp_port)
LOG.info("sending DHCP offer for port IP %(port_ip)s "
"port id %(port_id)s",
{'port_ip': lport.ip, 'port_id': lport.id})
elif dhcp_message_type == dhcp.DHCP_REQUEST:
send_packet = self._create_dhcp_response(
packet,
dhcp_packet,
dhcp.DHCP_ACK,
lport,
dhcp_port)
LOG.info("sending DHCP ACK for port IP %(port_ip)s "
"port id %(tunnel_id)s",
{'port_ip': lport.ip,
'tunnel_id': lport.id})
else:
LOG.error("DHCP message type %d not handled",
dhcp_message_type)
if send_packet:
unique_key = lport.unique_key
self.dispatch_packet(send_packet, unique_key)
def _create_dhcp_response(self, packet, dhcp_request,
response_type, lport, dhcp_port):
pkt_ipv4 = packet.get_protocol(ipv4.ipv4)
pkt_ethernet = packet.get_protocol(ethernet.ethernet)
try:
subnet = lport.subnets[0]
except IndexError:
LOG.warning("No subnet found for port %s", lport.id)
return
dhcp_server_address = self._dhcp_ip_by_subnet.get(subnet.id)
if not dhcp_server_address:
LOG.warning("Could not find DHCP server address for subnet %s",
subnet.id)
return
option_list = self._build_dhcp_options(dhcp_request,
response_type,
lport,
subnet,
dhcp_server_address)
options = dhcp.options(option_list=option_list)
dhcp_response = ryu_packet.Packet()
dhcp_response.add_protocol(ethernet.ethernet(
ethertype=ether.ETH_TYPE_IP,
dst=pkt_ethernet.src,
src=dhcp_port.mac))
dhcp_response.add_protocol(ipv4.ipv4(dst=pkt_ipv4.src,
src=dhcp_server_address,
proto=pkt_ipv4.proto))
dhcp_response.add_protocol(udp.udp(src_port=const.DHCP_SERVER_PORT,
dst_port=const.DHCP_CLIENT_PORT))
siaddr = lport.dhcp_params.siaddr or dhcp_server_address
dhcp_response.add_protocol(dhcp.dhcp(op=dhcp.DHCP_BOOT_REPLY,
chaddr=pkt_ethernet.src,
siaddr=siaddr,
boot_file=dhcp_request.boot_file,
yiaddr=lport.ip,
xid=dhcp_request.xid,
options=options))
return dhcp_response
def _build_dhcp_options(self, dhcp_request, response_type,
lport, subnet, srv_addr):
"""
according the RFC the server need to response with
with all the option that "explicitly configured options"
and supply as many of the "requested parameters" as
possible
https://www.ietf.org/rfc/rfc2131.txt (page 29)
"""
# explicitly configured options
default_opts = self._build_response_default_options(response_type,
lport, subnet,
srv_addr)
# requested options (according to dhcp_params.opt)
response_opts = self._build_response_requested_options(dhcp_request,
lport,
default_opts)
response_opts.update(default_opts)
option_list = [dhcp.option(tag, value)
for tag, value in response_opts.items()]
return option_list
def _build_response_default_options(self, response_type, lport,
subnet, srv_addr):
options_dict = {}
pkt_type_packed = struct.pack('!B', response_type)
dns = self._get_dns_address_list_bin(subnet)
host_routes = self._get_host_routes_list_bin(subnet, lport)
server_addr_bin = srv_addr.packed
netmask_bin = subnet.cidr.netmask.packed
domain_name_bin = struct.pack('!%ss' % len(self.domain_name),
self.domain_name.encode())
lease_time_bin = struct.pack('!I', self.lease_time)
options_dict[dhcp.DHCP_MESSAGE_TYPE_OPT] = pkt_type_packed
options_dict[dhcp.DHCP_SUBNET_MASK_OPT] = netmask_bin
options_dict[dhcp.DHCP_IP_ADDR_LEASE_TIME_OPT] = lease_time_bin
options_dict[dhcp.DHCP_SERVER_IDENTIFIER_OPT] = server_addr_bin
options_dict[dhcp.DHCP_DNS_SERVER_ADDR_OPT] = dns
options_dict[dhcp.DHCP_DOMAIN_NAME_OPT] = domain_name_bin
options_dict[dhcp.DHCP_CLASSLESS_ROUTE_OPT] = host_routes
gw_ip = self._get_port_gateway_address(subnet, lport)
if gw_ip:
gw_ip_bin = gw_ip.packed
options_dict[dhcp.DHCP_GATEWAY_ADDR_OPT] = gw_ip_bin
if response_type == dhcp.DHCP_ACK:
intreface_mtu = self._get_port_mtu(lport)
mtu_bin = struct.pack('!H', intreface_mtu)
options_dict[dhcp.DHCP_INTERFACE_MTU_OPT] = mtu_bin
return options_dict
def _build_response_requested_options(self, dhcp_request,
lport, default_opts):
options_dict = {}
req_list_opt = dhcp.DHCP_PARAMETER_REQUEST_LIST_OPT
requested_opts = self._get_dhcp_option_by_tag(dhcp_request,
req_list_opt)
if not requested_opts:
return {}
for opt in requested_opts:
# For python3 opt is already int.
if isinstance(opt, str):
opt_int = ord(opt)
else:
opt_int = opt
if opt_int in default_opts:
# already answered by the default options
continue
value = lport.dhcp_params.opts.get(opt_int)
if value:
value_bin = struct.pack('!%ss' % len(value),
value.encode())
options_dict[opt_int] = value_bin
return options_dict
def _get_dns_address_list_bin(self, subnet):
dns_servers = self.global_dns_list
if len(subnet.dns_nameservers) > 0:
dns_servers = subnet.dns_nameservers
dns_bin = b''
for address in dns_servers:
dns_bin += addrconv.ipv4.text_to_bin(address)
return dns_bin
def _get_host_routes_list_bin(self, subnet, lport):
host_routes = copy.copy(subnet.host_routes)
if self.conf.df_add_link_local_route:
# Add route for metadata request.
host_routes.append(host_route.HostRoute(
destination='%s/32' % const.METADATA_SERVICE_IP,
nexthop=lport.ip))
routes_bin = b''
opt = lport.dhcp_params.opts.get(dhcp.DHCP_CLASSLESS_ROUTE_OPT)
if opt:
dest_cidr, _c, via = opt.partition(',')
host_routes.append(
host_route.HostRoute(destination=dest_cidr,
nexthop=via))
# We must add the default route here. if a host supports classless
# route options, it must ignore the router option
gateway = self._get_port_gateway_address(subnet, lport)
if gateway is not None:
host_routes.append(
host_route.HostRoute(
destination='0.0.0.0/0',
nexthop=gateway,
),
)
for route in host_routes:
dest = route.destination.network
mask = route.destination.prefixlen
routes_bin += struct.pack('B', mask)
"""
for compact encoding
Width of subnet mask Number of significant octets
0 0
1- 8 1
9-16 2
17-24 3
25-32 4
"""
addr_bin = addrconv.ipv4.text_to_bin(dest)
dest_len = int(math.ceil(mask / 8.0))
routes_bin += addr_bin[:dest_len]
routes_bin += addrconv.ipv4.text_to_bin(route.nexthop)
return routes_bin
def _get_dhcp_option_by_tag(self, dhcp_packet, tag):
if dhcp_packet.options:
for opt in dhcp_packet.options.option_list:
if opt.tag == tag:
return opt.value
def _get_dhcp_message_type_opt(self, dhcp_packet):
opt_value = self._get_dhcp_option_by_tag(dhcp_packet,
dhcp.DHCP_MESSAGE_TYPE_OPT)
if opt_value:
return ord(opt_value)
def _get_port_gateway_address(self, subnet, lport):
gateway_ip = subnet.gateway_ip
if gateway_ip:
return gateway_ip
return lport.dhcp_params.opts.get(dhcp.DHCP_GATEWAY_ADDR_OPT)
def _get_port_mtu(self, lport):
# get network mtu from lswitch
lswitch = lport.lswitch
mtu = lswitch.mtu
if mtu:
return mtu
return self.default_interface_mtu
def _install_dhcp_classification_flow(self):
parser = self.parser
match = parser.OFPMatch(eth_type=ether.ETH_TYPE_IP,
ip_proto=n_const.PROTO_NUM_UDP,
udp_src=const.DHCP_CLIENT_PORT,
udp_dst=const.DHCP_SERVER_PORT)
self.add_flow_go_to_table(const.SERVICES_CLASSIFICATION_TABLE,
const.PRIORITY_MEDIUM,
const.DHCP_TABLE, match=match)
def _block_port_dhcp_traffic(self, unique_key, lport):
match = self.parser.OFPMatch(reg6=unique_key)
drop_inst = None
self.mod_flow(
inst=drop_inst,
priority=const.PRIORITY_VERY_HIGH,
hard_timeout=self.block_hard_timeout,
table_id=const.DHCP_TABLE,
match=match)
def _install_dhcp_packet_match_flow(self):
parser = self.parser
match = parser.OFPMatch(eth_type=ether.ETH_TYPE_IP,
ip_proto=n_const.PROTO_NUM_UDP,
udp_src=const.DHCP_CLIENT_PORT,
udp_dst=const.DHCP_SERVER_PORT)
self.add_flow_go_to_table(const.SERVICES_CLASSIFICATION_TABLE,
const.PRIORITY_MEDIUM,
const.DHCP_TABLE, match=match)
def _install_dhcp_port_flow(self, lswitch):
parser = self.parser
ofproto = self.ofproto
match = parser.OFPMatch(metadata=lswitch.unique_key)
actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER,
ofproto.OFPCML_NO_BUFFER)]
inst = [parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS,
actions)]
self.mod_flow(
inst=inst,
table_id=const.DHCP_TABLE,
priority=const.PRIORITY_MEDIUM,
match=match)
def _remove_dhcp_network_flow(self, lswitch):
parser = self.parser
ofproto = self.ofproto
match = parser.OFPMatch(metadata=lswitch.unique_key)
self.mod_flow(
table_id=const.DHCP_TABLE,
command=ofproto.OFPFC_DELETE,
priority=const.PRIORITY_MEDIUM,
match=match)
def _add_dhcp_ips_by_subnet(self, lport):
subnet_ids = (subnet.id for subnet in lport.subnets)
self._dhcp_ip_by_subnet.update(dict(zip(subnet_ids, lport.ips)))
@df_base_app.register_event(l2.LogicalPort, model_constants.EVENT_CREATED)
def _lport_created(self, lport):
if lport.device_owner != n_const.DEVICE_OWNER_DHCP:
return
self._install_dhcp_port_responders(lport)
self._install_dhcp_port_flow(lport.lswitch)
self._add_dhcp_ips_by_subnet(lport)
def _update_port_responders(self, lport, orig_lport):
self._uninstall_dhcp_port_responders(orig_lport)
self._install_dhcp_port_responders(lport)
def _update_dhcp_ips_by_subnet(self, lport, orig_lport):
self._add_dhcp_ips_by_subnet(lport)
orig_subnets = set(subnet.id for subnet in orig_lport.subnets)
new_subnets = set(subnet.id for subnet in lport.subnets)
deleted_subnets = orig_subnets - new_subnets
for subnet_id in deleted_subnets:
del self._dhcp_ip_by_subnet[subnet_id]
def _delete_lport_rate_limiter(self, lport):
if not lport.is_local:
return
if lport.id in self._port_rate_limiters:
del self._port_rate_limiters[lport.id]
@df_base_app.register_event(l2.LogicalPort, model_constants.EVENT_UPDATED)
def _lport_updated(self, lport, orig_lport):
if lport.device_owner != n_const.DEVICE_OWNER_DHCP:
return
v4_ips = set(ip for ip in lport.ips if
ip.version == n_const.IP_VERSION_4)
v4_old_ips = set(ip for ip in orig_lport.ips
if ip.version == n_const.IP_VERSION_4)
if v4_ips != v4_old_ips or lport.mac != orig_lport.mac:
self._update_port_responders(lport, orig_lport)
self._update_dhcp_ips_by_subnet(lport, orig_lport)
def _delete_dhcp_ips_by_subnet(self, lport):
for subnet in lport.subnets:
del self._dhcp_ip_by_subnet[subnet.id]
@df_base_app.register_event(l2.LogicalPort, model_constants.EVENT_DELETED)
def _lport_deleted(self, lport):
if lport.device_owner != n_const.DEVICE_OWNER_DHCP:
self._delete_lport_rate_limiter(lport)
return
self._uninstall_dhcp_port_responders(lport)
self._remove_dhcp_network_flow(lport.lswitch)
self._delete_dhcp_ips_by_subnet(lport)
def _install_dhcp_port_responders(self, lport):
ips_v4 = (ip for ip in lport.ips
if ip.version == n_const.IP_VERSION_4)
for ip in ips_v4:
icmp_responder.ICMPResponder(
app=self,
network_id=lport.lswitch.unique_key,
interface_ip=lport.ip,
table_id=const.L2_LOOKUP_TABLE,
).add()
arp_responder.ArpResponder(
app=self,
network_id=lport.lswitch.unique_key,
interface_ip=ip,
interface_mac=lport.mac,
).add()
def _uninstall_dhcp_port_responders(self, lport):
ips_v4 = (ip for ip in lport.ips
if ip.version == n_const.IP_VERSION_4)
for ip in ips_v4:
icmp_responder.ICMPResponder(
app=self,
network_id=lport.lswitch.unique_key,
interface_ip=lport.ip,
table_id=const.L2_LOOKUP_TABLE,
).remove()
arp_responder.ArpResponder(
app=self,
network_id=lport.lswitch.unique_key,
interface_ip=ip,
interface_mac=lport.mac,
).remove()