Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 79 additions & 21 deletions pkg/gswitch/ssh_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io"
"math/rand"
"net"
"strconv"
"sync/atomic"

"go.uber.org/zap"
Expand Down Expand Up @@ -56,36 +57,93 @@ func (c *connections) handleSSHConnection(ctx context.Context, tcpConn net.Conn,
go ssh.DiscardRequests(reqs)

for newChannel := range chans {
if newChannel.ChannelType() != "session" {
switch newChannel.ChannelType() {
case "session":
c.handleSessionChannel(ctx, newChannel, logger)
case "direct-tcpip":
c.handleDirectTCPIPChannel(ctx, newChannel, logger)
default:
newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
continue
}
}

channel, requests, err := newChannel.Accept()
if err != nil {
logger.Error("could not accept channel", zap.Error(err))
continue
}
return nil
}

func (c *connections) handleSessionChannel(ctx context.Context, newChannel ssh.NewChannel, logger *zap.Logger) {
channel, requests, err := newChannel.Accept()
if err != nil {
logger.Error("could not accept channel", zap.Error(err))
return
}

go func() {
defer channel.Close()

go func() {
defer channel.Close()

go func() {
for req := range requests {
req.Reply(req.Type == "shell" || req.Type == "pty-req", nil)
}
}()

logger.Debug("start CLI session")
session := NewCLISession(channel, c.username, c.password, logger, vendors["cisco"])
err := session.Run(ctx)
if err != nil {
logger.Debug("CLI session ended", zap.Error(err))
for req := range requests {
req.Reply(req.Type == "shell" || req.Type == "pty-req" || req.Type == "auth-agent-req@openssh.com", nil)
}
}()

logger.Debug("start CLI session")
session := NewCLISession(channel, c.username, c.password, logger, vendors["cisco"])
err := session.Run(ctx)
if err != nil {
logger.Debug("CLI session ended", zap.Error(err))
}
}()
}

type directTCPIPChannelData struct {
RAddr string
RPort uint32
LAddr string
LPort uint32
}

func (c *connections) handleDirectTCPIPChannel(ctx context.Context, newChannel ssh.NewChannel, logger *zap.Logger) {
var channelData directTCPIPChannelData
if err := ssh.Unmarshal(newChannel.ExtraData(), &channelData); err != nil {
newChannel.Reject(ssh.ConnectionFailed, fmt.Sprintf("invalid direct-tcpip data: %s", err))
return
}
remoteAddr := net.JoinHostPort(channelData.RAddr, strconv.Itoa(int(channelData.RPort)))
remoteConn, err := net.Dial("tcp", remoteAddr)
if err != nil {
newChannel.Reject(ssh.ConnectionFailed, err.Error())
return
}

return nil
channel, requests, err := newChannel.Accept()
if err != nil {
_ = remoteConn.Close()
logger.Error("could not accept direct-tcpip channel", zap.Error(err))
return
}
go ssh.DiscardRequests(requests)

go func() {
defer channel.Close()
defer remoteConn.Close()

logger.Debug("start direct-tcpip", zap.String("remote", remoteAddr))
copyDone := make(chan struct{}, 2)
go func() {
_, _ = io.Copy(channel, remoteConn)
copyDone <- struct{}{}
}()
go func() {
_, _ = io.Copy(remoteConn, channel)
copyDone <- struct{}{}
}()

select {
case <-ctx.Done():
case <-copyDone:
}
logger.Debug("direct-tcpip closed", zap.String("remote", remoteAddr))
}()
}

// TelnetConnection implements ssh.Channel over a Telnet TCP connection.
Expand Down
132 changes: 127 additions & 5 deletions pkg/server/devuath.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import (
"fmt"
"net/netip"
"os"
"strconv"
"strings"

"github.com/annetutil/gnetcli/pkg/credentials"
pb "github.com/annetutil/gnetcli/pkg/server/proto"
"github.com/kevinburke/ssh_config"
"go.uber.org/zap"
)

Expand Down Expand Up @@ -35,16 +38,15 @@ type proxyConfig struct {
func (m authApp) resolveProxyConfig(host string, ip netip.Addr) proxyConfig {
cfg := proxyConfig{ip: ip}

// Priority: explicit ProxyJump from app config > SSH config settings
// Priority: explicit ProxyJump from app config > SSH config settings.
if len(m.config.ProxyJump) > 0 {
cfg.proxyJump = m.config.ProxyJump
} else if m.config.SshConfig && m.sshConfig != nil {
// Read all SSH config settings when enabled
cfg.proxyJump = m.sshConfig.Get(host, "ProxyJump")
cfg.controlPath = m.sshConfig.Get(host, "ControlPath")
if realHost := m.sshConfig.Get(host, "Hostname"); len(realHost) > 0 {
cfg.connectHost = realHost
// Clear IP to ensure we connect to Hostname from config, not IP received from client
// Clear IP to ensure we connect to Hostname from config, not IP received from client.
cfg.ip = netip.Addr{}
}
}
Expand All @@ -57,6 +59,17 @@ func (m authApp) GetHostParams(host string, params *pb.HostParams) (hostParams,
return hostParams{}, fmt.Errorf("host connection params: %w", err)
}

if port == 0 && m.config.SshConfig && m.sshConfig != nil {
configPort := m.sshConfig.Get(host, "Port")
if len(configPort) > 0 {
parsedPort, err := strconv.Atoi(configPort)
if err != nil {
return hostParams{}, fmt.Errorf("invalid ssh config port for %s: %w", host, err)
}
port = parsedPort
}
}

cfg := m.resolveProxyConfig(host, ip)

creds, err := m.Get(host)
Expand All @@ -75,8 +88,7 @@ func (m authApp) GetHostParams(host string, params *pb.HostParams) (hostParams,
func (m authApp) Get(host string) (credentials.Credentials, error) {
if m.config.SshConfig {
sshConfigPassphrase := "" // TODO: pass it
// here we read ssh config each call
cred, err := BuildCredsFromSSHConfig(m.config.Login, m.config.Password.Value(), host, sshConfigPassphrase, m.config.PrivateKey, m.log)
cred, err := m.buildCredsFromSSHConfig(m.config.Login, m.config.Password.Value(), host, sshConfigPassphrase, m.config.PrivateKey)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -116,3 +128,113 @@ func NewAuthApp(config authAppConfig, logger *zap.Logger) authApp {
sshConfig: realSSHConfigReader{},
}
}

func NewAuthAppWithSSHConfig(config authAppConfig, logger *zap.Logger, sshConfigText string) (authApp, error) {
parsedConfig, err := ssh_config.Decode(strings.NewReader(sshConfigText))
if err != nil {
return authApp{}, err
}
return authApp{config: config, log: logger, sshConfig: parsedSSHConfigReader{config: parsedConfig}}, nil
}

func (m authApp) getSSHConfig(host, key string) string {
if m.sshConfig == nil {
return ""
}
return m.sshConfig.Get(host, key)
}

func (m authApp) getSSHConfigAll(host, key string) []string {
if m.sshConfig == nil {
return nil
}
return m.sshConfig.GetAll(host, key)
}

func (m authApp) buildCredsFromSSHConfig(login, password, host, sshConfigPassphrase, privateKeyPath string) (credentials.Credentials, error) {
var privateKeys [][]byte
if len(privateKeyPath) > 0 {
key, err := os.ReadFile(privateKeyPath)
if err != nil {
return nil, err
}
privateKeys = [][]byte{key}
} else {
var err error
privateKeys, err = m.getPrivateKeysFromSSHConfig(host)
if err != nil {
return nil, err
}
}
if len(login) == 0 {
configLogin := m.getSSHConfig(host, "User")
if len(configLogin) == 0 {
newLogin := credentials.GetLogin()
m.log.Debug("Use system login", zap.String("configLogin", newLogin))
login = newLogin
} else {
login = configLogin
m.log.Debug("Use login from config", zap.String("configLogin", configLogin))
}
} else {
m.log.Debug("Use login from input", zap.String("login", login))
}
agentSocket, err := m.getAgentSocketFromSSHConfig(host)
if err != nil {
return nil, err
}

opts := []credentials.CredentialsOption{
credentials.WithUsername(login),
credentials.WithLogger(m.log),
credentials.WithSSHAgentSocket(agentSocket),
}
if len(password) > 0 {
opts = append(opts, credentials.WithPassword(credentials.Secret(password)))
}
if len(privateKeys) > 0 {
opts = append(opts, credentials.WithPrivateKeys(privateKeys))
}
if len(sshConfigPassphrase) > 0 {
opts = append(opts, credentials.WithPassphrase(credentials.Secret(sshConfigPassphrase)))
}

return credentials.NewSimpleCredentials(opts...), nil
}

func (m authApp) getPrivateKeysFromSSHConfig(host string) ([][]byte, error) {
switch m.sshConfig.(type) {
case nil, realSSHConfigReader:
return credentials.GetPrivateKeysFromConfig(host)
}

identityFiles := m.getSSHConfigAll(host, "IdentityFile")
privKeys := make([][]byte, 0, len(identityFiles))
for _, v := range identityFiles {
content, err := os.ReadFile(v)
if err != nil {
return nil, fmt.Errorf("failed to read identity file %s: %w", v, err)
}
privKeys = append(privKeys, content)
}
return privKeys, nil
}

func (m authApp) getAgentSocketFromSSHConfig(host string) (string, error) {
switch m.sshConfig.(type) {
case nil, realSSHConfigReader:
return credentials.GetAgentSocketFromConfig(host)
}

ia := m.getSSHConfig(host, "IdentityAgent")
if ia == "none" {
return "", nil
}
if ia == "SSH_AUTH_SOCK" || len(ia) == 0 {
if m.getSSHConfig(host, "ForwardAgent") == "yes" || ia == "SSH_AUTH_SOCK" {
return credentials.GetDefaultAgentSocket(), nil
}
return "", nil
}
return ia, nil
}
8 changes: 7 additions & 1 deletion pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,11 @@ func (m *Server) createStreamerSSH(cfg StreamerConfig, add func(op gtrace.Operat
if len(jumpHostParams.controlPath) > 0 {
opts = append(opts, ssh.SSHTunnelWithControlFIle(jumpHostParams.controlPath))
}

if jumpHostParams.port > 0 {
opts = append(opts, ssh.SSHTunnelWithPort(jumpHostParams.port))
}
connHost = cfg.params.host

tun := ssh.NewSSHTunnel(jumpHostParams.host, jumpHostParams.GetCredentials(), opts...)

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
Expand Down Expand Up @@ -586,6 +589,9 @@ func (m *Server) getHostParams(hostname string, cmdParams *pb.HostParams) (hostP
if defaultHostParams.controlPath != "" {
res.controlPath = defaultHostParams.controlPath
}
if defaultHostParams.port != 0 {
res.port = defaultHostParams.port
}
if defaultHostParams.host != "" {
res.host = defaultHostParams.host
res.ip = defaultHostParams.ip
Expand Down
Loading
Loading