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/relay.h b/sys/include/net/dhcpv6/relay.h new file mode 100644 index 0000000000..6ac683c891 --- /dev/null +++ b/sys/include/net/dhcpv6/relay.h @@ -0,0 +1,53 @@ +/* + * 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 + +/** + * @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 + +void dhcpv6_relay_auto_init(void); +void dhcpv6_relay_init(event_queue_t *eq, uint16_t listen_netif, + uint16_t fwd_netif); + +#ifdef __cplusplus +} +#endif + +#endif /* NET_DHCPV6_RELAY_H */ +/** @} */ 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); + } +} + +/** @} */