Summary
On macOS (vz), instances come up with an IP and a default route, the server logs
allocated network, and everything looks healthy — but the guest cannot reach anything,
including its own gateway. Guests are configured with an address from
network.subnet_cidr (default 10.100.0.0/16), while Virtualization.framework's NAT
bridge is bridge100 at 192.168.64.1/24. The 10.100.0.0/16 subnet does not exist
anywhere on the host, so all guest traffic is blackholed.
This makes a default macOS install unusable for any workload that touches the network
(apt/apk, image pulls from inside a VM, outbound API calls).
Environment
- hypeman-api v0.3.0 (installed via
curl -fsSL https://get.hypeman.sh | bash)
- hypeman CLI v0.16.1
- macOS 26.2 (build 25C56), Apple Silicon (arm64)
- Hypervisor:
vz (Virtualization.framework)
Reproduction
$ hypeman run --name demo nginx:alpine
$ hypeman exec demo -- /bin/sh -c 'ip -4 -o addr show eth0; ip route; ping -c2 -W2 10.100.0.1'
10.100.225.140/16
default via 10.100.0.1 dev eth0
2 packets transmitted, 0 packets received, 100% packet loss # cannot reach its own gateway
Meanwhile on the host:
$ ifconfig bridge100 | grep inet
inet 192.168.64.1 netmask 0xffffff00 broadcast 192.168.64.255
$ ifconfig | grep "inet 10.100" # nothing
Confirming the subnet is the whole problem — reconfiguring the guest by hand onto the
real vmnet subnet restores full connectivity:
$ hypeman exec demo -- /bin/sh -c '
ip addr del 10.100.225.140/16 dev eth0
ip route del default via 10.100.0.1 dev eth0
ip addr add 192.168.64.200/24 dev eth0
ip route add default via 192.168.64.1 dev eth0
echo "nameserver 192.168.64.1" > /etc/resolv.conf
wget -qO- -T10 https://registry-1.docker.io/v2/'
wget: server returned error: HTTP/1.1 401 Unauthorized # TCP+TLS egress works
(Note ICMP to the internet stays blocked — vmnet NAT doesn't forward it — so egress has
to be tested with TCP rather than ping.)
Root cause
There are two sources of truth for the default network, and on macOS they disagree. The
wrong one wins.
-
lib/network/manager.go:120-128 — Initialize() seeds the cached default network
from m.config.Network.SubnetCIDR, which defaults to "10.100.0.0/16" at
cmd/api/config/config.go:329. This value is not platform-conditional.
-
lib/network/bridge_darwin.go:63-71 — queryNetworkState() returns the correct
macOS values (192.168.64.1, 192.168.64.0/24).
-
lib/network/allocate.go:424-433 — getOrInitDefaultNetwork() returns the cache
first:
func (m *manager) getOrInitDefaultNetwork(ctx context.Context) (*Network, error) {
if network := m.cachedDefaultNetwork(); network != nil {
return network, nil // <- always populated by Initialize() from config
}
network, err := m.getDefaultNetwork(ctx) // <- only path that reads queryNetworkState()
...
Because Initialize() always populates the cache, the darwin-aware branch is
effectively unreachable.
-
lib/network/allocate.go:203 allocates the guest IP from network.Subnet and
:230/:239 set Gateway: network.Gateway, which are then pushed to the guest agent
(ReconfigureNetworkRequest). So the guest is statically configured onto 10.100.x.x.
This also contradicts the intended design stated in the code and docs:
lib/network/bridge_darwin.go:65 — "The actual IP will be assigned by
Virtualization.framework's DHCP"
lib/network/README.md — "macOS | NAT | Virtualization.framework built-in NAT
(192.168.64.0/24)"
Expected
On macOS the guest should either take a DHCP lease from vz's NAT (as
bridge_darwin.go:65 intends), or be statically configured from the darwin
queryNetworkState() values — not from the Linux-shaped subnet_cidr default.
Suggested fix
Make the darwin backend the authority for its own addressing rather than layering a
Linux-shaped config default over it. Options, roughly in order of preference:
- Have
Initialize() seed the cache from queryNetworkState() when the platform
backend supplies concrete values, falling back to subnet_cidr only when it doesn't.
This removes the second source of truth entirely.
- Failing that, make the
subnet_cidr default platform-conditional
(192.168.64.0/24 on darwin).
A regression test on the existing macOS arm64 CI runner asserting that a booted guest can
complete a TCP connection off-box would catch this class of bug — the current darwin tests
cover fork speed, Rosetta, and reflink copy, but nothing asserts guest egress.
Workaround
Reconfigure the guest onto 192.168.64.0/24 as shown above. It survives
standby/restore.
Summary
On macOS (
vz), instances come up with an IP and a default route, the server logsallocated network, and everything looks healthy — but the guest cannot reach anything,including its own gateway. Guests are configured with an address from
network.subnet_cidr(default10.100.0.0/16), while Virtualization.framework's NATbridge is
bridge100at192.168.64.1/24. The 10.100.0.0/16 subnet does not existanywhere on the host, so all guest traffic is blackholed.
This makes a default macOS install unusable for any workload that touches the network
(
apt/apk, image pulls from inside a VM, outbound API calls).Environment
curl -fsSL https://get.hypeman.sh | bash)vz(Virtualization.framework)Reproduction
Meanwhile on the host:
Confirming the subnet is the whole problem — reconfiguring the guest by hand onto the
real vmnet subnet restores full connectivity:
(Note ICMP to the internet stays blocked — vmnet NAT doesn't forward it — so egress has
to be tested with TCP rather than
ping.)Root cause
There are two sources of truth for the default network, and on macOS they disagree. The
wrong one wins.
lib/network/manager.go:120-128—Initialize()seeds the cached default networkfrom
m.config.Network.SubnetCIDR, which defaults to"10.100.0.0/16"atcmd/api/config/config.go:329. This value is not platform-conditional.lib/network/bridge_darwin.go:63-71—queryNetworkState()returns the correctmacOS values (
192.168.64.1,192.168.64.0/24).lib/network/allocate.go:424-433—getOrInitDefaultNetwork()returns the cachefirst:
Because
Initialize()always populates the cache, the darwin-aware branch iseffectively unreachable.
lib/network/allocate.go:203allocates the guest IP fromnetwork.Subnetand:230/:239setGateway: network.Gateway, which are then pushed to the guest agent(
ReconfigureNetworkRequest). So the guest is statically configured onto 10.100.x.x.This also contradicts the intended design stated in the code and docs:
lib/network/bridge_darwin.go:65— "The actual IP will be assigned byVirtualization.framework's DHCP"
lib/network/README.md— "macOS | NAT | Virtualization.framework built-in NAT(192.168.64.0/24)"
Expected
On macOS the guest should either take a DHCP lease from vz's NAT (as
bridge_darwin.go:65intends), or be statically configured from the darwinqueryNetworkState()values — not from the Linux-shapedsubnet_cidrdefault.Suggested fix
Make the darwin backend the authority for its own addressing rather than layering a
Linux-shaped config default over it. Options, roughly in order of preference:
Initialize()seed the cache fromqueryNetworkState()when the platformbackend supplies concrete values, falling back to
subnet_cidronly when it doesn't.This removes the second source of truth entirely.
subnet_cidrdefault platform-conditional(
192.168.64.0/24on darwin).A regression test on the existing macOS arm64 CI runner asserting that a booted guest can
complete a TCP connection off-box would catch this class of bug — the current darwin tests
cover fork speed, Rosetta, and reflink copy, but nothing asserts guest egress.
Workaround
Reconfigure the guest onto
192.168.64.0/24as shown above. It survivesstandby/restore.