[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:
whd
2021-10-20 17:14:56 -07:00
committed by GitHub
parent d2c0dac4ba
commit c88a37b658
16 changed files with 667 additions and 3 deletions
@@ -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()
+1 -1
View File
@@ -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')
+26
View File
@@ -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