Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Unix forwarding server implementations #196

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

deansheather
Copy link

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.

@gustavosbarreto
Copy link
Collaborator

@henrybarreto, could you take a look at this to see if it makes sense to support it in @shellhub-io?

@samchouse
Copy link

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 tailscale/tailscale#12081 (comment).

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
+               }
+       }
+}

@gustavosbarreto
Copy link
Collaborator

Before we can merge, we'll need to resolve the conflict. Could you please take a look at it?

@deansheather
Copy link
Author

@Xenfo I applied your patch minus the chmod thing. I don't think it's a good default to chmod to 777.

@samchouse
Copy link

samchouse commented Jul 23, 2024

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.

@gustavosbarreto
Copy link
Collaborator

gustavosbarreto commented Jul 23, 2024

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.

@deansheather
Copy link
Author

I'll make some changes later today.

@samchouse
Copy link

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.

@deansheather
Copy link
Author

I forgot to do it. If you want to do it, go ahead, but please keep my details in co-authored-by

@samchouse
Copy link

I opened deansheather#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.

Adds optional (disabled by default) implementations of local->remote and
remote->local Unix forwarding through OpenSSH's protocol extensions:

- [email protected]
    - [email protected]
    - [email protected]
- [email protected]

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

Co-authored-by: Samuel Corsi-House <[email protected]>
@deansheather
Copy link
Author

Thanks for your changes @samchouse

@gustavosbarreto the reverse forwarding callback now looks like:

// ReverseUnixForwardingCallback is a hook for allowing reverse unix forwarding
// ([email protected]). Returning ErrRejected will reject the
// request.
type ReverseUnixForwardingCallback func(ctx Context, socketPath string) (net.Listener, error)

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