diff --git a/Android.mk b/Android.mk index 42560711d..fbbec045b 100644 --- a/Android.mk +++ b/Android.mk @@ -369,6 +369,7 @@ LOCAL_SRC_FILES := \ src/posix/platform/backbone.cpp \ src/posix/platform/daemon.cpp \ src/posix/platform/entropy.cpp \ + src/posix/platform/firewall.cpp \ src/posix/platform/hdlc_interface.cpp \ src/posix/platform/infra_if.cpp \ src/posix/platform/logging.cpp \ @@ -384,6 +385,7 @@ LOCAL_SRC_FILES := \ src/posix/platform/system.cpp \ src/posix/platform/trel_udp6.cpp \ src/posix/platform/udp.cpp \ + src/posix/platform/utils.cpp \ third_party/mbedtls/repo/library/aes.c \ third_party/mbedtls/repo/library/aesni.c \ third_party/mbedtls/repo/library/arc4.c \ diff --git a/script/test b/script/test index d92edc39e..9007e596a 100755 --- a/script/test +++ b/script/test @@ -246,6 +246,8 @@ do_cert_suite() export PYTHONPATH=tests/scripts/thread-cert export VIRTUAL_TIME + sudo modprobe ip6table_filter + python3 tests/scripts/thread-cert/run_cert_suite.py --multiply "${MULTIPLY:-1}" "$@" exit 0 } diff --git a/src/posix/platform/CMakeLists.txt b/src/posix/platform/CMakeLists.txt index 1f9ba7aec..a3b05d556 100644 --- a/src/posix/platform/CMakeLists.txt +++ b/src/posix/platform/CMakeLists.txt @@ -78,6 +78,7 @@ add_library(openthread-posix backbone.cpp daemon.cpp entropy.cpp + firewall.cpp hdlc_interface.cpp infra_if.cpp logging.cpp @@ -93,6 +94,7 @@ add_library(openthread-posix system.cpp trel_udp6.cpp udp.cpp + utils.cpp virtual_time.cpp ) diff --git a/src/posix/platform/Makefile.am b/src/posix/platform/Makefile.am index 7a2c72339..90bb59d32 100644 --- a/src/posix/platform/Makefile.am +++ b/src/posix/platform/Makefile.am @@ -48,6 +48,7 @@ libopenthread_posix_a_SOURCES = \ backbone.cpp \ daemon.cpp \ entropy.cpp \ + firewall.cpp \ hdlc_interface.cpp \ infra_if.cpp \ logging.cpp \ @@ -63,6 +64,7 @@ libopenthread_posix_a_SOURCES = \ system.cpp \ trel_udp6.cpp \ udp.cpp \ + utils.cpp \ virtual_time.cpp \ $(NULL) diff --git a/src/posix/platform/firewall.cpp b/src/posix/platform/firewall.cpp new file mode 100644 index 000000000..671dff7ca --- /dev/null +++ b/src/posix/platform/firewall.cpp @@ -0,0 +1,132 @@ +/* + * 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. + */ + +/** + * @file + * @brief + * This file includes the implementation of OTBR firewall. + */ + +#include "firewall.hpp" + +#include + +#include "common/code_utils.hpp" +#include "openthread/netdata.h" +#include "posix/platform/utils.hpp" + +namespace ot { +namespace Posix { + +#if defined(__linux__) && OPENTHREAD_POSIX_CONFIG_FIREWALL_ENABLE + +#if !OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE || !OPENTHREAD_CONFIG_PLATFORM_NETIF_ENABLE +#error Configurations 'OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE' and 'OPENTHREAD_CONFIG_PLATFORM_NETIF_ENABLE' are required. +#endif + +static const char kIpsetCommand[] = OPENTHREAD_POSIX_CONFIG_IPSET_BINARY; +static const char kIngressDenySrcIpSet[] = "otbr-ingress-deny-src"; +static const char kIngressDenySrcSwapIpSet[] = "otbr-ingress-deny-src-swap"; +static const char kIngressAllowDstIpSet[] = "otbr-ingress-allow-dst"; +static const char kIngressAllowDstSwapIpSet[] = "otbr-ingress-allow-dst-swap"; + +class IpSetManager +{ +public: + otError FlushIpSet(const char *aName); + otError AddToIpSet(const char *aSetName, const char *aAddress); + otError SwapIpSets(const char *aSetName1, const char *aSetName2); +}; + +inline otError IpSetManager::FlushIpSet(const char *aName) +{ + return ExecuteCommand("%s flush %s", kIpsetCommand, aName); +} + +inline otError IpSetManager::AddToIpSet(const char *aSetName, const char *aAddress) +{ + return ExecuteCommand("%s add %s %s -exist", kIpsetCommand, aSetName, aAddress); +} + +inline otError IpSetManager::SwapIpSets(const char *aSetName1, const char *aSetName2) +{ + return ExecuteCommand("%s swap %s %s", kIpsetCommand, aSetName1, aSetName2); +} + +void UpdateIpSets(otInstance *aInstance) +{ + otError error = OT_ERROR_NONE; + otNetworkDataIterator iterator = OT_NETWORK_DATA_ITERATOR_INIT; + otBorderRouterConfig config; + otIp6Prefix prefix; + char prefixBuf[OT_IP6_PREFIX_STRING_SIZE]; + IpSetManager ipSetManager; + + // 1. Flush the '*-swap' ipsets + SuccessOrExit(error = ipSetManager.FlushIpSet(kIngressAllowDstSwapIpSet)); + SuccessOrExit(error = ipSetManager.FlushIpSet(kIngressDenySrcSwapIpSet)); + + // 2. Update otbr-deny-src-swap + while (otNetDataGetNextOnMeshPrefix(aInstance, &iterator, &config) == OT_ERROR_NONE) + { + if (config.mDp) + { + continue; + } + otIp6PrefixToString(&config.mPrefix, prefixBuf, sizeof(prefixBuf)); + SuccessOrExit(error = ipSetManager.AddToIpSet(kIngressDenySrcSwapIpSet, prefixBuf)); + } + memcpy(prefix.mPrefix.mFields.m8, otThreadGetMeshLocalPrefix(aInstance)->m8, + sizeof(otThreadGetMeshLocalPrefix(aInstance)->m8)); + prefix.mLength = OT_IP6_PREFIX_BITSIZE; + otIp6PrefixToString(&prefix, prefixBuf, sizeof(prefixBuf)); + SuccessOrExit(error = ipSetManager.AddToIpSet(kIngressDenySrcSwapIpSet, prefixBuf)); + + // 3. Update otbr-allow-dst-swap + iterator = OT_NETWORK_DATA_ITERATOR_INIT; + while (otNetDataGetNextOnMeshPrefix(aInstance, &iterator, &config) == OT_ERROR_NONE) + { + otIp6PrefixToString(&config.mPrefix, prefixBuf, sizeof(prefixBuf)); + SuccessOrExit(error = ipSetManager.AddToIpSet(kIngressAllowDstSwapIpSet, prefixBuf)); + } + + // 4. Swap ipsets to let them take effect + SuccessOrExit(error = ipSetManager.SwapIpSets(kIngressDenySrcSwapIpSet, kIngressDenySrcIpSet)); + SuccessOrExit(error = ipSetManager.SwapIpSets(kIngressAllowDstSwapIpSet, kIngressAllowDstIpSet)); + +exit: + if (error != OT_ERROR_NONE) + { + otLogWarnPlat("Failed to update ipsets: %s", otThreadErrorToString(error)); + } +} + +#endif // defined(__linux__) && OPENTHREAD_POSIX_CONFIG_FIREWALL_ENABLE + +} // namespace Posix +} // namespace ot diff --git a/src/posix/platform/firewall.hpp b/src/posix/platform/firewall.hpp new file mode 100644 index 000000000..8c4378a6a --- /dev/null +++ b/src/posix/platform/firewall.hpp @@ -0,0 +1,48 @@ +/* + * 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. + */ + +#ifndef OT_POSIX_PLATFORM_FIREWALL_HPP_ +#define OT_POSIX_PLATFORM_FIREWALL_HPP_ + +#include "posix/platform/openthread-posix-config.h" + +#if OPENTHREAD_POSIX_CONFIG_FIREWALL_ENABLE + +#include "common/logging.hpp" +#include "openthread/thread.h" + +namespace ot { +namespace Posix { + +void UpdateIpSets(otInstance *aInstance); + +} // namespace Posix +} // namespace ot + +#endif // OPENTHREAD_POSIX_CONFIG_FIREWALL_ENABLE +#endif // OT_POSIX_PLATFORM_FIREWALL_HPP_ diff --git a/src/posix/platform/netif.cpp b/src/posix/platform/netif.cpp index 71cf92159..e094f9edf 100644 --- a/src/posix/platform/netif.cpp +++ b/src/posix/platform/netif.cpp @@ -163,6 +163,9 @@ unsigned int otSysGetThreadNetifIndex(void) } #if OPENTHREAD_CONFIG_PLATFORM_NETIF_ENABLE +#if OPENTHREAD_POSIX_CONFIG_FIREWALL_ENABLE +#include "firewall.hpp" +#endif #include "posix/platform/ip6_utils.hpp" using namespace ot::Posix::Ip6Utils; @@ -742,6 +745,12 @@ void platformNetifStateChange(otInstance *aInstance, otChangedFlags aFlags) UpdateExternalRoutes(aInstance); } #endif +#if OPENTHREAD_POSIX_CONFIG_FIREWALL_ENABLE + if (OT_CHANGED_THREAD_NETDATA & aFlags) + { + ot::Posix::UpdateIpSets(aInstance); + } +#endif } static void processReceive(otMessage *aMessage, void *aContext) diff --git a/src/posix/platform/openthread-posix-config.h b/src/posix/platform/openthread-posix-config.h index 5d2fa55b5..2bb116672 100644 --- a/src/posix/platform/openthread-posix-config.h +++ b/src/posix/platform/openthread-posix-config.h @@ -188,6 +188,38 @@ #define OPENTHREAD_POSIX_CONFIG_MAX_EXTERNAL_ROUTE_NUM 8 #endif +/** + * @def OPENTHREAD_POSIX_CONFIG_FIREWALL_ENABLE + * + * Define as 1 to enable firewall. + * + * The rules are implemented using ip6tables and ipset. The rules are as follows. + * + * ip6tables -A $OTBR_FORWARD_INGRESS_CHAIN -m pkttype --pkt-type unicast -i $THREAD_IF -p ip -j DROP + * ip6tables -A $OTBR_FORWARD_INGRESS_CHAIN -m set --match-set otbr-ingress-deny-src src -p ip -j DROP + * ip6tables -A $OTBR_FORWARD_INGRESS_CHAIN -m set --match-set otbr-ingress-allow-dst dst -p ip -j ACCEPT + * ip6tables -A $OTBR_FORWARD_INGRESS_CHAIN -m pkttype --pkt-type unicast -p ip -j DROP + * ip6tables -A $OTBR_FORWARD_INGRESS_CHAIN -p ip -j ACCEPT + * + */ +#ifdef __linux__ +#if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE & OPENTHREAD_CONFIG_PLATFORM_NETIF_ENABLE +#ifndef OPENTHREAD_POSIX_CONFIG_FIREWALL_ENABLE +#define OPENTHREAD_POSIX_CONFIG_FIREWALL_ENABLE 1 +#endif +#endif +#endif + +#ifndef OPENTHREAD_POSIX_CONFIG_FIREWALL_ENABLE +#define OPENTHREAD_POSIX_CONFIG_FIREWALL_ENABLE 0 +#endif + +#if OPENTHREAD_POSIX_CONFIG_FIREWALL_ENABLE +#ifndef OPENTHREAD_POSIX_CONFIG_IPSET_BINARY +#define OPENTHREAD_POSIX_CONFIG_IPSET_BINARY "ipset" +#endif +#endif + #ifdef __APPLE__ /** diff --git a/src/posix/platform/system.cpp b/src/posix/platform/system.cpp index 8b78f9be5..b029373c2 100644 --- a/src/posix/platform/system.cpp +++ b/src/posix/platform/system.cpp @@ -49,6 +49,7 @@ #include "common/code_utils.hpp" #include "common/debug.hpp" #include "posix/platform/daemon.hpp" +#include "posix/platform/firewall.hpp" #include "posix/platform/infra_if.hpp" #include "posix/platform/mainloop.hpp" #include "posix/platform/radio_url.hpp" diff --git a/src/posix/platform/utils.cpp b/src/posix/platform/utils.cpp new file mode 100644 index 000000000..5218841e9 --- /dev/null +++ b/src/posix/platform/utils.cpp @@ -0,0 +1,88 @@ +/* + * 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. + */ + +/** + * @file + * The file implements POSIX system utilities. + */ + +#include "utils.hpp" + +#include +#include +#include + +#include "common/code_utils.hpp" +#include "common/logging.hpp" + +namespace ot { +namespace Posix { + +enum +{ + kSystemCommandMaxLength = 1024, ///< Max length of a system call command. + kOutputBufferSize = 1024, ///< Buffer size of command output. +}; + +otError ExecuteCommand(const char *aFormat, ...) +{ + char cmd[kSystemCommandMaxLength]; + char buf[kOutputBufferSize]; + va_list args; + FILE * file; + int exitCode; + otError error = OT_ERROR_NONE; + + va_start(args, aFormat); + vsnprintf(cmd, sizeof(cmd), aFormat, args); + va_end(args); + + file = popen(cmd, "r"); + VerifyOrExit(file != nullptr, error = OT_ERROR_FAILED); + while (fgets(buf, sizeof(buf), file)) + { + size_t length = strlen(buf); + if (buf[length] == '\n') + { + buf[length] = '\0'; + } + otLogInfoPlat("%s", buf); + } + exitCode = pclose(file); + otLogInfoPlat("Execute command `%s` = %d", cmd, exitCode); + VerifyOrExit(exitCode == 0, error = OT_ERROR_FAILED); +exit: + if (error != OT_ERROR_NONE && errno != 0) + { + otLogInfoPlat("Got an error when executing command `%s`: `%s`", cmd, strerror(errno)); + } + return error; +} + +} // namespace Posix +} // namespace ot diff --git a/src/posix/platform/utils.hpp b/src/posix/platform/utils.hpp new file mode 100644 index 000000000..c3f9dda72 --- /dev/null +++ b/src/posix/platform/utils.hpp @@ -0,0 +1,52 @@ +/* + * 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. + */ + +#ifndef OT_POSIX_PLATFORM_UTILS_HPP_ +#define OT_POSIX_PLATFORM_UTILS_HPP_ + +#include "openthread/error.h" + +namespace ot { +namespace Posix { + +/** + * This method formats a system command to execute. + * + * @param[in] aFormat A pointer to the format string. + * @param[in] ... Arguments for the format specification. + * + * @retval OT_ERROR_NONE The command was executed successfully. + * @retval OT_ERROR_FAILED It failed to execute the command. + * + */ +otError ExecuteCommand(const char *aFormat, ...); + +} // namespace Posix +} // namespace ot + +#endif // OT_POSIX_PLATFORM_UTILS_HPP_ diff --git a/tests/scripts/thread-cert/border_router/test_firewall.py b/tests/scripts/thread-cert/border_router/test_firewall.py new file mode 100644 index 000000000..8725584e2 --- /dev/null +++ b/tests/scripts/thread-cert/border_router/test_firewall.py @@ -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(20) + + 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() diff --git a/tests/scripts/thread-cert/node.py b/tests/scripts/thread-cert/node.py index dabbc1e1f..3a0b2d3b4 100755 --- a/tests/scripts/thread-cert/node.py +++ b/tests/scripts/thread-cert/node.py @@ -1719,7 +1719,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 diff --git a/tests/scripts/thread-cert/pktverify/packet_verifier.py b/tests/scripts/thread-cert/pktverify/packet_verifier.py index 5c02166c1..c78ec7e0b 100644 --- a/tests/scripts/thread-cert/pktverify/packet_verifier.py +++ b/tests/scripts/thread-cert/pktverify/packet_verifier.py @@ -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 diff --git a/tests/scripts/thread-cert/pktverify/test_info.py b/tests/scripts/thread-cert/pktverify/test_info.py index c44f52ab5..937929b0b 100644 --- a/tests/scripts/thread-cert/pktverify/test_info.py +++ b/tests/scripts/thread-cert/pktverify/test_info.py @@ -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 '' diff --git a/tests/scripts/thread-cert/run_cert_suite.py b/tests/scripts/thread-cert/run_cert_suite.py index a3b7d5755..512ab815d 100755 --- a/tests/scripts/thread-cert/run_cert_suite.py +++ b/tests/scripts/thread-cert/run_cert_suite.py @@ -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') diff --git a/tests/scripts/thread-cert/thread_cert.py b/tests/scripts/thread-cert/thread_cert.py index 5d32e1120..514cc4b46 100755 --- a/tests/scripts/thread-cert/thread_cert.py +++ b/tests/scripts/thread-cert/thread_cert.py @@ -361,6 +361,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