Skip to content

Commit f22e9fa

Browse files
committed
pkg/hostagent: Detect the VM’s IP address on the same subnet as the host OS.
Guest IP address will be detected in requirement process. `hostagent.go`: - Add fields to `HostAgent` - `guestIfnameOnSameSubnetAsHost string` - `guestIPAddress string` - Add `HostAgent.WriteSSHConfigFile()` helper to write SSHConfigFile - Add `HostAgent.sshAddressPort()` helper to provide ipAddress and port for SSH `requirements.go`: - Add `stdoutParser func(string) error` field to `requirement` - Add `HostAgent.detectGuestIfnameOnSameSubnetAtHost()` to parse `ip -j neighbor` command output - Add `HostAgent.detectGuestIPAddress()` to parse `ip -j addr` command output - Add two requirements to `HostAgent.essentialRequirements()` - "detect guest interface on same subnet as the host" - "detect guest IP address" Signed-off-by: Norio Nomura <norio.nomura@gmail.com>
1 parent e49932f commit f22e9fa

File tree

2 files changed

+152
-9
lines changed

2 files changed

+152
-9
lines changed

pkg/hostagent/hostagent.go

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ type HostAgent struct {
8282

8383
statusMu sync.RWMutex
8484
currentStatus events.Status
85+
86+
// Guest interface name on the same subnet as the host,
87+
guestIfnameOnSameSubnetAsHost string
88+
// Guest IP address on the same subnet as the host.
89+
guestIPAddress string
90+
guestIPAddressMu sync.RWMutex
8591
}
8692

8793
type options struct {
@@ -257,6 +263,27 @@ func New(ctx context.Context, instName string, stdout io.Writer, signalCh chan o
257263
return a, nil
258264
}
259265

266+
func (a *HostAgent) WriteSSHConfigFile(ctx context.Context) error {
267+
sshExe, err := sshutil.NewSSHExe()
268+
if err != nil {
269+
return err
270+
}
271+
sshOpts, err := sshutil.SSHOpts(
272+
ctx,
273+
sshExe,
274+
a.instDir,
275+
*a.instConfig.User.Name,
276+
*a.instConfig.SSH.LoadDotSSHPubKeys,
277+
*a.instConfig.SSH.ForwardAgent,
278+
*a.instConfig.SSH.ForwardX11,
279+
*a.instConfig.SSH.ForwardX11Trusted)
280+
if err != nil {
281+
return err
282+
}
283+
sshAddress, sshPort := a.sshAddressPort()
284+
return writeSSHConfigFile(sshExe.Exe, a.instName, a.instDir, sshAddress, sshPort, sshOpts)
285+
}
286+
260287
func writeSSHConfigFile(sshPath, instName, instDir, instSSHAddress string, sshLocalPort int, sshOpts []string) error {
261288
if instDir == "" {
262289
return fmt.Errorf("directory is unknown for the instance %q", instName)
@@ -483,6 +510,19 @@ func (a *HostAgent) Info(_ context.Context) (*hostagentapi.Info, error) {
483510
return info, nil
484511
}
485512

513+
func (a *HostAgent) sshAddressPort() (string, int) {
514+
a.guestIPAddressMu.RLock()
515+
defer a.guestIPAddressMu.RUnlock()
516+
sshAddress := a.instSSHAddress
517+
sshPort := a.sshLocalPort
518+
if a.guestIPAddress != "" {
519+
sshAddress = a.guestIPAddress
520+
sshPort = 22
521+
logrus.Debugf("Using the guest IP address %q directly", sshAddress)
522+
}
523+
return sshAddress, sshPort
524+
}
525+
486526
func (a *HostAgent) startHostAgentRoutines(ctx context.Context) error {
487527
if *a.instConfig.Plain {
488528
msg := "Running in plain mode. Mounts, dynamic port forwarding, containerd, etc. will be ignored. Guest agent will not be running."
@@ -496,7 +536,8 @@ func (a *HostAgent) startHostAgentRoutines(ctx context.Context) error {
496536
}
497537
a.cleanUp(func() error {
498538
logrus.Debugf("shutting down the SSH master")
499-
if exitMasterErr := ssh.ExitMaster(a.instSSHAddress, a.sshLocalPort, a.sshConfig); exitMasterErr != nil {
539+
sshAddress, sshPort := a.sshAddressPort()
540+
if exitMasterErr := ssh.ExitMaster(sshAddress, sshPort, a.sshConfig); exitMasterErr != nil {
500541
logrus.WithError(exitMasterErr).Warn("failed to exit SSH master")
501542
}
502543
return nil
@@ -512,7 +553,8 @@ sudo mkdir -p -m 700 /run/host-services
512553
sudo ln -sf "${SSH_AUTH_SOCK}" /run/host-services/ssh-auth.sock
513554
sudo chown -R "${USER}" /run/host-services`
514555
faDesc := "linking ssh auth socket to static location /run/host-services/ssh-auth.sock"
515-
stdout, stderr, err := ssh.ExecuteScript(a.instSSHAddress, a.sshLocalPort, a.sshConfig, faScript, faDesc)
556+
sshAddress, sshPort := a.sshAddressPort()
557+
stdout, stderr, err := ssh.ExecuteScript(sshAddress, sshPort, a.sshConfig, faScript, faDesc)
516558
logrus.Debugf("stdout=%q, stderr=%q, err=%v", stdout, stderr, err)
517559
if err != nil {
518560
errs = append(errs, fmt.Errorf("stdout=%q, stderr=%q: %w", stdout, stderr, err))

pkg/hostagent/requirements.go

Lines changed: 108 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
package hostagent
55

66
import (
7+
"context"
8+
"encoding/json"
79
"errors"
810
"fmt"
11+
"net"
12+
"runtime"
913
"strings"
1014
"time"
1115

@@ -110,20 +114,25 @@ func (a *HostAgent) waitForRequirement(r requirement) error {
110114
AdditionalArgs: sshutil.DisableControlMasterOptsFromSSHArgs(sshConfig.AdditionalArgs),
111115
}
112116
}
113-
stdout, stderr, err := ssh.ExecuteScript(a.instSSHAddress, a.sshLocalPort, sshConfig, script, r.description)
117+
sshAddress, sshPort := a.sshAddressPort()
118+
stdout, stderr, err := ssh.ExecuteScript(sshAddress, sshPort, sshConfig, script, r.description)
114119
logrus.Debugf("stdout=%q, stderr=%q, err=%v", stdout, stderr, err)
115120
if err != nil {
116121
return fmt.Errorf("stdout=%q, stderr=%q: %w", stdout, stderr, err)
117122
}
123+
if r.stdoutParser != nil {
124+
return r.stdoutParser(stdout)
125+
}
118126
return nil
119127
}
120128

121129
type requirement struct {
122-
description string
123-
script string
124-
debugHint string
125-
fatal bool
126-
noMaster bool
130+
description string
131+
script string
132+
debugHint string
133+
fatal bool
134+
noMaster bool
135+
stdoutParser func(string) error
127136
}
128137

129138
func (a *HostAgent) essentialRequirements() []requirement {
@@ -139,7 +148,35 @@ Make sure that the YAML field "ssh.localPort" is not used by other processes on
139148
If any private key under ~/.ssh is protected with a passphrase, you need to have ssh-agent to be running.
140149
`,
141150
noMaster: true,
142-
})
151+
},
152+
)
153+
if runtime.GOOS == "darwin" {
154+
// Limit the Guest IP address detection only to macOS for now.
155+
req = append(req,
156+
requirement{
157+
description: "detect guest interface on same subnet as the host",
158+
script: `#!/bin/bash
159+
ip -j neighbor
160+
`,
161+
debugHint: `Detecting the guest has interface in same subnet on the host.
162+
This is only supported on macOS for now.
163+
If the guest does not have interface in same subnet on the host, SSH connection against the guest OS will be made via the localhost port forwarding.`,
164+
noMaster: true,
165+
stdoutParser: a.detectGuestIfnameOnSameSubnetAtHost,
166+
},
167+
requirement{
168+
description: "detect guest IP address",
169+
script: `#!/bin/bash
170+
ip -4 -j addr
171+
`,
172+
debugHint: `Detecting the guest IP address on the interface in same subnet on the host.
173+
This is only supported on macOS for now.
174+
If the interface does not have IPv4 address, SSH connection against the guest OS will be made via the localhost port forwarding.`,
175+
noMaster: true,
176+
stdoutParser: a.detectGuestIPAddress,
177+
},
178+
)
179+
}
143180
startControlMasterReq := requirement{
144181
description: "Explicitly start ssh ControlMaster",
145182
script: `#!/bin/bash
@@ -268,3 +305,67 @@ Check "/var/log/cloud-init-output.log" in the guest to see where the process is
268305
})
269306
return req
270307
}
308+
309+
// detectGuestIfnameOnSameSubnetAtHost detects the guest interface name on the same subnet on the host
310+
// by comparing the MAC addresses of the host network interfaces and the output of "ip -j neighbor" command in the guest.
311+
func (a *HostAgent) detectGuestIfnameOnSameSubnetAtHost(stdout string) error {
312+
var neighbors []struct {
313+
DST string `json:"dst"`
314+
DEV string `json:"dev"`
315+
LLADDR string `json:"lladdr"`
316+
}
317+
if err := json.Unmarshal([]byte(stdout), &neighbors); err != nil {
318+
return fmt.Errorf("failed to parse ip neighbor output %q: %w", stdout, err)
319+
}
320+
interfaces, err := net.Interfaces()
321+
if err != nil {
322+
return fmt.Errorf("failed to get network interfaces: %w", err)
323+
}
324+
for _, neighbor := range neighbors {
325+
for _, ifi := range interfaces {
326+
if ifi.HardwareAddr.String() == neighbor.LLADDR {
327+
a.guestIPAddressMu.Lock()
328+
a.guestIfnameOnSameSubnetAsHost = neighbor.DEV
329+
a.guestIPAddressMu.Unlock()
330+
logrus.Infof("Detected the guest has interface %q in same subnet on the host", a.guestIfnameOnSameSubnetAsHost)
331+
return nil
332+
}
333+
}
334+
}
335+
logrus.Info("The guest does not have interface in same subnet on the host")
336+
return nil
337+
}
338+
339+
// detectGuestIPAddress detects the guest IP address on the interface in same subnet on the host
340+
// by parsing the output of "ip -j addr" command in the guest.
341+
func (a *HostAgent) detectGuestIPAddress(stdout string) error {
342+
if a.guestIfnameOnSameSubnetAsHost == "" {
343+
return nil
344+
}
345+
var addrs []struct {
346+
IFNAME string `json:"ifname"`
347+
ADDRS []struct {
348+
Family string `json:"family"`
349+
Local string `json:"local"`
350+
} `json:"addr_info"`
351+
}
352+
if err := json.Unmarshal([]byte(stdout), &addrs); err != nil {
353+
return fmt.Errorf("failed to parse ip addr output %q: %w", stdout, err)
354+
}
355+
for _, addr := range addrs {
356+
if addr.IFNAME == a.guestIfnameOnSameSubnetAsHost {
357+
for _, addr := range addr.ADDRS {
358+
if addr.Family == "inet" {
359+
a.guestIPAddressMu.Lock()
360+
a.guestIPAddress = addr.Local
361+
a.guestIPAddressMu.Unlock()
362+
a.WriteSSHConfigFile(context.Background())
363+
logrus.Infof("The guest IP address on the interface %q is %q", a.guestIfnameOnSameSubnetAsHost, addr.Local)
364+
return nil
365+
}
366+
}
367+
}
368+
}
369+
logrus.Infof("The interface %q does not have IPv4 address", a.guestIfnameOnSameSubnetAsHost)
370+
return nil
371+
}

0 commit comments

Comments
 (0)