a73x

internal/agent/vfkit/guestnet.go

Ref:   Size: 4.5 KiB   History

package vfkit

import (
	"net"
	"net/netip"
	"os"
	"strings"
)

// GuestCIDR reports the subnet this Mac's guests are on: the network of
// whichever host interface contains an address vmnet has actually handed out.
// It returns "" when it cannot say, which by the empty-means-no-answer rule
// leaves the fleet's record alone and retries on the next report.
//
// Both halves are things an unprivileged agent can see, which is the whole
// reason for this shape. The obvious source — vmnet's own configuration at
// /Library/Preferences/SystemConfiguration/com.apple.vmnet.plist, which states
// the address and netmask outright — is ROOT-ONLY, and the agent deliberately
// does not run as root on macOS: the guest network belongs to
// Virtualization.framework and the entitlement to vfkit, so there is nothing
// else it would need privileges for. Reading one config file is a poor reason
// to take them. (`defaults` cannot read that file either, for an unrelated
// reason: it resolves domains through cfprefsd rather than opening the path.)
//
// The lease database is world-readable and the interface list needs no
// privileges at all, and between them they give exactly what the plist would
// have: an address inside the guest network, and a netmask to size it with.
// Neither is guessed — in particular the prefix length is the interface's own,
// so nothing here assumes a /24 the way deriving from a lease alone would.
//
// A host with no guests reports nothing. vmnet's bridge exists only while a
// guest is attached to it, and the lease database is empty until one has asked
// for an address, so a Mac that has never run a guest has no guest network to
// describe. That is honest rather than a failure, and it costs nothing: the
// answer arrives with the first guest, and on every report after it.
func GuestCIDR() string {
	return guestCIDR(readFile(defaultLeasesPath), hostNetworks())
}

// guestCIDR is GuestCIDR with both readings injected, so the pairing is
// testable on a machine that has neither of them.
func guestCIDR(leases string, networks []netip.Prefix) string {
	for _, addr := range leasedAddresses(leases) {
		for _, pfx := range networks {
			if pfx.Contains(addr) {
				return pfx.String()
			}
		}
	}
	return ""
}

// hostNetworks returns every IPv4 network configured on this host's interfaces,
// each masked to its own prefix length. Addresses are flattened across
// interfaces deliberately: which interface holds the guest network is not
// something this needs to know, and the name vmnet uses is not contractual —
// bridge100 today, bridge101 for a second vmnet network, while bridge0 is
// Thunderbolt's and holds no IPv4 address at all. A network containing an
// address vmnet leased to a guest IS the guest network, whatever it is called.
func hostNetworks() []netip.Prefix {
	ifaces, err := net.Interfaces()
	if err != nil {
		return nil
	}
	var out []netip.Prefix
	for _, iface := range ifaces {
		addrs, err := iface.Addrs()
		if err != nil {
			continue
		}
		for _, a := range addrs {
			if pfx, ok := prefixOf(a); ok {
				out = append(out, pfx)
			}
		}
	}
	return out
}

func prefixOf(a net.Addr) (netip.Prefix, bool) {
	ipnet, ok := a.(*net.IPNet)
	if !ok || ipnet.IP.To4() == nil {
		return netip.Prefix{}, false
	}
	ones, bits := ipnet.Mask.Size()
	// A non-contiguous mask reports (0, 0), and a /0 on a host interface is not
	// a network anything is meaningfully "on".
	if bits != 32 || ones == 0 {
		return netip.Prefix{}, false
	}
	host, ok := netip.AddrFromSlice(ipnet.IP.To4())
	if !ok {
		return netip.Prefix{}, false
	}
	return netip.PrefixFrom(host, ones).Masked(), true
}

// readFile returns path's contents, or "" when it cannot be read.
func readFile(path string) string {
	raw, err := os.ReadFile(path)
	if err != nil {
		return ""
	}
	return string(raw)
}

// leasedAddresses returns every address in the lease database. Any one of them
// locates the guest network — they are all on it — so the caller takes the
// first that lands inside a live interface. Stale entries from destroyed VMs
// are useful here rather than a problem: they still name the subnet, and one
// that no longer matches any interface is simply skipped.
func leasedAddresses(leases string) []netip.Addr {
	var out []netip.Addr
	for _, line := range strings.Split(leases, "\n") {
		key, value, ok := strings.Cut(strings.TrimSpace(line), "=")
		if !ok || key != "ip_address" {
			continue
		}
		if a, err := netip.ParseAddr(parseIP(value)); err == nil && a.Is4() {
			out = append(out, a)
		}
	}
	return out
}