Skip to content

Commit 50c60ca

Browse files
authored
refactor: add support for bridged networking (#462)
Refactors to add support bridged networking. - Adds the ability to retrieve the IP address of the guest operating system from VMware Tools. - Detects the IP address of the host to use when using bridged networking. - Uses the IP address of the guest operating system received from VMware Tools when using bridged networking. - Uses the IP address of the guest operating system received from VMware Tools as a fallback for NAT and host-only networking. Note: The method for NAT and host-only networking may be subsequently refactored to only use the VMware Tools method, but this commit limits the scope to only adding support for bridged networking. Signed-off-by: Ryan Johnson <ryan@tenthirtyam.org>
1 parent 79d9ece commit 50c60ca

12 files changed

Lines changed: 229 additions & 73 deletions

File tree

.web-docs/components/builder/iso/README.md

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,11 @@ JSON Example:
117117
- `memory` (int) - The amount of memory for the virtual machine in MB. Defaults to `512`.
118118

119119
- `network` (string) - The network which the virtual machine will connect for desktop
120-
hypervisors. Use the generic values that map to a device, such as
121-
`hostonly`, `nat`, or `bridged`. Defaults to `nat`.
120+
hypervisors. Recommended values are `nat`, `hostonly`, or `bridged`.
121+
Defaults to `nat`.
122122

123-
~> **Note:** If not set to one of these generic values, then it is
124-
assumed to be a network device (_e.g._, `VMnet0..x`).
123+
~> **Note:** If not set to one of these recommended values, then
124+
it is assumed to be a custom network device configuration.
125125

126126
- `network_name` (string) - The network which the virtual machine will connect on a remote
127127
hypervisor.
@@ -907,12 +907,6 @@ You can tune this delay on a per-builder basis by specifying
907907
<!-- End of code generated from the comments of the VNCConfig struct in bootcommand/config.go; -->
908908

909909

910-
-> **Note**: For the `HTTPIP` to be resolved, the `network` interface type must
911-
be set to either `hostonly` or `nat`. It is recommended to leave the default
912-
network configuration while you are building the virtual machine, and use the
913-
`vmx_data_post` hook to modify the network configuration after the virtual
914-
machine build is complete.
915-
916910
**Optional**:
917911

918912
<!-- Code generated from the comments of the BootConfig struct in bootcommand/config.go; DO NOT EDIT MANUALLY -->

.web-docs/components/builder/vmx/README.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -682,12 +682,6 @@ You can tune this delay on a per-builder basis by specifying
682682
<!-- End of code generated from the comments of the VNCConfig struct in bootcommand/config.go; -->
683683

684684

685-
-> **Note**: For the `HTTPIP` to be resolved, the `network` interface type must
686-
be set to either `hostonly` or `nat`. It is recommended to leave the default
687-
network configuration while you are building the virtual machine, and use the
688-
`vmx_data_post` hook to modify the network configuration after the virtual
689-
machine build is complete.
690-
691685
**Optional**
692686

693687
<!-- Code generated from the comments of the BootConfig struct in bootcommand/config.go; DO NOT EDIT MANUALLY -->

builder/vmware/common/driver.go

Lines changed: 107 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,9 @@ type Driver interface {
323323
// HostIP retrieves the host IP address for the virtual machine based on the state.
324324
HostIP(multistep.StateBag) (string, error)
325325

326+
// GetGuestIPAddress retrieves the guest IP address for the virtual machine using VMware Tools.
327+
GetGuestIPAddress(string) (string, error)
328+
326329
// Export exports a virtual machine using the provided arguments.
327330
Export([]string) error
328331

@@ -473,17 +476,16 @@ func readCustomDeviceName(vmxData map[string]string) (string, error) {
473476

474477
// VmwareDriver is a struct that provides methods and paths needed for virtual machine management.
475478
type VmwareDriver struct {
476-
// These methods define paths that are utilized by the driver
477-
// A driver must overload these in order to point to the correct
478-
// files so that the address detection (ip and ethernet) machinery
479-
// works.
479+
// These methods define paths that are used by the driver.
480480
DhcpLeasesPath func(string) string
481481
DhcpConfPath func(string) string
482482
VmnetnatConfPath func(string) string
483483

484-
// This method returns an object with the NetworkNameMapper interface
485-
// that maps network to device and vice versa.
484+
// This method returns an object with the NetworkNameMapper interface that maps network to device and vice versa.
486485
NetworkMapper func() (NetworkNameMapper, error)
486+
487+
// GetHostIPForDevice returns the IP address for a given device.
488+
GetHostIPForDevice func(device string) (string, error)
487489
}
488490

489491
// GuestAddress retrieves the MAC address of a guest virtual machine from the .vmx configuration.
@@ -533,12 +535,12 @@ func (d *VmwareDriver) PotentialGuestIP(state multistep.StateBag) ([]string, err
533535
return nil, err
534536
}
535537

536-
// Search the desktop hypervisor for DHCP leases.
538+
// Search for DHCP leases.
537539
if addrs := d.getDhcpLeasesHypervisor(devices, hwaddr); len(addrs) > 0 {
538540
return addrs, nil
539541
}
540542

541-
// If VMware Fusion on macOS, search the Apple DHCP leases as a fallback.
543+
// Fallback: For VMware Fusion, search the Apple DHCP leases..
542544
if runtime.GOOS == osMacOS {
543545
if addrs := d.getDhcpLeasesMacos(hwaddr); len(addrs) > 0 {
544546
return addrs, nil
@@ -551,23 +553,22 @@ func (d *VmwareDriver) PotentialGuestIP(state multistep.StateBag) ([]string, err
551553
// HostAddress retrieves the host's hardware address linked to the network device specified in the state.
552554
func (d *VmwareDriver) HostAddress(state multistep.StateBag) (string, error) {
553555

554-
// grab mapper for converting network<->device
556+
// Grab mapper for converting a network to a device.
555557
netmap, err := d.NetworkMapper()
556558
if err != nil {
557559
return "", err
558560
}
559561

560-
// convert network to name
562+
// Convert network to name.
561563
network := state.Get("vmnetwork").(string)
562564
devices, err := netmap.NameIntoDevices(network)
563565

564-
// log them to see what was detected
566+
// Log discovered devices.
565567
for _, device := range devices {
566568
log.Printf("[INFO] Discovered device matching %s: %s", network, device)
567569
}
568570

569-
// we were unable to find the device, maybe it's a custom one...
570-
// so, check to see if it's in the .vmx configuration
571+
// If unable to find the device, it may be custom.
571572
if err != nil || network == "custom" {
572573
vmxPath := state.Get("vmx_path").(string)
573574
vmxData, err := readVMXConfig(vmxPath)
@@ -586,7 +587,6 @@ func (d *VmwareDriver) HostAddress(state multistep.StateBag) (string, error) {
586587

587588
var lastError error
588589
for _, device := range devices {
589-
// parse dhcpd configuration
590590
pathDhcpConfig := d.DhcpConfPath(device)
591591
if _, err := os.Stat(pathDhcpConfig); err != nil {
592592
return "", fmt.Errorf("unable to find vmnetdhcp conf file: %s", pathDhcpConfig)
@@ -598,22 +598,22 @@ func (d *VmwareDriver) HostAddress(state multistep.StateBag) (string, error) {
598598
continue
599599
}
600600

601-
// find the entry configured in the dhcpd
601+
// Read the DHCP configuration file.
602602
interfaceConfig, err := config.HostByName(device)
603603
if err != nil {
604604
lastError = err
605605
continue
606606
}
607607

608-
// finally grab the hardware address
608+
// Find the entry configured in the dhcpd.
609609
address, err := interfaceConfig.Hardware()
610610
if err == nil {
611611
return address.String(), nil
612612
}
613613

614-
// we didn't find it, so search through our interfaces for the device name
614+
// If not found, search through the interfaces for the device name.
615615
interfaceList, err := net.Interfaces()
616-
if err == nil {
616+
if err != nil {
617617
return "", err
618618
}
619619

@@ -632,23 +632,22 @@ func (d *VmwareDriver) HostAddress(state multistep.StateBag) (string, error) {
632632
// HostIP retrieves the host machine's IP address associated with the specific network device defined in the state.
633633
func (d *VmwareDriver) HostIP(state multistep.StateBag) (string, error) {
634634

635-
// grab mapper for converting network<->device
635+
// Grab mapper for converting a network to a device.
636636
netmap, err := d.NetworkMapper()
637637
if err != nil {
638638
return "", err
639639
}
640640

641-
// convert network to name
641+
// Convert network to name.
642642
network := state.Get("vmnetwork").(string)
643643
devices, err := netmap.NameIntoDevices(network)
644644

645-
// log them to see what was detected
645+
// Log discovered devices.
646646
for _, device := range devices {
647647
log.Printf("[INFO] Discovered device matching %s: %s", network, device)
648648
}
649649

650-
// we were unable to find the device, maybe it's a custom one...
651-
// so, check to see if it's in the .vmx configuration
650+
// If unable to find the device, it may be custom.
652651
if err != nil || network == "custom" {
653652
vmxPath := state.Get("vmx_path").(string)
654653
vmxData, err := readVMXConfig(vmxPath)
@@ -662,38 +661,119 @@ func (d *VmwareDriver) HostIP(state multistep.StateBag) (string, error) {
662661
if err != nil {
663662
return "", err
664663
}
665-
log.Printf("[INFO] Discovered custom device matching %s: %s", network, device)
664+
log.Printf("[INFO] Discovered a custom device matching %s: %s", network, device)
666665
}
667666

668667
var lastError error
669668
for _, device := range devices {
670-
// parse dhcpd configuration
669+
networkName, err := netmap.DeviceIntoName(device)
670+
671+
// Check if this is a bridged network device.
672+
isBridged := err == nil && strings.EqualFold(networkName, "bridged")
673+
674+
if isBridged {
675+
// Bridged networks connect to the physical network.
676+
// Find the host's IP address on the physical network interface.
677+
log.Printf("[INFO] Attempting to discover host IP address for bridged network on device %s.", device)
678+
679+
var address string
680+
var err error
681+
if d.GetHostIPForDevice != nil {
682+
// Get the host IP address using the device. This allows for mocking in tests.
683+
address, err = d.GetHostIPForDevice(device)
684+
} else {
685+
// Get the host IP address using the bridged network interface default route.
686+
address, err = getHostIPForBridgedNetwork()
687+
}
688+
if err != nil {
689+
lastError = err
690+
continue
691+
}
692+
return address, nil
693+
}
694+
695+
// For non-bridged networks, use the DHCP leases path.
671696
pathDhcpConfig := d.DhcpConfPath(device)
672697
if _, err := os.Stat(pathDhcpConfig); err != nil {
673698
return "", fmt.Errorf("unable to find vmnetdhcp conf file: %s", pathDhcpConfig)
674699
}
700+
701+
// Read the DHCP configuration file.
675702
config, err := ReadDhcpConfig(pathDhcpConfig)
676703
if err != nil {
677704
lastError = err
678705
continue
679706
}
680707

681-
// find the entry configured in the dhcpd
708+
// Find the entry configured in the dhcpd.
682709
interfaceConfig, err := config.HostByName(device)
683710
if err != nil {
684711
lastError = err
685712
continue
686713
}
687714

715+
// Retrieve the IP address.
688716
address, err := interfaceConfig.IP4()
689717
if err != nil {
690718
lastError = err
691719
continue
692720
}
693721

722+
// Return the IP address.
694723
return address.String(), nil
695724
}
696-
return "", fmt.Errorf("unable to find host IP from devices %v, last error: %s", devices, lastError)
725+
return "", fmt.Errorf("unable to retrieve host IP address from devices %v, last error: %s", devices, lastError)
726+
}
727+
728+
// getHostIPForBridgedNetwork retrieves the host's IP address for a bridged network.
729+
func getHostIPForBridgedNetwork() (string, error) {
730+
// Determine the source IP used for default-route communications.
731+
if conn, err := net.Dial("udp4", "1.1.1.1:53"); err == nil {
732+
defer conn.Close()
733+
if addr, ok := conn.LocalAddr().(*net.UDPAddr); ok {
734+
log.Printf("[INFO] Discovered host IP address for bridged network using default route: %s", addr.IP)
735+
return addr.IP.String(), nil
736+
}
737+
}
738+
739+
// Fallback: Walk all interfaces and return the first non-loopback IPv4 address that doesn't have a virtual/tunnel prefix.
740+
virtualPrefixes := []string{"docker", "br-", "veth", "podman", "virbr", "vmnet", "vboxnet", "utun", "tun", "tap"}
741+
742+
ifaces, err := net.Interfaces()
743+
if err != nil {
744+
return "", fmt.Errorf("unable to enumerate network interfaces: %s", err)
745+
}
746+
747+
for _, iface := range ifaces {
748+
if iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 {
749+
continue
750+
}
751+
lower := strings.ToLower(iface.Name)
752+
virtual := false
753+
for _, prefix := range virtualPrefixes {
754+
if strings.HasPrefix(lower, prefix) {
755+
virtual = true
756+
break
757+
}
758+
}
759+
if virtual {
760+
continue
761+
}
762+
addrs, err := iface.Addrs()
763+
if err != nil {
764+
continue
765+
}
766+
for _, addr := range addrs {
767+
if ipnet, ok := addr.(*net.IPNet); ok {
768+
if ipv4 := ipnet.IP.To4(); ipv4 != nil {
769+
log.Printf("[INFO] Discovered host IP address for bridged network: %s on interface %s", ipv4, iface.Name)
770+
return ipv4.String(), nil
771+
}
772+
}
773+
}
774+
}
775+
776+
return "", fmt.Errorf("unable to find a non-loopback IPv4 address for bridged networking")
697777
}
698778

699779
// GetDhcpLeasesPaths returns a copy of the DHCP leases paths.

builder/vmware/common/driver_fusion.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"bytes"
99
"fmt"
1010
"log"
11+
"net"
1112
"os"
1213
"os/exec"
1314
"path/filepath"
@@ -324,6 +325,36 @@ func (d *FusionDriver) GetVmwareDriver() VmwareDriver {
324325
return d.VmwareDriver
325326
}
326327

328+
// GetGuestIPAddress retrieves the guest IP address for the virtual machine using VMware Tools.
329+
func (d *FusionDriver) GetGuestIPAddress(vmxPath string) (string, error) {
330+
cleanVmx := filepath.Clean(vmxPath)
331+
absVmxPath, err := filepath.Abs(cleanVmx)
332+
if err != nil {
333+
return "", fmt.Errorf("failed to get absolute path for .vmx: %s", err)
334+
}
335+
336+
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "getGuestIPAddress", absVmxPath) //nolint:gosec
337+
output, err := cmd.Output()
338+
if err != nil {
339+
// VMware Tools might not be running yet.
340+
return "", fmt.Errorf("failed to retrieve IP address using VMware Tools: %s", err)
341+
}
342+
343+
// Parse the IP address from output.
344+
ipAddr := strings.TrimSpace(string(output))
345+
if ipAddr == "" {
346+
return "", fmt.Errorf("returned an empty IP address")
347+
}
348+
349+
// Validate the IP address.
350+
if net.ParseIP(ipAddr) == nil {
351+
return "", fmt.Errorf("returned an invalid IP address: %s", ipAddr)
352+
}
353+
354+
log.Printf("[INFO] Discovered guest IP address using VMware Tools: %s", ipAddr)
355+
return ipAddr, nil
356+
}
357+
327358
func (d *FusionDriver) getFusionVersion() (*version.Version, error) {
328359
var stderr bytes.Buffer
329360

builder/vmware/common/driver_mock.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,3 +304,7 @@ func (d *DriverMock) GetVmwareDriver() VmwareDriver {
304304
func (d *DriverMock) VerifyOvfTool(_ bool, _ bool) error {
305305
return nil
306306
}
307+
308+
func (d *DriverMock) GetGuestIPAddress(vmxPath string) (string, error) {
309+
return "192.168.1.100", nil
310+
}

builder/vmware/common/driver_parser.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1103,7 +1103,11 @@ func (e *ConfigDeclaration) Hardware() (net.HardwareAddr, error) {
11031103
}
11041104
}
11051105

1106-
if len(result) > 0 {
1106+
if len(result) == 0 {
1107+
return nil, fmt.Errorf("no hardware address found")
1108+
}
1109+
1110+
if len(result) > 1 {
11071111
return nil, fmt.Errorf("more than one hardware address returned : %v", result)
11081112
}
11091113

0 commit comments

Comments
 (0)