Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
a4c124c
Network manager
sjmiller609 Nov 12, 2025
d7ca8e6
Fix tests
sjmiller609 Nov 12, 2025
cd2abad
an instance test with network passes, but needs more work
sjmiller609 Nov 12, 2025
b6b3a4d
Test time
sjmiller609 Nov 14, 2025
b73a4fd
Simplify to default network only
sjmiller609 Nov 14, 2025
4c19d0f
Remove network logic from configdisk.go
sjmiller609 Nov 14, 2025
0397f85
Random IP distribution
sjmiller609 Nov 14, 2025
e850a01
Address concurrency and locking
sjmiller609 Nov 14, 2025
c52c28f
Fix json parsing
sjmiller609 Nov 14, 2025
95ceb07
Add a way to run just one test
sjmiller609 Nov 14, 2025
071a3b9
Get network allocation before deleting VMM
sjmiller609 Nov 14, 2025
682a2bb
Delete network working but delete looks messy
sjmiller609 Nov 14, 2025
556f0dc
Don't need to manage DNS yet
sjmiller609 Nov 14, 2025
8230726
Don't use CAP_SYS_ADMIN
sjmiller609 Nov 14, 2025
0168776
Inheritable CAP_NET_ADMIN
sjmiller609 Nov 14, 2025
4205eb5
Check for standby resume in network test
sjmiller609 Nov 14, 2025
36e9e62
Cleanup taps on standby
sjmiller609 Nov 14, 2025
01c31bc
Fix path to ch snap config
sjmiller609 Nov 14, 2025
7b50f04
WIP: proved that network not working after restore
sjmiller609 Nov 14, 2025
04bb214
add stainless github action (#8)
rgarcia Nov 18, 2025
c78123d
Merge remote-tracking branch 'origin/main' into network-manager
sjmiller609 Nov 21, 2025
6f3818c
Enable network capabilities on make dev
sjmiller609 Nov 21, 2025
81190e9
Address review comments
sjmiller609 Nov 25, 2025
e3b5bde
fix make dev capabilities
sjmiller609 Nov 25, 2025
cad74f2
Fix network init
sjmiller609 Nov 25, 2025
da0ce57
fixing host setup initialization for partially-initialized state
sjmiller609 Nov 25, 2025
d4c9a23
Improve error message with stale bridge config
sjmiller609 Nov 25, 2025
4468372
cleanup orphaned taps and naming convention hype-*
sjmiller609 Nov 25, 2025
2a3fcbb
Put our iptables rules at the top
sjmiller609 Nov 25, 2025
252fbf4
Discover host uplink
sjmiller609 Nov 25, 2025
e812a14
400 instead of 500 on name conflict
sjmiller609 Nov 25, 2025
ebe27aa
Adjust test for new tap name
sjmiller609 Nov 25, 2025
988c4e0
Working on networking test
sjmiller609 Nov 25, 2025
2490ea7
Fix network test
sjmiller609 Nov 25, 2025
23bd349
Delete redundant test
sjmiller609 Nov 25, 2025
82fc9f0
Merge remote-tracking branch 'origin/main' into network-manager
sjmiller609 Nov 25, 2025
604b353
Addressing PR review comments
sjmiller609 Nov 26, 2025
61f21fa
including ip and mac
sjmiller609 Nov 26, 2025
26e97b9
Change default subnet to 10.100.0.0/16
sjmiller609 Nov 26, 2025
77131db
Detect and informatively error on network conflict
sjmiller609 Nov 26, 2025
eaf3b46
Derive gateway IP instead of config separately from subnet
sjmiller609 Nov 26, 2025
f96cffb
Add high level explainer to README
sjmiller609 Nov 26, 2025
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
3 changes: 2 additions & 1 deletion .air.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -tags containers_image_openpgp -o ./tmp/main ./cmd/api"
cmd = "go build -tags containers_image_openpgp -o ./tmp/main ./cmd/api && sudo setcap cap_net_admin,cap_net_bind_service=+eip ./tmp/main"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata", "bin", "scripts", "data", "kernel"]
exclude_file = []
Expand All @@ -20,6 +20,7 @@ tmp_dir = "tmp"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
Expand Down
65 changes: 65 additions & 0 deletions .github/workflows/stainless-sdks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: Stainless SDK preview on PRs

on:
pull_request:
types:
- opened
- synchronize
- reopened
- closed

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

env:
STAINLESS_ORG: ${{ vars.STAINLESS_ORG }}
STAINLESS_PROJECT: ${{ vars.STAINLESS_PROJECT }}
OAS_PATH: openapi.yaml
CONFIG_PATH: stainless.yaml

jobs:
preview:
if: github.event.action != 'closed'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 2

- name: Run preview builds
uses: stainless-api/upload-openapi-spec-action/preview@v1
with:
stainless_api_key: ${{ secrets.STAINLESS_API_KEY }}
org: ${{ env.STAINLESS_ORG }}
project: ${{ env.STAINLESS_PROJECT }}
oas_path: ${{ env.OAS_PATH }}
config_path: ${{ env.CONFIG_PATH }}
make_comment: true
github_token: ${{ secrets.GITHUB_TOKEN }}

merge:
if: github.event.action == 'closed' && github.event.pull_request.merged == true
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 2

- name: Run merge build
uses: stainless-api/upload-openapi-spec-action/merge@v1
with:
stainless_api_key: ${{ secrets.STAINLESS_API_KEY }}
org: ${{ env.STAINLESS_ORG }}
project: ${{ env.STAINLESS_PROJECT }}
oas_path: ${{ env.OAS_PATH }}
make_comment: true
github_token: ${{ secrets.GITHUB_TOKEN }}
37 changes: 35 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,42 @@ dev: $(AIR)
$(AIR) -c .air.toml

# Run tests
# Compile test binaries and grant network capabilities (runs as user, not root)
# Usage: make test - runs all tests
# make test TEST=TestCreateInstanceWithNetwork - runs specific test
test: ensure-ch-binaries lib/system/exec_agent/exec-agent
go test -tags containers_image_openpgp -v -timeout 30s ./...
@echo "Building test binaries..."
@mkdir -p $(BIN_DIR)/tests
@for pkg in $$(go list -tags containers_image_openpgp ./...); do \
pkg_name=$$(basename $$pkg); \
go test -c -tags containers_image_openpgp -o $(BIN_DIR)/tests/$$pkg_name.test $$pkg 2>/dev/null || true; \
done
@echo "Granting capabilities to test binaries..."
@for test in $(BIN_DIR)/tests/*.test; do \
if [ -f "$$test" ]; then \
sudo setcap 'cap_net_admin,cap_net_bind_service=+eip' $$test 2>/dev/null || true; \
fi; \
done
@echo "Running tests as current user with capabilities..."
@if [ -n "$(TEST)" ]; then \
echo "Running specific test: $(TEST)"; \
for test in $(BIN_DIR)/tests/*.test; do \
if [ -f "$$test" ]; then \
echo ""; \
echo "Checking $$(basename $$test) for $(TEST)..."; \
$$test -test.run=$(TEST) -test.v -test.timeout=60s 2>&1 | grep -q "PASS\|FAIL" && \
$$test -test.run=$(TEST) -test.v -test.timeout=60s || true; \
fi; \
done; \
else \
for test in $(BIN_DIR)/tests/*.test; do \
if [ -f "$$test" ]; then \
echo ""; \
echo "Running $$(basename $$test)..."; \
$$test -test.v -test.parallel=10 -test.timeout=60s || exit 1; \
fi; \
done; \
fi

# Generate JWT token for testing
# Usage: make gen-jwt [USER_ID=test-user]
Expand All @@ -131,4 +165,3 @@ clean:
rm -f lib/exec/exec.pb.go
rm -f lib/exec/exec_grpc.pb.go
rm -f lib/system/exec_agent/exec-agent

112 changes: 102 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,17 @@ Run containerized workloads in VMs, powered by [Cloud Hypervisor](https://github

### Prerequisites

**Go 1.25.4+**, **KVM**, **erofs-utils**
**Go 1.25.4+**, **KVM**, **erofs-utils**, **dnsmasq**

```bash
# Verify prerequisites
mkfs.erofs --version
dnsmasq --version
```

**Install on Debian/Ubuntu:**
```bash
sudo apt-get install erofs-utils dnsmasq
```

**KVM Access:** User must be in `kvm` group for VM access:
Expand All @@ -20,13 +27,100 @@ sudo usermod -aG kvm $USER
# Log out and back in, or use: newgrp kvm
```

**Network Capabilities:**

Before running or testing Hypeman, ensure IPv4 forwarding is enabled:

```bash
# Enable IPv4 forwarding (temporary - until reboot)
sudo sysctl -w net.ipv4.ip_forward=1

# Enable IPv4 forwarding (persistent across reboots)
echo 'net.ipv4.ip_forward=1' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
```

**Why:** Required for routing traffic between VM network and external network.

The hypeman binary needs network administration capabilities to create bridges and TAP devices:
```bash
# After building, grant network capabilities
sudo setcap 'cap_net_admin,cap_net_bind_service=+eip' /path/to/hypeman

# For development builds
sudo setcap 'cap_net_admin,cap_net_bind_service=+eip' ./bin/hypeman

# Verify capabilities
getcap ./bin/hypeman
```

**Note:** The `i` (inheritable) flag allows child processes spawned by hypeman (like `ip` and `iptables` commands) to inherit capabilities via the ambient capability set.

**Note:** These capabilities must be reapplied after each rebuild. For production deployments, set capabilities on the installed binary. For local testing, this is handled automatically in `make test`.

### Configuration

#### Environment variables

Hypeman can be configured using the following environment variables:

| Variable | Description | Default |
|----------|-------------|---------|
| `PORT` | HTTP server port | `8080` |
| `DATA_DIR` | Directory for storing VM images, volumes, and other data | `/var/lib/hypeman` |
| `BRIDGE_NAME` | Name of the network bridge for VM networking | `vmbr0` |
| `SUBNET_CIDR` | CIDR notation for the VM network subnet (gateway derived automatically) | `10.100.0.0/16` |
| `UPLINK_INTERFACE` | Host network interface to use for VM internet access | _(auto-detect)_ |
| `JWT_SECRET` | Secret key for JWT authentication (required for production) | _(empty)_ |
| `DNS_SERVER` | DNS server IP address for VMs | `1.1.1.1` |
| `MAX_CONCURRENT_BUILDS` | Maximum number of concurrent image builds | `1` |
| `MAX_OVERLAY_SIZE` | Maximum size for overlay filesystem | `100GB` |

**Important: Subnet Configuration**

The default subnet `10.100.0.0/16` is chosen to avoid common conflicts. Hypeman will detect conflicts with existing routes on startup and fail with guidance.

If you need a different subnet, set `SUBNET_CIDR` in your environment. The gateway is automatically derived as the first IP in the subnet (e.g., `10.100.0.0/16` → `10.100.0.1`).

**Alternative subnets if needed:**
- `172.30.0.0/16` - Private range between common Docker (172.17.x.x) and AWS (172.31.x.x) ranges
- `10.200.0.0/16` - Another private range option

**Example:**
```bash
# In your .env file
SUBNET_CIDR=172.30.0.0/16
```

**Finding the uplink interface (`UPLINK_INTERFACE`)**

`UPLINK_INTERFACE` tells Hypeman which host interface to use for routing VM traffic to the outside world (for iptables MASQUERADE rules). On many hosts this is `eth0`, but laptops and more complex setups often use Wi‑Fi or other names.

**Quick way to discover it:**
```bash
# Ask the kernel which interface is used to reach the internet
ip route get 1.1.1.1
```
Look for the `dev` field in the output, for example:
```text
1.1.1.1 via 192.168.12.1 dev wlp2s0 src 192.168.12.98
```
In this case, `wlp2s0` is the uplink interface, so you would set:
```bash
UPLINK_INTERFACE=wlp2s0
```

You can also inspect all routes:
```bash
ip route show
```
Pick the interface used by the default route (usually the line starting with `default`). Avoid using local bridges like `docker0`, `br-...`, `virbr0`, or `vmbr0` as the uplink; those are typically internal virtual networks, not your actual internet-facing interface.

**Setup:**

```bash
cp .env.example .env
# Edit .env and set JWT_SECRET
# Edit .env and set JWT_SECRET and other configuration values
```

#### Data directory
Expand Down Expand Up @@ -54,29 +148,27 @@ make build
```
### Running the Server

1. Copy the example environment file and modify the values:
```bash
cp .env.example .env
# Edit .env and set JWT_SECRET and other configuration values
```

2. Generate a JWT token for testing (optional):
1. Generate a JWT token for testing (optional):
```bash
make gen-jwt
```

3. Start the server with hot-reload for development:
2. Start the server with hot-reload for development:
```bash
make dev
```
The server will start on port 8080 (configurable via `PORT` environment variable).

### Testing

Network tests require elevated permissions to create bridges and TAP devices.

```bash
make test
```

The test command compiles test binaries, grants capabilities via `sudo setcap`, then runs tests as the current user (not root). You may be prompted for your sudo password during the capability grant step.

### Code Generation

After modifying `openapi.yaml`, regenerate the Go code:
Expand Down
4 changes: 4 additions & 0 deletions cmd/api/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"github.com/onkernel/hypeman/cmd/api/config"
"github.com/onkernel/hypeman/lib/images"
"github.com/onkernel/hypeman/lib/instances"
"github.com/onkernel/hypeman/lib/network"
"github.com/onkernel/hypeman/lib/oapi"
"github.com/onkernel/hypeman/lib/volumes"
)
Expand All @@ -14,6 +15,7 @@ type ApiService struct {
ImageManager images.Manager
InstanceManager instances.Manager
VolumeManager volumes.Manager
NetworkManager network.Manager
}

var _ oapi.StrictServerInterface = (*ApiService)(nil)
Expand All @@ -24,12 +26,14 @@ func New(
imageManager images.Manager,
instanceManager instances.Manager,
volumeManager volumes.Manager,
networkManager network.Manager,
) *ApiService {
return &ApiService{
Config: config,
ImageManager: imageManager,
InstanceManager: instanceManager,
VolumeManager: volumeManager,
NetworkManager: networkManager,
}
}

4 changes: 3 additions & 1 deletion cmd/api/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/onkernel/hypeman/cmd/api/config"
"github.com/onkernel/hypeman/lib/images"
"github.com/onkernel/hypeman/lib/instances"
"github.com/onkernel/hypeman/lib/network"
"github.com/onkernel/hypeman/lib/paths"
"github.com/onkernel/hypeman/lib/system"
"github.com/onkernel/hypeman/lib/volumes"
Expand All @@ -28,8 +29,9 @@ func newTestService(t *testing.T) *ApiService {
}

systemMgr := system.NewManager(p)
networkMgr := network.NewManager(p, cfg)
maxOverlaySize := int64(100 * 1024 * 1024 * 1024) // 100GB for tests
instanceMgr := instances.NewManager(p, imageMgr, systemMgr, maxOverlaySize)
instanceMgr := instances.NewManager(p, imageMgr, systemMgr, networkMgr, maxOverlaySize)
volumeMgr := volumes.NewManager(p)

// Register cleanup for orphaned Cloud Hypervisor processes
Expand Down
7 changes: 7 additions & 0 deletions cmd/api/api/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,17 @@ func TestExecInstanceNonTTY(t *testing.T) {

// Create instance
t.Log("Creating instance...")
networkDisabled := false
instResp, err := svc.CreateInstance(ctx(), oapi.CreateInstanceRequestObject{
Body: &oapi.CreateInstanceRequest{
Name: "exec-test",
Image: "docker.io/library/nginx:alpine",
Network: &struct {
Enabled *bool `json:"enabled,omitempty"`
Name *string `json:"name,omitempty"`
}{
Enabled: &networkDisabled,
},
},
})
require.NoError(t, err)
Expand Down
Loading
Loading