1
0
mirror of https://github.com/RIOT-OS/RIOT.git synced 2024-12-29 04:50:03 +01:00
RIOT/tests/net/gnrc_ipv6_ext_frag/tests-as-root/01-run.py

435 lines
15 KiB
Python
Executable File

#!/usr/bin/env python3
# Copyright (C) 2019 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 pexpect
import socket
import sys
import subprocess
import time
from scapy.all import Ether, ICMPv6PacketTooBig, IPv6, IPv6ExtHdrFragment, \
UDP, raw, sendp, srp1
from testrunner import run, check_unittests
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,
}
def pktbuf_empty(child):
child.sendline("pktbuf")
child.expect(r"packet buffer: first byte: (?P<first_byte>0x[0-9a-fA-F]+), "
r"last byte: 0x[0-9a-fA-F]+ \(size: (?P<size>\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 pktbuf_size(child):
child.sendline("pktbuf")
child.expect(r"packet buffer: first byte: (?P<first_byte>0x[0-9a-fA-F]+), "
r"last byte: 0x[0-9a-fA-F]+ \(size: (?P<size>\d+)\)")
size = child.match.group("size")
return int(size)
def start_udp_server(child, port):
child.sendline("udp server start {}".format(port))
child.expect_exact("Success: started UDP server on port {}".format(port))
def stop_udp_server(child):
child.sendline("udp server stop")
# either way: it is stopped
child.expect(["Success: stopped UDP server",
"Error: server was not running"])
def udp_send(child, addr, iface, port, length, num=1, delay=1000000):
child.sendline("udp send {addr}%{iface} {port} {length} {num} {delay}"
.format(**vars()))
child.expect(r"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():
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<master>[^\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<lladdr>[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 get_host_mtu(tap):
res = check_and_search_output(
["ip", "link", "show", tap],
r"mtu (?P<mtu>1500)",
"mtu"
)
if res is None:
raise AssertionError(
"Can't find host link-local address on interface {}".format(tap)
)
else:
return int(res)
def test_reass_successful_udp(child, iface, hw_dst, ll_dst, ll_src):
port = 1337
mtu = get_host_mtu(iface)
byte_max = 0xff
payload_len = (byte_max * ((mtu // byte_max) + 1))
if not (mtu % byte_max):
payload_len += 1
start_udp_server(child, port)
with socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) as s:
res = socket.getaddrinfo("{}%{}".format(ll_src, iface), None)
s.bind(res[0][4])
s.sendto(bytes(i for i in range(byte_max)) * (payload_len // byte_max),
(ll_dst, port))
child.expect(
r"~~ SNIP 0 - size: {} byte, type: NETTYPE_UNDEF \(\d+\)"
.format(payload_len)
)
# 4 snips: payload, UDP header, IPv6 header, netif header
# (fragmentation header was removed)
child.expect(
r"~~ PKT - 4 snips, total size: (\d+) byte"
)
size = int(child.match.group(1))
# 40 = IPv6 header length; 8 = UDP header length
# >= since netif header also has a length
assert size >= (payload_len + 40 + 8)
stop_udp_server(child)
pktbuf_empty(child)
def test_reass_too_short_header(child, iface, hw_dst, ll_dst, ll_src):
sendp(Ether(dst=hw_dst) / IPv6(dst=ll_dst, src=ll_src,
nh=EXT_HDR_NH[IPv6ExtHdrFragment]) / "\x11",
iface=iface, verbose=0)
pktbuf_empty(child)
def test_reass_offset_too_large(child, iface, hw_dst, ll_dst, ll_src):
size = pktbuf_size(child)
sendp(Ether(dst=hw_dst) / IPv6(dst=ll_dst, src=ll_src) /
IPv6ExtHdrFragment(offset=((size * 2) // 8)) / "x" * 128,
iface=iface, verbose=0)
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 _check_iface(child):
# get TAP MAC address
child.sendline("ifconfig")
ethos_id = None
mock_id = None
hwaddr = None
for _ in range(2):
child.expect(r"Iface\s+(\d+)\s+.*")
match = re.search(r"HWaddr:\s+([0-9A-F:]{17})\s+",
child.match.group(0))
if match is not None:
# interface has a hardware address
ethos_id = int(child.match.group(1))
hwaddr = match.group(1)
else:
mock_id = int(child.match.group(1))
# consume MTU for later calls of `ifconfig {mock_id}`
child.expect(r"MTU:(\d+)")
# check if interface is configured properly
assert ethos_id is not None
assert mock_id is not None
assert hwaddr is not None
return ethos_id, mock_id, hwaddr
def test_ipv6_ext_frag_send_success(child, s, iface, ll_dst):
length = get_host_mtu(iface)
port = s.getsockname()[1]
ethos_id, _, _ = _check_iface(child)
udp_send(child, ll_dst, ethos_id, 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]
ethos_id, _, _ = _check_iface(child)
udp_send(child, ll_dst, ethos_id, 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]
ethos_id, _, _ = _check_iface(child)
udp_send(child, ll_dst, ethos_id, 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]
ethos_id, _, _ = _check_iface(child)
# trigger neighbor discovery so it doesn't fill the packet buffer
udp_send(child, ll_dst, ethos_id, 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, ethos_id, port, 1)
data, _ = s.recvfrom(1)
last_nd = time.time()
udp_send(child, ll_dst, ethos_id, 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):
ethos_id, mock_id, hwaddr = _check_iface(child)
child.sendline("ifconfig {}".format(mock_id))
child.expect(r"MTU:(\d+)")
mtu = int(child.match.group(1))
# configure routes
child.sendline("nib route add {} {}/128 fe80::1".format(mock_id, g_dst))
child.sendline("nib route add {} {}/128 {}"
.format(ethos_id, g_src, ll_dst))
child.sendline("nib route")
child.expect(r"{}/128 via fe80::1 dev #{}".format(g_dst, mock_id))
child.expect(r"{}/128 via {} dev #{}".format(g_src, ll_dst, ethos_id))
child.sendline("nib neigh add {} fe80::1".format(mock_id))
child.sendline("nib neigh")
child.expect(r"fe80::1 dev #{} lladdr\s+-".format(mock_id))
return mock_id, mtu, hwaddr
def _fwd_teardown(child, mock_id):
# remove route
child.sendline("nib neigh del {} fe80::1".format(mock_id))
child.sendline("nib route del {} affe::/64".format(mock_id))
def test_ipv6_ext_frag_fwd_success(child, s, iface, ll_dst):
mock_id, 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, mock_id)
def test_ipv6_ext_frag_fwd_too_big(child, s, iface, ll_dst):
mock_id, 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, mock_id)
def testfunc(child):
tap = get_bridge(os.environ["TAP"])
child.sendline("unittests")
# wait for and check result of unittests
print("." * check_unittests(child), 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.sendline("send-test-pkt")
child.expect(r"Sending UDP test packets to port (\d+)\r\n")
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")
print("Skipping datagram reception tests due to ethos bug.")
return
# datagram reception tests
res = 1
count = 0
while res:
# check `ifconfig` and also get addresses from it until
# link-local address becomes valid
time.sleep(1)
child.sendline("ifconfig")
child.expect(r"HWaddr: (?P<hwaddr>[A-Fa-f:0-9]+)\s")
hwaddr_dst = child.match.group("hwaddr").lower()
res = child.expect([
r"(?P<lladdr>fe80::[A-Fa-f:0-9]+)\s+scope:\s+link\s+VAL",
pexpect.TIMEOUT
])
count += 1
if res and (count > 5):
raise pexpect.TIMEOUT("Link-local address did not become valid")
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 PermissionError:
print("\n\x1b[1;33mSkipping {} because of missing "
"privileges\x1b[0m".format(func.__name__))
except Exception as e:
print("FAILED")
raise e
run(test_reass_successful_udp)
run(test_reass_too_short_header)
run(test_reass_offset_too_large)
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=2, echo=False))