mirror of
https://github.com/bpg/terraform-provider-proxmox.git
synced 2025-06-29 18:21:10 +00:00
* chore(deps): update golangci/golangci-lint (v2.0.2 → v2.1.2) | datasource | package | from | to | | --------------- | ---------------------- | ------ | ------ | | github-releases | golangci/golangci-lint | v2.0.2 | v2.1.2 | * fix linter errors Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> --------- Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
689 lines
18 KiB
Go
689 lines
18 KiB
Go
/*
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
*/
|
|
|
|
package ssh
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/terraform-plugin-log/tflog"
|
|
"github.com/pkg/sftp"
|
|
"github.com/skeema/knownhosts"
|
|
"golang.org/x/crypto/ssh"
|
|
"golang.org/x/crypto/ssh/agent"
|
|
"golang.org/x/net/proxy"
|
|
|
|
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
|
|
"github.com/bpg/terraform-provider-proxmox/utils"
|
|
)
|
|
|
|
const (
|
|
// TrySudo is a shell function that tries to execute a command with sudo if the user has sudo permissions.
|
|
TrySudo = `try_sudo(){ if [ $(sudo -n pvesm apiinfo 2>&1 | grep "APIVER" | wc -l) -gt 0 ]; then sudo $1; else $1; fi }`
|
|
)
|
|
|
|
// NewErrUserHasNoPermission creates a new error indicating that the SSH user does not have required permissions.
|
|
func NewErrUserHasNoPermission(username string) error {
|
|
return fmt.Errorf("the SSH user '%s' does not have required permissions. "+
|
|
"Make sure 'sudo' is installed and the user is configured in sudoers file. "+
|
|
"Refer to the documentation for more details", username)
|
|
}
|
|
|
|
// Client is an interface for performing SSH requests against the Proxmox Nodes.
|
|
type Client interface {
|
|
// Username returns the SSH username.
|
|
Username() string
|
|
|
|
// ExecuteNodeCommands executes a command on a node.
|
|
ExecuteNodeCommands(ctx context.Context, nodeName string, commands []string) ([]byte, error)
|
|
|
|
// NodeUpload uploads a file to a node.
|
|
NodeUpload(ctx context.Context, nodeName string,
|
|
remoteFileDir string, fileUploadRequest *api.FileUploadRequest) error
|
|
|
|
// NodeStreamUpload uploads a file to a node by streaming its content over SSH.
|
|
NodeStreamUpload(ctx context.Context, nodeName string,
|
|
remoteFileDir string, fileUploadRequest *api.FileUploadRequest) error
|
|
}
|
|
|
|
type client struct {
|
|
username string
|
|
password string
|
|
agent bool
|
|
agentSocket string
|
|
privateKey string
|
|
socks5Server string
|
|
socks5Username string
|
|
socks5Password string
|
|
nodeResolver NodeResolver
|
|
}
|
|
|
|
// NewClient creates a new SSH client.
|
|
func NewClient(
|
|
username string, password string,
|
|
agent bool, agentSocket string,
|
|
privateKey string,
|
|
socks5Server string, socks5Username string, socks5Password string,
|
|
nodeResolver NodeResolver,
|
|
) (Client, error) {
|
|
if agent &&
|
|
runtime.GOOS != "linux" &&
|
|
runtime.GOOS != "darwin" &&
|
|
runtime.GOOS != "freebsd" &&
|
|
runtime.GOOS != "windows" {
|
|
return nil, errors.New(
|
|
"the ssh agent flag is only supported on POSIX and Windows systems, please set it to 'false'" +
|
|
" or remove it from your provider configuration",
|
|
)
|
|
}
|
|
|
|
if (socks5Username != "" || socks5Password != "") && socks5Server == "" {
|
|
return nil, errors.New("socks5 server is required when socks5 username or password is set")
|
|
}
|
|
|
|
if nodeResolver == nil {
|
|
return nil, errors.New("node resolver is required")
|
|
}
|
|
|
|
return &client{
|
|
username: username,
|
|
password: password,
|
|
agent: agent,
|
|
agentSocket: agentSocket,
|
|
privateKey: privateKey,
|
|
socks5Server: socks5Server,
|
|
socks5Username: socks5Username,
|
|
socks5Password: socks5Password,
|
|
nodeResolver: nodeResolver,
|
|
}, nil
|
|
}
|
|
|
|
func (c *client) Username() string {
|
|
return c.username
|
|
}
|
|
|
|
// ExecuteNodeCommands executes commands on a given node.
|
|
func (c *client) ExecuteNodeCommands(ctx context.Context, nodeName string, commands []string) ([]byte, error) {
|
|
node, err := c.nodeResolver.Resolve(ctx, nodeName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to find node endpoint: %w", err)
|
|
}
|
|
|
|
tflog.Debug(ctx, "executing commands on the node using SSH", map[string]interface{}{
|
|
"node_address": node.Address,
|
|
"node_port": node.Port,
|
|
"commands": commands,
|
|
})
|
|
|
|
sshClient, err := c.openNodeShell(ctx, node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer func(sshClient *ssh.Client) {
|
|
e := sshClient.Close()
|
|
if e != nil {
|
|
tflog.Warn(ctx, "failed to close SSH client", map[string]interface{}{
|
|
"error": e,
|
|
})
|
|
}
|
|
}(sshClient)
|
|
|
|
output, err := c.executeCommands(ctx, sshClient, commands)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return output, nil
|
|
}
|
|
|
|
func (c *client) executeCommands(ctx context.Context, sshClient *ssh.Client, commands []string) ([]byte, error) {
|
|
sshSession, err := sshClient.NewSession()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create SSH session: %w", err)
|
|
}
|
|
|
|
defer func(session *ssh.Session) {
|
|
e := session.Close()
|
|
if e != nil && !errors.Is(e, io.EOF) {
|
|
tflog.Warn(ctx, "failed to close SSH session", map[string]interface{}{
|
|
"error": e,
|
|
})
|
|
}
|
|
}(sshSession)
|
|
|
|
script := strings.Join(commands, "; ")
|
|
|
|
output, err := sshSession.CombinedOutput(
|
|
fmt.Sprintf(
|
|
// explicitly use bash to support shell features like pipes and var assignment
|
|
"/bin/bash -c '%s'",
|
|
// shell script escaping for single quotes
|
|
strings.ReplaceAll(script, `'`, `'"'"'`),
|
|
),
|
|
)
|
|
if err != nil {
|
|
return nil, errors.New(string(output))
|
|
}
|
|
|
|
return output, nil
|
|
}
|
|
|
|
func (c *client) NodeUpload(
|
|
ctx context.Context,
|
|
nodeName string,
|
|
remoteFileDir string,
|
|
d *api.FileUploadRequest,
|
|
) error {
|
|
ip, err := c.nodeResolver.Resolve(ctx, nodeName)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to find node endpoint: %w", err)
|
|
}
|
|
|
|
tflog.Debug(ctx, "uploading file to the node datastore using SFTP", map[string]interface{}{
|
|
"node_address": ip,
|
|
"remote_dir": remoteFileDir,
|
|
"file_name": d.FileName,
|
|
"content_type": d.ContentType,
|
|
})
|
|
|
|
fileInfo, err := d.File.Stat()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get file info: %w", err)
|
|
}
|
|
|
|
fileSize := fileInfo.Size()
|
|
|
|
sshClient, err := c.openNodeShell(ctx, ip)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open SSH client: %w", err)
|
|
}
|
|
|
|
defer func(sshClient *ssh.Client) {
|
|
e := sshClient.Close()
|
|
if e != nil {
|
|
tflog.Warn(ctx, "failed to close SSH client", map[string]interface{}{
|
|
"error": e,
|
|
})
|
|
}
|
|
}(sshClient)
|
|
|
|
if d.ContentType != "" {
|
|
remoteFileDir = filepath.Join(remoteFileDir, d.ContentType)
|
|
}
|
|
|
|
remoteFilePath := strings.ReplaceAll(filepath.Join(remoteFileDir, d.FileName), `\`, "/")
|
|
|
|
sftpClient, err := sftp.NewClient(sshClient)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create SFTP client: %w", err)
|
|
}
|
|
|
|
defer func(sftpClient *sftp.Client) {
|
|
e := sftpClient.Close()
|
|
if e != nil {
|
|
tflog.Warn(ctx, "failed to close SFTP client", map[string]interface{}{
|
|
"error": e,
|
|
})
|
|
}
|
|
}(sftpClient)
|
|
|
|
err = sftpClient.MkdirAll(remoteFileDir)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create directory %s: %w", remoteFileDir, err)
|
|
}
|
|
|
|
remoteFile, err := sftpClient.Create(remoteFilePath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create file %s: %w", remoteFilePath, err)
|
|
}
|
|
|
|
defer func(remoteFile *sftp.File) {
|
|
e := remoteFile.Close()
|
|
if e != nil {
|
|
tflog.Warn(ctx, "failed to close remote file", map[string]interface{}{
|
|
"error": e,
|
|
})
|
|
}
|
|
}(remoteFile)
|
|
|
|
bytesUploaded, err := remoteFile.ReadFrom(d.File)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to upload file %s: %w", remoteFilePath, err)
|
|
}
|
|
|
|
if bytesUploaded != fileSize {
|
|
return fmt.Errorf("failed to upload file %s: uploaded %d bytes, expected %d bytes",
|
|
remoteFilePath, bytesUploaded, fileSize)
|
|
}
|
|
|
|
tflog.Debug(ctx, "uploaded file to datastore", map[string]interface{}{
|
|
"remote_file_path": remoteFilePath,
|
|
"size": bytesUploaded,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *client) NodeStreamUpload(
|
|
ctx context.Context,
|
|
nodeName string,
|
|
remoteFileDir string,
|
|
d *api.FileUploadRequest,
|
|
) error {
|
|
ip, err := c.nodeResolver.Resolve(ctx, nodeName)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to find node endpoint: %w", err)
|
|
}
|
|
|
|
tflog.Debug(ctx, "uploading file to the node datastore via SSH input stream ", map[string]interface{}{
|
|
"node_address": ip,
|
|
"remote_dir": remoteFileDir,
|
|
"file_name": d.FileName,
|
|
"content_type": d.ContentType,
|
|
})
|
|
|
|
fileInfo, err := d.File.Stat()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get file info: %w", err)
|
|
}
|
|
|
|
fileSize := fileInfo.Size()
|
|
|
|
sshClient, err := c.openNodeShell(ctx, ip)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open SSH client: %w", err)
|
|
}
|
|
|
|
defer func(sshClient *ssh.Client) {
|
|
e := sshClient.Close()
|
|
if e != nil {
|
|
tflog.Warn(ctx, "failed to close SSH client", map[string]interface{}{
|
|
"error": e,
|
|
})
|
|
}
|
|
}(sshClient)
|
|
|
|
if d.ContentType != "" {
|
|
remoteFileDir = filepath.Join(remoteFileDir, d.ContentType)
|
|
}
|
|
|
|
remoteFilePath := strings.ReplaceAll(filepath.Join(remoteFileDir, d.FileName), `\`, "/")
|
|
|
|
err = c.uploadFile(ctx, sshClient, d, remoteFilePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = c.checkUploadedFile(ctx, sshClient, remoteFilePath, fileSize)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if d.Mode != "" {
|
|
parsedFileMode, parseErr := strconv.ParseUint(d.Mode, 8, 12)
|
|
if parseErr != nil {
|
|
return fmt.Errorf("failed to parse file mode %q: %w", d.Mode, parseErr)
|
|
}
|
|
|
|
mode := uint32(parsedFileMode)
|
|
if err = c.changeModeUploadedFile(ctx, sshClient, remoteFilePath, os.FileMode(mode)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
tflog.Debug(ctx, "uploaded file to datastore", map[string]interface{}{
|
|
"remote_file_path": remoteFilePath,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *client) uploadFile(
|
|
ctx context.Context,
|
|
sshClient *ssh.Client,
|
|
req *api.FileUploadRequest,
|
|
remoteFilePath string,
|
|
) error {
|
|
sshSession, err := sshClient.NewSession()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create SSH session: %w", err)
|
|
}
|
|
|
|
defer func(session *ssh.Session) {
|
|
e := session.Close()
|
|
if e != nil && !errors.Is(e, io.EOF) {
|
|
tflog.Warn(ctx, "failed to close SSH session", map[string]interface{}{
|
|
"error": e,
|
|
})
|
|
}
|
|
}(sshSession)
|
|
|
|
sshSession.Stdin = req.File
|
|
|
|
output, err := sshSession.CombinedOutput(
|
|
fmt.Sprintf(`%s; try_sudo "/usr/bin/tee %s"`, TrySudo, remoteFilePath),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("error transferring file: %s", string(output))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *client) checkUploadedFile(
|
|
ctx context.Context,
|
|
sshClient *ssh.Client,
|
|
remoteFilePath string,
|
|
fileSize int64,
|
|
) error {
|
|
sftpClient, err := sftp.NewClient(sshClient)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create SFTP client: %w", err)
|
|
}
|
|
|
|
defer func(sftpClient *sftp.Client) {
|
|
e := sftpClient.Close()
|
|
if e != nil {
|
|
tflog.Warn(ctx, "failed to close SFTP client", map[string]interface{}{
|
|
"error": e,
|
|
})
|
|
}
|
|
}(sftpClient)
|
|
|
|
remoteFile, err := sftpClient.Open(remoteFilePath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open remote file %s: %w", remoteFilePath, err)
|
|
}
|
|
|
|
remoteStat, err := remoteFile.Stat()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read remote file %s: %w", remoteFilePath, err)
|
|
}
|
|
|
|
bytesUploaded := remoteStat.Size()
|
|
if bytesUploaded != fileSize {
|
|
return fmt.Errorf("failed to upload file %s: uploaded %d bytes, expected %d bytes",
|
|
remoteFilePath, bytesUploaded, fileSize)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *client) changeModeUploadedFile(
|
|
ctx context.Context,
|
|
sshClient *ssh.Client,
|
|
remoteFilePath string,
|
|
fileMode os.FileMode,
|
|
) error {
|
|
sftpClient, err := sftp.NewClient(sshClient)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create SFTP client: %w", err)
|
|
}
|
|
|
|
defer func(sftpClient *sftp.Client) {
|
|
e := sftpClient.Close()
|
|
if e != nil {
|
|
tflog.Warn(ctx, "failed to close SFTP client", map[string]interface{}{
|
|
"error": e,
|
|
})
|
|
}
|
|
}(sftpClient)
|
|
|
|
remoteFile, err := sftpClient.Open(remoteFilePath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open remote file %s: %w", remoteFilePath, err)
|
|
}
|
|
|
|
remoteStat, err := remoteFile.Stat()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read remote file %s: %w", remoteFilePath, err)
|
|
}
|
|
|
|
if err = sftpClient.Chmod(remoteFilePath, fileMode); err != nil {
|
|
return fmt.Errorf("failed to change file mode of remote file from %#o (%s) to %#o (%s): %w",
|
|
remoteStat.Mode().Perm(), remoteStat.Mode(), fileMode.Perm(), fileMode, err)
|
|
}
|
|
|
|
tflog.Debug(ctx, "changed mode of uploaded file", map[string]interface{}{
|
|
"before": fmt.Sprintf("%#o (%s)", remoteStat.Mode().Perm(), remoteStat.Mode()),
|
|
"after": fmt.Sprintf("%#o (%s)", fileMode.Perm(), fileMode),
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// openNodeShell establishes a new SSH connection to a node.
|
|
func (c *client) openNodeShell(ctx context.Context, node ProxmoxNode) (*ssh.Client, error) {
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to determine the home directory: %w", err)
|
|
}
|
|
|
|
var sshHost string
|
|
if strings.Contains(node.Address, ":") {
|
|
// IPv6
|
|
sshHost = fmt.Sprintf("[%s]:%d", node.Address, node.Port)
|
|
} else {
|
|
// IPv4
|
|
sshHost = fmt.Sprintf("%s:%d", node.Address, node.Port)
|
|
}
|
|
|
|
sshPath := path.Join(homeDir, ".ssh")
|
|
if _, err = os.Stat(sshPath); os.IsNotExist(err) {
|
|
e := os.Mkdir(sshPath, 0o700)
|
|
if e != nil && !os.IsExist(e) {
|
|
return nil, fmt.Errorf("failed to create %s: %w", sshPath, e)
|
|
}
|
|
}
|
|
|
|
khPath := path.Join(sshPath, "known_hosts")
|
|
if _, err = os.Stat(khPath); os.IsNotExist(err) {
|
|
e := os.WriteFile(khPath, []byte{}, 0o600)
|
|
if e != nil {
|
|
return nil, fmt.Errorf("failed to create %s: %w", khPath, e)
|
|
}
|
|
}
|
|
|
|
kh, err := knownhosts.New(khPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read %s: %w", khPath, err)
|
|
}
|
|
|
|
// Create a custom permissive host key callback which still errors on hosts
|
|
// with changed keys, but allows unknown hosts and adds them to known_hosts
|
|
cb := ssh.HostKeyCallback(func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
|
khErr := kh(hostname, remote, key)
|
|
if knownhosts.IsHostKeyChanged(khErr) {
|
|
return fmt.Errorf("REMOTE HOST IDENTIFICATION HAS CHANGED for host %s! This may indicate a MitM attack", hostname)
|
|
}
|
|
|
|
if knownhosts.IsHostUnknown(khErr) {
|
|
f, fErr := os.OpenFile(khPath, os.O_APPEND|os.O_WRONLY, 0o600)
|
|
if fErr == nil {
|
|
defer utils.CloseOrLogError(ctx)(f)
|
|
fErr = knownhosts.WriteKnownHost(f, hostname, remote, key)
|
|
}
|
|
|
|
if fErr == nil {
|
|
tflog.Info(ctx, fmt.Sprintf("Added host %s to known_hosts", hostname))
|
|
} else {
|
|
tflog.Error(ctx, fmt.Sprintf("Failed to add host %s to known_hosts", hostname), map[string]interface{}{
|
|
"error": khErr,
|
|
})
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
return khErr
|
|
})
|
|
|
|
tflog.Info(ctx, fmt.Sprintf("agent is set to %t", c.agent))
|
|
|
|
var sshClient *ssh.Client
|
|
if c.agent {
|
|
sshClient, err = c.createSSHClientAgent(ctx, cb, kh, sshHost)
|
|
if err == nil {
|
|
return sshClient, nil
|
|
}
|
|
|
|
tflog.Error(ctx, "Failed SSH connection through agent",
|
|
map[string]interface{}{
|
|
"error": err,
|
|
})
|
|
}
|
|
|
|
if c.privateKey != "" {
|
|
sshClient, err = c.createSSHClientWithPrivateKey(ctx, cb, kh, sshHost)
|
|
if err == nil {
|
|
return sshClient, nil
|
|
}
|
|
|
|
tflog.Error(ctx, "Failed SSH connection with private key",
|
|
map[string]interface{}{
|
|
"error": err,
|
|
})
|
|
}
|
|
|
|
tflog.Info(ctx, "Falling back to password authentication for SSH connection")
|
|
|
|
sshClient, err = c.createSSHClient(ctx, cb, kh, sshHost)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to authenticate user %q over SSH to %q. Please verify that ssh-agent is "+
|
|
"correctly loaded with an authorized key via 'ssh-add -L' (NOTE: configurations in ~/.ssh/config are "+
|
|
"not considered by the provider): %w", c.username, sshHost, err)
|
|
}
|
|
|
|
return sshClient, nil
|
|
}
|
|
|
|
func (c *client) createSSHClient(
|
|
ctx context.Context,
|
|
cb ssh.HostKeyCallback,
|
|
kh knownhosts.HostKeyCallback,
|
|
sshHost string,
|
|
) (*ssh.Client, error) {
|
|
if c.password == "" {
|
|
tflog.Error(ctx, "Using password authentication fallback for SSH connection, but the SSH password is empty")
|
|
}
|
|
|
|
sshConfig := &ssh.ClientConfig{
|
|
User: c.username,
|
|
Auth: []ssh.AuthMethod{ssh.Password(c.password)},
|
|
HostKeyCallback: cb,
|
|
HostKeyAlgorithms: kh.HostKeyAlgorithms(sshHost),
|
|
}
|
|
|
|
return c.connect(ctx, sshHost, sshConfig)
|
|
}
|
|
|
|
// createSSHClientAgent establishes an ssh connection through the agent authentication mechanism.
|
|
func (c *client) createSSHClientAgent(
|
|
ctx context.Context,
|
|
cb ssh.HostKeyCallback,
|
|
kh knownhosts.HostKeyCallback,
|
|
sshHost string,
|
|
) (*ssh.Client, error) {
|
|
conn, err := dialSocket(c.agentSocket)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed connecting to SSH auth socket '%s': %w", c.agentSocket, err)
|
|
}
|
|
|
|
ag := agent.NewClient(conn)
|
|
|
|
sshConfig := &ssh.ClientConfig{
|
|
User: c.username,
|
|
Auth: []ssh.AuthMethod{ssh.PublicKeysCallback(ag.Signers), ssh.Password(c.password)},
|
|
HostKeyCallback: cb,
|
|
HostKeyAlgorithms: kh.HostKeyAlgorithms(sshHost),
|
|
}
|
|
|
|
return c.connect(ctx, sshHost, sshConfig)
|
|
}
|
|
|
|
func (c *client) createSSHClientWithPrivateKey(
|
|
ctx context.Context,
|
|
cb ssh.HostKeyCallback,
|
|
kh knownhosts.HostKeyCallback,
|
|
sshHost string,
|
|
) (*ssh.Client, error) {
|
|
privateKey, err := ssh.ParsePrivateKey([]byte(c.privateKey))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse private key: %w", err)
|
|
}
|
|
|
|
sshConfig := &ssh.ClientConfig{
|
|
User: c.username,
|
|
Auth: []ssh.AuthMethod{ssh.PublicKeys(privateKey)},
|
|
HostKeyCallback: cb,
|
|
HostKeyAlgorithms: kh.HostKeyAlgorithms(sshHost),
|
|
}
|
|
|
|
return c.connect(ctx, sshHost, sshConfig)
|
|
}
|
|
|
|
func (c *client) connect(ctx context.Context, sshHost string, sshConfig *ssh.ClientConfig) (*ssh.Client, error) {
|
|
if c.socks5Server != "" {
|
|
sshClient, err := c.socks5SSHClient(sshHost, sshConfig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to dial %s via SOCKS5 proxy %s: %w", sshHost, c.socks5Server, err)
|
|
}
|
|
|
|
tflog.Debug(ctx, "SSH connection via SOCKS5 established", map[string]interface{}{
|
|
"host": sshHost,
|
|
"socks5_server": c.socks5Server,
|
|
"user": c.username,
|
|
})
|
|
|
|
return sshClient, nil
|
|
}
|
|
|
|
sshClient, err := ssh.Dial("tcp", sshHost, sshConfig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to dial %s: %w", sshHost, err)
|
|
}
|
|
|
|
tflog.Debug(ctx, "SSH connection established", map[string]interface{}{
|
|
"host": sshHost,
|
|
"user": c.username,
|
|
})
|
|
|
|
return sshClient, nil
|
|
}
|
|
|
|
func (c *client) socks5SSHClient(sshServerAddress string, sshConfig *ssh.ClientConfig) (*ssh.Client, error) {
|
|
dialer, err := proxy.SOCKS5("tcp", c.socks5Server, &proxy.Auth{
|
|
User: c.socks5Username,
|
|
Password: c.socks5Password,
|
|
}, proxy.Direct)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create SOCKS5 proxy dialer: %w", err)
|
|
}
|
|
|
|
conn, err := dialer.Dial("tcp", sshServerAddress)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to dial %s via SOCKS5 proxy %s: %w", sshServerAddress, c.socks5Server, err)
|
|
}
|
|
|
|
sshConn, ch, reqs, err := ssh.NewClientConn(conn, sshServerAddress, sshConfig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create SSH client connection: %w", err)
|
|
}
|
|
|
|
return ssh.NewClient(sshConn, ch, reqs), nil
|
|
}
|