mirror of
https://github.com/RIOT-OS/RIOT.git
synced 2024-12-29 04:50:03 +01:00
tests: Add test for stateless DHCPv6
This commit is contained in:
parent
c62d6eb834
commit
4297bfcdee
48
tests/gnrc_dhcpv6_client_stateless/Makefile
Normal file
48
tests/gnrc_dhcpv6_client_stateless/Makefile
Normal file
@ -0,0 +1,48 @@
|
||||
DEVELHELP := 1
|
||||
include $(CURDIR)/../Makefile.tests_common
|
||||
|
||||
export TAP ?= tap0
|
||||
|
||||
USEMODULE += auto_init_gnrc_netif
|
||||
USEMODULE += dhcpv6_client_mud_url
|
||||
USEMODULE += gnrc_dhcpv6_client
|
||||
USEMODULE += gnrc_ipv6_default
|
||||
USEMODULE += auto_init_dhcpv6_client
|
||||
USEMODULE += gnrc_netdev_default
|
||||
USEMODULE += gnrc_pktdump
|
||||
USEMODULE += ps
|
||||
USEMODULE += shell
|
||||
USEMODULE += shell_commands
|
||||
|
||||
# use Ethernet as link-layer protocol
|
||||
ifeq (native,$(BOARD))
|
||||
# Has to be provided here and not in Makefile.dep, so TERMFLAGS are properly
|
||||
# configured
|
||||
USEMODULE += netdev_default
|
||||
IFACE ?= tapbr0
|
||||
else
|
||||
IFACE ?= tap0
|
||||
ETHOS_BAUDRATE ?= 115200
|
||||
CFLAGS += -DETHOS_BAUDRATE=$(ETHOS_BAUDRATE)
|
||||
TERMPROG ?= sudo $(RIOTBASE)/dist/tools/ethos/ethos
|
||||
TERMFLAGS ?= $(IFACE) $(PORT) $(ETHOS_BAUDRATE)
|
||||
TERMDEPS += ethos
|
||||
endif
|
||||
|
||||
# The test requires some setup and to be run as root
|
||||
# So it cannot currently be run on CI
|
||||
TEST_ON_CI_BLACKLIST += all
|
||||
|
||||
# As there is an 'app.config' we want to explicitly disable Kconfig by setting
|
||||
# the variable to empty
|
||||
SHOULD_RUN_KCONFIG ?=
|
||||
|
||||
include $(RIOTBASE)/Makefile.include
|
||||
|
||||
|
||||
ifeq (,$(filter native,$(BOARD)))
|
||||
.PHONY: ethos
|
||||
|
||||
ethos:
|
||||
$(Q)env -u CC -u CFLAGS $(MAKE) -C $(RIOTBASE)/dist/tools/ethos
|
||||
endif
|
4
tests/gnrc_dhcpv6_client_stateless/Makefile.board.dep
Normal file
4
tests/gnrc_dhcpv6_client_stateless/Makefile.board.dep
Normal file
@ -0,0 +1,4 @@
|
||||
# Put board specific dependencies here
|
||||
ifneq (native,$(BOARD))
|
||||
USEMODULE += stdio_ethos
|
||||
endif
|
51
tests/gnrc_dhcpv6_client_stateless/Makefile.ci
Normal file
51
tests/gnrc_dhcpv6_client_stateless/Makefile.ci
Normal file
@ -0,0 +1,51 @@
|
||||
BOARD_INSUFFICIENT_MEMORY := \
|
||||
airfy-beacon \
|
||||
arduino-duemilanove \
|
||||
arduino-leonardo \
|
||||
arduino-mega2560 \
|
||||
arduino-nano \
|
||||
arduino-uno \
|
||||
atmega1284p \
|
||||
atmega328p \
|
||||
atmega328p-xplained-mini \
|
||||
atxmega-a1u-xpro \
|
||||
atxmega-a3bu-xplained \
|
||||
bluepill-stm32f030c8 \
|
||||
b-l072z-lrwan1 \
|
||||
derfmega128 \
|
||||
hifive1 \
|
||||
hifive1b \
|
||||
i-nucleo-lrwan1 \
|
||||
im880b \
|
||||
lsn50 \
|
||||
mega-xplained \
|
||||
microbit \
|
||||
microduino-corerf \
|
||||
msb-430 \
|
||||
msb-430h \
|
||||
nrf51dongle \
|
||||
nrf6310 \
|
||||
nucleo-f030r8 \
|
||||
nucleo-f031k6 \
|
||||
nucleo-f042k6 \
|
||||
nucleo-f070rb \
|
||||
nucleo-f072rb \
|
||||
nucleo-f303k8 \
|
||||
nucleo-f334r8 \
|
||||
nucleo-l011k4 \
|
||||
nucleo-l031k6 \
|
||||
nucleo-l053r8 \
|
||||
samd10-xmini \
|
||||
saml10-xpro \
|
||||
saml11-xpro \
|
||||
slstk3400a \
|
||||
stk3200 \
|
||||
stm32f030f4-demo \
|
||||
stm32f0discovery \
|
||||
stm32l0538-disco \
|
||||
telosb \
|
||||
waspmote-pro \
|
||||
yunjia-nrf51822 \
|
||||
z1 \
|
||||
zigduino \
|
||||
#
|
43
tests/gnrc_dhcpv6_client_stateless/README.md
Normal file
43
tests/gnrc_dhcpv6_client_stateless/README.md
Normal file
@ -0,0 +1,43 @@
|
||||
# `gnrc_dhcpv6_client_stateless` test
|
||||
|
||||
This test utilizes [scapy] to test the DHCPv6 client configuration for
|
||||
stateless DHCP.
|
||||
|
||||
The protocol procedure is modelled using a scapy [Automaton] by first waiting for an
|
||||
NDP Router Solicitation (RS) which is replied to with an NDP Router Advertisement (RA).
|
||||
The RA contains a set O-bit which indicates that additional information can be acquired by sending
|
||||
a DHCPv6 Information Request (IR). The Automaton now waits for an IR, expects it to contain a
|
||||
number of options, and sends a DHCPv6 Reply back to the client.
|
||||
After this procedure is completed, a check for a correctly assigned global IP address (from the RA
|
||||
using SLAAC) is performed. If this final assertion is correct, the test succeeds.
|
||||
|
||||
The procedure is visualized in the following graph:
|
||||
|
||||
![Visualization of the test procedure as a graph.](./test-graph.svg)
|
||||
|
||||
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-as-root
|
||||
```
|
||||
|
||||
Note that root privileges are required since `scapy` needs to construct Ethernet
|
||||
frames to properly communicate over the TAP interface.
|
||||
|
||||
The test succeeds if you see the string `SUCCESS`.
|
||||
|
||||
If any problems are encountered (i.e. if the test prints the string `FAILED`),
|
||||
set the echo parameter in the `run()` function at the bottom of the test script
|
||||
(tests-as-root/01-run.py) to `True`. The test script will then offer a more detailed
|
||||
output.
|
||||
|
||||
[scapy]: https://scapy.readthedocs.io/en/latest/
|
||||
[Automaton]: https://scapy.readthedocs.io/en/latest/api/scapy.automaton.html
|
3
tests/gnrc_dhcpv6_client_stateless/app.config
Normal file
3
tests/gnrc_dhcpv6_client_stateless/app.config
Normal file
@ -0,0 +1,3 @@
|
||||
CONFIG_KCONFIG_USEMODULE_GNRC_IPV6_NIB=y
|
||||
CONFIG_GNRC_IPV6_NIB_ARSM=y
|
||||
CONFIG_GNRC_IPV6_NIB_SLAAC=y
|
27
tests/gnrc_dhcpv6_client_stateless/main.c
Normal file
27
tests/gnrc_dhcpv6_client_stateless/main.c
Normal file
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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 <m.lenders@fu-berlin.de>
|
||||
*/
|
||||
|
||||
#include "shell.h"
|
||||
|
||||
int main(void)
|
||||
{
|
||||
char line_buf[SHELL_DEFAULT_BUFSIZE];
|
||||
shell_run(NULL, line_buf, SHELL_DEFAULT_BUFSIZE);
|
||||
|
||||
/* should be never reached */
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** @} */
|
47
tests/gnrc_dhcpv6_client_stateless/test-graph.svg
Normal file
47
tests/gnrc_dhcpv6_client_stateless/test-graph.svg
Normal file
@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 2.40.1 (20161225.0304)
|
||||
-->
|
||||
<!-- Title: Automaton_metaclass Pages: 1 -->
|
||||
<svg width="233pt" height="248pt"
|
||||
viewBox="0.00 0.00 232.88 248.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 244)">
|
||||
<title>Automaton_metaclass</title>
|
||||
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-244 228.88,-244 228.88,4 -4,4"/>
|
||||
<!-- WAITING_FOR_NDP_RS -->
|
||||
<g id="node1" class="node">
|
||||
<title>WAITING_FOR_NDP_RS</title>
|
||||
<polygon fill="#0000ff" stroke="#000000" points="195.94,-240 28.94,-240 28.94,-204 195.94,-204 195.94,-240"/>
|
||||
<text text-anchor="middle" x="112.44" y="-218.3" font-family="Times,serif" font-size="14.00" fill="#000000">WAITING_FOR_NDP_RS</text>
|
||||
</g>
|
||||
<!-- WAITING_FOR_DHCP_IR -->
|
||||
<g id="node3" class="node">
|
||||
<title>WAITING_FOR_DHCP_IR</title>
|
||||
<ellipse fill="none" stroke="#000000" cx="112.44" cy="-120" rx="112.3801" ry="18"/>
|
||||
<text text-anchor="middle" x="112.44" y="-116.3" font-family="Times,serif" font-size="14.00" fill="#000000">WAITING_FOR_DHCP_IR</text>
|
||||
</g>
|
||||
<!-- WAITING_FOR_NDP_RS->WAITING_FOR_DHCP_IR -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>WAITING_FOR_NDP_RS->WAITING_FOR_DHCP_IR</title>
|
||||
<path fill="none" stroke="#ff0000" d="M112.44,-203.7644C112.44,-188.317 112.44,-165.9149 112.44,-148.164"/>
|
||||
<polygon fill="#ff0000" stroke="#ff0000" points="115.9401,-148.0776 112.44,-138.0777 108.9401,-148.0777 115.9401,-148.0776"/>
|
||||
<text text-anchor="start" x="112.44" y="-174.8" font-family="Times,serif" font-size="14.00" fill="#000000">received_ICMP</text>
|
||||
<text text-anchor="middle" x="156.44" y="-159.8" font-family="Times,serif" font-size="14.00" fill="#000000">>[on_NDP_RS]</text>
|
||||
</g>
|
||||
<!-- END -->
|
||||
<g id="node2" class="node">
|
||||
<title>END</title>
|
||||
<polygon fill="#00ff00" stroke="#000000" points="143.3474,-10.5442 143.3474,-25.4558 125.2423,-36 99.6378,-36 81.5326,-25.4558 81.5326,-10.5442 99.6378,0 125.2423,0 143.3474,-10.5442"/>
|
||||
<text text-anchor="middle" x="112.44" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">END</text>
|
||||
</g>
|
||||
<!-- WAITING_FOR_DHCP_IR->END -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>WAITING_FOR_DHCP_IR->END</title>
|
||||
<path fill="none" stroke="#ff0000" d="M112.44,-101.7644C112.44,-86.317 112.44,-63.9149 112.44,-46.164"/>
|
||||
<polygon fill="#ff0000" stroke="#ff0000" points="115.9401,-46.0776 112.44,-36.0777 108.9401,-46.0777 115.9401,-46.0776"/>
|
||||
<text text-anchor="start" x="112.44" y="-72.8" font-family="Times,serif" font-size="14.00" fill="#000000">received_DHCPv6</text>
|
||||
<text text-anchor="middle" x="166.44" y="-57.8" font-family="Times,serif" font-size="14.00" fill="#000000">>[on_DHCPv6_IR]</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.8 KiB |
250
tests/gnrc_dhcpv6_client_stateless/tests-as-root/01-run.py
Executable file
250
tests/gnrc_dhcpv6_client_stateless/tests-as-root/01-run.py
Executable file
@ -0,0 +1,250 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (C) 2018 Freie Universität Berlin
|
||||
# 2021 Jan Romann <jan.romann@uni-bremen.de>
|
||||
#
|
||||
# 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 os
|
||||
import pexpect
|
||||
import random
|
||||
import sys
|
||||
import time
|
||||
|
||||
from scapy.all import (
|
||||
DHCP6_Reply,
|
||||
DHCP6_InfoRequest,
|
||||
ICMPv6NDOptSrcLLAddr,
|
||||
ICMPv6NDOptMTU,
|
||||
sendp,
|
||||
Ether,
|
||||
IPv6,
|
||||
UDP,
|
||||
ICMPv6ND_RS,
|
||||
ICMPv6ND_RA,
|
||||
DHCP6OptClientId,
|
||||
DHCP6OptServerId,
|
||||
ICMPv6NDOptPrefixInfo,
|
||||
DUID_LL,
|
||||
Automaton,
|
||||
ATMT,
|
||||
)
|
||||
from testrunner import run
|
||||
|
||||
try:
|
||||
from scapy.all import DHCP6OptMudUrl
|
||||
|
||||
mud_option_loaded = True
|
||||
except ImportError:
|
||||
from scapy.all import DHCP6OptUnknown
|
||||
|
||||
DHCP6OptMudUrl = DHCP6OptUnknown
|
||||
mud_option_loaded = False
|
||||
|
||||
TIMEOUT = 1
|
||||
|
||||
MUD_OPTION_CODE = 112
|
||||
MUD_TEST_URL = b"https://example.org"
|
||||
|
||||
|
||||
class StatelessDHCPv6Test(Automaton):
|
||||
"""
|
||||
Scapy Automaton used for performing stateless DHCPv6 tests.
|
||||
"""
|
||||
|
||||
def parse_args(self, child, **kwargs):
|
||||
"""
|
||||
Initializes the Automaton.
|
||||
|
||||
Receives the TAP interface that is being
|
||||
used as a keyword argument (`iface`).
|
||||
Also generates a randomized prefix for
|
||||
SLAAC testing.
|
||||
"""
|
||||
super().parse_args(**kwargs)
|
||||
self.child = child
|
||||
self.iface = kwargs["iface"]
|
||||
self.prefix = "2001:db8:{:x}:{:x}::".format(
|
||||
random.randint(0, 0xFFFF), random.randint(0, 0xFFFF)
|
||||
)
|
||||
|
||||
@ATMT.state(initial=1)
|
||||
def WAITING_FOR_NDP_RS(self):
|
||||
"""
|
||||
The initial state.
|
||||
|
||||
The Automaton waits for an NDP Router Solication.
|
||||
"""
|
||||
pass
|
||||
|
||||
@ATMT.receive_condition(WAITING_FOR_NDP_RS, prio=1)
|
||||
def received_ICMP(self, pkt):
|
||||
"""
|
||||
Checks if an incoming packet contains an NDP Router Solicitaion (RS).
|
||||
|
||||
If an RS has been received, `on_NDP_RS` will be called with the packet
|
||||
as an argument and the Automaton's state will change to
|
||||
`WAITING_FOR_DHCP_IR`.
|
||||
"""
|
||||
if ICMPv6ND_RS in pkt:
|
||||
raise self.WAITING_FOR_DHCP_IR().action_parameters(pkt)
|
||||
|
||||
@ATMT.action(received_ICMP)
|
||||
def on_NDP_RS(self, pkt):
|
||||
"""
|
||||
Called when an NDP Router Solicitation has been received.
|
||||
|
||||
Calls `send_RA` to send an NDP Router Advertisement to all IPv6 nodes.
|
||||
"""
|
||||
self.send_RA()
|
||||
|
||||
@staticmethod
|
||||
def build_router_advertise_header():
|
||||
"""
|
||||
Builds Ethernet and IPv6 headers for sending a packet to all IPv6 nodes.
|
||||
"""
|
||||
return Ether() / IPv6(dst="ff02::1")
|
||||
|
||||
def send_RA(self):
|
||||
"""
|
||||
Composes and sends an NDP Router Advertisement (RA).
|
||||
|
||||
The RA contains a prefix which will be used by the RIOT
|
||||
application for configuring a global IPv6 addresses using
|
||||
Stateless Address Autoconfiguration (SLAAC).
|
||||
"""
|
||||
header = self.build_router_advertise_header()
|
||||
ra = ICMPv6ND_RA(M=0, O=1)
|
||||
src_ll_addr = ICMPv6NDOptSrcLLAddr(lladdr=header[Ether].src)
|
||||
mtu = ICMPv6NDOptMTU()
|
||||
prefix_info = ICMPv6NDOptPrefixInfo(prefix=self.prefix, prefixlen=64)
|
||||
sendp(
|
||||
header / ra / src_ll_addr / mtu / prefix_info,
|
||||
iface=self.iface,
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
@ATMT.state()
|
||||
def WAITING_FOR_DHCP_IR(self):
|
||||
"""
|
||||
The second state.
|
||||
|
||||
The Automaton waits for a DHCPv6 Information Request.
|
||||
"""
|
||||
pass
|
||||
|
||||
@ATMT.receive_condition(WAITING_FOR_DHCP_IR, prio=1)
|
||||
def received_DHCPv6(self, pkt):
|
||||
"""
|
||||
Checks if an expected DHCPv6 Information Request (IR) was received.
|
||||
|
||||
The method asserts that the expected options are contained in the IR,
|
||||
triggers `on_DHCPv6_IR` if this is the case, and lets the Automaton
|
||||
switch to the final state `END`.
|
||||
"""
|
||||
if DHCP6_InfoRequest in pkt:
|
||||
hwaddrs = get_hwaddrs(self.child)
|
||||
|
||||
assert DHCP6OptClientId in pkt and DUID_LL in pkt[DHCP6OptClientId].duid
|
||||
assert pkt[DHCP6OptClientId].duid[DUID_LL].lladdr in hwaddrs
|
||||
|
||||
# The information-request contained a MUD URL option
|
||||
assert DHCP6OptMudUrl in pkt
|
||||
mud_option = pkt[DHCP6OptMudUrl]
|
||||
assert mud_option.optlen == len(MUD_TEST_URL)
|
||||
|
||||
if mud_option_loaded:
|
||||
assert mud_option.mudstring == MUD_TEST_URL
|
||||
else:
|
||||
assert mud_option.optcode == MUD_OPTION_CODE
|
||||
assert mud_option.data == MUD_TEST_URL
|
||||
|
||||
raise self.END().action_parameters(pkt)
|
||||
|
||||
@ATMT.action(received_DHCPv6)
|
||||
def on_DHCPv6_IR(self, pkt):
|
||||
"""
|
||||
Calls `send_DHCPv6_Reply` for sending a DHCPv6 Reply message.
|
||||
"""
|
||||
self.send_DHCPv6_Reply(pkt)
|
||||
|
||||
@staticmethod
|
||||
def build_reply_headers(pkt):
|
||||
"""
|
||||
Constructs the Ethernet, IPv6, and UDP headers for the DHCPv6 Reply.
|
||||
|
||||
Uses the received packet for inserting the correct addresses and ports.
|
||||
"""
|
||||
src_ether = pkt[Ether].src
|
||||
src_ip = pkt[IPv6].src
|
||||
sport = pkt[UDP].sport
|
||||
dport = pkt[UDP].dport
|
||||
return Ether(dst=src_ether) / IPv6(dst=src_ip) / UDP(sport=dport, dport=sport)
|
||||
|
||||
def send_DHCPv6_Reply(self, pkt):
|
||||
"""
|
||||
Sends out the DHCPv6 Reply message.
|
||||
"""
|
||||
header = self.build_reply_headers(pkt)
|
||||
trid = pkt[DHCP6_InfoRequest].trid
|
||||
srv_duid = header[Ether].src
|
||||
cli_id = DHCP6OptClientId(duid=pkt[DHCP6OptClientId].duid)
|
||||
srv_id = DHCP6OptServerId(duid=DUID_LL(lladdr=srv_duid))
|
||||
sendp(
|
||||
header / DHCP6_Reply(trid=trid) / cli_id / srv_id,
|
||||
iface=self.iface,
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
@ATMT.timeout(WAITING_FOR_NDP_RS, 10.0)
|
||||
@ATMT.timeout(WAITING_FOR_DHCP_IR, 10.0)
|
||||
def waiting_timeout(self):
|
||||
"""
|
||||
Defines a timeout of 10 seconds for both the first and second state.
|
||||
"""
|
||||
raise self.ERROR_TIMEOUT()
|
||||
|
||||
@ATMT.state(final=1)
|
||||
def END(self):
|
||||
"""
|
||||
The final state.
|
||||
|
||||
Checks if the global IPv6 address has been configured correctly and
|
||||
terminates the test.
|
||||
"""
|
||||
time.sleep(1)
|
||||
|
||||
# check if global address was configured
|
||||
self.child.sendline("ifconfig")
|
||||
# remove one trailing ':' from prefix just to be safe ;-)
|
||||
self.child.expect(r"inet6 addr:\s+{}[0-9a-fA-F:]+\s".format(self.prefix[:-1]))
|
||||
print("SUCCESS")
|
||||
|
||||
|
||||
def get_hwaddrs(child):
|
||||
"""
|
||||
Extracts the RIOT device's MAC Address from the command line for assertions.
|
||||
"""
|
||||
hwaddrs = []
|
||||
child.sendline("ifconfig")
|
||||
child.expect(r"HWaddr:\s+(([A-Fa-f0-9]{2}:?)+)\s")
|
||||
hwaddrs.append(child.match.group(1).lower())
|
||||
if len(hwaddrs[0]) == 5: # short address
|
||||
res = child.expect([pexpect.TIMEOUT, r"Long HWaddr:\s+(([A-Fa-f0-9]{2}:?)+)\s"])
|
||||
if res > 0:
|
||||
hwaddrs.append(child.match.group(1).lower())
|
||||
return hwaddrs
|
||||
|
||||
|
||||
def testfunc(child):
|
||||
"""
|
||||
The test function that is called by the test runner.
|
||||
"""
|
||||
iface = os.environ["TAP"]
|
||||
StatelessDHCPv6Test(child, iface=iface).run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(run(testfunc, timeout=TIMEOUT, echo=True))
|
Loading…
Reference in New Issue
Block a user