From: thinca Date: Wed, 18 Feb 2026 21:34:57 +0000 (+0000) Subject: patch 9.2.0021: channel: connection timeout fails to fall back to IPv4 X-Git-Tag: v9.2.0021^0 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=dd989ec9ca3654584e2813f26a322f9de18406bf;p=thirdparty%2Fvim.git patch 9.2.0021: channel: connection timeout fails to fall back to IPv4 Problem: When ch_open() tries to connect to a hostname that resolves to multiple addresses (e.g., both IPv6 and IPv4), it uses a single waittime for all connection attempts. If the first IPv6 connection attempt times out, it consumes almost all of the waittime, leaving insufficient time (often just 1ms) for the IPv4 attempt to succeed. (reporter) Solution: Implement a simplified version of Happy Eyeballs (RFC 8305) to improve connection fallback behavior when IPv6 is unavailable or slow (thinca). Distribute the waittime across multiple addresses: - First address: use up to 250ms (RFC 8305 Connection Attempt Delay) or half of the total waittime, whichever is smaller - Middle addresses: divide remaining time equally - Last address: use all remaining time This ensures that IPv4 fallback has sufficient time to succeed even when IPv6 connection attempts fail or timeout. closes: #19233 Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: thinca Signed-off-by: Christian Brabandt --- diff --git a/runtime/doc/channel.txt b/runtime/doc/channel.txt index e2259823b7..5c1de248d6 100644 --- a/runtime/doc/channel.txt +++ b/runtime/doc/channel.txt @@ -1,4 +1,4 @@ -*channel.txt* For Vim version 9.2. Last change: 2026 Feb 14 +*channel.txt* For Vim version 9.2. Last change: 2026 Feb 18 VIM REFERENCE MANUAL by Bram Moolenaar @@ -130,6 +130,11 @@ a Unix-domain socket path prefixed by "unix:". E.g. > [2001:db8::1]:8765 " IPv6 + port unix:/tmp/my-socket " Unix-domain socket path +When a domain name resolves to multiple addresses (e.g., both IPv6 and IPv4), +Vim tries each address in order. If a connection is slow or unreachable, it +quickly falls back to the next address. This helps when IPv6 or IPv4 is +unreachable on the network. + {options} is a dictionary with optional entries: *channel-open-options* "mode" can be: *channel-mode* diff --git a/src/channel.c b/src/channel.c index 5c7fb07bd8..a1c9bb7a91 100644 --- a/src/channel.c +++ b/src/channel.c @@ -935,13 +935,31 @@ channel_open( return NULL; } + // Count the number of addresses for timeout distribution + int addr_count = 0; for (addr = res; addr != NULL; addr = addr->ai_next) + addr_count++; + + // On Mac and Solaris a zero timeout almost never works. Waiting for + // one millisecond already helps a lot. Later Mac systems (using IPv6) + // need more time, 15 milliseconds appears to work well. + // Let's do it for all systems, because we don't know why this is + // needed. + if (waittime == 0) + waittime = 15; + + int addr_index = 0; + for (addr = res; addr != NULL; addr = addr->ai_next, addr_index++) { const char *dst = hostname; # ifdef HAVE_INET_NTOP const void *src = NULL; char buf[NUMBUFLEN]; # endif + int try_waittime; + int before_waittime; + int consumed; + int remaining_addrs; if (addr->ai_family == AF_INET6) { @@ -974,18 +992,44 @@ channel_open( ch_log(channel, "Trying to connect to %s port %d", dst, port); - // On Mac and Solaris a zero timeout almost never works. Waiting for - // one millisecond already helps a lot. Later Mac systems (using IPv6) - // need more time, 15 milliseconds appears to work well. - // Let's do it for all systems, because we don't know why this is - // needed. - if (waittime == 0) - waittime = 15; + // Distribute the timeout across addresses for better fallback behavior. + // This implements a simplified version of Happy Eyeballs (RFC 8305). + if (addr->ai_next == NULL) + try_waittime = waittime; + else if (addr_index == 0) + { + if (waittime > 500) + try_waittime = 250; + else if (waittime > 30) + try_waittime = waittime / 2; + else + try_waittime = waittime; + } + else + { + remaining_addrs = addr_count - addr_index; + try_waittime = waittime / remaining_addrs; + } + before_waittime = try_waittime; sd = channel_connect(channel, addr->ai_addr, (int)addr->ai_addrlen, - &waittime); + &try_waittime); + + // Update the overall waittime based on consumed time + consumed = before_waittime - try_waittime; + waittime -= consumed; + if (waittime < 0) + waittime = 0; + if (sd >= 0) break; + + // If we have no time left, stop trying + if (waittime <= 0 && addr->ai_next != NULL) + { + ch_log(channel, "Out of time, stopping connection attempts"); + break; + } } freeaddrinfo(res); diff --git a/src/version.c b/src/version.c index 394a536da0..53c825fd0e 100644 --- a/src/version.c +++ b/src/version.c @@ -734,6 +734,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 21, /**/ 20, /**/