odhcpd: backport fixes from Git HEAD (2025-10-10)

A lot of recent commits in odhcpd main branch are structural/semantic
changes, so instead of adding those changes to 24.10 branch it's better
to focus on the fixes.

Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
This commit is contained in:
Álvaro Fernández Rojas
2025-10-18 19:27:27 +02:00
parent be32ab85cf
commit 180646ce4b
4 changed files with 476 additions and 0 deletions
@@ -0,0 +1,61 @@
From f0d855358b86a36efbfeb5a9de5a1d2a4d9d80fe Mon Sep 17 00:00:00 2001
From: Haoyi Ci <cihaoyi@outlook.com>
Date: Thu, 2 Oct 2025 16:14:05 +0800
Subject: [PATCH] ndp: Allow NS loopback for master iface
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This commit modifies handle_solicit() in ndp.c to correct IPv6 relay
handling of Neighbor Solicitation (NS) requests when no upstream
solicitation is received.
Background: In IPv6 relay mode, odhcpd discovers local devices only upon
receiving upstream NS packets. If no upstream NS arrives (e.g. because the
upstream routers neighbor cache is still valid or no solicitation was
ever sent), OpenWrt may attempt neighbor resolution via the master (WAN)
interface instead of the LAN, leaving local devices undiscoverable and
breaking connectivity.
- When an NS packet is sent by the host's master interface, do not
immediately return; instead continue searching slave interfaces for the
target neighbor.
- When odhcpd responds to NS packets, add a check to prevent replying to
NS packets that were sent by the host itself.
Signed-off-by: Haoyi Ci cihaoyi@outlook.com
Link: https://github.com/openwrt/odhcpd/pull/240
Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
---
src/ndp.c | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
--- a/src/ndp.c
+++ b/src/ndp.c
@@ -332,6 +332,7 @@ static void handle_solicit(void *addr, v
struct interface *c;
char ipbuf[INET6_ADDRSTRLEN];
uint8_t mac[6];
+ bool is_self_sent;
/* Solicitation is for duplicate address detection */
bool ns_is_dad = IN6_IS_ADDR_UNSPECIFIED(&ip6->ip6_src);
@@ -354,7 +355,8 @@ static void handle_solicit(void *addr, v
syslog(LOG_DEBUG, "Got a NS for %s on %s", ipbuf, iface->name);
odhcpd_get_mac(iface, mac);
- if (!memcmp(ll->sll_addr, mac, sizeof(mac)))
+ is_self_sent = !memcmp(ll->sll_addr, mac, sizeof(mac));
+ if (is_self_sent && !iface->master)
return; /* Looped back */
avl_for_each_element(&interfaces, c, avl) {
@@ -366,7 +368,7 @@ static void handle_solicit(void *addr, v
/* Catch global-addressed NS and answer them manually.
* The kernel won't answer these and cannot route them either. */
if (!IN6_IS_ADDR_MULTICAST(&ip6->ip6_dst) &&
- IN6_IS_ADDR_LINKLOCAL(&ip6->ip6_src)) {
+ IN6_IS_ADDR_LINKLOCAL(&ip6->ip6_src) && !is_self_sent) {
bool is_proxy_neigh = netlink_get_interface_proxy_neigh(iface->ifindex,
&req->nd_ns_target) == 1;
@@ -0,0 +1,86 @@
From 5eac9c56ff3b0a013c5241f449ca144f70bf4c02 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= <noltari@gmail.com>
Date: Tue, 7 Oct 2025 10:30:15 +0200
Subject: [PATCH] router: fix SLAAC on subnets > 64
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
---
src/router.c | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
--- a/src/router.c
+++ b/src/router.c
@@ -448,13 +448,13 @@ struct nd_opt_dnr_info {
/* IPv6 RA PIOs */
static struct ra_pio *router_find_ra_pio(struct interface *iface,
- struct nd_opt_prefix_info *p)
+ struct odhcpd_ipaddr *addr)
{
for (size_t i = 0; i < iface->pio_cnt; i++) {
struct ra_pio *cur_pio = &iface->pios[i];
- if (p->nd_opt_pi_prefix_len == cur_pio->length &&
- !odhcpd_bmemcmp(&p->nd_opt_pi_prefix, &cur_pio->prefix, cur_pio->length))
+ if (addr->prefix == cur_pio->length &&
+ !odhcpd_bmemcmp(&addr->addr.in6, &cur_pio->prefix, cur_pio->length))
return cur_pio;
}
@@ -462,12 +462,12 @@ static struct ra_pio *router_find_ra_pio
}
static void router_add_ra_pio(struct interface *iface,
- struct nd_opt_prefix_info *p)
+ struct odhcpd_ipaddr *addr)
{
char ipv6_str[INET6_ADDRSTRLEN];
struct ra_pio *new_pios, *pio;
- pio = router_find_ra_pio(iface, p);
+ pio = router_find_ra_pio(iface, addr);
if (pio) {
if (pio->lifetime) {
pio->lifetime = 0;
@@ -490,8 +490,8 @@ static void router_add_ra_pio(struct int
pio = &iface->pios[iface->pio_cnt];
iface->pio_cnt++;
- memcpy(&pio->prefix, &p->nd_opt_pi_prefix, sizeof(pio->prefix));
- pio->length = p->nd_opt_pi_prefix_len;
+ memcpy(&pio->prefix, &addr->addr.in6, sizeof(pio->prefix));
+ pio->length = addr->prefix;
pio->lifetime = 0;
iface->pio_update = true;
@@ -538,10 +538,10 @@ static void router_clear_ra_pio(time_t n
}
static void router_stale_ra_pio(struct interface *iface,
- struct nd_opt_prefix_info *p,
+ struct odhcpd_ipaddr *addr,
time_t now)
{
- struct ra_pio *pio = router_find_ra_pio(iface, p);
+ struct ra_pio *pio = router_find_ra_pio(iface, addr);
char ipv6_str[INET6_ADDRSTRLEN];
if (!pio || pio->lifetime)
@@ -774,12 +774,12 @@ static int send_router_advert(struct int
p->nd_opt_pi_preferred_time = 0;
p->nd_opt_pi_valid_time = 0;
- router_stale_ra_pio(iface, p, now);
+ router_stale_ra_pio(iface, addr, now);
} else {
p->nd_opt_pi_preferred_time = htonl(preferred_lt);
p->nd_opt_pi_valid_time = htonl(valid_lt);
- router_add_ra_pio(iface, p);
+ router_add_ra_pio(iface, addr);
}
}
@@ -0,0 +1,285 @@
From d402cdae431668f55f9d82b7072b0afa3b8090df Mon Sep 17 00:00:00 2001
From: Stephen Groat <stephen.groat@datadoghq.com>
Date: Wed, 8 Oct 2025 11:54:51 -0700
Subject: [PATCH] ndp: fix macOS IPv6 compatibility by using link-local source
addresses
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
macOS ignores NDP packets that don't originate from link-local addresses,
causing IPv6 connectivity issues with odhcpd. This change ensures NDP
packets (Neighbor Advertisements and ICMP Echo Requests) are sent using
link-local source addresses for RFC 4861 compliance.
Changes:
* Add ndp_from_link_local configuration flag (defaults to true)
* Add odhcpd_send_with_src() to allow explicit source address control
* Add odhcpd_try_send_with_src() helper to eliminate code duplication
* Add odhcpd_get_interface_linklocal_addr() with caching for performance
* Update send_na() and ping6() to use link-local source addresses when
enabled
* Add RFC 4861, §4.2 comments explaining the mandated behavior
* Maintain backward compatibility with fallback behavior
Fixes: openwrt/openwrt#7561 #202
Signed-off-by: Stephen Groat <stephengroat@gmail.com>
Link: https://github.com/openwrt/odhcpd/pull/242
Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
---
README.md | 1 +
src/config.c | 7 ++++
src/ndp.c | 12 ++++---
src/odhcpd.c | 91 ++++++++++++++++++++++++++++++++++++++++------------
src/odhcpd.h | 11 +++++++
5 files changed, 97 insertions(+), 25 deletions(-)
--- a/README.md
+++ b/README.md
@@ -114,6 +114,7 @@ and may also receive information from ub
| ra_pref64 |string | - | Announce PREF64 option for NAT64 prefix (RFC8781) [IPv6 prefix] |
| ndproxy_routing |bool | 1 | Learn routes from NDP |
| ndproxy_slave |bool | 0 | NDProxy external slave |
+| ndp_from_link_local |bool | 1 | Use link-local source addresses for NDP operations (RFC 4861, §4.2 compliance) and macOS compatibility |
| prefix_filter |string |`::/0` | Only advertise on-link prefixes within the provided IPv6 prefix; others are filtered out. [IPv6 prefix] |
| ntp |list |`<local address>`| NTP servers to announce accepts IPv4 and IPv6 |
--- a/src/config.c
+++ b/src/config.c
@@ -117,6 +117,7 @@ enum {
IFACE_ATTR_PD_CER,
IFACE_ATTR_NDPROXY_ROUTING,
IFACE_ATTR_NDPROXY_SLAVE,
+ IFACE_ATTR_NDP_FROM_LINK_LOCAL,
IFACE_ATTR_PREFIX_FILTER,
IFACE_ATTR_MAX_PREFERRED_LIFETIME,
IFACE_ATTR_MAX_VALID_LIFETIME,
@@ -171,6 +172,7 @@ static const struct blobmsg_policy iface
[IFACE_ATTR_RA_PREF64] = { .name = "ra_pref64", .type = BLOBMSG_TYPE_STRING },
[IFACE_ATTR_NDPROXY_ROUTING] = { .name = "ndproxy_routing", .type = BLOBMSG_TYPE_BOOL },
[IFACE_ATTR_NDPROXY_SLAVE] = { .name = "ndproxy_slave", .type = BLOBMSG_TYPE_BOOL },
+ [IFACE_ATTR_NDP_FROM_LINK_LOCAL] = { .name = "ndp_from_link_local", .type = BLOBMSG_TYPE_BOOL },
[IFACE_ATTR_PREFIX_FILTER] = { .name = "prefix_filter", .type = BLOBMSG_TYPE_STRING },
[IFACE_ATTR_MAX_PREFERRED_LIFETIME] = { .name = "max_preferred_lifetime", .type = BLOBMSG_TYPE_STRING },
[IFACE_ATTR_MAX_VALID_LIFETIME] = { .name = "max_valid_lifetime", .type = BLOBMSG_TYPE_STRING },
@@ -271,6 +273,8 @@ static void set_interface_defaults(struc
iface->ra = MODE_DISABLED;
iface->ndp = MODE_DISABLED;
iface->learn_routes = 1;
+ iface->ndp_from_link_local = true;
+ iface->cached_linklocal_valid = false;
iface->dhcp_leasetime = 43200;
iface->max_preferred_lifetime = ND_PREFERRED_LIMIT;
iface->max_valid_lifetime = ND_VALID_LIMIT;
@@ -1451,6 +1455,9 @@ int config_parse_interface(void *data, s
if ((c = tb[IFACE_ATTR_NDPROXY_SLAVE]))
iface->external = blobmsg_get_bool(c);
+ if ((c = tb[IFACE_ATTR_NDP_FROM_LINK_LOCAL]))
+ iface->ndp_from_link_local = blobmsg_get_bool(c);
+
if ((c = tb[IFACE_ATTR_PREFIX_FILTER]))
odhcpd_parse_addr6_prefix(blobmsg_get_string(c),
&iface->pio_filter_addr,
--- a/src/ndp.c
+++ b/src/ndp.c
@@ -280,7 +280,7 @@ static void ndp_netevent_cb(unsigned lon
/* Send an ICMP-ECHO. This is less for actually pinging but for the
* neighbor cache to be kept up-to-date. */
static void ping6(struct in6_addr *addr,
- const struct interface *iface)
+ struct interface *iface)
{
struct sockaddr_in6 dest = { .sin6_family = AF_INET6, .sin6_addr = *addr , };
struct icmp6_hdr echo = { .icmp6_type = ICMP6_ECHO_REQUEST };
@@ -291,13 +291,16 @@ static void ping6(struct in6_addr *addr,
syslog(LOG_DEBUG, "Pinging for %s on %s", ipbuf, iface->name);
netlink_setup_route(addr, 128, iface->ifindex, NULL, 128, true);
- odhcpd_send(iface->ndp_ping_fd, &dest, &iov, 1, iface);
+
+ /* Use link-local address as source for RFC 4861 compliance and macOS compatibility */
+ odhcpd_try_send_with_src(iface->ndp_ping_fd, &dest, &iov, 1, iface);
+
netlink_setup_route(addr, 128, iface->ifindex, NULL, 128, false);
}
/* Send a Neighbor Advertisement. */
static void send_na(struct in6_addr *to_addr,
- const struct interface *iface, struct in6_addr *for_addr,
+ struct interface *iface, struct in6_addr *for_addr,
const uint8_t *mac)
{
struct sockaddr_in6 dest = { .sin6_family = AF_INET6, .sin6_addr = *to_addr };
@@ -319,7 +322,8 @@ static void send_na(struct in6_addr *to_
inet_ntop(AF_INET6, to_addr, ipbuf, sizeof(ipbuf));
syslog(LOG_DEBUG, "Answering NS to %s on %s", ipbuf, iface->ifname);
- odhcpd_send(iface->ndp_ping_fd, &dest, &iov, 1, iface);
+ /* Use link-local address as source for RFC 4861 compliance and macOS compatibility */
+ odhcpd_try_send_with_src(iface->ndp_ping_fd, &dest, &iov, 1, iface);
}
/* Handle solicitations */
--- a/src/odhcpd.c
+++ b/src/odhcpd.c
@@ -189,10 +189,10 @@ int odhcpd_get_flags(const struct interf
}
-/* Forwards a packet on a specific interface */
-ssize_t odhcpd_send(int socket, struct sockaddr_in6 *dest,
+/* Forwards a packet on a specific interface with optional source address */
+ssize_t odhcpd_send_with_src(int socket, struct sockaddr_in6 *dest,
struct iovec *iov, size_t iov_len,
- const struct interface *iface)
+ const struct interface *iface, const struct in6_addr *src_addr)
{
/* Construct headers */
uint8_t cmsg_buf[CMSG_SPACE(sizeof(struct in6_pktinfo))] = {0};
@@ -214,6 +214,10 @@ ssize_t odhcpd_send(int socket, struct s
struct in6_pktinfo *pktinfo = (struct in6_pktinfo*)CMSG_DATA(chdr);
pktinfo->ipi6_ifindex = iface->ifindex;
+ /* Set source address if provided */
+ if (src_addr)
+ pktinfo->ipi6_addr = *src_addr;
+
/* Also set scope ID if link-local */
if (IN6_IS_ADDR_LINKLOCAL(&dest->sin6_addr)
|| IN6_IS_ADDR_MC_LINKLOCAL(&dest->sin6_addr))
@@ -232,30 +236,75 @@ ssize_t odhcpd_send(int socket, struct s
return sent;
}
+/* Forwards a packet on a specific interface */
+ssize_t odhcpd_send(int socket, struct sockaddr_in6 *dest,
+ struct iovec *iov, size_t iov_len,
+ const struct interface *iface)
+{
+ return odhcpd_send_with_src(socket, dest, iov, iov_len, iface, NULL);
+}
+
-static int odhcpd_get_linklocal_interface_address(int ifindex, struct in6_addr *lladdr)
+int odhcpd_get_interface_linklocal_addr(struct interface *iface, struct in6_addr *addr)
{
- int ret = -1;
- struct sockaddr_in6 addr;
- socklen_t alen = sizeof(addr);
- int sock = socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
+ /* Return cached address if valid */
+ if (iface->cached_linklocal_valid) {
+ *addr = iface->cached_linklocal_addr;
+ return 0;
+ }
- if (sock < 0)
- return -1;
+ /* First try to get link-local address from interface addresses */
+ for (size_t i = 0; i < iface->addr6_len; ++i) {
+ if (IN6_IS_ADDR_LINKLOCAL(&iface->addr6[i].addr.in6)) {
+ *addr = iface->addr6[i].addr.in6;
+ /* Cache the result for future use */
+ iface->cached_linklocal_addr = *addr;
+ iface->cached_linklocal_valid = true;
+ return 0;
+ }
+ }
- memset(&addr, 0, sizeof(addr));
- addr.sin6_family = AF_INET6;
- inet_pton(AF_INET6, ALL_IPV6_ROUTERS, &addr.sin6_addr);
- addr.sin6_scope_id = ifindex;
-
- if (!connect(sock, (struct sockaddr*)&addr, sizeof(addr)) &&
- !getsockname(sock, (struct sockaddr*)&addr, &alen)) {
- *lladdr = addr.sin6_addr;
- ret = 0;
+ /* Fallback to socket-based method */
+ struct sockaddr_in6 sockaddr;
+ socklen_t alen = sizeof(sockaddr);
+ int sock = socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
+
+ if (sock >= 0) {
+ memset(&sockaddr, 0, sizeof(sockaddr));
+ sockaddr.sin6_family = AF_INET6;
+ inet_pton(AF_INET6, ALL_IPV6_ROUTERS, &sockaddr.sin6_addr);
+ sockaddr.sin6_scope_id = iface->ifindex;
+
+ if (!connect(sock, (struct sockaddr*)&sockaddr, sizeof(sockaddr)) &&
+ !getsockname(sock, (struct sockaddr*)&sockaddr, &alen)) {
+ *addr = sockaddr.sin6_addr;
+ /* Cache the result for future use */
+ iface->cached_linklocal_addr = *addr;
+ iface->cached_linklocal_valid = true;
+ close(sock);
+ return 0;
+ }
+ close(sock);
}
- close(sock);
- return ret;
+ return -1;
+}
+
+/* Try to send with link-local source address for RFC 4861 compliance and macOS compatibility.
+ * RFC 4861, §4.2 mandates that Neighbor Advertisement source address MUST be
+ * the link-local address assigned to the interface from which this message is sent. */
+ssize_t odhcpd_try_send_with_src(int socket, struct sockaddr_in6 *dest,
+ struct iovec *iov, size_t iov_len,
+ struct interface *iface)
+{
+ struct in6_addr src_addr;
+
+ if (iface->ndp_from_link_local && odhcpd_get_interface_linklocal_addr(iface, &src_addr) == 0) {
+ return odhcpd_send_with_src(socket, dest, iov, iov_len, iface, &src_addr);
+ } else {
+ /* Fall back to default behavior if no link-local address is available or flag is disabled */
+ return odhcpd_send(socket, dest, iov, iov_len, iface);
+ }
}
/*
@@ -303,7 +352,7 @@ int odhcpd_get_interface_dns_addr(const
return 0;
}
- return odhcpd_get_linklocal_interface_address(iface->ifindex, addr);
+ return odhcpd_get_interface_linklocal_addr(iface, addr);
}
struct interface* odhcpd_get_interface_by_index(int ifindex)
--- a/src/odhcpd.h
+++ b/src/odhcpd.h
@@ -333,6 +333,9 @@ struct interface {
// NDP
int learn_routes;
+ bool ndp_from_link_local;
+ struct in6_addr cached_linklocal_addr;
+ bool cached_linklocal_valid;
// RA
uint8_t ra_flags;
@@ -469,11 +472,19 @@ int odhcpd_register(struct odhcpd_event
int odhcpd_deregister(struct odhcpd_event *event);
void odhcpd_process(struct odhcpd_event *event);
+ssize_t odhcpd_send_with_src(int socket, struct sockaddr_in6 *dest,
+ struct iovec *iov, size_t iov_len,
+ const struct interface *iface, const struct in6_addr *src_addr);
ssize_t odhcpd_send(int socket, struct sockaddr_in6 *dest,
struct iovec *iov, size_t iov_len,
const struct interface *iface);
+ssize_t odhcpd_try_send_with_src(int socket, struct sockaddr_in6 *dest,
+ struct iovec *iov, size_t iov_len,
+ struct interface *iface);
int odhcpd_get_interface_dns_addr(const struct interface *iface,
struct in6_addr *addr);
+int odhcpd_get_interface_linklocal_addr(struct interface *iface,
+ struct in6_addr *addr);
int odhcpd_get_interface_config(const char *ifname, const char *what);
int odhcpd_get_mac(const struct interface *iface, uint8_t mac[6]);
int odhcpd_get_flags(const struct interface *iface);
@@ -0,0 +1,44 @@
From 30780debd691aee7567784daf1fdfd8db500a485 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20H=C3=A4rdeman?= <david@hardeman.nu>
Date: Thu, 9 Oct 2025 11:08:14 +0200
Subject: [PATCH] odhcpd: fix a compilation error
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
odhcpd_get_interface_dns_addr() calls odhcpd_get_interface_linklocal_addr(),
the former takes a const struct interface ptr, the latter takes a non-const
struct interface ptr.
The end result is an unhappy compiler.
Signed-off-by: David Härdeman <david@hardeman.nu>
Link: https://github.com/openwrt/odhcpd/pull/272
Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
---
src/odhcpd.c | 2 +-
src/odhcpd.h | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
--- a/src/odhcpd.c
+++ b/src/odhcpd.c
@@ -314,7 +314,7 @@ ssize_t odhcpd_try_send_with_src(int soc
* - use an IPv6 ULA address if the already selected IPv6 address is not an ULA address
* - use the IPv6 address with the longest preferred lifetime
*/
-int odhcpd_get_interface_dns_addr(const struct interface *iface, struct in6_addr *addr)
+int odhcpd_get_interface_dns_addr(struct interface *iface, struct in6_addr *addr)
{
time_t now = odhcpd_time();
ssize_t m = -1;
--- a/src/odhcpd.h
+++ b/src/odhcpd.h
@@ -481,7 +481,7 @@ ssize_t odhcpd_send(int socket, struct s
ssize_t odhcpd_try_send_with_src(int socket, struct sockaddr_in6 *dest,
struct iovec *iov, size_t iov_len,
struct interface *iface);
-int odhcpd_get_interface_dns_addr(const struct interface *iface,
+int odhcpd_get_interface_dns_addr(struct interface *iface,
struct in6_addr *addr);
int odhcpd_get_interface_linklocal_addr(struct interface *iface,
struct in6_addr *addr);