Repository navigation
macOS guests 5/6: ingress and fixed addresses for macOS guests - #514
Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 23c6890. Configure here.
23c6890 to
837ba07
Compare
8e6d7c8 to
8385500
Compare
8385500 to
62cae75
Compare
62cae75 to
ca64689
Compare
a18ab32 to
6e48e3e
Compare
6e48e3e to
802a888
Compare
4861a6c to
8a799f6
Compare
82a8e04 to
daa1eeb
Compare
2c54fec to
61d3638
Compare
61d3638 to
ac91b7e
Compare
sjmiller609
left a comment
There was a problem hiding this comment.
a few design questions:
-
generic TCP forwarding: should this support arbitrary TCP services, with an SSH-like CLI?
# SSH today ssh -N -L 8080:127.0.0.1:3000 user@vm # proposed Hypeman equivalent hypeman port-forward my-vm 8080:3000
both expose the remote service on local
localhost:8080, without interpreting HTTP. -
comparison with exec: exec already streams stdin/stdout over WebSocket → gRPC/vsock. should forwarding follow that pattern, streaming socket bytes instead?
-
our platform use case: do we actually need this feature, or could metro-api reach kernel-images-api through guest networking, as on Linux? what prevents that existing routing pattern? I like this feature for hypeman as it seems very useful, but also it seems more complicated if not actually necessary.
-
performance is important: CDP is latency-sensitive; screenshots and other large payloads need both low latency and high throughput. So any new network path like this if it's used for our platform use cases and not just simple things needs careful performance comparisons.
ff629f7 to
6e68565
Compare
cba6359 to
cfffeec
Compare
cfffeec to
a3051ba
Compare
sjmiller609
left a comment
There was a problem hiding this comment.
Is it doable to have Hypeman own the IP allocation as on Linux instead of using vmnet DHCP? The main concern is accidentally routing to the wrong VM during lifecycle changes or DHCP lease expiry. I think it's worth checking on, as it would be ideal to have static guest private IPs like it works on linux. Would be OK with DHCP if this is the only way that works, but we have to pay close attention to the potential cases for wrong-VM routing.
- mac-specific: the current DHCP lease parser can return expired/superseded addresses, including during startup.
- shared with Linux: Caddy caches upstream IPs for one minute, so address reuse can send old traffic to a replacement VM. fixed ingress rules also follow reusable names rather than immutable IDs.
macOS 26+ supports custom vmnet networks with DHCP reservations or static addressing. could we investigate that instead of discovering ownership from the lease file? reservations can’t change while a network is active, so we’d need to account for that.
ideally: persist instance→IP allocation, retain it through stop/standby, and coordinate reuse with cache expiry/invalidation.
can we test stop/delete/recreate, restore, and same-name reuse while continuously requesting the old route? it should reach the intended VM or fail, never return another VM’s response.
|
@sjmiller609 good call. I'm going to spike-test it now: one vmnet network per guest (macOS 26+), with a hypeman-assigned subnet and a DHCP reservation for the guest's MAC, so the IP is fixed and kept through stop/standby instead of being read from the lease file. I'll report back with results, including whether save/restore works with the new attachment. |
Route ingress to macOS guests the same way as Linux guests: Caddy resolves the target through hypeman's DNS server and proxies to the guest's IP, which for a macOS guest is its current vmnet lease. Caddy and DNS now start on macOS-only servers, and the API and resolver no longer refuse macOS targets. The Caddy daemon manager finds and checks its process with ps on macOS, which has no /proc. macOS builds still do not embed Caddy: make build-caddy-darwin builds the same version and DNS modules into bin/caddy, and hypeman looks for it next to its own binary before PATH.
Hypeman assigns a networked macOS guest a /30 from network.macos_subnet_cidr and stores its address with the instance, keeping it through stop and standby. vz-shim creates a vmnet network for that subnet with a DHCP reservation for the guest's MAC, so the address no longer comes from reading vmnet's shared lease file, which could return expired or reassigned addresses. A released address is held for five minutes before reuse, longer than ingress caches upstream addresses. A memory restore whose saved address another guest now holds is refused; a cold-boot restore gets a new address. vmnet networks need macOS 26 and Code-Hex/vz v4, which is not tagged yet, so the darwin build pins it at upstream main (787f319). vz-shim adapts to v4's caller-owned state notifications.
a3051ba to
9f35f32
Compare
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
@sjmiller609 the spike test worked! the only caveat is that our dependency for vz bindings doesn't have a release that supports vmnet so it's pinned to a specific commit |
Deleting a running guest killed its hypervisor process. A vz-shim owns its guest's vmnet network, and macOS keeps a network whose owner is killed while the VM is attached, refusing that subnet until the host restarts. Delete now asks a networked macOS guest's shim to stop the VM and exit, as stop does, and kills it only if it does not exit.

Summary
PR 5 of 6. Ingress for macOS guests, with fixed guest addresses. Base:
main(#498, #499, #500 and #506 have merged). Three commits: ingress (9c956f2), per-guest vmnet networks (9f35f32), and stopping a macOS guest's shim on delete instead of killing it (0897c3f).This replaces the earlier vsock port-forwarding design for this PR. Ingress is how callers already reach Linux guests, and native measurements showed it fits macOS guests too:
The forwarding implementation is kept on
archive/macos-guest-ports-vsock(ff629f7) if it's wanted later as a generic hypeman feature.What changes
network.macos_subnet_cidr(default192.168.128.0/20), stores the guest address with the instance and keeps it through stop and standby, as Linux does. vz-shim creates a shared-mode vmnet network for that subnet with a DHCP reservation for the guest's MAC (infinite lease). This replaces reading vmnet's shared lease file (/var/db/dhcpd_leases), which could return an expired address or one vmnet had since given to another guest.vmm.shutdown), as stop already does, and kills it only if it doesn't exit in 2 s.main(787f319). vmnet networks aren't in any tagged release (v3.8.0 predates them; v4 is untagged), so the darwin code moves from v3.7.1 to that commit. The only v4 API change affecting vz-shim is state notifications: the caller now owns the channel and events can be dropped or stale, so the shim re-reads the state after each one. Move to the v4.0.0 tag when it's released.CreateIngressno longer returns 501 there, and the API's macOS-target pre-check is gone./proc; on darwin it now usesps, the same way hypeman already finds vz-shims. The Linux helpers moved unchanged intodaemon_linux.go.make build-caddy-darwinbuilds the same version and DNS modules as Linux intobin/caddy. Hypeman looks for it next to its own binary, then onPATH, and fails to start without it.Linux code touched
lib/ingress/daemon.go:isProcessRunningandfindCaddyPIDmoved verbatim todaemon_linux.go.lib/instances/ingress_resolver.go: the macOS refusal is removed; Linux resolution is unchanged.cmd/api/main.go: ingress initialization is no longer skipped in macOS-only mode; unchanged elsewhere.Makefile: newbuild-caddy-darwintarget.cmd/api/config: newnetwork.macos_subnet_cidr, validated as IPv4 /8–/29 and not overlappingnetwork.subnet_cidr.lib/instances/delete.go: holds a macOS guest's address before removing its data; Linux deletes are unchanged.go.mod: vz v3 → v4 (darwin-only code), plus thegolang.org/xversions v4 requires (x/sysv0.46,x/cryptov0.53 and related).Testing
TestIngressResolverResolvesMacOSGuestLease: a guest resolves to its address; a guest without one has no target.TestMacOSAddressAssignment,TestMacOSAddressReuseHold,TestMacOSAddressAvailable: first free /29, skipping other instances, creates in progress and held addresses; reuse only after the hold.TestMacOSMemoryRestoreRefusesAddressNowInUseand anetwork-addresscase in the snapshot payload test; both fail without their checks.TestValidateMacOSSubnetCIDR, and the VZ starter test now checks the subnet and address passed to vz-shim.TestCaddyPIDFromPS,TestIsProcessRunningOnDarwin(darwin CI): ps parsing, and a zombie not counted as running.main: the same failures, plusTestRegistryLayerCaching(Docker Hub rate limit) andTestCreateImage_InvalidTag(fails onmaintoo when run alone).lib/hypervisor/vz,cmd/vz-shim, macOSlib/instances): pass.lib/ingress,lib/instancesandcmd/api(tests included): clean.{instance}pattern rules routed to a running guest's lease; an unknown instance got 503; Caddy started and stopped with hypeman. CDP round trip through Caddy was 0.27 ms p50 versus 0.22 ms direct to the guest IP, with the same large-payload throughput.TestDeleteStopsMacOSShimBeforeKilling: delete asks a networked macOS guest's shim to stop; Linux guests are killed as before. Fails without the fix. On the QA host, deleting running guests left their networks fully stopped, where before the fix each delete leaked its subnet.Caveats
Caddy needs local network access to dial guests. On the QA host, a hypeman started over SSH lost it once the session ended, and a launchd daemon running as a regular user never had it; Caddy returned 502 ("no route to host"). Allowing the guest pool with
AllowedEthernetLocalNetworkAddresses(Apple TN3179) fixed it without root. Documented indocs/macos-experimental.md.Starting a macOS guest needs the hypeman user logged in on the host; with nobody logged in after a restart, VZ refused with
Code=-9.Saving a macOS guest's memory (standby, memory snapshots) needs that user's screen unlocked; with it locked, VZ fails with
Code=11("permission denied") because it cannot create its Secure Enclave key. Documented with thesysadminctl -screenLock offsetting.Guests can reach the host's own listeners, including Caddy's ports, sshd and Screen Sharing. Hosts running guests for different tenants should not expose other host services on guest networks. Documented in
docs/macos-experimental.md.Guests are on separate vmnet networks, so they can't reach each other's addresses.
Instances started before this change keep the shared NAT and report no address until their next start. A memory snapshot saved on the shared NAT restores on it.
Changing
network.macos_subnet_cidrdoesn't move existing guests; they keep their stored addresses.A shim that dies while its guest runs (crash, external SIGKILL) still leaks its network: that guest can't start and its subnet can't be reused until the host restarts. Not handled automatically yet.
Stack
Each PR builds on the one below it. macOS logic lives in its own files (
macos*.go,*_darwin.go,exec_lifecycle.go); shared Linux files get small hooks such asif stored.MacOS != nil.