Skip to content

streamlocal: bring in upstream unix forwarding implementation#7

Draft
kradalby wants to merge 8 commits intomasterfrom
kradalby/unix-forwarding
Draft

streamlocal: bring in upstream unix forwarding implementation#7
kradalby wants to merge 8 commits intomasterfrom
kradalby/unix-forwarding

Conversation

@kradalby
Copy link
Member

This PR brings in gliderlabs#196 to add Unix socket forwarding support.

In addition it aims to address the open comments from that PR.
I had Claude review the code to propose other improvements and bring them in.
Modernised the code to use Go 1.26 features.

deansheather and others added 8 commits March 13, 2026 08:17
Adds optional (disabled by default) implementations of local->remote and
remote->local Unix forwarding through OpenSSH's protocol extensions:

- streamlocal-forward@openssh.com
    - cancel-streamlocal-forward@openssh.com
    - forwarded-streamlocal@openssh.com
- direct-streamlocal@openssh.com

Adds tests for Unix forwarding, reverse Unix forwarding and reverse TCP
forwarding.

Co-authored-by: Samuel Corsi-House <chouse.samuel@gmail.com>
The Reserved field in remoteUnixForwardChannelData is uint32 but the
OpenSSH PROTOCOL spec (Section 2.4) and x/crypto/ssh forwardedStreamLocalPayload
both define it as string. The current code works by coincidence because
uint32(0) and string("") produce identical wire bytes, but this is a
latent protocol bug.

Also fix server_test.go rebase artifact: newLocalListener was renamed to
newLocalTCPListener by the unix forwarding commit.

Spec: https://github.com/openssh/openssh-portable/blob/master/PROTOCOL
Ref: https://cs.opensource.google/go/x/crypto/+/master:ssh/streamlocal.go

Updates gliderlabs#196
Address review feedback from gliderlabs#196:

- Return true for duplicate forward requests instead of false. Returning
  false causes clients with ExitOnForwardFailure=yes to disconnect,
  which diverges from OpenSSH behavior. (mafredri, ge9)

  Ref: gliderlabs#196 (comment)
  Ref: https://github.com/coder/coder/blob/b828412edd913bef6665cf8a0b2ca7ac93334012/agent/agentssh/forward.go#L76-L91

- Use context-aware net.ListenConfig in SimpleUnixReverseForwardingCallback
  instead of bare net.Listen, matching the dialer pattern used in
  SimpleUnixLocalForwardingCallback. (mafredri)

  Ref: gliderlabs#196 (comment)

Updates gliderlabs#196
Several concurrency issues exist in the shared ForwardedUnixHandler:

- Key the forwards map by (sessionID, addr) instead of addr alone. A
  single handler instance is shared across SSH connections, so addr-only
  keys allow cross-session collisions and let any session cancel another
  session's forwards. This matches the approach used in coder/coder's
  production fork of this code.

- Pass the parent context (connection-scoped) to bicopy instead of the
  derived context. The derived context is cancelled when the accept loop
  exits, which prematurely tears down active forwarded connections that
  are still transferring data.

- Delete the map entry atomically in the cancel handler instead of
  relying on the accept-loop goroutine to clean up asynchronously.
  This prevents a timing window where the stale entry would reject
  legitimate re-forward requests.

Updates gliderlabs#196
- Fix doc comment for ErrRejected: the comment said "ErrReject" but the
  variable is "ErrRejected". Also enumerate which callbacks honor it.

- Use %v on error instead of %+v on err.Error() string in reject
  message. The %+v verb on a string is identical to %s; the stack
  trace behavior of %+v only works on the error value itself.

- Fix incorrect os.Stat check in TestReverseUnixForwardingWorks. The
  condition "err == nil && !os.IsNotExist(err)" has a redundant second
  clause since os.IsNotExist(nil) is always false. Simplify to
  "err == nil".

Updates gliderlabs#196
SimpleUnixLocalForwardingCallback and SimpleUnixReverseForwardingCallback
perform no validation on client-supplied socket paths. A malicious client
can request forwarding to arbitrary paths, which in the reverse case
causes directory creation (MkdirAll), file deletion (unlink), and socket
binding at the requested path.

Add prominent documentation warnings that these helpers are intended for
trusted environments and that production deployments should implement
custom callbacks with path validation and access control.

Updates gliderlabs#196
Silence golangci-lint errcheck warnings for unchecked return values
in the new streamlocal and tcpip forwarding tests.

Updates gliderlabs#196
Use maps.Copy instead of manual for-range copy loops in
ensureHandlers, as suggested by golang.org/x/tools/go/analysis/passes/modernize.
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.

2 participants