mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 21:03:19 +00:00
319 lines
9.4 KiB
Markdown
319 lines
9.4 KiB
Markdown
# v86 Sandbox Evaluation
|
|
|
|
v86 is an x86 emulator written in JavaScript/WebAssembly that can run Linux in the browser or Node.js. This document details our evaluation for using it as a sandboxed execution environment.
|
|
|
|
## Overview
|
|
|
|
- **What it is**: x86 PC emulator (32-bit, Pentium 4 level)
|
|
- **How it works**: Translates machine code to WebAssembly at runtime
|
|
- **Guest OS**: Alpine Linux 3.21 (32-bit x86)
|
|
- **Available packages**: Node.js 22, Python 3.12, git, curl, etc. (full Alpine repos)
|
|
|
|
## Key Findings
|
|
|
|
### What Works
|
|
|
|
| Feature | Status | Notes |
|
|
|---------|--------|-------|
|
|
| Outbound TCP | ✅ | HTTP, HTTPS, TLS all work |
|
|
| Outbound UDP | ✅ | DNS queries work |
|
|
| WebSocket client | ✅ | Can connect to external WebSocket servers |
|
|
| File I/O | ✅ | 9p filesystem for host<->guest file exchange |
|
|
| State save/restore | ✅ | ~80-100MB state files, instant resume |
|
|
| Package persistence | ✅ | Installed packages persist in saved state |
|
|
| npm install | ✅ | Works (outbound HTTPS) |
|
|
| git clone | ✅ | Works (outbound HTTPS) |
|
|
|
|
### What Doesn't Work
|
|
|
|
| Feature | Status | Notes |
|
|
|---------|--------|-------|
|
|
| Inbound connections | ❌ | VM is behind NAT (10.0.2.x), needs port forwarding |
|
|
| ICMP ping | ❌ | Userspace network stack limitation |
|
|
| 64-bit | ❌ | v86 only emulates 32-bit x86 |
|
|
|
|
## Architecture
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ Host (Node.js) │
|
|
│ │
|
|
│ ┌──────────────┐ ┌─────────────────────────────┐ │
|
|
│ │ rootlessRelay│◄───►│ v86 │ │
|
|
│ │ (WebSocket) │ │ ┌─────────────────────┐ │ │
|
|
│ │ │ │ │ Alpine Linux │ │ │
|
|
│ │ - DHCP │ │ │ - Node.js 22 │ │ │
|
|
│ │ - DNS proxy │ │ │ - Python 3.12 │ │ │
|
|
│ │ - NAT │ │ │ - etc. │ │ │
|
|
│ └──────────────┘ │ └─────────────────────┘ │ │
|
|
│ │ │ │ │ │
|
|
│ │ │ 9p filesystem │ │
|
|
│ ▼ │ │ │ │
|
|
│ Internet │ ▼ │ │
|
|
│ │ Host filesystem │ │
|
|
│ └─────────────────────────────┘ │
|
|
└─────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
## Components & Sizes
|
|
|
|
| Component | Size | Purpose |
|
|
|-----------|------|---------|
|
|
| v86.wasm | ~2 MB | x86 emulator |
|
|
| libv86.mjs | ~330 KB | JavaScript runtime |
|
|
| seabios.bin | ~128 KB | BIOS |
|
|
| vgabios.bin | ~36 KB | VGA BIOS |
|
|
| Alpine rootfs | ~57 MB | Compressed filesystem (loaded on-demand) |
|
|
| alpine-fs.json | ~160 KB | Filesystem index |
|
|
| rootlessRelay | ~75 KB | Network relay |
|
|
| **Total** | **~60 MB** | Without saved state |
|
|
| Saved state | ~80-100 MB | Optional, for instant resume |
|
|
|
|
## Installation
|
|
|
|
```bash
|
|
npm install v86 ws
|
|
```
|
|
|
|
## Building the Alpine Image
|
|
|
|
v86 provides Docker tooling to build the Alpine image:
|
|
|
|
```bash
|
|
git clone https://github.com/copy/v86.git
|
|
cd v86/tools/docker/alpine
|
|
|
|
# Edit Dockerfile to add packages:
|
|
# ENV ADDPKGS=nodejs,npm,python3,git,curl
|
|
|
|
./build.sh
|
|
```
|
|
|
|
This creates:
|
|
- `images/alpine-fs.json` - Filesystem index
|
|
- `images/alpine-rootfs-flat/` - Compressed file chunks
|
|
|
|
## Network Relay Setup
|
|
|
|
v86 needs a network relay for TCP/UDP connectivity. We use rootlessRelay:
|
|
|
|
```bash
|
|
git clone https://github.com/obegron/rootlessRelay.git
|
|
cd rootlessRelay
|
|
npm install
|
|
```
|
|
|
|
### Required Patches for Host Access
|
|
|
|
To allow the VM to connect to host services via the gateway IP (10.0.2.2), apply these patches to `relay.js`:
|
|
|
|
**Patch 1: Disable reverse TCP handling for gateway (line ~684)**
|
|
```javascript
|
|
// Change:
|
|
if (protocol === 6 && dstIP === GATEWAY_IP) {
|
|
this.handleReverseTCP(ipPacket);
|
|
return;
|
|
}
|
|
|
|
// To:
|
|
if (false && protocol === 6 && dstIP === GATEWAY_IP) { // PATCHED
|
|
this.handleReverseTCP(ipPacket);
|
|
return;
|
|
}
|
|
```
|
|
|
|
**Patch 2: Redirect gateway TCP to localhost (line ~792)**
|
|
```javascript
|
|
// Change:
|
|
const socket = net.connect(dstPort, dstIP, () => {
|
|
|
|
// To:
|
|
const actualDstIP = dstIP === GATEWAY_IP ? "127.0.0.1" : dstIP;
|
|
const socket = net.connect(dstPort, actualDstIP, () => {
|
|
```
|
|
|
|
**Patch 3: Redirect gateway UDP to localhost (lines ~1431 and ~1449)**
|
|
```javascript
|
|
// Change:
|
|
this.udpSocket.send(payload, dstPort, dstIP, (err) => {
|
|
|
|
// To:
|
|
const actualUdpDstIP = dstIP === GATEWAY_IP ? "127.0.0.1" : dstIP;
|
|
this.udpSocket.send(payload, dstPort, actualUdpDstIP, (err) => {
|
|
```
|
|
|
|
### Starting the Relay
|
|
|
|
```bash
|
|
ENABLE_WSS=false LOG_LEVEL=1 node relay.js
|
|
# Listens on ws://127.0.0.1:8086/
|
|
```
|
|
|
|
## Basic Usage
|
|
|
|
```javascript
|
|
import { V86 } from "v86";
|
|
import path from "node:path";
|
|
|
|
const emulator = new V86({
|
|
wasm_path: path.join(__dirname, "node_modules/v86/build/v86.wasm"),
|
|
bios: { url: path.join(__dirname, "bios/seabios.bin") },
|
|
vga_bios: { url: path.join(__dirname, "bios/vgabios.bin") },
|
|
filesystem: {
|
|
basefs: path.join(__dirname, "images/alpine-fs.json"),
|
|
baseurl: path.join(__dirname, "images/alpine-rootfs-flat/"),
|
|
},
|
|
autostart: true,
|
|
memory_size: 512 * 1024 * 1024,
|
|
bzimage_initrd_from_filesystem: true,
|
|
cmdline: "rw root=host9p rootfstype=9p rootflags=trans=virtio,cache=loose modules=virtio_pci tsc=reliable console=ttyS0",
|
|
net_device: {
|
|
type: "virtio",
|
|
relay_url: "ws://127.0.0.1:8086/",
|
|
},
|
|
});
|
|
|
|
// Capture output
|
|
emulator.add_listener("serial0-output-byte", (byte) => {
|
|
process.stdout.write(String.fromCharCode(byte));
|
|
});
|
|
|
|
// Send commands
|
|
emulator.serial0_send("echo hello\n");
|
|
```
|
|
|
|
## Communication Methods
|
|
|
|
### 1. Serial Console (stdin/stdout)
|
|
|
|
```javascript
|
|
// Send command
|
|
emulator.serial0_send("ls -la\n");
|
|
|
|
// Receive output
|
|
let output = "";
|
|
emulator.add_listener("serial0-output-byte", (byte) => {
|
|
output += String.fromCharCode(byte);
|
|
});
|
|
```
|
|
|
|
### 2. 9p Filesystem (file I/O)
|
|
|
|
```javascript
|
|
// Write file to VM
|
|
const data = new TextEncoder().encode("#!/bin/sh\necho hello\n");
|
|
await emulator.create_file("/tmp/script.sh", data);
|
|
|
|
// Read file from VM
|
|
const result = await emulator.read_file("/tmp/output.txt");
|
|
console.log(new TextDecoder().decode(result));
|
|
```
|
|
|
|
### 3. Network (TCP to host services)
|
|
|
|
From inside the VM, connect to `10.0.2.2:PORT` to reach `localhost:PORT` on the host (requires patched relay).
|
|
|
|
```bash
|
|
# Inside VM
|
|
wget http://10.0.2.2:8080/ # Connects to host's localhost:8080
|
|
```
|
|
|
|
## State Save/Restore
|
|
|
|
```javascript
|
|
// Save state (includes all installed packages, files, etc.)
|
|
const state = await emulator.save_state();
|
|
fs.writeFileSync("vm-state.bin", Buffer.from(state));
|
|
|
|
// Restore state (instant resume, ~2 seconds)
|
|
const stateBuffer = fs.readFileSync("vm-state.bin");
|
|
await emulator.restore_state(stateBuffer.buffer);
|
|
```
|
|
|
|
## Network Setup Inside VM
|
|
|
|
After boot, run these commands to enable networking:
|
|
|
|
```bash
|
|
modprobe virtio-net
|
|
ip link set eth0 up
|
|
udhcpc -i eth0
|
|
```
|
|
|
|
Or as a one-liner:
|
|
```bash
|
|
modprobe virtio-net && ip link set eth0 up && udhcpc -i eth0
|
|
```
|
|
|
|
The VM will get IP `10.0.2.15` (or similar) via DHCP from the relay.
|
|
|
|
## Performance
|
|
|
|
| Metric | Value |
|
|
|--------|-------|
|
|
| Cold boot | ~20-25 seconds |
|
|
| State restore | ~2-3 seconds |
|
|
| Memory usage | ~512 MB (configurable) |
|
|
|
|
## Typical Workflow for Mom
|
|
|
|
1. **First run**:
|
|
- Start rootlessRelay
|
|
- Boot v86 with Alpine (~25s)
|
|
- Setup network
|
|
- Install needed packages (`apk add nodejs npm python3 git`)
|
|
- Save state
|
|
|
|
2. **Subsequent runs**:
|
|
- Start rootlessRelay
|
|
- Restore saved state (~2s)
|
|
- Ready to execute commands
|
|
|
|
3. **Command execution**:
|
|
- Send commands via `serial0_send()`
|
|
- Capture output via `serial0-output-byte` listener
|
|
- Exchange files via 9p filesystem
|
|
|
|
## Alternative: fetch Backend (No Relay Needed)
|
|
|
|
For HTTP-only networking, v86 has a built-in `fetch` backend:
|
|
|
|
```javascript
|
|
net_device: {
|
|
type: "virtio",
|
|
relay_url: "fetch",
|
|
}
|
|
```
|
|
|
|
This uses the browser/Node.js `fetch()` API for HTTP requests. Limitations:
|
|
- Only HTTP/HTTPS (no raw TCP/UDP)
|
|
- No WebSocket
|
|
- Host access via `http://<port>.external` (e.g., `http://8080.external`)
|
|
|
|
## Files Reference
|
|
|
|
After building, you need these files:
|
|
|
|
```
|
|
project/
|
|
├── node_modules/v86/build/
|
|
│ ├── v86.wasm
|
|
│ └── libv86.mjs
|
|
├── bios/
|
|
│ ├── seabios.bin
|
|
│ └── vgabios.bin
|
|
├── images/
|
|
│ ├── alpine-fs.json
|
|
│ └── alpine-rootfs-flat/
|
|
│ └── *.bin.zst (many files)
|
|
└── rootlessRelay/
|
|
└── relay.js (patched)
|
|
```
|
|
|
|
## Resources
|
|
|
|
- [v86 GitHub](https://github.com/copy/v86)
|
|
- [v86 Networking Docs](https://github.com/copy/v86/blob/master/docs/networking.md)
|
|
- [v86 Alpine Setup](https://github.com/copy/v86/tree/master/tools/docker/alpine)
|
|
- [rootlessRelay](https://github.com/obegron/rootlessRelay)
|
|
- [v86 npm package](https://www.npmjs.com/package/v86)
|