Skip to content

streamlocal: bring in upstream unix forwarding implementation#7

Merged
kradalby merged 8 commits intomasterfrom
kradalby/unix-forwarding
Mar 16, 2026
Merged

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

Conversation

@kradalby
Copy link
Copy Markdown
Member

@kradalby kradalby commented Mar 13, 2026

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. In addition, we added a lot more tests.
Modernised the code to use Go 1.26 features.

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>
Replace SimpleUnixLocalForwardingCallback and SimpleUnixReverseForwardingCallback
with configurable, secure-by-default alternatives:

- NewLocalUnixForwardingCallback(opts) validates socket paths before dialing
- NewReverseUnixForwardingCallback(opts) validates paths, applies restrictive
  socket permissions (default mode 0600 matching OpenSSH), and does not
  create parent directories

Add UnixForwardingOptions with:
- AllowAll: permits any absolute path (for ACL-gated deployments)
- AllowedDirectories/DeniedPrefixes: directory-level allow/deny lists
- BindUnlink: opt-in socket replacement (only unlinks actual sockets)
- BindMask: configurable umask (default 0177, matching OpenSSH)
- PathValidator: hook for custom per-request validation

Add UserSocketDirectories(homeDir, uid) helper returning common socket
directories (/tmp, ~, /run/user/<uid>) for easy integration.

Surface descriptive error messages to SSH clients: path validation errors
are sent as 'administratively prohibited' with the specific reason instead
of a generic 'unix forwarding is disabled'.

Updates gliderlabs#196
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
Silence golangci-lint errcheck warnings for unchecked return values
in the new streamlocal and tcpip forwarding tests.

Updates gliderlabs#196
@kradalby kradalby force-pushed the kradalby/unix-forwarding branch from 734a443 to f6f7556 Compare March 16, 2026 15:08
@kradalby kradalby marked this pull request as ready for review March 16, 2026 15:44
Use maps.Copy instead of manual for-range copy loops in
ensureHandlers, as suggested by golang.org/x/tools/go/analysis/passes/modernize.
@kradalby kradalby force-pushed the kradalby/unix-forwarding branch from f6f7556 to 56325c3 Compare March 16, 2026 15:47
@kradalby kradalby merged commit b4ce3bd into master Mar 16, 2026
4 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.

3 participants