diff --git a/sys/include/net/gnrc/ipv6/ext.h b/sys/include/net/gnrc/ipv6/ext.h index ee66c48dfbfc..4eecd510cc32 100644 --- a/sys/include/net/gnrc/ipv6/ext.h +++ b/sys/include/net/gnrc/ipv6/ext.h @@ -45,6 +45,17 @@ extern "C" { * @ingroup config * @{ */ +/** + * @brief IPv6 fragmentation send buffer size + * + * This limits the total amount of datagrams that can be fragmented at the same time. + * + * @note Only applicable with [gnrc_ipv6_ext_frag](@ref net_gnrc_ipv6_ext_frag) module + */ +#ifndef GNRC_IPV6_EXT_FRAG_SEND_SIZE +#define GNRC_IPV6_EXT_FRAG_SEND_SIZE (1U) +#endif + /** * @brief IPv6 fragmentation reassembly buffer size * @@ -76,6 +87,7 @@ extern "C" { #ifndef GNRC_IPV6_EXT_FRAG_RBUF_TIMEOUT_US #define GNRC_IPV6_EXT_FRAG_RBUF_TIMEOUT_US (10U * US_PER_SEC) #endif + /** @} **/ /** diff --git a/sys/include/net/gnrc/ipv6/ext/frag.h b/sys/include/net/gnrc/ipv6/ext/frag.h index 397e617c48ff..4c9b7eaecb53 100644 --- a/sys/include/net/gnrc/ipv6/ext/frag.h +++ b/sys/include/net/gnrc/ipv6/ext/frag.h @@ -20,6 +20,7 @@ #ifndef NET_GNRC_IPV6_EXT_FRAG_H #define NET_GNRC_IPV6_EXT_FRAG_H +#include #include #include "clist.h" @@ -34,7 +35,22 @@ extern "C" { /** * @brief Message type to time reassembly buffer garbage collection */ -#define GNRC_IPV6_EXT_FRAG_RBUF_GC (0xfe00U) +#define GNRC_IPV6_EXT_FRAG_RBUF_GC (0xfe00U) + +/** + * @brief Message type to continue fragmenting a datagram from a given + * fragmentation send buffer + * + * Expected type: @ref gnrc_ipv6_ext_frag_send_t + */ +#define GNRC_IPV6_EXT_FRAG_CONTINUE (0xfe01U) + +/** + * @brief Message type to send a fragment of an IPv6 datagram. + * + * Expected type: @ref gnrc_pktsnip_t + */ +#define GNRC_IPV6_EXT_FRAG_SEND (0xfe02U) /** * @brief Data type to describe limits of a single fragment in the reassembly @@ -47,6 +63,18 @@ typedef struct gnrc_ipv6_ext_frag_limits { * fragment */ } gnrc_ipv6_ext_frag_limits_t; +/** + * @brief Fragmentation send buffer type + */ +typedef struct { + gnrc_pktsnip_t *pkt; /**< the IPv6 packet to fragment */ + gnrc_pktsnip_t *per_frag; /**< per fragment headers */ + uint32_t id; /**< the identification for the fragment header */ + uint16_t path_mtu; /**< path MTU to destination of + * gnrc_ipv6_ext_frag_send_t::pkt */ + uint16_t offset; /**< current fragmentation offset */ +} gnrc_ipv6_ext_frag_send_t; + /** * @brief A reassembly buffer entry */ @@ -71,6 +99,26 @@ typedef struct { */ void gnrc_ipv6_ext_frag_init(void); +/** + * @brief Send an IPv6 packet fragmented + * + * @param[in] pkt The IPv6 packet. The packet must have an already + * prepared @ref GNRC_NETTYPE_NETIF snip as its first + * snip. The packet must contain at least an IPv6 header + * and any number of IPv6 extension headers after that. + * @param[in] path_mtu Path MTU to destination of IPv6 packet. + */ +void gnrc_ipv6_ext_frag_send_pkt(gnrc_pktsnip_t *pkt, unsigned path_mtu); + +/** + * @brief (Continue to) fragment packet already in fragmentation send buffer + * + * @pre `snd_buf != NULL` + * + * @param[in,out] snd_buf A fragmentation send buffer entry. May not be NULL. + */ +void gnrc_ipv6_ext_frag_send(gnrc_ipv6_ext_frag_send_t *snd_buf); + /** * @brief Reassemble fragmented IPv6 packet * diff --git a/sys/net/gnrc/network_layer/ipv6/ext/frag/gnrc_ipv6_ext_frag.c b/sys/net/gnrc/network_layer/ipv6/ext/frag/gnrc_ipv6_ext_frag.c index ef1b7ea0dae0..26c458dd7ee9 100644 --- a/sys/net/gnrc/network_layer/ipv6/ext/frag/gnrc_ipv6_ext_frag.c +++ b/sys/net/gnrc/network_layer/ipv6/ext/frag/gnrc_ipv6_ext_frag.c @@ -14,12 +14,18 @@ */ #include +#include #include "byteorder.h" #include "net/ipv6/ext/frag.h" #include "net/ipv6/addr.h" +#include "net/ipv6/hdr.h" +#include "net/gnrc/ipv6.h" #include "net/gnrc/ipv6/ext.h" +#include "net/gnrc/ipv6/ext/frag.h" +#include "net/gnrc/nettype.h" #include "net/gnrc/pktbuf.h" +#include "random.h" #include "sched.h" #include "xtimer.h" @@ -28,12 +34,20 @@ #define ENABLE_DEBUG (0) #include "debug.h" +static gnrc_ipv6_ext_frag_send_t _snd_bufs[GNRC_IPV6_EXT_FRAG_SEND_SIZE]; static gnrc_ipv6_ext_frag_rbuf_t _rbuf[GNRC_IPV6_EXT_FRAG_RBUF_SIZE]; static gnrc_ipv6_ext_frag_limits_t _limits_pool[GNRC_IPV6_EXT_FRAG_LIMITS_POOL_SIZE]; static clist_node_t _free_limits; static xtimer_t _gc_xtimer; static msg_t _gc_msg = { .type = GNRC_IPV6_EXT_FRAG_RBUF_GC }; +/** + * @todo Implement better mechanism as described in + * https://tools.ietf.org/html/rfc7739 (for minimal approach + * destination cache is required) + */ +static uint32_t _last_id; + typedef enum { FRAG_LIMITS_NEW = 0, /**< limits are not present and do not overlap */ FRAG_LIMITS_DUPLICATE, /**< fragment limits are already present */ @@ -46,11 +60,289 @@ void gnrc_ipv6_ext_frag_init(void) #ifdef TEST_SUITES memset(_rbuf, 0, sizeof(_rbuf)); #endif + _last_id = random_uint32(); for (unsigned i = 0; i < GNRC_IPV6_EXT_FRAG_LIMITS_POOL_SIZE; i++) { clist_rpush(&_free_limits, (clist_node_t *)&_limits_pool[i]); } } +/* + * ================== + * IPv6 fragmentation + * ================== + */ + +/** + * @brief Allocates a fragmentation send buffer entry from pool + * + * @return A free fragmentation send buffer entry. + */ +static gnrc_ipv6_ext_frag_send_t *_snd_buf_alloc(void); + +/** + * @brief Removes a fragmentation send buffer and releases the stored + * datagrams and fragments. + * + * @param[in] snd_buf A fragmentation send buffer entry + */ +static void _snd_buf_free(gnrc_ipv6_ext_frag_send_t *snd_buf); + +/** + * @brief Removes a fragmentation send buffer without releasing the stored + * datagrams and fragments. + * + * @param[in] snd_buf A fragmentation send buffer entry + */ +static void _snd_buf_del(gnrc_ipv6_ext_frag_send_t *snd_buf); + +/** + * @brief Dermines the last Per-Fragment extension header of a datagram. + * + * @see [RFC 8200, section 4.5](https://tools.ietf.org/html/rfc8200#section-4.5) + * for definition of _Per-Fragment extension header_ + * + * @param[in] pkt An IPv6 datagram + * + * @return The last Per-Fragment extension header in @p pkt. + * @return NULL, unexpected error. Should never be reached. + */ +static gnrc_pktsnip_t *_determine_last_per_frag(gnrc_pktsnip_t *pkt); + +void gnrc_ipv6_ext_frag_send_pkt(gnrc_pktsnip_t *pkt, unsigned path_mtu) +{ + gnrc_ipv6_ext_frag_send_t *snd_buf = _snd_buf_alloc(); + gnrc_pktsnip_t *last_per_frag; + + assert(pkt->type == GNRC_NETTYPE_NETIF); + if (snd_buf == NULL) { + DEBUG("ipv6_ext_frag: can not allocate fragmentation send buffer\n"); + gnrc_pktbuf_release_error(pkt, ENOMEM); + return; + } + last_per_frag = _determine_last_per_frag(pkt); + snd_buf->per_frag = pkt; + snd_buf->pkt = last_per_frag->next; + /* separate per-fragment headers from rest */ + last_per_frag->next = NULL; + snd_buf->id = _last_id; + _last_id += random_uint32_range(1, 64); + snd_buf->path_mtu = path_mtu; + snd_buf->offset = 0; + gnrc_ipv6_ext_frag_send(snd_buf); +} + +void gnrc_ipv6_ext_frag_send(gnrc_ipv6_ext_frag_send_t *snd_buf) +{ + assert(snd_buf != NULL); + gnrc_pktsnip_t *last = NULL, *ptr, *to_send = NULL; + ipv6_ext_frag_t *frag_hdr; + uint8_t *nh = NULL; + network_uint16_t *len = NULL; + msg_t msg; + /* see if fragment to send fits into the path MTU */ + bool last_fragment = (snd_buf->path_mtu > + (gnrc_pkt_len(snd_buf->per_frag->next) + + sizeof(ipv6_ext_frag_t) + + gnrc_pkt_len(snd_buf->pkt))); + uint16_t remaining = snd_buf->path_mtu & 0xfff8; /* lower multiple of 8 */ + + /* prepare fragment for sending */ + ptr = snd_buf->per_frag; + if (!last_fragment) { + /* this won't be the last fragment + * => we need to duplicate the per-fragment headers */ + gnrc_pktbuf_hold(ptr, 1); + } + else { + /* prevent duplicate release of per_frag */ + snd_buf->per_frag = NULL; + } + /* first add per-fragment headers */ + while (ptr) { + gnrc_pktsnip_t *tmp = gnrc_pktbuf_start_write(ptr); + if (tmp == NULL) { + DEBUG("ipv6_ext_frag: packet buffer full, canceling fragmentation\n"); + if (ptr->users > 1) { + /* we are not the last fragment, so we need to also release + * our hold on the snips we did not duplicate so far + * and all also release all the snips we did duplicated so far + */ + if (to_send != NULL) { + gnrc_pktbuf_release(to_send); + } + else { + gnrc_pktbuf_release(ptr); + } + } + _snd_buf_free(snd_buf); + return; + } + ptr = tmp; + if (to_send == NULL) { + to_send = ptr; + } + switch (ptr->type) { + case GNRC_NETTYPE_IPV6: { + ipv6_hdr_t *hdr = ptr->data; + nh = &hdr->nh; + len = &hdr->len; + break; + } + case GNRC_NETTYPE_IPV6_EXT: { + ipv6_ext_t *hdr = ptr->data; + nh = &hdr->nh; + break; + } + default: + break; + } + if (ptr->type != GNRC_NETTYPE_NETIF) { + remaining -= ptr->size; + } + if (last) { + last->next = ptr; + } + last = ptr; + ptr = ptr->next; + } + assert(nh != NULL); + /* then the fragment header */ + ptr = gnrc_ipv6_ext_build(last, last->next, *nh, sizeof(ipv6_ext_frag_t)); + if (ptr == NULL) { + DEBUG("ipv6_ext_frag: unable to create fragmentation header\n"); + gnrc_pktbuf_release(to_send); + _snd_buf_free(snd_buf); + return; + } + remaining -= sizeof(ipv6_ext_frag_t); + frag_hdr = ptr->data; + ipv6_ext_frag_set_offset(frag_hdr, snd_buf->offset); + if (!last_fragment) { + ipv6_ext_frag_set_more(frag_hdr); + } + frag_hdr->id = byteorder_htonl(snd_buf->id); + *nh = PROTNUM_IPV6_EXT_FRAG; + last = ptr; + /* then the rest */ + while (remaining && snd_buf->pkt) { + if (last_fragment || + (snd_buf->pkt->size <= remaining)) { + ptr = snd_buf->pkt; + snd_buf->pkt = ptr->next; + } + else { + ptr = gnrc_pktbuf_mark(snd_buf->pkt, remaining, + GNRC_NETTYPE_UNDEF); + if (ptr == NULL) { + DEBUG("ipv6_ext_frag: packet buffer full, canceling fragmentation\n"); + gnrc_pktbuf_release(to_send); + _snd_buf_free(snd_buf); + return; + } + assert(snd_buf->pkt->next == ptr); /* we just created it with mark */ + snd_buf->pkt->next = snd_buf->pkt->next->next; + } + ptr->next = NULL; + last->next = ptr; + last = ptr; + remaining -= ptr->size; + snd_buf->offset += ptr->size; + } + assert(len != NULL); + /* adapt IPv6 header length field */ + *len = byteorder_htons(gnrc_pkt_len(to_send->next->next)); + /* tell gnrc_ipv6 to send the above prepared fragment */ + msg.type = GNRC_IPV6_EXT_FRAG_SEND; + msg.content.ptr = to_send; + msg_try_send(&msg, gnrc_ipv6_pid); + if (last_fragment) { + /* last fragment => we don't need the send buffer anymore. + * But as we just sent it to gnrc_ipv6 we still need the packet + * allocated, so not _snd_buf_free()! */ + _snd_buf_del(snd_buf); + } + else { + /* tell gnrc_ipv6 to continue fragmenting the datagram in snd_buf + * later */ + msg.type = GNRC_IPV6_EXT_FRAG_CONTINUE; + msg.content.ptr = snd_buf; + msg_try_send(&msg, gnrc_ipv6_pid); + } +} + +static gnrc_ipv6_ext_frag_send_t *_snd_buf_alloc(void) +{ + for (unsigned i = 0; i < GNRC_IPV6_EXT_FRAG_SEND_SIZE; i++) { + gnrc_ipv6_ext_frag_send_t *snd_buf = &_snd_bufs[i]; + if (snd_buf->pkt == NULL) { + return snd_buf; + } + } + return NULL; +} + +static void _snd_buf_del(gnrc_ipv6_ext_frag_send_t *snd_buf) +{ + snd_buf->per_frag = NULL; + snd_buf->pkt = NULL; +} + +static void _snd_buf_free(gnrc_ipv6_ext_frag_send_t *snd_buf) +{ + if (snd_buf->per_frag) { + gnrc_pktbuf_release(snd_buf->per_frag); + } + if (snd_buf->pkt) { + gnrc_pktbuf_release(snd_buf->pkt); + } + _snd_buf_del(snd_buf); +} + +static gnrc_pktsnip_t *_determine_last_per_frag(gnrc_pktsnip_t *ptr) +{ + gnrc_pktsnip_t *last_per_frag = NULL; + unsigned nh = PROTNUM_RESERVED; + + /* ignore NETIF header */ + ptr = ptr->next; + while (ptr) { + switch (ptr->type) { + case GNRC_NETTYPE_IPV6: { + ipv6_hdr_t *hdr = ptr->data; + last_per_frag = ptr; + nh = hdr->nh; + break; + } + case GNRC_NETTYPE_IPV6_EXT: { + ipv6_ext_t *hdr = ptr->data; + switch (nh) { + /* "[...] that is, all headers up to and including the + * Routing header if present, else the Hop-by-Hop Options + * header if present, [...]" + * (IPv6 header comes before Hop-by-Hop Options comes before + * Routing header, so an override to keep the quoted + * priorities is ensured) */ + case PROTNUM_IPV6_EXT_HOPOPT: + case PROTNUM_IPV6_EXT_RH: + last_per_frag = ptr; + break; + default: + break; + } + nh = hdr->nh; + break; + } + default: + assert(last_per_frag != NULL); + return last_per_frag; + } + ptr = ptr->next; + } + /* should not be reached */ + assert(false); + return NULL; +} + /* * =============== * IPv6 reassembly diff --git a/sys/net/gnrc/network_layer/ipv6/gnrc_ipv6.c b/sys/net/gnrc/network_layer/ipv6/gnrc_ipv6.c index 886088b59735..171309e5a3fa 100644 --- a/sys/net/gnrc/network_layer/ipv6/gnrc_ipv6.c +++ b/sys/net/gnrc/network_layer/ipv6/gnrc_ipv6.c @@ -73,6 +73,10 @@ static void _receive(gnrc_pktsnip_t *pkt); * prep_hdr: prepare header for sending (call to _fill_ipv6_hdr()), otherwise * assume it is already prepared */ static void _send(gnrc_pktsnip_t *pkt, bool prep_hdr); + +#ifdef MODULE_GNRC_IPV6_EXT_FRAG +static void _send_by_netif_hdr(gnrc_pktsnip_t *pkt); +#endif /* MODULE_GNRC_IPV6_EXT_FRAG */ /* Main event loop for IPv6 */ static void *_event_loop(void *args); @@ -212,6 +216,14 @@ static void *_event_loop(void *args) case GNRC_IPV6_EXT_FRAG_RBUF_GC: gnrc_ipv6_ext_frag_rbuf_gc(); break; + case GNRC_IPV6_EXT_FRAG_CONTINUE: + DEBUG("ipv6: continue fragmenting packet\n"); + gnrc_ipv6_ext_frag_send(msg.content.ptr); + break; + case GNRC_IPV6_EXT_FRAG_SEND: + DEBUG("ipv6: send fragment\n"); + _send_by_netif_hdr(msg.content.ptr); + break; #endif /* MODULE_GNRC_IPV6_EXT_FRAG */ case GNRC_IPV6_NIB_SND_UC_NS: case GNRC_IPV6_NIB_SND_MC_NS: @@ -435,6 +447,38 @@ static bool _safe_fill_ipv6_hdr(gnrc_netif_t *netif, gnrc_pktsnip_t *pkt, } /* functions for sending */ +static bool _fragment_pkt_if_needed(gnrc_pktsnip_t *pkt, + gnrc_netif_t *netif, + bool from_me) +{ +#ifdef MODULE_GNRC_IPV6_EXT_FRAG + /* TODO: get path MTU when PMTU discovery is implemented */ + unsigned path_mtu = netif->ipv6.mtu; + + if (from_me && (gnrc_pkt_len(pkt->next) > path_mtu)) { + gnrc_netif_hdr_t *hdr = pkt->data; + hdr->if_pid = netif->pid; + gnrc_ipv6_ext_frag_send_pkt(pkt, path_mtu); + return true; + } +#else /* MODULE_GNRC_IPV6_EXT_FRAG */ + (void)pkt; + (void)netif; + (void)from_me; +#endif /* MODULE_GNRC_IPV6_EXT_FRAG */ + return false; +} + +#ifdef MODULE_GNRC_IPV6_EXT_FRAG +static void _send_by_netif_hdr(gnrc_pktsnip_t *pkt) +{ + assert(pkt->type == GNRC_NETTYPE_NETIF); + gnrc_netif_t *netif = gnrc_netif_hdr_get_netif(pkt->data); + + _send_to_iface(netif, pkt); +} +#endif /* MODULE_GNRC_IPV6_EXT_FRAG */ + static void _send_unicast(gnrc_pktsnip_t *pkt, bool prep_hdr, gnrc_netif_t *netif, ipv6_hdr_t *ipv6_hdr, uint8_t netif_hdr_flags) @@ -457,6 +501,11 @@ static void _send_unicast(gnrc_pktsnip_t *pkt, bool prep_hdr, netif_hdr_flags)) == NULL) { return; } + /* prep_hdr => The packet is from me */ + if (_fragment_pkt_if_needed(pkt, netif, prep_hdr)) { + DEBUG("ipv6: packet is fragmented\n"); + return; + } DEBUG("ipv6: send unicast over interface %" PRIkernel_pid "\n", netif->pid); /* and send to interface */ @@ -468,6 +517,7 @@ static void _send_unicast(gnrc_pktsnip_t *pkt, bool prep_hdr, } static inline void _send_multicast_over_iface(gnrc_pktsnip_t *pkt, + bool prep_hdr, gnrc_netif_t *netif, uint8_t netif_hdr_flags) { @@ -476,6 +526,11 @@ static inline void _send_multicast_over_iface(gnrc_pktsnip_t *pkt, GNRC_NETIF_HDR_FLAGS_MULTICAST)) == NULL) { return; } + /* prep_hdr => The packet is from me */ + if (_fragment_pkt_if_needed(pkt, netif, prep_hdr)) { + DEBUG("ipv6: packet is fragmented\n"); + return; + } DEBUG("ipv6: send multicast over interface %" PRIkernel_pid "\n", netif->pid); #ifdef MODULE_NETSTATS_IPV6 netif->ipv6.stats.tx_mcast_count++; @@ -528,12 +583,12 @@ static void _send_multicast(gnrc_pktsnip_t *pkt, bool prep_hdr, return; } } - _send_multicast_over_iface(pkt, netif, netif_hdr_flags); + _send_multicast_over_iface(pkt, prep_hdr, netif, netif_hdr_flags); } } else { if (_safe_fill_ipv6_hdr(netif, pkt, prep_hdr)) { - _send_multicast_over_iface(pkt, netif, netif_hdr_flags); + _send_multicast_over_iface(pkt, prep_hdr, netif, netif_hdr_flags); } } #else /* GNRC_NETIF_NUMOF */ @@ -547,7 +602,7 @@ static void _send_multicast(gnrc_pktsnip_t *pkt, bool prep_hdr, } } if (_safe_fill_ipv6_hdr(netif, pkt, prep_hdr)) { - _send_multicast_over_iface(pkt, netif, netif_hdr_flags); + _send_multicast_over_iface(pkt, prep_hdr, netif, netif_hdr_flags); } #endif /* GNRC_NETIF_NUMOF */ } diff --git a/tests/gnrc_ipv6_ext_frag/Makefile b/tests/gnrc_ipv6_ext_frag/Makefile index 68fcb1fc63b4..fa4feb503a92 100644 --- a/tests/gnrc_ipv6_ext_frag/Makefile +++ b/tests/gnrc_ipv6_ext_frag/Makefile @@ -3,12 +3,17 @@ DEVELHELP := 1 include ../Makefile.tests_common BOARD_INSUFFICIENT_MEMORY := arduino-duemilanove arduino-leonardo \ - arduino-mega2560 arduino-nano arduino-uno chronos \ + arduino-mega2560 arduino-nano arduino-uno \ + blackpill bluepill hifive1 hifive1b \ i-nucleo-lrwan1 mega-xplained msb-430 msb-430h \ nucleo-f030r8 nucleo-f031k6 nucleo-f042k6 \ + nucleo-f070rb nucleo-f072rb nucleo-f302r8 \ nucleo-f303k8 nucleo-f334r8 nucleo-l031k6 \ - nucleo-l053r8 stm32f0discovery stm32l0538-disco \ - telosb waspmote-pro wsn430-v1_3b wsn430-v1_4 z1 + nucleo-l053r8 saml10-xpro saml11-xpro \ + stm32f0discovery stm32l0538-disco telosb \ + waspmote-pro wsn430-v1_3b wsn430-v1_4 z1 +# chronos, hamilton, ruuvitag, and thingy52 boards don't support ethos +BOARD_BLACKLIST := chronos hamilton ruuvitag thingy52 export TAP ?= tap0 @@ -16,15 +21,22 @@ CFLAGS += -DOUTPUT=TEXT CFLAGS += -DTEST_SUITES="gnrc_ipv6_ext_frag" CFLAGS += -DGNRC_IPV6_EXT_FRAG_LIMITS_POOL_SIZE=3 -# use Ethernet as link-layer protocol for native -# The only current general option for non-native boards, ethos, performs poorly -# with the rapidly sent, large packets sent by the Linux kernel. ifeq (native,$(BOARD)) USEMODULE += netdev_tap TERMFLAGS ?= $(TAP) +else + USEMODULE += stdio_ethos - USEMODULE += auto_init_gnrc_netif + ETHOS_BAUDRATE ?= 115200 + CFLAGS += -DETHOS_BAUDRATE=$(ETHOS_BAUDRATE) + TERMDEPS += ethos + TERMPROG ?= sudo $(RIOTTOOLS)/ethos/ethos + TERMFLAGS ?= $(TAP) $(PORT) $(ETHOS_BAUDRATE) endif +USEMODULE += auto_init_gnrc_netif +# add dummy interface to test forwarding to smaller MTU +USEMODULE += netdev_test +GNRC_NETIF_NUMOF := 2 # Specify the mandatory networking modules for IPv6 USEMODULE += gnrc_ipv6_router_default USEMODULE += gnrc_icmpv6_error @@ -42,8 +54,13 @@ USEMODULE += shell USEMODULE += shell_commands USEMODULE += ps -# native requires sudo for the `scapy` tests, but those are not executed for -# non-native boards -TEST_ON_CI_BLACKLIST += native +# The test requires some setup and to be run as root +# So it cannot currently be run +TEST_ON_CI_BLACKLIST += all + +.PHONY: ethos + +ethos: + $(Q)env -u CC -u CFLAGS make -C $(RIOTTOOLS)/ethos include $(RIOTBASE)/Makefile.include diff --git a/tests/gnrc_ipv6_ext_frag/main.c b/tests/gnrc_ipv6_ext_frag/main.c index a41fba7560eb..605072be7e11 100644 --- a/tests/gnrc_ipv6_ext_frag/main.c +++ b/tests/gnrc_ipv6_ext_frag/main.c @@ -20,18 +20,31 @@ */ #include +#include +#include #include "byteorder.h" #include "clist.h" #include "embUnit.h" +#include "net/ipv6/addr.h" #include "net/ipv6/ext/frag.h" #include "net/protnum.h" #include "net/gnrc.h" #include "net/gnrc/ipv6/ext.h" #include "net/gnrc/ipv6/ext/frag.h" #include "net/gnrc/ipv6/hdr.h" +#include "net/gnrc/ipv6/nib.h" +#include "net/gnrc/netif/raw.h" +#include "net/gnrc/udp.h" +#include "net/netdev_test.h" +#include "od.h" +#include "random.h" #include "shell.h" +#include "xtimer.h" +#define TEST_SAMPLE "This is a test. Failure might sometimes be an " \ + "option, but not today. " +#define TEST_PORT (20908U) #define TEST_FRAG1 { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ 0xab, 0xcf, 0xde, 0xb8, 0x18, 0x48, 0xe3, 0x70, \ 0x30, 0x1a, 0xba, 0x27, 0xa6, 0xa7, 0xce, 0xeb, \ @@ -63,10 +76,18 @@ #define TEST_HL (64U) extern int udp_cmd(int argc, char **argv); - +/* shell_test_cmd is used to test weird snip configurations, + * the rest can just use udp_cmd */ +static int shell_test_cmd(int argc, char **argv); + +static netdev_test_t mock_netdev; +static gnrc_netif_t *eth_netif, *mock_netif; +static ipv6_addr_t *local_addr; +static char mock_netif_stack[THREAD_STACKSIZE_DEFAULT]; static char line_buf[SHELL_DEFAULT_BUFSIZE]; static const shell_command_t shell_commands[] = { { "udp", "send data over UDP and listen on UDP ports", udp_cmd }, + { "test", "sends data according to a specified numeric test", shell_test_cmd }, { NULL, NULL, NULL } }; @@ -443,9 +464,174 @@ static void run_unittests(void) TESTS_END(); } +static gnrc_pktsnip_t *_build_udp_packet(const ipv6_addr_t *dst, + unsigned payload_size, + gnrc_pktsnip_t *payload) +{ + udp_hdr_t *udp_hdr; + ipv6_hdr_t *ipv6_hdr; + gnrc_netif_hdr_t *netif_hdr; + gnrc_pktsnip_t *hdr; + + if (payload == NULL) { + uint8_t *data; + + payload = gnrc_pktbuf_add(NULL, NULL, payload_size, GNRC_NETTYPE_UNDEF); + if (payload == NULL) { + return NULL; + } + data = payload->data; + while (payload_size) { + unsigned test_sample_len = sizeof(TEST_SAMPLE) - 1; + + if (test_sample_len > payload_size) { + test_sample_len = payload_size; + } + + memcpy(data, TEST_SAMPLE, test_sample_len); + data += test_sample_len; + payload_size -= test_sample_len; + } + } + hdr = gnrc_udp_hdr_build(payload, TEST_PORT, TEST_PORT); + if (hdr == NULL) { + gnrc_pktbuf_release(payload); + return NULL; + } + udp_hdr = hdr->data; + udp_hdr->length = byteorder_htons(gnrc_pkt_len(hdr)); + payload = hdr; + hdr = gnrc_ipv6_hdr_build(payload, local_addr, dst); + if (hdr == NULL) { + gnrc_pktbuf_release(payload); + return NULL; + } + ipv6_hdr = hdr->data; + ipv6_hdr->len = byteorder_htons(gnrc_pkt_len(payload)); + ipv6_hdr->nh = PROTNUM_UDP; + ipv6_hdr->hl = GNRC_NETIF_DEFAULT_HL; + gnrc_udp_calc_csum(payload, hdr); + payload = hdr; + hdr = gnrc_netif_hdr_build(NULL, 0, NULL, 0); + if (hdr == NULL) { + gnrc_pktbuf_release(payload); + return NULL; + } + netif_hdr = hdr->data; + netif_hdr->if_pid = eth_netif->pid; + netif_hdr->flags |= GNRC_NETIF_HDR_FLAGS_MULTICAST; + hdr->next = payload; + return hdr; +} + +static void test_ipv6_ext_frag_send_pkt_single_frag(const ipv6_addr_t *dst) +{ + gnrc_pktsnip_t *pkt; + + TEST_ASSERT_NOT_NULL(local_addr); + pkt = _build_udp_packet(dst, sizeof(TEST_SAMPLE) - 1, NULL); + TEST_ASSERT_NOT_NULL(pkt); + gnrc_ipv6_ext_frag_send_pkt(pkt, eth_netif->ipv6.mtu); +} + +static void test_ipv6_ext_frag_payload_snips_not_divisible_of_8(const ipv6_addr_t *dst) +{ + gnrc_pktsnip_t *pkt, *payload = NULL; + unsigned payload_size = 0; + + TEST_ASSERT_NOT_NULL(local_addr); + /* TEST_SAMPLE's string length is not a multiple of 8*/ + TEST_ASSERT((sizeof(TEST_SAMPLE) - 1) & 0x7); + + while (payload_size <= eth_netif->ipv6.mtu) { + pkt = gnrc_pktbuf_add(payload, TEST_SAMPLE, sizeof(TEST_SAMPLE) - 1, + GNRC_NETTYPE_UNDEF); + TEST_ASSERT_NOT_NULL(pkt); + payload_size += pkt->size; + payload = pkt; + } + pkt = _build_udp_packet(dst, 0, payload); + TEST_ASSERT_NOT_NULL(pkt); + gnrc_ipv6_ext_frag_send_pkt(pkt, eth_netif->ipv6.mtu); +} + +static int shell_test_cmd(int argc, char **argv) +{ + static ipv6_addr_t dst; + static void (* const _shell_tests[])(const ipv6_addr_t *) = { + test_ipv6_ext_frag_send_pkt_single_frag, + test_ipv6_ext_frag_payload_snips_not_divisible_of_8, + }; + int test_num; + + if ((argc < 3) || (ipv6_addr_from_str(&dst, argv[1]) == NULL)) { + puts("usage: test []"); + return 1; + } + test_num = atoi(argv[2]); + if ((unsigned)test_num >= ARRAY_SIZE(_shell_tests)) { + printf(" must be between 0 and %u\n", + (unsigned)ARRAY_SIZE(_shell_tests) - 1); + return 1; + } + printf("Running test %d\n", test_num); + _shell_tests[test_num](&dst); + return 0; +} + +/* TODO: test if forwarded packet is not fragmented */ + +static int mock_get_device_type(netdev_t *dev, void *value, size_t max_len) +{ + (void)dev; + assert(max_len == sizeof(uint16_t)); + *((uint16_t *)value) = NETDEV_TYPE_TEST; + return sizeof(uint16_t); +} + +static int mock_get_max_packet_size(netdev_t *dev, void *value, size_t max_len) +{ + (void)dev; + assert(max_len == sizeof(uint16_t)); + assert(eth_netif != NULL); + *((uint16_t *)value) = eth_netif->ipv6.mtu - 8; + return sizeof(uint16_t); +} + +static int mock_send(netdev_t *dev, const iolist_t *iolist) +{ + (void)dev; + int res = 0; + while(iolist != NULL) { + od_hex_dump(iolist->iol_base, iolist->iol_len, + OD_WIDTH_DEFAULT); + res += iolist->iol_len; + iolist = iolist->iol_next; + } + return res; +} + int main(void) { + eth_netif = gnrc_netif_iter(NULL); + /* create mock netif to test forwarding too large fragments */ + netdev_test_setup(&mock_netdev, 0); + netdev_test_set_get_cb(&mock_netdev, NETOPT_DEVICE_TYPE, + mock_get_device_type); + netdev_test_set_get_cb(&mock_netdev, NETOPT_MAX_PDU_SIZE, + mock_get_max_packet_size); + netdev_test_set_send_cb(&mock_netdev, mock_send); + mock_netif = gnrc_netif_raw_create(mock_netif_stack, + sizeof(mock_netif_stack), + GNRC_NETIF_PRIO, "mock_netif", + (netdev_t *)&mock_netdev); run_unittests(); + printf("Sending UDP test packets to port %u\n", TEST_PORT); + for (unsigned i = 0; i < GNRC_NETIF_IPV6_ADDRS_NUMOF; i++) { + if (ipv6_addr_is_link_local(ð_netif->ipv6.addrs[i])) { + local_addr = ð_netif->ipv6.addrs[i]; + } + } shell_run(shell_commands, line_buf, SHELL_DEFAULT_BUFSIZE); return 0; } diff --git a/tests/gnrc_ipv6_ext_frag/tests/01-run.py b/tests/gnrc_ipv6_ext_frag/tests/01-run.py index cfde1c861589..8fa7c1341fe6 100755 --- a/tests/gnrc_ipv6_ext_frag/tests/01-run.py +++ b/tests/gnrc_ipv6_ext_frag/tests/01-run.py @@ -14,10 +14,14 @@ import subprocess import time -from scapy.all import Ether, IPv6, IPv6ExtHdrFragment, sendp +from scapy.all import Ether, ICMPv6PacketTooBig, IPv6, IPv6ExtHdrFragment, \ + UDP, raw, sendp, srp1 from testrunner import run +RECV_BUFSIZE = 2 * 1500 +TEST_SAMPLE = b"This is a test. Failure might sometimes be an option, but " \ + b"not today. " EXT_HDR_NH = { IPv6ExtHdrFragment: 44, } @@ -54,6 +58,13 @@ def stop_udp_server(child): "Error: server was not running"]) +def udp_send(child, addr, port, length, num=1, delay=1000000): + child.sendline("udp send {addr}%6 {port} {length} {num} {delay}" + .format(**vars())) + child.expect("Success: send {length} byte to \[[0-9a-f:]+\]:{port}" + .format(**vars())) + + def check_and_search_output(cmd, pattern, res_group, *args, **kwargs): output = subprocess.check_output(cmd, *args, **kwargs).decode("utf-8") for line in output.splitlines(): @@ -145,6 +156,165 @@ def test_reass_offset_too_large(child, iface, hw_dst, ll_dst, ll_src): pktbuf_empty(child) +def test_ipv6_ext_frag_shell_test_0(child, s, iface, ll_dst): + child.sendline("test {} 0".format(ll_dst)) + data, _ = s.recvfrom(RECV_BUFSIZE) + assert data == TEST_SAMPLE + pktbuf_empty(child) + + +def test_ipv6_ext_frag_shell_test_1(child, s, iface, ll_dst): + child.sendline("test {} 1".format(ll_dst)) + data, _ = s.recvfrom(RECV_BUFSIZE) + offset = 0 + while (offset < len(data)): + assert data[offset:(offset + len(TEST_SAMPLE))] == TEST_SAMPLE + offset += len(TEST_SAMPLE) + pktbuf_empty(child) + + +def test_ipv6_ext_frag_send_success(child, s, iface, ll_dst): + length = get_host_mtu(iface) + port = s.getsockname()[1] + udp_send(child, ll_dst, port, length) + data, _ = s.recvfrom(length) + assert len(data) == length + pktbuf_empty(child) + + +def test_ipv6_ext_frag_send_last_fragment_filled(child, s, iface, ll_dst): + # every fragment has an IPv6 header and a fragmentation header so subtract + # them + mtu = get_host_mtu(iface) - len(IPv6() / IPv6ExtHdrFragment()) + # first fragment has UDP header (so subtract it) and is rounded down to + # the nearest multiple of 8 + length = (mtu - len(UDP())) & 0xfff8 + # second fragment fills the whole available MTU + length += mtu + port = s.getsockname()[1] + udp_send(child, ll_dst, port, length) + data, _ = s.recvfrom(length) + assert len(data) == length + pktbuf_empty(child) + + +def test_ipv6_ext_frag_send_last_fragment_only_one_byte(child, s, + iface, ll_dst): + mtu = get_host_mtu(iface) + # subtract IPv6 and UDP header as they are not part of the UDP payload + length = (mtu - len(IPv6() / UDP())) + length += 1 + port = s.getsockname()[1] + udp_send(child, ll_dst, port, length) + data, _ = s.recvfrom(length) + assert len(data) == length + pktbuf_empty(child) + + +def test_ipv6_ext_frag_send_full_pktbuf(child, s, iface, ll_dst): + length = pktbuf_size(child) + # remove some slack for meta-data and header and 1 addition fragment header + length -= (len(IPv6() / IPv6ExtHdrFragment() / UDP()) + + (len(IPv6() / IPv6ExtHdrFragment())) + 96) + port = s.getsockname()[1] + # trigger neighbor discovery so it doesn't fill the packet buffer + udp_send(child, ll_dst, port, 1) + data, _ = s.recvfrom(1) + last_nd = time.time() + count = 0 + while True: + if (time.time() - last_nd) > 5: + # trigger neighbor discovery so it doesn't fill the packet buffer + udp_send(child, ll_dst, port, 1) + data, _ = s.recvfrom(1) + last_nd = time.time() + udp_send(child, ll_dst, port, length) + count += 1 + try: + data, _ = s.recvfrom(length) + except socket.timeout: + # 8 is the alignment unit of the packet buffer + # and 20 the size of a packet snip, so take next multiple of 8 to + # 28 + length -= 24 + else: + break + finally: + pktbuf_empty(child) + assert(count > 1) + + +def _fwd_setup(child, ll_dst, g_src, g_dst): + # check if interface is configured properly + child.sendline("ifconfig 7") + child.expect(r"MTU:(\d+)") + mtu = int(child.match.group(1)) + # configure routes + child.sendline("nib route add 7 {}/128 fe80::1".format(g_dst)) + child.sendline("nib route add 6 {}/128 {}".format(g_src, ll_dst)) + child.sendline("nib route") + child.expect(r"{}/128 via fe80::1 dev #7".format(g_dst)) + child.expect(r"{}/128 via {} dev #6".format(g_src, ll_dst)) + child.sendline("nib neigh add 7 fe80::1") + child.sendline("nib neigh") + child.expect(r"fe80::1 dev #7 lladdr\s+-") + # get TAP MAC address + child.sendline("ifconfig 6") + child.expect("HWaddr: ([0-9A-F:]+)") + hwaddr = child.match.group(1) + # consume MTU for later calls of `ifconfig 7` + child.expect(r"MTU:(\d+)") + return mtu, hwaddr + + +def _fwd_teardown(child): + # remove route + child.sendline("nib neigh del 7 fe80::1") + child.sendline("nib route del 7 affe::/64") + + +def test_ipv6_ext_frag_fwd_success(child, s, iface, ll_dst): + mtu, dst_mac = _fwd_setup(child, ll_dst, "beef::1", "affe::1") + payload_fit = mtu - len(IPv6() / IPv6ExtHdrFragment() / UDP()) + pkt = Ether(dst=dst_mac) / IPv6(src="beef::1", dst="affe::1") / \ + IPv6ExtHdrFragment(m=True, id=0x477384a9) / \ + UDP(sport=1337, dport=1337) / ("x" * payload_fit) + # fill missing fields + pkt = Ether(raw(pkt)) + sendp(pkt, verbose=0, iface=iface) + # check hexdump of mock device + ipv6 = pkt[IPv6] + ipv6.hlim -= 1 # the packet will have passed a hop + # segment packet as GNRC does + segments = [bytes(ipv6)[:40], bytes(ipv6.payload)] + for seg in segments: + addr = 0 + for i in range(0, len(seg), 16): + bs = seg[i:i+16] + exp_str = ("{:08X}" + (" {:02X}") * len(bs)).format(addr, *bs) + child.expect_exact(exp_str) + addr += 16 + _fwd_teardown(child) + + +def test_ipv6_ext_frag_fwd_too_big(child, s, iface, ll_dst): + mtu, dst_mac = _fwd_setup(child, ll_dst, "beef::1", "affe::1") + assert(get_host_mtu(iface) > mtu) + payload_fit = get_host_mtu(iface) - len(IPv6() / IPv6ExtHdrFragment() / + UDP()) + pkt = srp1(Ether(dst=dst_mac) / IPv6(src="beef::1", dst="affe::1") / + IPv6ExtHdrFragment(m=True, id=0x477384a9) / + UDP(sport=1337, dport=1337) / ("x" * payload_fit), + timeout=2, verbose=0, iface=iface) + # packet should not be fragmented further but an ICMPv6 error should be + # returned instead + assert(pkt is not None) + assert(ICMPv6PacketTooBig in pkt) + assert(IPv6ExtHdrFragment in pkt) + assert(pkt[IPv6ExtHdrFragment].id == 0x477384a9) + _fwd_teardown(child) + + def testfunc(child): tap = get_bridge(os.environ["TAP"]) @@ -152,12 +322,45 @@ def testfunc(child): print("." * int(child.match.group(1)), end="", flush=True) lladdr_src = get_host_lladdr(tap) + + def run_sock_test(func, s): + if child.logfile == sys.stdout: + func(child, s, tap, lladdr_src) + else: + try: + func(child, s, tap, lladdr_src) + print(".", end="", flush=True) + except PermissionError: + print("\n\x1b[1;33mSkipping {} because of missing " + "privileges\x1b[0m".format(func.__name__)) + except Exception as e: + print("FAILED") + raise e + + child.expect(r"Sending UDP test packets to port (\d+)") + + port = int(child.match.group(1)) + with socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) as s: + res = socket.getaddrinfo("{}%{}".format(lladdr_src, tap), port) + s.bind(res[0][4]) + s.settimeout(.3) + run_sock_test(test_ipv6_ext_frag_shell_test_0, s) + run_sock_test(test_ipv6_ext_frag_shell_test_1, s) + run_sock_test(test_ipv6_ext_frag_send_success, s) + run_sock_test(test_ipv6_ext_frag_send_last_fragment_filled, s) + run_sock_test(test_ipv6_ext_frag_send_last_fragment_only_one_byte, s) + run_sock_test(test_ipv6_ext_frag_send_full_pktbuf, s) + run_sock_test(test_ipv6_ext_frag_fwd_success, s) + run_sock_test(test_ipv6_ext_frag_fwd_too_big, s) + if os.environ.get("BOARD", "") != "native": # ethos currently can't handle the larger, rapidly sent packets by the # IPv6 fragmentation of the Linux Kernel - print("SUCCESS for unittests.") - print("Skipping interaction tests due to ethos bug.") + print("SUCCESS") + print("Skipping datagram reception tests due to ethos bug.") return + + # datagram reception tests res = 1 count = 0 while res: @@ -197,4 +400,9 @@ def run(func): if __name__ == "__main__": - sys.exit(run(testfunc, timeout=1, echo=False)) + if os.geteuid() != 0: + print("\x1b[1;31mThis test requires root privileges.\n" + "It's constructing and sending Ethernet frames.\x1b[0m\n", + file=sys.stderr) + sys.exit(1) + sys.exit(run(testfunc, timeout=2, echo=False))