From c62d6eb8343412379e1c46c05e7bebe6f25cf01a Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Thu, 12 Aug 2021 19:29:22 +0200 Subject: [PATCH 1/4] sys/net/dhcpv6: Implement stateless DHCPv6 --- sys/include/net/dhcpv6.h | 2 + sys/include/net/dhcpv6/client.h | 23 ++++ sys/net/application_layer/dhcpv6/_dhcpv6.h | 31 +++++ sys/net/application_layer/dhcpv6/client.c | 136 ++++++++++++++++++--- sys/net/gnrc/network_layer/ipv6/nib/nib.c | 17 +++ 5 files changed, 189 insertions(+), 20 deletions(-) diff --git a/sys/include/net/dhcpv6.h b/sys/include/net/dhcpv6.h index 3b30cfa588..16bc347182 100644 --- a/sys/include/net/dhcpv6.h +++ b/sys/include/net/dhcpv6.h @@ -80,7 +80,9 @@ extern "C" { #define DHCPV6_OPT_IA_PD (25U) /**< identity association for prefix * delegation (IA_PD) option */ #define DHCPV6_OPT_IAPFX (26U) /**< IA prefix option */ +#define DHCPV6_OPT_IRT (32U) /**< Information Refresh Time Option */ #define DHCPV6_OPT_SMR (82U) /**< SOL_MAX_RT option */ +#define DHCPV6_OPT_IMR (83U) /**< INF_MAX_RT option */ #define DHCPV6_OPT_MUD_URL (112U) /**< MUD URL option (see RFC 8520) */ /** @} */ diff --git a/sys/include/net/dhcpv6/client.h b/sys/include/net/dhcpv6/client.h index 13b29429b7..f836717283 100644 --- a/sys/include/net/dhcpv6/client.h +++ b/sys/include/net/dhcpv6/client.h @@ -280,6 +280,29 @@ static inline uint32_t dhcpv6_client_addr_valid_until(unsigned netif, */ #define MAX_MUD_URL_LENGTH (0xFF - sizeof(dhcpv6_opt_mud_url_t)) +/** + * @brief Definition of DHCPv6 client configuration modes. + */ +enum { + DHCPV6_CLIENT_CONF_MODE_INACTIVE, + DHCPV6_CLIENT_CONF_MODE_STATEFUL, + DHCPV6_CLIENT_CONF_MODE_STATELESS, +}; + +/** + * @brief Changes the DHCPv6 client's configuration mode. + * + * @param[in] configuration_mode The new configuration mode. + */ +void dhcpv6_client_set_conf_mode(uint8_t configuration_mode); + +/** + * @brief Retrieves the DHCPv6 client's current configuration mode. + * + * @return The current configuration mode. + */ +uint8_t dhcpv6_client_get_conf_mode(void); + /** @} */ #ifdef __cplusplus diff --git a/sys/net/application_layer/dhcpv6/_dhcpv6.h b/sys/net/application_layer/dhcpv6/_dhcpv6.h index 5c7494f57a..9547617332 100644 --- a/sys/net/application_layer/dhcpv6/_dhcpv6.h +++ b/sys/net/application_layer/dhcpv6/_dhcpv6.h @@ -67,6 +67,14 @@ extern "C" { #define DHCPV6_REB_TIMEOUT (10U) /**< REB_TIMEOUT (in sec) */ #define DHCPV6_REB_MAX_RT (600U) /**< REB_MAX_RT (in sec) */ + +#define DHCPV6_INF_MAX_DELAY (1U) /**< INF_MAX_DELAY (in sec) */ +#define DHCPV6_INF_TIMEOUT (1U) /**< INF_TIMEOUT (in sec) */ +#define DHCPV6_INF_MAX_RT (3600U) /**< INF_MAX_RT (in sec) */ + +#define DHCPV6_IRT_DEFAULT (86400U) /**< IRT_DEFAULT (in sec) */ +#define DHCPV6_IRT_MINIMUM (600U) /**< IRT_MINIMUM (in sec) */ + /** @} */ #define DHCPV6_DUID_MAX_LEN (128U) /**< maximum length of DUID */ @@ -286,6 +294,18 @@ typedef struct __attribute__((packed)) { uint8_t opts[]; /**< IAprefix options */ } dhcpv6_opt_iapfx_t; +/** + * @brief DHCPv6 Information Refresh Time option format + * @see [RFC 8415, section 21.23] + * (https://tools.ietf.org/html/rfc8415#section-21.23) + */ +typedef struct __attribute__((packed)) { + network_uint16_t type; /**< @ref DHCPV6_OPT_IRT */ + network_uint16_t len; /**< always 4 */ + network_uint32_t value; /**< Time duration relative to the + current time (in sec) */ +} dhcpv6_opt_irt_t; + /** * @brief DHCPv6 SOL_MAX_RT option format * @see [RFC 8415, section 21.24] @@ -297,6 +317,17 @@ typedef struct __attribute__((packed)) { network_uint32_t value; /**< overriding value for SOL_MAX_RT (in sec) */ } dhcpv6_opt_smr_t; +/** + * @brief DHCPv6 INF_MAX_RT option format + * @see [RFC 8415, section 21.25] + * (https://tools.ietf.org/html/rfc8415#section-21.25) + */ +typedef struct __attribute__((packed)) { + network_uint16_t type; /**< @ref DHCPV6_OPT_IMR */ + network_uint16_t len; /**< always 4 */ + network_uint32_t value; /**< overriding value for INF_MAX_RT (in sec) */ +} dhcpv6_opt_imr_t; + /** * @brief MUD URL DHCPv6 option format * @see [RFC 8520, section 10] diff --git a/sys/net/application_layer/dhcpv6/client.c b/sys/net/application_layer/dhcpv6/client.c index 27c7d06afb..94b2a12436 100644 --- a/sys/net/application_layer/dhcpv6/client.c +++ b/sys/net/application_layer/dhcpv6/client.c @@ -88,7 +88,7 @@ static uint8_t duid[DHCPV6_CLIENT_DUID_LEN]; static addr_lease_t addr_leases[CONFIG_DHCPV6_CLIENT_ADDR_LEASE_MAX]; static pfx_lease_t pfx_leases[CONFIG_DHCPV6_CLIENT_PFX_LEASE_MAX]; static server_t server; -static event_timeout_t solicit_renew_timeout, rebind_timeout; +static event_timeout_t solicit_renew_timeout, information_refresh_timeout, rebind_timeout; static event_queue_t *event_queue; static sock_udp_t sock; static sock_udp_ep_t local = { .family = AF_INET6, .port = DHCPV6_CLIENT_PORT }; @@ -97,14 +97,17 @@ static sock_udp_ep_t remote = { .family = AF_INET6, .port = DHCPV6_SERVER_PORT, .ipv6 = DHCPV6_ALL_RELAY_AGENTS_AND_SERVERS } }; static uint32_t sol_max_rt = DHCPV6_SOL_MAX_RT; +static uint32_t inf_max_rt = DHCPV6_INF_MAX_RT; static uint32_t t2, rebind_time; static uint32_t transaction_start; static uint32_t transaction_id; static uint8_t duid_len = sizeof(dhcpv6_duid_l2_t); +static uint8_t configuration_mode = DHCPV6_CLIENT_CONF_MODE_INACTIVE; static const char mud_url[] = CONFIG_DHCPV6_CLIENT_MUD_URL; static void _post_solicit_servers(void); +static void _refresh_information(event_t *event); static void _solicit_servers(event_t *event); static void _request(event_t *event); static void _renew(event_t *event); @@ -116,6 +119,7 @@ static void _set_event_timeout_sec(event_timeout_t *timeout, event_t *event, uint32_t delay_sec); static void _clear_event_timeout(event_timeout_t *timeout); +static event_t refresh_information = { .handler = _refresh_information }; static event_t solicit_servers = { .handler = _solicit_servers }; static event_t request = { .handler = _request }; static event_t renew = { .handler = _renew }; @@ -202,16 +206,53 @@ void dhcpv6_client_init(event_queue_t *eq, uint16_t netif) remote.netif = netif; } +static void _restart(void) +{ + + _clear_event_timeout(&solicit_renew_timeout); + _clear_event_timeout(&rebind_timeout); + _clear_event_timeout(&information_refresh_timeout); + + switch (configuration_mode) + { + case DHCPV6_CLIENT_CONF_MODE_INACTIVE: + return; + case DHCPV6_CLIENT_CONF_MODE_STATEFUL: { + uint32_t delay = random_uint32_range(0, DHCPV6_SOL_MAX_DELAY * MS_PER_SEC); + _set_event_timeout_ms(&solicit_renew_timeout, &solicit_servers, delay); + break; + } + case DHCPV6_CLIENT_CONF_MODE_STATELESS: { + uint32_t delay = random_uint32_range(0, DHCPV6_INF_MAX_DELAY * MS_PER_SEC); + _set_event_timeout_ms(&information_refresh_timeout, &refresh_information, delay); + break; + } + default: + DEBUG("DHCPv6 Client: Invalid configuration mode!"); + assert(0); + break; + } +} + +void dhcpv6_client_set_conf_mode(uint8_t _configuration_mode) { + if (configuration_mode != _configuration_mode) { + configuration_mode = _configuration_mode; + _restart(); + } +} + +uint8_t dhcpv6_client_get_conf_mode(void) { + return configuration_mode; +} + void dhcpv6_client_start(void) { duid_len = dhcpv6_client_get_duid_l2(local.netif, (dhcpv6_duid_l2_t *)&duid); assert(event_queue != NULL); if (duid_len > 0) { - uint32_t delay = random_uint32_range(0, DHCPV6_SOL_MAX_DELAY * MS_PER_SEC); - sock_udp_create(&sock, &local, NULL, 0); - _set_event_timeout_ms(&solicit_renew_timeout, &solicit_servers, delay); + _restart(); } } @@ -246,6 +287,8 @@ int dhcpv6_client_req_ia_na(unsigned netif) return -ENOTSUP; } + dhcpv6_client_set_conf_mode(DHCPV6_CLIENT_CONF_MODE_STATEFUL); + addr_lease_t *lease = NULL; for (unsigned i = 0; i < CONFIG_DHCPV6_CLIENT_ADDR_LEASE_MAX; i++) { @@ -532,6 +575,10 @@ static bool _check_cid_opt(dhcpv6_opt_duid_t *cid) static bool _check_sid_opt(dhcpv6_opt_duid_t *sid) { + if (configuration_mode == DHCPV6_CLIENT_CONF_MODE_STATELESS) { + return true; + } + if (IS_ACTIVE(ENABLE_DEBUG)) { if ((byteorder_ntohs(sid->len) != server.duid_len) || (memcmp(sid->duid, server.duid.u8, server.duid_len) != 0)) { @@ -932,13 +979,15 @@ static bool _parse_ia_na_option(dhcpv6_opt_ia_na_t *ia_na) return true; } -static bool _parse_reply(uint8_t *rep, size_t len) +static bool _parse_reply(uint8_t *rep, size_t len, uint8_t request_type) { dhcpv6_opt_duid_t *cid = NULL, *sid = NULL; dhcpv6_opt_ia_pd_t *ia_pd = NULL; dhcpv6_opt_ia_na_t *ia_na = NULL; dhcpv6_opt_status_t *status = NULL; dhcpv6_opt_smr_t *smr = NULL; + dhcpv6_opt_imr_t *imr = NULL; + dhcpv6_opt_irt_t *irt = NULL; size_t orig_len = len; DEBUG("DHCPv6 client: received REPLY\n"); @@ -975,6 +1024,12 @@ static bool _parse_reply(uint8_t *rep, size_t len) case DHCPV6_OPT_SMR: smr = (dhcpv6_opt_smr_t *)opt; break; + case DHCPV6_OPT_IMR: + imr = (dhcpv6_opt_imr_t *)opt; + break; + case DHCPV6_OPT_IRT: + irt = (dhcpv6_opt_irt_t *)opt; + break; default: break; } @@ -992,6 +1047,21 @@ static bool _parse_reply(uint8_t *rep, size_t len) if (smr != NULL) { sol_max_rt = byteorder_ntohl(smr->value); } + if (imr != NULL) { + inf_max_rt = byteorder_ntohl(imr->value); + } + if (request_type == DHCPV6_INFO_REQUEST){ + uint32_t refresh_time; + if (irt != NULL) { + refresh_time = byteorder_ntohl(irt->value); + if (refresh_time < DHCPV6_IRT_MINIMUM) { + refresh_time = DHCPV6_IRT_MINIMUM; + } + } else { + refresh_time = DHCPV6_IRT_DEFAULT; + } + _set_event_timeout_sec(&information_refresh_timeout, &refresh_information, refresh_time); + } if (!_check_status_opt(status)) { return false; } @@ -1028,19 +1098,17 @@ static bool _parse_reply(uint8_t *rep, size_t len) return true; } -static size_t _compose_message(dhcpv6_msg_t *msg, uint8_t type) +static size_t _compose_message(dhcpv6_msg_t *msg, uint8_t type, bool reconfigure) { msg->type = type; _generate_tid(); _set_tid(msg->tid); - uint16_t oro_opts[] = { DHCPV6_OPT_SMR }; size_t msg_len = sizeof(dhcpv6_msg_t); - transaction_start = _now_cs(); - msg_len += _compose_cid_opt((dhcpv6_opt_duid_t *)&send_buf[msg_len]); - if (type != DHCPV6_REBIND && type != DHCPV6_SOLICIT) { + if (type != DHCPV6_REBIND && type != DHCPV6_SOLICIT && + (type != DHCPV6_INFO_REQUEST && !reconfigure)) { /* See RFC 8415, Appendix B */ msg_len += _compose_sid_opt((dhcpv6_opt_duid_t *)&send_buf[msg_len]); } @@ -1048,8 +1116,19 @@ static size_t _compose_message(dhcpv6_msg_t *msg, uint8_t type) msg_len += _compose_mud_url_opt((dhcpv6_opt_mud_url_t *)&send_buf[msg_len], sizeof(send_buf) - msg_len); - msg_len += _compose_oro_opt((dhcpv6_opt_oro_t *)&send_buf[msg_len], oro_opts, - ARRAY_SIZE(oro_opts)); + if (type == DHCPV6_INFO_REQUEST) { + uint16_t info_req_oro_opts[] = { + DHCPV6_OPT_SMR, + DHCPV6_OPT_IRT, + DHCPV6_OPT_IMR, + }; + msg_len += _compose_oro_opt((dhcpv6_opt_oro_t *)&send_buf[msg_len], info_req_oro_opts, + ARRAY_SIZE(info_req_oro_opts)); + } else { + uint16_t general_oro_opts[] = {DHCPV6_OPT_SMR}; + msg_len += _compose_oro_opt((dhcpv6_opt_oro_t *)&send_buf[msg_len], general_oro_opts, + ARRAY_SIZE(general_oro_opts)); + } msg_len += _add_ia_na(&send_buf[msg_len], sizeof(send_buf) - msg_len); msg_len += _add_ia_pd_from_config(&send_buf[msg_len], sizeof(send_buf) - msg_len); @@ -1069,7 +1148,7 @@ static void _solicit_servers(event_t *event) (void)event; - msg_len = _compose_message(msg, DHCPV6_SOLICIT); + msg_len = _compose_message(msg, DHCPV6_SOLICIT, false); time = (dhcpv6_opt_elapsed_time_t *)&send_buf[msg_len]; msg_len += _compose_elapsed_time_opt(time); @@ -1151,7 +1230,7 @@ static uint32_t _calculate_mrd_from_leases(void) return mrd; } -static void _request_renew_rebind(uint8_t type) +static void _request_renew_rebind(uint8_t type, bool reconfigure) { dhcpv6_msg_t *msg = (dhcpv6_msg_t *)&send_buf[0]; dhcpv6_opt_elapsed_time_t *time; @@ -1165,6 +1244,10 @@ static void _request_renew_rebind(uint8_t type) uint32_t mrd = 0; switch (type) { + case DHCPV6_INFO_REQUEST: + irt = DHCPV6_INF_TIMEOUT; + mrt = DHCPV6_OPT_IMR; + break; case DHCPV6_REQUEST: irt = DHCPV6_REQ_TIMEOUT; mrt = DHCPV6_REQ_MAX_RT; @@ -1190,8 +1273,14 @@ static void _request_renew_rebind(uint8_t type) default: return; } - retrans_timeout = _irt_ms(irt, false); - msg_len = _compose_message(msg, type); + + if (type == DHCPV6_INFO_REQUEST) { + retrans_timeout = _irt_ms(inf_max_rt, false); + } else { + retrans_timeout = _irt_ms(irt, false); + } + + msg_len = _compose_message(msg, type, reconfigure); time = (dhcpv6_opt_elapsed_time_t *)&send_buf[msg_len]; msg_len += _compose_elapsed_time_opt(time); @@ -1215,7 +1304,7 @@ static void _request_renew_rebind(uint8_t type) sock_udp_send(&sock, send_buf, msg_len, &remote); } if ((res > 0) && (recv_buf[0] == DHCPV6_REPLY)) { - if (!_parse_reply(recv_buf, res)) { + if (!_parse_reply(recv_buf, res, type)) { /* try again */ event_post(event_queue, &request); } @@ -1229,21 +1318,28 @@ static void _request(event_t *event) { (void)event; DEBUG("DHCPv6 client: send REQUEST\n"); - _request_renew_rebind(DHCPV6_REQUEST); + _request_renew_rebind(DHCPV6_REQUEST, false); } static void _renew(event_t *event) { (void)event; DEBUG("DHCPv6 client: send RENEW\n"); - _request_renew_rebind(DHCPV6_RENEW); + _request_renew_rebind(DHCPV6_RENEW, false); } static void _rebind(event_t *event) { (void)event; DEBUG("DHCPv6 client: send REBIND\n"); - _request_renew_rebind(DHCPV6_REBIND); + _request_renew_rebind(DHCPV6_REBIND, false); +} + +static void _refresh_information(event_t *event) +{ + (void)event; + DEBUG("DHCPv6 client: send INFORMATION REQUEST\n"); + _request_renew_rebind(DHCPV6_INFO_REQUEST, false); } static void _set_event_timeout_ms(event_timeout_t *timeout, event_t *event, diff --git a/sys/net/gnrc/network_layer/ipv6/nib/nib.c b/sys/net/gnrc/network_layer/ipv6/nib/nib.c index e99ce3b715..710322acde 100644 --- a/sys/net/gnrc/network_layer/ipv6/nib/nib.c +++ b/sys/net/gnrc/network_layer/ipv6/nib/nib.c @@ -32,6 +32,9 @@ #if IS_ACTIVE(CONFIG_GNRC_IPV6_NIB_DNS) #include "net/sock/dns.h" #endif +#if IS_ACTIVE(MODULE_DHCPV6_CLIENT) +#include "net/dhcpv6/client.h" +#endif #include "_nib-internal.h" #include "_nib-arsm.h" @@ -787,6 +790,20 @@ static void _handle_rtr_adv(gnrc_netif_t *netif, const ipv6_hdr_t *ipv6, if (dr == NULL) { return; } +#if IS_ACTIVE(MODULE_DHCPV6_CLIENT) + uint8_t current_conf_mode = dhcpv6_client_get_conf_mode(); + if (rtr_adv->flags & NDP_RTR_ADV_FLAGS_M) { + if (IS_USED(MODULE_DHCPV6_CLIENT_IA_NA)) { + netif->ipv6.aac_mode |= GNRC_NETIF_AAC_DHCP; + dhcpv6_client_req_ia_na(netif->pid); + } else { + dhcpv6_client_set_conf_mode(DHCPV6_CLIENT_CONF_MODE_STATEFUL); + } + } else if ((rtr_adv->flags & NDP_RTR_ADV_FLAGS_O) && + (current_conf_mode != DHCPV6_CLIENT_CONF_MODE_STATEFUL)) { + dhcpv6_client_set_conf_mode(DHCPV6_CLIENT_CONF_MODE_STATELESS); + } +#endif /* MODULE_DHCPV6_CLIENT */ /* stop sending router solicitations * see https://tools.ietf.org/html/rfc4861#section-6.3.7 */ From 4297bfcdeeaf4a53df091e821bd3634a2270e742 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Wed, 18 Aug 2021 19:44:14 +0200 Subject: [PATCH 2/4] tests: Add test for stateless DHCPv6 --- tests/gnrc_dhcpv6_client_stateless/Makefile | 48 ++++ .../Makefile.board.dep | 4 + .../gnrc_dhcpv6_client_stateless/Makefile.ci | 51 ++++ tests/gnrc_dhcpv6_client_stateless/README.md | 43 +++ tests/gnrc_dhcpv6_client_stateless/app.config | 3 + tests/gnrc_dhcpv6_client_stateless/main.c | 27 ++ .../test-graph.svg | 47 ++++ .../tests-as-root/01-run.py | 250 ++++++++++++++++++ 8 files changed, 473 insertions(+) create mode 100644 tests/gnrc_dhcpv6_client_stateless/Makefile create mode 100644 tests/gnrc_dhcpv6_client_stateless/Makefile.board.dep create mode 100644 tests/gnrc_dhcpv6_client_stateless/Makefile.ci create mode 100644 tests/gnrc_dhcpv6_client_stateless/README.md create mode 100644 tests/gnrc_dhcpv6_client_stateless/app.config create mode 100644 tests/gnrc_dhcpv6_client_stateless/main.c create mode 100644 tests/gnrc_dhcpv6_client_stateless/test-graph.svg create mode 100755 tests/gnrc_dhcpv6_client_stateless/tests-as-root/01-run.py diff --git a/tests/gnrc_dhcpv6_client_stateless/Makefile b/tests/gnrc_dhcpv6_client_stateless/Makefile new file mode 100644 index 0000000000..bcaab2663e --- /dev/null +++ b/tests/gnrc_dhcpv6_client_stateless/Makefile @@ -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 diff --git a/tests/gnrc_dhcpv6_client_stateless/Makefile.board.dep b/tests/gnrc_dhcpv6_client_stateless/Makefile.board.dep new file mode 100644 index 0000000000..32b35c7d54 --- /dev/null +++ b/tests/gnrc_dhcpv6_client_stateless/Makefile.board.dep @@ -0,0 +1,4 @@ +# Put board specific dependencies here +ifneq (native,$(BOARD)) + USEMODULE += stdio_ethos +endif diff --git a/tests/gnrc_dhcpv6_client_stateless/Makefile.ci b/tests/gnrc_dhcpv6_client_stateless/Makefile.ci new file mode 100644 index 0000000000..0bdbe566bb --- /dev/null +++ b/tests/gnrc_dhcpv6_client_stateless/Makefile.ci @@ -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 \ + # diff --git a/tests/gnrc_dhcpv6_client_stateless/README.md b/tests/gnrc_dhcpv6_client_stateless/README.md new file mode 100644 index 0000000000..fe7de84747 --- /dev/null +++ b/tests/gnrc_dhcpv6_client_stateless/README.md @@ -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 diff --git a/tests/gnrc_dhcpv6_client_stateless/app.config b/tests/gnrc_dhcpv6_client_stateless/app.config new file mode 100644 index 0000000000..23e61165bd --- /dev/null +++ b/tests/gnrc_dhcpv6_client_stateless/app.config @@ -0,0 +1,3 @@ +CONFIG_KCONFIG_USEMODULE_GNRC_IPV6_NIB=y +CONFIG_GNRC_IPV6_NIB_ARSM=y +CONFIG_GNRC_IPV6_NIB_SLAAC=y diff --git a/tests/gnrc_dhcpv6_client_stateless/main.c b/tests/gnrc_dhcpv6_client_stateless/main.c new file mode 100644 index 0000000000..e0eeb3ec91 --- /dev/null +++ b/tests/gnrc_dhcpv6_client_stateless/main.c @@ -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 + */ + +#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; +} + +/** @} */ diff --git a/tests/gnrc_dhcpv6_client_stateless/test-graph.svg b/tests/gnrc_dhcpv6_client_stateless/test-graph.svg new file mode 100644 index 0000000000..2435680573 --- /dev/null +++ b/tests/gnrc_dhcpv6_client_stateless/test-graph.svg @@ -0,0 +1,47 @@ + + + + + + +Automaton_metaclass + + + +WAITING_FOR_NDP_RS + +WAITING_FOR_NDP_RS + + + +WAITING_FOR_DHCP_IR + +WAITING_FOR_DHCP_IR + + + +WAITING_FOR_NDP_RS->WAITING_FOR_DHCP_IR + + +received_ICMP +>[on_NDP_RS] + + + +END + +END + + + +WAITING_FOR_DHCP_IR->END + + +received_DHCPv6 +>[on_DHCPv6_IR] + + + diff --git a/tests/gnrc_dhcpv6_client_stateless/tests-as-root/01-run.py b/tests/gnrc_dhcpv6_client_stateless/tests-as-root/01-run.py new file mode 100755 index 0000000000..825a8959b8 --- /dev/null +++ b/tests/gnrc_dhcpv6_client_stateless/tests-as-root/01-run.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2018 Freie Universität Berlin +# 2021 Jan Romann +# +# 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)) From ff42d6749b983836353344f94af1e0122ecc3767 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Wed, 1 Sep 2021 00:52:01 +0200 Subject: [PATCH 3/4] sys/net/gnrc/dhcpv6: Set stateful configuration mode --- sys/net/gnrc/application_layer/dhcpv6/client_simple_pd.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sys/net/gnrc/application_layer/dhcpv6/client_simple_pd.c b/sys/net/gnrc/application_layer/dhcpv6/client_simple_pd.c index 1de9fca0e5..d370c09385 100644 --- a/sys/net/gnrc/application_layer/dhcpv6/client_simple_pd.c +++ b/sys/net/gnrc/application_layer/dhcpv6/client_simple_pd.c @@ -133,6 +133,8 @@ static void *_dhcpv6_cl_simple_pd_thread(void *args) dhcpv6_client_init(&event_queue, upstream_netif->pid); /* configure client to request prefix delegation for WPAN interfaces */ _configure_dhcpv6_client(); + /* set client configuration mode to stateful */ + dhcpv6_client_set_conf_mode(DHCPV6_CLIENT_CONF_MODE_STATEFUL); /* start DHCPv6 client */ dhcpv6_client_start(); /* start event loop of DHCPv6 client */ From 1db7a2770e2bf035c910688f523bf65465cf4a27 Mon Sep 17 00:00:00 2001 From: Jan Romann Date: Wed, 1 Sep 2021 00:56:32 +0200 Subject: [PATCH 4/4] tests/gnrc_dhcpv6_client: Set stateful configuration mode --- tests/gnrc_dhcpv6_client/main.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/gnrc_dhcpv6_client/main.c b/tests/gnrc_dhcpv6_client/main.c index dde6c19ff3..19741e5752 100644 --- a/tests/gnrc_dhcpv6_client/main.c +++ b/tests/gnrc_dhcpv6_client/main.c @@ -44,6 +44,8 @@ void *_dhcpv6_client_thread(void *args) /* configure client to request prefix delegation of /64 subnet * interface netif */ dhcpv6_client_req_ia_pd(netif->pid, 64U); + /* set client configuration mode to stateful */ + dhcpv6_client_set_conf_mode(DHCPV6_CLIENT_CONF_MODE_STATEFUL); /* start DHCPv6 client */ dhcpv6_client_start(); /* start event loop of DHCPv6 client */