diff --git a/makefiles/pseudomodules.inc.mk b/makefiles/pseudomodules.inc.mk index acb2926f9e..d2c6506865 100644 --- a/makefiles/pseudomodules.inc.mk +++ b/makefiles/pseudomodules.inc.mk @@ -27,6 +27,7 @@ PSEUDOMODULES += dhcpv6_% PSEUDOMODULES += dhcpv6_client_dns PSEUDOMODULES += dhcpv6_client_ia_pd PSEUDOMODULES += dhcpv6_client_mud_url +PSEUDOMODULES += dhcpv6_relay PSEUDOMODULES += dns_msg PSEUDOMODULES += ecc_% PSEUDOMODULES += event_% diff --git a/sys/Makefile.dep b/sys/Makefile.dep index 169591ba90..82fbd63306 100644 --- a/sys/Makefile.dep +++ b/sys/Makefile.dep @@ -110,6 +110,16 @@ ifneq (,$(filter dhcpv6_client,$(USEMODULE))) endif endif +ifneq (,$(filter dhcpv6_relay,$(USEMODULE))) + USEMODULE += event + USEMODULE += sock_async_event + USEMODULE += sock_udp +endif + +ifneq (,$(filter auto_init_dhcpv6_relay,$(USEMODULE))) + USEMODULE += dhcpv6_relay +endif + ifneq (,$(filter dns_%,$(USEMODULE))) USEMODULE += dns endif diff --git a/sys/auto_init/auto_init.c b/sys/auto_init/auto_init.c index 79a6d44094..fdc1b0b1b0 100644 --- a/sys/auto_init/auto_init.c +++ b/sys/auto_init/auto_init.c @@ -267,6 +267,12 @@ void auto_init(void) dhcpv6_client_auto_init(); } + if (IS_USED(MODULE_AUTO_INIT_DHCPV6_RELAY)) { + LOG_DEBUG("Auto init DHCPv6 relay agent.\n"); + extern void dhcpv6_relay_auto_init(void); + dhcpv6_relay_auto_init(); + } + if (IS_USED(MODULE_GNRC_DHCPV6_CLIENT_SIMPLE_PD)) { LOG_DEBUG("Auto init DHCPv6 client for simple prefix delegation\n"); extern void gnrc_dhcpv6_client_simple_pd_init(void); diff --git a/sys/include/net/dhcpv6.h b/sys/include/net/dhcpv6.h index 47497cf642..2a8e518886 100644 --- a/sys/include/net/dhcpv6.h +++ b/sys/include/net/dhcpv6.h @@ -47,9 +47,15 @@ extern "C" { #define DHCPV6_SOLICIT (1U) /**< SOLICIT */ #define DHCPV6_ADVERTISE (2U) /**< ADVERTISE */ #define DHCPV6_REQUEST (3U) /**< REQUEST */ +#define DHCPV6_CONFIRM (4U) /**< CONFIRM */ #define DHCPV6_RENEW (5U) /**< RENEW */ #define DHCPV6_REBIND (6U) /**< REBIND */ #define DHCPV6_REPLY (7U) /**< REPLY */ +#define DHCPV6_RELEASE (8U) /**< RELEASE */ +#define DHCPV6_DECLINE (9U) /**< DECLINE */ +#define DHCPV6_INFO_REQUEST (11U) /**< INFORMATION-REQUEST */ +#define DHCPV6_RELAY_FORW (12U) /**< RELAY-FORW */ +#define DHCPV6_RELAY_REPL (13U) /**< RELAY-REPL */ /** @ } */ /** @@ -64,7 +70,9 @@ extern "C" { #define DHCPV6_OPT_ORO (6U) /**< option request option */ #define DHCPV6_OPT_PREF (7U) /**< preference option */ #define DHCPV6_OPT_ELAPSED_TIME (8U) /**< elapsed time option */ +#define DHCPV6_OPT_RELAY_MSG (9U) /**< relay message option */ #define DHCPV6_OPT_STATUS (13U) /**< status code option */ +#define DHCPV6_OPT_IID (18U) /**< interface-id option */ #define DHCPV6_OPT_DNS_RNS (23U) /**< DNS recursive name server option */ #define DHCPV6_OPT_IA_PD (25U) /**< identity association for prefix * delegation (IA_PD) option */ diff --git a/sys/include/net/dhcpv6/client.h b/sys/include/net/dhcpv6/client.h index e86181f03f..24ae3d1304 100644 --- a/sys/include/net/dhcpv6/client.h +++ b/sys/include/net/dhcpv6/client.h @@ -48,7 +48,7 @@ extern "C" { #define DHCPV6_CLIENT_BUFLEN (256) /**< default length for send and receive buffer */ /** - * @defgroup net_dhcpv6_conf DHCPv6 client compile configurations + * @defgroup net_dhcpv6_conf DHCPv6 compile configurations * @ingroup config * @{ */ diff --git a/sys/include/net/dhcpv6/relay.h b/sys/include/net/dhcpv6/relay.h new file mode 100644 index 0000000000..7c66e72e7f --- /dev/null +++ b/sys/include/net/dhcpv6/relay.h @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2021 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. + */ + +/** + * @defgroup net_dhcpv6_relay DHCPv6 relay agent + * @ingroup net_dhcpv6 + * @brief DHCPv6 relay agent implementation + * @{ + * + * @file + * @brief DHCPv6 client definitions + * + * @author Martine Lenders + */ +#ifndef NET_DHCPV6_RELAY_H +#define NET_DHCPV6_RELAY_H + +#include + +#include "event.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @addtogroup net_dhcpv6_conf + * @{ + */ +/** + * @brief Maximum hop count in a relay-forward message (HOP_COUNT_LIMIT) + * + * @see [RFC 8415, section 7.6](https://tools.ietf.org/html/rfc8415#section-7.6) + */ +#ifndef CONFIG_DHCPV6_RELAY_HOP_LIMIT +#define CONFIG_DHCPV6_RELAY_HOP_LIMIT (8U) +#endif + +#ifndef CONFIG_DHCPV6_RELAY_BUFLEN +#define CONFIG_DHCPV6_RELAY_BUFLEN (256U) /**< default length for send and receive buffer */ +#endif +/** @} */ + +/** + * @brief Auto-initializes the relay agent in its own thread or event thread + * when available + * + * @note Only used with `auto_init_dhcpv6_relay`. + */ +void dhcpv6_relay_auto_init(void); + +/** + * @brief Initializes the relay agent + * + * @pre `event_queue->waiter != NULL` (event queue is initialized) + * + * @param[in] event_queue Event queue to use with the relay agent. Needs to + * be initialized in the handler thread. + * @param[in] listen_netif The network interface the relay agent listens on for + * incoming client or relay forward messages from other + * relay agents. + * @param[in] fwd_netif The network interface the relay agent relays + * messages upstreams and listens for relay replies + * on. + */ +void dhcpv6_relay_init(event_queue_t *event_queue, uint16_t listen_netif, + uint16_t fwd_netif); + +#ifdef __cplusplus +} +#endif + +#endif /* NET_DHCPV6_RELAY_H */ +/** @} */ diff --git a/sys/net/application_layer/dhcpv6/Kconfig b/sys/net/application_layer/dhcpv6/Kconfig index 0ab324af0f..be1cd59f86 100644 --- a/sys/net/application_layer/dhcpv6/Kconfig +++ b/sys/net/application_layer/dhcpv6/Kconfig @@ -4,13 +4,21 @@ # General Public License v2.1. See the file LICENSE in the top level # directory for more details. # + menuconfig KCONFIG_USEMODULE_DHCPV6 bool "Configure DHCPv6" depends on USEMODULE_DHCPV6 help - Configure DHCPv6 client using Kconfig. + Configure DHCPv6 using Kconfig. if KCONFIG_USEMODULE_DHCPV6 +menuconfig KCONFIG_USEMODULE_DHCPV6_CLIENT + bool "Configure DHCPv6 client" + depends on USEMODULE_DHCPV6_CLIENT + help + Configure DHCPv6 client using Kconfig. + +if KCONFIG_USEMODULE_DHCPV6_CLIENT config DHCPV6_CLIENT_PFX_LEASE_MAX int "Maximum number of prefix leases to be stored" @@ -31,4 +39,23 @@ config DHCPV6_CLIENT_MUD_URL string "URL pointing to a Manufacturer Usage Description file" endif # KCONFIG_USEMODULE_DHCPV6_CLIENT_MUD_URL -endif # KCONFIG_USEMODULE_DHCPv6 +endif # KCONFIG_USEMODULE_DHCPV6_CLIENT + +menuconfig KCONFIG_USEMODULE_DHCPV6_RELAY + bool "Configure DHCPv6 relay agent" + depends on USEMODULE_DHCPV6_RELAY + help + Configure DHCPv6 relay agent using Kconfig. + +if KCONFIG_USEMODULE_DHCPV6_RELAY + +config DHCPV6_RELAY_HOP_LIMIT + int "Maximum hop count in relay-forward message (HOP_COUNT_LIMIT)" + default 8 + +config DHCPV6_RELAY_BUFLEN + int "Default length of relay agent send and receive buffer" + default 256 + +endif # KCONFIG_USEMODULE_DHCPV6_RELAY +endif # KCONFIG_USEMODULE_DHCPV6 diff --git a/sys/net/application_layer/dhcpv6/_dhcpv6.h b/sys/net/application_layer/dhcpv6/_dhcpv6.h index 8fc8bebe36..3e6cb4722d 100644 --- a/sys/net/application_layer/dhcpv6/_dhcpv6.h +++ b/sys/net/application_layer/dhcpv6/_dhcpv6.h @@ -84,6 +84,26 @@ typedef struct __attribute__((packed)) { uint8_t type; /**< message type (see [DHCPv6 messeg types ](@ref net_dhcp6_msg_types)) */ uint8_t tid[3]; /**< transaction ID */ } dhcpv6_msg_t; + +/** + * @brief Relay Agents/Server message format + * @see [RFC 8415, section 9] + * (https://tools.ietf.org/html/rfc8415#section-9) + */ +typedef struct __attribute__((packed)) { + uint8_t type; /**< message type (see [DHCPv6 messeg types ](@ref net_dhcp6_msg_types)) */ + uint8_t hop_count; /**< number of relays that have already relayed the message */ + /** + * @brief optional address to identify the link on which the client is + * located. + */ + ipv6_addr_t link_address; + /** + * @brief The address of the client or relay agent from which the message + * to be relayed was received. + */ + ipv6_addr_t peer_address; +} dhcpv6_relay_msg_t; /** @} */ /** @@ -160,6 +180,17 @@ typedef struct __attribute__((packed)) { network_uint16_t elapsed_time; } dhcpv6_opt_elapsed_time_t; +/** + * @brief DHCPv6 relay message option + * @see [RFC 8415, section 21.10] + * (https://tools.ietf.org/html/rfc8415#section-21.10) + */ +typedef struct __attribute__((packed)) { + network_uint16_t type; /**< @ref DHCPV6_OPT_RELAY_MSG */ + network_uint16_t len; /**< length of dhcpv6_opt_iid_t::msg in byte */ + uint16_t msg[]; /**< the relayed message */ +} dhcpv6_opt_relay_msg_t; + /** * @brief DHCPv6 status code option format * @see [RFC 8415, section 21.13] @@ -172,6 +203,17 @@ typedef struct __attribute__((packed)) { char msg[]; /**< UTF-8 encoded text string (not 0-terminated!) */ } dhcpv6_opt_status_t; +/** + * @brief DHCPv6 interface-id option + * @see [RFC 8415, section 21.18] + * (https://tools.ietf.org/html/rfc8415#section-21.18) + */ +typedef struct __attribute__((packed)) { + network_uint16_t type; /**< @ref DHCPV6_OPT_IID */ + network_uint16_t len; /**< length of dhcpv6_opt_iid_t::iid in byte */ + uint8_t iid[]; /**< opaque interface identifier */ +} dhcpv6_opt_iid_t; + /** * @brief DHCPv6 DNS recursive name server option * @see [RFC 3646, section 3] diff --git a/sys/net/application_layer/dhcpv6/relay.c b/sys/net/application_layer/dhcpv6/relay.c new file mode 100644 index 0000000000..3fd36b61e0 --- /dev/null +++ b/sys/net/application_layer/dhcpv6/relay.c @@ -0,0 +1,415 @@ +/* + * Copyright (C) 2021 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 + +#include "event.h" +#include "event/thread.h" +#include "log.h" +#include "net/dhcpv6.h" +#include "net/dhcpv6/client.h" /* required for dhcpv6_duid_l2_t in _dhcpv6.h */ +#include "net/dhcpv6/relay.h" +#include "net/ipv6/addr.h" +#include "net/netif.h" +#include "net/sock/async/event.h" +#include "net/sock/udp.h" +#include "net/sock/util.h" +#include "thread.h" + +#include "_dhcpv6.h" + +#define ENABLE_DEBUG 0 +#include "debug.h" + +#define AUTO_INIT_PRIO (THREAD_PRIORITY_MAIN - 1) + +static char _auto_init_stack[THREAD_STACKSIZE_DEFAULT]; +static struct { + uint8_t inbuf[CONFIG_DHCPV6_RELAY_BUFLEN]; + uint8_t outbuf[CONFIG_DHCPV6_RELAY_BUFLEN]; + sock_udp_t *listen_sock; + sock_udp_t *fwd_sock; + uint16_t fwd_netif; +} _relay_state; + +static void *_dhcpv6_relay_auto_init_thread(void *); +static void _udp_handler(sock_udp_t *sock, sock_async_flags_t type, + void *arg); +static void _dhcpv6_handler(const sock_udp_ep_t *remote, const uint8_t *msg, + size_t msg_size); +static void _forward_msg(const sock_udp_ep_t *remote, const uint8_t *msg, size_t + msg_size, bool is_client_msg); +static void _forward_reply(const uint8_t *in_msg, size_t in_msg_size); + +static int16_t _only_one_netif(void) +{ + if (IS_USED(MODULE_NETIF)) { + netif_t *netif = netif_iter(NULL); + + return (netif_iter(netif) == NULL) ? netif_get_id(netif) : -1; + } + else { + return -1; + } +} + +void dhcpv6_relay_auto_init(void) +{ + if (IS_USED(MODULE_AUTO_INIT_DHCPV6_RELAY)) { + int16_t netif = _only_one_netif(); + if (netif > 0) { + if (IS_USED(MODULE_EVENT_THREAD)) { + dhcpv6_relay_init(EVENT_PRIO_LOWEST, netif, netif); + } + else { + thread_create(_auto_init_stack, ARRAY_SIZE(_auto_init_stack), + AUTO_INIT_PRIO, THREAD_CREATE_STACKTEST, + _dhcpv6_relay_auto_init_thread, + (void *)(intptr_t)netif, "dhcpv6_relay"); + } + } + else { + LOG_WARNING("DHCPv6 relay: auto init failed, more than 1 interface\n"); + } + } +} + +static int _join_all_relays_and_server(uint16_t netif_id) +{ + netif_t *netif = netif_get_by_id(netif_id); + ipv6_addr_t all_relays_and_server = { + .u8 = DHCPV6_ALL_RELAY_AGENTS_AND_SERVERS + }; + assert(netif != NULL); + + return netif_set_opt(netif, NETOPT_IPV6_GROUP, 0, &all_relays_and_server, + sizeof(all_relays_and_server)); +} + +void dhcpv6_relay_init(event_queue_t *eq, uint16_t listen_netif, + uint16_t fwd_netif) +{ + sock_udp_ep_t local = { .family = AF_INET6, .port = DHCPV6_SERVER_PORT, + .netif = listen_netif }; + static sock_udp_t listen_sock; + int res; + + assert(eq->waiter != NULL); + memset(&listen_sock, 0, sizeof(listen_sock)); + _relay_state.fwd_netif = fwd_netif; + res = _join_all_relays_and_server(listen_netif); + if (res < 0) { + DEBUG("DHCPv6 relay: unable to join All_DHCP_Relay_Agents_and_Servers: " + "%d\n", -res); + return; + } + /* initialize client-listening sock */ + res = sock_udp_create(&listen_sock, &local, NULL, 0); + if (res < 0) { + DEBUG("DHCPv6 relay: unable to open listen sock: %d\n", -res); + return; + } + _relay_state.listen_sock = &listen_sock; + sock_udp_event_init(_relay_state.listen_sock, eq, _udp_handler, NULL); + + if (listen_netif != fwd_netif) { + static sock_udp_t fwd_sock; + + memset(&fwd_sock, 0, sizeof(fwd_sock)); + /* initialize forwarding / reply-listening sock */ + local.netif = fwd_netif; + res = sock_udp_create(&fwd_sock, &local, NULL, 0); + if (res < 0) { + DEBUG("DHCPv6 relay: unable to open fwd sock: %d\n", -res); + return; + } + _relay_state.fwd_sock = &fwd_sock; + sock_udp_event_init(_relay_state.fwd_sock, eq, _udp_handler, NULL); + } +} + +static void *_dhcpv6_relay_auto_init_thread(void *args) +{ + event_queue_t queue; + int16_t netif = (intptr_t)args; + + event_queue_init(&queue); + dhcpv6_relay_init(&queue, netif, netif); + event_loop(&queue); + return NULL; +} + +static void _udp_handler(sock_udp_t *sock, sock_async_flags_t type, + void *arg) +{ + (void)arg; + if (type == SOCK_ASYNC_MSG_RECV) { + sock_udp_ep_t remote = { .family = AF_INET6 }; + ssize_t res = sock_udp_recv(sock, _relay_state.inbuf, + sizeof(_relay_state.inbuf), 0, &remote); + + if (res < 0) { + DEBUG("DHCPv6 relay: Error receiving UDP message: %d\n", (int)-res); + return; + } + _dhcpv6_handler(&remote, _relay_state.inbuf, res); + } +} + +static void _dhcpv6_handler(const sock_udp_ep_t *remote, const uint8_t *msg, + size_t msg_size) +{ + bool is_client_msg = false; + + if (msg_size == 0) { + DEBUG("DHCPv6 relay: incoming message size 0\n"); + return; + } + if (remote->family != AF_INET6) { + DEBUG("DHCPv6 relay: incoming message source not an IPv6 address\n"); + return; + } + switch (msg[0]) { + case DHCPV6_SOLICIT: + case DHCPV6_REQUEST: + case DHCPV6_CONFIRM: + case DHCPV6_RENEW: + case DHCPV6_REBIND: + case DHCPV6_RELEASE: + case DHCPV6_DECLINE: + case DHCPV6_INFO_REQUEST: + is_client_msg = true; + /* intentionally falls through */ + case DHCPV6_RELAY_FORW: + _forward_msg(remote, msg, msg_size, is_client_msg); + break; + case DHCPV6_RELAY_REPL: + _forward_reply(msg, msg_size); + break; + default: + DEBUG("DHCPv6 relay: unexpected incoming message type %u\n", + msg[0]); + break; + } +} + +static uint16_t _compose_iid_opt(dhcpv6_opt_iid_t *opt, + const sock_udp_ep_t *remote) +{ + opt->type = byteorder_htons(DHCPV6_OPT_IID); + opt->len = byteorder_htons(sizeof(remote->netif)); + memcpy(opt->iid, &remote->netif, sizeof(remote->netif)); + return sizeof(remote->netif) + sizeof(dhcpv6_opt_iid_t); +} + +static uint16_t _compose_relay_msg_opt(dhcpv6_opt_relay_msg_t *opt, + const uint8_t *in_msg, + size_t in_msg_size) +{ + opt->type = byteorder_htons(DHCPV6_OPT_RELAY_MSG); + opt->len = byteorder_htons((uint16_t)in_msg_size); + memcpy(opt->msg, in_msg, in_msg_size); + return (uint16_t)in_msg_size + sizeof(dhcpv6_opt_relay_msg_t); +} + +static bool _addr_unspec(const uint8_t *addr, size_t addr_len) +{ + for (unsigned i = 0; i < addr_len; i++) { + if (addr[i] != 0U) { + return false; + } + } + return true; +} + +static bool _remote_unspec(const sock_udp_ep_t *remote) +{ + switch (remote->family) { + case AF_INET6: + return _addr_unspec(remote->addr.ipv6, sizeof(remote->addr.ipv6)); + default: + return true; + } +} + +static void _forward_msg(const sock_udp_ep_t *remote, const uint8_t *in_msg, + size_t in_msg_size, bool is_client_msg) +{ + dhcpv6_relay_msg_t *out_fwd = (dhcpv6_relay_msg_t *)_relay_state.outbuf; + int res; + sock_udp_ep_t send_remote = { + .family = AF_INET6, + .netif = _relay_state.fwd_netif, + .addr = { .ipv6 = DHCPV6_ALL_RELAY_AGENTS_AND_SERVERS }, + .port = DHCPV6_SERVER_PORT, + }; + uint16_t out_fwd_len = sizeof(dhcpv6_relay_msg_t); + + assert(in_msg_size <= UINT16_MAX); + + if (in_msg_size < sizeof(dhcpv6_msg_t)) { + DEBUG("DHCPv6 relay: incoming message too small\n"); + return; + } + if (_remote_unspec(remote)) { + DEBUG("DHCPv6 relay: incoming message came from unspecified remote\n"); + return; + } + if (is_client_msg) { + out_fwd->hop_count = 0; + } + else { + const dhcpv6_relay_msg_t *in_fwd = (dhcpv6_relay_msg_t *)in_msg; + + if (in_fwd->hop_count > CONFIG_DHCPV6_RELAY_HOP_LIMIT) { + DEBUG("DHCPv6 relay: incoming message exceeded hop limit\n"); + return; + } + if (in_msg_size < sizeof(dhcpv6_relay_msg_t)) { + DEBUG("DHCPv6 relay: incoming forward message too small\n"); + return; + } + /* TODO: check if peer-address is myself to prevent network spam when + * fwd_netif == listen_netif */ + out_fwd->hop_count = in_fwd->hop_count + 1; + } + + out_fwd->type = DHCPV6_RELAY_FORW; + /* set link-address to unspecified address, we will provide an Interface-ID + * option instead */ + memset(&out_fwd->link_address, 0, sizeof(out_fwd->link_address)); + assert(sizeof(out_fwd->peer_address) == sizeof(remote->addr.ipv6)); + memcpy(&out_fwd->peer_address, &remote->addr.ipv6, + sizeof(out_fwd->peer_address)); + + /* set mandatory options */ + out_fwd_len += _compose_iid_opt( + (dhcpv6_opt_iid_t *)&_relay_state.outbuf[out_fwd_len], remote + ); + if ((out_fwd_len + in_msg_size + sizeof(dhcpv6_opt_relay_msg_t)) > + sizeof(_relay_state.outbuf)) { + DEBUG("DHCPv6 relay: output buffer too small to relay message\n"); + return; + } + out_fwd_len += _compose_relay_msg_opt( + (dhcpv6_opt_relay_msg_t *)&_relay_state.outbuf[out_fwd_len], + in_msg, in_msg_size + ); + res = sock_udp_send(_relay_state.fwd_sock, out_fwd, out_fwd_len, + &send_remote); + if (res < 0) { + DEBUG("DHCPv6 relay: sending forward message failed: %d\n", -res); + } +} + +static uint16_t _get_iid(dhcpv6_opt_iid_t *opt) +{ + return (opt->iid[1] << 8) | (opt->iid[0] & 0xff); +} + +static inline size_t _opt_len(dhcpv6_opt_t *opt) +{ + return sizeof(dhcpv6_opt_t) + byteorder_ntohs(opt->len); +} + +static inline dhcpv6_opt_t *_opt_next(dhcpv6_opt_t *opt) +{ + return (dhcpv6_opt_t *)(((uint8_t *)opt) + _opt_len(opt)); +} + +static void _forward_reply(const uint8_t *in_msg, size_t in_msg_size) +{ + const dhcpv6_relay_msg_t *in_reply = (const dhcpv6_relay_msg_t *)in_msg; + const uint8_t *out_msg = NULL; + size_t out_msg_len = 0; + int res; + sock_udp_ep_t target = { .family = AF_INET6 }; + + if (in_msg_size < sizeof(dhcpv6_relay_msg_t)) { + DEBUG("DHCPv6 relay: incoming reply message too small\n"); + return; + } + if (_addr_unspec(in_reply->peer_address.u8, + sizeof(in_reply->peer_address.u8))) { + DEBUG("DHCPv6 relay: incoming reply message has unspecified peer " + "address\n"); + return; + } + in_msg_size -= sizeof(dhcpv6_relay_msg_t); + for (dhcpv6_opt_t *opt = (dhcpv6_opt_t *)(&in_msg[sizeof(dhcpv6_relay_msg_t)]); + in_msg_size > 0; in_msg_size -= _opt_len(opt), opt = _opt_next(opt)) { + + uint16_t opt_len = byteorder_ntohs(opt->len); + + if (opt_len > in_msg_size) { + DEBUG("DHCPv6 relay: invalid option size\n"); + return; + } + switch (byteorder_ntohs(opt->type)) { + case DHCPV6_OPT_IID: + if (opt_len != sizeof(uint16_t)) { + DEBUG("DHCPv6 relay: unexpected interface-ID length\n"); + return; + } + target.netif = _get_iid((dhcpv6_opt_iid_t *)opt); + break; + case DHCPV6_OPT_RELAY_MSG: { + out_msg = ((uint8_t *)opt) + sizeof(dhcpv6_opt_relay_msg_t); + out_msg_len = opt_len; + break; + } + default: + DEBUG("DHCPv6 relay: ignoring unknown option %u\n", + byteorder_ntohs(opt->type)); + break; + } + } + + if (target.netif == 0) { + DEBUG("DHCPv6 relay: no interface ID option found\n"); + return; + } + if ((out_msg == NULL) || (out_msg_len == 0)) { + DEBUG("DHCPv6 relay: no reply to forward found\n"); + return; + } + if (out_msg[0] == DHCPV6_RELAY_REPL) { + /* out message is heading for the next relay */ + target.port = DHCPV6_SERVER_PORT; + } + else { + /* out message is heading for the client it is destined to */ + target.port = DHCPV6_CLIENT_PORT; + } + assert(sizeof(in_reply->peer_address) == sizeof(target.addr.ipv6)); + + memcpy(&target.addr.ipv6, &in_reply->peer_address, sizeof(target.addr.ipv6)); + if (IS_USED(MODULE_SOCK_UTIL) && ENABLE_DEBUG) { + static char addr_str[IPV6_ADDR_MAX_STR_LEN]; + uint16_t port; + + if (sock_udp_ep_fmt(&target, addr_str, &port) > 0) { + DEBUG("DHCPv6 relay: forwarding reply towards target [%s]:%u\n", + addr_str, port); + } + } + res = sock_udp_send(NULL, out_msg, out_msg_len, &target); + if (res < 0) { + DEBUG("DHCPv6 relay: forwarding reply towards target failed: %d\n", + -res); + } +} + +/** @} */ diff --git a/tests/gnrc_dhcpv6_relay/Makefile b/tests/gnrc_dhcpv6_relay/Makefile new file mode 100644 index 0000000000..b9cd770f02 --- /dev/null +++ b/tests/gnrc_dhcpv6_relay/Makefile @@ -0,0 +1,49 @@ +include ../Makefile.tests_common + +RIOTBASE ?= $(CURDIR)/../.. + +export TAP ?= tap0 + +USEMODULE += auto_init_dhcpv6_relay +USEMODULE += event_thread +USEMODULE += gnrc_ipv6_default +USEMODULE += gnrc_netif_single # Only one interface used and it makes + # shell commands easier + +# use Ethernet as link-layer protocol +ifeq (native,$(BOARD)) + TERMFLAGS ?= $(TAP) +else + 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 + +USEMODULE += ps +USEMODULE += shell +USEMODULE += shell_commands + +# 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 + +ifndef CONFIG_DHCPV6_RELAY_HOP_LIMIT + CONFIG_DHCPV6_RELAY_HOP_LIMIT=8 + CFLAGS += -DCONFIG_DHCPV6_RELAY_HOP_LIMIT=$(CONFIG_DHCPV6_RELAY_HOP_LIMIT) +endif +export CONFIG_DHCPV6_RELAY_HOP_LIMIT +ifndef CONFIG_DHCPV6_RELAY_BUFLEN + CONFIG_DHCPV6_RELAY_BUFLEN=256 + CFLAGS += -DCONFIG_DHCPV6_RELAY_BUFLEN=$(CONFIG_DHCPV6_RELAY_BUFLEN) +endif +export CONFIG_DHCPV6_RELAY_BUFLEN diff --git a/tests/gnrc_dhcpv6_relay/Makefile.board.dep b/tests/gnrc_dhcpv6_relay/Makefile.board.dep new file mode 100644 index 0000000000..b595b8605c --- /dev/null +++ b/tests/gnrc_dhcpv6_relay/Makefile.board.dep @@ -0,0 +1,6 @@ +# Put board specific dependencies here +ifeq (native,$(BOARD)) + USEMODULE += netdev_tap +else + USEMODULE += stdio_ethos +endif diff --git a/tests/gnrc_dhcpv6_relay/Makefile.ci b/tests/gnrc_dhcpv6_relay/Makefile.ci new file mode 100644 index 0000000000..49a318d78c --- /dev/null +++ b/tests/gnrc_dhcpv6_relay/Makefile.ci @@ -0,0 +1,41 @@ +BOARD_INSUFFICIENT_MEMORY := \ + arduino-duemilanove \ + arduino-leonardo \ + arduino-mega2560 \ + arduino-nano \ + arduino-uno \ + atmega1284p \ + atmega328p-xplained-mini \ + atmega328p \ + atxmega-a1u-xpro \ + atxmega-a3bu-xplained \ + bluepill-stm32f030c8 \ + derfmega128 \ + i-nucleo-lrwan1 \ + mega-xplained \ + microduino-corerf \ + msb-430 \ + msb-430h \ + 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 \ + z1 \ + zigduino \ + # diff --git a/tests/gnrc_dhcpv6_relay/main.c b/tests/gnrc_dhcpv6_relay/main.c new file mode 100644 index 0000000000..6002f3a272 --- /dev/null +++ b/tests/gnrc_dhcpv6_relay/main.c @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2021 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 shell_buffer[SHELL_DEFAULT_BUFSIZE]; + + /* start shell */ + shell_run(NULL, shell_buffer, sizeof(shell_buffer)); + return 0; +} + +/** @} */ diff --git a/tests/gnrc_dhcpv6_relay/tests-as-root/01-run.py b/tests/gnrc_dhcpv6_relay/tests-as-root/01-run.py new file mode 100755 index 0000000000..000b301c00 --- /dev/null +++ b/tests/gnrc_dhcpv6_relay/tests-as-root/01-run.py @@ -0,0 +1,421 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2021 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 os +import re +import subprocess +import time +import unittest + +from scapy.all import Ether, IPv6, ICMPv6DestUnreach, UDP, DHCP6, \ + DHCP6_Solicit, DHCP6_Advertise, DHCP6_Request, DHCP6_Confirm, \ + DHCP6_Renew, DHCP6_Rebind, DHCP6_Reply, DHCP6_Release, DHCP6_Decline, \ + DHCP6_Reconf, DHCP6_InfoRequest, DHCP6_RelayForward, DHCP6_RelayReply, \ + DHCP6OptIfaceId, DHCP6OptRelayMsg, DHCP6OptClientId, DHCP6OptUnknown, \ + raw, sendp, AsyncSniffer +from testrunner.unittest import PexpectTestCase + + +class TestDHCPv6RelayAgent(PexpectTestCase): + CLIENT_PORT = 546 + SERVER_PORT = 547 + BOARD = os.environ["BOARD"] + CONFIG_DHCPV6_RELAY_BUFLEN = int(os.environ["CONFIG_DHCPV6_RELAY_BUFLEN"]) + CONFIG_DHCPV6_RELAY_HOP_LIMIT = \ + int(os.environ["CONFIG_DHCPV6_RELAY_HOP_LIMIT"]) + LOGFILE = None + tap = os.environ["TAP"] + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._get_bridge() + cls._get_tap_lladdr() + if cls.BOARD == "native": + cls.pre_sniffer_wait = cls.TIMEOUT / 20 + cls.post_sniffer_wait = cls.TIMEOUT / 20 + else: + cls.pre_sniffer_wait = cls.TIMEOUT / 20 + cls.post_sniffer_wait = cls.TIMEOUT / 20 + cls._get_node_ifaceid() + cls.spawn.sendline('ifconfig') + # check if node joined All_DHCP_Relay_Agents_and_Servers + cls.spawn.expect_exact('inet6 group: ff02::1:2') + + def setUp(self): + self.test_trid = 0xc0ffee + + @staticmethod + def _check_and_search_output(cmd, pattern, res_group, *args, **kwargs): + output = subprocess.check_output(cmd, *args, **kwargs).decode() + for line in output.splitlines(): + match = re.search(pattern, line) + if match is not None: + return match.group(res_group) + return None + + @classmethod + def _get_bridge(cls): + res = cls._check_and_search_output( + ["bridge", "link"], + r"{}.+master\s+(?P[^\s]+)".format(cls.tap), + "master" + ) + cls.tap = cls.tap if res is None else res + + @classmethod + def _get_tap_lladdr(cls): + res = cls._check_and_search_output( + ["ip", "addr", "show", "dev", cls.tap, "scope", "link"], + r"inet6\s+(?P[0-9A-Fa-f:]+)/\d+", + "lladdr" + ) + if res is None: + raise AssertionError( + "Can't find host link-local address on interface {}".format( + cls.tap + ) + ) + cls.tap_lladdr = res + + @classmethod + def _get_node_ifaceid(cls): + pkt = DHCP6_Solicit() + result = cls.send_and_exp_pkts(pkt, DHCP6_RelayForward) + assert len(result) > 0 + cls.node_ifaceid = result[0][DHCP6OptIfaceId].ifaceid + + @staticmethod + def _contains_dhcp(pkt): + # just using DHCP6 in pkt does not work + def expand(pkt): + yield pkt + while pkt.payload: + pkt = pkt.payload + yield pkt + return any(isinstance(layer, + (DHCP6, DHCP6_RelayForward, DHCP6_RelayReply)) + for layer in expand(pkt)) + + @classmethod + def _udp(cls, client=True): + if client: + return UDP(sport=cls.CLIENT_PORT, dport=cls.SERVER_PORT) + else: + return UDP(sport=cls.SERVER_PORT, dport=cls.SERVER_PORT) + + @staticmethod + def _lower_headers(ipv6_src=None): + return Ether(dst="33:33:00:01:00:02") / \ + IPv6(src=ipv6_src, dst="ff02::1:2") + + @classmethod + def _sendp(cls, pkt, client=True, ipv6_src=None): + sendp(cls._lower_headers(ipv6_src=ipv6_src) / cls._udp(client=client) / + pkt, iface=cls.tap, verbose=0 if cls.LOGFILE is None else 2) + + @classmethod + def send_and_exp_pkts(cls, send_pkt, exp_type, client=True): + sniffer = AsyncSniffer(iface=cls.tap) + sniffer.start() + time.sleep(cls.pre_sniffer_wait) + cls._sendp(send_pkt, client=client) + time.sleep(cls.post_sniffer_wait) + return [pkt for pkt in sniffer.stop() if exp_type in pkt and + # filter out sent packet + (UDP not in pkt or + raw(pkt[UDP].payload) != raw(send_pkt)) and + # filter out ICMPv6 since error notifications can also contain + # the sent packets and we are not interested in error + # notifications + ICMPv6DestUnreach not in pkt] + + @classmethod + def send_and_exp_any_dhcp6(cls, send_pkt, client=True, ipv6_src=None): + sniffer = AsyncSniffer(iface=cls.tap) + sniffer.start() + time.sleep(cls.pre_sniffer_wait) + cls._sendp(send_pkt, client=client, ipv6_src=ipv6_src) + time.sleep(cls.post_sniffer_wait) + return [pkt for pkt in sniffer.stop() if cls._contains_dhcp(pkt) and + # filter out sent packet + (UDP not in pkt or + raw(pkt[UDP].payload) != raw(send_pkt)) and + # filter out ICMPv6 since error notifications can also contain + # the sent packets and we are not interested in error + # notifications + ICMPv6DestUnreach not in pkt] + + def assert_node_responsive(self): + self.spawn.sendline("") + self.spawn.expect(r"> ") + + def assert_legal_relay_forward(self, pkt, exp_hopcount): + self.assertEqual(pkt[UDP].dport, self.SERVER_PORT) + self.assertEqual(pkt[DHCP6_RelayForward].hopcount, exp_hopcount) + # either linkaddr is set or there is an Interface-ID option in + # the message + if pkt[DHCP6_RelayForward].linkaddr == "::": + self.assertIn(DHCP6OptIfaceId, pkt) + # assure ifaceid for later tests + self.assertEqual(pkt[DHCP6OptIfaceId].ifaceid, + self.node_ifaceid) + self.assertIn(DHCP6OptRelayMsg, pkt) + + def assert_pkt_ignored(self, pkt, client_pkt=True, ipv6_src=None): + result = self.send_and_exp_any_dhcp6(pkt, client=client_pkt, + ipv6_src=ipv6_src) + self.assert_empty(result) + self.assert_node_responsive() + + def assert_len(self, collection, exp_len): + assert len(collection) == exp_len, \ + f'{collection} is not of length {exp_len}' + + def assert_empty(self, collection): + assert len(collection) == 0, f'{collection} is not empty' + + def assert_not_empty(self, collection): + assert len(collection) > 0, f'{collection} is empty' + + def assert_relayed_reply_pkt_w_trid(self, pkt, exp_rpkt): + self.assertNotIn(DHCP6_RelayReply, pkt) + self.assertNotIn(DHCP6OptIfaceId, pkt) + self.assertNotIn(DHCP6OptRelayMsg, pkt) + self.assertIn(exp_rpkt, pkt) + self.assertEqual(pkt[exp_rpkt].trid, self.test_trid) + + def test_dhcpv6_client_msgs(self): + for msg_type in [DHCP6_Solicit, DHCP6_Request, DHCP6_Confirm, + DHCP6_Renew, DHCP6_Rebind, DHCP6_Release, + DHCP6_Decline, DHCP6_InfoRequest]: + pkt = msg_type(trid=self.test_trid) + result = self.send_and_exp_pkts(pkt, DHCP6_RelayForward) + self.assert_len(result, 1) + pkt = result[0] + self.assert_legal_relay_forward(pkt, 0) + self.assertIn(msg_type, pkt[DHCP6OptRelayMsg].message) + self.assertEqual(pkt[DHCP6OptRelayMsg][msg_type].trid, + self.test_trid) + self.assert_node_responsive() + + def test_dhcpv6_server_msgs(self): + for msg_type in [DHCP6_Advertise, DHCP6_Reply, DHCP6_Reconf]: + pkt = msg_type(trid=self.test_trid) + self.assert_pkt_ignored(pkt, client_pkt=False) + + def test_dhcpv6_client_msg_too_long(self): + pkt = DHCP6_Solicit() / \ + DHCP6OptUnknown(optcode=1, + optlen=self.CONFIG_DHCPV6_RELAY_BUFLEN) / \ + (b"x" * self.CONFIG_DHCPV6_RELAY_BUFLEN) + self.assert_pkt_ignored(pkt) + + def test_dhcpv6_client_msg_too_small(self): + for dhcp6_len in range(len(DHCP6())): + with self.subTest(dhcp6_len=dhcp6_len): + pkt = (b"\x01" * dhcp6_len) + self.assert_pkt_ignored(pkt) + + def test_dhcpv6_client_msg_from_unspec(self): + self.assert_pkt_ignored(DHCP6_Confirm(), ipv6_src="::") + + def test_dhcpv6_client_msg_too_long_for_fwd(self): + buflen = self.CONFIG_DHCPV6_RELAY_BUFLEN + buflen -= len(DHCP6_Solicit()) # remove SOLICIT header + buflen -= len(DHCP6OptUnknown()) # remove option header of SOLICIT + buflen -= len(DHCP6_RelayForward()) # remove RELAY-FORWARD header + # remove Interface-ID option of RELAY-FORWARD + buflen -= len(DHCP6OptIfaceId(ifaceid=self.node_ifaceid)) + # remove Relay-Message option header of RELAY-FORWARD + buflen -= len(DHCP6OptUnknown(optlen=0)) + + pkt = DHCP6_Solicit() / \ + DHCP6OptUnknown(optcode=1, optlen=buflen) / (b"x" * buflen) + result = self.send_and_exp_pkts(pkt, DHCP6_RelayForward) + # should just fit + self.assert_len(result, 1) + self.assert_legal_relay_forward(result[0], 0) + self.assert_node_responsive() + buflen += 1 + pkt = DHCP6_Solicit() / \ + DHCP6OptUnknown(optcode=1, optlen=buflen) / (b"x" * buflen) + # SOLICIT should be too long + self.assert_pkt_ignored(pkt) + + def _test_dhcpv6_relay_forward(self, pkt, hopcount, exp_peeraddr, + exp_ifaceid): + result = self.send_and_exp_pkts(pkt, DHCP6_RelayForward, client=False) + self.assert_len(result, 1) + pkt = result[0] + self.assert_legal_relay_forward(pkt, hopcount + 1) + rpkt = pkt[DHCP6OptRelayMsg].message[DHCP6_RelayForward] + self.assertEqual(rpkt.peeraddr, exp_peeraddr) + self.assertEqual(rpkt[DHCP6OptIfaceId].ifaceid, exp_ifaceid) + self.assert_node_responsive() + return rpkt + + def test_dhcpv6_relay_forward(self): + hopcount = self.CONFIG_DHCPV6_RELAY_HOP_LIMIT - 1 + peeraddr = "fe80::f00:1337" + ifaceid = b"ab" + self.assertGreater(hopcount, 0) + pkt = DHCP6_RelayForward(peeraddr=peeraddr, hopcount=hopcount) / \ + DHCP6OptIfaceId(ifaceid=ifaceid) / \ + DHCP6OptRelayMsg(message=DHCP6_Rebind(trid=self.test_trid)) + rpkt = self._test_dhcpv6_relay_forward(pkt, hopcount, peeraddr, + ifaceid) + self.assertIn(DHCP6_Rebind, rpkt[DHCP6OptRelayMsg].message) + self.assertEqual(rpkt[DHCP6OptRelayMsg].message[DHCP6_Rebind].trid, + self.test_trid) + + def test_dhcpv6_relay_forward_options_reversed(self): + hopcount = self.CONFIG_DHCPV6_RELAY_HOP_LIMIT - 1 + peeraddr = "fe80::f00:1337" + ifaceid = b"ab" + self.assertGreater(hopcount, 0) + pkt = DHCP6_RelayForward(peeraddr=peeraddr, hopcount=hopcount) / \ + DHCP6OptRelayMsg(message=DHCP6_Rebind(trid=self.test_trid)) / \ + DHCP6OptIfaceId(ifaceid=ifaceid) + rpkt = self._test_dhcpv6_relay_forward(pkt, hopcount, peeraddr, + ifaceid) + self.assertIn(DHCP6_Rebind, rpkt[DHCP6OptRelayMsg].message) + self.assertEqual(rpkt[DHCP6OptRelayMsg].message[DHCP6_Rebind].trid, + self.test_trid) + + def test_dhcpv6_relay_forward_hop_limit_exceeded(self): + test_trid = 0xc0ffee + hoplimit = self.CONFIG_DHCPV6_RELAY_HOP_LIMIT + pkt = DHCP6_RelayForward(hopcount=hoplimit + 1, + peeraddr="fe80::f00:1337") / \ + DHCP6OptIfaceId(ifaceid=b"ab") / \ + DHCP6OptRelayMsg(message=DHCP6_Rebind(trid=test_trid)) + self.assert_pkt_ignored(pkt, client_pkt=False) + + def test_dhcpv6_relay_forward_too_small(self): + for i in range(len(DHCP6()), len(DHCP6_RelayForward())): + pkt = (chr(12) + ("\0" * (i - 1))).encode() + self.assert_pkt_ignored(pkt, client_pkt=False) + + def test_dhcpv6_relay_forward_from_unspec(self): + hopcount = self.CONFIG_DHCPV6_RELAY_HOP_LIMIT - 1 + peeraddr = "fe80::f00:1337" + ifaceid = b"ab" + self.assertGreater(hopcount, 0) + pkt = DHCP6_RelayForward(peeraddr=peeraddr, hopcount=hopcount) / \ + DHCP6OptIfaceId(ifaceid=ifaceid) / \ + DHCP6OptRelayMsg(message=DHCP6_Rebind(trid=self.test_trid)) + self.assert_pkt_ignored(pkt, ipv6_src="::") + + def _test_dhcpv6_simple_relay_reply(self, pkt, exp_rpkt): + result = self.send_and_exp_pkts(pkt, exp_rpkt, client=False) + self.assert_len(result, 1) + pkt = result[0] + self.assert_relayed_reply_pkt_w_trid(pkt, exp_rpkt) + self.assert_node_responsive() + + def test_dhcpv6_simple_relay_reply(self): + pkt = DHCP6_RelayReply(peeraddr=self.tap_lladdr) / \ + DHCP6OptIfaceId(ifaceid=self.node_ifaceid) / \ + DHCP6OptRelayMsg(message=DHCP6_Advertise(trid=self.test_trid)) + self._test_dhcpv6_simple_relay_reply(pkt, DHCP6_Advertise) + + def test_dhcpv6_simple_relay_reply_options_reversed(self): + pkt = DHCP6_RelayReply(peeraddr=self.tap_lladdr) / \ + DHCP6OptRelayMsg(message=DHCP6_Reply(trid=self.test_trid)) / \ + DHCP6OptIfaceId(ifaceid=self.node_ifaceid) + self._test_dhcpv6_simple_relay_reply(pkt, DHCP6_Reply) + + def test_dhcpv6_simple_relay_reply_foreign_option(self): + pkt = DHCP6_RelayReply(peeraddr=self.tap_lladdr) / \ + DHCP6OptIfaceId(ifaceid=self.node_ifaceid) / \ + DHCP6OptClientId() / \ + DHCP6OptRelayMsg(message=DHCP6_Reply(trid=self.test_trid)) + result = self.send_and_exp_pkts(pkt, DHCP6_Reply, client=False) + self.assert_len(result, 1) + pkt = result[0] + self.assert_relayed_reply_pkt_w_trid(pkt, DHCP6_Reply) + self.assert_node_responsive() + + def test_dhcpv6_simple_relay_reply_foreign_option_w_bogus_optlen(self): + pkt = DHCP6_RelayReply(peeraddr=self.tap_lladdr) / \ + DHCP6OptIfaceId(ifaceid=self.node_ifaceid) / \ + DHCP6OptRelayMsg(message=DHCP6_Reply(trid=self.test_trid)) / \ + DHCP6OptClientId(optlen=32) + self.assert_pkt_ignored(pkt, client_pkt=False) + + def test_dhcpv6_nested_relay_reply(self): + peeraddr = "fe80::f00:affe" + ifaceid = b"abcd" + pkt = DHCP6_RelayReply(peeraddr=self.tap_lladdr) / \ + DHCP6OptIfaceId(ifaceid=self.node_ifaceid) / \ + DHCP6OptRelayMsg( + message=DHCP6_RelayReply(peeraddr=peeraddr) / + # noqa: E131 (easier to read with this indentation) + DHCP6OptIfaceId(ifaceid=ifaceid) / + DHCP6OptRelayMsg( + message=DHCP6_Reconf(trid=self.test_trid) + ) + ) + result = self.send_and_exp_pkts(pkt, DHCP6_Reconf, client=False) + self.assert_len(result, 1) + pkt = result[0] + self.assertIn(DHCP6_RelayReply, pkt) + self.assertNotIn(DHCP6_RelayReply, pkt[DHCP6_RelayReply].message) + self.assertEqual(pkt[DHCP6_RelayReply].peeraddr, peeraddr) + self.assertIn(DHCP6OptIfaceId, pkt) + self.assertEqual(pkt[DHCP6OptIfaceId].ifaceid, ifaceid) + self.assertIn(DHCP6_Reconf, pkt[DHCP6_RelayReply].message) + self.assertEqual(pkt[DHCP6_Reconf].trid, self.test_trid) + self.assert_node_responsive() + + def test_dhcpv6_relay_reply_too_small(self): + for dhcp6_len in range(len(DHCP6()), len(DHCP6_RelayReply())): + with self.subTest(dhcp6_len=dhcp6_len): + pkt = (chr(13) + ("\0" * (dhcp6_len - 1))).encode() + self.assert_pkt_ignored(pkt, client_pkt=False) + + def test_dhcpv6_relay_reply_unexpeted_ifaceid_len(self): + pkt = DHCP6_RelayReply(peeraddr=self.tap_lladdr) / \ + DHCP6OptIfaceId(ifaceid="hello!") / \ + DHCP6OptRelayMsg(message=DHCP6_Reply(trid=self.test_trid)) + self.assert_pkt_ignored(pkt, client_pkt=False) + + def test_dhcpv6_relay_reply_unexpected_ifaceid(self): + pkt = DHCP6_RelayReply(peeraddr=self.tap_lladdr) / \ + DHCP6OptIfaceId(ifaceid=b"ab") / \ + DHCP6OptRelayMsg(message=DHCP6_Reply(trid=self.test_trid)) + self.assert_pkt_ignored(pkt, client_pkt=False) + + def test_dhcpv6_relay_reply_unexpected_peeraddr(self): + pkt = DHCP6_RelayReply(peeraddr="fe80::abcd:f00:1337") / \ + DHCP6OptIfaceId(ifaceid=self.node_ifaceid) / \ + DHCP6OptRelayMsg(message=DHCP6_Reply(trid=self.test_trid)) + self.assert_pkt_ignored(pkt, client_pkt=False) + + def test_dhcpv6_relay_reply_invalid_optlen(self): + pkt = DHCP6_RelayReply(peeraddr=self.tap_lladdr) / \ + DHCP6OptIfaceId(ifaceid=self.node_ifaceid) / \ + DHCP6OptRelayMsg(optlen=32, + message=DHCP6_Reply(trid=self.test_trid)) + self.assert_pkt_ignored(pkt, client_pkt=False) + + def test_dhcpv6_relay_reply_no_ifaceid_no_linkaddr(self): + pkt = DHCP6_RelayReply(peeraddr=self.tap_lladdr) / \ + DHCP6OptRelayMsg(message=DHCP6_Reply(trid=self.test_trid)) + self.assert_pkt_ignored(pkt, client_pkt=False) + + def test_dhcpv6_relay_reply_empty(self): + pkt = DHCP6_RelayReply(peeraddr=self.tap_lladdr) / \ + DHCP6OptIfaceId(ifaceid=self.node_ifaceid) / \ + DHCP6OptRelayMsg() + self.assert_pkt_ignored(pkt, client_pkt=False) + + +if __name__ == "__main__": + unittest.main()