Add telnet support

This commit is contained in:
Manfred Touron 2018-01-04 13:29:10 +01:00
parent a9f86d1d01
commit 3ebcdd9c3d
5 changed files with 178 additions and 55 deletions

View file

@ -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)

View file

@ -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

20
db.go
View file

@ -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

104
ssh.go
View file

@ -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) {

87
telnet.go Normal file
View file

@ -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)
}
}
}