Neutron agent and drivers for BaGPipe-based BGP VPNs
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

419 lines
15 KiB

# vim: tabstop=4 shiftwidth=4 softtabstop=4
# encoding: utf-8
# Copyright 2014 Orange
#
# 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 errno
import socket
import netaddr
from oslo_config import cfg
from oslo_serialization import jsonutils
import pyroute2
from pyroute2 import common as pr_common # pylint: disable=no-name-in-module
from pyroute2 import netlink # pylint: disable=no-name-in-module
from networking_bagpipe.bagpipe_bgp.common import log_decorator
from networking_bagpipe.bagpipe_bgp.common import looking_glass as lg
from networking_bagpipe.bagpipe_bgp import constants as consts
from networking_bagpipe.bagpipe_bgp.engine import exa
from networking_bagpipe.bagpipe_bgp.vpn import dataplane_drivers as dp_drivers
from networking_bagpipe.privileged import privileged_utils
ipr = pyroute2.IPRoute() # pylint: disable=no-member
VRF_INTERFACE_PREFIX = "bvrf-"
RT_TABLE_BASE = 1000
RT_PROT_BAGPIPE = 19
# NOTE(tmorin): can this be removed ? (is jsonutils to_primitive() enough?)
def json_set_default(obj):
if isinstance(obj, set):
return list(obj)
raise TypeError
def sysctl(sysctl_path, val):
privileged_utils.sysctl(knob='.'.join(sysctl_path,), value=val)
def proxy_arp(ifname, enable):
sysctl(['net', 'ipv4', 'conf', ifname, 'proxy_arp'], int(enable))
sysctl(['net', 'ipv4', 'conf', ifname, 'proxy_arp_pvlan'], int(enable))
class MPLSLinuxVRFDataplane(dp_drivers.VPNInstanceDataplane):
def __init__(self, *args, **kwargs):
super(MPLSLinuxVRFDataplane, self).__init__(*args, **kwargs)
# FIXME: maybe not thread safe ?
self.ip = self.driver.ip
self.vrf_if = ("%s%d" % (VRF_INTERFACE_PREFIX,
self.instance_id))[:consts.LINUX_DEV_LEN]
self.rt_table = RT_TABLE_BASE + self.instance_id
self.log.info("Initializing VRF interface %s", self.vrf_if)
self.flush_routes()
self.log.debug("Creating VRF interface...")
# Create VRF interface
with self.ip.create(kind='vrf',
ifname=self.vrf_if,
vrf_table=self.rt_table,
reuse=True) as i:
i.up()
self.vrf_if_idx = i.index
# Create ip rule for VRF route table
# TODO(tmorin): do in IPDB, when possible (check: what if
# rule exist, but is not known by IPDB?)
for family in socket.AF_INET, socket.AF_INET6:
ipr.flush_rules(family=family,
iifname=self.vrf_if)
ipr.flush_rules(family=family,
oifname=self.vrf_if)
ipr.rule('add', family=family,
priority=100, iifname=self.vrf_if, table=self.rt_table,
action='FR_ACT_TO_TBL')
ipr.rule('add', family=family,
priority=100, oifname=self.vrf_if, table=self.rt_table,
action='FR_ACT_TO_TBL')
# if VRF traffic does not match any route,
# lookups must *not* fallback to main/default
# routing table
ipr.rule('add', family=family,
priority=101, iifname=self.vrf_if,
action='FR_ACT_UNREACHABLE')
ipr.rule('add', family=family,
priority=101, oifname=self.vrf_if,
action='FR_ACT_UNREACHABLE')
def add_route(self, route):
route.update({'proto': RT_PROT_BAGPIPE})
self.ip.routes.add(route).commit()
def flush_routes(self):
ipr.flush_routes(table=self.rt_table)
@log_decorator.log_info
def cleanup(self):
# bring down and disconnect all interfaces from vrf interface
with self.ip.interfaces[self.vrf_if_idx] as vrf:
for interface in vrf.ports:
with self.ip.interfaces[interface] as i:
i.down()
vrf.del_port(interface)
vrf.remove()
ipr.flush_rules(iifname=self.vrf_if)
ipr.flush_rules(oifname=self.vrf_if)
self.flush_routes()
@log_decorator.log_info
def vif_plugged(self, mac_address, ip_address, localport, label,
direction):
interface = localport['linuxif']
if interface not in self.ip.interfaces:
self.log.warning("interface %s not in interfaces, ignoring plug",
interface)
return
# ip link set dev localport master vrf_interface
with self.ip.interfaces[self.vrf_if_idx] as vrf:
vrf.add_port(interface)
with self.ip.interfaces[interface] as i:
i.up()
# add static ARP entry toward interface (because we can)
ipr.neigh('add',
dst=ip_address,
lladdr=mac_address,
ifindex=self.ip.interfaces[interface].index,
state=netlink.rtnl.ndmsg.states['permanent'])
# Setup ARP proxying
proxy_arp(interface, True)
# Configure gateway address on this port
# FIXME: that would need to be per vif port
# Retrieve broadcast IP address
broadcast_ip = str(netaddr.IPNetwork("%s/%s" % (self.gateway_ip,
self.network_plen)
).broadcast)
try:
ipr.addr('add', index=self.ip.interfaces[interface].index,
address=self.gateway_ip, mask=self.network_plen,
broadcast=broadcast_ip)
except netlink.exceptions.NetlinkError as x:
if x.code == errno.EEXIST:
# the route already exists, fine
self.log.debug("route %s already exists on %s",
self.gateway_ip,
interface)
else:
raise
# Configure mapping for incoming MPLS traffic
# with this port label
req = {'family': pr_common.AF_MPLS,
'oif': self.ip.interfaces[interface].index,
'dst': label, # FIXME how to check for BoS?
'via': {'family': socket.AF_INET,
'addr': ip_address}
}
try:
self.add_route(req)
except netlink.exceptions.NetlinkError as x:
if x.code == errno.EEXIST:
# the route already exists, fine
self.log.warning("MPLS state for %d already exists", label)
else:
raise
@log_decorator.log_info
def vif_unplugged(self, mac_address, ip_address, localport, label,
direction, last_endpoint=True):
interface = localport['linuxif']
if interface not in self.ip.interfaces:
self.log.warning("interface %s not in interfaces, ignoring plug",
interface)
return
# bring the interface down, we don't want
# traffic from this interface to leak out of the VRF
with self.ip.interfaces[interface] as i:
i.down()
# Disable ARP proxying
proxy_arp(interface, False)
# ip link set dev localport master vrf_interface
with self.ip.interfaces[self.vrf_if_idx] as i:
i.del_port(interface)
# Unconfigure gateway address on this port
# FIXME: that would need to be per vif port
# Retrieve broadcast IP address
ip = netaddr.IPNetwork("%s/%s" % (self.gateway_ip, self.network_plen))
broadcast_ip = str(ip.broadcast)
ipr.addr('del', index=self.ip.interfaces[interface].index,
address=self.gateway_ip, mask=self.network_plen,
broadcast=broadcast_ip)
with self.ip.routes.tables['mpls'][label] as r:
r.remove()
def _read_mpls_in(self, label):
routes = [r for r in self.ip.routes.tables["mpls"]
if r['dst'] == label]
assert len(routes) == 1
res = (routes[0]['oif'], routes[0]['via']['addr'])
self.log.debug("Found %s for %d with IPDB", res, label)
return res
def _nh(self, remote_pe, label, encaps):
mpls = True
if str(remote_pe) == self.driver.get_local_address():
# FIXME: does not work yet, from this table,
# 'gateway' is considered unreachable
# we could drop 'gateway' and just keep oif
# but this would only work for connected routes
# if remote_pe is ourselves
# we lookup the route for the label and deliver directly
# to this oif/gateway
# (oif, gateway) = self._read_mpls_in(label)
# mpls = False
gateway = '127.0.0.1'
oif = self.ip.interfaces['lo'].index
else:
gateway = remote_pe
oif = self.driver.mpls_interface_index
nh = {'oif': oif}
if gateway:
nh['gateway'] = gateway
if mpls:
nh['encap'] = {'type': 'mpls',
'labels': [{'bos': 1,
'label': label}]}
self.log.debug("nh: %s", nh)
return nh
def _get_route(self, prefix):
return self.ip.routes.tables[self.rt_table][prefix]
@log_decorator.log_info
def setup_dataplane_for_remote_endpoint(self, prefix, remote_pe, label,
nlri, encaps,
lb_consistent_hash_order=0):
prefix = str(prefix)
if prefix == "0.0.0.0/0":
prefix = 'default'
try:
with self._get_route(prefix) as r:
self.log.debug("a route to %s already exists, adding nexthop",
prefix)
r.add_nh(self._nh(remote_pe, label, encaps))
except KeyError:
self.log.debug("no route to %s yet, creating", prefix)
req = {'table': self.rt_table,
'dst': prefix,
'multipath': [self._nh(remote_pe, label, encaps)]}
self.log.debug("adding route: %s", req)
self.add_route(req)
@log_decorator.log_info
def remove_dataplane_for_remote_endpoint(self, prefix, remote_pe, label,
nlri, encaps,
lb_consistent_hash_order=0):
prefix = str(prefix)
if prefix == "0.0.0.0/0":
prefix = 'default'
try:
with self._get_route(prefix) as r:
# FIXME: encap info is missing here
if r['multipath']:
r.del_nh(self._nh(remote_pe, label, None))
else: # last route
r.remove()
except KeyError:
self.log.warning("no route found on remove_dataplane_for"
"_remote_endpoint for %s", prefix)
# Looking Glass ##
def get_lg_map(self):
return {"routes": (lg.SUBTREE, self.get_lg_routes),
"route_table": (lg.VALUE, self.rt_table),
"vrf_if": (lg.VALUE, self.vrf_if),
}
@log_decorator.log_info
def get_lg_routes(self, path_prefix):
routes = self.ip.routes.tables[self.rt_table]
return [{r['dst']:
jsonutils.loads(jsonutils.dumps(r, default=json_set_default))}
for r in routes]
class MPLSLinuxDataplaneDriver(dp_drivers.DataplaneDriver):
"""Dataplane driver relying on the MPLS stack in the Linux kernel
This dataplane driver relies on the MPLS stack in the Linux kernel,
and on linux vrf interfaces.
"""
required_kernel = "4.4"
dataplane_instance_class = MPLSLinuxVRFDataplane
type = consts.IPVPN
ecmp_support = True
driver_opts = [
cfg.StrOpt("mpls_interface",
help=("Interface used to send/receive MPLS traffic. "
"Use '*gre*' to choose automatic creation of a tunnel"
" interface for MPLS/GRE encap")),
]
def __init__(self):
super(MPLSLinuxDataplaneDriver, self).__init__()
privileged_utils.modprobe('mpls_router')
privileged_utils.modprobe('mpls_gso')
privileged_utils.modprobe('mpls_iptunnel')
privileged_utils.modprobe('vrf')
self.ip = pyroute2.IPDB() # pylint: disable=no-member
@log_decorator.log_info
def reset_state(self):
# remove all VRF interfaces
for itf in self.ip.interfaces.keys():
if isinstance(itf, str) and itf.startswith(VRF_INTERFACE_PREFIX):
# bring the interface vrf slave interfaces down,
# we don't want traffic from these interfaces
# to leak out of the VRF after removal of VRF interface
for index in self.ip.interfaces[itf].ports:
with self.ip.interfaces[index] as port:
port.down()
ipr.link('del', ifname=itf)
# Flush all routes setup by us in past runs
ipr.flush_routes(proto=RT_PROT_BAGPIPE)
# Flush all MPLS routes redirecting traffic to network namespaces
# (just in case, should be covered by the above)
ipr.flush_routes(family=pr_common.AF_MPLS)
@log_decorator.log_info
def initialize(self):
sysctl('net.mpls.platform_labels', 2**20-1)
if "*gre*" in self.config["mpls_interface"]:
self.mpls_interface = "gre_wildcard"
raise Exception("MPLS/GRE not supported yet")
else:
self.mpls_interface = self.config["mpls_interface"]
sysctl('net.mpls.conf.%s.input' % self.mpls_interface, 1)
self.mpls_interface_index = self.ip.interfaces[self.mpls_interface
].index
# for traffic from ourselves:
sysctl('net.mpls.conf.lo.input', 1)
# enable forwarding
sysctl('net.ipv4.ip_forward', 1)
def supported_encaps(self):
yield exa.Encapsulation(exa.Encapsulation.Type.MPLS)
# we also accept route with no encap specified
yield exa.Encapsulation(exa.Encapsulation.Type.DEFAULT)
# Looking glass ####
def get_lg_map(self):
return {"mpls": (lg.SUBTREE, self.get_lg_mpls_routes),
}
def get_lg_mpls_routes(self, path_prefix):
return [{r['dst']:
jsonutils.loads(jsonutils.dumps(r, default=json_set_default))}
for r in self.ip.routes.tables['mpls']]