Skip to content

Commit a6fd933

Browse files
committed
pkg/hostagent: Use in-process SSH client on executing requirement scripts
Use an in-process SSH client on executing requirement scripts other than starting an SSH ControlMaster process. To fall back to external SSH, add the `LIMA_EXTERNAL_SSH_REQUIREMENT` environment variable. - pkg/sshutil: Add `ExecuteScriptViaInProcessClient()` Signed-off-by: Norio Nomura <norio.nomura@gmail.com> # Conflicts: # pkg/sshutil/sshutil.go # Conflicts: # pkg/sshutil/sshutil.go
1 parent 0250449 commit a6fd933

File tree

3 files changed

+121
-17
lines changed

3 files changed

+121
-17
lines changed

pkg/hostagent/requirements.go

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ package hostagent
66
import (
77
"errors"
88
"fmt"
9+
"os"
910
"runtime"
11+
"strconv"
1012
"strings"
13+
"sync"
1114
"time"
1215

1316
"github.com/lima-vm/sshocker/pkg/ssh"
@@ -103,39 +106,65 @@ func (a *HostAgent) waitForRequirement(r requirement) error {
103106
if err != nil {
104107
return err
105108
}
109+
var stdout, stderr string
106110
sshConfig := a.sshConfig
107-
if r.noMaster || runtime.GOOS == "windows" {
108-
// Remove ControlMaster, ControlPath, and ControlPersist options,
109-
// because Cygwin-based SSH clients do not support multiplexing when executing commands.
110-
// References:
111-
// https://inbox.sourceware.org/cygwin/c98988a5-7e65-4282-b2a1-bb8e350d5fab@acm.org/T/
112-
// https://stackoverflow.com/questions/20959792/is-ssh-controlmaster-with-cygwin-on-windows-actually-possible
113-
// By removing these options:
114-
// - Avoids execution failures when the control master is not yet available.
115-
// - Prevents error messages such as:
116-
// > mux_client_request_session: read from master failed: Connection reset by peer
117-
// > ControlSocket ....sock already exists, disabling multiplexing
118-
// > mm_send_fd: sendmsg(2): Connection reset by peer\\r\\nmux_client_request_session: send fds failed\\r\\n
119-
sshConfig = &ssh.SSHConfig{
120-
ConfigFile: sshConfig.ConfigFile,
121-
Persist: false,
122-
AdditionalArgs: sshutil.DisableControlMasterOptsFromSSHArgs(sshConfig.AdditionalArgs),
111+
if r.external || determineUseExternalSSH() {
112+
if r.noMaster || runtime.GOOS == "windows" {
113+
// Remove ControlMaster, ControlPath, and ControlPersist options,
114+
// because Cygwin-based SSH clients do not support multiplexing when executing commands.
115+
// References:
116+
// https://inbox.sourceware.org/cygwin/c98988a5-7e65-4282-b2a1-bb8e350d5fab@acm.org/T/
117+
// https://stackoverflow.com/questions/20959792/is-ssh-controlmaster-with-cygwin-on-windows-actually-possible
118+
// By removing these options:
119+
// - Avoids execution failures when the control master is not yet available.
120+
// - Prevents error messages such as:
121+
// > mux_client_request_session: read from master failed: Connection reset by peer
122+
// > ControlSocket ....sock already exists, disabling multiplexing
123+
// > mm_send_fd: sendmsg(2): Connection reset by peer\\r\\nmux_client_request_session: send fds failed\\r\\n
124+
sshConfig = &ssh.SSHConfig{
125+
ConfigFile: sshConfig.ConfigFile,
126+
Persist: false,
127+
AdditionalArgs: sshutil.DisableControlMasterOptsFromSSHArgs(sshConfig.AdditionalArgs),
128+
}
123129
}
130+
stdout, stderr, err = ssh.ExecuteScript(a.instSSHAddress, a.sshLocalPort, sshConfig, script, r.description)
131+
} else {
132+
stdout, stderr, err = sshutil.ExecuteScriptViaInProcessClient(a.instSSHAddress, a.sshLocalPort, *a.instConfig.User.Name, a.instName, script, r.description)
124133
}
125-
stdout, stderr, err := ssh.ExecuteScript(a.instSSHAddress, a.sshLocalPort, sshConfig, script, r.description)
126134
logrus.Debugf("stdout=%q, stderr=%q, err=%v", stdout, stderr, err)
127135
if err != nil {
128136
return fmt.Errorf("stdout=%q, stderr=%q: %w", stdout, stderr, err)
129137
}
130138
return nil
131139
}
132140

141+
var determineUseExternalSSH = sync.OnceValue(func() bool {
142+
var useExternalSSH bool
143+
// allow overriding via LIMA_EXTERNAL_SSH_REQUIREMENT environment variable
144+
if envVar := os.Getenv("LIMA_EXTERNAL_SSH_REQUIREMENT"); envVar != "" {
145+
if b, err := strconv.ParseBool(envVar); err != nil {
146+
logrus.WithError(err).Warnf("invalid LIMA_EXTERNAL_SSH_REQUIREMENT value %q", envVar)
147+
} else {
148+
useExternalSSH = b
149+
}
150+
}
151+
if useExternalSSH {
152+
logrus.Info("using external ssh command for executing requirement scripts")
153+
} else {
154+
logrus.Info("using in-process ssh client for executing requirement scripts")
155+
}
156+
return useExternalSSH
157+
})
158+
133159
type requirement struct {
134160
description string
135161
script string
136162
debugHint string
137163
fatal bool
138164
noMaster bool
165+
// Execute the script externally via the ssh command instead of using the in-process client.
166+
// noMaster will be ignored if external is false.
167+
external bool
139168
}
140169

141170
func (a *HostAgent) essentialRequirements() []requirement {
@@ -158,6 +187,7 @@ If any private key under ~/.ssh is protected with a passphrase, you need to have
158187
true
159188
`,
160189
debugHint: `The persistent ssh ControlMaster should be started immediately.`,
190+
external: true,
161191
}
162192
if *a.instConfig.Plain {
163193
req = append(req, startControlMasterReq)

pkg/sshutil/sshutil.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"time"
3232

3333
"github.com/coreos/go-semver/semver"
34+
sshocker "github.com/lima-vm/sshocker/pkg/ssh"
3435
"github.com/mattn/go-shellwords"
3536
"github.com/sirupsen/logrus"
3637
"golang.org/x/crypto/ssh"
@@ -688,3 +689,68 @@ func GenerateSSHHostKeys(instDir, hostname string) (map[string]string, error) {
688689
}
689690
return res, nil
690691
}
692+
693+
// ExecuteScriptViaInProcessClient executes the given script on the remote host via in-process SSH client.
694+
func ExecuteScriptViaInProcessClient(host string, port int, user, instanceName, script, scriptName string) (stdout, stderr string, err error) {
695+
// Prepare signer
696+
signer, err := UserPrivateKey()
697+
if err != nil {
698+
return "", "", err
699+
}
700+
// Prepare HostKeyCallback
701+
hostKeyChecker, err := HostKeyCheckerWithKeysInKnownHosts(instanceName)
702+
if err != nil {
703+
return "", "", err
704+
}
705+
706+
// Prepare ssh client config
707+
sshConfig := &ssh.ClientConfig{
708+
User: user,
709+
Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
710+
HostKeyCallback: hostKeyChecker,
711+
Timeout: 10 * time.Second,
712+
}
713+
714+
// Connect to SSH server
715+
addr := net.JoinHostPort(host, fmt.Sprintf("%d", port))
716+
var dialer net.Dialer
717+
dialer.Timeout = sshConfig.Timeout
718+
conn, err := dialer.DialContext(context.Background(), "tcp", addr)
719+
if err != nil {
720+
return "", "", fmt.Errorf("failed to dial %q: %w", addr, err)
721+
}
722+
sshConn, chans, reqs, err := ssh.NewClientConn(conn, addr, sshConfig)
723+
if err != nil {
724+
return "", "", fmt.Errorf("failed to create ssh.Conn to %q: %w", addr, err)
725+
}
726+
client := ssh.NewClient(sshConn, chans, reqs)
727+
if err != nil {
728+
return "", "", fmt.Errorf("failed to create SSH client to %q: %w", addr, err)
729+
}
730+
defer client.Close()
731+
732+
// Create session
733+
session, err := client.NewSession()
734+
if err != nil {
735+
return "", "", fmt.Errorf("failed to create SSH session to %q: %w", addr, err)
736+
}
737+
defer session.Close()
738+
739+
// Execute script
740+
interpreter, err := sshocker.ParseScriptInterpreter(script)
741+
if err != nil {
742+
return "", "", err
743+
}
744+
// Provide the script via stdin
745+
session.Stdin = strings.NewReader(strings.TrimPrefix(script, "#!"+interpreter+"\n"))
746+
// Capture stdout and stderr
747+
var stdoutBuf, stderrBuf bytes.Buffer
748+
session.Stdout = &stdoutBuf
749+
session.Stderr = &stderrBuf
750+
logrus.Debugf("executing ssh for script %q", scriptName)
751+
err = session.Run(interpreter)
752+
if err != nil {
753+
return stdoutBuf.String(), stderrBuf.String(), fmt.Errorf("failed to execute script %q: stdout=%q, stderr=%q: %w", scriptName, stdoutBuf.String(), stderrBuf.String(), err)
754+
}
755+
return stdoutBuf.String(), stderrBuf.String(), nil
756+
}

website/content/en/docs/config/environment-variables.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,14 @@ This page documents the environment variables used in Lima.
106106
lima
107107
```
108108

109+
### `LIMA_EXTERNAL_SSH_REQUIREMENT`
110+
- **Description**: Specifies whether to use an external SSH client for checking requirements instead of the built-in SSH client.
111+
- **Default**: `false`
112+
- **Usage**:
113+
```sh
114+
export LIMA_EXTERNAL_SSH_REQUIREMENT=true
115+
```
116+
109117
### `LIMA_SSH_OVER_VSOCK`
110118
- **Description**: Specifies to use vsock for SSH connection instead of port forwarding.
111119
- **Default**: `true` (since v2.0.0)

0 commit comments

Comments
 (0)