--- /dev/null
+/*
+ * dhcpv6relay.c - DHCPv6 relay plugin.
+ *
+ * Copyright (c) 2025 Ultimate Linux Solutions (Pty) Ltd represented by
+ * Jaco Kroon <jaco@uls.co.za>. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THE AUTHORS OF THIS SOFTWARE DISCLAIM ALL WARRANTIES WITH REGARD TO
+ * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+ * AND FITNESS, IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
+ * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+ * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
+ * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+#include "dhcpv6relay.h"
+
+#include <pppd/pppd.h>
+#include <pppd/options.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <ifaddrs.h>
+#include <netdb.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <arpa/inet.h>
+#include <net/if.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+
+char pppd_version[] = PPPD_VERSION;
+
+static int dhcpv6relay_setserver(const char** argv);
+
+static bool dhcpv6relay_trusted = false;
+static unsigned dhcpv6relay_metric = 0;
+
+static struct option options[] = {
+ { "dhcpv6-server", o_special, (void*) &dhcpv6relay_setserver,
+ "DHCPv6 server to proxy DHCPv6 requests to",
+ OPT_PRIV },
+ { "dhcpv6-trusted", o_bool, &dhcpv6relay_trusted,
+ "DHCPv6 trusted interface (allow incoming relay messages)",
+ OPT_PRIO | OPT_PRIV | 1 },
+ { "dhcpv6-untrusted", o_bool, &dhcpv6relay_trusted,
+ "DHCPv6 untrusted interface (discard incoming relay messages)",
+ OPT_PRIOSUB },
+ { "dhcpv6-metric", o_int, &dhcpv6relay_metric,
+ "Metric to use DHCPv6 supplied routes",
+ OPT_PRIV|OPT_LLIMIT, NULL, 0, 0 },
+ { NULL }
+};
+
+static char* dhcpv6relay_server = NULL;
+static int dhcpv6relay_sock_ll = -1;
+static int dhcpv6relay_sock_mc = -1;
+static int dhcpv6relay_upstream = -1;
+static struct sockaddr_storage dhcpv6relay_sa;
+static struct dhcpv6relay_route_entry *dhcpv6relay_delegations = NULL;
+
+static
+const char* dhcpv6_type2string(int msg_type)
+{
+ switch (msg_type) {
+ case DHCPv6_MSGTYPE_SOLICIT:
+ return "solicit";
+ case DHCPv6_MSGTYPE_ADVERTISE:
+ return "advertise";
+ case DHCPv6_MSGTYPE_REQUEST:
+ return "request";
+ case DHCPv6_MSGTYPE_CONFIRM:
+ return "confirm";
+ case DHCPv6_MSGTYPE_RENEW:
+ return "renew";
+ case DHCPv6_MSGTYPE_REBIND:
+ return "rebind";
+ case DHCPv6_MSGTYPE_REPLY:
+ return "reply";
+ case DHCPv6_MSGTYPE_RELEASE:
+ return "release";
+ case DHCPv6_MSGTYPE_DECLINE:
+ return "decline";
+ case DHCPv6_MSGTYPE_RECONFIGURE:
+ return "reconfigure";
+ case DHCPv6_MSGTYPE_INFORMATION_REQUEST:
+ return "information_request";
+ case DHCPv6_MSGTYPE_RELAY_FORW:
+ return "relay-forw";
+ case DHCPv6_MSGTYPE_RELAY_REPL:
+ return "relay-repl";
+ default:
+ return NULL;
+ }
+}
+
+static
+int dhcpv6relay_setserver(const char** argv)
+{
+ int r;
+ struct addrinfo *ai = NULL, *i, hint = {
+ .ai_flags = 0,
+ .ai_family = 0, /* we *prefer* IPv6, but will accept IPv4 */
+ .ai_socktype = SOCK_DGRAM, /* UDP */
+ .ai_protocol = 0,
+ .ai_addrlen = 0,
+ .ai_addr = NULL,
+ .ai_canonname = NULL,
+ .ai_next = NULL,
+ };
+ char bfr_ip[INET6_ADDRSTRLEN];
+ char bfr_port[6];
+
+ free(dhcpv6relay_server);
+ dhcpv6relay_server = NULL;
+
+ if (!*argv || !**argv)
+ return 1;
+
+ r = getaddrinfo(*argv, "dhcpv6-server", &hint, &ai);
+ if (r != 0) {
+ error("DHCPv6 relay: Unable to set server address to %s: %s",
+ *argv, gai_strerror(r));
+ return 0;
+ }
+
+ dhcpv6relay_sa.ss_family = 0;
+ for (i = ai; i && dhcpv6relay_sa.ss_family != AF_INET6; i = i->ai_next) {
+ if (!dhcpv6relay_sa.ss_family || i->ai_family == AF_INET6) {
+ memcpy(&dhcpv6relay_sa, i->ai_addr, i->ai_addrlen);
+ if (dhcpv6relay_sa.ss_family == AF_INET6)
+ break;
+ }
+ }
+
+ freeaddrinfo(ai);
+ if (dhcpv6relay_sa.ss_family) {
+ getnameinfo((struct sockaddr*)&dhcpv6relay_sa, sizeof(dhcpv6relay_sa),
+ bfr_ip, sizeof(bfr_ip),
+ bfr_port, sizeof(bfr_port),
+ NI_NUMERICHOST | NI_NUMERICSERV | NI_DGRAM);
+ notice("DHCPv6 relay: Using server [%s]:%s", bfr_ip, bfr_port);
+
+ dhcpv6relay_server = strdup(*argv);
+ } else {
+ error("DHCPv6 relay: Failed to resolve %s to an IP address.",
+ *argv);
+ }
+
+ return 1;
+}
+
+static
+void routes_remove_all()
+{
+ char in6addr[INET6_ADDRSTRLEN];
+ struct dhcpv6relay_route_entry* c = dhcpv6relay_delegations;
+
+ while (c) {
+ struct dhcpv6relay_route_entry* n = c->next;
+
+ if (!sifdelroute(AF_INET6, &c->prefix, c->len, dhcpv6relay_metric))
+ error("DHCPv6 relay: failed to remove route for %s/%d",
+ inet_ntop(AF_INET6, &c->prefix, in6addr, sizeof(in6addr)), c->len);
+
+ free(c);
+ c = n;
+ }
+
+ dhcpv6relay_delegations = NULL;
+}
+
+static
+void dhcpv6relay_down(void*, int)
+{
+ routes_remove_all();
+ if (dhcpv6relay_sock_ll >= 0) {
+ remove_fd(dhcpv6relay_sock_ll);
+ close(dhcpv6relay_sock_ll);
+ dhcpv6relay_sock_ll = -1;
+ }
+ if (dhcpv6relay_sock_mc >= 0) {
+ remove_fd(dhcpv6relay_sock_mc);
+ close(dhcpv6relay_sock_mc);
+ dhcpv6relay_sock_mc = -1;
+ }
+ if (dhcpv6relay_upstream >= 0) {
+ remove_fd(dhcpv6relay_upstream);
+ close(dhcpv6relay_upstream);
+ dhcpv6relay_upstream = -1;
+ }
+}
+
+static
+struct dhcpv6relay_route_entry** dhcpv6relay_find_route_entry(const struct in6_addr* addr, uint8_t prefixlen)
+{
+ struct dhcpv6relay_route_entry** s = &dhcpv6relay_delegations;
+ while (*s) {
+ if (memcmp(&(*s)->prefix, addr, sizeof(*addr)) == 0 && (*s)->len == prefixlen)
+ return s;
+ s = &(*s)->next;
+ }
+ return NULL;
+}
+
+static void dhcpv6relay_route_timeout(void* _r);
+
+static
+void dhcpv6relay_real_release_route(struct dhcpv6relay_route_entry** _r)
+{
+ char in6addr[INET6_ADDRSTRLEN];
+ struct dhcpv6relay_route_entry* r = *_r;
+
+ if (!sifdelroute(AF_INET6, &r->prefix, r->len, dhcpv6relay_metric))
+ error("DHCPv6 relay: failed to remove route for %s/%d",
+ inet_ntop(AF_INET6, &r->prefix, in6addr, sizeof(in6addr)), r->len);
+ else
+ notice("DHCPv6 relay: removed route %s/%d",
+ inet_ntop(AF_INET6, &r->prefix, in6addr, sizeof(in6addr)), r->len);
+
+ ppp_untimeout(dhcpv6relay_route_timeout, r);
+ *_r = r->next;
+ free(r);
+}
+
+static
+void dhcpv6relay_route_timeout(void* _r)
+{
+ struct dhcpv6relay_route_entry** s = &dhcpv6relay_delegations;
+ while (*s && *s != _r)
+ s = &(*s)->next;
+
+ if (!*s) {
+ error("DHCPv6 relay: timeout on already released route delegation.");
+ } else {
+ dhcpv6relay_real_release_route(s);
+ }
+}
+
+static
+void dhcpv6relay_release_route(const struct in6_addr* addr, uint8_t prefixlen, uint32_t /* lifetime */)
+{
+ char in6addr[INET6_ADDRSTRLEN];
+
+ struct dhcpv6relay_route_entry** r = dhcpv6relay_find_route_entry(addr, prefixlen);
+
+ if (!r) {
+ error("DHCPv6 relay: Release of route %s/%d that was never delegated.",
+ inet_ntop(AF_INET6, addr, in6addr, sizeof(in6addr)), prefixlen);
+ } else {
+ dhcpv6relay_real_release_route(r);
+ }
+}
+
+static
+void dhcpv6relay_add_route(const struct in6_addr* addr, uint8_t prefixlen, uint32_t lifetime)
+{
+ char in6addr[INET6_ADDRSTRLEN];
+ struct dhcpv6relay_route_entry** _r = dhcpv6relay_find_route_entry(addr, prefixlen);
+ struct dhcpv6relay_route_entry* r = _r ? *_r : NULL;
+ if (r) {
+ /* route is already installed, just update preferred lifetime. */
+ r->valid_until = time(NULL) + lifetime;
+ ppp_untimeout(dhcpv6relay_route_timeout, r);
+ ppp_timeout(dhcpv6relay_route_timeout, r, lifetime, 0);
+ return;
+ }
+
+ if (!sifaddroute(AF_INET6, addr, prefixlen, dhcpv6relay_metric))
+ return error("DHCPv6 relay: failed to install route for %s/%d",
+ inet_ntop(AF_INET6, addr, in6addr, sizeof(in6addr)), prefixlen);
+
+ notice("DHCPv6 relay: installed route %s/%d",
+ inet_ntop(AF_INET6, addr, in6addr, sizeof(in6addr)), prefixlen);
+
+ r = malloc(sizeof(*r));
+ r->next = dhcpv6relay_delegations;
+ r->prefix = *addr;
+ r->len = prefixlen;
+ r->valid_until = time(NULL) + lifetime;
+ ppp_timeout(dhcpv6relay_route_timeout, r, lifetime, 0);
+
+ dhcpv6relay_delegations = r;
+}
+
+
+void dhcpv6relay_process_ia_pd(const unsigned char *bfr, uint16_t len, dhcpv6relay_route_func routefunc)
+{
+ if (len < 12)
+ return; /* IAID, T1, T2, 4 octets each, we don't care */
+ bfr += 12;
+ len -= 12;
+ while (len > 4) {
+ uint16_t opttype = ntohs(*(uint16_t*)bfr);
+ uint16_t optlen = ntohs(*(uint16_t*)(bfr+2));
+ bfr += 4;
+ len -= 4;
+
+ switch (opttype) {
+ case DHCPv6_OPTION_IAPREFIX:
+ /* 4 octets preferred, 4 octets valid lifetime, 1 octet length, 16 octets prefix */
+ routefunc((const struct in6_addr*)(bfr + 9), bfr[8], ntohl(*(const uint32_t*)(bfr+4)));
+ break;
+ default:
+ /* nothing */
+ }
+
+ bfr += optlen;
+ len -= optlen;
+ }
+}
+
+static
+void dhcpv6relay_process_packet_for_routes(const unsigned char *bfr, uint16_t len)
+{
+ if (len < 1)
+ return;
+ uint8_t pkttype = *bfr;
+
+ switch (pkttype) {
+ case DHCPv6_MSGTYPE_RELAY_FORW:
+ case DHCPv6_MSGTYPE_RELAY_REPL:
+ /* these have 34 byte headers, so we can jump over that, then look for the relay message option
+ * and recurse on that as we really don't care about anything else. */
+ if (len < 34)
+ return;
+ bfr += 34;
+ len -= 34;
+ while (len > 4) {
+ uint16_t opttype = ntohs(*(uint16_t*)bfr);
+ uint16_t optlen = ntohs(*(uint16_t*)(bfr+2));
+ bfr += 4;
+ len -= 4;
+ if (optlen > len)
+ return;
+ if (opttype == DHCPv6_OPTION_RELAY_MSG) {
+ dhcpv6relay_process_packet_for_routes(bfr, optlen);
+ return; /* there may be only one */
+ }
+ bfr += optlen;
+ len -= optlen;
+ }
+ break;
+ case DHCPv6_MSGTYPE_REPLY:
+ case DHCPv6_MSGTYPE_RELEASE:
+ dhcpv6relay_route_func func;
+ if (pkttype == DHCPv6_MSGTYPE_RELEASE)
+ func = dhcpv6relay_release_route;
+ else
+ func = dhcpv6relay_add_route;
+
+ /* everything else has a 4 byte header, the packet type (1 octet) and a transaction id (3 octets)
+ * which we don't care about, so just skip ahead to the options that we do care about */
+ if (len < 4)
+ return;
+ bfr += 4;
+ len -= 4;
+ while (len > 4) {
+ uint16_t opttype = ntohs(*(uint16_t*)bfr);
+ uint16_t optlen = ntohs(*(uint16_t*)(bfr+2));
+ bfr += 4;
+ len -= 4;
+ if (optlen > len) /* packet overrun */
+ return;
+
+ switch (opttype) {
+ case DHCPv6_OPTION_IA_PD:
+ dhcpv6relay_process_ia_pd(bfr, optlen, func);
+ break;
+ default:
+ }
+ bfr += optlen;
+
+ len -= optlen;
+ }
+ break;
+ default:
+ /* nothing to do, we don't care about these. */
+ break;
+ }
+}
+
+static
+void dhcpv6relay_server_event(int fd, void*)
+{
+ unsigned char buffer[1024];
+ unsigned char *options = buffer + 34; /* skip fixed header */
+ unsigned char *fwd_packet = NULL;
+ uint16_t fwd_len = 0;
+ char in6addr[INET6_ADDRSTRLEN];
+ struct sockaddr_in6 sa;
+ socklen_t slen = sizeof(sa);
+ bool valid_source = true;
+ int hlim = 0;
+ ssize_t r = recvfrom(fd, buffer, sizeof(buffer), MSG_DONTWAIT,
+ (struct sockaddr*)&sa, &slen);
+ if (r < 0) {
+ error("DHCPv6 relay: Failed to read from %s socket: %s",
+ fd == dhcpv6relay_sock_ll ? "LL" : "MC",
+ strerror(errno));
+ return;
+ }
+ if (r >= sizeof(buffer)) {
+ error("DHCPv6 buffer overrun, recvfrom returned %d, max %u",
+ r, sizeof(buffer));
+ return;
+ }
+
+ /* notice("Received %d bytes from fd=%d (%s), with source [%s]:%d, packet type: %s", r, fd,
+ "UPSTREAM",
+ inet_ntop(sa.sin6_family, sa.sin6_family == AF_INET ?
+ (void*)&((struct sockaddr_in*)&sa)->sin_addr : (void*)&sa.sin6_addr,
+ in6addr, sizeof(in6addr)),
+ ntohs(sa.sin6_port), r ? dhcpv6_type2string(buffer[0]) : "empty"); */
+
+ if (sa.sin6_family != dhcpv6relay_sa.ss_family) {
+ valid_source = false;
+ } else if (sa.sin6_family == AF_INET6) {
+ valid_source = sa.sin6_port == ((struct sockaddr_in6*)&dhcpv6relay_sa)->sin6_port
+
+ && memcmp(&sa.sin6_addr, &((struct sockaddr_in6*)&dhcpv6relay_sa)->sin6_addr,
+ sizeof(sa.sin6_addr)) == 0;
+ } else if (sa.sin6_family == AF_INET) {
+ valid_source = ((struct sockaddr_in*)&sa)->sin_port ==
+ ((struct sockaddr_in*)&dhcpv6relay_sa)->sin_port
+
+ && ((struct sockaddr_in*)&sa)->sin_addr.s_addr ==
+ ((struct sockaddr_in*)&dhcpv6relay_sa)->sin_addr.s_addr;
+ } else {
+ error("DHCv6 relay: Received non-IP packet on upstream socket.");
+ return;
+ }
+
+ if (!valid_source) {
+ error("DHCPv6 relay: Received packet from unexpected source [%s]:%d on upstream socket.",
+ inet_ntop(sa.sin6_family, sa.sin6_family == AF_INET ?
+ (void*)&((struct sockaddr_in*)&sa)->sin_addr : (void*)&sa.sin6_addr,
+ in6addr, sizeof(in6addr)),
+ sa.sin6_family == AF_INET ? ((struct sockaddr_in*)&sa)->sin_port : sa.sin6_port);
+ return;
+ }
+
+ /* relay-repl is at least 34 bytes, without required options,
+ * so blindly discard anything smaller */
+ if (r < 34) {
+ error("DHCPv6 relay: Received packet on upstream socket is too small to be a valid relay-repl.");
+ return;
+ }
+
+ if (buffer[0] != DHCPv6_MSGTYPE_RELAY_REPL) {
+ error("DHCPv6 relay: packet received from upstream server is a %s, expected relay-repl.",
+ dhcpv6_type2string(buffer[0]));
+ return;
+ }
+
+ /* don't partircularly care about the hop-count or linkaddr, we do need
+ * peeraddr, but can recover that later */
+
+ r -= 34;
+ while (r > 4) {
+ /* each option header is a 32-bits, 16-bits type and 16-bit length */
+ uint16_t type = ntohs(*(uint16_t*)options);
+ uint16_t len = ntohs(*(uint16_t*)(options + 2));
+ options += 4;
+ r -= 4;
+ if (len > r) {
+ error("DHCPv6 relay: Error parsing packet from server, option %u specified "
+ "len=%u but only %d bytes available.", type, len, r);
+ return;
+ }
+ switch (type) {
+ case DHCPv6_OPTION_RELAY_MSG:
+ fwd_packet = options;
+ fwd_len = len;
+ break;
+ /*case DHCPv6_OPTION_RELAY_PORT:
+ notice("Got relay-port: %u", ntohs(*(uint16_t*)options));
+ break; */
+ default:
+ /* notice("DHCPv6 relay: Skipping processing of option %u of length %u.",
+ type, len); */
+ }
+ options += len;
+ r -= len;
+ }
+
+ if (!fwd_packet) {
+ error("DHCPv6 relay: relay-repl message from server did not contain a relay-msg option.");
+ return;
+ }
+
+ memset(&sa, 0, sizeof(sa));
+ sa.sin6_family = AF_INET6;
+ if (fwd_packet[0] == DHCPv6_MSGTYPE_RELAY_REPL) {
+ /* this should only ever happen towards "trusted" ports, wich is not the default. */
+ /* TODO: Honour option 135 towards downstream, would need to see an example, spec
+ * is unclear and observed behaviour from KEA doesn't make sense. */
+ sa.sin6_port = getservbyname("dhcpv6-server", "udp")->s_port;
+ hlim = 64;
+ } else {
+ sa.sin6_port = getservbyname("dhcpv6-client", "udp")->s_port;
+ }
+ memcpy(&sa.sin6_addr, buffer + 18 /* peer-link address */, sizeof(sa.sin6_addr));
+ sa.sin6_scope_id = if_nametoindex(ppp_ifname());
+
+ setsockopt(dhcpv6relay_sock_ll, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &hlim, sizeof(hlim));
+ r = sendto(dhcpv6relay_sock_ll, fwd_packet, fwd_len, 0,
+ (struct sockaddr*)&sa, sizeof(sa));
+ if (r < 0) {
+ error("DHCPv6 relay: Error transmitting server response to client: %s",
+ strerror(errno));
+ }
+
+ dhcpv6relay_process_packet_for_routes(fwd_packet, fwd_len);
+}
+
+static
+int dhcpv6relay_init_upstream()
+{
+ /* use family from sa so that we can do DHCPv6 / IPv4. */
+ dhcpv6relay_upstream = socket(dhcpv6relay_sa.ss_family, SOCK_DGRAM, 0);
+ if (dhcpv6relay_upstream < 0) {
+ error("DHCPv6 relay: Failed to bind upstream socket: %s",
+ strerror(errno));
+ return 0;
+ }
+ if (connect(dhcpv6relay_upstream, (struct sockaddr*)&dhcpv6relay_sa, sizeof(dhcpv6relay_sa)) < 0) {
+ error("DHCPv6 relay: Failed to connect upstream socket: %s",
+ strerror(errno));
+ close(dhcpv6relay_upstream);
+ dhcpv6relay_upstream = -1;
+ return 0;
+ }
+ add_fd_callback(dhcpv6relay_upstream, dhcpv6relay_server_event, NULL);
+
+ return 1;
+}
+
+static
+void dhcpv6relay_client_event(int fd, void*)
+{
+ unsigned char buffer[1024];
+ unsigned char fwd_head[256];
+ const char* remote_id;
+ const char* subscriber_id;
+ struct iovec v[] = {
+ {
+ .iov_base = fwd_head,
+ .iov_len = 0,
+ },
+ {
+ .iov_base = buffer,
+ .iov_len = 0,
+ },
+ };
+ struct msghdr wv = {
+ .msg_name = &dhcpv6relay_sa,
+ .msg_namelen = sizeof(dhcpv6relay_sa),
+ .msg_iov = v,
+ .msg_iovlen = sizeof(v) / sizeof(*v),
+ .msg_control = NULL,
+ .msg_controllen = 0,
+ .msg_flags = 0,
+ };
+ /* char in6addr[INET6_ADDRSTRLEN] */;
+ struct sockaddr_in6 sa;
+ uint16_t sport;
+ socklen_t slen = sizeof(sa);
+ ssize_t r = recvfrom(fd, buffer, sizeof(buffer), MSG_DONTWAIT,
+ (struct sockaddr*)&sa, &slen);
+ if (r < 0) {
+ if (errno != EAGAIN)
+ error("DHCPv6 relay: Failed to read from %s socket: %s",
+ fd == dhcpv6relay_sock_ll ? "LL" : "MC",
+ strerror(errno));
+ return;
+ }
+ if (r >= sizeof(buffer)) {
+ error("DHCPv6 relay: buffer overrun, recvfrom returned %d, max %u (%s socket)",
+ r, sizeof(buffer), fd == dhcpv6relay_sock_ll ? "LL" : "MC");
+ return;
+ }
+ if (r < 4) {
+ error("DHCPv6 relay: buffer underrun, we only got %d bytes from client, need at least 4 to be valid.", r);
+ return;
+ }
+ v[1].iov_len = r;
+
+ /* notice("Received %d bytes from fd=%d (%s), with source [%s]:%d, packet type: %s", r, fd,
+ fd == dhcpv6relay_sock_ll ? "LL" : "MC",
+ inet_ntop(sa.sin6_family, &sa.sin6_addr, in6addr, sizeof(in6addr)),
+ ntohs(sa.sin6_port), r ? dhcpv6_type2string(buffer[0]) : "empty"); */
+
+ /* disallow Reply and Relay-Reply messages */
+ if (buffer[0] == DHCPv6_MSGTYPE_REPLY || buffer[0] == DHCPv6_MSGTYPE_RELAY_REPL) {
+ warn("Discarding DHCPv6 %s message received on PPP interface.",
+ dhcpv6_type2string(buffer[0]));
+ return;
+ }
+
+ /* if the interface is not trusted, also discard Relay-Fwd messages */
+ if (!dhcpv6relay_trusted && buffer[0] == DHCPv6_MSGTYPE_RELAY_FORW) {
+ warn("Discarding DHCPv6 %s message received on untrusted PPP interface.",
+ dhcpv6_type2string(buffer[0]));
+ return;
+ }
+
+ if (dhcpv6relay_upstream < 0 && !dhcpv6relay_init_upstream())
+ return;
+
+ dhcpv6relay_process_packet_for_routes(buffer, r);
+
+ /* populate the forward header */
+ fwd_head[0] = DHCPv6_MSGTYPE_RELAY_FORW; /* msg-type */
+ fwd_head[1] = buffer[0] == DHCPv6_MSGTYPE_RELAY_FORW ? buffer[1] + 1 : 0; /* hop count */
+ memset(&fwd_head[2], 0, 16); /* link-address, unspecified */
+ memcpy(&fwd_head[18], &sa.sin6_addr, 16); /* peer-address */
+ v[0].iov_len = 34;
+
+ slen = sizeof(sa);
+ if (getsockname(dhcpv6relay_upstream, (struct sockaddr*)&sa, &slen) < 0) {
+ error("DHCPv6 relay: Unable to determine local sending port: %s",
+ strerror(errno));
+ return;
+ }
+
+#define push_checkbytes(x) do { if ((x) + v[0].iov_len > sizeof(fwd_head)) { error("DHCPv6 relay: Buffer overlow avoidance pushing %d bytes, need %d.", (x), (x) + v[0].iov_len - sizeof(fwd_head)); return; }} while(0)
+#define push_uint16(val) do { push_checkbytes(2); uint16_t t = (val); fwd_head[v[0].iov_len++] = t >> 8; fwd_head[v[0].iov_len++] = t & 0xFF; } while(0);
+#define push_bytes(ptr, bytes) do { push_checkbytes(bytes); memcpy(&fwd_head[v[0].iov_len], (ptr), (bytes)); v[0].iov_len += (bytes); } while(0)
+
+ /* On Linux at least sin6_port and sin_port would refer the same
+ * data but I can't guarantee that for solaris (and others) */
+ switch (sa.sin6_family) {
+ case AF_INET:
+ sport = ((struct sockaddr_in*)&sa)->sin_port;
+ break;
+ case AF_INET6:
+ sport = sa.sin6_port;
+ break;
+ default:
+ error("DHCPv6 relay: Upstream socket is bound to something other than IP ... can't relay.");
+ return;
+ }
+
+ push_uint16(DHCPv6_OPTION_RELAY_PORT);
+ push_uint16(2);
+ push_uint16(ntohs(sport));
+
+ remote_id = ppp_get_remote_number();
+ if (remote_id) {
+ r = strlen(remote_id);
+ push_uint16(DHCPv6_OPTION_REMOTE_ID);
+ push_uint16(r);
+ push_bytes(remote_id, r);
+ }
+
+ subscriber_id = ppp_peer_authname(NULL, 0);
+ if (subscriber_id) {
+ r = strlen(subscriber_id);
+ push_uint16(DHCPv6_OPTION_SUBSCRIBER_ID);
+ push_uint16(r);
+ push_bytes(subscriber_id, r);
+ }
+
+ /* This *must* be the last option since it refers the the content from v[1] */
+ push_uint16(DHCPv6_OPTION_RELAY_MSG);
+ push_uint16(v[1].iov_len);
+
+#undef push_checkbytes
+#undef push_uint16
+#undef push_bytes
+
+ if (sendmsg(dhcpv6relay_upstream, &wv, 0) < 0) {
+ error("DHCPv6 relay: Failed to transmit proxies request: %s",
+ strerror(errno));
+ }
+}
+
+static
+int dhcpv6relay_populate_ll(struct sockaddr_in6* res)
+{
+ /* can we rather shortcut to get the address directly from ipv6cp? */
+ struct ifaddrs *ifap, *ifa;
+ int r = getifaddrs(&ifap);
+ const struct sockaddr_in6* sa6;
+
+ if (r < 0) {
+ error("DHCPv6 relay: Unable to determine LL address");
+ return 0;
+ }
+
+ for (ifa = ifap; ifa; ifa = ifa->ifa_next) {
+ if (!ifa->ifa_addr || ifa->ifa_addr->sa_family != AF_INET6)
+ continue;
+
+ sa6 = (struct sockaddr_in6*)ifa->ifa_addr;
+ if (!sa6->sin6_scope_id)
+ continue; /* LL has sin6_scope_id set to interface id, != 0 */
+
+ if (strcmp(ifa->ifa_name, ppp_ifname()) != 0)
+ continue; /* wrong interface */
+
+ /* use it */
+ *res = *sa6;
+ freeifaddrs(ifap);
+ return 1;
+ }
+
+ error("DHCPv6 relay: No matching LL addresses available for use.");
+ freeifaddrs(ifap);
+ return 0;
+}
+
+static
+void dhcpv6relay_up(void*, int)
+{
+ struct sockaddr_in6 sa;
+ struct ipv6_mreq mreq;
+ struct servent *se;
+
+ /* no relay configured, so we can't work, simply don't listen
+ * for DHCP solicitations */
+ if (!dhcpv6relay_server)
+ return;
+
+ if (!dhcpv6relay_populate_ll(&sa))
+ return;
+
+ se = getservbyname("dhcpv6-server", "udp");
+ if (!se) {
+ error("DHCPv6 relay: Unable to determine UDP port number for dhcpv6-server: %s",
+ strerror(errno));
+ return;
+ }
+
+ sa.sin6_port = se->s_port;
+
+ dhcpv6relay_sock_ll = socket(AF_INET6, SOCK_DGRAM, 0);
+ if (dhcpv6relay_sock_ll < 0) {
+ error("DHCPv6 relay: Unable to create LL socket: %s", strerror(errno));
+ return dhcpv6relay_down(NULL, 0);
+ }
+ fcntl(dhcpv6relay_sock_ll, F_SETFD, FD_CLOEXEC);
+
+ if (bind(dhcpv6relay_sock_ll, (const struct sockaddr*)&sa, sizeof(sa)) < 0) {
+ error("DHCPv6 relay: Unable to bind LL socket: %s", strerror(errno));
+ return dhcpv6relay_down(NULL, 0);
+ }
+
+ memset(&mreq, 0, sizeof(mreq));
+ if (inet_pton(AF_INET6, "ff02::1:2", &mreq.ipv6mr_multiaddr) < 0) {
+ error("DHCPv6 relay: Error preparing multicast binding: %s", strerror(errno));
+ return dhcpv6relay_down(NULL, 0);
+ }
+
+ mreq.ipv6mr_interface = sa.sin6_scope_id;
+ if (setsockopt(dhcpv6relay_sock_ll, IPPROTO_IPV6, IPV6_JOIN_GROUP, &mreq, sizeof(mreq)) < 0) {
+ error("DHCPv6 relay: Error joining multicast group: %s", strerror(errno));
+ return dhcpv6relay_down(NULL, 0);
+ }
+
+ dhcpv6relay_sock_mc = socket(AF_INET6, SOCK_DGRAM, 0);
+ if (dhcpv6relay_sock_mc < 0) {
+ error("DHCPv6 relay: Unable to create MC socket: %s", strerror(errno));
+ return dhcpv6relay_down(NULL, 0);
+ }
+ fcntl(dhcpv6relay_sock_mc, F_SETFD, FD_CLOEXEC);
+
+ sa.sin6_addr = mreq.ipv6mr_multiaddr;
+ if (bind(dhcpv6relay_sock_mc, (const struct sockaddr*)&sa, sizeof(sa)) < 0) {
+ error("DHCPv6 relay: Unable to bind MC socket: %s", strerror(errno));
+ return dhcpv6relay_down(NULL, 0);
+ }
+
+ add_fd_callback(dhcpv6relay_sock_ll, dhcpv6relay_client_event, NULL);
+ add_fd_callback(dhcpv6relay_sock_mc, dhcpv6relay_client_event, NULL);
+
+ notice("DHCPv6 relay: ready.");
+}
+
+void
+plugin_init(void)
+{
+ ppp_add_options(options);
+ ppp_add_notify(NF_IPV6_UP, dhcpv6relay_up, NULL);
+ ppp_add_notify(NF_IPV6_DOWN, dhcpv6relay_down, NULL);
+}
--- /dev/null
+/*
+ * dhcpv6relay.h - DHCPv6 relay plugin.
+ *
+ * Copyright (c) 2025 Ultimate Linux Solutions (Pty) Ltd represented by
+ * Jaco Kroon <jaco@uls.co.za>. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THE AUTHORS OF THIS SOFTWARE DISCLAIM ALL WARRANTIES WITH REGARD TO
+ * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+ * AND FITNESS, IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
+ * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+ * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
+ * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+#ifndef DHCPV6RELAY_H
+#define DHCPV6RELAY_H
+
+#include <time.h>
+#include <stdint.h>
+#include <netinet/in.h>
+
+struct dhcpv6relay_route_entry {
+ struct dhcpv6relay_route_entry *next;
+ struct in6_addr prefix;
+ uint8_t len;
+ time_t valid_until;
+};
+
+typedef void (*dhcpv6relay_route_func)(const struct in6_addr*, uint8_t, uint32_t);
+
+#define DHCPv6_MSGTYPE_SOLICIT 1
+#define DHCPv6_MSGTYPE_ADVERTISE 2
+#define DHCPv6_MSGTYPE_REQUEST 3
+#define DHCPv6_MSGTYPE_CONFIRM 4
+#define DHCPv6_MSGTYPE_RENEW 5
+#define DHCPv6_MSGTYPE_REBIND 6
+#define DHCPv6_MSGTYPE_REPLY 7
+#define DHCPv6_MSGTYPE_RELEASE 8
+#define DHCPv6_MSGTYPE_DECLINE 9
+#define DHCPv6_MSGTYPE_RECONFIGURE 10
+#define DHCPv6_MSGTYPE_INFORMATION_REQUEST 11
+#define DHCPv6_MSGTYPE_RELAY_FORW 12
+#define DHCPv6_MSGTYPE_RELAY_REPL 13
+
+#define DHCPv6_OPTION_CLIENTID 1
+#define DHCPv6_OPTION_SERVERID 2
+#define DHCPv6_OPTION_IA_NA 3
+#define DHCPv6_OPTION_IA_TA 4
+#define DHCPv6_OPTION_IAADDR 5
+#define DHCPv6_OPTION_ORO 6
+#define DHCPv6_OPTION_PREFERENCE 7
+#define DHCPv6_OPTION_ELAPSED_TIME 8
+#define DHCPv6_OPTION_RELAY_MSG 9
+#define DHCPv6_OPTION_AUTH 11
+#define DHCPv6_OPTION_UNICAST 12
+#define DHCPv6_OPTION_STATUS_CODE 13
+#define DHCPv6_OPTION_RAPID_COMMIT 14
+#define DHCPv6_OPTION_USER_CLASS 15
+#define DHCPv6_OPTION_VENDOR_CLASS 16
+#define DHCPv6_OPTION_VENDOR_OPTS 17
+#define DHCPv6_OPTION_INTERFACE_ID 18
+#define DHCPv6_OPTION_RECONF_MSG 19
+#define DHCPv6_OPTION_RECONF_ACCEPT 20
+#define DHCPv6_OPTION_SIP_SERVER_D 21
+#define DHCPv6_OPTION_SIP_SERVER_A 22
+#define DHCPv6_OPTION_DNS_SERVERS 23
+#define DHCPv6_OPTION_DOMAIN_LIST 24
+#define DHCPv6_OPTION_IA_PD 25
+#define DHCPv6_OPTION_IAPREFIX 26
+#define DHCPv6_OPTION_NIS_SERVERS 27
+#define DHCPv6_OPTION_NISP_SERVERS 28
+#define DHCPv6_OPTION_NIS_DOMAIN_NAME 29
+#define DHCPv6_OPTION_NISP_DOMAIN_NAME 30
+#define DHCPv6_OPTION_SNTP_SERVERS 31
+#define DHCPv6_OPTION_INFORMATION_REFRESH_TIME 32
+#define DHCPv6_OPTION_BCMCS_SERVER_D 33
+#define DHCPv6_OPTION_BCMCS_SERVER_A 34
+#define DHCPv6_OPTION_GEOCONF_CIVIC 36
+#define DHCPv6_OPTION_REMOTE_ID 37
+#define DHCPv6_OPTION_SUBSCRIBER_ID 38
+#define DHCPv6_OPTION_CLIENT_FQDN 39
+#define DHCPv6_OPTION_PANA_AGENT 40
+#define DHCPv6_OPTION_NEW_POSIX_TIMEZONE 41
+#define DHCPv6_OPTION_NEW_TZDB_TIMEZONE 42
+#define DHCPv6_OPTION_ERO 43
+#define DHCPv6_OPTION_LQ_QUERY 44
+#define DHCPv6_OPTION_CLIENT_DATA 45
+#define DHCPv6_OPTION_CLT_TIME 46
+#define DHCPv6_OPTION_LQ_RELAY_DATA 47
+#define DHCPv6_OPTION_LQ_CLIENT_LINK 48
+#define DHCPv6_OPTION_MIP6_HNIDF 49
+#define DHCPv6_OPTION_MIP6_VDINF 50
+#define DHCPv6_OPTION_V6_LOST 51
+#define DHCPv6_OPTION_CAPWAP_AC_V6 52
+#define DHCPv6_OPTION_RELAY_ID 53
+#define DHCPv6_OPTION_IPv6_ADDRESS_MOS 54
+#define DHCPv6_OPTION_IPv6_FQDN_MOS 55
+#define DHCPv6_OPTION_NTP_SERVER 56
+#define DHCPv6_OPTION_V6_ACCESS_DOMAIN 57
+#define DHCPv6_OPTION_SIP_UA_CS_LIST 58
+#define DHCPv6_OPTION_BOOTFILE_URL 59
+#define DHCPv6_OPTION_BOOTFILE_PARAM 60
+#define DHCPv6_OPTION_CLIENT_ARCH_TYPE 61
+#define DHCPv6_OPTION_NII 62
+#define DHCPv6_OPTION_GEOLOCATION 63
+#define DHCPv6_OPTION_AFTR_NAME 64
+#define DHCPv6_OPTION_ERP_LOCAL_DOMAIN_NAME 65
+#define DHCPv6_OPTION_RSOO 66
+#define DHCPv6_OPTION_PD_EXCLUDE 67
+#define DHCPv6_OPTION_VSS 68
+#define DHCPv6_OPTION_MIP6_IDINF 69
+#define DHCPv6_OPTION_MIP6_UDINF 70
+#define DHCPv6_OPTION_MIP6_HNP 71
+#define DHCPv6_OPTION_MIP6_HAA 72
+#define DHCPv6_OPTION_MIP6_HAF 73
+#define DHCPv6_OPTION_RDNSS_SELECTION 74
+#define DHCPv6_OPTION_KRB_PRINCIPAL_NAME 75
+#define DHCPv6_OPTION_KRB_REALM_NAME 76
+#define DHCPv6_OPTION_KRB_DEFAULT_REALM_NAME 77
+#define DHCPv6_OPTION_KRB_KDC 78
+#define DHCPv6_OPTION_CLIENT_LINKLAYER_ADDR 79
+#define DHCPv6_OPTION_LINK_ADDRESS 80
+#define DHCPv6_OPTION_RADIUS 81
+#define DHCPv6_OPTION_SOL_MAX_RT 82
+#define DHCPv6_OPTION_INF_MAX_RT 83
+#define DHCPv6_OPTION_ADDRSEL 84
+#define DHCPv6_OPTION_ADDRSEL_TABLE 85
+#define DHCPv6_OPTION_V6_PCP_SERVER 86
+#define DHCPv6_OPTION_DHCPV4_MSG 87
+#define DHCPv6_OPTION_DHCP4_O_DHCP6_SERVER 88
+#define DHCPv6_OPTION_S46_RULE 89
+#define DHCPv6_OPTION_S46_BR 90
+#define DHCPv6_OPTION_S46_DMR 91
+#define DHCPv6_OPTION_S46_V4V6BIND 92
+#define DHCPv6_OPTION_S46_PORTPARAMS 93
+#define DHCPv6_OPTION_S46_CONT_MAPE 94
+#define DHCPv6_OPTION_S46_CONT_MAPT 95
+#define DHCPv6_OPTION_S46_CONT_LW 96
+#define DHCPv6_OPTION_4RD 97
+#define DHCPv6_OPTION_4RD_MAP_RULE 98
+#define DHCPv6_OPTION_4RD_NON_MAP_RULE 99
+#define DHCPv6_OPTION_LQ_BASE_TIME 100
+#define DHCPv6_OPTION_LQ_START_TIME 101
+#define DHCPv6_OPTION_LQ_END_TIME 102
+#define DHCPv6_OPTION_CAPTIVE_PORTAL 103
+#define DHCPv6_OPTION_MPL_PARAMETERS 104
+#define DHCPv6_OPTION_ANI_ATT 105
+#define DHCPv6_OPTION_ANI_NETWORK_NAME 106
+#define DHCPv6_OPTION_ANI_AP_NAME 107
+#define DHCPv6_OPTION_ANI_AP_BSSID 108
+#define DHCPv6_OPTION_ANI_OPERATOR_ID 109
+#define DHCPv6_OPTION_ANI_OPERATOR_REALM 110
+#define DHCPv6_OPTION_S46_PRIORITY 111
+#define DHCPv6_OPTION_MUD_URL_V6 112
+#define DHCPv6_OPTION_V6_PREFIX64 113
+#define DHCPv6_OPTION_F_BINDING_STATUS 114
+#define DHCPv6_OPTION_F_CONNECT_FLAGS 115
+#define DHCPv6_OPTION_F_DNS_REMOVAL_INFO 116
+#define DHCPv6_OPTION_F_DNS_HOST_NAME 117
+#define DHCPv6_OPTION_F_DNS_ZONE_NAME 118
+#define DHCPv6_OPTION_F_DNS_FLAGS 119
+#define DHCPv6_OPTION_F_EXPIRATION_TIME 120
+#define DHCPv6_OPTION_F_MAX_UNACKED_BNDUPD 121
+#define DHCPv6_OPTION_F_MCLT 122
+#define DHCPv6_OPTION_F_PARTNER_LIFETIME 123
+#define DHCPv6_OPTION_F_PARTNER_LIFETIME_SENT 124
+#define DHCPv6_OPTION_F_PARTNER_DOWN_TIME 125
+#define DHCPv6_OPTION_F_PARTNER_RAW_CLT_TIME 126
+#define DHCPv6_OPTION_F_PROTOCOL_VERSION 127
+#define DHCPv6_OPTION_F_KEEPALIVE_TIME 128
+#define DHCPv6_OPTION_F_RECONFIGURE_DATA 129
+#define DHCPv6_OPTION_F_RELATIONSHIP_NAME 130
+#define DHCPv6_OPTION_F_SERVER_FLAGS 131
+#define DHCPv6_OPTION_F_SERVER_STATE 132
+#define DHCPv6_OPTION_F_START_TIME_OF_STATE 133
+#define DHCPv6_OPTION_F_STATE_EXPIRATION_TIME 134
+#define DHCPv6_OPTION_RELAY_PORT 135
+#define DHCPv6_OPTION_V6_SZTP_REDIRECT 136
+#define DHCPv6_OPTION_S46_BIND_IPV6_PREFIX 137
+#define DHCPv6_OPTION_IA_LL 138
+#define DHCPv6_OPTION_LLADDR 139
+#define DHCPv6_OPTION_SLAP_QUAD 140
+#define DHCPv6_OPTION_V6_DOTS_RI 141
+#define DHCPv6_OPTION_V6_DOTS_ADDRESS 142
+#define DHCPv6_OPTION_IPV6_ADDRESS_ANDSF 143
+#define DHCPv6_OPTION_V6_DNR 144
+#define DHCPv6_OPTION_REGISTERED_DOMAIN 145
+#define DHCPv6_OPTION_FORWARD_DIST_MANAGER 146
+#define DHCPv6_OPTION_REVERSE_DIST_MANAGER 147
+
+#endif