Skip to content

cmd/tailcat: use a short deterministic host for ssh's ControlPath - #15

Merged
bradfitz merged 1 commit into
tailscale:mainfrom
korjavin:fix/ssh-controlpath-too-long-12
Aug 28, 2026
Merged

cmd/tailcat: use a short deterministic host for ssh's ControlPath#15
bradfitz merged 1 commit into
tailscale:mainfrom
korjavin:fix/ssh-controlpath-too-long-12

Conversation

@korjavin

Copy link
Copy Markdown
Contributor

Summary

Fixes #12. tailcat ssh passed the raw ConnBlob (the user@<addrblob> argument — potentially 100+ bytes once a full DERP Region is embedded rather than just a RegionID, see ConnInfo in tailcat.go) straight through as the destination hostname to the system ssh client. ssh substitutes that literal hostname into %n for ControlPath (commonly ~/.ssh/master-%r@%n:%p), and the expansion has to fit an AF_UNIX socket path — about 104-108 bytes total, including the home directory. A long ConnBlob blows that budget on its own, so ControlMaster/ControlPath multiplexing failed with unix_listener: path "..." too long for Unix domain socket in an otherwise completely ordinary ssh config, before tailcat was even invoked — exactly the report in #12.

Fix

sshDestHost() gives ssh a short, deterministic pseudo-hostname (an 8-byte SHA-256 prefix of the blob, tailcat-xxxxxxxxxxxxxxxx, 24 bytes fixed width) instead of the raw blob. The real blob is untouched: it still flows into ProxyCommand exactly as before, which is what actually does the connection routing — nothing about how tailcat proxies the connection changes, only the label ssh sees.

Deterministic hashing (not truncation) matters here: ssh's connection sharing keys a control socket off ControlPath, so the same blob has to keep producing the same short host across invocations for multiplexing to correctly reuse — and not collide across — the right server's socket.

Any user@ prefix on the original argument is preserved and now applied to the short host instead of the raw blob, so -l/%r semantics for a real system sshd (via tailcat --serve=22) are unaffected.

Tests

TestSSHDestHost (new) covers:

  • deterministic output for the same blob across calls;
  • no collision between two distinct blobs;
  • fixed-width output regardless of input blob length;
  • a realistic ControlPath built from the short host stays comfortably under the ~104-108 byte AF_UNIX limit.
go build ./...
go build -tags ts_omit_ssh ./...
go vet ./...
go test ./...

All pass (default and ts_omit_ssh build tags).

Also manually verified the hash output shape/length against the reporter's math (a real ConnBlob fixture already used elsewhere in this package's tests is 58 bytes; the fixed 24-byte replacement keeps a typical ControlPath well under the limit regardless of how long the underlying blob is, which is the actual point — a blob-length-dependent truncation wouldn't have that guarantee).

Fixes #12. "tailcat ssh" passed the raw ConnBlob (the "user@<addrblob>"
argument, potentially 100+ bytes once a full DERP Region is embedded
rather than just a RegionID) straight through as the ssh destination
hostname. ssh substitutes that literal hostname into %n for
ControlPath (commonly "~/.ssh/master-%r@%n:%p"), and the expansion has
to fit an AF_UNIX socket path -- about 104-108 bytes total, including
the home directory. A long ConnBlob blows that budget on its own, so
ControlMaster/ControlPath multiplexing failed with "too long for Unix
domain socket" in an otherwise ordinary ssh config, before tailcat
itself was even invoked.

Give ssh a short, deterministic pseudo-hostname (sshDestHost: an
8-byte SHA-256 prefix of the blob, "tailcat-xxxxxxxxxxxxxxxx", 24
bytes fixed width) instead of the blob itself, while keeping the real
blob flowing into ProxyCommand exactly as before -- that's still what
does the actual connection routing, so nothing about how tailcat
proxies the connection changes. Deterministic hashing (not
truncation) matters here: ssh's connection sharing keys a control
socket off ControlPath, so the same blob has to keep producing the
same short host across invocations for multiplexing to correctly
reuse (and not collide across) the right server's socket.

Any "user@" prefix on the original argument is preserved and now
applied to the short host instead of the raw blob, so -l/%r semantics
for a real system sshd (via "tailcat --serve=22") are unaffected.

TestSSHDestHost covers: deterministic output for the same blob, no
collision between distinct blobs, fixed-width output, and that a
realistic ControlPath built from the short host stays comfortably
under the ~104-108 byte AF_UNIX limit.

go build/vet/test ./... (default and -tags ts_omit_ssh) all pass.
@bradfitz
bradfitz merged commit 5384598 into tailscale:main Aug 28, 2026
2 checks passed
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.

Can't use ssh with connection multiplexing in a common configuration

2 participants