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

ssh/tailssh: add support for forwarding unix sockets #12081

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
42 changes: 38 additions & 4 deletions ssh/tailssh/tailssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,8 @@ func (srv *server) newConn() (*conn, error) {
c := &conn{srv: srv}
now := srv.now()
c.connID = fmt.Sprintf("ssh-conn-%s-%02x", now.UTC().Format("20060102T150405"), randBytes(5))
fwdHandler := &ssh.ForwardedTCPHandler{}
fwdHandlerTCP := &ssh.ForwardedTCPHandler{}
fwdHandlerUnix := &ssh.ForwardedUnixHandler{}
c.Server = &ssh.Server{
Version: "Tailscale",
ServerConfigCallback: c.ServerConfig,
Expand All @@ -452,18 +453,23 @@ func (srv *server) newConn() (*conn, error) {
Handler: c.handleSessionPostSSHAuth,
LocalPortForwardingCallback: c.mayForwardLocalPortTo,
ReversePortForwardingCallback: c.mayReversePortForwardTo,
LocalUnixForwardingCallback: c.mayForwardLocalUnixTo,
ReverseUnixForwardingCallback: c.mayReverseUnixForwardTo,
SubsystemHandlers: map[string]ssh.SubsystemHandler{
"sftp": c.handleSessionPostSSHAuth,
},
// Note: the direct-tcpip channel handler and LocalPortForwardingCallback
// only adds support for forwarding ports from the local machine.
// TODO(maisem/bradfitz): add remote port forwarding support.
ChannelHandlers: map[string]ssh.ChannelHandler{
"direct-tcpip": ssh.DirectTCPIPHandler,
"direct-tcpip": ssh.DirectTCPIPHandler,
"direct-streamlocal@openssh.com": ssh.DirectStreamLocalHandler,
},
RequestHandlers: map[string]ssh.RequestHandler{
"tcpip-forward": fwdHandler.HandleSSHRequest,
"cancel-tcpip-forward": fwdHandler.HandleSSHRequest,
"tcpip-forward": fwdHandlerTCP.HandleSSHRequest,
"cancel-tcpip-forward": fwdHandlerTCP.HandleSSHRequest,
"streamlocal-forward@openssh.com": fwdHandlerUnix.HandleSSHRequest,
"cancel-streamlocal-forward@openssh.com": fwdHandlerUnix.HandleSSHRequest,
},
}
ss := c.Server
Expand Down Expand Up @@ -514,6 +520,32 @@ func (c *conn) mayForwardLocalPortTo(ctx ssh.Context, destinationHost string, de
return false
}

// mayReverseUnixForwardTo reports whether the ctx should be allowed to unix forward
// to the specified host.
func (c *conn) mayReverseUnixForwardTo(ctx ssh.Context, socketPath string) bool {
if sshDisableForwarding() {
return false
}
if c.finalAction != nil && c.finalAction.AllowRemoteUnixForwarding {
metricRemoteUnixForward.Add(1)
return true
}
return false
}

// mayForwardLocalUnixTo reports whether the ctx should be allowed to unix forward
// to the specified host.
func (c *conn) mayForwardLocalUnixTo(ctx ssh.Context, socketPath string) bool {
if sshDisableForwarding() {
return false
}
if c.finalAction != nil && c.finalAction.AllowLocalUnixForwarding {
metricLocalUnixForward.Add(1)
return true
}
return false
}

// havePubKeyPolicy reports whether any policy rule may provide access by means
// of a ssh.PublicKey.
func (c *conn) havePubKeyPolicy() bool {
Expand Down Expand Up @@ -1928,6 +1960,8 @@ var (
metricSFTP = clientmetric.NewCounter("ssh_sftp_sessions")
metricLocalPortForward = clientmetric.NewCounter("ssh_local_port_forward_requests")
metricRemotePortForward = clientmetric.NewCounter("ssh_remote_port_forward_requests")
metricLocalUnixForward = clientmetric.NewCounter("ssh_local_unix_forward_requests")
metricRemoteUnixForward = clientmetric.NewCounter("ssh_remote_unix_forward_requests")
)

// userVisibleError is a wrapper around an error that implements
Expand Down
11 changes: 10 additions & 1 deletion tailcfg/tailcfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ type CapabilityVersion int
// - 98: 2024-06-13: iOS/tvOS clients may provide serial number as part of posture information
// - 99: 2024-06-14: Client understands NodeAttrDisableLocalDNSOverrideViaNRPT
// - 100: 2024-06-18: Client supports filtertype.Match.SrcCaps (issue #12542)
const CurrentCapabilityVersion CapabilityVersion = 100
// - 101: 2024-06-23: Client understands SSHAction.AllowLocalUnixForwarding and SSHAction.AllowRemoteUnixForwarding.
const CurrentCapabilityVersion CapabilityVersion = 101

type StableID string

Expand Down Expand Up @@ -2502,6 +2503,14 @@ type SSHAction struct {
// to use remote port forwarding if requested.
AllowRemotePortForwarding bool `json:"allowRemotePortForwarding,omitempty"`

// AllowLocalUnixForwarding, if true, allows accepted connections
// to use local unix forwarding if requested.
AllowLocalUnixForwarding bool `json:"allowLocalUnixForwarding,omitempty"`

// AllowRemoteUnixForwarding, if true, allows accepted connections
// to use remote unix forwarding if requested.
AllowRemoteUnixForwarding bool `json:"allowRemoteUnixForwarding,omitempty"`

Comment on lines +2506 to +2513
Copy link
Author

@Xenfo Xenfo May 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how to actually change these values on the client, could it be part of another project? Currently they seem to default to false.

Edit: Looking at Headscale's source code I think it's part of the proprietary code that Tailscale uses.

// Recorders defines the destinations of the SSH session recorders.
// The recording will be uploaded to http://addr:port/record.
Recorders []netip.AddrPort `json:"recorders,omitempty"`
Expand Down
2 changes: 2 additions & 0 deletions tailcfg/tailcfg_clone.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions tailcfg/tailcfg_view.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion tempfork/gliderlabs/ssh/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func TestPasswordAuth(t *testing.T) {

func TestPasswordAuthBadPass(t *testing.T) {
t.Parallel()
l := newLocalListener()
l := newLocalTCPListener()
srv := &Server{Handler: func(s Session) {}}
srv.SetOption(PasswordAuth(func(ctx Context, password string) bool {
return false
Expand Down
2 changes: 2 additions & 0 deletions tempfork/gliderlabs/ssh/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ type Server struct {
PtyCallback PtyCallback // callback for allowing PTY sessions, allows all if nil
ConnCallback ConnCallback // optional callback for wrapping net.Conn before handling
LocalPortForwardingCallback LocalPortForwardingCallback // callback for allowing local port forwarding, denies all if nil
LocalUnixForwardingCallback LocalUnixForwardingCallback // callback for allowing local unix forwarding (direct-streamlocal@openssh.com), denies all if nil
ReversePortForwardingCallback ReversePortForwardingCallback // callback for allowing reverse port forwarding, denies all if nil
ReverseUnixForwardingCallback ReverseUnixForwardingCallback // callback for allowing reverse unix forwarding (streamlocal-forward@openssh.com), denies all if nil
ServerConfigCallback ServerConfigCallback // callback for configuring detailed SSH options
SessionRequestCallback SessionRequestCallback // callback for allowing or denying SSH sessions

Expand Down
4 changes: 2 additions & 2 deletions tempfork/gliderlabs/ssh/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func TestAddHostKey(t *testing.T) {
}

func TestServerShutdown(t *testing.T) {
l := newLocalListener()
l := newLocalTCPListener()
testBytes := []byte("Hello world\n")
s := &Server{
Handler: func(s Session) {
Expand Down Expand Up @@ -82,7 +82,7 @@ func TestServerShutdown(t *testing.T) {
}

func TestServerClose(t *testing.T) {
l := newLocalListener()
l := newLocalTCPListener()
s := &Server{
Handler: func(s Session) {
time.Sleep(5 * time.Second)
Expand Down
19 changes: 15 additions & 4 deletions tempfork/gliderlabs/ssh/session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,25 @@ func (srv *Server) serveOnce(l net.Listener) error {
return e
}
srv.ChannelHandlers = map[string]ChannelHandler{
"session": DefaultSessionHandler,
"direct-tcpip": DirectTCPIPHandler,
"session": DefaultSessionHandler,
"direct-tcpip": DirectTCPIPHandler,
"direct-streamlocal@openssh.com": DirectStreamLocalHandler,
}

forwardedTCPHandler := &ForwardedTCPHandler{}
forwardedUnixHandler := &ForwardedUnixHandler{}
srv.RequestHandlers = map[string]RequestHandler{
"tcpip-forward": forwardedTCPHandler.HandleSSHRequest,
"cancel-tcpip-forward": forwardedTCPHandler.HandleSSHRequest,
"streamlocal-forward@openssh.com": forwardedUnixHandler.HandleSSHRequest,
"cancel-streamlocal-forward@openssh.com": forwardedUnixHandler.HandleSSHRequest,
}

srv.HandleConn(conn)
return nil
}

func newLocalListener() net.Listener {
func newLocalTCPListener() net.Listener {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
if l, err = net.Listen("tcp6", "[::1]:0"); err != nil {
Expand Down Expand Up @@ -66,7 +77,7 @@ func newClientSession(t *testing.T, addr string, config *gossh.ClientConfig) (*g
}

func newTestSession(t *testing.T, srv *Server, cfg *gossh.ClientConfig) (*gossh.Session, *gossh.Client, func()) {
l := newLocalListener()
l := newLocalTCPListener()
go srv.serveOnce(l)
return newClientSession(t, l.Addr().String(), cfg)
}
Expand Down
8 changes: 8 additions & 0 deletions tempfork/gliderlabs/ssh/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,17 @@ type ConnCallback func(ctx Context, conn net.Conn) net.Conn
// LocalPortForwardingCallback is a hook for allowing port forwarding
type LocalPortForwardingCallback func(ctx Context, destinationHost string, destinationPort uint32) bool

// LocalUnixForwardingCallback is a hook for allowing unix forwarding
// (direct-streamlocal@openssh.com)
type LocalUnixForwardingCallback func(ctx Context, socketPath string) bool

// ReversePortForwardingCallback is a hook for allowing reverse port forwarding
type ReversePortForwardingCallback func(ctx Context, bindHost string, bindPort uint32) bool

// ReverseUnixForwardingCallback is a hook for allowing reverse unix forwarding
// (streamlocal-forward@openssh.com).
type ReverseUnixForwardingCallback func(ctx Context, socketPath string) bool

// ServerConfigCallback is a hook for creating custom default server configs
type ServerConfigCallback func(ctx Context) *gossh.ServerConfig

Expand Down
Loading