[firewall] implement packet filtering in OT core (#9402)

This commit implements the packet logic in OT core. It aims to have
the same effect as what's already achieved by our iptables-based
firewall. Instead of leveraging iptables, this commit filters the
border routing packets in user space by checking the
source/destination addresses of a packet.

This commit also adds a job to do BR regression test when this feature
is enabled and iptables-based firewall is disabled.
This commit is contained in:
whd
2023-09-15 11:52:26 -07:00
committed by GitHub
parent b5b93423d8
commit a383e366ba
6 changed files with 107 additions and 39 deletions
+11
View File
@@ -156,6 +156,13 @@ jobs:
packet_verification: 2
nat64: 0
description: ""
- otbr_mdns: "avahi"
otbr_trel: 0
cert_scripts: ./tests/scripts/thread-cert/border_router/*.py
packet_verification: 1
nat64: 0
use_core_firewall: 1
description: "core firewall"
name: BR ${{ matrix.description }} (${{ matrix.otbr_mdns }}, TREL=${{matrix.otbr_trel}})
env:
REFERENCE_DEVICE: 1
@@ -173,6 +180,10 @@ jobs:
MAX_JOBS: 3
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- name: Set firewall environment variables
if: ${{ matrix.use_core_firewall }}
run: |
echo "FIREWALL=0" >> $GITHUB_ENV
- name: Build OTBR Docker
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+1
View File
@@ -342,6 +342,7 @@ do_build_otbr_docker()
"REST_API=0"
"WEB_GUI=0"
"MDNS=${OTBR_MDNS:-mDNSResponder}"
"FIREWALL=${FIREWALL:-1}"
)
if [[ ${NAT64} != 1 ]]; then
+24 -2
View File
@@ -847,7 +847,7 @@ Error Ip6::HandleExtensionHeaders(Message &aMessage,
case kProtoFragment:
IgnoreError(PassToHost(aMessage, aOrigin, aMessageInfo, aNextHeader,
/* aApplyFilter */ false, Message::kCopyToUse));
/* aApplyFilter */ false, aReceive, Message::kCopyToUse));
SuccessOrExit(error = HandleFragment(aMessage, aOrigin, aMessageInfo));
break;
@@ -947,6 +947,7 @@ Error Ip6::PassToHost(Message &aMessage,
const MessageInfo &aMessageInfo,
uint8_t aIpProto,
bool aApplyFilter,
bool aReceive,
Message::Ownership aMessageOwnership)
{
// This method passes the message to host by invoking the
@@ -977,6 +978,13 @@ Error Ip6::PassToHost(Message &aMessage,
// Do not pass IPv6 packets that exceed kMinimalMtu.
VerifyOrExit(aMessage.GetLength() <= kMinimalMtu, error = kErrorDrop);
// If the sender used mesh-local address as source, do not pass to
// host unless this message is intended for this device itself.
if (Get<Mle::Mle>().IsMeshLocalAddress(aMessageInfo.GetPeerAddr()))
{
VerifyOrExit(aReceive, error = kErrorDrop);
}
if (mIsReceiveIp6FilterEnabled && aApplyFilter)
{
#if !OPENTHREAD_CONFIG_PLATFORM_NETIF_ENABLE
@@ -1091,6 +1099,20 @@ Error Ip6::SendRaw(Message &aMessage, bool aAllowLoopBackToHost)
SuccessOrExit(error = header.ParseFrom(aMessage));
VerifyOrExit(!header.GetSource().IsMulticast(), error = kErrorInvalidSourceAddress);
#if OPENTHREAD_CONFIG_BACKBONE_ROUTER_ENABLE
// The filtering rules don't apply to packets from DUA.
if (!Get<BackboneRouter::Leader>().IsDomainUnicast(header.GetSource()))
#endif
{
// When the packet is forwarded from host to Thread, if its source is on-mesh or its destination is
// mesh-local, we'll drop the packet unless the packet originates from this device.
if (Get<NetworkData::Leader>().IsOnMesh(header.GetSource()) ||
Get<Mle::Mle>().IsMeshLocalAddress(header.GetDestination()))
{
VerifyOrExit(Get<ThreadNetif>().HasUnicastAddress(header.GetSource()), error = kErrorDrop);
}
}
if (header.GetDestination().IsMulticast())
{
SuccessOrExit(error = InsertMplOption(aMessage, header));
@@ -1217,7 +1239,7 @@ start:
if ((forwardHost || receive) && !aIsReassembled)
{
error = PassToHost(aMessage, aOrigin, messageInfo, nextHeader,
/* aApplyFilter */ !forwardHost,
/* aApplyFilter */ !forwardHost, receive,
(receive || forwardThread) ? Message::kCopyToUse : Message::kTakeCustody);
// Need to free the message if we did not pass its
+1
View File
@@ -391,6 +391,7 @@ private:
const MessageInfo &aMessageInfo,
uint8_t aIpProto,
bool aApplyFilter,
bool aReceive,
Message::Ownership aMessageOwnership);
Error HandleExtensionHeaders(Message &aMessage,
MessageOrigin aOrigin,
@@ -27,6 +27,7 @@
# POSSIBILITY OF SUCH DAMAGE.
#
import logging
import time
import unittest
import ipaddress
@@ -45,12 +46,13 @@ import thread_cert
# ----------------(eth)----------------------
# | |
# BR1 (Leader) HOST
# |
# ROUTER1
# | \
# ROUTER1 ROUTER2
BR1 = 1
ROUTER1 = 2
HOST = 3
ROUTER2 = 3
HOST = 4
class Firewall(thread_cert.TestCase):
@@ -59,14 +61,19 @@ class Firewall(thread_cert.TestCase):
TOPOLOGY = {
BR1: {
'name': 'BR_1',
'allowlist': [ROUTER1],
'allowlist': [ROUTER1, ROUTER2],
'is_otbr': True,
'version': '1.2',
'version': '1.3',
},
ROUTER1: {
'name': 'Router_1',
'allowlist': [BR1],
'version': '1.2',
'version': '1.3',
},
ROUTER2: {
'name': 'Router_2',
'allowlist': [BR1],
'version': '1.3',
},
HOST: {
'name': 'Host',
@@ -78,16 +85,20 @@ class Firewall(thread_cert.TestCase):
br1 = self.nodes[BR1]
self.br1 = br1
router1 = self.nodes[ROUTER1]
router2 = self.nodes[ROUTER2]
host = self.nodes[HOST]
br1.start()
self.simulator.go(config.LEADER_STARTUP_DELAY)
self.assertEqual('leader', br1.get_state())
br1.set_log_level(5)
router1.start()
router2.start()
host.start(start_radvd=True)
self.simulator.go(config.ROUTER_STARTUP_DELAY)
self.assertEqual('router', router1.get_state())
self.assertEqual('router', router2.get_state())
br1.set_domain_prefix(config.DOMAIN_PREFIX, 'prosD')
br1.register_netdata()
@@ -125,13 +136,13 @@ class Firewall(thread_cert.TestCase):
interface=router1.get_rloc(),
add_interface=True))
# 4. Host pings router1's OMR from BR1's OMR.
# 4. Host pings router1's OMR from router2'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],
interface=router2.get_ip6_address(config.ADDRESS_TYPE.OMR)[0],
add_interface=True))
# 5. Host pings router1's OMR from router1's MLE-ID.
# 5. Host pings router1's OMR from router1's ML-EID.
self.assertFalse(
host_ping_ether(router1.get_ip6_address(config.ADDRESS_TYPE.OMR)[0],
interface=router1.get_mleid(),
@@ -147,7 +158,7 @@ class Firewall(thread_cert.TestCase):
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.
# 7. Host pings router1's ML-EID from host's ULA address.
self.assertFalse(
host_ping_ether(router1.get_mleid(),
interface=host.get_ip6_address(config.ADDRESS_TYPE.ONLINK_ULA)[0],
@@ -175,13 +186,21 @@ class Firewall(thread_cert.TestCase):
interface=router1.get_ip6_address(config.ADDRESS_TYPE.OMR)[0],
add_interface=True))
# 12. Host pings MA1 from router1's MLE-ID.
# 12. Host pings MA1 from router1's ML-EID.
self.assertFalse(host_ping_ether(MA1, ttl=10, interface=router1.get_mleid(), add_interface=True))
# 13. Router1 pings Host from router1's MLE-ID.
# 13. Router1 pings Host from router1's ML-EID.
self.assertFalse(
router1.ping(host.get_ip6_address(config.ADDRESS_TYPE.ONLINK_ULA)[0], interface=router1.get_mleid()))
# 14. BR pings router1's ML-EID from BR's ML-EID.
self.assertTrue(br1.ping_ether(router1.get_mleid(), interface=br1.get_mleid()))
# 15. BR pings router1's OMR from BR's infra interface.
self.assertTrue(
br1.ping_ether(router1.get_ip6_address(config.ADDRESS_TYPE.OMR)[0],
interface=br1.get_ip6_address(config.ADDRESS_TYPE.ONLINK_ULA)[0]))
self.collect_ipaddrs()
self.collect_rlocs()
self.collect_rloc16s()
@@ -215,13 +234,13 @@ class Firewall(thread_cert.TestCase):
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.
# 4. Host pings router1's OMR from router2'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()
vars['Router_2_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.
# 5. Host pings router1's OMR from router1's ML-EID.
_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(
@@ -233,7 +252,7 @@ class Firewall(thread_cert.TestCase):
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.
# 7. Host pings router1's ML-EID 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(
@@ -263,16 +282,25 @@ class Firewall(thread_cert.TestCase):
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.
# 12. Host pings MA1 from router1's ML-EID.
_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()
# 13. Router1 pings Host from router1's MLE-ID.
# 13. Router1 pings Host from router1's ML-EID.
pkts.filter_eth_src(vars['BR_1_ETH']).filter_ipv6_src_dst(
vars['Router_1_MLEID'], vars['Host_BGUA']).filter_ping_request().must_not_next()
# 14. BR pings router1's ML-EID from BR's infra interface.
_pkt = pkts.filter_wpan_src64(vars['BR_1']).filter_ipv6_src_dst(
vars['BR_1_MLEID'], vars['Router_1_MLEID']).filter_ping_request().must_next()
pkts.filter_wpan_src64(vars['Router_1']).filter_ping_reply(identifier=_pkt.icmpv6.echo.identifier).must_next()
# 15. BR pings router1's OMR from BR's infra interface.
_pkt = pkts.filter_wpan_src64(vars['BR_1']).filter_ping_request().must_next()
pkts.filter_wpan_src64(vars['Router_1']).filter_ping_reply(identifier=_pkt.icmpv6.echo.identifier).must_next()
if __name__ == '__main__':
unittest.main()
+24 -19
View File
@@ -3630,6 +3630,24 @@ class LinuxHost():
return resp_count
def get_ip6_address(self, address_type: config.ADDRESS_TYPE):
"""Get specific type of IPv6 address configured on thread device.
Args:
address_type: the config.ADDRESS_TYPE type of IPv6 address.
Returns:
IPv6 address string.
"""
if address_type == config.ADDRESS_TYPE.BACKBONE_GUA:
return self._getBackboneGua()
elif address_type == config.ADDRESS_TYPE.ONLINK_ULA:
return self._getInfraUla()
elif address_type == config.ADDRESS_TYPE.ONLINK_GUA:
return self._getInfraGua()
else:
raise ValueError(f'unsupported address type: {address_type}')
def _getBackboneGua(self) -> Optional[str]:
for addr in self.get_ether_addrs():
if re.match(config.BACKBONE_PREFIX_REGEX_PATTERN, addr, re.I):
@@ -3885,6 +3903,12 @@ class OtbrNode(LinuxHost, NodeImpl, OtbrDocker):
cmd = f'python3 /app/third_party/openthread/repo/tests/scripts/thread-cert/mcast6.py {self.TUN_DEV} {ip} &'
self.bash(cmd)
def get_ip6_address(self, address_type: config.ADDRESS_TYPE):
try:
return super(OtbrNode, self).get_ip6_address(address_type)
except Exception as e:
return super(LinuxHost, self).get_ip6_address(address_type)
class HostNode(LinuxHost, OtbrDocker):
is_host = True
@@ -3922,25 +3946,6 @@ class HostNode(LinuxHost, OtbrDocker):
return addrs
def get_ip6_address(self, address_type: config.ADDRESS_TYPE):
"""Get specific type of IPv6 address configured on thread device.
Args:
address_type: the config.ADDRESS_TYPE type of IPv6 address.
Returns:
IPv6 address string.
"""
if address_type == config.ADDRESS_TYPE.BACKBONE_GUA:
return self._getBackboneGua()
elif address_type == config.ADDRESS_TYPE.ONLINK_ULA:
return self._getInfraUla()
elif address_type == config.ADDRESS_TYPE.ONLINK_GUA:
return self._getInfraGua()
else:
raise ValueError(f'unsupported address type: {address_type}')
if __name__ == '__main__':
unittest.main()