Skip to content

Commit da4d8eb

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> # Conflicts: # pkg/hostagent/requirements.go
1 parent 40e9649 commit da4d8eb

File tree

2 files changed

+159
-9
lines changed

2 files changed

+159
-9
lines changed

pkg/hostagent/hostagent.go

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

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

8893
type options struct {
@@ -258,6 +263,27 @@ func New(ctx context.Context, instName string, stdout io.Writer, signalCh chan o
258263
return a, nil
259264
}
260265

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+
261287
func writeSSHConfigFile(sshPath, instName, instDir, instSSHAddress string, sshLocalPort int, sshOpts []string) error {
262288
if instDir == "" {
263289
return fmt.Errorf("directory is unknown for the instance %q", instName)
@@ -481,6 +507,19 @@ func (a *HostAgent) startRoutinesAndWait(ctx context.Context, errCh <-chan error
481507
return a.driver.Stop(ctx)
482508
}
483509

510+
// GuestIP returns the guest's IPv4 address if available; otherwise the IPv6 address.
511+
// It returns nil if the guest is not reachable by a direct IP.
512+
func (a *HostAgent) GuestIP() net.IP {
513+
a.guestIPMu.RLock()
514+
defer a.guestIPMu.RUnlock()
515+
if a.guestIPv4 != nil {
516+
return a.guestIPv4
517+
} else if a.guestIPv6 != nil {
518+
return a.guestIPv6
519+
}
520+
return nil
521+
}
522+
484523
func (a *HostAgent) Info(_ context.Context) (*hostagentapi.Info, error) {
485524
info := &hostagentapi.Info{
486525
AutoStartedIdentifier: autostart.AutoStartedIdentifier(),
@@ -492,6 +531,12 @@ func (a *HostAgent) Info(_ context.Context) (*hostagentapi.Info, error) {
492531
func (a *HostAgent) sshAddressPort() (sshAddress string, sshPort int) {
493532
sshAddress = a.instSSHAddress
494533
sshPort = a.sshLocalPort
534+
guestIP := a.GuestIP()
535+
if guestIP != nil {
536+
sshAddress = guestIP.String()
537+
sshPort = 22
538+
logrus.Debugf("Using the guest IP address %q directly", sshAddress)
539+
}
495540
return sshAddress, sshPort
496541
}
497542

@@ -513,7 +558,8 @@ func (a *HostAgent) startHostAgentRoutines(ctx context.Context) error {
513558
return nil
514559
}
515560
logrus.Debugf("shutting down the SSH master")
516-
if exitMasterErr := ssh.ExitMaster(a.instSSHAddress, a.sshLocalPort, a.sshConfig); exitMasterErr != nil {
561+
sshAddress, sshPort := a.sshAddressPort()
562+
if exitMasterErr := ssh.ExitMaster(sshAddress, sshPort, a.sshConfig); exitMasterErr != nil {
517563
logrus.WithError(exitMasterErr).Warn("failed to exit SSH master")
518564
}
519565
return nil
@@ -529,7 +575,8 @@ sudo mkdir -p -m 700 /run/host-services
529575
sudo ln -sf "${SSH_AUTH_SOCK}" /run/host-services/ssh-auth.sock
530576
sudo chown -R "${USER}" /run/host-services`
531577
faDesc := "linking ssh auth socket to static location /run/host-services/ssh-auth.sock"
532-
stdout, stderr, err := ssh.ExecuteScript(a.instSSHAddress, a.sshLocalPort, a.sshConfig, faScript, faDesc)
578+
sshAddress, sshPort := a.sshAddressPort()
579+
stdout, stderr, err := ssh.ExecuteScript(sshAddress, sshPort, a.sshConfig, faScript, faDesc)
533580
logrus.Debugf("stdout=%q, stderr=%q, err=%v", stdout, stderr, err)
534581
if err != nil {
535582
errs = append(errs, fmt.Errorf("stdout=%q, stderr=%q: %w", stdout, stderr, err))

pkg/hostagent/requirements.go

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

66
import (
7+
"context"
8+
"encoding/json"
79
"errors"
810
"fmt"
11+
"net"
912
"runtime"
1013
"strings"
1114
"time"
@@ -122,20 +125,25 @@ func (a *HostAgent) waitForRequirement(r requirement) error {
122125
AdditionalArgs: sshutil.DisableControlMasterOptsFromSSHArgs(sshConfig.AdditionalArgs),
123126
}
124127
}
125-
stdout, stderr, err := ssh.ExecuteScript(a.instSSHAddress, a.sshLocalPort, sshConfig, script, r.description)
128+
sshAddress, sshPort := a.sshAddressPort()
129+
stdout, stderr, err := ssh.ExecuteScript(sshAddress, sshPort, sshConfig, script, r.description)
126130
logrus.Debugf("stdout=%q, stderr=%q, err=%v", stdout, stderr, err)
127131
if err != nil {
128132
return fmt.Errorf("stdout=%q, stderr=%q: %w", stdout, stderr, err)
129133
}
134+
if r.stdoutParser != nil {
135+
return r.stdoutParser(stdout)
136+
}
130137
return nil
131138
}
132139

133140
type requirement struct {
134-
description string
135-
script string
136-
debugHint string
137-
fatal bool
138-
noMaster bool
141+
description string
142+
script string
143+
debugHint string
144+
fatal bool
145+
noMaster bool
146+
stdoutParser func(string) error
139147
}
140148

141149
func (a *HostAgent) essentialRequirements() []requirement {
@@ -151,7 +159,24 @@ Make sure that the YAML field "ssh.localPort" is not used by other processes on
151159
If any private key under ~/.ssh is protected with a passphrase, you need to have ssh-agent to be running.
152160
`,
153161
noMaster: true,
154-
})
162+
},
163+
)
164+
if runtime.GOOS == "darwin" {
165+
// Limit the Guest IP address detection only to macOS for now.
166+
req = append(req,
167+
requirement{
168+
description: "detect guest IP address",
169+
script: `#!/bin/bash
170+
ip -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+
}
155180
startControlMasterReq := requirement{
156181
description: "Explicitly start ssh ControlMaster",
157182
script: `#!/bin/bash
@@ -280,3 +305,81 @@ Check "/var/log/cloud-init-output.log" in the guest to see where the process is
280305
})
281306
return req
282307
}
308+
309+
// detectGuestIPAddress detects the guest IP address on the interface in same subnet on the host
310+
// by parsing the output of "ip -j addr" command in the guest.
311+
func (a *HostAgent) detectGuestIPAddress(stdout string) error {
312+
var guestIfs []struct {
313+
IFNAME string `json:"ifname"`
314+
ADDRS []struct {
315+
Family string `json:"family"`
316+
Local net.IP `json:"local"`
317+
Scope string `json:"scope"`
318+
} `json:"addr_info"`
319+
}
320+
if err := json.Unmarshal([]byte(stdout), &guestIfs); err != nil {
321+
return fmt.Errorf("failed to parse ip addr output %q: %w", stdout, err)
322+
}
323+
var (
324+
guestIPv4 net.IP
325+
guestIPv6 net.IP
326+
)
327+
hostIfs, err := net.Interfaces()
328+
if err != nil {
329+
return fmt.Errorf("failed to get network interfaces: %w", err)
330+
}
331+
for _, hostIf := range hostIfs {
332+
if hostIf.Flags&net.FlagUp == 0 {
333+
continue
334+
}
335+
hostAddrs, err := hostIf.Addrs()
336+
if err != nil {
337+
return fmt.Errorf("failed to get addresses for interface %q: %w", hostIf.Name, err)
338+
}
339+
for _, hostAddr := range hostAddrs {
340+
hostIPNet, ok := hostAddr.(*net.IPNet)
341+
if !ok {
342+
continue
343+
}
344+
for _, guestIf := range guestIfs {
345+
if hostIPv4 := hostIPNet.IP.To4(); hostIPv4 != nil {
346+
for _, guestAddr := range guestIf.ADDRS {
347+
if guestAddr.Scope != "global" {
348+
continue
349+
} else if guestAddr.Family != "inet" {
350+
continue
351+
} else if hostIPNet.Contains(guestAddr.Local) {
352+
guestIPv4 = guestAddr.Local
353+
}
354+
}
355+
} else if hostIPv6 := hostIPNet.IP.To16(); hostIPv6 != nil {
356+
for _, guestAddr := range guestIf.ADDRS {
357+
if guestAddr.Scope != "global" {
358+
continue
359+
} else if guestAddr.Family != "inet6" {
360+
continue
361+
} else if hostIPNet.Contains(guestAddr.Local) {
362+
guestIPv6 = guestAddr.Local
363+
}
364+
}
365+
}
366+
}
367+
}
368+
}
369+
if guestIPv4 == nil && guestIPv6 == nil {
370+
logrus.Infof("The guest IPv4/IPv6 address is not found")
371+
return nil
372+
}
373+
if guestIPv4 != nil {
374+
logrus.Infof("The guest IPv4 address is %q", guestIPv4)
375+
}
376+
if guestIPv6 != nil {
377+
logrus.Infof("The guest IPv6 address is %q", guestIPv6)
378+
}
379+
a.guestIPMu.Lock()
380+
a.guestIPv4 = guestIPv4
381+
a.guestIPv6 = guestIPv6
382+
a.guestIPMu.Unlock()
383+
ctx := context.Background()
384+
return a.WriteSSHConfigFile(ctx)
385+
}

0 commit comments

Comments
 (0)