diff --git a/README.md b/README.md index d2a7319..8585cb2 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Macpine depends on QEMU >= 7.22.0: brew install qemu ``` -> **Known issue:** QEMU 11.1.1 has a regression that can hang `aarch64` instances on boot when using `vmnet` networking (see [Troubleshooting](https://beringresearch.github.io/macpine/troubleshooting/)). If you hit this, pin QEMU to 10.0.3 until it's fixed upstream. +See [Requirements](#requirements) below for a known QEMU regression and optional passwordless `vmnet` setup. ## Install from MacPorts @@ -73,6 +73,23 @@ make install # installs binaries to /usr/local/bin # PREFIX=/some/other/path make install installs to /some/other/path ``` +## Requirements + +* macOS (Apple Silicon or Intel) and QEMU >= 7.22.0, installed automatically by `brew install macpine` or manually via `brew install qemu`. +* **Known issue:** QEMU 11.1.1 has a regression that can hang `aarch64` instances on boot when using `vmnet` networking (see [Troubleshooting](https://beringresearch.github.io/macpine/troubleshooting/)). If you hit this, pin QEMU to 10.0.3 until it's fixed upstream. +* Instances with bridged (`vmnet`) networking require `sudo` for `alpine start`/`stop`/`ssh`/`exec`, **unless** [`socket_vmnet`](https://github.com/lima-vm/socket_vmnet) is installed and running. + +### Optional: bridged networking without `sudo` + +By default, `vmnet` networking (a real, LAN-reachable IP for the instance) requires root, because QEMU calls macOS's `Vmnet.framework` directly. Installing [`socket_vmnet`](https://github.com/lima-vm/socket_vmnet) removes that requirement: it does the one privileged step once, in the background, and `macpine` talks to it over a Unix socket instead. + +```bash +brew install socket_vmnet +sudo brew services start socket_vmnet # one-time; runs persistently in the background +``` + +`macpine` detects a running `socket_vmnet` daemon automatically. If it isn't found, `macpine` transparently falls back to QEMU's native `vmnet-shared` networking (requiring `sudo`, as before). + # Getting Started To create and start a new instance: diff --git a/docs/docs/install.md b/docs/docs/install.md index c960fb2..2a52b04 100644 --- a/docs/docs/install.md +++ b/docs/docs/install.md @@ -8,6 +8,21 @@ brew install qemu ``` +> **Known issue:** QEMU 11.1.1 has a regression that can hang `aarch64` instances on boot when using `vmnet` networking. See [Troubleshooting](troubleshooting.md#instance-hangs-on-boot-with-no-console-output-qemu-1111-regression) if you hit this. + +## Running `vmnet` networking without `sudo` + +Instances with `vmnet: true` in `config.yaml` (bridged networking, giving the instance a real LAN-reachable IP) normally require `alpine start`/`stop` to run as root. That's because QEMU's built-in `vmnet-shared` networking calls macOS's `Vmnet.framework` directly, which requires root privileges (or a special Apple-granted entitlement that a plain Homebrew build of QEMU doesn't have). + +To avoid `sudo` for every `vmnet` VM start/stop, install [`socket_vmnet`](https://github.com/lima-vm/socket_vmnet) — the same privileged-helper daemon used by Lima and Colima. It does the one privileged `vmnet` setup step in the background; unprivileged processes (including `macpine`) then talk to it over a Unix socket instead of calling `Vmnet.framework` themselves: + +```bash +brew install socket_vmnet +sudo brew services start socket_vmnet # one-time; runs the daemon persistently in the background +``` + +Once the daemon is running, `macpine` detects it automatically and uses it for any `vmnet` instance — no configuration needed, and no more `sudo` on `alpine start`/`stop`/`ssh`/`exec` for those instances. If `socket_vmnet` isn't installed or running, `macpine` falls back to QEMU's native `vmnet-shared` networking (requiring `sudo`) exactly as before. + ## Install the latest binary Download the [latest binary release](https://github.com/beringresearch/macpine/releases) for your system and add it to your path by placing to e.g. `/usr/local/bin/` diff --git a/qemu/ops.go b/qemu/ops.go index d552fa7..465f751 100644 --- a/qemu/ops.go +++ b/qemu/ops.go @@ -440,8 +440,15 @@ func (c *MachineConfig) Start() error { c.MACAddress = macAddress } + socketVmnetClient, socketVmnetSocket, useSocketVmnet := "", "", false + if c.VMNet { - networkDevice = "vmnet-shared,id=net0" + socketVmnetClient, socketVmnetSocket, useSocketVmnet = utils.DetectSocketVmnet() + if useSocketVmnet { + networkDevice = "socket,id=net0,fd=3" + } else { + networkDevice = "vmnet-shared,id=net0" + } } // Only parse ports of using qemu's default slirp network @@ -549,7 +556,13 @@ func (c *MachineConfig) Start() error { qemuArgs = append(qemuArgs, mountArgs...) } - cmd := exec.Command(qemuCmd, qemuArgs...) + var cmd *exec.Cmd + if useSocketVmnet { + log.Println("using socket_vmnet for unprivileged vmnet networking") + cmd = exec.Command(socketVmnetClient, append([]string{socketVmnetSocket, qemuCmd}, qemuArgs...)...) + } else { + cmd = exec.Command(qemuCmd, qemuArgs...) + } cmd.Stdout = os.Stdout diff --git a/utils/utils.go b/utils/utils.go index 88daaab..a6722bc 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -47,6 +47,46 @@ func GenerateMACAddress() (string, error) { return mac, nil } +// DetectSocketVmnet looks for a running socket_vmnet daemon +// (https://github.com/lima-vm/socket_vmnet) and its client binary, so that +// vmnet networking can be used without root privileges for the VM process +// itself. It returns ok=false if either piece isn't found, in which case +// callers should fall back to QEMU's native (root-requiring) vmnet-shared +// netdev. +func DetectSocketVmnet() (clientPath string, socketPath string, ok bool) { + socketCandidates := []string{ + "/opt/homebrew/var/run/socket_vmnet", // Homebrew on Apple Silicon + "/usr/local/var/run/socket_vmnet", // Homebrew on Intel + "/var/run/socket_vmnet", // manual/MacPorts install + } + for _, s := range socketCandidates { + fi, err := os.Stat(s) + if err != nil || fi.Mode()&os.ModeSocket == 0 { + continue + } + socketPath = s + break + } + if socketPath == "" { + return "", "", false + } + + clientCandidates := []string{ + "/opt/homebrew/opt/socket_vmnet/bin/socket_vmnet_client", + "/usr/local/opt/socket_vmnet/bin/socket_vmnet_client", + } + for _, c := range clientCandidates { + if fi, err := os.Stat(c); err == nil && !fi.IsDir() { + return c, socketPath, true + } + } + if p, err := exec.LookPath("socket_vmnet_client"); err == nil { + return p, socketPath, true + } + + return "", "", false +} + // Retry retries a function func Retry(attempts int, sleep time.Duration, f func() error) (err error) { for i := 0; i < attempts; i++ {