/* * 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); } } /** @} */