mirror of
https://github.com/oven-sh/bun
synced 2026-02-13 12:29:07 +00:00
fix(dns): implement RFC 6724 Rule 2 for IPv6 source address selection
This properly fixes the VPN IPv6 timeout issue by checking if a global IPv6 source address is available before preferring IPv6 destinations. RFC 6724 Rule 2 (Prefer matching scope) states that destinations should be sorted based on whether a matching-scope source address is available. When only link-local IPv6 source addresses exist (common on VPNs), global IPv6 destinations will fail because link-local cannot route to global. Implementation: - Added IPv6SourceAvailability to check for global IPv6 source addresses via getifaddrs() with 30-second caching - If global IPv6 source available: interleave IPv6/IPv4 (Happy Eyeballs) - If only link-local IPv6 source: prefer IPv4 over global IPv6 - Always deprioritize link-local IPv6 destinations This matches the behavior of properly configured systems and fixes connection timeouts on corporate VPNs (e.g., Cisco AnyConnect). Fixes #25619 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1428,10 +1428,100 @@ pub const internal = struct {
|
||||
fn isLinkLocalIPv6(this: *const ResultEntry) bool {
|
||||
if (this.info.family != std.c.AF.INET6) return false;
|
||||
const addr6: *const std.c.sockaddr.in6 = @ptrCast(@alignCast(&this.addr));
|
||||
// fe80::/10 means first 10 bits are 1111111010
|
||||
// First byte must be 0xFE, second byte high nibble must be 8, 9, A, or B (0b10xx)
|
||||
const bytes = @as(*const [16]u8, @ptrCast(&addr6.addr));
|
||||
return bytes[0] == 0xfe and (bytes[1] & 0xc0) == 0x80;
|
||||
return isLinkLocalIPv6Addr(&addr6.addr);
|
||||
}
|
||||
|
||||
/// Check if this is a global IPv6 address (not link-local, not loopback, not ULA).
|
||||
fn isGlobalIPv6(this: *const ResultEntry) bool {
|
||||
if (this.info.family != std.c.AF.INET6) return false;
|
||||
const addr6: *const std.c.sockaddr.in6 = @ptrCast(@alignCast(&this.addr));
|
||||
return isGlobalIPv6Addr(&addr6.addr);
|
||||
}
|
||||
};
|
||||
|
||||
/// Check if an IPv6 address is link-local (fe80::/10).
|
||||
fn isLinkLocalIPv6Addr(addr: *const [16]u8) bool {
|
||||
return addr[0] == 0xfe and (addr[1] & 0xc0) == 0x80;
|
||||
}
|
||||
|
||||
/// Check if an IPv6 address is a Unique Local Address (fc00::/7).
|
||||
fn isULAIPv6Addr(addr: *const [16]u8) bool {
|
||||
return (addr[0] & 0xfe) == 0xfc;
|
||||
}
|
||||
|
||||
/// Check if an IPv6 address is loopback (::1).
|
||||
fn isLoopbackIPv6Addr(addr: *const [16]u8) bool {
|
||||
const zero_part = @as(*const u128, @ptrCast(addr)).*;
|
||||
return zero_part == @as(u128, 1) << 120; // ::1 in big-endian
|
||||
}
|
||||
|
||||
/// Check if an IPv6 address is global scope (routable on the internet).
|
||||
/// Global addresses are in the 2000::/3 range (first 3 bits = 001).
|
||||
fn isGlobalIPv6Addr(addr: *const [16]u8) bool {
|
||||
// Global unicast: 2000::/3 (first byte 0x20-0x3F)
|
||||
return (addr[0] & 0xe0) == 0x20;
|
||||
}
|
||||
|
||||
/// RFC 6724 Rule 2: Check if a global-scope IPv6 source address is available.
|
||||
/// This determines whether we should prefer IPv6 destinations.
|
||||
/// Returns true if there's at least one non-link-local, non-loopback IPv6 address.
|
||||
const IPv6SourceAvailability = struct {
|
||||
var cached_result: enum { unknown, available, unavailable } = .unknown;
|
||||
var cache_timestamp: i64 = 0;
|
||||
const CACHE_TTL_MS: i64 = 30000; // 30 seconds
|
||||
|
||||
fn hasGlobalIPv6Source() bool {
|
||||
if (comptime bun.Environment.isWindows) {
|
||||
// TODO: Implement Windows support using GetAdaptersAddresses
|
||||
return true; // Assume available on Windows for now
|
||||
}
|
||||
|
||||
const now = std.time.milliTimestamp();
|
||||
if (cached_result != .unknown and (now - cache_timestamp) < CACHE_TTL_MS) {
|
||||
return cached_result == .available;
|
||||
}
|
||||
|
||||
// Check network interfaces for global IPv6 addresses
|
||||
var ifap: ?*c.ifaddrs = null;
|
||||
if (c.getifaddrs(&ifap) != 0) {
|
||||
// On error, assume IPv6 is available to avoid breaking things
|
||||
return true;
|
||||
}
|
||||
defer c.freeifaddrs(ifap);
|
||||
|
||||
var has_global_ipv6 = false;
|
||||
var ifa = ifap;
|
||||
while (ifa) |iface| : (ifa = iface.ifa_next) {
|
||||
// Skip interfaces that aren't up and running
|
||||
if (iface.ifa_flags & c.IFF_UP == 0) continue;
|
||||
if (iface.ifa_flags & c.IFF_RUNNING == 0) continue;
|
||||
if (iface.ifa_addr == null) continue;
|
||||
|
||||
// Skip loopback interfaces
|
||||
if (iface.ifa_flags & c.IFF_LOOPBACK != 0) continue;
|
||||
|
||||
// Check if this is an IPv6 address
|
||||
const sa: *std.posix.sockaddr = @ptrCast(@alignCast(iface.ifa_addr));
|
||||
if (sa.family != std.c.AF.INET6) continue;
|
||||
|
||||
const addr6: *const std.c.sockaddr.in6 = @ptrCast(@alignCast(iface.ifa_addr));
|
||||
const addr_bytes = @as(*const [16]u8, @ptrCast(&addr6.addr));
|
||||
|
||||
// Check if this is a global IPv6 address (2000::/3)
|
||||
if (isGlobalIPv6Addr(addr_bytes)) {
|
||||
has_global_ipv6 = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
cached_result = if (has_global_ipv6) .available else .unavailable;
|
||||
cache_timestamp = now;
|
||||
return has_global_ipv6;
|
||||
}
|
||||
|
||||
/// Invalidate the cache (e.g., when network changes are detected)
|
||||
pub fn invalidateCache() void {
|
||||
cached_result = .unknown;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1466,10 +1556,23 @@ pub const internal = struct {
|
||||
info_ = ai.next;
|
||||
}
|
||||
|
||||
// Sort addresses for optimal connection behavior:
|
||||
// 1. Move link-local IPv6 (fe80::/10) to the end - they can't route to global destinations
|
||||
// and cause timeouts on VPN networks. See: https://github.com/oven-sh/bun/issues/25619
|
||||
// 2. Interleave global IPv6 and IPv4 (Happy Eyeballs)
|
||||
// RFC 6724 compliant address sorting for optimal connection behavior.
|
||||
// See: https://github.com/oven-sh/bun/issues/25619
|
||||
//
|
||||
// Key insight: If no global IPv6 source address is available (only link-local),
|
||||
// then global IPv6 destinations will fail because link-local sources cannot
|
||||
// route to global destinations. This commonly happens on VPN networks.
|
||||
//
|
||||
// Sorting strategy:
|
||||
// 1. Check if a global IPv6 source is available (RFC 6724 Rule 2)
|
||||
// 2. If yes: interleave global IPv6 and IPv4 (Happy Eyeballs)
|
||||
// 3. If no: prefer IPv4 over global IPv6 (avoid scope mismatch timeouts)
|
||||
// 4. Always move link-local IPv6 destinations to the end
|
||||
|
||||
const has_global_ipv6_source = IPv6SourceAvailability.hasGlobalIPv6Source();
|
||||
|
||||
// First pass: move link-local IPv6 destinations to the end.
|
||||
// These can only reach link-local destinations, not internet hosts.
|
||||
var link_local_start: usize = count;
|
||||
for (0..link_local_start) |idx| {
|
||||
if (results[idx].isLinkLocalIPv6()) {
|
||||
@@ -1488,18 +1591,36 @@ pub const internal = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// Now interleave IPv4 and IPv6 among the non-link-local addresses
|
||||
var want: usize = std.c.AF.INET6;
|
||||
for (0..link_local_start) |idx| {
|
||||
if (results[idx].info.family == want) continue;
|
||||
for (idx + 1..link_local_start) |j| {
|
||||
if (results[j].info.family == want) {
|
||||
std.mem.swap(ResultEntry, &results[idx], &results[j]);
|
||||
want = if (want == std.c.AF.INET6) std.c.AF.INET else std.c.AF.INET6;
|
||||
// Second pass: sort non-link-local addresses based on IPv6 source availability.
|
||||
if (has_global_ipv6_source) {
|
||||
// Global IPv6 source available: interleave IPv6 and IPv4 (Happy Eyeballs)
|
||||
// Start with IPv6 as it's generally preferred when available.
|
||||
var want: usize = std.c.AF.INET6;
|
||||
for (0..link_local_start) |idx| {
|
||||
if (results[idx].info.family == want) continue;
|
||||
for (idx + 1..link_local_start) |j| {
|
||||
if (results[j].info.family == want) {
|
||||
std.mem.swap(ResultEntry, &results[idx], &results[j]);
|
||||
want = if (want == std.c.AF.INET6) std.c.AF.INET else std.c.AF.INET6;
|
||||
}
|
||||
} else {
|
||||
// the rest of the non-link-local list is all one address family
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No global IPv6 source: prefer IPv4 over global IPv6.
|
||||
// RFC 6724 Rule 2: Prefer matching scope. Since we only have link-local
|
||||
// IPv6 source, global IPv6 destinations will fail with scope mismatch.
|
||||
// Put all IPv4 addresses first, then global IPv6 (as fallback).
|
||||
var ipv4_end: usize = 0;
|
||||
for (0..link_local_start) |idx| {
|
||||
if (results[idx].info.family == std.c.AF.INET) {
|
||||
if (idx != ipv4_end) {
|
||||
std.mem.swap(ResultEntry, &results[idx], &results[ipv4_end]);
|
||||
}
|
||||
ipv4_end += 1;
|
||||
}
|
||||
} else {
|
||||
// the rest of the non-link-local list is all one address family
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3516,6 +3637,7 @@ const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const bun = @import("bun");
|
||||
const c = bun.c;
|
||||
const Async = bun.Async;
|
||||
const Environment = bun.Environment;
|
||||
const Global = bun.Global;
|
||||
|
||||
Reference in New Issue
Block a user