ssh does not fall back from IPv6 to IPv4 on a refused connect
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 35/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Quiet
- Tech stack
- c
- Domain
- networking, operating-systems, testing-qa
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.
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.
- Dominant language
- No language data
- Stars
- 8.3k
- Forks
- 820
- Avg merge
- 12m
- Merged PRs (30d)
- 1
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing 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.
More from PowerShell/Win32-OpenSSH
-
Area-ssh-agent Issue-Upstream Parity
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
PowerShell/Win32-OpenSSH#2458 · 1 reaction ·
-
Area-Authentication Area-Logging/Diagnostics Area-sshd Investigate Issue-Bug
Difficulty 4/5 3-5 days Newbie friendliness 48/100
PowerShell/Win32-OpenSSH#2466 · 1 comment · 1 reaction ·
-
Area-sshd Area-Terminal Investigate Issue-Regression
Difficulty 4/5 3-5 days Newbie friendliness 48/100
PowerShell/Win32-OpenSSH#2465 · 1 comment · 1 reaction ·
-
Area-ssh-agent Issue-Enhancement
Difficulty 5/5 Over a week Newbie friendliness 25/100
PowerShell/Win32-OpenSSH#2462 · 1 comment · 1 reaction ·
-
Area-ssh-agent Investigate
Difficulty 5/5 Over a week Newbie friendliness 25/100
PowerShell/Win32-OpenSSH#2460 · 1 reaction ·
All issues in PowerShell/Win32-OpenSSH
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
-
Difficulty 1/5 Under an hour Newbie friendliness 85/100
grpc/grpc-go#9481 · 1 comment ·
Maintainers usually reply within 2 days
-
bug severity:low
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
-
Type: Bug
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Maintainers usually reply within 1 day
-
agent:WSL LOW networking refactoring
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day