Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 65 additions & 4 deletions builder/vmware/common/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,9 @@ type Driver interface {
// HostIP retrieves the host IP address for the virtual machine based on the state.
HostIP(multistep.StateBag) (string, error)

// GetGuestIPAddress retrieves the guest IP address for the virtual machine using VMware Tools.
GetGuestIPAddress(string) (string, error)

// Export exports a virtual machine using the provided arguments.
Export([]string) error

Expand Down Expand Up @@ -484,6 +487,10 @@ type VmwareDriver struct {
// This method returns an object with the NetworkNameMapper interface
// that maps network to device and vice versa.
NetworkMapper func() (NetworkNameMapper, error)

// IPFinder returns the IP address for a given device. If nil, getHostIPForBridgedNetwork is used.
// This allows for mocking in tests.
IPFinder func(device string) (string, error)
}

// GuestAddress retrieves the MAC address of a guest virtual machine from the .vmx configuration.
Expand Down Expand Up @@ -586,7 +593,6 @@ func (d *VmwareDriver) HostAddress(state multistep.StateBag) (string, error) {

var lastError error
for _, device := range devices {
// parse dhcpd configuration
pathDhcpConfig := d.DhcpConfPath(device)
if _, err := os.Stat(pathDhcpConfig); err != nil {
return "", fmt.Errorf("unable to find vmnetdhcp conf file: %s", pathDhcpConfig)
Expand All @@ -613,7 +619,7 @@ func (d *VmwareDriver) HostAddress(state multistep.StateBag) (string, error) {

// we didn't find it, so search through our interfaces for the device name
interfaceList, err := net.Interfaces()
if err == nil {
if err != nil {
return "", err
}

Expand Down Expand Up @@ -667,7 +673,31 @@ func (d *VmwareDriver) HostIP(state multistep.StateBag) (string, error) {

var lastError error
for _, device := range devices {
// parse dhcpd configuration
// Check if this is a bridged network device.
networkName, err := netmap.DeviceIntoName(device)
isBridged := err == nil && strings.EqualFold(networkName, "bridged")

if isBridged {
// Bridged networks connect to the physical network.
// Find the host's IP address on the physical network interface.
log.Printf("[INFO] Detected bridged network for device %s, finding host IP on physical network", device)

var address string
var err error
if d.IPFinder != nil {
address, err = d.IPFinder(device)
} else {
// Get the first non-loopback interface IP address.
address, err = getHostIPForBridgedNetwork()
}
if err != nil {
lastError = err
continue
}
return address, nil
}

// For non-bridged networks, use the DHCP leases path.
pathDhcpConfig := d.DhcpConfPath(device)
if _, err := os.Stat(pathDhcpConfig); err != nil {
return "", fmt.Errorf("unable to find vmnetdhcp conf file: %s", pathDhcpConfig)
Expand All @@ -678,7 +708,6 @@ func (d *VmwareDriver) HostIP(state multistep.StateBag) (string, error) {
continue
}

// find the entry configured in the dhcpd
interfaceConfig, err := config.HostByName(device)
if err != nil {
lastError = err
Expand All @@ -696,6 +725,38 @@ func (d *VmwareDriver) HostIP(state multistep.StateBag) (string, error) {
return "", fmt.Errorf("unable to find host IP from devices %v, last error: %s", devices, lastError)
}

// getHostIPForBridgedNetwork returns the host's IP address on the physical
// network for use with bridged networking.
func getHostIPForBridgedNetwork() (string, error) {
interfaces, err := net.Interfaces()
if err != nil {
return "", fmt.Errorf("unable to enumerate network interfaces: %s", err)
}

for _, iface := range interfaces {
// Skip loopback and down interfaces.
if iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 {
continue
}

addrs, err := iface.Addrs()
if err != nil {
continue
}

for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok {
if ipv4 := ipnet.IP.To4(); ipv4 != nil {
log.Printf("[INFO] Found host IP for bridged network: %s on interface %s", ipv4.String(), iface.Name)
return ipv4.String(), nil
}
}
}
}

return "", fmt.Errorf("unable to find a non-loopback IPv4 address on any interface")
Comment on lines +730 to +757
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getHostIPForBridgedNetwork() returns the first non-loopback IPv4 address across all UP interfaces. On hosts with multiple active interfaces (VPN, Docker/Podman bridges, veths), this can pick an address that is not reachable from the bridged guest, breaking HTTP/communicator connectivity. Consider selecting the interface associated with the default route (or otherwise filtering out virtual/bridge interfaces), or deriving the bridged physical interface from VMware’s networking/bridge mapping configuration before choosing the IP.

Copilot uses AI. Check for mistakes.
}

// GetDhcpLeasesPaths returns a copy of the DHCP leases paths.
func GetDhcpLeasesPaths() []string {
return append([]string(nil), dhcpLeasesPaths...)
Expand Down
31 changes: 31 additions & 0 deletions builder/vmware/common/driver_fusion.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"bytes"
"fmt"
"log"
"net"
"os"
"os/exec"
"path/filepath"
Expand Down Expand Up @@ -324,6 +325,36 @@ func (d *FusionDriver) GetVmwareDriver() VmwareDriver {
return d.VmwareDriver
}

// GetGuestIPAddress retrieves the guest IP address for the virtual machine using VMware Tools.
func (d *FusionDriver) GetGuestIPAddress(vmxPath string) (string, error) {
cleanVmx := filepath.Clean(vmxPath)
absVmxPath, err := filepath.Abs(cleanVmx)
if err != nil {
return "", fmt.Errorf("failed to get absolute path for .vmx: %s", err)
}

cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "getGuestIPAddress", absVmxPath, "-wait") //nolint:gosec
output, err := cmd.Output()
if err != nil {
// VMware Tools might not be running yet.
return "", fmt.Errorf("failed to retrieve IP address using VMware Tools: %s", err)
}

// Parse the IP address from output.
ipAddr := strings.TrimSpace(string(output))
if ipAddr == "" {
return "", fmt.Errorf("returned an empty IP address")
}

// Validate the IP address.
if net.ParseIP(ipAddr) == nil {
return "", fmt.Errorf("returned an invalid IP address: %s", ipAddr)
}

log.Printf("[INFO] Discovered guest IP address using VMware Tools: %s", ipAddr)
return ipAddr, nil
}

func (d *FusionDriver) getFusionVersion() (*version.Version, error) {
var stderr bytes.Buffer

Expand Down
4 changes: 4 additions & 0 deletions builder/vmware/common/driver_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,3 +304,7 @@ func (d *DriverMock) GetVmwareDriver() VmwareDriver {
func (d *DriverMock) VerifyOvfTool(_ bool, _ bool) error {
return nil
}

func (d *DriverMock) GetGuestIPAddress(vmxPath string) (string, error) {
return "192.168.1.100", nil
}
6 changes: 5 additions & 1 deletion builder/vmware/common/driver_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -1103,7 +1103,11 @@ func (e *ConfigDeclaration) Hardware() (net.HardwareAddr, error) {
}
}

if len(result) > 0 {
if len(result) == 0 {
return nil, fmt.Errorf("no hardware address found")
}

if len(result) > 1 {
return nil, fmt.Errorf("more than one hardware address returned : %v", result)
}

Expand Down
25 changes: 25 additions & 0 deletions builder/vmware/common/driver_workstation.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"fmt"
"html/template"
"log"
"net"
"os"
"os/exec"
"path/filepath"
Expand Down Expand Up @@ -78,6 +79,30 @@ func (d *WorkstationDriver) GetVmwareDriver() VmwareDriver {
return d.VmwareDriver
}

// GetGuestIPAddress retrieves the guest IP address for the virtual machine using VMware Tools.
func (d *WorkstationDriver) GetGuestIPAddress(vmxPath string) (string, error) {
cmd := exec.Command(d.VmrunPath, "-T", "ws", "getGuestIPAddress", vmxPath, "-wait")
output, err := cmd.Output()
if err != nil {
// VMware Tools might not be running yet.
return "", fmt.Errorf("failed to retrieve IP address using VMware Tools: %s", err)
}

// Parse the IP address from output.
ipAddr := strings.TrimSpace(string(output))
if ipAddr == "" {
return "", fmt.Errorf("returned an empty IP address")
}

// Validate the IP address.
if net.ParseIP(ipAddr) == nil {
return "", fmt.Errorf("returned an invalid IP address: %s", ipAddr)
}

log.Printf("[INFO] Discovered guest IP address using VMware Tools: %s", ipAddr)
return ipAddr, nil
}

// Clone creates a copy of the source virtual machine at the destination path.
func (d *WorkstationDriver) Clone(dst, src string, linked bool, snapshot string) error {

Expand Down
46 changes: 38 additions & 8 deletions builder/vmware/common/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import (
"fmt"
"log"
"net"
"strings"

"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer-plugin-sdk/sdk-internals/communicator/ssh"
"golang.org/x/net/proxy"
)

// CommHost returns a function that determines the IP address of the guest that is ready to accept connections.
// CommHost returns a function that determines the IP address of the guest that
// is ready to accept connections.
func CommHost(config *SSHConfig) func(multistep.StateBag) (string, error) {
return func(state multistep.StateBag) (string, error) {
driver := state.Get("driver").(Driver)
Expand All @@ -28,10 +30,38 @@ func CommHost(config *SSHConfig) func(multistep.StateBag) (string, error) {

port := comm.Port()

// Get the list of potential addresses that the guest might use.
hosts, err := driver.PotentialGuestIP(state)
if err != nil {
return "", fmt.Errorf("failed to lookup IP address: %s", err)
// Check if this is a bridged network (case-insensitive).
network := state.Get("vmnetwork").(string)
isBridged := strings.EqualFold(network, "bridged")

var hosts []string
var err error

if isBridged {
// For bridged networks, wait for VMware Tools to provide the IP address.
if state.Get("vmtools_ip_attempt") == nil {
log.Printf("[INFO] Waiting for IP address from VMware Tools...")
state.Put("vmtools_ip_attempt", true)
}

vmxPath := state.Get("vmx_path").(string)
if addr, vmrunErr := driver.GetGuestIPAddress(vmxPath); vmrunErr == nil && addr != "" {
hosts = []string{addr}
} else {
return "", fmt.Errorf("waiting for VMware Tools to start: %s", vmrunErr)
}
} else {
// For NAT/host-only networks, use DHCP leases as the primary method.
hosts, err = driver.PotentialGuestIP(state)
if err != nil {
// Fallback: Check to see if VMware Tools can provide the IP address.
vmxPath := state.Get("vmx_path").(string)
if addr, vmrunErr := driver.GetGuestIPAddress(vmxPath); vmrunErr == nil && addr != "" {
hosts = []string{addr}
} else {
return "", fmt.Errorf("failed to lookup IP address: %s", err)
}
}
}

if len(hosts) == 0 {
Expand All @@ -54,15 +84,15 @@ func CommHost(config *SSHConfig) func(multistep.StateBag) (string, error) {
var connFunc func() (net.Conn, error)
for _, host := range hosts {
if pAddr != "" {
// Connect through a bastion host.
// Connect using a bastion host.
connFunc = ssh.ProxyConnectFunc(pAddr, pAuth, "tcp", fmt.Sprintf("%s:%d", host, port))
} else {
// Connect directly to the host.
// Connect directly.
connFunc = ssh.ConnectFunc("tcp", fmt.Sprintf("%s:%d", host, port))
}
conn, err := connFunc()

// If we can connect, then we can use this IP address.
// If the connection is successful, use this IP address.
if err == nil {
err := conn.Close()
if err != nil {
Expand Down