PowerShell / PowerShell/Win32-OpenSSH
ssh does not fall back from IPv6 to IPv4 on a refused connect
Nobody has claimed this yet.
- Dominant language
- No language data
- Stars
- 8.3k
- Forks
- 819
- PR merge metrics
- No merged PRs in 30d
Description
Summary
On Windows, ssh to a host that resolves to more than one address does not try the remaining addresses when the first connect() is refused. For a dual-stack host whose service answers only on IPv4, ssh tries the AAAA (IPv6) address, the connect is refused, and instead of falling back to the A (IPv4) address it aborts with:
banner exchange: Connection to UNKNOWN port -1: Connection refused
The identical ssh invocation — same config, same host — works on Linux/WSL, where it falls back to IPv4.
Version
OpenSSH_for_Windows_10.0p2 (Win32-OpenSSH); reproduced against current latestw_all. Affects earlier releases too.
Repro
Any host that resolves to ≥2 addresses where the first-tried address refuses the port; easiest is a dual-stack host (A + AAAA) whose sshd / port-forward listens on IPv4 only:
ssh -vvv user@dual-stack-host
Windows (fails — stops at the IPv6 address, never tries IPv4):
debug1: Connecting to host [2a01:db8::1] port 64724.
debug1: Connection established. <-- the connect was actually refused
debug3: socketio_getpeername - ERROR:10057
debug1: getpeername failed: The socket is not connected
debug1: kex_exchange_identification: write: Connection refused
banner exchange: Connection to UNKNOWN port -1: Connection refused
Linux/WSL (works — detects the refusal and falls back):
debug1: Connecting to host [2a01:db8::1] port 64724.
debug1: connect to address 2a01:db8::1 port 64724: Connection refused
debug1: Connecting to host [198.51.100.10] port 64724.
debug1: Connection established.
Workaround: ssh -4 host (or AddressFamily inet).
Cause
ssh_connect_direct() (sshconnect.c) iterates the getaddrinfo() results and uses timeout_connect() (misc.c): non-blocking connect() → poll(POLLOUT) → getsockopt(SO_ERROR); a non-zero SO_ERROR means "this address failed, try the next one".
On Windows the asynchronous ConnectEx() failure is delivered through the overlapped completion and captured by socketio_finish_connect() / socketio_is_io_available() into the w32_io write_details.error / read_details.error. Once that completion is consumed, the underlying socket's SO_ERROR is left at 0. But socketio_getsockopt() (contrib/win32/win32compat/socketio.c) forwards SO_ERROR straight to the Winsock getsockopt(), which therefore returns 0 — so timeout_connect() sees optval == 0, treats the refused connect as a success, and the address loop stops at the first (failed) address. getpeername() on the unconnected socket then yields UNKNOWN / -1, and the first banner write returns the original WSAECONNREFUSED (10061).
The same getsockopt(SO_ERROR) idiom is used for forwarded-channel connects in channels.c, which is affected identically.
Fix
Surface the captured async-connect error for SO_ERROR in socketio_getsockopt() (return the stored write_details.error / read_details.error, mapped through errno_from_WSAError()), so the standard non-blocking connect() + getsockopt(SO_ERROR) idiom — and thus the multi-address (IPv6 → IPv4) fallback — behaves as it does on POSIX.
Implemented, built, and verified on Windows x64: the failing repro above now detects the refused IPv6 connect and falls back to IPv4, matching Linux. Includes a win32compat regression test. PR to follow.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with contrib/win32/win32compat/socketio.c, especially socketio_getsockopt(), and trace its interaction with socketio_finish_connect(), socketio_is_io_available(), and timeout_connect() in misc.c. Run the mentioned win32compat regression test and verify that a refused first address is reported as failed so ssh_connect_direct() and forwarded-channel connects try the next address.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- c
- Domain
- networking, operating-systems, testing-qa
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100