44package hostagent
55
66import (
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
121129type 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
129138func (a * HostAgent ) essentialRequirements () []requirement {
@@ -139,7 +148,24 @@ Make sure that the YAML field "ssh.localPort" is not used by other processes on
139148If 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