mirror of
https://github.com/espressif/openthread.git
synced 2026-09-09 02:30:11 +00:00
[border-router] firewall: ingress filtering (#7043)
This commit implements part of the OTBR firewall. This implementation focuses on the ingress filtering part. We may also introduce egress filtering part when necessary. For security purpose, there are some packet forwarding rules to follow, which were originally introduced in the spec. - Inbound packets initiated with On-Link addresses source (OMR and mesh local prefix based addresses) should be blocked. - Inbound unicast packets whose destination address is not OMR address or DUA should be blocked. - Inbound unicast packets whose source address or destination address is link-local should be blocked. Note that we don’t need to explicitly add rules for link-local addresses since this should already be handled by the kernel. These rules can be easily implemented by iptables and ipset. - Before otbr-agent starts, there is a script creating the iptables rules. The rules themselves are constant so we don't need to change them dynamically. - During the runtime of otbr-agent, otbr-agent updates ipsets accordingly whenever there's a change of on-link prefixes.
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (c) 2021, The OpenThread Authors.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# 3. Neither the name of the copyright holder nor the
|
||||
# names of its contributors may be used to endorse or promote products
|
||||
# derived from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
import logging
|
||||
import unittest
|
||||
import ipaddress
|
||||
|
||||
import config
|
||||
import pktverify
|
||||
import pktverify.packet_verifier
|
||||
from pktverify.consts import MA1
|
||||
import thread_cert
|
||||
|
||||
# Test description:
|
||||
# This test verifies the functionality of firewall. OTBR will only
|
||||
# forward specific kinds of packets between the infra interface and the thread
|
||||
# interface.
|
||||
#
|
||||
# Topology:
|
||||
# ----------------(eth)----------------------
|
||||
# | |
|
||||
# BR1 (Leader) HOST
|
||||
# |
|
||||
# ROUTER1
|
||||
|
||||
BR1 = 1
|
||||
ROUTER1 = 2
|
||||
HOST = 3
|
||||
|
||||
|
||||
class Firewall(thread_cert.TestCase):
|
||||
USE_MESSAGE_FACTORY = False
|
||||
|
||||
TOPOLOGY = {
|
||||
BR1: {
|
||||
'name': 'BR_1',
|
||||
'allowlist': [ROUTER1],
|
||||
'is_otbr': True,
|
||||
'version': '1.2',
|
||||
},
|
||||
ROUTER1: {
|
||||
'name': 'Router_1',
|
||||
'allowlist': [BR1],
|
||||
'version': '1.2',
|
||||
},
|
||||
HOST: {
|
||||
'name': 'Host',
|
||||
'is_host': True,
|
||||
}
|
||||
}
|
||||
|
||||
def test(self):
|
||||
br1 = self.nodes[BR1]
|
||||
self.br1 = br1
|
||||
router1 = self.nodes[ROUTER1]
|
||||
host = self.nodes[HOST]
|
||||
|
||||
br1.start()
|
||||
self.simulator.go(5)
|
||||
self.assertEqual('leader', br1.get_state())
|
||||
|
||||
router1.start()
|
||||
host.start(start_radvd=True)
|
||||
self.simulator.go(5)
|
||||
self.assertEqual('router', router1.get_state())
|
||||
|
||||
br1.set_domain_prefix(config.DOMAIN_PREFIX, 'prosD')
|
||||
br1.register_netdata()
|
||||
|
||||
router1.add_ipmaddr(MA1)
|
||||
router1.register_netdata()
|
||||
|
||||
self.simulator.go(5)
|
||||
|
||||
def host_ping_ether(dest, interface, ttl=10, add_interface=False, add_route=False, gateway=None):
|
||||
if add_interface:
|
||||
host.bash(f'ip -6 addr add {interface}/64 dev {host.ETH_DEV} scope global')
|
||||
route = str(ipaddress.IPv6Network(f'{interface}/64', strict=False))
|
||||
host.bash(f'ip -6 route del {route} dev {host.ETH_DEV}')
|
||||
if add_route:
|
||||
host.bash(f'ip -6 route add {dest} dev {host.ETH_DEV} via {gateway}')
|
||||
self.simulator.go(2)
|
||||
result = host.ping_ether(dest, ttl=ttl, interface=interface)
|
||||
if add_route:
|
||||
host.bash(f'ip -6 route del {dest}')
|
||||
if add_interface:
|
||||
host.bash(f'ip -6 addr del {interface}/64 dev {host.ETH_DEV} scope global')
|
||||
self.simulator.go(1)
|
||||
return result
|
||||
|
||||
# 1. Host pings router1's OMR from host's infra address.
|
||||
self.assertTrue(host_ping_ether(router1.get_ip6_address(config.ADDRESS_TYPE.OMR)[0], interface=host.ETH_DEV))
|
||||
|
||||
# 2. Host pings router1's DUA from host's infra address.
|
||||
self.assertTrue(host_ping_ether(router1.get_ip6_address(config.ADDRESS_TYPE.DUA), interface=host.ETH_DEV))
|
||||
|
||||
# 3. Host pings router1's OMR from router1's RLOC.
|
||||
self.assertFalse(
|
||||
host_ping_ether(router1.get_ip6_address(config.ADDRESS_TYPE.OMR)[0],
|
||||
interface=router1.get_rloc(),
|
||||
add_interface=True))
|
||||
|
||||
# 4. Host pings router1's OMR from BR1's OMR.
|
||||
self.assertFalse(
|
||||
host_ping_ether(router1.get_ip6_address(config.ADDRESS_TYPE.OMR)[0],
|
||||
interface=br1.get_ip6_address(config.ADDRESS_TYPE.OMR)[0],
|
||||
add_interface=True))
|
||||
|
||||
# 5. Host pings router1's OMR from router1's MLE-ID.
|
||||
self.assertFalse(
|
||||
host_ping_ether(router1.get_ip6_address(config.ADDRESS_TYPE.OMR)[0],
|
||||
interface=router1.get_mleid(),
|
||||
add_interface=True))
|
||||
|
||||
host.bash(f'ip -6 route add {config.MESH_LOCAL_PREFIX} dev {host.ETH_DEV}')
|
||||
self.simulator.go(5)
|
||||
|
||||
# 6. Host pings router1's RLOC from host's ULA address.
|
||||
self.assertFalse(
|
||||
host_ping_ether(router1.get_rloc(),
|
||||
interface=host.get_ip6_address(config.ADDRESS_TYPE.ONLINK_ULA)[0],
|
||||
add_route=True,
|
||||
gateway=br1.get_ip6_address(config.ADDRESS_TYPE.BACKBONE_GUA)))
|
||||
|
||||
# 7. Host pings router1's MLE-ID from host's ULA address.
|
||||
self.assertFalse(
|
||||
host_ping_ether(router1.get_mleid(),
|
||||
interface=host.get_ip6_address(config.ADDRESS_TYPE.ONLINK_ULA)[0],
|
||||
add_route=True,
|
||||
gateway=br1.get_ip6_address(config.ADDRESS_TYPE.BACKBONE_GUA)))
|
||||
|
||||
# 8. Host pings router1's link-local address from host's infra address.
|
||||
self.assertFalse(
|
||||
host_ping_ether(router1.get_linklocal(),
|
||||
interface=host.ETH_DEV,
|
||||
add_route=True,
|
||||
gateway=br1.get_ip6_address(config.ADDRESS_TYPE.BACKBONE_GUA)))
|
||||
|
||||
# 9. Host pings MA1 from host's ULA address.
|
||||
self.assertTrue(host_ping_ether(MA1, ttl=10,
|
||||
interface=host.get_ip6_address(config.ADDRESS_TYPE.ONLINK_ULA)[0]))
|
||||
|
||||
# 10. Host pings MA1 from router1's RLOC.
|
||||
self.assertFalse(host_ping_ether(MA1, ttl=10, interface=router1.get_rloc(), add_interface=True))
|
||||
|
||||
# 11. Host pings MA1 from router1's OMR.
|
||||
self.assertFalse(
|
||||
host_ping_ether(MA1,
|
||||
ttl=10,
|
||||
interface=router1.get_ip6_address(config.ADDRESS_TYPE.OMR)[0],
|
||||
add_interface=True))
|
||||
|
||||
# 12. Host pings MA1 from router1's MLE-ID.
|
||||
self.assertFalse(host_ping_ether(MA1, ttl=10, interface=router1.get_mleid(), add_interface=True))
|
||||
|
||||
self.collect_ipaddrs()
|
||||
self.collect_rlocs()
|
||||
self.collect_rloc16s()
|
||||
self.collect_extra_vars()
|
||||
self.collect_omrs()
|
||||
self.collect_duas()
|
||||
|
||||
def verify(self, pv: pktverify.packet_verifier.PacketVerifier):
|
||||
pkts = pv.pkts
|
||||
vars = pv.vars
|
||||
pv.summary.show()
|
||||
logging.info(f'vars = {vars}')
|
||||
|
||||
pv.verify_attached('Router_1', 'BR_1')
|
||||
|
||||
# 1. Host pings router1's OMR from host's infra address.
|
||||
_pkt = pkts.filter_eth_src(vars['Host_ETH']).filter_ipv6_dst(
|
||||
vars['Router_1_OMR'][0]).filter_ping_request().must_next()
|
||||
pkts.filter_wpan_src64(vars['BR_1']).filter_wpan_dst16(
|
||||
vars['Router_1_RLOC16']).filter_ping_request(identifier=_pkt.icmpv6.echo.identifier).must_next()
|
||||
|
||||
# 2. Host pings router1's DUA from host's infra address.
|
||||
_pkt = pkts.filter_eth_src(vars['Host_ETH']).filter_ipv6_dst(
|
||||
vars['Router_1_DUA']).filter_ping_request().must_next()
|
||||
pkts.filter_wpan_src64(vars['BR_1']).filter_wpan_dst16(
|
||||
vars['Router_1_RLOC16']).filter_ping_request(identifier=_pkt.icmpv6.echo.identifier).must_next()
|
||||
|
||||
# 3. Host pings router1's OMR from router1's RLOC.
|
||||
_pkt = pkts.filter_eth_src(vars['Host_ETH']).filter_ipv6_src_dst(
|
||||
vars['Router_1_RLOC'], vars['Router_1_OMR'][0]).filter_ping_request().must_next()
|
||||
pkts.filter_wpan_src64(vars['BR_1']).filter_wpan_dst16(
|
||||
vars['Router_1_RLOC16']).filter_ping_request(identifier=_pkt.icmpv6.echo.identifier).must_not_next()
|
||||
|
||||
# 4. Host pings router1's OMR from BR1's OMR.
|
||||
_pkt = pkts.filter_eth_src(vars['Host_ETH']).filter_ipv6_src_dst(
|
||||
vars['BR_1_OMR'][0], vars['Router_1_OMR'][0]).filter_ping_request().must_next()
|
||||
pkts.filter_wpan_src64(vars['BR_1']).filter_wpan_dst64(
|
||||
vars['Router_1']).filter_ping_request(identifier=_pkt.icmpv6.echo.identifier).must_not_next()
|
||||
|
||||
# 5. Host pings router1's OMR from router1's MLE-ID.
|
||||
_pkt = pkts.filter_eth_src(vars['Host_ETH']).filter_ipv6_src_dst(
|
||||
vars['Router_1_MLEID'], vars['Router_1_OMR'][0]).filter_ping_request().must_next()
|
||||
pkts.filter_wpan_src64(vars['BR_1']).filter_wpan_dst16(
|
||||
vars['Router_1_RLOC16']).filter_ping_request(identifier=_pkt.icmpv6.echo.identifier).must_not_next()
|
||||
|
||||
# 6. Host pings router1's RLOC from host's ULA address.
|
||||
_pkt = pkts.filter_eth_src(vars['Host_ETH']).filter_ipv6_dst(
|
||||
vars['Router_1_RLOC']).filter_ping_request().must_next()
|
||||
pkts.filter_wpan_src64(vars['BR_1']).filter_wpan_dst16(
|
||||
vars['Router_1_RLOC16']).filter_ping_request(identifier=_pkt.icmpv6.echo.identifier).must_not_next()
|
||||
|
||||
# 7. Host pings router1's MLE-ID from host's ULA address.
|
||||
_pkt = pkts.filter_eth_src(vars['Host_ETH']).filter_ipv6_dst(
|
||||
vars['Router_1_MLEID']).filter_ping_request().must_next()
|
||||
pkts.filter_wpan_src64(vars['BR_1']).filter_wpan_dst16(
|
||||
vars['Router_1_RLOC16']).filter_ping_request(identifier=_pkt.icmpv6.echo.identifier).must_not_next()
|
||||
|
||||
# 8. Host pings router1's link-local address from host's infra address.
|
||||
_pkt = pkts.filter_eth_src(vars['Host_ETH']).filter_ipv6_dst(
|
||||
vars['Router_1_LLA']).filter_ping_request().must_next()
|
||||
pkts.filter_wpan_src64(vars['BR_1']).filter_wpan_dst16(
|
||||
vars['Router_1_RLOC16']).filter_ping_request(identifier=_pkt.icmpv6.echo.identifier).must_not_next()
|
||||
|
||||
# 9. Host pings MA1 from host's ULA address.
|
||||
_pkt = pkts.filter_eth_src(vars['Host_ETH']).filter_ipv6_dst(MA1).filter_ping_request().must_next()
|
||||
pkts.filter_wpan_src64(
|
||||
vars['BR_1']).filter_AMPLFMA().filter_ping_request(identifier=_pkt.icmpv6.echo.identifier).must_next()
|
||||
|
||||
# 10. Host pings MA1 from router1's RLOC.
|
||||
_pkt = pkts.filter_eth_src(vars['Host_ETH']).filter_ipv6_src_dst(vars['Router_1_RLOC'],
|
||||
MA1).filter_ping_request().must_next()
|
||||
pkts.filter_wpan_src64(
|
||||
vars['BR_1']).filter_AMPLFMA().filter_ping_request(identifier=_pkt.icmpv6.echo.identifier).must_not_next()
|
||||
|
||||
# 11. Host pings MA1 from router1's OMR.
|
||||
_pkt = pkts.filter_eth_src(vars['Host_ETH']).filter_ipv6_src_dst(vars['Router_1_OMR'][0],
|
||||
MA1).filter_ping_request().must_next()
|
||||
pkts.filter_wpan_src64(
|
||||
vars['BR_1']).filter_AMPLFMA().filter_ping_request(identifier=_pkt.icmpv6.echo.identifier).must_not_next()
|
||||
|
||||
# 12. Host pings MA1 from router1's MLE-ID.
|
||||
_pkt = pkts.filter_eth_src(vars['Host_ETH']).filter_ipv6_src_dst(vars['Router_1_MLEID'],
|
||||
MA1).filter_ping_request().must_next()
|
||||
pkts.filter_wpan_src64(
|
||||
vars['BR_1']).filter_AMPLFMA().filter_ping_request(identifier=_pkt.icmpv6.echo.identifier).must_not_next()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1657,7 +1657,7 @@ class NodeImpl:
|
||||
omr_addrs = []
|
||||
for addr in self.get_addrs():
|
||||
for prefix in prefixes:
|
||||
if (addr.startswith(prefix)):
|
||||
if (addr.startswith(prefix)) and (addr != self.__getDua()):
|
||||
omr_addrs.append(addr)
|
||||
break
|
||||
|
||||
|
||||
@@ -157,6 +157,14 @@ class PacketVerifier(object):
|
||||
key = self.test_info.get_node_name(i) + '_RLOC'
|
||||
self._vars[key] = rloc
|
||||
|
||||
for i, omr in self.test_info.omrs.items():
|
||||
key = self.test_info.get_node_name(i) + '_OMR'
|
||||
self._vars[key] = omr
|
||||
|
||||
for i, dua in self.test_info.duas.items():
|
||||
key = self.test_info.get_node_name(i) + '_DUA'
|
||||
self._vars[key] = dua
|
||||
|
||||
if self.test_info.leader_aloc:
|
||||
self._vars['LEADER_ALOC'] = self.test_info.leader_aloc
|
||||
|
||||
|
||||
@@ -53,6 +53,8 @@ class TestInfo(object):
|
||||
self.mleids = {int(k): Ipv6Addr(v) for k, v in test_info.get('mleids', {}).items()}
|
||||
self.rlocs = {int(k): Ipv6Addr(v) for k, v in test_info.get('rlocs', {}).items()}
|
||||
self.rloc16s = self._convert_hex_values(self._convert_keys_to_ints(test_info.get('rloc16s', {})))
|
||||
self.omrs = {int(k): [Ipv6Addr(x) for x in l] for k, l in test_info.get('omrs', {}).items()}
|
||||
self.duas = {int(k): Ipv6Addr(v) for k, v in test_info.get('duas', {}).items()}
|
||||
self.extra_vars = test_info.get('extra_vars', {})
|
||||
self.leader_aloc = Ipv6Addr(test_info.get('leader_aloc')) if 'leader_aloc' in test_info else ''
|
||||
|
||||
|
||||
@@ -94,8 +94,6 @@ def cleanup_backbone_env():
|
||||
|
||||
|
||||
def setup_backbone_env():
|
||||
bash('sudo modprobe ip6table_filter')
|
||||
|
||||
if THREAD_VERSION != '1.2':
|
||||
raise RuntimeError('Backbone tests only work with THREAD_VERSION=1.2')
|
||||
|
||||
|
||||
@@ -351,6 +351,32 @@ class TestCase(NcpSupportMixin, unittest.TestCase):
|
||||
|
||||
test_info['rlocs'][i] = node.get_rloc()
|
||||
|
||||
def collect_omrs(self):
|
||||
if not self._do_packet_verification:
|
||||
return
|
||||
|
||||
test_info = self._test_info
|
||||
test_info['omrs'] = {}
|
||||
|
||||
for i, node in self.nodes.items():
|
||||
if node.is_host:
|
||||
continue
|
||||
|
||||
test_info['omrs'][i] = node.get_ip6_address(config.ADDRESS_TYPE.OMR)
|
||||
|
||||
def collect_duas(self):
|
||||
if not self._do_packet_verification:
|
||||
return
|
||||
|
||||
test_info = self._test_info
|
||||
test_info['duas'] = {}
|
||||
|
||||
for i, node in self.nodes.items():
|
||||
if node.is_host:
|
||||
continue
|
||||
|
||||
test_info['duas'][i] = node.get_ip6_address(config.ADDRESS_TYPE.DUA)
|
||||
|
||||
def collect_leader_aloc(self, node):
|
||||
if not self._do_packet_verification:
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user