Browse Source

Initial import

master
Maze X 1 year ago
parent
commit
2dc697d7fb
9 changed files with 1010 additions and 0 deletions
  1. +40
    -0
      cmd/stronghold/main.go
  2. +13
    -0
      go.mod
  3. +28
    -0
      go.sum
  4. +350
    -0
      server/client.go
  5. +15
    -0
      server/crypto.go
  6. +48
    -0
      server/io.go
  7. +302
    -0
      server/proxy.go
  8. +187
    -0
      server/server.go
  9. +27
    -0
      testdata/ssh_host_rsa_key

+ 40
- 0
cmd/stronghold/main.go View File

@ -0,0 +1,40 @@
package main
import (
"flag"
"github.com/sirupsen/logrus"
"git.maze.io/maze/stronghold/server"
)
var (
defaultHostKeyFile = "testdata/ssh_host_rsa_key"
defaultRootPath = "testdata/session"
)
func main() {
var (
listen = flag.String("listen", server.DefaultAddr, "server listen address")
hostKey = flag.String("key", defaultHostKeyFile, "server host key")
root = flag.String("root", defaultRootPath, "server recording root")
debug = flag.Bool("debug", false, "enable debug messages")
)
flag.Parse()
if *debug {
logrus.SetLevel(logrus.DebugLevel)
}
s, err := server.New(*root, *hostKey)
if err != nil {
logrus.Fatalln(err)
}
s.PublicKeyHandler = server.PermitAllPublicKeys
logrus.WithField("addr", *listen).Info("server starting")
if err = s.ListenAndServe(*listen); err != nil {
logrus.Fatalln(err)
}
}

+ 13
- 0
go.mod View File

@ -0,0 +1,13 @@
module git.maze.io/maze/stronghold
go 1.13
require (
github.com/asergeyev/nradix v0.0.0-20170505151046-3872ab85bb56 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/sirupsen/logrus v1.4.2
github.com/yl2chen/cidranger v0.0.0-20191223063343-81d435411dd9
golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9 // indirect
maze.io/x/ttyrec v1.0.0
)

+ 28
- 0
go.sum View File

@ -0,0 +1,28 @@
github.com/asergeyev/nradix v0.0.0-20170505151046-3872ab85bb56/go.mod h1:8BhOLuqtSuT5NZtZMwfvEibi09RO3u79uqfHZzfDTR4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/yl2chen/cidranger v0.0.0-20191223063343-81d435411dd9 h1:CeL/MwfJSB75l4utIuvJ6qk3TNA2a49M/LHwi/uBwOY=
github.com/yl2chen/cidranger v0.0.0-20191223063343-81d435411dd9/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d h1:9FCpayM9Egr1baVnV1SX0H87m+XB0B8S0hAMi99X/3U=
golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9 h1:1/DFK4b7JH8DmkqhUk48onnSfrPzImPoVxuomtbT2nk=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
maze.io/x/ttyrec v1.0.0 h1:lHg6hWcxgNH2G/i47bsYtoyxbXBT+AU8oYijvQ3lAQs=
maze.io/x/ttyrec v1.0.0/go.mod h1:R/1HcHnIBP5SXyWKJ9fuz3L3dkLhOrUrqY/m8SXfiyQ=

+ 350
- 0
server/client.go View File

@ -0,0 +1,350 @@
package server
import (
"errors"
"io"
"net"
"strconv"
"sync"
"time"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
)
const (
requestTypeAgent = "auth-agent-req@openssh.com"
requestTypeEnv = "env"
requestTypeKeepAlive = "keepalive@openssh.com"
requestTypePTY = "pty-req"
requestTypeRoaming = "roaming@appgate.com"
requestTypeSession = "session"
requestTypeTCPIPForward = "tcpip-forward"
requestTypeX11 = "x11-req"
channelTypeAgent = "auth-agent@openssh.com"
channelTypeDirectTCPIP = "direct-tcpip"
channelTypeForwardedTCPIP = "forwarded-tcpip"
channelTypeSession = "session"
connectTimeout = 30 * time.Second
)
// clientConn connected to the server.
type clientConn struct {
netConn net.Conn
sshConn ssh.Conn
id string
closed chan struct{}
root string
requestMutex sync.RWMutex
ptyRequest *ptyRequest
sessionRequest *sessionRequest
envRequest map[string]string
agent agent.Agent
}
func newClient(netConn net.Conn, sshConn ssh.Conn, root string) *clientConn {
return &clientConn{
netConn: netConn,
sshConn: sshConn,
id: sessionID(sshConn),
closed: make(chan struct{}),
root: root,
envRequest: make(map[string]string),
}
}
func (client *clientConn) log() *logrus.Entry {
return logrus.WithFields(logrus.Fields{
"tag": "client",
"session": client.id,
"src": client.sshConn.RemoteAddr().String(),
"user": client.sshConn.User(),
})
}
func (client *clientConn) handleRequests(requests <-chan *ssh.Request) {
log := client.log()
for {
select {
case <-client.closed:
return
case request := <-requests:
if request == nil {
log.Warn("client request channel closed")
return
}
var (
log = log.WithField("request", request.Type)
ok bool
err error
response []byte
)
log.Debug("new OOB request")
switch request.Type {
case requestTypeAgent:
ok, err = client.handleAgentRequest(request)
case requestTypeEnv:
ok, err = client.handleEnvRequest(request)
case requestTypeKeepAlive:
ok = true
case requestTypeTCPIPForward:
ok, err = client.handleTCPIPForwardRequest(request)
case requestTypePTY:
ok, err = client.handlePTYRequest(request)
case requestTypeSession:
ok, err = client.handleSessionRequest(request)
case requestTypeX11:
// Request type is ignored (not supported)
default:
log.Debug("unknown/unhandled request type")
}
if err != nil {
log.WithError(err).Warn("failed to handle request")
ok = false
}
if request.WantReply {
_ = request.Reply(ok, response)
}
}
}
}
func (client *clientConn) handleAgentRequest(request *ssh.Request) (bool, error) {
return true, nil
}
// RFC 4254 Section 6.4.
type envRequest struct {
Name string
Value string
}
func (client *clientConn) handleEnvRequest(request *ssh.Request) (ok bool, err error) {
var payload envRequest
if err = ssh.Unmarshal(request.Payload, &payload); err != nil {
return
}
client.requestMutex.Lock()
client.envRequest[payload.Name] = payload.Value
client.requestMutex.Unlock()
return true, nil
}
// RFC 4254 Section 6.2.
type ptyRequest struct {
Term string
Columns uint32
Rows uint32
Width uint32
Height uint32
ModeList string
}
func (client *clientConn) handlePTYRequest(request *ssh.Request) (ok bool, err error) {
client.requestMutex.Lock()
defer client.requestMutex.Unlock()
if client.ptyRequest != nil {
err = errors.New("duplicate pty request")
return
}
client.ptyRequest = new(ptyRequest)
if err = ssh.Unmarshal(request.Payload, client.ptyRequest); err != nil {
return
}
return true, nil
}
// RFC 4254 Section 6.2.
type sessionRequest struct {
SenderChannel uint32
WindowSize uint32
MaxPacketSize uint32
}
func (client *clientConn) handleSessionRequest(request *ssh.Request) (ok bool, err error) {
client.requestMutex.Lock()
defer client.requestMutex.Unlock()
if client.sessionRequest != nil {
// Duplicate session request, only one is allowed.
err = errors.New("duplicate session request")
return
}
client.sessionRequest = new(sessionRequest)
if err = ssh.Unmarshal(request.Payload, client.sessionRequest); err != nil {
return
}
return true, nil
}
// RFC 4254 Section 7.1.
type forwardTCPIPRequest struct {
TargetHost string
TargetPort uint32
}
type forwardedTCPIPResponse struct {
TargetHost string
TargetPort uint32
OriginAddr string
OriginPort uint32
}
func (client *clientConn) handleTCPIPForwardRequest(request *ssh.Request) (ok bool, err error) {
var payload forwardTCPIPRequest
if err = ssh.Unmarshal(request.Payload, &payload); err != nil {
return
}
var (
remoteAddr = net.JoinHostPort(payload.TargetHost, strconv.FormatUint(uint64(payload.TargetPort), 10))
log = client.log().WithField("dst", remoteAddr)
)
log.Info("TCP/IP forward request")
var (
response = forwardedTCPIPResponse{
TargetHost: payload.TargetHost,
TargetPort: payload.TargetPort,
OriginAddr: "",
OriginPort: 0,
}
channel ssh.Channel
requests <-chan *ssh.Request
)
if channel, requests, err = client.sshConn.OpenChannel(channelTypeForwardedTCPIP, ssh.Marshal(&response)); err != nil {
return
}
_ = channel
go ssh.DiscardRequests(requests)
return true, nil
}
func (client *clientConn) handleNewChannels(channels <-chan ssh.NewChannel) error {
log := client.log()
for {
select {
case <-client.closed:
return io.EOF
case newChannel := <-channels:
if newChannel == nil {
close(client.closed)
return io.EOF
}
var (
channelType = newChannel.ChannelType()
log = log.WithField("channel", channelType)
)
log.Debug("new channel requested by client")
var channelErr error
switch channelType {
case channelTypeDirectTCPIP:
channelErr = client.handleDirectTCPIPChannel(newChannel)
case channelTypeSession:
default:
log.Debug("unknown/unhandled channel type")
if err := newChannel.Reject(ssh.UnknownChannelType, "unknown channel type"); err != nil {
return err
}
}
if channelErr != nil {
log.WithError(channelErr).Warn("failed to handle new channel")
}
}
}
}
// RFC 4254 Section 7.2.
type directTCPIP struct {
TargetHost string
TargetPort uint32
SourceIP string
SourcePort uint32
}
func (client *clientConn) handleDirectTCPIPChannel(newChannel ssh.NewChannel) (err error) {
var (
log = client.log()
payload directTCPIP
request = newChannel.ExtraData()
)
if err = ssh.Unmarshal(request, &payload); err != nil {
log.WithError(err).Warn("invalid direct TCP/IP payload")
_ = newChannel.Reject(ssh.ConnectionFailed, "invalid direct TCP/IP payload")
return
}
if client.agent == nil {
var (
agentChannel ssh.Channel
agentRequests <-chan *ssh.Request
)
log.Debug("connecting to ssh-agent")
if agentChannel, agentRequests, err = client.sshConn.OpenChannel(channelTypeAgent, nil); err != nil {
log.WithError(err).Warn("error opening channel to ssh-agent")
return newChannel.Reject(ssh.ConnectionFailed, err.Error())
}
go ssh.DiscardRequests(agentRequests)
log.Debug("connected to ssh-agent")
client.agent = agent.NewClient(agentChannel)
}
addr := net.JoinHostPort(payload.TargetHost, strconv.FormatUint(uint64(payload.TargetPort), 10))
remote := newSSHClient(client.sshConn.User())
if err := remote.ConnectAgent(client.agent); err != nil {
return newChannel.Reject(ssh.Prohibited, err.Error())
}
if err := remote.DialTimeout(addr, connectTimeout); err != nil {
return newChannel.Reject(ssh.ConnectionFailed, err.Error())
}
channel, channelRequests, err := newChannel.Accept()
if err != nil {
return err
}
go ssh.DiscardRequests(channelRequests)
server := newSSHInterceptingServer(client.root)
serverKey, err := loadPrivateKeyFile("testdata/ssh_host_rsa_key")
if err != nil {
_ = remote.Close()
_ = channel.Close()
return err
}
server.AddHostKeys(serverKey)
if err = server.Handshake(channelConn{channel}); err != nil {
_ = remote.Close()
_ = channel.Close()
return err
}
go server.ProxyClient(remote)
return
}

+ 15
- 0
server/crypto.go View File

@ -0,0 +1,15 @@
package server
import (
"io/ioutil"
"golang.org/x/crypto/ssh"
)
func loadPrivateKeyFile(name string) (ssh.Signer, error) {
b, err := ioutil.ReadFile(name)
if err != nil {
return nil, err
}
return ssh.ParsePrivateKey(b)
}

+ 48
- 0
server/io.go View File

@ -0,0 +1,48 @@
package server
import (
"io"
"os"
)
type writeCloser struct {
io.Writer
io.Closer
}
type safeFileWriter struct {
name string
mode os.FileMode
f *os.File
err error
}
func newSafeFileWriter(name string, mode os.FileMode) io.WriteCloser {
return &safeFileWriter{
name: name,
mode: mode,
}
}
func (sfw *safeFileWriter) Close() error {
if sfw.f == nil {
return nil
}
err := sfw.f.Close()
sfw.f = nil
return err
}
func (sfw *safeFileWriter) Write(p []byte) (int, error) {
if sfw.err == nil && sfw.f == nil {
sfw.f, sfw.err = os.OpenFile(sfw.name, os.O_APPEND|os.O_CREATE|os.O_WRONLY, sfw.mode)
}
if sfw.err != nil {
return len(p), nil
}
var n int
n, sfw.err = sfw.f.Write(p)
return n, nil
}

+ 302
- 0
server/proxy.go View File

@ -0,0 +1,302 @@
package server
import (
"encoding/hex"
"io"
"net"
"path/filepath"
"time"
"maze.io/x/ttyrec"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
)
// channelConn allows you to use an ssh channel as net.Conn
type channelConn struct{ ssh.Channel }
func (channelConn) LocalAddr() net.Addr { return nil }
func (channelConn) RemoteAddr() net.Addr { return nil }
func (channelConn) SetDeadline(_ time.Time) error { return nil }
func (channelConn) SetReadDeadline(_ time.Time) error { return nil }
func (channelConn) SetWriteDeadline(_ time.Time) error { return nil }
type sshClient struct {
ssh.Conn
NewChannels <-chan ssh.NewChannel
Requests <-chan *ssh.Request
netConn net.Conn
config *ssh.ClientConfig
}
func newSSHClient(user string) *sshClient {
return &sshClient{
config: &ssh.ClientConfig{
User: user,
Auth: nil,
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
ClientVersion: version,
Timeout: connectTimeout,
},
}
}
func (client *sshClient) Close() error {
if client.Conn == nil {
return nil
}
err := client.Conn.Close()
_ = client.netConn.Close()
client.netConn = nil
client.Conn = nil
return err
}
func (client *sshClient) ConnectAgent(agent agent.Agent) error {
signers, err := agent.Signers()
if err != nil {
return err
}
client.config.Auth = append(client.config.Auth, ssh.PublicKeys(signers...))
return nil
}
func (client *sshClient) DialTimeout(address string, timeout time.Duration) error {
log := logrus.WithFields(logrus.Fields{
"dst": address,
"timeout": timeout,
})
log.Info("connecting to SSH server")
var err error
if client.netConn, err = net.DialTimeout("tcp", address, timeout); err != nil {
return err
}
log.Debug("doing SSH client handshake")
if client.Conn, client.NewChannels, client.Requests, err = ssh.NewClientConn(client.netConn, address, client.config); err != nil {
log.WithError(err).Error("connection failed")
_ = client.netConn.Close()
client.netConn = nil
return err
}
return nil
}
type sshInterceptingServer struct {
*ssh.ServerConn
NewChannels <-chan ssh.NewChannel
Requests <-chan *ssh.Request
config *ssh.ServerConfig
root string
recorder io.WriteCloser
}
func newSSHInterceptingServer(root string) *sshInterceptingServer {
return &sshInterceptingServer{
config: &ssh.ServerConfig{
Config: ssh.Config{},
NoClientAuth: true,
MaxAuthTries: 128,
PasswordCallback: PermitAllPasswords,
PublicKeyCallback: PermitAllPublicKeys,
ServerVersion: version,
},
root: root,
}
}
func (server *sshInterceptingServer) AddHostKeys(signers ...ssh.Signer) {
for _, signer := range signers {
server.config.AddHostKey(signer)
}
}
func (server *sshInterceptingServer) Handshake(conn net.Conn) error {
var err error
log := logrus.WithFields(logrus.Fields{
"dst": conn.RemoteAddr(),
})
log.Info("starting SSH server connection")
if server.ServerConn, server.NewChannels, server.Requests, err = ssh.NewServerConn(conn, server.config); err != nil {
return err
}
return nil
}
func (server *sshInterceptingServer) ProxyClient(client *sshClient) {
defer client.Close()
defer server.Close()
log := logrus.WithFields(logrus.Fields{
"src": server.RemoteAddr(),
"dst": client.RemoteAddr(),
"tag": "proxy",
})
transcript := newSafeFileWriter(filepath.Join(server.root, hex.EncodeToString(client.SessionID()))+".ttyrec", 0600)
server.recorder = writeCloser{
Writer: ttyrec.NewEncoder(transcript),
Closer: transcript,
}
for {
select {
case newChannel := <-server.NewChannels:
if newChannel == nil {
return
}
log := log.WithFields(logrus.Fields{
"channel": newChannel.ChannelType(),
"flow": "server->client",
})
server.multiplexNewChannel(log, client, newChannel)
case newChannel := <-client.NewChannels:
if newChannel == nil {
return
}
log := log.WithFields(logrus.Fields{
"channel": newChannel.ChannelType(),
"flow": "client->server",
})
server.multiplexNewChannel(log, server, newChannel)
case request := <-server.Requests:
if request == nil {
return
}
log := log.WithFields(logrus.Fields{
"request": request.Type,
"flow": "server->client",
})
server.relayRequest(log, client, request)
case request := <-client.Requests:
if request == nil {
return
}
log := log.WithFields(logrus.Fields{
"channel": request.Type,
"flow": "client->server",
})
server.relayRequest(log, server, request)
}
}
}
func (server *sshInterceptingServer) multiplexNewChannel(log *logrus.Entry, conn ssh.Conn, newChannel ssh.NewChannel) {
log.Debug("new channel")
clientChannel, clientRequests, err := conn.OpenChannel(newChannel.ChannelType(), newChannel.ExtraData())
if err != nil {
log.WithError(err).Warn("failed to open channel")
if err, ok := err.(*ssh.OpenChannelError); ok {
_ = newChannel.Reject(err.Reason, err.Message)
} else {
_ = newChannel.Reject(ssh.Prohibited, err.Error())
}
return
}
serverChannel, serverRequests, err := newChannel.Accept()
if err != nil {
log.WithError(err).Warn("failed to accept channel")
go ssh.DiscardRequests(clientRequests)
_ = clientChannel.Close()
return
}
go server.multiplex(log, newChannel.ChannelType(), clientChannel, serverChannel, clientRequests, serverRequests)
}
func (server *sshInterceptingServer) multiplex(log *logrus.Entry, channelType string, clientChannel, serverChannel ssh.Channel, clientRequests, serverRequests <-chan *ssh.Request) {
var serverWriter io.Writer
switch channelType {
case "session":
serverWriter = io.MultiWriter(serverChannel, server.recorder)
default:
serverWriter = serverChannel
}
go func() {
_, _ = io.Copy(clientChannel, serverChannel)
}()
go func() {
_, _ = io.Copy(serverWriter, clientChannel)
}()
defer func() {
_ = clientChannel.Close()
_ = serverChannel.Close()
if server.recorder != nil {
_ = server.recorder.Close()
}
}()
for {
select {
case request := <-clientRequests:
if request == nil {
return
}
log := log.WithFields(logrus.Fields{
"request": request.Type,
"flow": "client->server",
})
log.WithField("request", request.Type).Debug("new inbound channel request")
server.relayChannelRequest(log, serverChannel, request)
case request := <-serverRequests:
if request == nil {
return
}
log := log.WithFields(logrus.Fields{
"request": request.Type,
"flow": "server->client",
})
server.relayChannelRequest(log, clientChannel, request)
}
}
}
func (server *sshInterceptingServer) relayRequest(log *logrus.Entry, conn ssh.Conn, request *ssh.Request) {
log.Debug("request")
ok, payload, err := conn.SendRequest(request.Type, request.WantReply, request.Payload)
if err != nil {
log.WithError(err).Warn("failed to relay request")
return
}
log.WithFields(logrus.Fields{
"ok": ok,
"want_reply": request.WantReply,
}).Debug("request response")
if request.WantReply {
if err = request.Reply(ok, payload); err != nil {
log.WithError(err).Warn("failed to reply to relayed request")
return
}
}
}
func (server *sshInterceptingServer) relayChannelRequest(log *logrus.Entry, channel ssh.Channel, request *ssh.Request) {
log.Debug("channel request")
ok, err := channel.SendRequest(request.Type, request.WantReply, request.Payload)
if err != nil {
log.WithError(err).Warn("failed to relay channel request")
return
}
log.WithFields(logrus.Fields{
"ok": ok,
"want_reply": request.WantReply,
}).Debug("channel request response")
if request.WantReply {
if err = request.Reply(ok, nil); err != nil {
log.WithError(err).Warn("failed to reply to relayed channel request")
}
}
}

+ 187
- 0
server/server.go View File

@ -0,0 +1,187 @@
package server
import (
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"hash/fnv"
"io"
"net"
"os"
"path/filepath"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
)
const (
DefaultAddr = ":22"
tag = "server"
maxAuthRetries = 16
version = "SSH-2.0-StrongHold"
)
// Server receives connections from SSH clients.
type Server struct {
// KeyboardInteractiveHandler is a callback for doing keyboard-interactive authentication.
KeyboardInteractiveHandler func(conn ssh.ConnMetadata, client ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error)
// PasswordHandler is a callback for doing password authentication.
PasswordHandler func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error)
// PublicKeyHandler is a callback for doing public key authentication.
PublicKeyHandler func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error)
root string
config *ssh.ServerConfig
closed chan struct{}
}
func New(root string, hostKeyFiles ...string) (*Server, error) {
if len(hostKeyFiles) == 0 {
return nil, errors.New("server: at least one key file must be specified")
}
if !filepath.IsAbs(root) {
var err error
if root, err = filepath.Abs(root); err != nil {
return nil, err
}
}
if info, err := os.Stat(root); err != nil {
if !os.IsNotExist(err) {
return nil, err
}
if err = os.MkdirAll(root, 0700); err != nil {
return nil, err
}
} else if !info.IsDir() {
return nil, fmt.Errorf("server: root %s exists but is not a directory", root)
}
server := &Server{
config: &ssh.ServerConfig{
NoClientAuth: true,
MaxAuthTries: maxAuthRetries,
ServerVersion: version,
},
closed: make(chan struct{}),
root: root,
}
for _, keyFile := range hostKeyFiles {
key, err := loadPrivateKeyFile(keyFile)
if err != nil {
return nil, fmt.Errorf("server: error loading private key from %s: %w", keyFile, err)
}
server.config.AddHostKey(key)
}
return server, nil
}
func (server *Server) ListenAndServe(addr string) error {
l, err := net.Listen("tcp", addr)
if err != nil {
return err
}
if server.KeyboardInteractiveHandler != nil {
server.config.NoClientAuth = false
server.config.KeyboardInteractiveCallback = server.KeyboardInteractiveHandler
}
if server.PasswordHandler != nil {
server.config.NoClientAuth = false
server.config.PasswordCallback = server.PasswordHandler
}
if server.PublicKeyHandler != nil {
server.config.NoClientAuth = false
server.config.PublicKeyCallback = server.PublicKeyHandler
}
for {
conn, err := l.Accept()
if err != nil {
logrus.WithError(err).Error("error accepting client")
continue
}
go server.handleConn(conn)
}
}
func (server *Server) handleConn(netConn net.Conn) {
defer netConn.Close()
log := logrus.WithFields(logrus.Fields{
"tag": tag,
"src": netConn.RemoteAddr().String(),
})
log.Info("new client connection")
defer log.Info("client connection closed")
sshConn, newChannels, requests, err := ssh.NewServerConn(netConn, server.config)
if err != nil {
log.WithError(err).Error("handshakeAsServer failed")
return
}
client := newClient(netConn, sshConn, server.root)
go client.handleRequests(requests)
if err = client.handleNewChannels(newChannels); err != nil {
if err != io.EOF {
log.WithError(err).Error("fatal error")
}
}
<-client.closed
}
func fingerprint(key ssh.PublicKey) string {
h := sha256.New()
_, _ = h.Write(key.Marshal())
return "SHA256:" + base64.RawStdEncoding.EncodeToString(h.Sum(nil))
}
func sessionID(metadata ssh.ConnMetadata) string {
h := fnv.New64()
_, _ = h.Write(metadata.SessionID())
return hex.EncodeToString(h.Sum(nil))
}
// PermitAllPasswords is a password handler that accepts all credentials.
func PermitAllPasswords(metadata ssh.ConnMetadata, _ []byte) (*ssh.Permissions, error) {
log := logrus.WithFields(logrus.Fields{
"session": sessionID(metadata),
"src": metadata.RemoteAddr().String(),
"user": metadata.User(),
})
log.Info("accepting password")
return new(ssh.Permissions), nil
}
// PermitAllPublicKeys is a public key handler that accepts all public keys.
func PermitAllPublicKeys(metadata ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
log := logrus.WithFields(logrus.Fields{
"session": sessionID(metadata),
"src": metadata.RemoteAddr().String(),
"user": metadata.User(),
})
log.WithFields(logrus.Fields{
"fingerprint": fingerprint(key),
"key_type": key.Type(),
}).Info("accepting public key")
return new(ssh.Permissions), nil
}
func PermitAllHostKeys(hostname string, remote net.Addr, key ssh.PublicKey) error {
log := logrus.WithFields(logrus.Fields{
"dst": remote.String(),
"hostname": hostname,
"fingerprint": fingerprint(key),
"key_type": key.Type(),
})
log.Info("accepting host key")
return nil
}

+ 27
- 0
testdata/ssh_host_rsa_key View File

@ -0,0 +1,27 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
NhAAAAAwEAAQAAAQEA1s3hLStmSY0wtMocufFW9ZEgzdUSyjnur2fJJCa3HHry7UaQDyTU
luxb9gsgQDpkemtQhKJhX90Mn4d2H2CPg6Zl0L8d2RRm/IiW6/msyS9oK8YGNfBg8u2Muo
bkerrrlz6sheS1KpSIdyCaBwp/OoP6o3BGXZLfU3qEls8Db+IXy0BssaDZo5vBv9hf/IEc
wWxTSLGOopqy2+01jijwq9vD6TtSRfGEDR6U/1za9IpiG1ZbuBXOneRSiv76FIiFqDYDPJ
gPxkgyWI//wVCvwJv8lURUcqv02oCSi68w/6Gg4knZy9WD7+eu1z+AqbDUq6HxaldJYjjx
7CZE7+xMxwAAA7gtotEyLaLRMgAAAAdzc2gtcnNhAAABAQDWzeEtK2ZJjTC0yhy58Vb1kS
DN1RLKOe6vZ8kkJrccevLtRpAPJNSW7Fv2CyBAOmR6a1CEomFf3Qyfh3YfYI+DpmXQvx3Z
FGb8iJbr+azJL2grxgY18GDy7Yy6huR6uuuXPqyF5LUqlIh3IJoHCn86g/qjcEZdkt9Teo
SWzwNv4hfLQGyxoNmjm8G/2F/8gRzBbFNIsY6imrLb7TWOKPCr28PpO1JF8YQNHpT/XNr0
imIbVlu4Fc6d5FKK/voUiIWoNgM8mA/GSDJYj//BUK/Am/yVRFRyq/TagJKLrzD/oaDiSd
nL1YPv567XP4CpsNSrofFqV0liOPHsJkTv7EzHAAAAAwEAAQAAAQEAno+BDa/ApMCVWI7t
H5D4CPNlTKthYGfdOhuZjZcKifp0AhSZWKRuTS4SQBA8cUzpSLiYyh93U6U/oS8EUescZS
jadXnuVYTK9NT62xwFoMje05ksknprhkc2H6ZdV/9M0utPvNJzrQfRqGYnbNUhz5cG18iU
GJTHR77jytLrDZV0QgXMLftrOck/k+ULlxspaBgygGdxUVUpueE6DVQKeknYhus/68Kypx
aYwh1/F0oHEdEZeXnqS8jyRttHZQE9G1eKQcabduCufwK9Y9FlSj4v0z72O3hsORmtWiZm
cYtVcxcYf9+BMAIdIHXofURx3arFNE5MG6chRSHlROEqGQAAAIBdqHN/9zuKD/lkQLpqvU
yoB8lUAaY087pUwOYmBiwy8YqVNCUlSDsvFeqK9VCdsPMoxriXNdSX+atiDqFPUjvuknBU
gkUzO7P7BQ9JM/NMQBHCt+i8eJNDyE+Y2epnKPwkufv4PuS6ms+I0Xc5quLOKEAjrxflLr
OdfeHrJilo0AAAAIEA9SAlrQ35+SBEhSo3wb5PmHi5peV5nNRNTO1uJuPsH2Z1SQx3hBsr
aJXxqnay5WernXdi/lq231dMEmYBPEp422mE0+Ss14oxLPVsttKU8dJrdfSAFLT/l+EDUZ
K1opEiJxGIq3lDNsZHQLGu8v7wS5m/cUZWoQW3wndbUshdPW0AAACBAOBVYKQnrE+AZr1X
1QUdOhz3UWKK+dTK0rakOKU1KBno1GQX1oy1NAwDwrdmpv//U0UVRz+RqtdOPXQ7gTVzW4
Lp0xdoNoEhsEIXPfQLV4EuzK+JUIleSnughO4FCTzwS7ImPt4zM6x0P+hbbe357ua2Yv2f
viCYj0cb6qWUGJaDAAAAAAEC
-----END OPENSSH PRIVATE KEY-----

Loading…
Cancel
Save