tests: provide tests for gnrc_ipv6_ext_frag fragmentation

This commit is contained in:
Martine Lenders 2019-06-04 19:59:39 +02:00 committed by Martine S. Lenders
parent 51ef1c997d
commit e62bb9c414
3 changed files with 425 additions and 14 deletions

View File

@ -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

View File

@ -20,18 +20,31 @@
*/
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#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 <dst_addr> [<num>]");
return 1;
}
test_num = atoi(argv[2]);
if ((unsigned)test_num >= ARRAY_SIZE(_shell_tests)) {
printf("<num> 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(&eth_netif->ipv6.addrs[i])) {
local_addr = &eth_netif->ipv6.addrs[i];
}
}
shell_run(shell_commands, line_buf, SHELL_DEFAULT_BUFSIZE);
return 0;
}

View File

@ -14,10 +14,14 @@ import sys
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 testfunc(child):
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))