diff --git a/doc/doxygen/src/gnrc_ipv6_auto_subnets_simple.puml b/doc/doxygen/src/gnrc_ipv6_auto_subnets_simple.puml new file mode 100644 index 0000000000..18c16e51d6 --- /dev/null +++ b/doc/doxygen/src/gnrc_ipv6_auto_subnets_simple.puml @@ -0,0 +1,35 @@ +' to generate SVG run plantuml -tsvg gnrc_ipv6_auto_subnets_simple.puml +@startuml +nwdiag { + + network level1 { + address = "2001:db8::/60"; + + router_a [address = "2001:db8::c8f4:13ff:fece:3f43", description = "1st level router"]; + leaf_a [address = "2001:db8::804b:fcff:feb6:43fb", description = "1st level leaf node"]; + } + + network level2 { + address = "2001:db8:0:8::/61"; + + router_a [address = "2001:db8:0:8:3c27:6dff:fe25:e95d"]; + router_b [address = "2001:db8:0:8:5075:35ff:fefa:30bc", description = "2nd level router"]; + } + + network level3 { + address = "2001:db8:0:c::/62"; + + router_b [address = "2001:db8:0:c:2ca3:9eff:fea9:68f7"]; + router_c [address = "2001:db8:0:c:fc33:13ff:fe93:5ae4", description = "3rd level router"]; + leaf_b1 [address = "2001:db8:0:c:209e:deff:fea9:fd1b", description = "3rd level leaf node"]; + leaf_b2 [address = "2001:db8:0:c:5491:a2ff:fe98:61a2", description = "3rd level leaf node"]; + } + + network level4 { + address = "2001:db8:0:e::/63"; + + router_c [address = "2001:db8:0:e:a8d9:e1ff:feab:d544"]; + leaf_c [address = "2001:db8:0:e:1cf5:33ff:fe7c:c70c", description = "4th level leaf node"]; + } +} +@enduml diff --git a/doc/doxygen/src/gnrc_ipv6_auto_subnets_simple.svg b/doc/doxygen/src/gnrc_ipv6_auto_subnets_simple.svg new file mode 100644 index 0000000000..1ec775bdd1 --- /dev/null +++ b/doc/doxygen/src/gnrc_ipv6_auto_subnets_simple.svg @@ -0,0 +1 @@ +level12001:db8::/60level22001:db8:0:8::/61level32001:db8:0:c::/62level42001:db8:0:e::/632001:db8::c8f4:13ff:fece:3f432001:db8:0:8:3c27:6dff:fe25:e95d2001:db8::804b:fcff:feb6:43fb2001:db8:0:8:5075:35ff:fefa:30bc2001:db8:0:c:2ca3:9eff:fea9:68f72001:db8:0:c:fc33:13ff:fe93:5ae42001:db8:0:e:a8d9:e1ff:feab:d5442001:db8:0:c:209e:deff:fea9:fd1b2001:db8:0:c:5491:a2ff:fe98:61a22001:db8:0:e:1cf5:33ff:fe7c:c70c1st level router1st level leaf node2nd level router3rd level router3rd level leaf node3rd level leaf node4th level leaf node diff --git a/examples/gnrc_networking_subnets/Makefile b/examples/gnrc_networking_subnets/Makefile new file mode 100644 index 0000000000..025cd6e13b --- /dev/null +++ b/examples/gnrc_networking_subnets/Makefile @@ -0,0 +1,50 @@ +# name of your application +APPLICATION = gnrc_networking-subnets + +# If no BOARD is found in the environment, use this default: +BOARD ?= native + +# This has to be the absolute path to the RIOT base directory: +RIOTBASE ?= $(CURDIR)/../.. + +# Include packages that pull up and auto-init the link layer. +USEMODULE += gnrc_netdev_default +USEMODULE += auto_init_gnrc_netif +# Activate ICMPv6 error messages +USEMODULE += gnrc_icmpv6_error +# Specify the mandatory networking modules for IPv6 and UDP +USEMODULE += gnrc_udp +# This application dumps received packets to STDIO using the pktdump module +USEMODULE += gnrc_pktdump +# Additional networking modules that can be dropped if not needed +USEMODULE += gnrc_icmpv6_echo +# Add also the shell, some shell commands +USEMODULE += shell +USEMODULE += shell_commands +USEMODULE += ps +USEMODULE += netstats_l2 +USEMODULE += netstats_ipv6 + +# leaf nodes only have a single interface +ifeq (1, $(LEAF)) + APPLICATION := $(APPLICATION)_leaf + USEMODULE += gnrc_ipv6_default +else + USEMODULE += gnrc_ipv6_router_default + USEMODULE += gnrc_ipv6_auto_subnets + CFLAGS += -DNETDEV_TAP_MAX=2 + PORT ?= tap_a0 tap_b0 +endif + +# Comment this out to disable code in RIOT that does safety checking +# which is not needed in a production environment but helps in the +# development process: +DEVELHELP ?= 1 + +# Change this to 0 show compiler invocation lines by default: +QUIET ?= 1 + +include $(RIOTBASE)/Makefile.include + +# Set a custom channel if needed +include $(RIOTMAKE)/default-radio-settings.inc.mk diff --git a/examples/gnrc_networking_subnets/Makefile.ci b/examples/gnrc_networking_subnets/Makefile.ci new file mode 100644 index 0000000000..04703170cd --- /dev/null +++ b/examples/gnrc_networking_subnets/Makefile.ci @@ -0,0 +1,38 @@ +BOARD_INSUFFICIENT_MEMORY := \ + arduino-duemilanove \ + arduino-leonardo \ + arduino-mega2560 \ + arduino-nano \ + arduino-uno \ + atmega1284p \ + atmega328p \ + atmega328p-xplained-mini \ + atxmega-a3bu-xplained \ + bluepill-stm32f030c8 \ + derfmega128 \ + i-nucleo-lrwan1 \ + ict_panhead \ + m1284p \ + mega-xplained \ + microduino-corerf \ + msb-430 \ + msb-430h \ + nucleo-f030r8 \ + nucleo-f031k6 \ + nucleo-f042k6 \ + nucleo-f303k8 \ + nucleo-f334r8 \ + nucleo-l011k4 \ + nucleo-l031k6 \ + nucleo-l053r8 \ + samd10-xmini \ + slstk3400a \ + stk3200 \ + stm32f030f4-demo \ + stm32f0discovery \ + stm32l0538-disco \ + telosb \ + waspmote-pro \ + z1 \ + zigduino \ + # diff --git a/examples/gnrc_networking_subnets/README.md b/examples/gnrc_networking_subnets/README.md new file mode 100644 index 0000000000..2707ee3fbb --- /dev/null +++ b/examples/gnrc_networking_subnets/README.md @@ -0,0 +1,78 @@ +# Auto-configuration for nested subnets on a (simple) tree topology + +This example demonstrates IPv6 subnet auto-configuration for networks on a +tree topology. + +This allows to connect multiple links with individual subnets and route +between them. +Each link can have an arbitrary number of hosts, but there can be only +a single router on each link. +Routers can have multiple interfaces to connect different downlinks. + +![](../../doc/doxygen/src/gnrc_ipv6_auto_subnets_simple.svg) + +## Setup on native + +To simulate such a network on `native` a `setup_taps.sh` script is provided that +will create TAP interfaces and bridges. +Each bridge will be it's own subnet and `native` instances will act as routers +to route between them. +The setup script will also start `radvd` to advertise a large prefix that allows +for sub-division by RIOT. + +The interfaces that should be created are specified in the `tapology.txt` file. + +Each router will have (at least) two interfaces that should be on different +bridges. + +To start the first router, run + + make term PORT="tap_a0 tap_b1" + +This will route between the `tap_a0` interface on `br0` and the `tap_b1` +interface on `br1`. +Start more `native` instances to simulate a cascading tree network: + + make term PORT="tap_b0 tap_c1" + make term PORT="tap_c0 tap_d1" + … + +It is also possible to connect non-routing leaf nodes with a single interface: + + make flash term LEAF=1 PORT=tap_b2 + + +## Setup on hardware + +On physical hardware the setup will be the same. +Routing nodes need at least two interfaces between which they can route. +For a simple setup, you can use `ethos` or `slipdev` to turn any UART into +a network interface. (If you need to use DHCPv6 IA_PD for obtaining a prefix, +use `slipdev_l2addr` instead of plain `slipdev`) + +### Obtaining the prefix + +#### Via router advertisements + +Usually routers are configured to advertise a /64 prefix that can not be divided +further when using SLAAC. +If you can configure your router to advertise a larger prefix instead, this +will be the easiest solution. + +#### Via DHCPv6 + +If you can't change the configuration of your router, but your router supports +DHCPv6 IA_PD, you can use this to request a larger prefix. + +This requires a gateway node that uses the `gnrc_dhcpv6_client_simple_pd` +module *instead* of `gnrc_ipv6_auto_subnets`. + +Make sure to set the `CONFIG_GNRC_DHCPV6_CLIENT_6LBR_UPSTREAM` option to the +ID of the upstream interface on the gateway device, e.g. + + CFLAGS += -DCONFIG_GNRC_DHCPV6_CLIENT_6LBR_UPSTREAM=6 + +As with GNRC the interface ID is based on the PID of the interface thread, this can +change if you add modules / threads and is a common source of errors. +If the configuration is set wrong, you will not get a prefix for the downstream +interface(s). diff --git a/examples/gnrc_networking_subnets/main.c b/examples/gnrc_networking_subnets/main.c new file mode 120000 index 0000000000..f6a0e7f508 --- /dev/null +++ b/examples/gnrc_networking_subnets/main.c @@ -0,0 +1 @@ +../gnrc_networking/main.c \ No newline at end of file diff --git a/examples/gnrc_networking_subnets/setup_taps.sh b/examples/gnrc_networking_subnets/setup_taps.sh new file mode 100755 index 0000000000..15c36626f8 --- /dev/null +++ b/examples/gnrc_networking_subnets/setup_taps.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +PREFIX=2001:db8::/60 + +SCRIPTPATH=$(dirname $(realpath "$0")) + +RIOTBASE=$SCRIPTPATH/../.. +RIOTTOOLS=$RIOTBASE/dist/tools +TOPOLOGY=$SCRIPTPATH/tapology.txt + +setup() { + echo "creating tap interfaces" + i=0 + sed '/^#/d' "$1" | while read -r level num; do + # we actually want to execute the output here. + # shellcheck disable=SC2091 + $(printf -- "sudo %s/tapsetup/tapsetup -b br%s -t tap_%s -c %s\n" "$RIOTTOOLS" "$i" "$level" "$num") > /dev/null; + i=$((i+1)) + done + + # add address to br0 so this can be tested by pinging the host system + sudo ip addr add ${PREFIX/::\//::1\/} dev br0 + + # start radvd with a large prefix + sudo "$RIOTTOOLS"/radvd/radvd.sh -c br0 $PREFIX +} + +teardown() { + echo "deleting tap interfaces" + i=0 + sed '/^#/d' "$1" | while read -r level num; do + $(printf -- "sudo %s/tapsetup/tapsetup -b br%s -t tap_%s -d\n" "$RIOTTOOLS" "$i" "$level") > /dev/null; + i=$((i+1)) + done + + # stop radvd + sudo "$RIOTTOOLS"/radvd/radvd.sh -d +} + +if [ $# -gt 1 ]; then + TOPOLOGY=$2 +fi + +if [ ! -f "$TOPOLOGY" ]; then + echo "no such file: $TOPOLOGY" + exit 1 +fi + +if [ $# -gt 0 ]; then + case $1 in + -c) + ;; + -d) + teardown "$TOPOLOGY" + exit + ;; + *) + echo "usage: $0 [-c ] [-d topology]" + exit 1 + ;; + esac +fi + +setup "$TOPOLOGY" diff --git a/examples/gnrc_networking_subnets/tapology.txt b/examples/gnrc_networking_subnets/tapology.txt new file mode 100644 index 0000000000..04e21b7865 --- /dev/null +++ b/examples/gnrc_networking_subnets/tapology.txt @@ -0,0 +1,9 @@ +# This file specifies the tap bridges and tap devices that +# should be created. +# +# bridge nodes +a 1 +b 3 +c 3 +d 3 +e 3 diff --git a/examples/gnrc_networking_subnets/udp.c b/examples/gnrc_networking_subnets/udp.c new file mode 120000 index 0000000000..c03f41c7ed --- /dev/null +++ b/examples/gnrc_networking_subnets/udp.c @@ -0,0 +1 @@ +../gnrc_networking/udp.c \ No newline at end of file diff --git a/makefiles/pseudomodules.inc.mk b/makefiles/pseudomodules.inc.mk index d21b36fdcf..c9f315e9a6 100644 --- a/makefiles/pseudomodules.inc.mk +++ b/makefiles/pseudomodules.inc.mk @@ -48,6 +48,7 @@ PSEUDOMODULES += gnrc_ipv6_nib_6lr PSEUDOMODULES += gnrc_ipv6_nib_dns PSEUDOMODULES += gnrc_ipv6_nib_rio PSEUDOMODULES += gnrc_ipv6_nib_router +PSEUDOMODULES += gnrc_ipv6_nib_rtr_adv_pio_cb PSEUDOMODULES += gnrc_netdev_default PSEUDOMODULES += gnrc_neterr PSEUDOMODULES += gnrc_netapi_callbacks diff --git a/sys/net/gnrc/Makefile b/sys/net/gnrc/Makefile index e079b12dc6..8c4242a83e 100644 --- a/sys/net/gnrc/Makefile +++ b/sys/net/gnrc/Makefile @@ -88,6 +88,9 @@ endif ifneq (,$(filter gnrc_rpl_p2p,$(USEMODULE))) DIRS += routing/rpl/p2p endif +ifneq (,$(filter gnrc_ipv6_auto_subnets,$(USEMODULE))) + DIRS += routing/ipv6_auto_subnets +endif ifneq (,$(filter gnrc_sixlowpan,$(USEMODULE))) DIRS += network_layer/sixlowpan endif diff --git a/sys/net/gnrc/Makefile.dep b/sys/net/gnrc/Makefile.dep index c9e7f352ad..05c501eabd 100644 --- a/sys/net/gnrc/Makefile.dep +++ b/sys/net/gnrc/Makefile.dep @@ -113,6 +113,12 @@ ifneq (,$(filter gnrc_rpl,$(USEMODULE))) USEMODULE += evtimer endif +ifneq (,$(filter gnrc_ipv6_auto_subnets,$(USEMODULE))) + USEMODULE += gnrc_ipv6_nib_rtr_adv_pio_cb + CFLAGS += -DCONFIG_GNRC_IPV6_NIB_ADV_ROUTER=0 + CFLAGS += -DCONFIG_GNRC_IPV6_NIB_ADD_RIO_IN_LAST_RA=1 +endif + ifneq (,$(filter gnrc_netif,$(USEMODULE))) USEMODULE += netif USEMODULE += l2util diff --git a/sys/net/gnrc/network_layer/ipv6/nib/nib.c b/sys/net/gnrc/network_layer/ipv6/nib/nib.c index e99ce3b715..dec7a6deae 100644 --- a/sys/net/gnrc/network_layer/ipv6/nib/nib.c +++ b/sys/net/gnrc/network_layer/ipv6/nib/nib.c @@ -754,6 +754,13 @@ static void _handle_rtr_adv(gnrc_netif_t *netif, const ipv6_hdr_t *ipv6, (ndp_opt_pi_t *)opt); #endif /* CONFIG_GNRC_IPV6_NIB_MULTIHOP_P6C */ next_timeout = _min(next_timeout, min_pfx_timeout); + + /* notify optional PIO consumer */ + if (IS_USED(MODULE_GNRC_IPV6_NIB_RTR_ADV_PIO_CB)) { + extern void gnrc_ipv6_nib_rtr_adv_pio_cb(gnrc_netif_t *netif, + const ndp_opt_pi_t *pio); + gnrc_ipv6_nib_rtr_adv_pio_cb(netif, (ndp_opt_pi_t *)opt); + } break; } /* ABRO was already secured in the option check above */ diff --git a/sys/net/gnrc/routing/ipv6_auto_subnets/Makefile b/sys/net/gnrc/routing/ipv6_auto_subnets/Makefile new file mode 100644 index 0000000000..fe99c627cb --- /dev/null +++ b/sys/net/gnrc/routing/ipv6_auto_subnets/Makefile @@ -0,0 +1,3 @@ +MODULE = gnrc_ipv6_auto_subnets + +include $(RIOTBASE)/Makefile.base diff --git a/sys/net/gnrc/routing/ipv6_auto_subnets/gnrc_ipv6_auto_subnets.c b/sys/net/gnrc/routing/ipv6_auto_subnets/gnrc_ipv6_auto_subnets.c new file mode 100644 index 0000000000..635892f78c --- /dev/null +++ b/sys/net/gnrc/routing/ipv6_auto_subnets/gnrc_ipv6_auto_subnets.c @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2021 ML!PA Consulting GmbH + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @defgroup net_gnrc_ipv6_auto_subnets Simple-Subnet auto-configuration + * @ingroup net_gnrc + * @brief Automatic configuration for cascading subnets + * + * About + * ===== + * + * This module provides an automatic configuration for networks with a simple + * tree topology. + * + * If a sufficiently large IPv6 prefix (> /64) is provided via Router Advertisements, + * a routing node with this module will automatically configure subnets from it + * by dividing it into sub-prefixes for each downstream interface. + * + * There can only be a single routing node on each level of the network but an + * arbitrary number of leaf nodes. + * + * ![Example Topology](gnrc_ipv6_auto_subnets_simple.svg) + * + * The downstream network(s) receive the sub-prefix via Router Advertisements + * and the process repeats until the bits of the prefix are exhausted. + * The smallest subnet must still have a /64 prefix. + * + * The new subnet must no longer be considered on-link by the hosts in the + * parent network. + * Therefore the downstream router will send a router advertisement with only + * a Route Information Option included to the upstream network. + * The Route Information Option contains the prefix of the downstream network + * so that upstream routers will no longer consider hosts in this subnet on-link + * but instead will use the downstream router to route to the new subnet. + * + * Usage + * ===== + * + * Simply add the `gnrc_ipv6_auto_subnets` module to the code of the nodes that + * should act as routers in the cascading network. + * The upstream network will be automatically chosen as the one that first + * receives a router advertisement. + * + * @{ + * + * @file + * @author Benjamin Valentin + */ + +#include "net/gnrc/ipv6/nib.h" +#include "net/gnrc/ndp.h" + +#define ENABLE_DEBUG 0 +#include "debug.h" + +static char addr_str[IPV6_ADDR_MAX_STR_LEN]; + +static void _init_sub_prefix(ipv6_addr_t *out, + const ipv6_addr_t *prefix, uint8_t bits, + uint8_t idx, uint8_t idx_bits) +{ + uint8_t bytes = bits / 8; + uint8_t rem = bits % 8; + int8_t shift = 8 - rem - idx_bits; + + /* first copy old prefix */ + memset(out, 0, sizeof(*out)); + ipv6_addr_init_prefix(out, prefix, bits); + + /* if new bits are between bytes, first copy over the most significant bits */ + if (shift < 0) { + out->u8[bytes] |= idx >> -shift; + out->u8[++bytes] = 0; + shift += 8; + } + + /* shift remaining bits at the end of the prefix */ + out->u8[bytes] |= idx << shift; +} + +static bool _remove_old_prefix(gnrc_netif_t *netif, + const ipv6_addr_t *pfx, uint8_t pfx_len, + gnrc_pktsnip_t **ext_opts) +{ + gnrc_ipv6_nib_pl_t entry; + gnrc_pktsnip_t *tmp; + void *state = NULL; + ipv6_addr_t old_pfx; + uint8_t old_pfx_len = 0; + + /* iterate prefix list to see if the prefix already exists */ + while (gnrc_ipv6_nib_pl_iter(netif->pid, &state, &entry)) { + uint8_t match_len = ipv6_addr_match_prefix(&entry.pfx, pfx); + + /* The prefix did not change - nothing to do here */ + if (match_len >= pfx_len && pfx_len == entry.pfx_len) { + return true; + } + + /* find prefix that is closest to the new prefix */ + if (match_len > old_pfx_len) { + old_pfx_len = entry.pfx_len; + old_pfx = entry.pfx; + } + } + + /* no prefix found */ + if (old_pfx_len == 0) { + return false; + } + + DEBUG("auto_subnets: remove old prefix %s/%u\n", + ipv6_addr_to_str(addr_str, &old_pfx, sizeof(addr_str)), old_pfx_len); + + /* invalidate old prefix in RIO */ + tmp = gnrc_ndp_opt_ri_build(&old_pfx, old_pfx_len, 0, + NDP_OPT_RI_FLAGS_PRF_NONE, *ext_opts); + if (tmp) { + *ext_opts = tmp; + } + + /* remove the prefix */ + gnrc_ipv6_nib_pl_del(netif->pid, &old_pfx, old_pfx_len); + + return false; +} + +static void _configure_subnets(uint8_t subnets, gnrc_netif_t *upstream, + const ndp_opt_pi_t *pio) +{ + gnrc_netif_t *downstream = NULL; + gnrc_pktsnip_t *ext_opts = NULL; + const ipv6_addr_t *prefix = &pio->prefix; + uint32_t valid_ltime = byteorder_ntohl(pio->valid_ltime); + uint32_t pref_ltime = byteorder_ntohl(pio->pref_ltime); + const uint8_t prefix_len = pio->prefix_len; + uint8_t new_prefix_len, subnet_len; + + DEBUG("auto_subnets: create %u subnets\n", subnets); + + /* Calculate remaining prefix length. + * For n subnets we consume floor(log_2 n) + 1 bits. + * To calculate floor(log_2 n) quickly, find the position of the + * most significant set bit by counting leading zeros. + */ + subnet_len = 32 - __builtin_clz(subnets); + new_prefix_len = prefix_len + subnet_len; + + if (new_prefix_len > 64) { + DEBUG("auto_subnets: can't split /%u into %u subnets\n", prefix_len, subnets); + return; + } + + while ((downstream = gnrc_netif_iter(downstream))) { + gnrc_pktsnip_t *tmp; + ipv6_addr_t new_prefix; + + if (downstream == upstream) { + continue; + } + + /* create subnet from upstream prefix */ + _init_sub_prefix(&new_prefix, prefix, prefix_len, subnets--, subnet_len); + + DEBUG("auto_subnets: configure prefix %s/%u on %u\n", + ipv6_addr_to_str(addr_str, &new_prefix, sizeof(addr_str)), + new_prefix_len, downstream->pid); + + /* first remove old prefix if the prefix changed */ + _remove_old_prefix(downstream, &new_prefix, new_prefix_len, &ext_opts); + + /* configure subnet on downstream interface */ + gnrc_netif_ipv6_add_prefix(downstream, &new_prefix, new_prefix_len, + valid_ltime, pref_ltime); + + /* start advertising subnet */ + gnrc_ipv6_nib_change_rtr_adv_iface(downstream, true); + + /* add route information option with new subnet */ + tmp = gnrc_ndp_opt_ri_build(&new_prefix, new_prefix_len, valid_ltime, + NDP_OPT_RI_FLAGS_PRF_NONE, ext_opts); + if (tmp == NULL) { + DEBUG("auto_subnets: No space left in packet buffer. Not adding RIO\n"); + } else { + ext_opts = tmp; + } + } + + /* immediately send an RA with RIO */ + if (ext_opts) { + gnrc_ndp_rtr_adv_send(upstream, NULL, + &ipv6_addr_all_nodes_link_local, true, ext_opts); + } else { + DEBUG("auto_subnets: Options empty, not sending RA\n"); + } +} + +void gnrc_ipv6_nib_rtr_adv_pio_cb(gnrc_netif_t *upstream, const ndp_opt_pi_t *pio) +{ + /* create a subnet for each downstream interface */ + unsigned subnets = gnrc_netif_numof() - 1; + + if (subnets == 0) { + return; + } + + if (pio->valid_ltime.u32 == 0) { + return; + } + + _configure_subnets(subnets, upstream, pio); +} +/** @} */