diff --git a/pkg/config/playbook.go b/pkg/config/playbook.go index a058bd14..387fe231 100644 --- a/pkg/config/playbook.go +++ b/pkg/config/playbook.go @@ -78,11 +78,12 @@ type Target struct { // Destination defines destination info type Destination struct { - Name string `yaml:"name" toml:"name"` - Host string `yaml:"host" toml:"host"` - Port int `yaml:"port" toml:"port"` - User string `yaml:"user" toml:"user"` - Tags []string `yaml:"tags" toml:"tags"` + Name string `yaml:"name" toml:"name"` + Host string `yaml:"host" toml:"host"` + Port int `yaml:"port" toml:"port"` + User string `yaml:"user" toml:"user"` + Tags []string `yaml:"tags" toml:"tags"` + ProxyCommand []string `yaml:"proxy_command" toml:"proxy_command"` } // Overrides defines override for task passed from cli diff --git a/pkg/executor/connector.go b/pkg/executor/connector.go index 49abf9d7..e51c1c6d 100644 --- a/pkg/executor/connector.go +++ b/pkg/executor/connector.go @@ -6,6 +6,7 @@ import ( "log" "net" "os" + "os/exec" "strings" "time" @@ -22,6 +23,31 @@ type Connector struct { logs Logs } +// In the ProxyCommand variables can be used %h, %p, %r (%r - username) +// before executing the command they needs to be replaced with the actual values +func SubstituteProxyCommand(username, address string, proxyCommand []string) ([]string, error) { + if len(proxyCommand) == 0 { + return []string{}, nil + } + + host, port, err := net.SplitHostPort(address) + if err != nil { + return nil, fmt.Errorf("failed to split hostAddr and port: %w", err) + } + + cmdArgs := make([]string, len(proxyCommand)) + + for i, arg := range proxyCommand { + arg = strings.Replace(arg, "%h", host, -1) + if port != "" { + arg = strings.Replace(arg, "%p", port, -1) + } + arg = strings.Replace(arg, "%r", username, -1) + cmdArgs[i] = arg + } + return cmdArgs, nil +} + // NewConnector creates a new Connector for a given user and private key. func NewConnector(privateKey string, timeout time.Duration, logs Logs) (res *Connector, err error) { res = &Connector{privateKey: privateKey, timeout: timeout, logs: logs} @@ -52,9 +78,9 @@ func (c *Connector) WithAgentForwarding() *Connector { } // Connect connects to a remote hostAddr and returns a remote executer, caller must close. -func (c *Connector) Connect(ctx context.Context, hostAddr, hostName, user string) (*Remote, error) { - log.Printf("[DEBUG] connect to %q (%s), user %q", hostAddr, hostName, user) - client, err := c.sshClient(ctx, hostAddr, user) +func (c *Connector) Connect(ctx context.Context, hostAddr, hostName, user string, proxyCommand []string) (*Remote, error) { + log.Printf("[DEBUG] connect to %q (%s), user %q, proxy command: %s", hostAddr, hostName, user, proxyCommand) + client, err := c.sshClient(ctx, hostAddr, user, proxyCommand) if err != nil { return nil, err } @@ -89,27 +115,67 @@ func (c *Connector) forwardAgent(client *ssh.Client) error { return nil } -func (c *Connector) sshClient(ctx context.Context, host, user string) (session *ssh.Client, err error) { +func sshDialWithProxy(ctx context.Context, host string, cmdArgs []string, config *ssh.ClientConfig) (*ssh.Client, error) { + + client, server := net.Pipe() + + cmd := exec.CommandContext(ctx, cmdArgs[0], cmdArgs[1:]...) + cmd.Stdin = server + cmd.Stdout = server + cmd.Stderr = os.Stderr + + if err := cmd.Start(); err != nil { + return nil, err + } + + ncc, chans, reqs, err := ssh.NewClientConn(client, host, config) + if err != nil { + return nil, err + } + + return ssh.NewClient(ncc, chans, reqs), nil + +} + +func (c *Connector) sshClient(ctx context.Context, host, user string, proxyCommand []string) (session *ssh.Client, err error) { + var conn net.Conn + var client *ssh.Client + log.Printf("[DEBUG] create ssh session to %s, user %s", host, user) + log.Printf("[DEBUG] ProxyCommand %s ", proxyCommand) if !strings.Contains(host, ":") { host += ":22" } - dialer := net.Dialer{Timeout: c.timeout} - conn, err := dialer.DialContext(ctx, "tcp", host) + cmdArgs, err := SubstituteProxyCommand(user, host, proxyCommand) if err != nil { - return nil, fmt.Errorf("failed to dial: %w", err) + return nil, fmt.Errorf("failed to parse proxy command: %w", err) } conf, err := c.sshConfig(user, c.privateKey) if err != nil { return nil, fmt.Errorf("failed to create ssh config: %w", err) } - ncc, chans, reqs, err := ssh.NewClientConn(conn, host, conf) - if err != nil { - return nil, fmt.Errorf("failed to create client connection to %s: %v", host, err) + + if len(proxyCommand) == 0 { + dialer := net.Dialer{Timeout: c.timeout} + conn, err = dialer.DialContext(ctx, "tcp", host) + if err != nil { + return nil, fmt.Errorf("failed to dial: %w", err) + } + + ncc, chans, reqs, err := ssh.NewClientConn(conn, host, conf) + if err != nil { + return nil, fmt.Errorf("failed to create client connection to %s: %v", host, err) + } + client = ssh.NewClient(ncc, chans, reqs) + + } else { + client, err = sshDialWithProxy(ctx, host, cmdArgs, conf) + if err != nil { + return nil, fmt.Errorf("failed to create client connection wtth proxy command %s, to %s: %v", proxyCommand, host, err) + } } - client := ssh.NewClient(ncc, chans, reqs) if err := c.forwardAgent(client); err != nil { return nil, fmt.Errorf("failed to forward agent to %s: %v", host, err) diff --git a/pkg/executor/connector_test.go b/pkg/executor/connector_test.go index 86a9453a..578e3647 100644 --- a/pkg/executor/connector_test.go +++ b/pkg/executor/connector_test.go @@ -2,6 +2,7 @@ package executor import ( "context" + "strings" "testing" "time" @@ -16,7 +17,7 @@ func TestConnector_Connect(t *testing.T) { t.Run("good connection", func(t *testing.T) { c, err := NewConnector("testdata/test_ssh_key", time.Second*10, MakeLogs(true, false, nil)) require.NoError(t, err) - sess, err := c.Connect(ctx, hostAndPort, "h1", "test") + sess, err := c.Connect(ctx, hostAndPort, "h1", "test", []string{}) require.NoError(t, err) defer sess.Close() }) @@ -24,7 +25,7 @@ func TestConnector_Connect(t *testing.T) { t.Run("bad user", func(t *testing.T) { c, err := NewConnector("testdata/test_ssh_key", time.Second*10, MakeLogs(true, false, nil)) require.NoError(t, err) - _, err = c.Connect(ctx, hostAndPort, "h1", "test33") + _, err = c.Connect(ctx, hostAndPort, "h1", "test33", []string{}) require.ErrorContains(t, err, "ssh: unable to authenticate") }) @@ -36,21 +37,129 @@ func TestConnector_Connect(t *testing.T) { t.Run("wrong port", func(t *testing.T) { c, err := NewConnector("testdata/test_ssh_key", time.Second*10, MakeLogs(true, false, nil)) require.NoError(t, err) - _, err = c.Connect(ctx, "127.0.0.1:12345", "h1", "test") + _, err = c.Connect(ctx, "127.0.0.1:12345", "h1", "test", []string{}) require.ErrorContains(t, err, "failed to dial: dial tcp 127.0.0.1:12345") }) t.Run("timeout", func(t *testing.T) { c, err := NewConnector("testdata/test_ssh_key", time.Nanosecond, MakeLogs(true, false, nil)) require.NoError(t, err) - _, err = c.Connect(ctx, hostAndPort, "h1", "test") + _, err = c.Connect(ctx, hostAndPort, "h1", "test", []string{}) require.ErrorContains(t, err, "i/o timeout") }) t.Run("unreachable host", func(t *testing.T) { c, err := NewConnector("testdata/test_ssh_key", time.Second, MakeLogs(true, false, nil)) require.NoError(t, err) - _, err = c.Connect(ctx, "10.255.255.1:22", "h1", "test") + _, err = c.Connect(ctx, "10.255.255.1:22", "h1", "test", []string{}) require.ErrorContains(t, err, "failed to dial: dial tcp 10.255.255.1:22: i/o timeout") }) } + +func TestConnector_ConnectWithProxy(t *testing.T) { + ctx := context.Background() + + bastionHostAndPort, _, teardown := start2TestContainers(t) + defer teardown() + + t.Run("good connection", func(t *testing.T) { + c, err := NewConnector("testdata/test_ssh_key", time.Second*10, MakeLogs(true, false, nil)) + require.NoError(t, err) + sess, err := c.Connect(ctx, bastionHostAndPort, "bastion-host", "test", []string{}) + require.NoError(t, err) + defer sess.Close() + }) + + // To test proxy command, the chain of connection will be next: + // localhost -> localhost: (this is also the bastion host) -> target-host:2222 + // In a real-world application, "target-host:2222" will be replaced with "%h:%p", but since + // testcontainers returns "localhost:" manually, overriding it. + + // "ssh -W" requires enabling AllowTcpForwarding, to enable it, modification was applied: + // see pkg/executor/remote_test.go, env variable DOCKER_MODS on test container. + // The "bastion-host" is a local host, and we are using a standard SSH client which tries to verify the host key; + // to bypass this check, "-o StrictHostKeyChecking=no” was added to the proxy command. + + // There is a situation that I am not sure if it is a bug or should be handled on client/spot side. + // If ssh server on proxy server works, but forbid TCP forwarding, go ssh client will connect but will not abort + // the connection or return error, it will just print to terminal + // "channel open failed: open failed: administratively prohibited: open failed". + + t.Run("good connection with proxy", func(t *testing.T) { + c, err := NewConnector("testdata/test_ssh_key", time.Second*10, MakeLogs(true, false, nil)) + require.NoError(t, err) + + bastionAddr := strings.Split(bastionHostAndPort, ":") + + proxyCommand := []string{ + "ssh", + "-W", + "target-host:2222", + "test@localhost", + "-p", + bastionAddr[1], + "-i", + "testdata/test_ssh_key", + "-o", + "StrictHostKeyChecking=no", + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + sess, err := c.Connect(ctx, "target-host:2222", "target-host", "test", proxyCommand) + require.NoError(t, err) + defer sess.Close() + }) + +} + +func TestSubstituteProxyCommand(t *testing.T) { + tests := []struct { + username string + address string + proxyCommand []string + expected []string + expectError bool + }{ + { + username: "user", + address: "example.com:22", + proxyCommand: []string{"ssh", "-W", "%h:%p", "%r@example.com"}, + expected: []string{"ssh", "-W", "example.com:22", "user@example.com"}, + expectError: false, + }, + { + username: "user", + address: "example.com:22", + proxyCommand: []string{"ssh", "-W", "%h:%p", "%r@example.com", "random arg with spaces"}, + expected: []string{"ssh", "-W", "example.com:22", "user@example.com", "random arg with spaces"}, + expectError: false, + }, + { + username: "user", + address: "example.com", + proxyCommand: []string{"ssh", "-W", "%h:%p", "%r@example.com"}, + expected: nil, + expectError: true, + }, + { + username: "user", + address: "example.com:22", + proxyCommand: []string{}, + expected: []string{}, + expectError: false, + }, + } + + for _, test := range tests { + t.Run(test.address, func(t *testing.T) { + result, err := SubstituteProxyCommand(test.username, test.address, test.proxyCommand) + if test.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, test.expected, result) + } + }) + } +} diff --git a/pkg/executor/remote_test.go b/pkg/executor/remote_test.go index 53a0eef2..6200859a 100644 --- a/pkg/executor/remote_test.go +++ b/pkg/executor/remote_test.go @@ -27,7 +27,7 @@ func TestExecuter_UploadAndDownload(t *testing.T) { c, err := NewConnector("testdata/test_ssh_key", time.Second*10, MakeLogs(true, false, nil)) require.NoError(t, err) - sess, err := c.Connect(ctx, hostAndPort, "h1", "test") + sess, err := c.Connect(ctx, hostAndPort, "h1", "test", []string{}) require.NoError(t, err) defer sess.Close() @@ -55,7 +55,7 @@ func TestExecuter_UploadGlobAndDownload(t *testing.T) { c, err := NewConnector("testdata/test_ssh_key", time.Second*10, MakeLogs(true, false, nil)) require.NoError(t, err) - sess, err := c.Connect(ctx, hostAndPort, "h1", "test") + sess, err := c.Connect(ctx, hostAndPort, "h1", "test", []string{}) require.NoError(t, err) defer sess.Close() @@ -113,7 +113,7 @@ func TestExecuter_Upload_FailedSourceNotFound(t *testing.T) { c, err := NewConnector("testdata/test_ssh_key", time.Second*10, MakeLogs(true, false, nil)) require.NoError(t, err) - sess, err := c.Connect(ctx, hostAndPort, "h1", "test") + sess, err := c.Connect(ctx, hostAndPort, "h1", "test", []string{}) require.NoError(t, err) defer sess.Close() @@ -129,7 +129,7 @@ func TestExecuter_Upload_FailedNoRemoteDir(t *testing.T) { c, err := NewConnector("testdata/test_ssh_key", time.Second*10, MakeLogs(true, false, nil)) require.NoError(t, err) - sess, err := c.Connect(ctx, hostAndPort, "h1", "test") + sess, err := c.Connect(ctx, hostAndPort, "h1", "test", []string{}) require.NoError(t, err) defer sess.Close() @@ -145,7 +145,7 @@ func TestExecuter_Upload_CantMakeRemoteDir(t *testing.T) { c, err := NewConnector("testdata/test_ssh_key", time.Second*10, MakeLogs(true, false, nil)) require.NoError(t, err) - sess, err := c.Connect(ctx, hostAndPort, "h1", "test") + sess, err := c.Connect(ctx, hostAndPort, "h1", "test", []string{}) require.NoError(t, err) defer sess.Close() @@ -161,7 +161,7 @@ func TestExecuter_Upload_Canceled(t *testing.T) { c, err := NewConnector("testdata/test_ssh_key", time.Second*10, MakeLogs(true, false, nil)) require.NoError(t, err) - sess, err := c.Connect(ctx, hostAndPort, "h1", "test") + sess, err := c.Connect(ctx, hostAndPort, "h1", "test", []string{}) require.NoError(t, err) defer sess.Close() @@ -178,7 +178,7 @@ func TestExecuter_UploadCanceledWithoutMkdir(t *testing.T) { c, err := NewConnector("testdata/test_ssh_key", time.Second*10, MakeLogs(true, false, nil)) require.NoError(t, err) - sess, err := c.Connect(ctx, hostAndPort, "h1", "test") + sess, err := c.Connect(ctx, hostAndPort, "h1", "test", []string{}) require.NoError(t, err) defer sess.Close() @@ -196,7 +196,7 @@ func TestUpload_UploadOverwriteWithAndWithoutForce(t *testing.T) { c, err := NewConnector("testdata/test_ssh_key", time.Second*10, MakeLogs(true, false, nil)) require.NoError(t, err) - sess, err := c.Connect(ctx, hostAndPort, "h1", "test") + sess, err := c.Connect(ctx, hostAndPort, "h1", "test", []string{}) require.NoError(t, err) defer sess.Close() @@ -229,7 +229,7 @@ func TestExecuter_ConnectCanceled(t *testing.T) { c, err := NewConnector("testdata/test_ssh_key", time.Second*10, MakeLogs(true, false, nil)) require.NoError(t, err) - _, err = c.Connect(ctx, hostAndPort, "h1", "test") + _, err = c.Connect(ctx, hostAndPort, "h1", "test", []string{}) assert.ErrorContains(t, err, "failed to dial: dial tcp: lookup localhost: i/o timeout") } @@ -242,7 +242,7 @@ func TestExecuter_Run(t *testing.T) { logs := MakeLogs(true, false, nil) c, err := NewConnector("testdata/test_ssh_key", time.Second*10, logs) require.NoError(t, err) - sess, err := c.Connect(ctx, hostAndPort, "h1", "test") + sess, err := c.Connect(ctx, hostAndPort, "h1", "test", []string{}) require.NoError(t, err) defer sess.Close() @@ -279,7 +279,7 @@ func TestExecuter_Run(t *testing.T) { capturedStdout := captureStdOut(t, func() { c, err := NewConnector("testdata/test_ssh_key", time.Second*10, MakeLogs(true, false, []string{"data2"})) require.NoError(t, err) - session, err := c.Connect(ctx, hostAndPort, "h1", "test") + session, err := c.Connect(ctx, hostAndPort, "h1", "test", []string{}) require.NoError(t, err) defer session.Close() @@ -322,7 +322,7 @@ func TestExecuter_Sync(t *testing.T) { c, err := NewConnector("testdata/test_ssh_key", time.Second*10, MakeLogs(true, false, nil)) require.NoError(t, err) - sess, err := c.Connect(ctx, hostAndPort, "h1", "test") + sess, err := c.Connect(ctx, hostAndPort, "h1", "test", []string{}) require.NoError(t, err) defer sess.Close() @@ -399,7 +399,7 @@ func TestExecuter_Delete(t *testing.T) { c, err := NewConnector("testdata/test_ssh_key", time.Second*10, MakeLogs(true, false, nil)) require.NoError(t, err) - sess, err := c.Connect(ctx, hostAndPort, "h1", "test") + sess, err := c.Connect(ctx, hostAndPort, "h1", "test", []string{}) require.NoError(t, err) defer sess.Close() @@ -460,7 +460,7 @@ func TestExecuter_DeleteWithExclude(t *testing.T) { c, err := NewConnector("testdata/test_ssh_key", time.Second*10, MakeLogs(true, false, nil)) require.NoError(t, err) - sess, err := c.Connect(ctx, hostAndPort, "h1", "test") + sess, err := c.Connect(ctx, hostAndPort, "h1", "test", []string{}) require.NoError(t, err) defer sess.Close() @@ -611,7 +611,7 @@ func Test_getRemoteFilesProperties(t *testing.T) { c, err := NewConnector("testdata/test_ssh_key", time.Second*10, MakeLogs(true, false, nil)) require.NoError(t, err) - sess, err := c.Connect(ctx, hostAndPort, "h1", "test") + sess, err := c.Connect(ctx, hostAndPort, "h1", "test", []string{}) require.NoError(t, err) defer sess.Close() @@ -685,3 +685,77 @@ func startTestContainer(t *testing.T) (hostAndPort string, teardown func()) { require.NoError(t, err) return fmt.Sprintf("%s:%s", host, port.Port()), func() { container.Terminate(ctx) } } + +func start2TestContainers(t *testing.T) (hostAndPort1, hostAndPort2 string, teardown func()) { + t.Helper() + ctx := context.Background() + pubKey, err := os.ReadFile("testdata/test_ssh_key.pub") + require.NoError(t, err) + + // Create a custom network + networkName := "test-network" + + networkRequest := testcontainers.NetworkRequest{ + Name: networkName, + CheckDuplicate: true, + } + network, err := testcontainers.GenericNetwork(ctx, testcontainers.GenericNetworkRequest{ + NetworkRequest: networkRequest, + }) + require.NoError(t, err) + + // Define the container request + containerRequest := func(name string) testcontainers.ContainerRequest { + return testcontainers.ContainerRequest{ + AlwaysPullImage: true, + Image: "lscr.io/linuxserver/openssh-server:latest", + ExposedPorts: []string{"2222/tcp"}, + WaitingFor: wait.NewLogStrategy("done.").WithStartupTimeout(time.Second * 60), + Networks: []string{networkName}, + NetworkAliases: map[string][]string{networkName: {name}}, + Hostname: name, + Files: []testcontainers.ContainerFile{ + {HostFilePath: "testdata/test_ssh_key.pub", ContainerFilePath: "/authorized_key"}, + }, + Env: map[string]string{ + "PUBLIC_KEY": string(pubKey), + "USER_NAME": "test", + "TZ": "Etc/UTC", + "DOCKER_MODS": "linuxserver/mods:openssh-server-ssh-tunnel", + }, + } + } + + // Start the bastion container + container1, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: containerRequest("bastion-host"), + Started: true, + }) + require.NoError(t, err) + + // Start the container with final ssh connection + container2, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: containerRequest("target-host"), + Started: true, + }) + require.NoError(t, err) + + // Get the host and port for both containers + host1, err := container1.Host(ctx) + require.NoError(t, err) + port1, err := container1.MappedPort(ctx, "2222") + require.NoError(t, err) + + host2, err := container2.Host(ctx) + require.NoError(t, err) + port2, err := container2.MappedPort(ctx, "2222") + require.NoError(t, err) + + teardown = func() { + container1.Terminate(ctx) + container2.Terminate(ctx) + network.Remove(ctx) + } + + return fmt.Sprintf("%s:%s", host1, port1.Port()), fmt.Sprintf("%s:%s", host2, port2.Port()), teardown +} diff --git a/pkg/runner/commands_test.go b/pkg/runner/commands_test.go index 88abc74c..e38c6486 100644 --- a/pkg/runner/commands_test.go +++ b/pkg/runner/commands_test.go @@ -164,7 +164,7 @@ func Test_execCmd(t *testing.T) { ctx := context.Background() connector, connErr := executor.NewConnector("testdata/test_ssh_key", time.Second*10, logs) require.NoError(t, connErr) - sess, errSess := connector.Connect(ctx, testingHostAndPort, "my-hostAddr", "test") + sess, errSess := connector.Connect(ctx, testingHostAndPort, "my-hostAddr", "test", []string{}) require.NoError(t, errSess) t.Run("copy a single file", func(t *testing.T) { @@ -427,7 +427,7 @@ func Test_execCmdWithTmp(t *testing.T) { logs := executor.MakeLogs(false, false, nil) connector, connErr := executor.NewConnector("testdata/test_ssh_key", time.Second*10, logs) require.NoError(t, connErr) - sess, errSess := connector.Connect(ctx, testingHostAndPort, "my-hostAddr", "test") + sess, errSess := connector.Connect(ctx, testingHostAndPort, "my-hostAddr", "test", []string{}) require.NoError(t, errSess) extractTmpPath := func(log string) string { diff --git a/pkg/runner/mocks/connector.go b/pkg/runner/mocks/connector.go index b3e19ee9..348b1c2e 100644 --- a/pkg/runner/mocks/connector.go +++ b/pkg/runner/mocks/connector.go @@ -47,7 +47,7 @@ type ConnectorMock struct { } // Connect calls ConnectFunc. -func (mock *ConnectorMock) Connect(ctx context.Context, hostAddr string, hostName string, user string) (*executor.Remote, error) { +func (mock *ConnectorMock) Connect(ctx context.Context, hostAddr, hostName, user string, proxyCommand []string) (*executor.Remote, error) { if mock.ConnectFunc == nil { panic("ConnectorMock.ConnectFunc: method is nil but Connector.Connect was just called") } diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go index 8ce85af9..d9e9bcf9 100644 --- a/pkg/runner/runner.go +++ b/pkg/runner/runner.go @@ -43,7 +43,7 @@ type Process struct { // Connector is an interface for connecting to a host, and returning remote executer. type Connector interface { - Connect(ctx context.Context, hostAddr, hostName, user string) (*executor.Remote, error) + Connect(ctx context.Context, hostAddr, hostName, user string, proxyCommand []string) (*executor.Remote, error) } // Playbook is an interface for getting task and target information from playbook. @@ -100,7 +100,8 @@ func (p *Process) Run(ctx context.Context, task, target string) (s ProcResp, err if tsk.User != "" { user = tsk.User // override user from task if any set } - resp, e := p.runTaskOnHost(ctx, tsk, fmt.Sprintf("%s:%d", host.Host, host.Port), host.Name, user) + + resp, e := p.runTaskOnHost(ctx, tsk, fmt.Sprintf("%s:%d", host.Host, host.Port), host.Name, user, host.ProxyCommand) if i == 0 { atomic.AddInt32(&commands, int32(resp.count)) } @@ -172,7 +173,7 @@ func (p *Process) Gen(targets []string, tmplRdr io.Reader, respWr io.Writer) err // runTaskOnHost executes all commands of a task on a target host. hostAddr can be a remote host or localhost with port. // returns number of executed commands, vars from all commands and error if any. -func (p *Process) runTaskOnHost(ctx context.Context, tsk *config.Task, hostAddr, hostName, user string) (taskOnHostResp, error) { +func (p *Process) runTaskOnHost(ctx context.Context, tsk *config.Task, hostAddr, hostName, user string, proxyCommand []string) (taskOnHostResp, error) { report := func(hostAddr, hostName, f string, vals ...any) { p.Logs.WithHost(hostAddr, hostName).Info.Printf(f, vals...) } @@ -184,7 +185,7 @@ func (p *Process) runTaskOnHost(ctx context.Context, tsk *config.Task, hostAddr, if p.anyRemoteCommand(tsk) { // make remote executor only if there is a remote command in the taks var err error - remote, err = p.Connector.Connect(ctx, hostAddr, hostName, user) + remote, err = p.Connector.Connect(ctx, hostAddr, hostName, user, proxyCommand) if err != nil { if hostName != "" { return taskOnHostResp{}, fmt.Errorf("can't connect to %s, user: %s: %w", hostName, user, err)