ssh
ssh copied to clipboard
feat: add Unix forwarding server implementations
Adds optional (disabled by default) implementations of local->remote and remote->local Unix forwarding through OpenSSH's protocol extensions:
Adds tests for Unix forwarding, reverse Unix forwarding and reverse TCP forwarding.
I recently had to write this code for my job so we could support GPG forwarding and couldn't find an implementation here so I thought I'd contribute it upstream so others can use it easily.
@henrybarreto, could you take a look at this to see if it makes sense to support it in @shellhub-io?
I'd love to see this PR merged in so that Tailscale can support it on their end. Since they want to start moving back to the original copy of this repo, this PR is now blocking support for stuff like GPG agent forwarding through Tailscale SSH. See https://github.com/tailscale/tailscale/pull/12081#issuecomment-2243494893.
While I was working on my PR over at Tailscale, I made a couple changes to streamlocal.go
for everything to work properly (mainly the file permissions and using the unlink function from https://github.com/coder/coder/blob/main/agent/agentssh/forward.go which I believe their license permits). I'm not sure if these changes would also benefit this repo or if it's specific to Tailscale.
@@ -128,12 +127,27 @@ func (h *ForwardedUnixHandler) HandleSSHRequest(ctx Context, srv *Server, req *g
return false, nil
}
+ // https://github.com/coder/coder/blob/main/agent/agentssh/forward.go
+ // Remove existing socket if it exists. We do not use os.Remove() here
+ // so that directories are kept. Note that it's possible that we will
+ // overwrite a regular file here. Both of these behaviors match OpenSSH,
+ // however, which is why we unlink.
+ err = unlink(addr)
+ if err != nil && !errors.Is(err, fs.ErrNotExist) {
+ // TODO: log
+ return false, nil
+ }
+
ln, err := net.Listen("unix", addr)
if err != nil {
// TODO: log unix listen failure
return false, nil
}
+ if err := os.Chmod(addr, os.FileMode(0777)); err != nil {
+ // TODO: log permission change failure
+ return false, nil
+ }
+
// The listener needs to successfully start before it can be added to
// the map, so we don't have to worry about checking for an existing
// listener as you can't listen on the same socket twice.
@@ -202,3 +216,15 @@ func (h *ForwardedUnixHandler) HandleSSHRequest(ctx Context, srv *Server, req *g
return false, nil
}
}
+
+// https://github.com/coder/coder/blob/main/agent/agentssh/forward.go
+// unlink removes files and unlike os.Remove, directories are kept.
+func unlink(path string) error {
+ // Ignore EINTR like os.Remove, see ignoringEINTR in os/file_posix.go
+ // for more details.
+ for {
+ err := syscall.Unlink(path)
+ if !errors.Is(err, syscall.EINTR) {
+ return err
+ }
+ }
+}
Before we can merge, we'll need to resolve the conflict. Could you please take a look at it?
@Xenfo I applied your patch minus the chmod thing. I don't think it's a good default to chmod to 777.
Yeah I had also brought that concern up in the Tailscale PR. I'm not really sure what could be done better here since I'm not too familiar with the library or proper permissions. Ideally they would be set so that user that SSHed is able to use it.
If there isn't a better default permission, it's essential that there's some way to set the permissions (maybe through a separate function that can be called by the library user?) for Tailscale since it runs as root. Any user not SSHing as root would not be able to access the sockets that are forwarded.
In my opinion, this responsibility should lie with the user of the ReverseUnixForwardingCallback
. The callback should receive the socket information and return a net.Listener
because that is what matters. All the filesystem business logic should be handled by the user. This way, the library remains completely agnostic about how applications handle this type of forwarding.
I'll make some changes later today.
Hey @deansheather if you need any help I'm willing to give this a go.
@gustavosbarreto is this what you're suggesting?
ReverseUnixForwardingCallback: ssh.ReverseUnixForwardingCallback(func(ctx Context, socketPath string) net.Listener {
ln, err := net.Listen("unix", addr)
if err != nil {
return nil
}
if err := os.Chmod(addr, os.FileMode(0777)); err != nil {
return nil
}
return ln
}),
If so then how would the check for allowing reverse forwarding be done? If we check if the callback returns nil
, we can only check after the unlink
call since net.Listener
can't be created before and we'd already have started modifying the system to do that.
I forgot to do it. If you want to do it, go ahead, but please keep my details in co-authored-by
I opened https://github.com/deansheather/ssh/pull/1 which implements it for remote forwarding, I'm unsure if this is also necessary for local forwarding. Lmk if the double callback looks fine.