IPv6 temp address(es) with pltime=0 briefly loses deprecated on RA → OS may pick the old address as the source. [Environment] OS: FreeBSD 14.3-RELEASE-p2 (amd64, Intel CPU) Interface: wlan0 SLAAC with temporary/privacy addresses enabled All IPv6 addresses/prefixes below are obfuscated (e.g., 2001:db8:1234:5678:aaaa:bbbb:cccc:dddd) [Summary] Upon receipt of a Router Advertisement (RA), any existing IPv6 temporary address(es) whose preferred lifetime has already expired (pltime=0) briefly loses its deprecated state (~1 s) and then returns to deprecated. During this flip-flop window, the OS may select that old address as the source for new outbound connections, causing intermittent ACL denials on remote systems that whitelist only the current preferred address. This behavior appears non-compliant with RFC 4862/4941/8981 expectations (a pltime=0 address should remain deprecated for the rest of its valid lifetime) and can lead to unexpected, undesirable side effects, including transient exposure of the user’s old IPv6 addresses, which should not occur. [Steps to Reproduce] • Enable temporary addresses and connect to a network that advertises a global prefix. • Wait until an older temp address reaches pltime 0 while still having vltime > 0 (deprecated but valid). • Sample ifconfig <if> inet6 once per second while RAs are being received. • Observe that on each RA the deprecated address loses the “deprecated” label, then regains it after ~1 second. [Expected Behavior] Once pltime=0, the address should remain deprecated continuously; subsequent RAs must not create a window where it appears usable for new connections. [Actual Behavior] On each RA, the already-expired temporary address(es) becomes non-deprecated for ~1 second and then flips back to deprecated. [Impact] • During the ~1-second flip after each RA, the kernel may select a previously deprecated temporary address (pltime=0) as the source for new flows. • Leads to intermittent IP-ACL denials, policy/geofencing mismatches, and flaky behavior in automation that relies on stable source addresses. • Privacy risk: transient re-exposure of old temporary IPv6 addresses in outbound traffic, logs, and third-party telemetry—contrary to the privacy intent of RFC 4941/8981 rotation. • Disrupts observability and auditing (source attribution changes mid-session), complicating incident analysis and per-IP accounting/billing. [Code details] (FreeBSD 14.3-RELEASE-p2) (by ChatGPT - may have errors) Where “deprecated” is decided — sys/netinet6/in6.h: #define IFA6_IS_DEPRECATED(a) \ ((a)->ia6_lifetime.ia6t_pltime != ND6_INFINITE_LIFETIME && \ (u_int32_t)((time_uptime - (a)->ia6_updatetime)) > \ (a)->ia6_lifetime.ia6t_pltime) #define IFA6_IS_INVALID(a) \ ((a)->ia6_lifetime.ia6t_vltime != ND6_INFINITE_LIFETIME && \ (u_int32_t)((time_uptime - (a)->ia6_updatetime)) > \ (a)->ia6_lifetime.ia6t_vltime) Where RA lifetimes are refreshed — Router Advertisements are processed in sys/netinet6/nd6_rtr.c (nd6_ra_input(...)). During PIO handling, address lifetimes are updated and the address “update time” (ia6_updatetime) is refreshed (via the address/prefix update path in the IPv6 stack). Why the flip-flop happens (step by step) 1. Before RA: the old temporary address has pltime=0, so it is deprecated. 2. RA arrives → the RA path refreshes lifetimes and sets ia6_updatetime = time_uptime (now). 3. The predicate becomes: (time_uptime - ia6_updatetime) > pltime → (0) > 0 == false Thus, for ~1 tick, the address evaluates as non-deprecated. 4. After ~1 second, the left side becomes > 0, the predicate turns true, and the address is again deprecated. 5. In that short window the kernel may pick the old temp address as a source for new flows (observed in practice). [Evidence] (obfuscated sources) ifconfig wlan0 inet6: inet6 2001:db8:aaaa:bbbb:5555:6666:7777:8888 prefixlen 64 deprecated autoconf temporary pltime 0 vltime 400150 # immediately after RA: inet6 2001:db8:aaaa:bbbb:5555:6666:7777:8888 prefixlen 64 autoconf temporary pltime 0 vltime 400100 # lost 'deprecated' for ~1s # next second: inet6 2001:db8:aaaa:bbbb:5555:6666:7777:8888 prefixlen 64 deprecated autoconf temporary pltime 0 vltime 400100 $ repeat 500 ( sleep 0.5; ping -o google.com ) ... --- google.com ping statistics --- 1 packets transmitted, 1 packets received, 0.0% packet loss round-trip min/avg/max/stddev = 17.833/17.833/17.833/0.000 ms PING(56=40+8+8 bytes) 2001:db8:aaaa:bbbb:1111:2222:3333:4444 --> 2a00:1450:401b:808::200e 16 bytes from 2a00:1450:401b:808::200e, icmp_seq=0 hlim=120 time=17.495 ms --- google.com ping statistics --- 1 packets transmitted, 1 packets received, 0.0% packet loss round-trip min/avg/max/stddev = 17.495/17.495/17.495/0.000 ms PING(56=40+8+8 bytes) 2001:db8:aaaa:bbbb:5555:6666:7777:8888 --> 2a00:1450:401b:808::200e # Used deprecated IPv6 as source 16 bytes from 2a00:1450:401b:808::200e, icmp_seq=0 hlim=118 time=18.777 ms --- google.com ping statistics --- 1 packets transmitted, 1 packets received, 0.0% packet loss round-trip min/avg/max/stddev = 18.777/18.777/18.777/0.000 ms PING(56=40+8+8 bytes) 2001:db8:aaaa:bbbb:5555:6666:7777:8888 --> 2a00:1450:401b:808::200e # Used deprecated IPv6 as source 16 bytes from 2a00:1450:401b:808::200e, icmp_seq=0 hlim=118 time=17.698 ms --- google.com ping statistics --- 1 packets transmitted, 1 packets received, 0.0% packet loss round-trip min/avg/max/stddev = 17.698/17.698/17.698/0.000 ms PING(56=40+8+8 bytes) 2001:db8:aaaa:bbbb:1111:2222:3333:4444 --> 2a00:1450:401b:808::200e 16 bytes from 2a00:1450:401b:808::200e, icmp_seq=0 hlim=120 time=18.116 ms --- google.com ping statistics --- 1 packets transmitted, 1 packets received, 0.0% packet loss round-trip min/avg/max/stddev = 18.116/18.116/18.116/0.000 ms PING(56=40+8+8 bytes) 2001:db8:aaaa:bbbb:1111:2222:3333:4444 --> 2a00:1450:401b:808::200e 16 bytes from 2a00:1450:401b:808::200e, icmp_seq=0 hlim=120 time=18.183 ms
(In reply to Dmitry Nexus from comment #0) Nice catch, thanks for reporting. While the current mechanism probably deserves a more thorough rewrite, this simple patch should at least fix the off-by-one error. Does it resolve the issue for you? --- a/sys/netinet6/in6.h +++ b/sys/netinet6/in6.h @@ -358,11 +358,11 @@ extern const struct in6_addr in6addr_linklocal_allv2routers; #define IFA6_IS_DEPRECATED(a) \ ((a)->ia6_lifetime.ia6t_pltime != ND6_INFINITE_LIFETIME && \ - (u_int32_t)((time_uptime - (a)->ia6_updatetime)) > \ + (u_int32_t)((time_uptime - (a)->ia6_updatetime)) >= \ (a)->ia6_lifetime.ia6t_pltime) #define IFA6_IS_INVALID(a) \ ((a)->ia6_lifetime.ia6t_vltime != ND6_INFINITE_LIFETIME && \ - (u_int32_t)((time_uptime - (a)->ia6_updatetime)) > \ + (u_int32_t)((time_uptime - (a)->ia6_updatetime)) >= \ (a)->ia6_lifetime.ia6t_vltime) #endif /* _KERNEL */
It will be nice to fix this issue before branching stable/15 this week.
(In reply to Marek Zarychta from comment #2) I am currently testing this and plan to provide feedback within a few hours.
This is related if FreeBSD is the gateway...... https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=288426
(In reply to Marek Zarychta from comment #1) Tested on a FreeBSD 14.3-RELEASE test system with the proposed patch applied. Router Advertisements were sent at a high frequency (every few seconds). For faster IPv6 temporary address rotation, I configured short lifetimes via sysctl: net.inet6.ip6.temppltime=600 net.inet6.ip6.tempvltime=1500 The system with multiple deprecated addresses was observed for 30+ minutes. The flip-flop of the deprecated flag did not occur anymore. Many thanks for the quick fix and support.
(In reply to Dmitry Nexus from comment #5) Thanks for testing! I have created review D52323.
A commit in branch main references this bug: URL: https://cgit.FreeBSD.org/src/commit/?id=588a5fad3e8b98955b60707e3e92b8b43566e3f7 commit 588a5fad3e8b98955b60707e3e92b8b43566e3f7 Author: Andrey V. Elsukov <ae@FreeBSD.org> AuthorDate: 2025-09-16 07:34:55 +0000 Commit: Andrey V. Elsukov <ae@FreeBSD.org> CommitDate: 2025-09-16 07:34:55 +0000 IPv6: fix off-by-one in pltime and vltime expiration checks Previously, the macros used '>' instead of '>=' when comparing elapsed time against the preferred and valid lifetimes. This caused any deprecated address to become usable again for one extra second after receiving each Router Advertisement. In that short window, the address could be selected as a source for outgoing connections. Update the checks to use '>=' so that addresses are deprecated or invalid when their lifetime expires. PR: 289177 Reported by: Dmitry Nexus <fbsd.4f6a at nexus tel> Reviewed by: zlei Submitted by: Marek Zarychta MFC after: 1 week Differential Revision: https://reviews.freebsd.org/D52323 sys/netinet6/in6.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-)
A commit in branch stable/15 references this bug: URL: https://cgit.FreeBSD.org/src/commit/?id=fb4be8661166e18afa4b10921f7d5fbd22460390 commit fb4be8661166e18afa4b10921f7d5fbd22460390 Author: Andrey V. Elsukov <ae@FreeBSD.org> AuthorDate: 2025-09-16 07:34:55 +0000 Commit: Andrey V. Elsukov <ae@FreeBSD.org> CommitDate: 2025-09-23 08:31:12 +0000 IPv6: fix off-by-one in pltime and vltime expiration checks Previously, the macros used '>' instead of '>=' when comparing elapsed time against the preferred and valid lifetimes. This caused any deprecated address to become usable again for one extra second after receiving each Router Advertisement. In that short window, the address could be selected as a source for outgoing connections. Update the checks to use '>=' so that addresses are deprecated or invalid when their lifetime expires. PR: 289177 Reported by: Dmitry Nexus <fbsd.4f6a at nexus tel> Reviewed by: zlei Submitted by: Marek Zarychta Differential Revision: https://reviews.freebsd.org/D52323 (cherry picked from commit 588a5fad3e8b98955b60707e3e92b8b43566e3f7) sys/netinet6/in6.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-)
A commit in branch stable/14 references this bug: URL: https://cgit.FreeBSD.org/src/commit/?id=586183128f2f04b84a9a564f83289963671f0ff2 commit 586183128f2f04b84a9a564f83289963671f0ff2 Author: Andrey V. Elsukov <ae@FreeBSD.org> AuthorDate: 2025-09-16 07:34:55 +0000 Commit: Andrey V. Elsukov <ae@FreeBSD.org> CommitDate: 2025-09-23 08:32:45 +0000 IPv6: fix off-by-one in pltime and vltime expiration checks Previously, the macros used '>' instead of '>=' when comparing elapsed time against the preferred and valid lifetimes. This caused any deprecated address to become usable again for one extra second after receiving each Router Advertisement. In that short window, the address could be selected as a source for outgoing connections. Update the checks to use '>=' so that addresses are deprecated or invalid when their lifetime expires. PR: 289177 Reported by: Dmitry Nexus <fbsd.4f6a at nexus tel> Reviewed by: zlei Submitted by: Marek Zarychta Differential Revision: https://reviews.freebsd.org/D52323 (cherry picked from commit 588a5fad3e8b98955b60707e3e92b8b43566e3f7) sys/netinet6/in6.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-)
Fixed in main, stable/15 and stable/14. Thanks!