From e62bb9c414517b72a24313a8fd17dd8933102a79 Mon Sep 17 00:00:00 2001 From: Martine Lenders Date: Tue, 4 Jun 2019 19:59:39 +0200 Subject: [PATCH] tests: provide tests for gnrc_ipv6_ext_frag fragmentation --- tests/gnrc_ipv6_ext_frag/Makefile | 37 ++-- tests/gnrc_ipv6_ext_frag/main.c | 188 +++++++++++++++++++- tests/gnrc_ipv6_ext_frag/tests/01-run.py | 216 ++++++++++++++++++++++- 3 files changed, 426 insertions(+), 15 deletions(-) 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))