From: Jaco Kroon Date: Thu, 6 Mar 2025 11:49:33 +0000 (+0200) Subject: dhcpv6relay plugin. X-Git-Url: https://git.ozlabs.org/?a=commitdiff_plain;h=7a4c2d6f352b52980bab48131f7ddeda42661d56;p=ppp.git dhcpv6relay plugin. After the plugin is loaded, it just needs dhcpv6-server option to know where to relay to. dhcpv6-trusted / dhcpv6-untrusted can be used to mark the port as trusted (will further forward relay-fwd messages) or unstrusted (will only accept and forward client originated messages. dhcpv6-metric - metric at which to install IPv6 routes into the kernel routing table for prefix delegations. Server needs to support fwd option 135 (aka relay-port) in order to respond to pppd. KEA (for one) does. Signed-off-by: Jaco Kroon --- diff --git a/configure.ac b/configure.ac index 8f20192..3900dc8 100644 --- a/configure.ac +++ b/configure.ac @@ -330,6 +330,7 @@ AC_CONFIG_FILES([ pppd/plugins/pppoatm/Makefile pppd/plugins/pppol2tp/Makefile pppd/plugins/radius/Makefile + pppd/plugins/dhcpv6relay/Makefile pppdump/Makefile pppstats/Makefile scripts/Makefile diff --git a/pppd/plugins/Makefile.am b/pppd/plugins/Makefile.am index 9480d51..267d669 100644 --- a/pppd/plugins/Makefile.am +++ b/pppd/plugins/Makefile.am @@ -17,5 +17,5 @@ winbind_la_LDFLAGS = $(PLUGIN_LDFLAGS) winbind_la_SOURCES = winbind.c if !SUNOS -SUBDIRS = pppoe pppoatm pppol2tp radius +SUBDIRS = pppoe pppoatm pppol2tp radius dhcpv6relay endif diff --git a/pppd/plugins/dhcpv6relay/Makefile.am b/pppd/plugins/dhcpv6relay/Makefile.am new file mode 100644 index 0000000..8d9a3e7 --- /dev/null +++ b/pppd/plugins/dhcpv6relay/Makefile.am @@ -0,0 +1,8 @@ +pppd_plugin_LTLIBRARIES = dhcpv6relay.la +pppd_plugindir = $(PPPD_PLUGIN_DIR) + +noinst_HEADERS = dhcpv6relay.h + +dhcpv6relay_la_CPPFLAGS = -I${top_srcdir} -DSYSCONFDIR=\"${sysconfdir}\" -DPLUGIN +dhcpv6relay_la_LDFLAGS = -module -avoid-version +dhcpv6relay_la_SOURCES = dhcpv6relay.c diff --git a/pppd/plugins/dhcpv6relay/dhcpv6relay.c b/pppd/plugins/dhcpv6relay/dhcpv6relay.c new file mode 100644 index 0000000..3fbf3e4 --- /dev/null +++ b/pppd/plugins/dhcpv6relay/dhcpv6relay.c @@ -0,0 +1,799 @@ +/* + * dhcpv6relay.c - DHCPv6 relay plugin. + * + * Copyright (c) 2025 Ultimate Linux Solutions (Pty) Ltd represented by + * Jaco Kroon . 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 +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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); +} diff --git a/pppd/plugins/dhcpv6relay/dhcpv6relay.h b/pppd/plugins/dhcpv6relay/dhcpv6relay.h new file mode 100644 index 0000000..cce6251 --- /dev/null +++ b/pppd/plugins/dhcpv6relay/dhcpv6relay.h @@ -0,0 +1,203 @@ +/* + * dhcpv6relay.h - DHCPv6 relay plugin. + * + * Copyright (c) 2025 Ultimate Linux Solutions (Pty) Ltd represented by + * Jaco Kroon . 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 +#include +#include + +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