Skip to content

fix(cockpit): TCP fallback for SSH hosts without unix-socket forwarding (Tailscale SSH) - #260

Open
SeniorHan wants to merge 3 commits into
jacobaraujo7:mainfrom
SeniorHan:fix/tailscale-ssh-tcp-fallback
Open

SeniorHan wants to merge 3 commits into
jacobaraujo7:mainfrom
SeniorHan:fix/tailscale-ssh-tcp-fallback

Conversation

@SeniorHan

Copy link
Copy Markdown
Contributor

Closes #259.

Problem

On hosts served by Tailscale SSH, opening a remote workspace always fails with "Failed to install the server" — the server installs and boots fine, but the client's ssh -N -L <local unix socket>:<remote unix socket> tunnel can never carry the handshake: tailscaled's SSH server does not implement the direct-streamlocal@openssh.com channel type (channel 1: open failed: unknown channel type), so every connect attempt gets EOF and the client silently retries until the server idles out.

Regular TCP forwarding (ssh -L 127.0.0.1:x:127.0.0.1:port) works fine through Tailscale SSH — verified.

Fix

Reuses the existing Windows TCP+token transport as a fallback on POSIX:

  • cockpit_server.dart — new --tcp flag: bind loopback TCP + token instead of a unix socket (rendezvous JSON written at the --socket path). Unix-socket mode stays the default; an old binary on the host simply ignores the flag.
  • posix_host_shell.dart — startServer always passes --tcp; readEndpoint decides the transport in one remote command (test -S → unix endpoint, else parse the rendezvous JSON → TcpEndpoint(port, token), mirroring windows_host_shell.dart); awaitEndpoint polls readEndpoint and tolerates a half-written file.
  • ssh_tunnel.dart — the local forward end now matches the remote endpoint type: a TCP rendezvous forwards as 127.0.0.1:<localPort>:127.0.0.1:<remotePort> on any platform, while unix remotes keep today's behavior.
  • remote_host_connector.dart — _connectWithFallback: after a failed connect, re-read the announcement and retry once with the other transport; final failure now carries the boot-log tail instead of a detail-less throw.
  • Bonus hardening: _installAndStartServer computes the binary digest at bootstrap when the server is installed but unreachable, so a host frozen on an old binary (too old for new start arguments) gets silently replaced instead of failing forever.

Verification

  • dart analyze on the touched packages: no new findings vs. baseline.
  • cockpit_protocol tests pass (one pre-existing failure, unchanged on clean baseline); the new POSIX endpoint-parsing logic was exercised end-to-end against a real host: rendezvous → token'd handshake over a loopback TCP ssh forward → hello.ack.
  • Tested against a real Tailscale SSH host (tailscaled on Ubuntu 24.04): the previous build fails reproducibly, this build connects.
  • New unit tests in cockpit/test/data/host_shell_test.dart cover the rendezvous parsing, unix-socket preference, and the --tcp start arguments.

hankangshuai added 3 commits October 21, 2026 11:10
…(Tailscale SSH)

O Tailscale SSH recusa o canal direct-streamlocal@openssh.com ('unknown
channel type'), então o túnel UDS→UDS nunca abre e a abertura falhava
com serverInstallFailed mesmo com o servidor instalado e de pé.

- cockpit-server: flag --tcp força o transporte TCP de loopback + token
  (rendezvous JSON no caminho do --socket); default segue socket UNIX.
- PosixHostShell: startServer sempre passa --tcp; readEndpoint decide o
  transporte num único comando (test -S → socket UNIX; senão cat do
  rendezvous → TcpEndpoint com porta+token).
- SshTunnel: a ponta local segue o transporte da ponta remota — remoto
  TCP vira -L 127.0.0.1:<porta>:127.0.0.1:<porta>, que qualquer servidor
  OpenSSH aceita.
- RemoteHostConnector: se ninguém atender pelo anúncio lido, tenta a
  outra ponta anunciada antes de falhar; falha final carrega o boot log.
- Testes do dialeto POSIX cobrem rendezvous, JSON pela metade e --tcp.
… answers

Old binaries that reject new start arguments (--tcp) can never be reached,
so the post-connection freshness probe never ran and the host stayed frozen
on the old install. Compute the digest at bootstrap when the marker is not
set yet: one extra SSH round-trip only when a server is installed and not
already known stale.

@jacobaraujo7 jacobaraujo7 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tracking this down. The diagnosis is right (Tailscale SSH has no direct-streamlocal channel) and reusing the Windows TCP + token transport is the right tool. I would like to merge it, but it needs one more pass first.

Blocking

  1. The PR's own new test fails: PosixHostShell readEndpoint devolve socket UNIX quando o anúncio é um socket expects test -S in the command, while the code sends if [ -S ... ]. Please run flutter test test/data/ against the final code (on main it is green).
  2. Scope. Commit 54ef432 touches pi-extension/package.json (deps moved to peerDependencies: "*") and adds a 4,758-line package-lock.json to a pnpm project, and the branch also reorders the generated plugin registrants for macOS, Linux and Windows. None of that belongs here; please drop it. The SKILL.md description quoting is a valid fix on its own, so send it as a separate PR.
  3. Default transport. startServer now passes --tcp unconditionally, so every POSIX host moves from the unix socket to loopback TCP to cover one SSH server, and readEndpoint on POSIX gains an extra SSH round trip on every open (plan 58 avoided that on purpose). I would rather keep UDS as the default and fall back to --tcp only when the unix forward fails (the unknown channel type error is a clean signal), or at least make the TCP start opt-in per host.

Smaller

  • LocalEndpoint.debugForceTcp is a debug knob being used as a production switch. Give it a real name and path in cockpit_protocol.

Keep the rest: --tcp being ignored by old binaries, the stale-server digest at bootstrap, and the boot-log tail on the final failure are all good.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Remote workspace fails with hosts behind Tailscale SSH (unix socket forwarding unsupported)

2 participants