bgp: add out-filter function

Added out-filter function to Ryu BGPSpeaker. It supports IPv4 and
IPv6.

Signed-off-by: Hiroshi Yokoi <yokoi.hiroshi@po.ntts.co.jp>
Signed-off-by: FUJITA Tomonori <fujita.tomonori@lab.ntt.co.jp>
This commit is contained in:
Hiroshi Yokoi 2014-07-17 16:28:29 +09:00 committed by FUJITA Tomonori
parent 9d7433e44a
commit 3c0c9ce737
7 changed files with 323 additions and 6 deletions

View File

@ -10,3 +10,6 @@ BGPSpeaker class
.. autoclass:: ryu.services.protocols.bgp.bgpspeaker.EventPrefix
:members:
.. autoclass:: ryu.services.protocols.bgp.bgpspeaker.PrefixList
:members:

View File

@ -78,6 +78,9 @@ def update_neighbor(neigh_ip_address, changes):
if k == neighbors.ENABLED:
rets.append(update_neighbor_enabled(neigh_ip_address, v))
if k == neighbors.OUT_FILTER:
rets.append(_update_outfilter(neigh_ip_address, v))
return all(rets)
@ -88,6 +91,12 @@ def _update_med(neigh_ip_address, value):
return True
def _update_outfilter(neigh_ip_address, value):
neigh_conf = _get_neighbor_conf(neigh_ip_address)
neigh_conf.out_filter = value
return True
@RegisterWithArgChecks(name='neighbor.delete',
req_args=[neighbors.IP_ADDRESS])
def delete_neighbor(neigh_ip_address):

View File

@ -52,7 +52,14 @@ from ryu.services.protocols.bgp.rtconf.neighbors import DEFAULT_CAP_MBGP_VPNV4
from ryu.services.protocols.bgp.rtconf.neighbors import DEFAULT_CAP_MBGP_VPNV6
from ryu.services.protocols.bgp.rtconf.neighbors import PEER_NEXT_HOP
from ryu.services.protocols.bgp.rtconf.neighbors import PASSWORD
from ryu.services.protocols.bgp.rtconf.neighbors import OUT_FILTER
from ryu.services.protocols.bgp.application import RyuBGPSpeaker
from netaddr.ip import IPAddress, IPNetwork
from ryu.lib.packet.bgp import RF_IPv4_UC, RF_IPv6_UC
OUT_FILTER_RF_IPv4_UC = RF_IPv4_UC
OUT_FILTER_RF_IPv6_UC = RF_IPv6_UC
class EventPrefix(object):
@ -80,6 +87,131 @@ class EventPrefix(object):
self.is_withdraw = is_withdraw
class PrefixList(object):
"""
used to specify a prefix for out-filter.
We can create PrefixList object as follows.
prefix_list = PrefixList('10.5.111.0/24', policy=PrefixList.POLICY_PERMIT)
================ ==================================================
Attribute Description
================ ==================================================
prefix A prefix used for out-filter
policy PrefixList.POLICY.PERMIT or PrefixList.POLICY_DENY
ge Prefix length that will be applied out-filter.
ge means greater than or equal.
le Prefix length that will be applied out-filter.
le means less than or equal.
================ ==================================================
For example, when PrefixList object is created as follows:
* p = PrefixList('10.5.111.0/24',
policy=PrefixList.POLICY_DENY,
ge=26, le=28)
prefixes which match 10.5.111.0/24 and its length matches
from 26 to 28 will be filtered and stopped to send to neighbor
because of POLICY_DENY. If you specify POLICY_PERMIT,
the path is sent to neighbor.
If you don't want to send prefixes 10.5.111.64/26 and 10.5.111.32/27
and 10.5.111.16/28, and allow to send other 10.5.111.0's prefixes,
you can do it by specifying as follows;
* p = PrefixList('10.5.111.0/24',
policy=PrefixList.POLICY_DENY,
ge=26, le=28).
"""
POLICY_DENY = 0
POLICY_PERMIT = 1
def __init__(self, prefix, policy=POLICY_PERMIT, ge=None, le=None):
self._prefix = prefix
self._policy = policy
self._network = IPNetwork(prefix)
self._ge = ge
self._le = le
def __cmp__(self, other):
return cmp(self.prefix, other.prefix)
def __repr__(self):
policy = 'PERMIT' \
if self._policy == self.POLICY_PERMIT else 'DENY'
return 'PrefixList(prefix=%s,policy=%s,ge=%s,le=%s)'\
% (self._prefix, policy, self._ge, self._le)
@property
def prefix(self):
return self._prefix
@property
def policy(self):
return self._policy
@property
def ge(self):
return self._ge
@property
def le(self):
return self._le
def evaluate(self, prefix):
""" This method evaluates the prefix.
Returns this object's policy and the result of matching.
If the specified prefix matches this object's prefix and
ge and le condition,
this method returns True as the matching result.
``prefix`` specifies the prefix. prefix must be string.
"""
result = False
length = prefix.length
net = IPNetwork(prefix.formatted_nlri_str)
if net in self._network:
if self._ge is None and self._le is None:
result = True
elif self._ge is None and self._le:
if length <= self._le:
result = True
elif self._ge and self._le is None:
if self._ge <= length:
result = True
elif self._ge and self._le:
if self._ge <= length <= self._le:
result = True
return self.policy, result
def clone(self):
""" This method clones PrefixList object.
Returns PrefixList object that has the same values with the
original one.
"""
return PrefixList(self.prefix,
policy=self._policy,
ge=self._ge,
le=self._le)
class BGPSpeaker(object):
def __init__(self, as_number, router_id,
bgp_server_port=DEFAULT_BGP_SERVER_PORT,
@ -319,3 +451,65 @@ class BGPSpeaker(object):
show = {}
show['params'] = ['rib', family]
return call('operator.show', **show)
def out_filter_set(self, address, prefix_lists,
route_family=OUT_FILTER_RF_IPv4_UC):
""" This method sets out-filter to neighbor.
``address`` specifies the IP address of the peer.
``prefix_lists`` specifies prefix list to filter path advertisement.
This parameter must be list that has PrefixList objects.
``route_family`` specifies the route family for out-filter.
This parameter must be bgpspeaker.OUT_FILTER_RF_IPv4_UC or
bgpspeaker.OUT_FILTER_RF_IPv6_UC.
If you want to define out-filter that send only a particular
prefix to neighbor, prefix_lists can be created as follows;
p = PrefixList('10.5.111.0/24', policy=PrefixList.POLICY_PERMIT)
all = PrefixList('0.0.0.0/0', policy=PrefixList.POLICY_DENY)
pList = [p, all]
self.bgpspeaker.out_filter_set(neighbor_address, pList)
NOTE:
out-filter evaluates prefixes in the order of PrefixList in the pList.
"""
assert route_family in (OUT_FILTER_RF_IPv4_UC,
OUT_FILTER_RF_IPv6_UC),\
"route family must be IPv4 or IPv6"
if prefix_lists is None:
prefix_lists = []
func_name = 'neighbor.update'
prefix_value = {'prefix_lists': prefix_lists,
'route_family': route_family}
filter_param = {neighbors.OUT_FILTER: prefix_value}
param = {}
param[neighbors.IP_ADDRESS] = address
param[neighbors.CHANGES] = filter_param
call(func_name, **param)
def out_filter_get(self, address):
""" This method gets out-filter setting from the specified neighbor.
``address`` specifies the IP address of the peer.
Returns list object that has PrefixList objects.
"""
func_name = 'neighbor.get'
param = {}
param[neighbors.IP_ADDRESS] = address
settings = call(func_name, **param)
return settings[OUT_FILTER]

View File

@ -632,6 +632,18 @@ class Destination(object):
def _get_num_withdraws(self):
return len(self._withdraw_list)
def sent_routes_by_peer(self, peer):
"""get sent routes corresponding to specified peer.
Returns SentRoute list.
"""
result = []
for route in self._sent_routes.values():
if route.sent_peer == peer:
result.append(route)
return result
class Path(object):
"""Represents a way of reaching an IP destination.

View File

@ -29,6 +29,7 @@ from ryu.services.protocols.bgp.base import SUPPORTED_GLOBAL_RF
from ryu.services.protocols.bgp import constants as const
from ryu.services.protocols.bgp.model import OutgoingRoute
from ryu.services.protocols.bgp.model import SentRoute
from ryu.services.protocols.bgp.bgpspeaker import PrefixList
from ryu.services.protocols.bgp.net_ctrl import NET_CONTROLLER
from ryu.services.protocols.bgp.rtconf.neighbors import NeighborConfListener
from ryu.services.protocols.bgp.signals.emit import BgpSignalBus
@ -440,6 +441,48 @@ class Peer(Source, Sink, NeighborConfListener, Activity):
for af in negotiated_afs:
self._fire_route_refresh(af)
def on_update_out_filter(self, conf_evt):
LOG.debug('on_update_out_filter fired')
event_value = conf_evt.value
prefix_lists = event_value['prefix_lists']
rf = event_value['route_family']
table = self._core_service.\
table_manager.get_global_table_by_route_family(rf)
for destination in table.itervalues():
LOG.debug('dest : %s' % destination)
sent_routes = destination.sent_routes_by_peer(self)
if len(sent_routes) == 0:
continue
for sent_route in sent_routes:
nlri = sent_route.path.nlri
nlri_str = nlri.formatted_nlri_str
send_withdraw = False
for pl in prefix_lists:
policy, result = pl.evaluate(nlri)
if policy == PrefixList.POLICY_PERMIT and result:
send_withdraw = False
break
elif policy == PrefixList.POLICY_DENY and result:
send_withdraw = True
break
outgoing_route = None
if send_withdraw:
# send withdraw routes that have already been sent
withdraw_clone = sent_route.path.clone(for_withdrawal=True)
outgoing_route = OutgoingRoute(withdraw_clone)
LOG.debug('send withdraw %s because of out filter'
% nlri_str)
else:
outgoing_route = OutgoingRoute(sent_route.path,
for_route_refresh=True)
LOG.debug('resend path : %s' % nlri_str)
self.enque_outgoing_msg(outgoing_route)
def __str__(self):
return 'Peer(ip: %s, asn: %s)' % (self._neigh_conf.ip_address,
self._neigh_conf.remote_as)
@ -483,12 +526,35 @@ class Peer(Source, Sink, NeighborConfListener, Activity):
Also, checks if any policies prevent sending this message.
Populates Adj-RIB-out with corresponding `SentRoute`.
"""
# evaluate prefix list
rf = outgoing_route.path.route_family
allow_to_send = True
if rf in (RF_IPv4_UC, RF_IPv6_UC):
prefix_lists = self._neigh_conf.out_filter
if not outgoing_route.path.is_withdraw:
for prefix_list in prefix_lists:
nlri = outgoing_route.path.nlri
policy, is_matched = prefix_list.evaluate(nlri)
if policy == PrefixList.POLICY_PERMIT and is_matched:
allow_to_send = True
break
elif policy == PrefixList.POLICY_DENY and is_matched:
allow_to_send = False
blocked_cause = prefix_list.prefix + ' - DENY'
break
# TODO(PH): optimized by sending several prefixes per update.
# Construct and send update message.
update_msg = self._construct_update(outgoing_route)
self._protocol.send(update_msg)
# Collect update statistics.
self.state.incr(PeerCounterNames.SENT_UPDATES)
if allow_to_send:
update_msg = self._construct_update(outgoing_route)
self._protocol.send(update_msg)
# Collect update statistics.
self.state.incr(PeerCounterNames.SENT_UPDATES)
else:
LOG.debug('prefix : %s is not sent by filter : %s'
% (nlri, blocked_cause))
# We have to create sent_route for every OutgoingRoute which is
# not a withdraw or was for route-refresh msg.

View File

@ -63,6 +63,9 @@ MULTI_EXIT_DISC = 'multi_exit_disc'
# Extended community attribute route origin.
SITE_OF_ORIGINS = 'site_of_origins'
# OUT FILTER
OUT_FILTER = 'out_filter'
# Constants related to errors.
CONF_NAME = 'conf_name'
CONF_VALUE = 'conf_value'

View File

@ -59,6 +59,7 @@ from ryu.services.protocols.bgp.rtconf.base import SITE_OF_ORIGINS
from ryu.services.protocols.bgp.rtconf.base import validate
from ryu.services.protocols.bgp.rtconf.base import validate_med
from ryu.services.protocols.bgp.rtconf.base import validate_soo_list
from ryu.services.protocols.bgp.rtconf.base import OUT_FILTER
from ryu.services.protocols.bgp.utils.validation import is_valid_ipv4
from ryu.services.protocols.bgp.utils.validation import is_valid_old_asn
@ -73,6 +74,7 @@ LOCAL_ADDRESS = 'local_address'
LOCAL_PORT = 'local_port'
PEER_NEXT_HOP = 'next_hop'
PASSWORD = 'password'
OUT_FILTER = 'out_filter'
# Default value constants.
DEFAULT_CAP_GR_NULL = True
@ -102,7 +104,7 @@ def validate_enabled(enabled):
@validate(name=CHANGES)
def validate_changes(changes):
for k, v in changes.iteritems():
if k not in (MULTI_EXIT_DISC, ENABLED):
if k not in (MULTI_EXIT_DISC, ENABLED, OUT_FILTER):
raise ConfigValueError(desc="Unknown field to change: %s" % k)
if k == MULTI_EXIT_DISC:
@ -169,8 +171,10 @@ class NeighborConf(ConfWithId, ConfWithStats):
UPDATE_ENABLED_EVT = 'update_enabled_evt'
UPDATE_MED_EVT = 'update_med_evt'
UPDATE_OUT_FILTER_EVT = 'update_out_filter_evt'
VALID_EVT = frozenset([UPDATE_ENABLED_EVT, UPDATE_MED_EVT])
VALID_EVT = frozenset([UPDATE_ENABLED_EVT, UPDATE_MED_EVT,
UPDATE_OUT_FILTER_EVT])
REQUIRED_SETTINGS = frozenset([REMOTE_AS, IP_ADDRESS])
OPTIONAL_SETTINGS = frozenset([CAP_REFRESH,
CAP_ENHANCED_REFRESH,
@ -245,6 +249,9 @@ class NeighborConf(ConfWithId, ConfWithStats):
self._settings[RTC_AS] = \
compute_optional_conf(RTC_AS, default_rt_as, **kwargs)
# out filter configuration
self._settings[OUT_FILTER] = []
# Since ConfWithId' default values use str(self) and repr(self), we
# call super method after we have initialized other settings.
super(NeighborConf, self)._init_opt_settings(**kwargs)
@ -372,6 +379,23 @@ class NeighborConf(ConfWithId, ConfWithStats):
def rtc_as(self):
return self._settings[RTC_AS]
@property
def out_filter(self):
return self._settings[OUT_FILTER]
@out_filter.setter
def out_filter(self, value):
self._settings[OUT_FILTER] = []
prefix_lists = value['prefix_lists']
for prefix_list in prefix_lists:
# copy PrefixList object and put it in the _settings
self._settings[OUT_FILTER].append(prefix_list.clone())
LOG.debug('set out-filter : %s' % prefix_lists)
# check sent_route
self._notify_listeners(NeighborConf.UPDATE_OUT_FILTER_EVT, value)
def exceeds_max_prefix_allowed(self, prefix_count):
allowed_max = self._settings[MAX_PREFIXES]
does_exceed = False
@ -515,6 +539,8 @@ class NeighborConfListener(ConfWithIdListener, ConfWithStatsListener):
self.on_update_enabled)
neigh_conf.add_listener(NeighborConf.UPDATE_MED_EVT,
self.on_update_med)
neigh_conf.add_listener(NeighborConf.UPDATE_OUT_FILTER_EVT,
self.on_update_out_filter)
@abstractmethod
def on_update_enabled(self, evt):
@ -523,6 +549,10 @@ class NeighborConfListener(ConfWithIdListener, ConfWithStatsListener):
def on_update_med(self, evt):
raise NotImplementedError('This method should be overridden.')
@abstractmethod
def on_update_out_filter(self, evt):
raise NotImplementedError('This method should be overridden.')
class NeighborsConfListener(BaseConfListener):
"""Base listener for change events to neighbor configuration container."""