Skip to content

Commit 9fd0a86

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` - `guestIPv4 net.IP` - `guestIPv6 net.IP` - Add methods `GuestIP()`, `GuestIPv4()`, and `GuestIPv6()` to `HostAgent` - 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 IPv4 address" - "detect guest IPv6 address" Signed-off-by: Norio Nomura <norio.nomura@gmail.com> # Conflicts: # pkg/hostagent/hostagent.go pkg/hostagent: Remove `HostAgent.detectGuestIfnameOnSameSubnetAtHost()` Signed-off-by: Norio Nomura <norio.nomura@gmail.com>
1 parent a8e8bef commit 9fd0a86

File tree

2 files changed

+160
-9
lines changed

2 files changed

+160
-9
lines changed

pkg/hostagent/hostagent.go

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

8383
statusMu sync.RWMutex
8484
currentStatus events.Status
85+
86+
// Guest IP address on the same subnet as the host.
87+
guestIPv4 net.IP
88+
guestIPv6 net.IP
89+
guestIPMu sync.RWMutex
8590
}
8691

8792
type options struct {
@@ -257,6 +262,27 @@ func New(ctx context.Context, instName string, stdout io.Writer, signalCh chan o
257262
return a, nil
258263
}
259264

265+
func (a *HostAgent) WriteSSHConfigFile(ctx context.Context) error {
266+
sshExe, err := sshutil.NewSSHExe()
267+
if err != nil {
268+
return err
269+
}
270+
sshOpts, err := sshutil.SSHOpts(
271+
ctx,
272+
sshExe,
273+
a.instDir,
274+
*a.instConfig.User.Name,
275+
*a.instConfig.SSH.LoadDotSSHPubKeys,
276+
*a.instConfig.SSH.ForwardAgent,
277+
*a.instConfig.SSH.ForwardX11,
278+
*a.instConfig.SSH.ForwardX11Trusted)
279+
if err != nil {
280+
return err
281+
}
282+
sshAddress, sshPort := a.sshAddressPort()
283+
return writeSSHConfigFile(sshExe.Exe, a.instName, a.instDir, sshAddress, sshPort, sshOpts)
284+
}
285+
260286
func writeSSHConfigFile(sshPath, instName, instDir, instSSHAddress string, sshLocalPort int, sshOpts []string) error {
261287
if instDir == "" {
262288
return fmt.Errorf("directory is unknown for the instance %q", instName)
@@ -476,6 +502,19 @@ func (a *HostAgent) startRoutinesAndWait(ctx context.Context, errCh <-chan error
476502
return a.driver.Stop(ctx)
477503
}
478504

505+
// GuestIP returns the guest's IPv4 address if available; otherwise the IPv6 address.
506+
// It returns nil if the guest is not reachable by a direct IP.
507+
func (a *HostAgent) GuestIP() net.IP {
508+
a.guestIPMu.RLock()
509+
defer a.guestIPMu.RUnlock()
510+
if a.guestIPv4 != nil {
511+
return a.guestIPv4
512+
} else if a.guestIPv6 != nil {
513+
return a.guestIPv6
514+
}
515+
return nil
516+
}
517+
479518
func (a *HostAgent) Info(_ context.Context) (*hostagentapi.Info, error) {
480519
info := &hostagentapi.Info{
481520
SSHLocalPort: a.sshLocalPort,
@@ -486,6 +525,12 @@ func (a *HostAgent) Info(_ context.Context) (*hostagentapi.Info, error) {
486525
func (a *HostAgent) sshAddressPort() (sshAddress string, sshPort int) {
487526
sshAddress = a.instSSHAddress
488527
sshPort = a.sshLocalPort
528+
guestIP := a.GuestIP()
529+
if guestIP != nil {
530+
sshAddress = guestIP.String()
531+
sshPort = 22
532+
logrus.Debugf("Using the guest IP address %q directly", sshAddress)
533+
}
489534
return sshAddress, sshPort
490535
}
491536

@@ -502,7 +547,8 @@ func (a *HostAgent) startHostAgentRoutines(ctx context.Context) error {
502547
}
503548
a.cleanUp(func() error {
504549
logrus.Debugf("shutting down the SSH master")
505-
if exitMasterErr := ssh.ExitMaster(a.instSSHAddress, a.sshLocalPort, a.sshConfig); exitMasterErr != nil {
550+
sshAddress, sshPort := a.sshAddressPort()
551+
if exitMasterErr := ssh.ExitMaster(sshAddress, sshPort, a.sshConfig); exitMasterErr != nil {
506552
logrus.WithError(exitMasterErr).Warn("failed to exit SSH master")
507553
}
508554
return nil
@@ -518,7 +564,8 @@ sudo mkdir -p -m 700 /run/host-services
518564
sudo ln -sf "${SSH_AUTH_SOCK}" /run/host-services/ssh-auth.sock
519565
sudo chown -R "${USER}" /run/host-services`
520566
faDesc := "linking ssh auth socket to static location /run/host-services/ssh-auth.sock"
521-
stdout, stderr, err := ssh.ExecuteScript(a.instSSHAddress, a.sshLocalPort, a.sshConfig, faScript, faDesc)
567+
sshAddress, sshPort := a.sshAddressPort()
568+
stdout, stderr, err := ssh.ExecuteScript(sshAddress, sshPort, a.sshConfig, faScript, faDesc)
522569
logrus.Debugf("stdout=%q, stderr=%q, err=%v", stdout, stderr, err)
523570
if err != nil {
524571
errs = append(errs, fmt.Errorf("stdout=%q, stderr=%q: %w", stdout, stderr, err))

pkg/hostagent/requirements.go

Lines changed: 111 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,24 @@ 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 IP address",
158+
script: `#!/bin/bash
159+
ip -j addr
160+
`,
161+
debugHint: `Detecting the guest IP address on the interface in same subnet on the host.
162+
This is only supported on macOS for now.
163+
If the interface does not have IPv4 address, SSH connection against the guest OS will be made via the localhost port forwarding.`,
164+
noMaster: true,
165+
stdoutParser: a.detectGuestIPAddress,
166+
},
167+
)
168+
}
143169
startControlMasterReq := requirement{
144170
description: "Explicitly start ssh ControlMaster",
145171
script: `#!/bin/bash
@@ -268,3 +294,81 @@ Check "/var/log/cloud-init-output.log" in the guest to see where the process is
268294
})
269295
return req
270296
}
297+
298+
// detectGuestIPAddress detects the guest IP address on the interface in same subnet on the host
299+
// by parsing the output of "ip -j addr" command in the guest.
300+
func (a *HostAgent) detectGuestIPAddress(stdout string) error {
301+
var guestIfs []struct {
302+
IFNAME string `json:"ifname"`
303+
ADDRS []struct {
304+
Family string `json:"family"`
305+
Local net.IP `json:"local"`
306+
Scope string `json:"scope"`
307+
} `json:"addr_info"`
308+
}
309+
if err := json.Unmarshal([]byte(stdout), &guestIfs); err != nil {
310+
return fmt.Errorf("failed to parse ip addr output %q: %w", stdout, err)
311+
}
312+
var (
313+
guestIPv4 net.IP
314+
guestIPv6 net.IP
315+
)
316+
hostIfs, err := net.Interfaces()
317+
if err != nil {
318+
return fmt.Errorf("failed to get network interfaces: %w", err)
319+
}
320+
for _, hostIf := range hostIfs {
321+
if hostIf.Flags&net.FlagUp == 0 {
322+
continue
323+
}
324+
hostAddrs, err := hostIf.Addrs()
325+
if err != nil {
326+
return fmt.Errorf("failed to get addresses for interface %q: %w", hostIf.Name, err)
327+
}
328+
for _, hostAddr := range hostAddrs {
329+
hostIPNet, ok := hostAddr.(*net.IPNet)
330+
if !ok {
331+
continue
332+
}
333+
for _, guestIf := range guestIfs {
334+
if hostIPv4 := hostIPNet.IP.To4(); hostIPv4 != nil {
335+
for _, guestAddr := range guestIf.ADDRS {
336+
if guestAddr.Scope != "global" {
337+
continue
338+
} else if guestAddr.Family != "inet" {
339+
continue
340+
} else if hostIPNet.Contains(guestAddr.Local) {
341+
guestIPv4 = guestAddr.Local
342+
}
343+
}
344+
} else if hostIPv6 := hostIPNet.IP.To16(); hostIPv6 != nil {
345+
for _, guestAddr := range guestIf.ADDRS {
346+
if guestAddr.Scope != "global" {
347+
continue
348+
} else if guestAddr.Family != "inet6" {
349+
continue
350+
} else if hostIPNet.Contains(guestAddr.Local) {
351+
guestIPv6 = guestAddr.Local
352+
}
353+
}
354+
}
355+
}
356+
}
357+
}
358+
if guestIPv4 == nil && guestIPv6 == nil {
359+
logrus.Infof("The guest IPv4/IPv6 address is not found")
360+
return nil
361+
}
362+
if guestIPv4 != nil {
363+
logrus.Infof("The guest IPv4 address is %q", guestIPv4)
364+
}
365+
if guestIPv6 != nil {
366+
logrus.Infof("The guest IPv6 address is %q", guestIPv6)
367+
}
368+
a.guestIPMu.Lock()
369+
a.guestIPv4 = guestIPv4
370+
a.guestIPv6 = guestIPv6
371+
a.guestIPMu.Unlock()
372+
ctx := context.Background()
373+
return a.WriteSSHConfigFile(ctx)
374+
}

0 commit comments

Comments
 (0)