Merge branch 'feature/streamlocal-forward' into develop

This commit is contained in:
Ryo Ota 2023-08-11 11:48:07 +09:00
commit 8aa0cf8b37
5 changed files with 196 additions and 36 deletions

View file

@ -46,24 +46,25 @@ There are several permissions:
* --allow-direct-tcpip * --allow-direct-tcpip
* --allow-execute * --allow-execute
* --allow-sftp * --allow-sftp
* --allow-streamlocal-forward
* --allow-tcpip-forward * --allow-tcpip-forward
**All permissions are allowed when nothing is specified.** The log shows "allowed: " and "NOT allowed: " permissions as follows: **All permissions are allowed when nothing is specified.** The log shows "allowed: " and "NOT allowed: " permissions as follows:
```console ```console
$ handy-sshd --user "john:" $ handy-sshd --user "john:"
2023/08/11 09:42:05 INFO listening on :2222... 2023/08/11 11:40:44 INFO listening on :2222...
2023/08/11 09:42:05 INFO allowed: "tcpip-forward", "direct-tcpip", "execute", "sftp", "direct-streamlocal" 2023/08/11 11:40:44 INFO allowed: "tcpip-forward", "direct-tcpip", "execute", "sftp", "streamlocal-forward", "direct-streamlocal"
2023/08/11 09:42:05 INFO NOT allowed: none 2023/08/11 11:40:44 INFO NOT allowed: none
``` ```
For example, specifying `--allow-direct-tcpip` and `--allow-execute` allows only them: For example, specifying `--allow-direct-tcpip` and `--allow-execute` allows only them:
```console ```console
$ handy-sshd --user "john:" --allow-direct-tcpip --allow-execute $ handy-sshd --user "john:" --allow-direct-tcpip --allow-execute
2023/08/11 09:38:58 INFO listening on :2222... 2023/08/11 11:41:03 INFO listening on :2222...
2023/08/11 09:38:58 INFO allowed: "direct-tcpip", "execute" 2023/08/11 11:41:03 INFO allowed: "direct-tcpip", "execute"
2023/08/11 09:38:58 INFO NOT allowed: "tcpip-forward", "sftp", "direct-streamlocal" 2023/08/11 11:41:03 INFO NOT allowed: "tcpip-forward", "sftp", "streamlocal-forward", "direct-streamlocal"
``` ```
## --help ## --help
@ -79,6 +80,7 @@ Flags:
--allow-direct-tcpip client can use local forwarding and SOCKS proxy --allow-direct-tcpip client can use local forwarding and SOCKS proxy
--allow-execute client can use shell/interactive shell --allow-execute client can use shell/interactive shell
--allow-sftp client can use SFTP and SSHFS --allow-sftp client can use SFTP and SSHFS
--allow-streamlocal-forward client can use Unix domain socket remote forwarding
--allow-tcpip-forward client can use remote forwarding --allow-tcpip-forward client can use remote forwarding
-h, --help help for handy-sshd -h, --help help for handy-sshd
--host string SSH server host (e.g. 127.0.0.1) --host string SSH server host (e.g. 127.0.0.1)

View file

@ -26,6 +26,7 @@ type flagType struct {
allowDirectTcpip bool allowDirectTcpip bool
allowExecute bool allowExecute bool
allowSftp bool allowSftp bool
allowStreamlocalForward bool
allowDirectStreamlocal bool allowDirectStreamlocal bool
} }
@ -50,6 +51,7 @@ func RootCmd() *cobra.Command {
{name: "direct-tcpip", flagPtr: &flag.allowDirectTcpip}, {name: "direct-tcpip", flagPtr: &flag.allowDirectTcpip},
{name: "execute", flagPtr: &flag.allowExecute}, {name: "execute", flagPtr: &flag.allowExecute},
{name: "sftp", flagPtr: &flag.allowSftp}, {name: "sftp", flagPtr: &flag.allowSftp},
{name: "streamlocal-forward", flagPtr: &flag.allowStreamlocalForward},
{name: "direct-streamlocal", flagPtr: &flag.allowDirectStreamlocal}, {name: "direct-streamlocal", flagPtr: &flag.allowDirectStreamlocal},
} }
rootCmd := cobra.Command{ rootCmd := cobra.Command{
@ -76,6 +78,7 @@ func RootCmd() *cobra.Command {
rootCmd.PersistentFlags().BoolVarP(&flag.allowDirectTcpip, "allow-direct-tcpip", "", false, "client can use local forwarding and SOCKS proxy") rootCmd.PersistentFlags().BoolVarP(&flag.allowDirectTcpip, "allow-direct-tcpip", "", false, "client can use local forwarding and SOCKS proxy")
rootCmd.PersistentFlags().BoolVarP(&flag.allowExecute, "allow-execute", "", false, "client can use shell/interactive shell") rootCmd.PersistentFlags().BoolVarP(&flag.allowExecute, "allow-execute", "", false, "client can use shell/interactive shell")
rootCmd.PersistentFlags().BoolVarP(&flag.allowSftp, "allow-sftp", "", false, "client can use SFTP and SSHFS") rootCmd.PersistentFlags().BoolVarP(&flag.allowSftp, "allow-sftp", "", false, "client can use SFTP and SSHFS")
rootCmd.PersistentFlags().BoolVarP(&flag.allowStreamlocalForward, "allow-streamlocal-forward", "", false, "client can use Unix domain socket remote forwarding")
rootCmd.PersistentFlags().BoolVarP(&flag.allowDirectStreamlocal, "allow-direct-streamlocal", "", false, "client can use Unix domain socket local forwarding") rootCmd.PersistentFlags().BoolVarP(&flag.allowDirectStreamlocal, "allow-direct-streamlocal", "", false, "client can use Unix domain socket local forwarding")
return &rootCmd return &rootCmd
@ -107,6 +110,7 @@ func rootRunEWithExtra(cmd *cobra.Command, args []string, flag *flagType, allPer
AllowDirectTcpip: flag.allowDirectTcpip, AllowDirectTcpip: flag.allowDirectTcpip,
AllowExecute: flag.allowExecute, AllowExecute: flag.allowExecute,
AllowSftp: flag.allowSftp, AllowSftp: flag.allowSftp,
AllowStreamlocalForward: flag.allowStreamlocalForward,
AllowDirectStreamlocal: flag.allowDirectStreamlocal, AllowDirectStreamlocal: flag.allowDirectStreamlocal,
} }
var sshUsers []sshUser var sshUsers []sshUser

View file

@ -60,6 +60,7 @@ func TestAllPermissionsAllowed(t *testing.T) {
assertExec(t, client) assertExec(t, client)
assertPtyTerminal(t, client) assertPtyTerminal(t, client)
assertSftp(t, client) assertSftp(t, client)
assertUnixRemotePortForwarding(t, client)
assertUnixLocalPortForwarding(t, client) assertUnixLocalPortForwarding(t, client)
} }
@ -167,6 +168,7 @@ func TestAllowExecute(t *testing.T) {
assertExec(t, client) assertExec(t, client)
assertPtyTerminal(t, client) assertPtyTerminal(t, client)
assertNoSftp(t, client) assertNoSftp(t, client)
assertNoUnixRemotePortForwarding(t, client)
assertNoUnixLocalPortForwarding(t, client) assertNoUnixLocalPortForwarding(t, client)
} }
@ -197,6 +199,38 @@ func TestAllowTcpipForward(t *testing.T) {
assertNoExec(t, client) assertNoExec(t, client)
assertNoPtyTerminal(t, client) assertNoPtyTerminal(t, client)
assertNoSftp(t, client) assertNoSftp(t, client)
assertNoUnixRemotePortForwarding(t, client)
assertNoUnixLocalPortForwarding(t, client)
}
func TestAllowStreamlocalForward(t *testing.T) {
rootCmd := RootCmd()
port := getAvailableTcpPort()
rootCmd.SetArgs([]string{"--port", strconv.Itoa(port), "--user", "john:mypassword", "--allow-streamlocal-forward"})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
var stderrBuf bytes.Buffer
rootCmd.SetErr(&stderrBuf)
rootCmd.ExecuteContext(ctx)
}()
waitTCPServer(port)
sshClientConfig := &ssh.ClientConfig{
User: "john",
Auth: []ssh.AuthMethod{ssh.Password("mypassword")},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
address := net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
client, err := ssh.Dial("tcp", address, sshClientConfig)
assert.NoError(t, err)
defer client.Close()
assert.NoError(t, err)
assertNoRemotePortForwarding(t, client)
assertNoLocalPortForwarding(t, client)
assertNoExec(t, client)
assertNoPtyTerminal(t, client)
assertNoSftp(t, client)
assertUnixRemotePortForwarding(t, client)
assertNoUnixLocalPortForwarding(t, client) assertNoUnixLocalPortForwarding(t, client)
} }
@ -227,6 +261,7 @@ func TestAllowDirectTcpip(t *testing.T) {
assertNoExec(t, client) assertNoExec(t, client)
assertNoPtyTerminal(t, client) assertNoPtyTerminal(t, client)
assertNoSftp(t, client) assertNoSftp(t, client)
assertNoUnixRemotePortForwarding(t, client)
assertNoUnixLocalPortForwarding(t, client) assertNoUnixLocalPortForwarding(t, client)
} }
@ -257,6 +292,7 @@ func TestAllowDirectStreamlocal(t *testing.T) {
assertNoExec(t, client) assertNoExec(t, client)
assertNoPtyTerminal(t, client) assertNoPtyTerminal(t, client)
assertNoSftp(t, client) assertNoSftp(t, client)
assertNoUnixRemotePortForwarding(t, client)
assertUnixLocalPortForwarding(t, client) assertUnixLocalPortForwarding(t, client)
} }
@ -286,5 +322,6 @@ func TestAllowSftp(t *testing.T) {
assertNoLocalPortForwarding(t, client) assertNoLocalPortForwarding(t, client)
assertNoExec(t, client) assertNoExec(t, client)
assertNoPtyTerminal(t, client) assertNoPtyTerminal(t, client)
assertNoUnixRemotePortForwarding(t, client)
assertSftp(t, client) assertSftp(t, client)
} }

View file

@ -187,6 +187,7 @@ func assertRemotePortForwarding(t *testing.T, client *ssh.Client) {
remotePort := getAvailableTcpPort() remotePort := getAvailableTcpPort()
ln, err := client.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(remotePort))) ln, err := client.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(remotePort)))
assert.NoError(t, err) assert.NoError(t, err)
defer ln.Close()
acceptedConnChan := make(chan net.Conn) acceptedConnChan := make(chan net.Conn)
go func() { go func() {
conn, err := ln.Accept() conn, err := ln.Accept()
@ -224,6 +225,50 @@ func assertNoRemotePortForwarding(t *testing.T, client *ssh.Client) {
assert.Equal(t, "ssh: tcpip-forward request denied by peer", err.Error()) assert.Equal(t, "ssh: tcpip-forward request denied by peer", err.Error())
} }
func assertUnixRemotePortForwarding(t *testing.T, client *ssh.Client) {
remoteUnixSocket := path.Join(os.TempDir(), "test-unix-socket-"+uuid.New().String())
ln, err := client.ListenUnix(remoteUnixSocket)
assert.NoError(t, err)
defer os.Remove(remoteUnixSocket)
defer ln.Close()
acceptedConnChan := make(chan net.Conn)
go func() {
conn, err := ln.Accept()
assert.NoError(t, err)
acceptedConnChan <- conn
}()
conn, err := net.Dial("unix", remoteUnixSocket)
assert.NoError(t, err)
defer conn.Close()
acceptedConn := <-acceptedConnChan
defer acceptedConn.Close()
{
localToRemote := [3]byte{1, 2, 3}
_, err = conn.Write(localToRemote[:])
assert.NoError(t, err)
var buf [len(localToRemote)]byte
_, err = io.ReadFull(acceptedConn, buf[:])
assert.NoError(t, err)
assert.Equal(t, buf, localToRemote)
}
{
remoteToLocal := [4]byte{10, 20, 30, 40}
_, err = acceptedConn.Write(remoteToLocal[:])
assert.NoError(t, err)
var buf [len(remoteToLocal)]byte
_, err = io.ReadFull(conn, buf[:])
assert.NoError(t, err)
assert.Equal(t, buf, remoteToLocal)
}
}
func assertNoUnixRemotePortForwarding(t *testing.T, client *ssh.Client) {
remoteUnixSocket := path.Join(os.TempDir(), "test-unix-socket-"+uuid.New().String())
_, err := client.ListenUnix(remoteUnixSocket)
assert.Error(t, err)
assert.Equal(t, "ssh: streamlocal-forward@openssh.com request denied by peer", err.Error())
}
func assertSftp(t *testing.T, client *ssh.Client) { func assertSftp(t *testing.T, client *ssh.Client) {
sftpClient, err := sftp.NewClient(client) sftpClient, err := sftp.NewClient(client)
assert.NoError(t, err) assert.NoError(t, err)

View file

@ -34,6 +34,7 @@ type Server struct {
AllowDirectTcpip bool AllowDirectTcpip bool
AllowExecute bool // this should not be split into "allow-exec" and "allow-pty-req" for now because "pty-req" can be used not for shell execution. AllowExecute bool // this should not be split into "allow-exec" and "allow-pty-req" for now because "pty-req" can be used not for shell execution.
AllowSftp bool AllowSftp bool
AllowStreamlocalForward bool
AllowDirectStreamlocal bool AllowDirectStreamlocal bool
// TODO: DNS server ? // TODO: DNS server ?
@ -121,6 +122,8 @@ func (s *Server) handleSession(shell string, newChannel ssh.NewChannel) {
} }
case "subsystem": case "subsystem":
s.handleSessionSubSystem(req, connection) s.handleSessionSubSystem(req, connection)
default:
s.Logger.Info("unknown request", "req_type", req.Type)
} }
} }
} }
@ -311,8 +314,20 @@ func (s *Server) HandleGlobalRequests(sshConn *ssh.ServerConn, reqs <-chan *ssh.
req.Reply(false, nil) req.Reply(false, nil)
break break
} }
go func() {
s.handleTcpipForward(sshConn, req) s.handleTcpipForward(sshConn, req)
// TODO: support: streamlocal-forward@openssh.com https://github.com/golang/crypto/blob/master/ssh/streamlocal.go }()
case "streamlocal-forward@openssh.com":
if !s.AllowStreamlocalForward {
s.Logger.Info("streamlocal-forward not allowed")
req.Reply(false, nil)
break
}
go func() {
s.handleStreamlocalForward(sshConn, req)
}()
// TODO: support cancel-tcpip-forward
// TODO: support cancel-streamlocal-forward@openssh.com
default: default:
// discard // discard
if req.WantReply { if req.WantReply {
@ -347,6 +362,7 @@ func (s *Server) handleTcpipForward(sshConn *ssh.ServerConn, req *ssh.Request) {
for { for {
conn, err := ln.Accept() conn, err := ln.Accept()
if err != nil { if err != nil {
s.Logger.Info("failed to accept", "err", err)
return return
} }
var replyMsg struct { var replyMsg struct {
@ -387,3 +403,59 @@ func (s *Server) handleTcpipForward(sshConn *ssh.ServerConn, req *ssh.Request) {
}() }()
} }
} }
// client side: https://github.com/golang/crypto/blob/b4ddeeda5bc71549846db71ba23e83ecb26f36ed/ssh/streamlocal.go#L34
func (s *Server) handleStreamlocalForward(sshConn *ssh.ServerConn, req *ssh.Request) {
// https://github.com/openssh/openssh-portable/blob/f9f18006678d2eac8b0c5a5dddf17ab7c50d1e9f/PROTOCOL#L272
var msg struct {
SocketPath string
}
if err := ssh.Unmarshal(req.Payload, &msg); err != nil {
req.Reply(false, nil)
return
}
ln, err := net.Listen("unix", msg.SocketPath)
if err != nil {
req.Reply(false, nil)
return
}
req.Reply(true, nil)
go func() {
sshConn.Wait()
ln.Close()
s.Logger.Info("connection closed", "address", ln.Addr().String())
}()
for {
conn, err := ln.Accept()
if err != nil {
s.Logger.Info("failed to accept", "err", err)
return
}
// https://github.com/openssh/openssh-portable/blob/f9f18006678d2eac8b0c5a5dddf17ab7c50d1e9f/PROTOCOL#L255
var replyMsg struct {
SocketPath string
Reserved string
}
replyMsg.SocketPath = msg.SocketPath
go func() {
channel, reqs, err := sshConn.OpenChannel("forwarded-streamlocal@openssh.com", ssh.Marshal(&replyMsg))
if err != nil {
req.Reply(false, nil)
conn.Close()
return
}
go ssh.DiscardRequests(reqs)
go func() {
io.Copy(channel, conn)
conn.Close()
channel.Close()
}()
go func() {
io.Copy(conn, channel)
conn.Close()
channel.Close()
}()
}()
}
}