Imported from zebreus/nixho (
AGENTS.md). Install upstream withnpx skills add zebreus/nixho. Copyright stays with the author.
nixho – Agent Guide
A NixOS deployment and provisioning tool. Self-contained Rust binary plus SQLite DB.
Components:
- Authoritative DNS server that auto-allocates IPv6 /96 blocks from a configurable prefix pool on first lookup.
- SSH server that accepts
nixos-rebuilddeployments. Per-hostname host keys, TOFU client auth, pipesnix-store --servefor store path transfers, intercepts activation commands. - Libvirt VM backend that provisions qcow2 overlay VMs via
virshCLI, streams activation output, and configures IPv6 forwarding.
CI / Nested KVM
CI will ALWAYS support nested KVM. All VM tests rely on nested virtualisation (libvirt/QEMU inside a NixOS VM test). Do not add checks or workarounds for missing KVM — it is guaranteed to be available.
Networking Model
Terminology
- nixho host: The machine running the nixho binary. Runs DNS, SSH, and manages guest VMs.
- Guest VM: A NixOS virtual machine provisioned by nixho via libvirt/QEMU. Each hostname maps to one guest VM.
- Tap device: A per-guest-VM network interface on the host (
nxh0,nxh1, …). Each guest VM gets its own tap device — there is no shared bridge (no virbr0, no dnsmasq). - Uplink interface: The host's external network interface (configured via
NIXHO_UPLINK_IFACE). Used for proxy NDP so guest VMs are reachable from outside the host. - Internal prefix: A small private IPv4 subnet (default
192.168.2.0/24) used on each tap device for host↔VM communication. The host gets.1(gateway), the VM gets.2. DNAT/SNAT on the host makes the public customer IPv4 transparent to the VM. Configured viaNIXHO_IPV4_INTERNAL_PREFIX+NIXHO_IPV4_INTERNAL_PREFIX_LEN. - IPv6 link-local gateway: Guest VMs use
fe80::1as their IPv6 default gateway (a link-local address assigned to each tap device on the host side).
Topology
┌─────────────────────────────────┐
│ nixho host │
external network │ │
◄──────────────────► │ uplink iface (e.g. eth1) │
(proxy NDP for /96 │ │ │
blocks) │ │ IPv6 forwarding │
│ │ IPv4 DNAT/SNAT per-VM │
│ │ │
│ nxh0 ◄───► Guest VM 0 │
│ nxh1 ◄───► Guest VM 1 │
│ nxh2 ◄───► Guest VM 2 │
│ ... │
└─────────────────────────────────┘
Each guest VM connects to the host via a dedicated tap device. There is no shared bridge.
IPv6
Each hostname gets a block allocated from a configurable prefix. The block size is determined by NIXHO_IPV6_PREFIX_CUSTOMER_LEN (default /96). The allocation is automatic on first DNS lookup — the allocator assigns sequential indices and computes the block base address.
- Prefix: Configured via
NIXHO_IPV6_PREFIX(address) +NIXHO_IPV6_PREFIX_LEN(length) +NIXHO_IPV6_PREFIX_CUSTOMER_LEN(per-customer block size). E.g.fd00:1::with prefix length64and customer length96means each name gets a /96 insidefd00:1::/64. - Routing: The host adds a /<customer_len> route for each guest via its tap device. External reachability is via proxy NDP entries on the uplink interface — the host answers NDP solicitations for guest addresses.
- Gateway: Guest VMs use
fe80::1as their IPv6 default gateway (a link-local address assigned to each tap device on the host side). - DNS: AAAA records resolve to the block base address. PTR records cover the entire prefix.
IPv4
Each hostname gets a public IPv4 address from a configurable prefix. The VM itself only sees an internal address (from the internal prefix); DNAT/SNAT on the host makes the public address transparent. IPv4 A records are never auto-assigned; use NIXHO_COMMAND=set-ipv4 with the host's real public IPv4 to publish an A record.
- Public prefix: Configured via three separate variables:
NIXHO_IPV4_PREFIX— base address (e.g.10.87.0.0)NIXHO_IPV4_PREFIX_LEN— prefix length (e.g.16→10.87.0.0/16is the overall block)NIXHO_IPV4_PREFIX_CUSTOMER_LEN— per-customer prefix length (e.g.28→ each customer gets a /28 subnet = 16 addresses)
- Addressing: Customer n gets the base address of the n-th /customer_len subnet, plus 1. E.g. with
10.87.0.0/16/28: customer 0 →10.87.0.1, customer 1 →10.87.0.17, etc. - Internal prefix: Configured via
NIXHO_IPV4_INTERNAL_PREFIX(default192.168.2.0) +NIXHO_IPV4_INTERNAL_PREFIX_LEN(default24). Each tap device uses.1as the gateway and.2as the VM address. Since each tap is a point-to-point link, the same internal addresses can be reused across VMs. - Gateway: Guest VMs use the internal gateway (
.1of the internal prefix, e.g.192.168.2.1) as their IPv4 default gateway. IPv6 usesfe80::1. - NAT: The host runs per-VM iptables DNAT (PREROUTING: public → internal) and SNAT (POSTROUTING: internal → public) rules. A
localroute for the public IP ensures the kernel accepts packets for DNAT processing.
Project Structure
nixho/
├── crates/
│ ├── nixho/ # Binary: wires services together
│ ├── nixho-config/ # Shared configuration: env var parsing, Ipv6Prefix, DnsConfig, SshConfig, BackendConfig
│ ├── nixho-store/ # Shared SQLite DB wrapper (Db struct)
│ ├── nixho-dns/ # DNS server: handler, allocator, store layer, Rust API
│ ├── nixho-ssh/ # SSH server: handler, store (host+client keys), deployments, command routing
│ └── nixho-backend/ # VM backend: libvirt provisioning, activation streaming, IPv6 forwarding
├── nix/
│ ├── module.nix # NixOS systemd service module
│ ├── vm-test-resolver.nix # DNS resolver VM test
│ ├── vm-test-ssh.nix # SSH basics VM test
│ ├── vm-test-rebuild-nixho.nix # nixos-rebuild against nixho VM test
│ ├── vm-test-rebuild-reference.nix # nixos-rebuild against reference NixOS VM test
│ ├── vm-test-rebuild-unmapped.nix # Unmapped IP VM test
│ ├── vm-test-rebuild-substituter.nix # Substituter VM test
│ ├── vm-test-deploy-vm.nix # Full end-to-end deploy with nested VM
│ ├── vm-test-deploy-config.nix # Shared deploy config for VM tests
│ ├── vm-test-ipv6-routing.nix # IPv6 routing/forwarding VM test
│ ├── vm-test-internal-prefix.nix # IPv4 internal prefix DNAT/SNAT VM test
│ └── vm-test-streaming.nix # Activation output streaming VM test
├── flake.nix # Nix flake: packages, checks, devShells
└── Cargo.toml # Rust workspace
- nixho-config: Centralised configuration. Defines
Config,DnsConfig,SshConfig,BackendConfig,Ipv6Prefix,Ipv4Prefix,Commandenum. All env var names and parsing live here. Prefix types are constructed from separate address + length values (no CIDR string parsing). - nixho-store: Thin shared DB layer.
DbwrapsArc<Mutex<Connection>>, cloneable. Service crates manage their own tables viaDb::conn(). - nixho-dns: Authoritative DNS crate. Manages
dns_namestable, exposesDnsServicewith Rust API (get_or_allocate,set_ipv4,reverse_lookup_v6/v4,list_all). Prefix length is configurable (allocator uses u64 indices and u128 arithmetic). - nixho-ssh: SSH server crate using
russh. Managesssh_host_keys,ssh_client_keys, anddeploymentstables. Identifies target hostname via destination IPv6 reverse lookup in DNS store. Routes exec commands:nix-store --serve(spawned subprocess),nix-env --set(profile namespacing + deployment recording),switch-to-configuration(intercepted, triggers backend activation). - nixho-backend: VM hosting backend.
Backendtrait withprovision,activate,destroy,enable_forwarding.LibvirtBackendusesvirshCLI (not thevirtC library) to manage qcow2 overlays, cloud-init ISOs, and domain XML. Activation streams output viaActivationStream(mpsc channel + JoinHandle). Managesvmstable viaBackendStore. - nixho (binary): Reads all config from environment variables via
nixho-config. Starts DNS and SSH servers concurrently. - Nix: Crane builds the Rust app; the NixOS module runs it as a systemd service with env vars.
Configuration
All configuration is via environment variables:
| Variable | Description | Default |
|---|---|---|
NIXHO_COMMAND |
serve or set-ipv4 |
serve |
NIXHO_DB |
SQLite database path | ./nixho.db |
NIXHO_ZONE |
DNS zone name | nixho.st |
NIXHO_IPV6_PREFIX |
IPv6 prefix address (e.g. fd00:1::, 2001:db8::) |
(required) |
NIXHO_IPV6_PREFIX_LEN |
IPv6 prefix length (e.g. 64, 48) |
(required) |
NIXHO_IPV6_PREFIX_CUSTOMER_LEN |
Per-customer IPv6 block size (e.g. 96, 128) |
(required) |
NIXHO_DNS_PORT |
DNS port | 53 |
NIXHO_SSH_PORT |
SSH port | 22 |
NIXHO_LIBVIRT_URI |
Libvirt connection URI | qemu:///system |
NIXHO_BASE_IMAGE |
Path to base NixOS qcow2 image | /var/lib/nixho/base.qcow2 |
NIXHO_VM_MEMORY_KIB |
VM memory in KiB | 1048576 |
NIXHO_VM_VCPUS |
VM vCPU count | 1 |
NIXHO_UPLINK_IFACE |
Network interface for proxy NDP | eth1 |
NIXHO_IPV4_PREFIX |
IPv4 prefix address for customer VMs (e.g. 10.87.0.0) |
(optional) |
NIXHO_IPV4_PREFIX_LEN |
IPv4 prefix length (e.g. 16) |
(required if prefix set) |
NIXHO_IPV4_PREFIX_CUSTOMER_LEN |
IPv4 per-customer prefix length (e.g. 28) |
(required if prefix set) |
NIXHO_IPV4_INTERNAL_PREFIX |
Internal IPv4 prefix for tap devices (e.g. 192.168.2.0) |
192.168.2.0 |
NIXHO_IPV4_INTERNAL_PREFIX_LEN |
Internal IPv4 prefix length | 24 |
NIXHO_IPV4_HOST_ADDRESS |
Host's public IPv4 address (for A records via set-ipv4) | (optional) |
NIXHO_IPV6_HOST_ADDRESS |
Host's public IPv6 address | (optional) |
NIXHO_IPV4_NAME |
Name for set-ipv4 | (required for set-ipv4) |
NIXHO_IPV4_ADDR |
IPv4 address for set-ipv4 | (required for set-ipv4) |
The IPv6 prefix length must be between /1 and the customer length. The customer length can be up to /128. DNS and SSH always listen on [::]:port (all interfaces). Common prefix values: /48, /56, /64; common customer values: /96, /128.
Build & Run
# Fast Rust iterations
cargo build
# Quick test run (not on port 53/22 to avoid needing root)
NIXHO_IPV6_PREFIX=fd00:1:: NIXHO_IPV6_PREFIX_LEN=64 NIXHO_IPV6_PREFIX_CUSTOMER_LEN=96 NIXHO_DNS_PORT=5300 NIXHO_SSH_PORT=2222 cargo run
Testing
Build and run Rust unit tests:
nix build .#nixho
Run the entire test suite (clippy, tests, fmt, VM tests):
nix flake check
- Always run
nix build .#nixhobefore committing Rust changes. - Run
nix flake checkbefore merging to ensure all checks pass. - Add or update tests when changing behavior; prefer tests in the library crates for logic.