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