Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Expand Down
15 changes: 15 additions & 0 deletions docs/docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/`
Expand Down
17 changes: 15 additions & 2 deletions qemu/ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
40 changes: 40 additions & 0 deletions utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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++ {
Expand Down
Loading