diff --git a/README.md b/README.md index fb79541..519730a 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ macOS VM engine for x86 Linux/KVM, built on [cocoon](https://github.com/cocoonst - **Fully-automated macOS install** — a GitHub Action boots OpenCore, erases APFS, drives the installer by OCR, and publishes a golden qcow2 (Tahoe 26 / Sequoia 15) to ghcr; the `:26` image is **SSH-ready** (`cocoon`/`cocoon`) - **Docker-like CLI** — `create`, `run`, `start`, `stop`, `list`, `inspect`, `console`, `rm`, `snapshot`, `restore`, `clone` - **Parallel Range image pull** — the multi-GB qcow2 is pulled in 8 concurrent HTTP Range chunks with an sha256 digest check (oras-go for auth; no `oras` binary needed) -- **Per-VM Apple identity** — `--random-smbios` injects a unique serial/MLB/UUID/ROM (guest MAC = ROM) into a per-VM OpenCore, so clones never share a serial +- **Per-VM Apple identity** — `--random-smbios` injects a unique serial/MLB/UUID/ROM into a per-VM OpenCore; the ROM is the guest MAC outside CNI, so clones never share a serial - **CNI networking with TC redirect** — `--net cni` joins cocoon's forwarding plane so the guest DHCPs a real LAN IP; also user/tap/bridge; reachable VNC (launch-scoped, password-gated on CNI) - **Snapshot, clone & data disks** — offline qcow2-internal snapshots, CoW clones that cold-boot a fresh Apple identity, and up to 4 extra AHCI data disks - **Intel & AMD, built on cocoon** — one boot recipe boots both hosts; imports cocoon's `cloudimg` store, `network` plane, and copy-on-write conventions diff --git a/cmd/vm/lifecycle.go b/cmd/vm/lifecycle.go index f905beb..c3745ec 100644 --- a/cmd/vm/lifecycle.go +++ b/cmd/vm/lifecycle.go @@ -154,7 +154,7 @@ func (h *Handler) create(cmd *cobra.Command, image string) (*record, error) { if r.DataDisks, err = createDataDisks(ctx, dir, diskSpecs); err != nil { return nil, err } - // OpenCore before networking: a random SMBIOS sets r.MAC = ROM, which prepareNet keeps as the guest MAC. + // OpenCore seeds the default guest MAC from ROM before network provisioning. randomSMBIOS, _ := cmd.Flags().GetBool("random-smbios") if err = prepareOpenCore(ctx, dir, oc, randomSMBIOS, r); err != nil { return nil, err diff --git a/cmd/vm/net_linux.go b/cmd/vm/net_linux.go index 2910fe9..e51141b 100644 --- a/cmd/vm/net_linux.go +++ b/cmd/vm/net_linux.go @@ -80,8 +80,10 @@ func provisionNet(cmd *cobra.Command, r *record) (tap, netns, mac string, err er if len(cfgs) == 0 { return "", "", "", errors.New("network add returned no NIC") } - // SMBIOS ROM wins as the guest MAC; cocoon's generated MAC is only a fallback - mac = cmp.Or(r.MAC, cfgs[0].MAC) + mac = cfgs[0].MAC + if r.NetMode != netCNI { + mac = cmp.Or(r.MAC, mac) + } return cfgs[0].TAP, nsPath, mac, nil } diff --git a/cmd/vm/utils.go b/cmd/vm/utils.go index 42bd11b..a363fa2 100644 --- a/cmd/vm/utils.go +++ b/cmd/vm/utils.go @@ -99,9 +99,7 @@ func applyNet(cmd *cobra.Command, r *record) error { if err != nil { return err } - if r.MAC == "" { - r.MAC = mac - } + r.MAC = mac if netTap != "" { r.Tap, r.Netns, r.TapOwned = netTap, netns, userTap == "" } diff --git a/docs/index.md b/docs/index.md index 1b4b273..60d9e38 100644 --- a/docs/index.md +++ b/docs/index.md @@ -46,7 +46,7 @@ cocoon-macos CLI ──► image: pull the golden macOS qcow2 from ghcr (paralle - **COW overlays** — an instant copy-on-write clone of the immutable golden base per VM - **Per-VM Apple identity** — `--random-smbios` injects a unique serial/MLB/UUID/ROM - (guest MAC = ROM) into a per-VM OpenCore, so clones never share a serial + into a per-VM OpenCore; the ROM is the guest MAC outside CNI - **CNI networking with TC redirect** — `--net cni` joins cocoon's forwarding plane so the guest DHCPs a real LAN IP; also `user`/`tap`/`bridge` - **Reachable VNC** — loopback VNC on user/tap/bridge; a host-side proxy fronts diff --git a/docs/networking.md b/docs/networking.md index 979f4d7..75b4faf 100644 --- a/docs/networking.md +++ b/docs/networking.md @@ -11,8 +11,12 @@ `tap`/`bridge`/`cni` make a macOS VM join the **same** forwarding plane as cocoon's Cloud Hypervisor / Firecracker VMs on the node, so the guest can DHCP a **real LAN IP** from the upstream -network. The guest NIC MAC stays equal to the SMBIOS ROM. Auto-create (`bridge`/`cni`) is Linux-only -(needs `CAP_NET_ADMIN`); `user` and a pre-created `--tap` work everywhere. +network. In `tap` and `bridge` mode the guest NIC MAC stays equal to the SMBIOS ROM. Auto-create +(`bridge`/`cni`) is Linux-only (needs `CAP_NET_ADMIN`); `user` and a pre-created `--tap` work +everywhere. + +In `cni` mode, the guest NIC uses the MAC CNI assigned to `eth0` because `macspoofchk` allow-lists +that address; the SMBIOS ROM identity remains unchanged. Auto-created devices carry cocoon-macos's own host name family (`net_scope` `cm`: TAPs `cm-`, netns `cm-`), so a cocoon daemon's GC on the same node never reads a live diff --git a/qemu/launch.go b/qemu/launch.go index 524bf7d..3ff2c1c 100644 --- a/qemu/launch.go +++ b/qemu/launch.go @@ -36,7 +36,7 @@ type Spec struct { OpenCore string OVMFCode string OVMFVars string - MAC string // set to the SMBIOS ROM for --random-smbios + MAC string // guest NIC MAC: SMBIOS ROM by default, CNI eth0 in CNI mode Tap string // pre-created host TAP; set => -netdev tap (bridged/routed), empty => user-mode SLIRP MonSock string diff --git a/qemu/smbios.go b/qemu/smbios.go index 591f90c..8e8812d 100644 --- a/qemu/smbios.go +++ b/qemu/smbios.go @@ -17,7 +17,7 @@ type SMBIOS struct { Serial string `json:"serial"` // SystemSerialNumber MLB string `json:"mlb"` // board serial UUID string `json:"uuid"` // SystemUUID - ROM string `json:"rom"` // 6-byte ROM as hex; also the guest en0 MAC + ROM string `json:"rom"` // 6-byte ROM as hex; default guest NIC MAC outside CNI } // RandomSMBIOS generates a unique per-VM identity (fixed model + random serial/MLB/UUID/ROM). @@ -41,7 +41,7 @@ func RandomSMBIOS() (SMBIOS, error) { return SMBIOS{Model: smbiosModel, Serial: serial, MLB: mlb, UUID: uuid, ROM: rom}, nil } -// MAC returns the ROM formatted as the guest NIC MAC (so en0 == ROM, as iServices expects). +// MAC returns the ROM formatted as the default guest NIC MAC; CNI supplies its own runtime MAC. func (s SMBIOS) MAC() string { b, err := hex.DecodeString(s.ROM) if err != nil || len(b) != 6 {