anthropics / anthropics/claude-code
[BUG] SSH connection fails with "no route to host" (EHOSTUNREACH) despite identical ssh command working outside the app
- Ngôn ngữ chính
- Python
- Star
- 145k
- Fork
- 23.1k
- Chỉ số merge pull request
- Chỉ số pull request đang chờ
Mô tả
## Bug Summary
Adding an SSH connection in Claude Code Desktop to a specific LAN host fails with **"SSH host unreachable (no route to host)"**, even though the exact same `ssh` command (same binary, same host, same port, same key) succeeds reliably every time when run manually in Terminal on the same machine, at effectively the same moment.
Captured via a live `fs_usage` syscall trace: the app's own spawned `ssh` subprocess performs a real DNS resolution (succeeds, with valid answer data from mDNSResponder), then issues a real `connect()` syscall that fails in ~150 microseconds with errno 65 (`EHOSTUNREACH`) — a genuine synchronous kernel-level rejection, not a timeout, and not accompanied by any Sandbox/TCC "deny" log entry.
## Environment
- Claude Code Desktop (macOS), versions seen under `~/Library/Application Support/Claude/claude-code`: 2.1.258 / 2.1.260
- macOS Darwin 25.6.0
- Target: an SSH server on a LAN host (`myhost-a.example.com` → private IP), resolved via a local DNS override (not public DNS) — same resolution mechanism used successfully by a second, working SSH connection in the same app instance
- SSH client actually invoked by the app (per syscall trace): Homebrew-installed OpenSSH at `/opt/homebrew/Cellar/openssh/10.5p1/bin/ssh`
## Steps to Reproduce
1. Add an SSH connection in Claude Code Desktop pointing to a LAN host (hostname or raw IP), correct port, correct key.
2. Click "Test connection" (or trigger any action that opens a connection, e.g. sending a message).
3. Observe: `SSH host unreachable (no route to host)`
4. From a Terminal on the same Mac, at the same time: `ssh -p user@host` connects successfully every time, using the identical OpenSSH binary the app itself invokes.
## What we ruled out (all directly tested)
- **Wrong port** — confirmed correct port in the UI field; also tested against a second port mapped on the server (both fail identically).
- **DNS resolution** — confirmed correct resolution via the same resolver path in Terminal, and via syscall trace showing the app's own `ssh` subprocess completing a real mDNSResponder round-trip with valid answer payloads.
- **ARP cache staleness** — confirmed ARP entry present/warm at time of failure; still fails.
- **Raw IP instead of hostname** — same failure, ruling out hostname-specific resolution issues.
- **macOS "Local Network" privacy permission** — confirmed already granted in System Settings; also tried toggling off/on + relaunch; no change.
- **Third-party firewall / MDM** — none installed (no Little Snitch/LuLu/similar found; `profiles status` confirms not MDM-enrolled).
- **App Sandbox / TCC network-outbound denial** — checked unified system log (`log show`) around failure timestamps; zero Sandbox or TCC deny events logged for the process.
- **Routing table conflicts** (e.g. with Tailscale) — routing table shows a single unambiguous route to the target subnet via the physical LAN interface; no competing/overlapping route via any VPN/tunnel interface.
- **SSH client binary differences** — both Apple's stock `/usr/bin/ssh` and the Homebrew-installed OpenSSH connect successfully when run manually; trace confirms the app invokes the Homebrew binary directly, same as Terminal.
- **`AllowTcpForwarding` (tcp_forwarding) server setting** — enabled; no change (also not expected to matter, since this fails before SSH protocol negotiation even begins).
- **Auth method (key vs. password)** — not relevant; failure occurs before authentication is ever negotiated (this is a pre-TCP-handshake kernel error).
- **App-level caching** — fully quit/relaunched the app, and deleted/recreated the connection entry from scratch; same failure.
- **Stale permission/policy cache** — full system reboot; same failure persists.
## Key evidence: syscall trace
Captured via `sudo fs_usage -w -f network,exec` while triggering the connection from the app. Relevant excerpt from the spawned `ssh` subprocess (paths/hostnames redacted):
```
socket F=4
connect F=4 /private/var/run/mDNSResponder
sendto/recvfrom ... # real DNS query/response round trip (~6.6ms), valid answer payloads
open /Library/Preferences/com.apple.networkd.plist # read twice, immediately before connect()
socket F=5
connect F=5 [ 65] # fails in ~150 microseconds; 65 = EHOSTUNREACH
close F=5
write F=2 B=0x46 # ~70 bytes to stderr: "ssh: connect to host port : No route to host"
exit [255]
```
DNS resolution completes successfully, followed by a synchronous kernel-level rejection of `connect()` in well under a millisecond — far too fast to be a timeout — with no Sandbox/TCC log activity at all. The same destination is reachable via the same physical network path, from the same machine, via a manually-run `ssh` process, at effectively the same moment.
## App's own log output (`~/Library/Logs/Claude/ssh.log` / `main.log`)
```
[info] LocalSessions.testSSHConnection: host=@ (probe)
[info] [OpenSSHConnection] starting master for @
[info] [OpenSSHConnection] master stderr: ssh: connect to host port : No route to host
[warn] [OpenSSHConnection] master for @ exited before ready; stderr tail: ssh: connect to host port : No route to host
[error] [RemoteServerController] Connection failed (262ms, trigger: send_message, attempt 1): SSH host unreachable (no route to host)
```
Note: the specific error text is not always identical between attempts — some failures report `Network is unreachable` (`ENETUNREACH`) instead of `No route to host` (`EHOSTUNREACH`), notably around sleep/wake events, suggesting some non-determinism in the underlying condition.
## Expected behavior
The SSH connection should succeed, matching the manually-run `ssh` command with identical parameters from the same machine.
## Additional notes
- A separate, working SSH connection to a different LAN host (`myhost-b.example.com`, resolved via the identical private-DNS mechanism) succeeds without any special configuration in the same app instance — this is specific to something about this particular destination/connection, not a global SSH feature failure. We were unable to identify what differs, since every checkable network-layer condition (routing, ARP, DNS, permissions) is identical between the working and failing hosts.
- We suspect this may relate to macOS's per-process network policy routing (NECP — Network Extension Control Policy), given `com.apple.networkd.plist` is read immediately before the failing `connect()`, but couldn't confirm or resolve this without deeper kernel tooling (`dtrace`/`dtruss` against system binaries requires disabling SIP, which we did not want to do).
- Possibly related to #26809 (SSH Port directive from `~/.ssh/config` ignored), though that issue was about the Port field being ignored — we confirmed the Port field itself is correctly used in this case; the failure happens at a lower network layer than port selection.
Hướng dẫn đóng góp
Chưa lập chỉ mục được hướng dẫn đóng góp cho kho mã nguồn này
Hướng nghiên cứu
Start with LocalSessions.testSSHConnection and the OpenSSHConnection logs at ~/Library/Logs/Claude/ssh.log and main.log, then reproduce the failure while comparing the app-spawned ssh process with the manual command. Use the reported fs_usage trace and macOS network behavior as the initial evidence; done means the app connects to the affected LAN host reliably with the existing parameters.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Đánh giá
- Công nghệ
- macos
- Lĩnh vực
- desktop-dev, networking
- Loại issue
- Lỗi
- Độ khó
- 5/5
- Thời gian dự kiến
- Hơn một tuần
- Mức độ hoạt động
- Sôi nổi
- Độ rõ ràng
- Cần làm rõ
- Mức phù hợp với người mới
- 30/100