diff --git a/Makefile.dep b/Makefile.dep index ee10ee9edf..b092f3ecd6 100644 --- a/Makefile.dep +++ b/Makefile.dep @@ -330,6 +330,10 @@ ifneq (,$(filter gnrc_ipv6_ext_frag,$(USEMODULE))) USEMODULE += xtimer endif +ifneq (,$(filter gnrc_ipv6_ext_opt,$(USEMODULE))) + USEMODULE += gnrc_ipv6_ext +endif + ifneq (,$(filter gnrc_ipv6_ext_rh,$(USEMODULE))) USEMODULE += gnrc_ipv6_ext endif diff --git a/sys/include/net/gnrc/ipv6/ext/opt.h b/sys/include/net/gnrc/ipv6/ext/opt.h new file mode 100644 index 0000000000..02fbcc2c43 --- /dev/null +++ b/sys/include/net/gnrc/ipv6/ext/opt.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2020 Freie Universität Berlin + * + * 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_ext_opt Support for IPv6 option extension headers + * @ingroup net_gnrc_ipv6_ext + * @brief GNRC implementation of IPv6 hop-by-hop and destination option + * header extension + * @{ + * + * @file + * @brief GNRC hop-by-hop and destination option header definitions. + * + * @author Martine Lenders + */ +#ifndef NET_GNRC_IPV6_EXT_OPT_H +#define NET_GNRC_IPV6_EXT_OPT_H + +#include + +#include "net/gnrc/pkt.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Processes all options within an IPv6 option header + * + * @pre `pkt != NULL` + * @pre `(protnum == PROTNUM_IPV6_EXT_HOPOPT) || (protnum == PROTNUM_IPV6_EXT_DST)` + * + * @param[in] pkt The packet containing the option header. The option + * must be contained in the first snip, with all + * preceding headers marked (in receive order). + * Must not be NULL. + * @param[in] protnum The protocol number of the option header. Must be + * @ref PROTNUM_IPV6_EXT_HOPOPT or @ref + * PROTNUM_IPV6_EXT_DST + * + * @return @p pkt with the option header marked on success. + * @return NULL, if the packet was consumed by the option handling. + * @return NULL, on error. @p pkt is released with EINVAL in that case and if + * necessary and [`gnrc_icmpv6_error`](@ref net_gnrc_icmpv6_error) is + * used, the according ICMPv6 error message is sent. + */ +gnrc_pktsnip_t *gnrc_ipv6_ext_opt_process(gnrc_pktsnip_t *pkt, + uint8_t protnum); + +#ifdef __cplusplus +} +#endif + +#endif /* NET_GNRC_IPV6_EXT_OPT_H */ +/** @} */ diff --git a/sys/include/net/ipv6/ext/opt.h b/sys/include/net/ipv6/ext/opt.h new file mode 100644 index 0000000000..685ea0b790 --- /dev/null +++ b/sys/include/net/ipv6/ext/opt.h @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2020 Freie Universität Berlin + * + * 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_ipv6_ext_opt IPv6 destination and hop-by-hop options + * @ingroup net_ipv6_ext + * @brief Definitions for IPv6 destination and hop-by-hop options + * extension headers + * @{ + * + * @file + * @brief Destination and hop-by-hop options extension header definitions. + * + * @author Martine Lenders + */ +#ifndef NET_IPV6_EXT_OPT_H +#define NET_IPV6_EXT_OPT_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @name Destination and hop-by-hop option types + * @see [IANA, IPv6 parameters] + * (https://www.iana.org/assignments/ipv6-parameters/ipv6-parameters.xhtml#ipv6-parameters-2) + * @{ + */ +#define IPV6_EXT_OPT_PAD1 (0x00U) /**< Pad1 */ +#define IPV6_EXT_OPT_PADN (0x01U) /**< PadN */ +#define IPV6_EXT_OPT_JUMBO (0xC2U) /**< Jumbo payload */ +#define IPV6_EXT_OPT_RPL (0x63U) /**< RPL Option */ +#define IPV6_EXT_OPT_TEL (0x04U) /**< Tunnel Encapsulation Limit */ +#define IPV6_EXT_OPT_RTR_ALERT (0x05U) /**< Router Alert */ +#define IPV6_EXT_OPT_QUICK_START (0x26U) /**< Quick-Start */ +#define IPV6_EXT_OPT_CALIPSO (0x07U) /**< CALIPSO */ +#define IPV6_EXT_OPT_SMF_DPD (0x08U) /**< SMF_DPD */ +#define IPV6_EXT_OPT_HOME_ADDR (0xC9U) /**< Home Address */ +#define IPV6_EXT_OPT_ILNP_NONCE (0x8BU) /**< ILNP Nonce */ +#define IPV6_EXT_OPT_LIO (0x8CU) /**< Line-Identification Option */ +#define IPV6_EXT_OPT_MPL (0x6DU) /**< MPL Option */ +#define IPV6_EXT_OPT_IP_DFF (0xEEU) /**< IP_DFF */ +#define IPV6_EXT_OPT_PDM (0x0FU) /**< Performance and Diagnostic Metrics */ +/** @} */ + +/** + * @name Processing actions + * @see [RFC 8200, section 4.2](https://tools.ietf.org/html/rfc8200#section-4.2) + * + * > The Option Type identifiers are internally encoded such that their + * > highest-order 2 bits specify the action that must be taken if the + * > processing IPv6 node does not recognize the Option Type + * @{ + */ +/** + * @brief mask to decode action from type + */ +#define IPV6_EXT_OPT_ACTION_MASK (0xc0) +/** + * @brief skip over this option and continue processing the header + */ +#define IPV6_EXT_OPT_ACTION_SKIP (0x00) +#define IPV6_EXT_OPT_ACTION_DISC (0x40) /**< discard the packet */ +/** + * @brief discard the packet + * + * > and, regardless of whether or not the packet's Destination Address + * > was a multicast address, send an ICMP Parameter Problem, Code 2, + * > message to the packet's Source Address, pointing to the + * > unrecognized Option Type. + */ +#define IPV6_EXT_OPT_ACTION_DISC_ERR_MCAST (0x80) + +/** + * @brief discard the packet + * + * > and, only if the packet's Destination Address was not a multicast + * > address, send an ICMP Parameter Problem, Code 2, message to the + * > packet's Source Address, pointing to the unrecognized Option Type. + */ +#define IPV6_EXT_OPT_ACTION_DISC_ERR (0xc0) +/** @} */ + +#ifdef __cplusplus +} +#endif + +#endif /* NET_IPV6_EXT_OPT_H */ +/** @} */ diff --git a/sys/net/gnrc/Makefile b/sys/net/gnrc/Makefile index 6b6c1f3eda..dfd7cb3af2 100644 --- a/sys/net/gnrc/Makefile +++ b/sys/net/gnrc/Makefile @@ -19,6 +19,9 @@ endif ifneq (,$(filter gnrc_ipv6_ext_frag,$(USEMODULE))) DIRS += network_layer/ipv6/ext/frag endif +ifneq (,$(filter gnrc_ipv6_ext_opt,$(USEMODULE))) + DIRS += network_layer/ipv6/ext/opt +endif ifneq (,$(filter gnrc_ipv6_ext_rh,$(USEMODULE))) DIRS += network_layer/ipv6/ext/rh endif diff --git a/sys/net/gnrc/network_layer/ipv6/ext/gnrc_ipv6_ext.c b/sys/net/gnrc/network_layer/ipv6/ext/gnrc_ipv6_ext.c index 3aaaa90f23..82fdfc18ad 100644 --- a/sys/net/gnrc/network_layer/ipv6/ext/gnrc_ipv6_ext.c +++ b/sys/net/gnrc/network_layer/ipv6/ext/gnrc_ipv6_ext.c @@ -24,6 +24,7 @@ #include "net/gnrc/icmpv6/error.h" #include "net/gnrc/ipv6.h" #include "net/gnrc/ipv6/ext/frag.h" +#include "net/gnrc/ipv6/ext/opt.h" #include "net/gnrc/ipv6/ext/rh.h" #if defined(MODULE_GNRC_SIXLOWPAN_IPHC_NHC) && \ defined(MODULE_GNRC_IPV6_EXT_FRAG) @@ -311,6 +312,10 @@ static gnrc_pktsnip_t *_demux(gnrc_pktsnip_t *pkt, unsigned protnum) #endif /* MODULE_GNRC_IPV6_EXT_FRAG */ case PROTNUM_IPV6_EXT_HOPOPT: case PROTNUM_IPV6_EXT_DST: + if (IS_USED(MODULE_GNRC_IPV6_EXT_OPT)) { + return gnrc_ipv6_ext_opt_process(pkt, protnum); + } + /* Intentionally falls through */ case PROTNUM_IPV6_EXT_AH: case PROTNUM_IPV6_EXT_ESP: case PROTNUM_IPV6_EXT_MOB: diff --git a/sys/net/gnrc/network_layer/ipv6/ext/opt/Makefile b/sys/net/gnrc/network_layer/ipv6/ext/opt/Makefile new file mode 100644 index 0000000000..4df77e0662 --- /dev/null +++ b/sys/net/gnrc/network_layer/ipv6/ext/opt/Makefile @@ -0,0 +1,3 @@ +MODULE := gnrc_ipv6_ext_opt + +include $(RIOTBASE)/Makefile.base diff --git a/sys/net/gnrc/network_layer/ipv6/ext/opt/gnrc_ipv6_ext_opt.c b/sys/net/gnrc/network_layer/ipv6/ext/opt/gnrc_ipv6_ext_opt.c new file mode 100644 index 0000000000..13fd4fbcb1 --- /dev/null +++ b/sys/net/gnrc/network_layer/ipv6/ext/opt/gnrc_ipv6_ext_opt.c @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2020 Freie Universität Berlin + * + * 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. + */ + +/** + * @{ + * + * @file + * @author Martine Lenders + */ + +#include "net/ipv6.h" +#include "net/ipv6/ext.h" +#include "net/ipv6/ext/opt.h" +#include "net/gnrc/icmpv6/error.h" +#include "net/gnrc/pktbuf.h" + +#include "net/gnrc/ipv6/ext/opt.h" + +#define ENABLE_DEBUG (0) +#include "debug.h" + +/** + * @brief Determine what action to do, when option is not recognized + * + * @see https://tools.ietf.org/html/rfc8200#section-4.2 + * + * @param[in] type Type of the option + */ +static inline uint8_t _unrec_action(uint8_t type) +{ + return (type & IPV6_EXT_OPT_ACTION_MASK); +} + +static bool _multicast_dst(gnrc_pktsnip_t *pkt) +{ + gnrc_pktsnip_t *ipv6 = gnrc_pktsnip_search_type(pkt, GNRC_NETTYPE_IPV6); + ipv6_hdr_t *ipv6_hdr; + + assert(ipv6 != NULL); + ipv6_hdr = ipv6->data; + return ipv6_addr_is_multicast(&ipv6_hdr->dst); +} + +gnrc_pktsnip_t *gnrc_ipv6_ext_opt_process(gnrc_pktsnip_t *pkt, + uint8_t protnum) +{ + assert(pkt != NULL); + assert((protnum == PROTNUM_IPV6_EXT_HOPOPT) || + (protnum == PROTNUM_IPV6_EXT_DST)); + gnrc_pktsnip_t *hdr; + ipv6_ext_t *opt_hdr = pkt->data; + uint8_t *opts; + size_t hdr_len; + + if (pkt->size < sizeof(ipv6_ext_t)) { + DEBUG("gnrc_ipv6_ext_opt: packet of invalid size\n"); + goto error; + } + hdr_len = ((opt_hdr->len * IPV6_EXT_LEN_UNIT) + IPV6_EXT_LEN_UNIT); + hdr = gnrc_pktbuf_mark(pkt, hdr_len, GNRC_NETTYPE_IPV6_EXT); + if (hdr == NULL) { + DEBUG("gnrc_ipv6_ext_opt: unable to mark option header\n"); + goto error; + } + opts = hdr->data; + for (unsigned offset = sizeof(ipv6_ext_t); offset < hdr_len;) { + uint8_t opt_type = opts[offset++]; + uint8_t opt_len; + + if (opt_type == IPV6_EXT_OPT_PAD1) { + /* nothing more to do */ + continue; + } + opt_len = opts[offset++]; + if (opt_len > (hdr_len - offset)) { + DEBUG("gnrc_ipv6_ext_opt: invalid option size\n"); + goto error; + } + switch (opt_type) { + /* IPV6_EXT_OPT_PAD1 already handled before length check due + * to special format */ + case IPV6_EXT_OPT_PADN: + /* nothing to do, offset will be progressed below */ + break; + default: { + bool send_error = false; + + switch (_unrec_action(opt_type)) { + case IPV6_EXT_OPT_ACTION_SKIP: + DEBUG("gnrc_ipv6_ext_opt: skipping unknown " + "option %02x\n", opt_type); + /* skip here already, as we don't reach the + * incrementation of offset below */ + offset += opt_len; + continue; + case IPV6_EXT_OPT_ACTION_DISC: + break; + case IPV6_EXT_OPT_ACTION_DISC_ERR_MCAST: + send_error = IS_USED(MODULE_GNRC_ICMPV6_ERROR); + break; + case IPV6_EXT_OPT_ACTION_DISC_ERR: + send_error = IS_USED(MODULE_GNRC_ICMPV6_ERROR) && + !_multicast_dst(pkt); + break; + } + DEBUG("gnrc_ipv6_ext_opt: discarding packet with unknown " + "option %02x\n", opt_type); + if (send_error) { + DEBUG("gnrc_ipv6_ext_opt: reporting parameter problem " + "for option %02x (pos at %02x)\n", opts[offset - 2U], + opt_type); + gnrc_icmpv6_error_param_prob_send( + ICMPV6_ERROR_PARAM_PROB_OPT, + /* offset was already progressed to opt data*/ + &opts[offset - 2U], pkt); + } + goto error; + } + } + offset += opt_len; + } + return pkt; +error: + gnrc_pktbuf_release_error(pkt, EINVAL); + return NULL; +} + +/** @} */ diff --git a/tests/gnrc_ipv6_ext_opt/Makefile b/tests/gnrc_ipv6_ext_opt/Makefile new file mode 100644 index 0000000000..ffb162a43d --- /dev/null +++ b/tests/gnrc_ipv6_ext_opt/Makefile @@ -0,0 +1,39 @@ +DEVELHELP = 1 +# name of your application +include ../Makefile.tests_common + +export TAP ?= tap0 + +# use Ethernet as link-layer protocol +ifeq (native,$(BOARD)) + TERMFLAGS ?= $(TAP) +else + 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 +USEMODULE += gnrc_ipv6_default +USEMODULE += gnrc_icmpv6_error +USEMODULE += gnrc_pktdump +USEMODULE += gnrc_pktbuf_cmd +# IPv6 extension headers +USEMODULE += gnrc_ipv6_ext_opt +USEMODULE += od +# Add also the shell, some shell commands +USEMODULE += shell +USEMODULE += shell_commands +USEMODULE += ps + +# 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_opt/Makefile.board.dep b/tests/gnrc_ipv6_ext_opt/Makefile.board.dep new file mode 100644 index 0000000000..b595b8605c --- /dev/null +++ b/tests/gnrc_ipv6_ext_opt/Makefile.board.dep @@ -0,0 +1,6 @@ +# Put board specific dependencies here +ifeq (native,$(BOARD)) + USEMODULE += netdev_tap +else + USEMODULE += stdio_ethos +endif diff --git a/tests/gnrc_ipv6_ext_opt/Makefile.ci b/tests/gnrc_ipv6_ext_opt/Makefile.ci new file mode 100644 index 0000000000..ee88afc3a3 --- /dev/null +++ b/tests/gnrc_ipv6_ext_opt/Makefile.ci @@ -0,0 +1,30 @@ +BOARD_INSUFFICIENT_MEMORY := \ + arduino-duemilanove \ + arduino-leonardo \ + arduino-mega2560 \ + arduino-nano \ + arduino-uno \ + atmega1284p \ + atmega328p \ + derfmega128 \ + i-nucleo-lrwan1 \ + mega-xplained \ + microduino-corerf \ + msb-430 \ + msb-430h \ + nucleo-f030r8 \ + nucleo-f031k6 \ + nucleo-f042k6 \ + nucleo-f303k8 \ + nucleo-f334r8 \ + nucleo-l031k6 \ + nucleo-l053r8 \ + stm32f030f4-demo \ + stm32f0discovery \ + stm32l0538-disco \ + telosb \ + waspmote-pro \ + wsn430-v1_3b \ + wsn430-v1_4 \ + z1 \ + # diff --git a/tests/gnrc_ipv6_ext_opt/README.md b/tests/gnrc_ipv6_ext_opt/README.md new file mode 100644 index 0000000000..af74945125 --- /dev/null +++ b/tests/gnrc_ipv6_ext_opt/README.md @@ -0,0 +1,33 @@ +# `gnrc_ipv6_ext_opt` test + +This test utilizes [scapy] to test the IPv6 destination and hop-by-hop option +parsing. + +It is intended to just test the basic parsing functionality. For specific +option types please provide a separate test application. + +To test, compile and flash the application to any board of your liking (since +`ethos` is used to communicate with non-native boards it really doesn't matter +as long as the application fits). + +``` +make flash +``` + +And run the tests using + +``` +sudo make test +``` + +Note that root privileges are required since `scapy` needs to construct Ethernet +frames to properly communicate over the TAP interface. + +The tests succeeds if you see the string `SUCCESS`. + +If any problems are encountered (i.e. if the test prints the sting `FAILED`), +set the echo parameter in the `run()` function at the bottom of the test script +(tests/01-run.py) to `True`. The test script will then offer a more detailed +output. + +[scapy]: https://scapy.readthedocs.io/en/latest/ diff --git a/tests/gnrc_ipv6_ext_opt/main.c b/tests/gnrc_ipv6_ext_opt/main.c new file mode 100644 index 0000000000..a205fbb724 --- /dev/null +++ b/tests/gnrc_ipv6_ext_opt/main.c @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2015-2020 Freie Universität Berlin + * + * 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. + */ + +/** + * @ingroup tests + * @{ + * + * @file + * @brief Tests option extension header handling of gnrc stack. + * + * @author Hauke Petersen + * @author Takuo Yonezawa + * @author Martine S. Lenders + * + * @} + */ + +#include + +#include "shell.h" +#include "net/gnrc/pktbuf.h" +#include "net/gnrc/pktdump.h" +#include "net/gnrc/netreg.h" + +static char line_buf[SHELL_DEFAULT_BUFSIZE]; +static gnrc_netreg_entry_t ip_entry = GNRC_NETREG_ENTRY_INIT_PID( + 0, KERNEL_PID_UNDEF + ); + +static inline void _ipreg_usage(char *cmd) +{ + printf("Usage: %s {reg|unreg} ", cmd); +} + +static int _ipreg(int argc, char **argv) +{ + if ((argc > 2) && (strcmp("reg", argv[1]) == 0)) { + uint32_t protnum; + if (ip_entry.target.pid != KERNEL_PID_UNDEF) { + printf("Already registered to protnum %" PRIu32 "\n", + ip_entry.demux_ctx); + return 1; + } + protnum = atoi(argv[2]); + gnrc_netreg_entry_init_pid(&ip_entry, protnum, gnrc_pktdump_pid); + gnrc_netreg_register(GNRC_NETTYPE_IPV6, &ip_entry); + printf("Registered to protocol number %" PRIu32 "\n", protnum); + } + else if ((argc > 1) && (strcmp("unreg", argv[1]) == 0)) { + printf("Unregistered from protocol number %" PRIu32 "\n", + ip_entry.demux_ctx); + gnrc_netreg_unregister(GNRC_NETTYPE_IPV6, &ip_entry); + gnrc_netreg_entry_init_pid(&ip_entry, 0, KERNEL_PID_UNDEF); + } + else { + _ipreg_usage(argv[0]); + return 1; + } + + return 0; +} + +static const shell_command_t shell_commands[] = { + { "ip", "Registers pktdump to a protocol number", _ipreg }, + { NULL, NULL, NULL } +}; + +int main(void) +{ + shell_run(shell_commands, line_buf, SHELL_DEFAULT_BUFSIZE); + return 0; +} diff --git a/tests/gnrc_ipv6_ext_opt/tests/01-run.py b/tests/gnrc_ipv6_ext_opt/tests/01-run.py new file mode 100755 index 0000000000..f581632dab --- /dev/null +++ b/tests/gnrc_ipv6_ext_opt/tests/01-run.py @@ -0,0 +1,619 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2018-2020 Freie Universität Berlin +# +# 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. + +import re +import os +import sys +import subprocess +import threading + +from pexpect import TIMEOUT +from scapy.all import Ether, IPv6, UDP, ICMPv6ParamProblem, \ + IPv6ExtHdrHopByHop, IPv6ExtHdrDestOpt, \ + Pad1, PadN, HBHOptUnknown, \ + sendp, srp1, AsyncSniffer +from testrunner import run + + +EXT_HDR_NH = { + IPv6ExtHdrHopByHop: 0, + UDP: 17, + IPv6ExtHdrDestOpt: 60, +} +TEST_OPTION_TYPES = { + # See experimental options + # https://www.iana.org/assignments/ipv6-parameters/ipv6-parameters.xhtml#ipv6-parameters-2 + "ACTION_SKIP": 0x1E, + "ACTION_DISCARD": 0x5E, + "ACTION_DISCARD_ERROR_MCAST": 0x9E, + "ACTION_DISCARD_ERROR": 0xDE, +} +HW_MCAST = "33:33:00:00:00:01" +MCAST = "ff02::1" +RECV_TIMEOUT = 0.2 + + +class StartCheckAsyncSniffer(AsyncSniffer): + def _cb(self): + self._started.set() + + def __init__(self, *args, **kwargs): + kwargs["started_callback"] = self._cb + self._started = threading.Event() + self._started.clear() + super().__init__(*args, **kwargs) + + def wait_for_started(self, timeout=None): + self._started.wait(timeout) + + +def pktbuf_empty(child): + child.sendline("pktbuf") + child.expect(r"packet buffer: first byte: (?P0x[0-9a-fA-F]+), " + r"last byte: 0x[0-9a-fA-F]+ \(size: (?P\d+)\)") + first_byte = child.match.group("first_byte") + size = child.match.group("size") + child.expect( + r"~ unused: {} \(next: (\(nil\)|0), size: {}\) ~".format( + first_byte, size)) + + +def register_protnum(child, protnum): + child.sendline("ip reg %d" % protnum) + child.expect("Registered to protocol number %d" % protnum) + + +def unregister(child): + child.sendline("ip unreg") + child.expect(r"Unregistered from protocol number \d") + + +def test_empty_hop_by_hop_opt_wo_register(child, iface, hw_dst, ll_dst, ll_src): + # Try sending an empty hop-by-hop-option header + sendp(Ether(dst=hw_dst) / IPv6(dst=ll_dst, src=ll_src) / + IPv6ExtHdrHopByHop() / UDP(), iface=iface, verbose=0) + pktbuf_empty(child) + + +def test_empty_hop_by_hop_opt_w_register(child, iface, hw_dst, ll_dst, ll_src): + # Register to hop-by-hop-option header + register_protnum(child, EXT_HDR_NH[IPv6ExtHdrHopByHop]) + # Try sending an empty hop-by-hop-option header + sendp(Ether(dst=hw_dst) / IPv6(dst=ll_dst, src=ll_src) / + IPv6ExtHdrHopByHop() / UDP() / "\x01\x02", iface=iface, verbose=0) + child.expect(r"~~ SNIP 0 - size:\s+(\d+) byte, type: NETTYPE_\w+ \(\d+\)") + ipv6_payload_len = int(child.match.group(1)) + # NH = 17 (UDP), len = 0x00, PadN option (0x01) of length 0x04 + child.expect(r"00000000 11 00 01 04 00 00 00 00") + child.expect(r"~~ SNIP 1 - size:\s+40 byte, type: NETTYPE_IPV6 \(\d+\)") + child.expect_exact(r"length: {} next header: {}".format( + ipv6_payload_len, EXT_HDR_NH[IPv6ExtHdrHopByHop] + )) + child.expect_exact(r"destination address: {}".format(ll_dst)) + pktbuf_empty(child) + unregister(child) + + +def test_empty_hop_by_hop_opt_large_hdr_len(child, iface, hw_dst, ll_dst, + ll_src): + # Register to hop-by-hop-option header + register_protnum(child, EXT_HDR_NH[IPv6ExtHdrHopByHop]) + # Try sending an empty hop-by-hop-option header with too big header length + sendp(Ether(dst=hw_dst) / IPv6(dst=ll_dst, src=ll_src) / + IPv6ExtHdrHopByHop(len=20) / UDP() / "\x01\x02", + iface=iface, verbose=0) + pktbuf_empty(child) + unregister(child) + + +def test_empty_duplicate_hop_by_hop_opt(child, iface, hw_dst, ll_dst, ll_src): + # Try sending two empty hop-by-hop-option header + p = srp1(Ether(dst=hw_dst) / IPv6(dst=ll_dst, src=ll_src) / + IPv6ExtHdrHopByHop() / IPv6ExtHdrHopByHop() / UDP() / "\x03\x04", + iface=iface, timeout=RECV_TIMEOUT, verbose=0) + # should return parameter problem message + assert p is not None + assert ICMPv6ParamProblem in p + assert p[ICMPv6ParamProblem].code == 1 # unrecognized next header + assert p[ICMPv6ParamProblem].ptr >= 40 # after IPv6 header + pktbuf_empty(child) + + +def test_empty_non_first_hop_by_hop_opt(child, iface, hw_dst, ll_dst, ll_src): + # Try sending empty hop-by-hop-option header after destination option + # header + p = srp1(Ether(dst=hw_dst) / IPv6(dst=ll_dst, src=ll_src) / + IPv6ExtHdrDestOpt() / IPv6ExtHdrHopByHop() / UDP() / "\x05\x06", + iface=iface, timeout=RECV_TIMEOUT, verbose=0) + # should return parameter problem message + assert p is not None + assert ICMPv6ParamProblem in p + assert p[ICMPv6ParamProblem].code == 1 # unrecognized next header + assert p[ICMPv6ParamProblem].ptr >= 40 # after IPv6 header + pktbuf_empty(child) + + +def test_empty_duplicate_non_first_hop_by_hop_opt(child, iface, hw_dst, ll_dst, + ll_src): + # Try sending empty hop-by-hop-option header after destination option + # header and another hop-by-hop-option header + p = srp1(Ether(dst=hw_dst) / IPv6(dst=ll_dst, src=ll_src) / + IPv6ExtHdrHopByHop() / IPv6ExtHdrDestOpt() / IPv6ExtHdrHopByHop() / + UDP() / "\x07\x08", + iface=iface, timeout=RECV_TIMEOUT, verbose=0) + # should return parameter problem message + assert p is not None + assert ICMPv6ParamProblem in p + assert p[ICMPv6ParamProblem].code == 1 # unrecognized next header + assert p[ICMPv6ParamProblem].ptr >= 48 # after IPv6 header and HopByHopOpt + pktbuf_empty(child) + + +def test_hop_by_hop_opt_only_one_pad1(child, iface, hw_dst, ll_dst, ll_src): + # Register to hop-by-hop-option header + register_protnum(child, EXT_HDR_NH[UDP]) + # send malformed packet with only one Pad1 option + sendp(Ether(dst=hw_dst) / IPv6(dst=ll_dst, src=ll_src) / + # autopad=0 already introduces one Pad1 option and doesn't work with + # options parameter + IPv6ExtHdrHopByHop(autopad=0) / UDP() / "\x01\x02", + iface=iface, verbose=0) + res = child.expect( + # 10 bytes == UDP header plus 2 byte payload + [r"~~ SNIP 0 - size: 10 byte, type: NETTYPE_UNDEF \(\d+\)", TIMEOUT], + timeout=RECV_TIMEOUT + ) + # We expect the header parsing to be messed up + assert res > 0 + pktbuf_empty(child) + unregister(child) + + +def test_hop_by_hop_opt_7_pad1(child, iface, hw_dst, ll_dst, ll_src): + # Register to hop-by-hop-option header + register_protnum(child, EXT_HDR_NH[UDP]) + sendp(Ether(dst=hw_dst) / IPv6(dst=ll_dst, src=ll_src) / + # autopad=0 already introduces one Pad1 option and doesn't work with + # options parameter + IPv6ExtHdrHopByHop(nh=EXT_HDR_NH[UDP], autopad=0) / + Pad1() / Pad1() / Pad1() / Pad1() / Pad1() / Pad1() / + UDP() / "\x01\x02", + iface=iface, verbose=0) + child.expect( + r"~~ SNIP 0 - size: 10 byte, type: NETTYPE_UNDEF \(\d+\)" + ) + pktbuf_empty(child) + unregister(child) + + +def test_hop_by_hop_opt_broken_padn(child, iface, hw_dst, ll_dst, ll_src): + # Register to hop-by-hop-option header + register_protnum(child, EXT_HDR_NH[UDP]) + sendp(Ether(dst=hw_dst) / IPv6(dst=ll_dst, src=ll_src) / + # autopad=0 doesn't work with options parameter + IPv6ExtHdrHopByHop(nh=EXT_HDR_NH[UDP], autopad=0) / + PadN(optlen=7, optdata="\x11\x22\x33\x44\x55\x66\x77") / + UDP() / "\x01\x02", + iface=iface, verbose=0) + res = child.expect( + # 10 bytes == UDP header plus 2 byte payload + [r"~~ SNIP 0 - size: 10 byte, type: NETTYPE_UNDEF \(\d+\)", TIMEOUT], + timeout=RECV_TIMEOUT + ) + # We expect the header parsing to be messed up + assert res > 0 + pktbuf_empty(child) + unregister(child) + + +def test_hop_by_hop_opt_skip_unknown(child, iface, hw_dst, ll_dst, ll_src): + # Register to hop-by-hop-option header + register_protnum(child, EXT_HDR_NH[UDP]) + p = srp1(Ether(dst=hw_dst) / IPv6(dst=ll_dst, src=ll_src) / + IPv6ExtHdrHopByHop( + options=HBHOptUnknown( + otype=TEST_OPTION_TYPES["ACTION_SKIP"], optlen=4, + optdata="\x11\x22\x33\x44" + ) + ) / + UDP() / "\x01\x02", + iface=iface, verbose=0, timeout=RECV_TIMEOUT) + assert p is None + child.expect( + r"~~ SNIP 0 - size: 10 byte, type: NETTYPE_UNDEF \(\d+\)" + ) + pktbuf_empty(child) + unregister(child) + + +def test_hop_by_hop_opt_discard_unknown_1(child, iface, hw_dst, ll_dst, ll_src): + # Register to hop-by-hop-option header + register_protnum(child, EXT_HDR_NH[UDP]) + p = srp1(Ether(dst=hw_dst) / IPv6(dst=ll_dst, src=ll_src) / + IPv6ExtHdrHopByHop( + options=HBHOptUnknown( + otype=TEST_OPTION_TYPES["ACTION_DISCARD"], optlen=4, + optdata="\x11\x22\x33\x44" + ) + ) / + UDP() / "\x01\x02", + iface=iface, verbose=0, timeout=RECV_TIMEOUT) + assert p is None + res = child.expect( + # the packet should be not received at all + [r"PKTDUMP: data received:", TIMEOUT], + timeout=RECV_TIMEOUT + ) + assert res > 0 + pktbuf_empty(child) + unregister(child) + + +def test_hop_by_hop_opt_discard_unknown_2(child, iface, hw_dst, ll_dst, ll_src): + # Register to hop-by-hop-option header + register_protnum(child, EXT_HDR_NH[UDP]) + p = srp1(Ether(dst=hw_dst) / IPv6(dst=ll_dst, src=ll_src) / + IPv6ExtHdrHopByHop( + options=[ + HBHOptUnknown( + otype=TEST_OPTION_TYPES["ACTION_SKIP"], optlen=6, + optdata="\x11\x22\x33\x44\x55\x66" + ), + HBHOptUnknown( + otype=TEST_OPTION_TYPES["ACTION_DISCARD"], optlen=4, + optdata="\x11\x22\x33\x44" + ) + ] + ) / + UDP() / "\x01\x02", + iface=iface, verbose=0, timeout=RECV_TIMEOUT) + assert p is None + res = child.expect( + # the packet should be not received at all + [r"PKTDUMP: data received:", TIMEOUT], + timeout=RECV_TIMEOUT + ) + assert res > 0 + pktbuf_empty(child) + unregister(child) + + +def test_hop_by_hop_opt_discard_unknown_3(child, iface, hw_dst, ll_dst, ll_src): + # Register to hop-by-hop-option header + register_protnum(child, EXT_HDR_NH[UDP]) + p = srp1(Ether(dst=hw_dst) / IPv6(dst=ll_dst, src=ll_src) / + IPv6ExtHdrHopByHop( + options=HBHOptUnknown( + otype=TEST_OPTION_TYPES["ACTION_DISCARD_ERROR_MCAST"], + optlen=4, optdata="\x11\x22\x33\x44" + ) + ) / + UDP() / "\x01\x02", + iface=iface, verbose=0, timeout=RECV_TIMEOUT) + assert p is not None + assert ICMPv6ParamProblem in p + # unrecognized IPv6 option encountered + assert p[ICMPv6ParamProblem].code == 2 + # first after IPv6 header + extension header => 40 + 2 = 42 + assert p[ICMPv6ParamProblem].ptr == 42 + res = child.expect( + # the packet should be not received at all + [r"PKTDUMP: data received:", TIMEOUT], + timeout=RECV_TIMEOUT + ) + assert res > 0 + pktbuf_empty(child) + unregister(child) + + +def test_hop_by_hop_opt_discard_unknown_3_mcast(child, iface, hw_dst, ll_dst, + ll_src): + # Register to hop-by-hop-option header + register_protnum(child, EXT_HDR_NH[UDP]) + # sniff for parameter problem as with multicast srp1 does not work + sniffer = StartCheckAsyncSniffer(iface=iface, count=1, + filter="icmp6[0] == 4") + sniffer.start() + sniffer.wait_for_started() + sendp(Ether(dst=HW_MCAST) / IPv6(dst=MCAST, src=ll_src) / + IPv6ExtHdrHopByHop( + options=HBHOptUnknown( + otype=TEST_OPTION_TYPES["ACTION_DISCARD_ERROR_MCAST"], + optlen=4, optdata="\x11\x22\x33\x44" + ) + ) / + UDP() / "\x01\x02", + iface=iface, verbose=0) + sniffer.join(RECV_TIMEOUT) + ps = sniffer.results + assert len(ps) == 1 + p = ps[0] + assert ICMPv6ParamProblem in p + # unrecognized IPv6 option encountered + assert p[ICMPv6ParamProblem].code == 2 + # first after IPv6 header + extension header => 40 + 2 = 42 + assert p[ICMPv6ParamProblem].ptr == 42 + res = child.expect( + # the packet should be not received at all + [r"PKTDUMP: data received:", TIMEOUT], + timeout=RECV_TIMEOUT + ) + assert res > 0 + pktbuf_empty(child) + unregister(child) + + +def test_hop_by_hop_opt_discard_unknown_4(child, iface, hw_dst, ll_dst, ll_src): + # Register to hop-by-hop-option header + register_protnum(child, EXT_HDR_NH[UDP]) + p = srp1(Ether(dst=hw_dst) / IPv6(dst=ll_dst, src=ll_src) / + IPv6ExtHdrHopByHop( + options=[ + HBHOptUnknown( + otype=TEST_OPTION_TYPES["ACTION_SKIP"], optlen=6, + optdata="\x11\x22\x33\x44\x55\x66" + ), + HBHOptUnknown( + otype=TEST_OPTION_TYPES["ACTION_DISCARD_ERROR_MCAST"], + optlen=4, optdata="\x11\x22\x33\x44" + ) + ] + ) / + UDP() / "\x01\x02", + iface=iface, verbose=0, timeout=RECV_TIMEOUT) + assert p is not None + assert ICMPv6ParamProblem in p + # unrecognized IPv6 option encountered + assert p[ICMPv6ParamProblem].code == 2 + # first after IPv6 header + extension header + skipped option + # => 40 + 2 + 2 + 6 = 50 + assert p[ICMPv6ParamProblem].ptr == 50 + res = child.expect( + # the packet should be not received at all + [r"PKTDUMP: data received:", TIMEOUT], + timeout=RECV_TIMEOUT + ) + assert res > 0 + pktbuf_empty(child) + unregister(child) + + +def test_hop_by_hop_opt_discard_unknown_5(child, iface, hw_dst, ll_dst, ll_src): + # Register to hop-by-hop-option header + register_protnum(child, EXT_HDR_NH[UDP]) + p = srp1(Ether(dst=hw_dst) / IPv6(dst=ll_dst, src=ll_src) / + IPv6ExtHdrHopByHop( + options=HBHOptUnknown( + otype=TEST_OPTION_TYPES["ACTION_DISCARD_ERROR"], + optlen=4, optdata="\x11\x22\x33\x44" + ) + ) / + UDP() / "\x01\x02", + iface=iface, verbose=0, timeout=RECV_TIMEOUT) + assert p is not None + assert ICMPv6ParamProblem in p + # unrecognized IPv6 option encountered + assert p[ICMPv6ParamProblem].code == 2 + # first after IPv6 header + extension header => 40 + 2 = 42 + assert p[ICMPv6ParamProblem].ptr == 42 + res = child.expect( + # the packet should be not received at all + [r"PKTDUMP: data received:", TIMEOUT], + timeout=RECV_TIMEOUT + ) + assert res > 0 + pktbuf_empty(child) + unregister(child) + + +def test_hop_by_hop_opt_discard_unknown_5_mcast(child, iface, hw_dst, ll_dst, + ll_src): + # Register to hop-by-hop-option header + register_protnum(child, EXT_HDR_NH[UDP]) + # sniff for parameter problem as with multicast srp1 does not work + sniffer = StartCheckAsyncSniffer(iface=iface, count=1, + filter="icmp6[0] == 4") + sniffer.start() + sniffer.wait_for_started() + sendp(Ether(dst=HW_MCAST) / IPv6(dst=MCAST, src=ll_src) / + IPv6ExtHdrHopByHop( + options=HBHOptUnknown( + otype=TEST_OPTION_TYPES["ACTION_DISCARD_ERROR"], + optlen=4, optdata="\x11\x22\x33\x44" + ) + ) / + UDP() / "\x01\x02", + iface=iface, verbose=0) + sniffer.join(RECV_TIMEOUT) + ps = sniffer.results + assert ps is None + res = child.expect( + # the packet should be not received at all + [r"PKTDUMP: data received:", TIMEOUT], + timeout=RECV_TIMEOUT + ) + assert res > 0 + pktbuf_empty(child) + unregister(child) + + +def test_empty_dst_opt_wo_register(child, iface, hw_dst, ll_dst, ll_src): + # Try sending an empty Destination-Option header + sendp(Ether(dst=hw_dst) / IPv6(dst=ll_dst, src=ll_src) / + IPv6ExtHdrDestOpt() / UDP(), iface=iface, verbose=0) + pktbuf_empty(child) + + +def test_empty_dst_opt_w_register(child, iface, hw_dst, ll_dst, ll_src): + # Register to Destination-Option header + register_protnum(child, EXT_HDR_NH[IPv6ExtHdrDestOpt]) + # Try sending an empty Destination-Option header + sendp(Ether(dst=hw_dst) / IPv6(dst=ll_dst, src=ll_src) / + IPv6ExtHdrDestOpt() / UDP() / "\x01\x02", iface=iface, verbose=0) + child.expect(r"~~ SNIP 0 - size:\s+(\d+) byte, type: NETTYPE_\w+ \(\d+\)") + ipv6_payload_len = int(child.match.group(1)) + # NH = 17 (UDP), len = 0x00, PadN option (0x01) of length 0x04 + child.expect(r"00000000 11 00 01 04 00 00 00 00") + child.expect(r"~~ SNIP 1 - size:\s+40 byte, type: NETTYPE_IPV6 \(\d+\)") + child.expect_exact(r"length: {} next header: {}".format( + ipv6_payload_len, EXT_HDR_NH[IPv6ExtHdrDestOpt] + )) + child.expect_exact(r"destination address: {}".format(ll_dst)) + pktbuf_empty(child) + unregister(child) + + +def test_empty_dst_opt_large_hdr_len(child, iface, hw_dst, ll_dst, ll_src): + # Register to hop-by-hop-option header + register_protnum(child, EXT_HDR_NH[IPv6ExtHdrDestOpt]) + # Try sending an empty hop-by-hop-option header with too big header length + sendp(Ether(dst=hw_dst) / IPv6(dst=ll_dst, src=ll_src) / + IPv6ExtHdrDestOpt(len=20) / UDP() / "\x01\x02", + iface=iface, verbose=0) + pktbuf_empty(child) + unregister(child) + + +def test_dst_opt_only_one_pad1(child, iface, hw_dst, ll_dst, ll_src): + # Register to hop-by-hop-option header + register_protnum(child, EXT_HDR_NH[UDP]) + # send malformed packet with only one Pad1 option + sendp(Ether(dst=hw_dst) / IPv6(dst=ll_dst, src=ll_src) / + IPv6ExtHdrDestOpt(autopad=0) / UDP() / "\x01\x02", + iface=iface, verbose=0) + res = child.expect( + # 10 bytes == UDP header plus 2 byte payload + [r"~~ SNIP 0 - size: 10 byte, type: NETTYPE_UNDEF \(\d+\)", TIMEOUT], + timeout=RECV_TIMEOUT + ) + # We expect the header parsing to be messed up + assert res > 0 + pktbuf_empty(child) + unregister(child) + + +def test_dst_opt_7_pad1(child, iface, hw_dst, ll_dst, ll_src): + # Register to hop-by-hop-option header + register_protnum(child, EXT_HDR_NH[UDP]) + sendp(Ether(dst=hw_dst) / IPv6(dst=ll_dst, src=ll_src) / + IPv6ExtHdrDestOpt(nh=EXT_HDR_NH[UDP], autopad=0) / + Pad1() / Pad1() / Pad1() / Pad1() / Pad1() / Pad1() / + UDP() / "\x01\x02", + iface=iface, verbose=0) + child.expect( + r"~~ SNIP 0 - size: 10 byte, type: NETTYPE_UNDEF \(\d+\)", + timeout=RECV_TIMEOUT + ) + pktbuf_empty(child) + unregister(child) + + +def test_dst_opt_broken_padn(child, iface, hw_dst, ll_dst, ll_src): + # Register to hop-by-hop-option header + register_protnum(child, EXT_HDR_NH[UDP]) + sendp(Ether(dst=hw_dst) / IPv6(dst=ll_dst, src=ll_src) / + IPv6ExtHdrDestOpt(nh=EXT_HDR_NH[UDP], autopad=0) / + PadN(optlen=7, optdata="\x11\x22\x33\x44\x55\x66\x77") / + UDP() / "\x01\x02", + iface=iface, verbose=0) + res = child.expect( + # 10 bytes == UDP header plus 2 byte payload + [r"~~ SNIP 0 - size: 10 byte, type: NETTYPE_UNDEF \(\d+\)", TIMEOUT], + timeout=RECV_TIMEOUT + ) + # We expect the header parsing to be messed up + assert res > 0 + pktbuf_empty(child) + unregister(child) + + +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(): + m = re.search(pattern, line) + if m is not None: + return m.group(res_group) + return None + + +def get_bridge(tap): + res = check_and_search_output( + ["bridge", "link"], + r"{}.+master\s+(?P[^\s]+)".format(tap), + "master" + ) + return tap if res is None else res + + +def get_host_lladdr(tap): + res = check_and_search_output( + ["ip", "addr", "show", "dev", tap, "scope", "link"], + r"inet6 (?P[0-9A-Fa-f:]+)/64", + "lladdr" + ) + if res is None: + raise AssertionError( + "Can't find host link-local address on interface {}".format(tap) + ) + else: + return res + + +def testfunc(child): + tap = get_bridge(os.environ["TAP"]) + + lladdr_src = get_host_lladdr(tap) + child.sendline("ifconfig") + child.expect(r"HWaddr: (?P[A-Fa-f:0-9]+)\s") + hwaddr_dst = child.match.group("hwaddr").lower() + child.expect(r"(?Pfe80::[A-Fa-f:0-9]+)\s") + lladdr_dst = child.match.group("lladdr").lower() + + def run(func): + if child.logfile == sys.stdout: + func(child, tap, hwaddr_dst, lladdr_dst, lladdr_src) + else: + try: + func(child, tap, hwaddr_dst, lladdr_dst, lladdr_src) + print(".", end="", flush=True) + except Exception as e: + print("FAILED") + raise e + + run(test_empty_hop_by_hop_opt_wo_register) + run(test_empty_hop_by_hop_opt_w_register) + run(test_empty_duplicate_hop_by_hop_opt) + run(test_empty_non_first_hop_by_hop_opt) + run(test_empty_duplicate_non_first_hop_by_hop_opt) + run(test_hop_by_hop_opt_only_one_pad1) + run(test_hop_by_hop_opt_7_pad1) + run(test_hop_by_hop_opt_broken_padn) + run(test_hop_by_hop_opt_skip_unknown) + run(test_hop_by_hop_opt_discard_unknown_1) + run(test_hop_by_hop_opt_discard_unknown_2) + run(test_hop_by_hop_opt_discard_unknown_3) + run(test_hop_by_hop_opt_discard_unknown_3_mcast) + run(test_hop_by_hop_opt_discard_unknown_4) + run(test_hop_by_hop_opt_discard_unknown_5) + run(test_hop_by_hop_opt_discard_unknown_5_mcast) + run(test_empty_dst_opt_wo_register) + run(test_empty_dst_opt_w_register) + run(test_empty_dst_opt_large_hdr_len) + run(test_dst_opt_only_one_pad1) + run(test_dst_opt_7_pad1) + run(test_dst_opt_broken_padn) + print("SUCCESS") + + +if __name__ == "__main__": + 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=1, echo=False))