mirror of
https://github.com/nwtgck/handy-sshd.git
synced 2025-07-27 08:16:38 +00:00
Merge branch 'feature/streamlocal-forward' into develop
This commit is contained in:
commit
8aa0cf8b37
5 changed files with 196 additions and 36 deletions
14
README.md
14
README.md
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
74
server.go
74
server.go
|
@ -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()
|
||||||
|
}()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue