PowerShell / PowerShell/Win32-OpenSSH

ssh does not fall back from IPv6 to IPv4 on a refused connect

Open
#2,441 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Area-ssh Issue-Upstream Parity
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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.