From 3ebcdd9c3ddca601ab7fe3b1826d5b51c7748979 Mon Sep 17 00:00:00 2001 From: Manfred Touron Date: Thu, 4 Jan 2018 13:29:10 +0100 Subject: [PATCH] Add telnet support --- CHANGELOG.md | 1 + README.md | 21 ++++++----- db.go | 20 +++++++--- ssh.go | 104 +++++++++++++++++++++++++++++++-------------------- telnet.go | 87 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 178 insertions(+), 55 deletions(-) create mode 100644 telnet.go diff --git a/CHANGELOG.md b/CHANGELOG.md index fe6328c..deebb90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## master (unreleased) * The default created user now has the same username as the user starting sshportal (was hardcoded "admin") +* Add Telnet support ## v1.7.1 (2018-01-03) diff --git a/README.md b/README.md index 49ae9cc..c558878 100644 --- a/README.md +++ b/README.md @@ -39,15 +39,18 @@ Jump host/Jump server without the jump, a.k.a Transparent SSH bastion * Audit log (logging every user action) * Host Keys verifications shared across users * Healthcheck user (replying OK to any user) -* ipv4 and ipv6 support -* [`scp`](https://linux.die.net/man/1/scp) support -* [`rsync`](https://linux.die.net/man/1/rsync) support -* [tunneling](https://www.ssh.com/ssh/tunneling/example) (local forward, remote forward, dynamic forward) support -* [`sftp`](https://www.ssh.com/ssh/sftp/) support -* [`ssh-agent`](https://www.ssh.com/ssh/agent) support -* [`X11 forwarding`](http://en.tldp.org/HOWTO/XDMCP-HOWTO/ssh.html) support -* Git support (can be used to easily use multiple user keys on GitHub, or access your own firewalled gitlab server) -* Do not require any SSH client modification or custom `.ssh/config`, works with every tested SSH programming libraries and every tested SSH +* SSH compatibility + * ipv4 and ipv6 support + * [`scp`](https://linux.die.net/man/1/scp) support + * [`rsync`](https://linux.die.net/man/1/rsync) support + * [tunneling](https://www.ssh.com/ssh/tunneling/example) (local forward, remote forward, dynamic forward) support + * [`sftp`](https://www.ssh.com/ssh/sftp/) support + * [`ssh-agent`](https://www.ssh.com/ssh/agent) support + * [`X11 forwarding`](http://en.tldp.org/HOWTO/XDMCP-HOWTO/ssh.html) support + * Git support (can be used to easily use multiple user keys on GitHub, or access your own firewalled gitlab server) + * Do not require any SSH client modification or custom `.ssh/config`, works with every tested SSH programming libraries and every tested SSH clients +* SSH to non-SSH proxy + * [Telnet](https://www.ssh.com/ssh/telnet) support ## (Known) limitations diff --git a/db.go b/db.go index 03af4ba..1a0bd2a 100644 --- a/db.go +++ b/db.go @@ -158,6 +158,13 @@ const ( ACLActionDeny = "deny" ) +type BastionScheme string + +const ( + BastionSchemeSSH BastionScheme = "ssh" + BastionSchemeTelnet = "telnet" +) + func init() { unixUserRegexp := regexp.MustCompile("[a-z_][a-z0-9_-]*") @@ -171,6 +178,7 @@ func init() { } // Host helpers + func ParseInputURL(input string) (*url.URL, error) { if !strings.Contains(input, "://") { input = "ssh://" + input @@ -195,15 +203,15 @@ func (host *Host) String() string { } return "" } -func (host *Host) Scheme() string { +func (host *Host) Scheme() BastionScheme { if host.URL != "" { u, err := url.Parse(host.URL) if err != nil { - return "ssh" + return BastionSchemeSSH } - return u.Scheme + return BastionScheme(u.Scheme) } else if host.Addr != "" { - return "ssh" + return BastionSchemeSSH } return "" } @@ -268,9 +276,9 @@ func (host *Host) Port() uint64 { } defaultPort: switch host.Scheme() { - case "ssh": + case BastionSchemeSSH: return 22 - case "telnet": + case BastionSchemeTelnet: return 23 default: return 0 diff --git a/ssh.go b/ssh.go index 01efed8..4bd5254 100644 --- a/ssh.go +++ b/ssh.go @@ -88,7 +88,7 @@ func channelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewCh switch newChan.ChannelType() { case "session": default: - // TODO: handle direct-tcp + // TODO: handle direct-tcp (only for ssh scheme) if err := newChan.Reject(gossh.UnknownChannelType, "unsupported channel type"); err != nil { log.Printf("error: failed to reject channel: %v", err) } @@ -100,7 +100,7 @@ func channelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewCh switch actx.userType() { case UserTypeBastion: log.Printf("New connection(bastion): sshUser=%q remote=%q local=%q dbUser=id:%q,email:%s", conn.User(), conn.RemoteAddr(), conn.LocalAddr(), actx.user.ID, actx.user.Email) - host, clientConfig, err := bastionConfig(ctx) + host, err := HostByName(actx.db, actx.inputUsername) if err != nil { ch, _, err2 := newChan.Accept() if err2 != nil { @@ -112,66 +112,90 @@ func channelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewCh return } - sess := Session{ - UserID: actx.user.ID, - HostID: host.ID, - Status: SessionStatusActive, - } - if err = actx.db.Create(&sess).Error; err != nil { + switch host.Scheme() { + case BastionSchemeSSH: + clientConfig, err := bastionClientConfig(ctx, host) + if err != nil { + ch, _, err2 := newChan.Accept() + if err2 != nil { + return + } + fmt.Fprintf(ch, "error: %v\n", err) + // FIXME: force close all channels + _ = ch.Close() + return + } + + sess := Session{ + UserID: actx.user.ID, + HostID: host.ID, + Status: SessionStatusActive, + } + if err = actx.db.Create(&sess).Error; err != nil { + ch, _, err2 := newChan.Accept() + if err2 != nil { + return + } + fmt.Fprintf(ch, "error: %v\n", err) + _ = ch.Close() + return + } + + err = bastionsession.ChannelHandler(srv, conn, newChan, ctx, bastionsession.Config{ + Addr: host.DialAddr(), + ClientConfig: clientConfig, + }) + + now := time.Now() + sessUpdate := Session{ + Status: SessionStatusClosed, + ErrMsg: fmt.Sprintf("%v", err), + StoppedAt: &now, + } + switch sessUpdate.ErrMsg { + case "lch closed the connection", "rch closed the connection": + sessUpdate.ErrMsg = "" + } + actx.db.Model(&sess).Updates(&sessUpdate) + case BastionSchemeTelnet: + tmpSrv := ssh.Server{ + // PtyCallback: srv.PtyCallback, + Handler: telnetHandler(host), + } + ssh.DefaultChannelHandler(&tmpSrv, conn, newChan, ctx) + default: ch, _, err2 := newChan.Accept() if err2 != nil { return } - fmt.Fprintf(ch, "error: %v\n", err) + fmt.Fprintf(ch, "error: unknown bastion scheme: %q\n", host.Scheme()) + // FIXME: force close all channels _ = ch.Close() - return } - - err = bastionsession.ChannelHandler(srv, conn, newChan, ctx, bastionsession.Config{ - Addr: host.DialAddr(), - ClientConfig: clientConfig, - }) - - now := time.Now() - sessUpdate := Session{ - Status: SessionStatusClosed, - ErrMsg: fmt.Sprintf("%v", err), - StoppedAt: &now, - } - switch sessUpdate.ErrMsg { - case "lch closed the connection", "rch closed the connection": - sessUpdate.ErrMsg = "" - } - actx.db.Model(&sess).Updates(&sessUpdate) default: // shell ssh.DefaultChannelHandler(srv, conn, newChan, ctx) } } -func bastionConfig(ctx ssh.Context) (*Host, *gossh.ClientConfig, error) { +func bastionClientConfig(ctx ssh.Context, host *Host) (*gossh.ClientConfig, error) { actx := ctx.Value(authContextKey).(*authContext) - host, err := HostByName(actx.db, actx.inputUsername) - if err != nil { - return nil, nil, err - } - clientConfig, err := host.clientConfig(dynamicHostKey(actx.db, host)) if err != nil { - return nil, nil, err + return nil, err } var tmpUser User if err = actx.db.Preload("Groups").Preload("Groups.ACLs").Where("id = ?", actx.user.ID).First(&tmpUser).Error; err != nil { - return nil, nil, err + return nil, err } var tmpHost Host if err = actx.db.Preload("Groups").Preload("Groups.ACLs").Where("id = ?", host.ID).First(&tmpHost).Error; err != nil { - return nil, nil, err + return nil, err } action, err2 := CheckACLs(tmpUser, tmpHost) if err2 != nil { - return nil, nil, err2 + return nil, err2 } HostDecrypt(actx.globalContext.String("aes-key"), host) @@ -180,11 +204,11 @@ func bastionConfig(ctx ssh.Context) (*Host, *gossh.ClientConfig, error) { switch action { case ACLActionAllow: case ACLActionDeny: - return nil, nil, fmt.Errorf("you don't have permission to that host") + return nil, fmt.Errorf("you don't have permission to that host") default: - return nil, nil, fmt.Errorf("invalid ACL action: %q", action) + return nil, fmt.Errorf("invalid ACL action: %q", action) } - return host, clientConfig, nil + return clientConfig, nil } func shellHandler(s ssh.Session) { diff --git a/telnet.go b/telnet.go new file mode 100644 index 0000000..c154dde --- /dev/null +++ b/telnet.go @@ -0,0 +1,87 @@ +package main + +import ( + "bufio" + "bytes" + "fmt" + "io" + "log" + "time" + + "github.com/gliderlabs/ssh" + oi "github.com/reiver/go-oi" + telnet "github.com/reiver/go-telnet" +) + +type bastionTelnetCaller struct { + ssh ssh.Session +} + +func (caller bastionTelnetCaller) CallTELNET(ctx telnet.Context, w telnet.Writer, r telnet.Reader) { + go func(writer io.Writer, reader io.Reader) { + var buffer [1]byte // Seems like the length of the buffer needs to be small, otherwise will have to wait for buffer to fill up. + p := buffer[:] + + for { + // Read 1 byte. + n, err := reader.Read(p) + if n <= 0 && nil == err { + continue + } else if n <= 0 && nil != err { + break + } + + if _, err = oi.LongWrite(writer, p); err != nil { + log.Printf("telnet longwrite failed: %v", err) + } + } + }(caller.ssh, r) + + var buffer bytes.Buffer + var p []byte + + var crlfBuffer = [2]byte{'\r', '\n'} + crlf := crlfBuffer[:] + + scanner := bufio.NewScanner(caller.ssh) + scanner.Split(scannerSplitFunc) + + for scanner.Scan() { + buffer.Write(scanner.Bytes()) + buffer.Write(crlf) + + p = buffer.Bytes() + + n, err := oi.LongWrite(w, p) + if nil != err { + break + } + if expected, actual := int64(len(p)), n; expected != actual { + err := fmt.Errorf("transmission problem: tried sending %d bytes, but actually only sent %d bytes", expected, actual) + fmt.Fprint(caller.ssh, err.Error()) + return + } + buffer.Reset() + } + + // Wait a bit to receive data from the server (that we would send to io.Stdout). + time.Sleep(3 * time.Millisecond) +} + +func scannerSplitFunc(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF { + return 0, nil, nil + } + return bufio.ScanLines(data, atEOF) +} + +func telnetHandler(host *Host) ssh.Handler { + return func(s ssh.Session) { + // FIXME: log session in db + //actx := s.Context().Value(authContextKey).(*authContext) + caller := bastionTelnetCaller{ssh: s} + if err := telnet.DialToAndCall(host.DialAddr(), caller); err != nil { + fmt.Fprintf(s, "error: %v", err) + } + } +}