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,35 @@ 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 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,69 @@ 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+ continue
328+ }
329+ a .guestIPAddressMu .Lock ()
330+ a .guestIfnameOnSameSubnetAsHost = neighbor .DEV
331+ a .guestIPAddressMu .Unlock ()
332+ logrus .Infof ("Detected the guest has interface %q in same subnet on the host" , a .guestIfnameOnSameSubnetAsHost )
333+ return nil
334+ }
335+ }
336+ logrus .Info ("The guest does not have interface in same subnet on the host" )
337+ return nil
338+ }
339+
340+ // detectGuestIPAddress detects the guest IP address on the interface in same subnet on the host
341+ // by parsing the output of "ip -j addr" command in the guest.
342+ func (a * HostAgent ) detectGuestIPAddress (stdout string ) error {
343+ if a .guestIfnameOnSameSubnetAsHost == "" {
344+ return nil
345+ }
346+ var addrs []struct {
347+ IFNAME string `json:"ifname"`
348+ ADDRS []struct {
349+ Family string `json:"family"`
350+ Local string `json:"local"`
351+ } `json:"addr_info"`
352+ }
353+ if err := json .Unmarshal ([]byte (stdout ), & addrs ); err != nil {
354+ return fmt .Errorf ("failed to parse ip addr output %q: %w" , stdout , err )
355+ }
356+ for _ , addr := range addrs {
357+ if addr .IFNAME == a .guestIfnameOnSameSubnetAsHost {
358+ for _ , addr := range addr .ADDRS {
359+ if addr .Family != "inet" {
360+ continue
361+ }
362+ a .guestIPAddressMu .Lock ()
363+ a .guestIPAddress = addr .Local
364+ a .guestIPAddressMu .Unlock ()
365+ logrus .Infof ("The guest IP address on the interface %q is %q" , a .guestIfnameOnSameSubnetAsHost , addr .Local )
366+ ctx := context .Background ()
367+ return a .WriteSSHConfigFile (ctx )
368+ }
369+ }
370+ }
371+ logrus .Infof ("The interface %q does not have IPv4 address" , a .guestIfnameOnSameSubnetAsHost )
372+ return nil
373+ }
0 commit comments