Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
/target
/crates/*/target

# Staged emberd binary for image builds (built with `make emberd-image`)
/images/emberd

# Swift build artifacts
/ember-vz/.build
/ember-vz/.swiftpm
Expand Down
18 changes: 18 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = ["crates/ember-core", "crates/ember-linux", "crates/ember-macos"]
members = ["crates/ember-core", "crates/ember-linux", "crates/ember-macos", "emberd"]
default-members = ["."]

[workspace.dependencies]
Expand Down
32 changes: 27 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

UNAME := $(shell uname -s)

.PHONY: build release clean fmt check clippy test udeps
.PHONY: build release clean fmt check clippy test udeps emberd

build:
cargo build
Expand All @@ -22,23 +22,45 @@ ifeq ($(UNAME),Darwin)
cp ember-vz/.build/release/ember-vz target/release/
endif

# Build emberd (in-VM daemon). Runs inside Linux VMs so the vsock listener
# only compiles on Linux, but UDS-only mode works on macOS for testing.
emberd:
cargo build -p emberd

emberd-release:
cargo build -p emberd --release

# Build emberd for Linux and stage at images/emberd for Dockerfile COPY.
# Uses Docker (via Colima on macOS) so no cross-compilation toolchain needed.
emberd-image:
ifeq ($(UNAME),Linux)
cargo build -p emberd --release
cp target/release/emberd images/emberd
else
docker run --rm -v "$(CURDIR)":/src -w /src \
-e CARGO_TARGET_DIR=/tmp/emberd-target \
rust:latest \
sh -c 'cargo build -p emberd --release && cp /tmp/emberd-target/release/emberd /src/images/emberd'
endif
@echo "emberd binary staged at images/emberd"

clean:
cargo clean
ifeq ($(UNAME),Darwin)
cd ember-vz && swift package clean
endif

fmt:
cargo fmt
cargo fmt --all

check:
cargo check
cargo check --workspace

clippy:
cargo clippy -- -D warnings
cargo clippy --workspace -- -D warnings

test:
cargo test
cargo test --workspace

udeps:
cargo machete
17 changes: 17 additions & 0 deletions emberd/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "emberd"
version = "0.1.0"
edition = "2021"
description = "Lightweight in-VM daemon for Ember VMs"
license = "MIT"

[dependencies]
clap = { version = "4.5", features = ["derive"] }
serde_json = "1.0"
base64 = "0.22"

[target.'cfg(target_os = "linux")'.dependencies]
nix = { version = "0.29", features = ["socket"] }

[dev-dependencies]
tempfile = "3"
130 changes: 130 additions & 0 deletions emberd/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# emberd

Lightweight in-VM daemon for [Ember](https://github.com/aljoscha/ember) VMs.
Serves JSON-lines requests over vsock or Unix domain sockets, providing
structured host-guest communication for the
[Thermite](https://github.com/jasonhernandez/thermite) orchestrator.

## Why

Without emberd, Thermite communicates with VMs via SSH — shelling out for
every exec, file read, and status check. This works but adds latency and
fragility at scale. emberd replaces SSH with a direct vsock channel:
structured requests in, structured responses out, no shell parsing.

Thermite's `EmberdClient` speaks this protocol natively. When emberd is
running in a VM, Thermite automatically routes through vsock. When it's
not (legacy images, crash), Thermite falls back to SSH transparently.

## Protocol

JSON lines over vsock port 1024. One request per line, one response per line.

### ping

```
-> {"op":"ping"}
<- {"ok":true,"uptime_seconds":123.45}
```

### exec

```
-> {"op":"exec","command":"echo hello","env":{"FOO":"bar"}}
<- {"exit_code":0,"stdout":"hello\n","stderr":""}
```

### read_file

```
-> {"op":"read_file","path":"/tmp/result.json"}
<- {"data":"eyJzdGF0dXMiOiAiZG9uZSJ9"}
```

Content is base64-encoded.

### write_file

```
-> {"op":"write_file","path":"/tmp/config.json","data":"eyJ0YXNrIjogIlNFQy0yMDAifQ=="}
<- {"ok":true}
```

### agent_status

```
-> {"op":"agent_status"}
<- {"running":true,"pid":9999,"task_id":"SEC-200"}
```

Scans `/proc` for a process matching `thermite-entrypoint` and reads
`/tmp/thermite-task-id` for the task ID.

## Build

```bash
# From the ember repo root:
cargo build -p emberd # debug
cargo build -p emberd --release # release
```

emberd is a workspace member with minimal dependencies (clap, serde_json,
base64). The vsock listener uses nix and only compiles on Linux. On macOS,
emberd builds in UDS-only mode for testing.

## Usage

```bash
# Production (inside a Linux VM, listens on vsock port 1024)
emberd

# Custom port
emberd --port 2048

# Testing (Unix domain socket, works on any platform)
emberd --uds /tmp/emberd.sock
```

## Testing

```bash
cargo test -p emberd
```

15 tests covering all operations, error handling, and a full UDS
integration roundtrip.

## Image integration

emberd is baked into Ember VM images via Dockerfile and starts on boot:

```ini
# /etc/systemd/system/emberd.service
[Service]
Type=simple
ExecStart=/usr/local/bin/emberd --port 1024
Restart=always
```

Build and stage for image builds:

```bash
make emberd-image # builds for Linux, copies to images/emberd
```

## Architecture

```
Host (Thermite) Guest VM (emberd)
+-----------------+ +------------------+
| EmberdClient | | emberd |
| VsockTransport |--> UDS --> Firecracker/AVF --> AF_VSOCK |
| (or SSH | bridge | port 1024 |
| fallback) | | JSON lines |
+-----------------+ +------------------+
```

- **Host side**: Thermite connects to `<state_dir>/vms/<name>/vsock.sock`
- **Bridge**: Firecracker or ember-vz (AVF) bridges UDS to vsock
- **Guest side**: emberd listens on `AF_VSOCK` port 1024
- **Fallback**: If emberd isn't running, Thermite uses SSH automatically
Loading
Loading