Add rate limiter to icmp handler(L3 proactive app)

To support traceroute or its backend network functionality, some icmp
messages are generated or re-processed at local controller. This
provides the possiblity of DDoS attack.

To prevent that, several global rate limters are added. So that the
controller won't be overwhelmed. Global rate limters should be enough,
as icmp works as control message.

Change-Id: I2d588a7db246bce4bef8e4f32a4fdd0dfb14384d
Implements: blueprint traceroute-support
This commit is contained in:
Hong Hui Xiao 2017-02-14 15:55:22 +08:00
parent 9f456ed1d8
commit d1762b7aa5
3 changed files with 127 additions and 18 deletions

View File

@ -23,6 +23,8 @@ from ryu.ofproto import ether
from dragonflow._i18n import _LI, _LW
from dragonflow.common import exceptions
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_error_generator
@ -43,6 +45,13 @@ class L3ProactiveApp(df_base_app.DFlowApp):
self.router_port_rarp_cache = {}
self.api.register_table_handler(const.L3_LOOKUP_TABLE,
self.packet_in_handler)
self.conf = cfg.CONF.df_l3_app
self.ttl_invalid_handler_rate_limit = df_utils.RateLimiter(
max_rate=self.conf.router_ttl_invalid_max_rate,
time_unit=1)
self.port_icmp_unreach_respond_rate_limit = df_utils.RateLimiter(
max_rate=self.conf.router_port_unreach_max_rate,
time_unit=1)
self.register_local_cookie_bits(COOKIE_NAME, 24)
def switch_features_handler(self, ev):
@ -51,12 +60,20 @@ class L3ProactiveApp(df_base_app.DFlowApp):
def packet_in_handler(self, event):
msg = event.msg
ofproto = self.ofproto
pkt = packet.Packet(msg.data)
e_pkt = pkt.get_protocol(ethernet.ethernet)
if msg.reason == ofproto.OFPR_INVALID_TTL:
LOG.debug("Get an invalid TTL packet at table %s",
const.L3_LOOKUP_TABLE)
if self.ttl_invalid_handler_rate_limit():
LOG.warning(
_LW("Get more than %(rate)s TTL invalid "
"packets per second at table %(table)s"),
{'rate': self.conf.router_ttl_invalid_max_rate,
'table': const.L3_LOOKUP_TABLE})
return
pkt = packet.Packet(msg.data)
e_pkt = pkt.get_protocol(ethernet.ethernet)
router_port_ip = self.router_port_rarp_cache.get(e_pkt.dst)
if router_port_ip:
icmp_ttl_pkt = icmp_error_generator.generate(
@ -70,6 +87,15 @@ class L3ProactiveApp(df_base_app.DFlowApp):
return
# The packet's IP destination is router interface.
if self.port_icmp_unreach_respond_rate_limit():
LOG.warning(
_LW("Get more than %(rate)s packets to router port "
"per second at table %(table)s"),
{'rate': self.conf.router_port_unreach_max_rate,
'table': const.L3_LOOKUP_TABLE})
return
pkt = packet.Packet(msg.data)
tcp_pkt = pkt.get_protocol(tcp.tcp)
udp_pkt = pkt.get_protocol(udp.udp)
if tcp_pkt or udp_pkt:

View File

@ -908,33 +908,76 @@ class TestL3App(test_base.DFTestBase):
key = (self.subnet1.subnet_id, self.port1.port_id)
return {key: policy}
def test_icmp_ttl_packet(self):
def _create_rate_limit_port_policies(self, rate, icmp_filter):
ignore_action = app_testing_objects.IgnoreAction()
port_policy = self._create_icmp_test_port_policies(
raise_action = app_testing_objects.RaiseAction("Unexpected packet")
# Disable port policy rule, so that any further packets will hit the
# default action, which is raise_action in this case.
count_action = app_testing_objects.CountAction(
rate, app_testing_objects.DisableRuleAction())
key = (self.subnet1.subnet_id, self.port1.port_id)
rules = [
app_testing_objects.PortPolicyRule(
# Detect ICMP, end simulation
icmp_filter(self._get_ip),
actions=[count_action]
),
app_testing_objects.PortPolicyRule(
# Ignore gratuitous ARP packets
app_testing_objects.RyuARPGratuitousFilter(),
actions=[ignore_action]
),
app_testing_objects.PortPolicyRule(
# Ignore IPv6 packets
app_testing_objects.RyuIPv6Filter(),
actions=[ignore_action]
),
]
policy = app_testing_objects.PortPolicy(
rules=rules,
default_action=raise_action
)
return {key: policy}
def test_icmp_ttl_packet_with_rate_limit(self):
ignore_action = app_testing_objects.IgnoreAction()
port_policy = self._create_rate_limit_port_policies(
cfg.CONF.df_l3_app.router_ttl_invalid_max_rate,
app_testing_objects.RyuICMPTimeExceedFilter)
initial_packet = self._create_packet(
self.port2.port.get_logical_port().get_ip(),
ryu.lib.packet.ipv4.inet.IPPROTO_ICMP,
ttl=1)
send_action = app_testing_objects.SendAction(
self.subnet1.subnet_id,
self.port1.port_id,
str(initial_packet))
policy = self.store(
app_testing_objects.Policy(
initial_actions=[
app_testing_objects.SendAction(
self.subnet1.subnet_id,
self.port1.port_id,
str(initial_packet)
),
send_action,
send_action,
send_action,
send_action
],
port_policies=port_policy,
unknown_port_action=ignore_action
)
)
policy.start(self.topology)
policy.wait(const.DEFAULT_RESOURCE_READY_TIMEOUT)
# Since the rate limit, we expect timeout to wait for 4th packet hit
# the policy.
self.assertRaises(
app_testing_objects.TimeoutException,
policy.wait,
const.DEFAULT_RESOURCE_READY_TIMEOUT)
if len(policy.exceptions) > 0:
raise policy.exceptions[0]
def _test_udp_router_interface(self):
def test_udp_concrete_router_interface(self):
# By default, fullstack will start l3 agent. So there will be concrete
# router interface.
self.port1.port.update({"security_groups": []})
ignore_action = app_testing_objects.IgnoreAction()
port_policy = self._create_icmp_test_port_policies(
@ -959,12 +1002,7 @@ class TestL3App(test_base.DFTestBase):
if len(policy.exceptions) > 0:
raise policy.exceptions[0]
def test_udp_concrete_router_interface(self):
# By default, fullstack will start l3 agent. So there will be concrete
# router interface.
self._test_udp_router_interface()
def test_udp_virtual_router_interface(self):
def test_udp_virtual_router_interface_with_rate_limit(self):
# Delete the concrete router interface.
router_port_id = self.router.router_interfaces[
self.subnet1.subnet_id]['port_id']
@ -983,7 +1021,39 @@ class TestL3App(test_base.DFTestBase):
self.nb_api.update_lrouter(**lrouter.inner_obj)
time.sleep(const.DEFAULT_CMD_TIMEOUT)
self._test_udp_router_interface()
self.port1.port.update({"security_groups": []})
ignore_action = app_testing_objects.IgnoreAction()
port_policy = self._create_rate_limit_port_policies(
cfg.CONF.df_l3_app.router_port_unreach_max_rate,
app_testing_objects.RyuICMPUnreachFilter)
initial_packet = self._create_packet(
"192.168.12.1", ryu.lib.packet.ipv4.inet.IPPROTO_UDP)
send_action = app_testing_objects.SendAction(
self.subnet1.subnet_id,
self.port1.port_id,
str(initial_packet))
policy = self.store(
app_testing_objects.Policy(
initial_actions=[
send_action,
send_action,
send_action,
send_action
],
port_policies=port_policy,
unknown_port_action=ignore_action
)
)
policy.start(self.topology)
# Since the rate limit, we expect timeout to wait for 4th packet hit
# the policy.
self.assertRaises(
app_testing_objects.TimeoutException,
policy.wait,
const.DEFAULT_RESOURCE_READY_TIMEOUT)
if len(policy.exceptions) > 0:
raise policy.exceptions[0]
class TestSGApp(test_base.DFTestBase):

View File

@ -0,0 +1,13 @@
---
prelude: >
Traceroute support.
features:
- As a Openflow based SDN, traceroute and its back-end network
functionalities are not naturally supported. Dragonflow supports traceroute
base on RFC792, RFC4884, RFC1812 and RFC5508. Traceroute in Dragonflow
cluster works for virtual router and NAT device.
security:
- To prevent DDoS, the rate of responding ICMP error message is controlled by
several configuration options. They are 'dnat_ttl_invalid_max_rate',
'dnat_icmp_error_max_rate', 'router_ttl_invalid_max_rate' and
'router_port_unreach_max_rate'.