Replace mapdns with daemon DNS

Serve daemon-managed .vm names directly from bangerd on 127.0.0.1:42069 instead of shelling out to mapdns. This keeps DNS state tied to VM lifecycle and lets the daemon rebuild records from running VMs after startup or reconcile.

Add a small in-process authoritative DNS server, register and remove records from the VM start/stop/delete paths, and show the listener in daemon status. Remove the mapdns config and preflight surface, stop helper-flow DNS publishing in customize.sh and interactive.sh, drop dns.sh from the runtime bundle, and update docs/tests for the new local-resolver integration model.

Validated with GOCACHE=/tmp/banger-gocache go test ./..., GOCACHE=/tmp/banger-gocache make build, and bash -n customize.sh interactive.sh.
This commit is contained in:
Thales Maciel 2026-03-17 15:49:35 -03:00
parent 430f66d5dd
commit 0a0b0b617b
No known key found for this signature in database
GPG key ID: 33112E6833C34679
24 changed files with 576 additions and 278 deletions

View file

@ -3,7 +3,7 @@
## Project Structure & Module Organization ## Project Structure & Module Organization
- `cmd/banger` and `cmd/bangerd` are the primary user-facing entrypoints. - `cmd/banger` and `cmd/bangerd` are the primary user-facing entrypoints.
- `internal/` contains the daemon, CLI, RPC, storage, Firecracker, and system integration code. - `internal/` contains the daemon, CLI, RPC, storage, Firecracker, and system integration code.
- `customize.sh`, `make-rootfs.sh`, and `interactive.sh` remain as image-build/customization helpers; normal VM lifecycle and NAT management are handled by the Go control plane. - `customize.sh`, `make-rootfs.sh`, and `interactive.sh` remain as image-build/customization helpers; normal VM lifecycle, NAT, and `.vm` DNS are handled by the Go control plane.
- Source checkouts use a generated `./runtime/` bundle for Firecracker, kernels, modules, rootfs images, and helper copies. Bundle defaults come from `./runtime/bundle.json` when present. Those runtime artifacts are not meant to be tracked directly in Git. - Source checkouts use a generated `./runtime/` bundle for Firecracker, kernels, modules, rootfs images, and helper copies. Bundle defaults come from `./runtime/bundle.json` when present. Those runtime artifacts are not meant to be tracked directly in Git.
- The daemon keeps state under XDG directories rather than the old repo-local `state/` layout. - The daemon keeps state under XDG directories rather than the old repo-local `state/` layout.

View file

@ -13,7 +13,7 @@ RUNTIME_SOURCE_DIR ?= runtime
RUNTIME_ARCHIVE ?= dist/banger-runtime.tar.gz RUNTIME_ARCHIVE ?= dist/banger-runtime.tar.gz
BINARIES := banger bangerd BINARIES := banger bangerd
GO_SOURCES := $(shell find cmd internal -type f -name '*.go' | sort) GO_SOURCES := $(shell find cmd internal -type f -name '*.go' | sort)
RUNTIME_EXECUTABLES := firecracker customize.sh dns.sh packages.sh namegen RUNTIME_EXECUTABLES := firecracker customize.sh packages.sh namegen
RUNTIME_DATA_FILES := packages.apt id_ed25519 rootfs-docker.ext4 RUNTIME_DATA_FILES := packages.apt id_ed25519 rootfs-docker.ext4
RUNTIME_OPTIONAL_DATA_FILES := rootfs.ext4 bundle.json RUNTIME_OPTIONAL_DATA_FILES := rootfs.ext4 bundle.json
RUNTIME_BOOT_FILES := wtf/root/boot/vmlinux-6.8.0-94-generic wtf/root/boot/initrd.img-6.8.0-94-generic RUNTIME_BOOT_FILES := wtf/root/boot/vmlinux-6.8.0-94-generic wtf/root/boot/initrd.img-6.8.0-94-generic

View file

@ -8,7 +8,6 @@ Persistent Firecracker development VMs managed through a Go daemon, CLI, and TUI
- Guest rootfs patching: `e2cp`, `e2rm`, `debugfs` - Guest rootfs patching: `e2cp`, `e2rm`, `debugfs`
- Guest work disk creation/resizing: `mkfs.ext4`, `e2fsck`, `resize2fs`, `mount`, `umount`, `cp` - Guest work disk creation/resizing: `mkfs.ext4`, `e2fsck`, `resize2fs`, `mount`, `umount`, `cp`
- SSH and logs: `ssh` - SSH and logs: `ssh`
- DNS publishing: `mapdns`
- Optional NAT: `iptables`, `sysctl` - Optional NAT: `iptables`, `sysctl`
- Image build helper flow: `bash`, `curl`, `jq`, `sha256sum` - Image build helper flow: `bash`, `curl`, `jq`, `sha256sum`
@ -127,8 +126,8 @@ banger daemon socket
banger daemon stop banger daemon stop
``` ```
`banger daemon status` prints the daemon PID, socket path, and `bangerd.log` `banger daemon status` prints the daemon PID, socket path, daemon log path, and
location. the built-in DNS listener address.
State lives under XDG directories: State lives under XDG directories:
- config: `~/.config/banger` - config: `~/.config/banger`
@ -141,15 +140,10 @@ the executable. Source-checkout binaries resolve it from `./runtime` next to the
repo-built `./banger`. You can override either with `runtime_dir` in repo-built `./banger`. You can override either with `runtime_dir` in
`~/.config/banger/config.toml` or `BANGER_RUNTIME_DIR`. `~/.config/banger/config.toml` or `BANGER_RUNTIME_DIR`.
`mapdns` uses its own default data store unless you set `mapdns_data_file` or
`BANGER_MAPDNS_DATA_FILE`.
Useful config keys: Useful config keys:
- `log_level` - `log_level`
- `runtime_dir` - `runtime_dir`
- `firecracker_bin` - `firecracker_bin`
- `mapdns_bin`
- `mapdns_data_file`
- `ssh_key_path` - `ssh_key_path`
- `namegen_path` - `namegen_path`
- `customize_script` - `customize_script`
@ -202,7 +196,10 @@ NAT is applied by the Go control plane using host `iptables` rules derived from
the VM's current guest IP and TAP device. The remaining shell helpers also the VM's current guest IP and TAP device. The remaining shell helpers also
route NAT changes through `banger` instead of a standalone shell NAT script. route NAT changes through `banger` instead of a standalone shell NAT script.
Running VMs are published as `<vm-name>.vm` through `mapdns`. `bangerd` also serves a tiny authoritative DNS service on `127.0.0.1:42069`
for daemon-managed VMs. Known `A` records resolve `<vm-name>.vm` to the VM's
guest IPv4 address. Integrate your local resolver separately if you want
transparent `.vm` lookups on the host.
## Storage Model ## Storage Model
- VMs share a read-only base rootfs image. - VMs share a read-only base rootfs image.
@ -249,5 +246,5 @@ The runtime VM lifecycle is managed through `banger`. The remaining shell script
`BANGER_STATE_DIR`/XDG state `BANGER_STATE_DIR`/XDG state
- `make-rootfs.sh`: convenience wrapper for rebuilding `./runtime/rootfs-docker.ext4` - `make-rootfs.sh`: convenience wrapper for rebuilding `./runtime/rootfs-docker.ext4`
- `interactive.sh`: manual one-off rootfs customization over SSH - `interactive.sh`: manual one-off rootfs customization over SSH
- `packages.sh`, `dns.sh`: shell helper libraries - `packages.sh`: shell helper library
- `verify.sh`: smoke test for the Go workflow (`./verify.sh --nat` adds NAT coverage) - `verify.sh`: smoke test for the Go workflow (`./verify.sh --nat` adds NAT coverage)

View file

@ -40,7 +40,6 @@ if [[ ! -d "$RUNTIME_DIR" ]]; then
log "run 'make runtime-bundle' or set BANGER_RUNTIME_DIR" log "run 'make runtime-bundle' or set BANGER_RUNTIME_DIR"
exit 1 exit 1
fi fi
source "$RUNTIME_DIR/dns.sh"
source "$RUNTIME_DIR/packages.sh" source "$RUNTIME_DIR/packages.sh"
STATE="${BANGER_STATE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/banger/image-build}" STATE="${BANGER_STATE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/banger/image-build}"
VM_ROOT="$STATE/vms" VM_ROOT="$STATE/vms"
@ -235,7 +234,6 @@ mkdir -p "$VM_DIR"
API_SOCK="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/banger/fc-$VM_TAG.sock" API_SOCK="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/banger/fc-$VM_TAG.sock"
LOG_FILE="$VM_DIR/firecracker.log" LOG_FILE="$VM_DIR/firecracker.log"
TAP_DEV="tap-fc-$VM_TAG" TAP_DEV="tap-fc-$VM_TAG"
DNS_NAME=""
# Allocate guest IP # Allocate guest IP
NEXT_IP_FILE="$STATE/next_ip" NEXT_IP_FILE="$STATE/next_ip"
@ -252,7 +250,6 @@ cleanup() {
fi fi
sudo ip link del "$TAP_DEV" 2>/dev/null || true sudo ip link del "$TAP_DEV" 2>/dev/null || true
rm -f "$API_SOCK" rm -f "$API_SOCK"
banger_dns_remove_record_name "${DNS_NAME:-}"
rm -rf "$VM_DIR" rm -rf "$VM_DIR"
} }
trap cleanup EXIT trap cleanup EXIT
@ -340,8 +337,6 @@ fi
VM_CONFIG_JSON="$(sudo -E curl --unix-socket "$API_SOCK" -sS http://localhost/vm/config)" VM_CONFIG_JSON="$(sudo -E curl --unix-socket "$API_SOCK" -sS http://localhost/vm/config)"
CREATED_AT="$(date -Iseconds)" CREATED_AT="$(date -Iseconds)"
DNS_NAME="$(banger_dns_name "$VM_NAME")"
banger_dns_write_record "$VM_NAME" "$GUEST_IP"
jq -n \ jq -n \
--arg id "$VM_ID" \ --arg id "$VM_ID" \
--arg name "$VM_NAME" \ --arg name "$VM_NAME" \
@ -353,9 +348,8 @@ jq -n \
--arg log "$LOG_FILE" \ --arg log "$LOG_FILE" \
--arg rootfs "$OUT_ROOTFS" \ --arg rootfs "$OUT_ROOTFS" \
--arg kernel "$KERNEL" \ --arg kernel "$KERNEL" \
--arg dns_name "$DNS_NAME" \
--argjson config "$VM_CONFIG_JSON" \ --argjson config "$VM_CONFIG_JSON" \
'{meta:{id:$id,name:$name,pid:$pid,created_at:$created_at,guest_ip:$guest_ip,tap:$tap,api_sock:$api_sock,log:$log,rootfs:$rootfs,kernel:$kernel,dns_name:$dns_name},config:$config}' \ '{meta:{id:$id,name:$name,pid:$pid,created_at:$created_at,guest_ip:$guest_ip,tap:$tap,api_sock:$api_sock,log:$log,rootfs:$rootfs,kernel:$kernel},config:$config}' \
> "$VM_DIR/vm.json" > "$VM_DIR/vm.json"
log "enabling NAT for customization" log "enabling NAT for customization"

104
dns.sh
View file

@ -1,104 +0,0 @@
#!/usr/bin/env bash
MAPDNS_BIN="${MAPDNS_BIN:-${BANGER_MAPDNS_BIN:-mapdns}}"
MAPDNS_DATA_FILE="${MAPDNS_DATA_FILE:-${BANGER_MAPDNS_DATA_FILE:-}}"
banger_mapdns_cmd() {
local subcommand="$1"
shift
if [[ -n "$MAPDNS_DATA_FILE" ]]; then
"$MAPDNS_BIN" "$subcommand" --data-file "$MAPDNS_DATA_FILE" "$@"
return
fi
"$MAPDNS_BIN" "$subcommand" "$@"
}
banger_dns_name() {
local vm_name="$1"
printf '%s.vm' "$vm_name"
}
banger_dns_write_record() {
local vm_name="$1"
local guest_ip="$2"
local dns_name
if [[ -n "$MAPDNS_DATA_FILE" ]]; then
mkdir -p "$(dirname "$MAPDNS_DATA_FILE")"
fi
dns_name="$(banger_dns_name "$vm_name")"
banger_mapdns_cmd set "$dns_name" "$guest_ip" >/dev/null
}
banger_dns_record_exists() {
local dns_name="$1"
[[ -n "$dns_name" ]] || return 1
banger_mapdns_cmd list | awk '{print $1}' | rg -Fxq "$dns_name"
}
banger_dns_remove_record_name() {
local dns_name="${1:-}"
[[ -n "$dns_name" ]] || return 0
if ! banger_dns_record_exists "$dns_name"; then
return 0
fi
banger_mapdns_cmd rm "$dns_name" >/dev/null
}
banger_vm_process_running() {
local pid="$1"
local api_sock="$2"
[[ -n "$pid" && -n "$api_sock" ]] || return 1
ps -p "$pid" -o comm=,args= 2>/dev/null | rg -q "firecracker.*--api-sock $api_sock"
}
banger_wait_for_vm_exit() {
local pid="$1"
local api_sock="$2"
local timeout_secs="${3:-30}"
local deadline=$((SECONDS + timeout_secs))
while banger_vm_process_running "$pid" "$api_sock"; do
if (( SECONDS >= deadline )); then
return 1
fi
sleep 0.1
done
}
banger_teardown_vm_runtime() {
local tap="${1:-}"
local api_sock="${2:-}"
local dm_name="${3:-}"
local dm_dev="${4:-}"
local cow_loop="${5:-}"
local base_loop="${6:-}"
if [[ -n "$tap" ]]; then
sudo ip link del "$tap" 2>/dev/null || true
fi
if [[ -n "$api_sock" ]]; then
rm -f "$api_sock"
fi
if [[ -n "$dm_name" || -n "$dm_dev" ]]; then
sudo dmsetup remove "${dm_name:-$dm_dev}" 2>/dev/null || true
fi
if [[ -n "$cow_loop" ]]; then
sudo losetup -d "$cow_loop" 2>/dev/null || true
fi
if [[ -n "$base_loop" ]]; then
sudo losetup -d "$base_loop" 2>/dev/null || true
fi
}
banger_mark_vm_stopped() {
local vm_json="$1"
[[ -f "$vm_json" ]] || return 0
jq \
'del(.meta.pid, .meta.base_loop, .meta.cow_loop, .meta.dm_dev)' \
"$vm_json" > "$vm_json.tmp" && mv "$vm_json.tmp" "$vm_json"
}

12
go.mod
View file

@ -8,6 +8,7 @@ require (
github.com/charmbracelet/lipgloss v0.5.1-0.20220407020210-a86f21a0ae43 github.com/charmbracelet/lipgloss v0.5.1-0.20220407020210-a86f21a0ae43
github.com/firecracker-microvm/firecracker-go-sdk v1.0.0 github.com/firecracker-microvm/firecracker-go-sdk v1.0.0
github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-isatty v0.0.20
github.com/miekg/dns v1.1.72
github.com/pelletier/go-toml v1.9.5 github.com/pelletier/go-toml v1.9.5
github.com/sirupsen/logrus v1.9.4 github.com/sirupsen/logrus v1.9.4
github.com/spf13/cobra v1.8.1 github.com/spf13/cobra v1.8.1
@ -59,10 +60,13 @@ require (
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f // indirect github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f // indirect
go.mongodb.org/mongo-driver v1.8.3 // indirect go.mongodb.org/mongo-driver v1.8.3 // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect golang.org/x/mod v0.31.0 // indirect
golang.org/x/sys v0.34.0 // indirect golang.org/x/net v0.48.0 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/sys v0.39.0 // indirect
golang.org/x/term v0.38.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/tools v0.40.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
modernc.org/libc v1.66.3 // indirect modernc.org/libc v1.66.3 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect

30
go.sum
View file

@ -516,6 +516,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/mdlayher/socket v0.2.0/go.mod h1:QLlNPkFR88mRUNQIzRBMfXxwKal8H7u1h3bL1CV+f0E= github.com/mdlayher/socket v0.2.0/go.mod h1:QLlNPkFR88mRUNQIzRBMfXxwKal8H7u1h3bL1CV+f0E=
github.com/mdlayher/vsock v1.1.1/go.mod h1:Y43jzcy7KM3QB+/FK15pfqGxDMCMzUXWegEfIbSM18U= github.com/mdlayher/vsock v1.1.1/go.mod h1:Y43jzcy7KM3QB+/FK15pfqGxDMCMzUXWegEfIbSM18U=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@ -783,8 +785,9 @@ golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -817,8 +820,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -858,8 +861,9 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -876,8 +880,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -953,12 +957,13 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -967,8 +972,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -1016,8 +1022,8 @@ golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjs
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View file

@ -41,7 +41,6 @@ if [[ ! -d "$RUNTIME_DIR" ]]; then
log "run 'make runtime-bundle' or set BANGER_RUNTIME_DIR" log "run 'make runtime-bundle' or set BANGER_RUNTIME_DIR"
exit 1 exit 1
fi fi
source "$RUNTIME_DIR/dns.sh"
STATE="${BANGER_STATE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/banger/interactive}" STATE="${BANGER_STATE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/banger/interactive}"
VM_ROOT="$STATE/vms" VM_ROOT="$STATE/vms"
mkdir -p "$VM_ROOT" mkdir -p "$VM_ROOT"
@ -180,7 +179,6 @@ mkdir -p "$VM_DIR"
API_SOCK="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/banger/fc-$VM_TAG.sock" API_SOCK="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/banger/fc-$VM_TAG.sock"
LOG_FILE="$VM_DIR/firecracker.log" LOG_FILE="$VM_DIR/firecracker.log"
TAP_DEV="tap-fc-$VM_TAG" TAP_DEV="tap-fc-$VM_TAG"
DNS_NAME=""
# Allocate guest IP # Allocate guest IP
NEXT_IP_FILE="$STATE/next_ip" NEXT_IP_FILE="$STATE/next_ip"
@ -197,7 +195,6 @@ cleanup() {
fi fi
sudo ip link del "$TAP_DEV" 2>/dev/null || true sudo ip link del "$TAP_DEV" 2>/dev/null || true
rm -f "$API_SOCK" rm -f "$API_SOCK"
banger_dns_remove_record_name "${DNS_NAME:-}"
rm -rf "$VM_DIR" rm -rf "$VM_DIR"
} }
trap cleanup EXIT trap cleanup EXIT
@ -281,8 +278,6 @@ fi
VM_CONFIG_JSON="$(sudo -E curl --unix-socket "$API_SOCK" -sS http://localhost/vm/config)" VM_CONFIG_JSON="$(sudo -E curl --unix-socket "$API_SOCK" -sS http://localhost/vm/config)"
CREATED_AT="$(date -Iseconds)" CREATED_AT="$(date -Iseconds)"
DNS_NAME="$(banger_dns_name "$VM_NAME")"
banger_dns_write_record "$VM_NAME" "$GUEST_IP"
jq -n \ jq -n \
--arg id "$VM_ID" \ --arg id "$VM_ID" \
--arg name "$VM_NAME" \ --arg name "$VM_NAME" \
@ -294,9 +289,8 @@ jq -n \
--arg log "$LOG_FILE" \ --arg log "$LOG_FILE" \
--arg rootfs "$OUT_ROOTFS" \ --arg rootfs "$OUT_ROOTFS" \
--arg kernel "$KERNEL" \ --arg kernel "$KERNEL" \
--arg dns_name "$DNS_NAME" \
--argjson config "$VM_CONFIG_JSON" \ --argjson config "$VM_CONFIG_JSON" \
'{meta:{id:$id,name:$name,pid:$pid,created_at:$created_at,guest_ip:$guest_ip,tap:$tap,api_sock:$api_sock,log:$log,rootfs:$rootfs,kernel:$kernel,dns_name:$dns_name},config:$config}' \ '{meta:{id:$id,name:$name,pid:$pid,created_at:$created_at,guest_ip:$guest_ip,tap:$tap,api_sock:$api_sock,log:$log,rootfs:$rootfs,kernel:$kernel},config:$config}' \
> "$VM_DIR/vm.json" > "$VM_DIR/vm.json"
log "enabling NAT for interactive session" log "enabling NAT for interactive session"

View file

@ -20,6 +20,7 @@ import (
"banger/internal/paths" "banger/internal/paths"
"banger/internal/rpc" "banger/internal/rpc"
"banger/internal/system" "banger/internal/system"
"banger/internal/vmdns"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -112,10 +113,10 @@ func newDaemonCommand() *cobra.Command {
} }
ping, pingErr := rpc.Call[api.PingResult](cmd.Context(), layout.SocketPath, "ping", api.Empty{}) ping, pingErr := rpc.Call[api.PingResult](cmd.Context(), layout.SocketPath, "ping", api.Empty{})
if pingErr != nil { if pingErr != nil {
_, err = fmt.Fprintf(cmd.OutOrStdout(), "stopped\nsocket: %s\nlog: %s\n", layout.SocketPath, layout.DaemonLog) _, err = fmt.Fprintf(cmd.OutOrStdout(), "stopped\nsocket: %s\nlog: %s\ndns: %s\n", layout.SocketPath, layout.DaemonLog, vmdns.DefaultListenAddr)
return err return err
} }
_, err = fmt.Fprintf(cmd.OutOrStdout(), "running\npid: %d\nsocket: %s\nlog: %s\n", ping.PID, layout.SocketPath, layout.DaemonLog) _, err = fmt.Fprintf(cmd.OutOrStdout(), "running\npid: %d\nsocket: %s\nlog: %s\ndns: %s\n", ping.PID, layout.SocketPath, layout.DaemonLog, vmdns.DefaultListenAddr)
return err return err
}, },
}, },

View file

@ -263,6 +263,9 @@ func TestDaemonStatusIncludesLogPathWhenStopped(t *testing.T) {
if !strings.Contains(output, "log: "+filepath.Join(stateHome, "banger", "bangerd.log")) { if !strings.Contains(output, "log: "+filepath.Join(stateHome, "banger", "bangerd.log")) {
t.Fatalf("output = %q, want daemon log path", output) t.Fatalf("output = %q, want daemon log path", output)
} }
if !strings.Contains(output, "dns: 127.0.0.1:42069") {
t.Fatalf("output = %q, want dns listener", output)
}
} }
func TestBuildDaemonCommandIsDetachedFromCallerContext(t *testing.T) { func TestBuildDaemonCommandIsDetachedFromCallerContext(t *testing.T) {

View file

@ -18,8 +18,6 @@ type fileConfig struct {
RepoRoot string `toml:"repo_root"` RepoRoot string `toml:"repo_root"`
LogLevel string `toml:"log_level"` LogLevel string `toml:"log_level"`
FirecrackerBin string `toml:"firecracker_bin"` FirecrackerBin string `toml:"firecracker_bin"`
MapDNSBin string `toml:"mapdns_bin"`
MapDNSDataFile string `toml:"mapdns_data_file"`
SSHKeyPath string `toml:"ssh_key_path"` SSHKeyPath string `toml:"ssh_key_path"`
NamegenPath string `toml:"namegen_path"` NamegenPath string `toml:"namegen_path"`
CustomizeScript string `toml:"customize_script"` CustomizeScript string `toml:"customize_script"`
@ -80,12 +78,6 @@ func Load(layout paths.Layout) (model.DaemonConfig, error) {
if file.LogLevel != "" { if file.LogLevel != "" {
cfg.LogLevel = file.LogLevel cfg.LogLevel = file.LogLevel
} }
if file.MapDNSBin != "" {
cfg.MapDNSBin = file.MapDNSBin
}
if file.MapDNSDataFile != "" {
cfg.MapDNSDataFile = file.MapDNSDataFile
}
if file.SSHKeyPath != "" { if file.SSHKeyPath != "" {
cfg.SSHKeyPath = file.SSHKeyPath cfg.SSHKeyPath = file.SSHKeyPath
} }
@ -149,18 +141,9 @@ func Load(layout paths.Layout) (model.DaemonConfig, error) {
} }
cfg.MetricsPollInterval = duration cfg.MetricsPollInterval = duration
} }
if value := os.Getenv("BANGER_MAPDNS_BIN"); value != "" {
cfg.MapDNSBin = value
}
if value := os.Getenv("BANGER_MAPDNS_DATA_FILE"); value != "" {
cfg.MapDNSDataFile = value
}
if value := os.Getenv("BANGER_LOG_LEVEL"); value != "" { if value := os.Getenv("BANGER_LOG_LEVEL"); value != "" {
cfg.LogLevel = value cfg.LogLevel = value
} }
if cfg.MapDNSBin == "" {
cfg.MapDNSBin = "mapdns"
}
return cfg, nil return cfg, nil
} }

View file

@ -127,9 +127,7 @@ func TestLoadFallsBackToLegacyRuntimeLayoutWithoutBundleMetadata(t *testing.T) {
} }
} }
func TestLoadAppliesMapDNSEnvOverrides(t *testing.T) { func TestLoadAppliesLogLevelEnvOverride(t *testing.T) {
t.Setenv("BANGER_MAPDNS_BIN", "/opt/bin/mapdns")
t.Setenv("BANGER_MAPDNS_DATA_FILE", "/tmp/mapdns-records.json")
t.Setenv("BANGER_LOG_LEVEL", "debug") t.Setenv("BANGER_LOG_LEVEL", "debug")
cfg, err := Load(paths.Layout{ConfigDir: t.TempDir()}) cfg, err := Load(paths.Layout{ConfigDir: t.TempDir()})
@ -137,12 +135,6 @@ func TestLoadAppliesMapDNSEnvOverrides(t *testing.T) {
t.Fatalf("Load: %v", err) t.Fatalf("Load: %v", err)
} }
if cfg.MapDNSBin != "/opt/bin/mapdns" {
t.Fatalf("MapDNSBin = %q", cfg.MapDNSBin)
}
if cfg.MapDNSDataFile != "/tmp/mapdns-records.json" {
t.Fatalf("MapDNSDataFile = %q", cfg.MapDNSDataFile)
}
if cfg.LogLevel != "debug" { if cfg.LogLevel != "debug" {
t.Fatalf("LogLevel = %q", cfg.LogLevel) t.Fatalf("LogLevel = %q", cfg.LogLevel)
} }

View file

@ -22,6 +22,7 @@ import (
"banger/internal/rpc" "banger/internal/rpc"
"banger/internal/store" "banger/internal/store"
"banger/internal/system" "banger/internal/system"
"banger/internal/vmdns"
) )
type Daemon struct { type Daemon struct {
@ -35,10 +36,11 @@ type Daemon struct {
once sync.Once once sync.Once
pid int pid int
listener net.Listener listener net.Listener
vmDNS *vmdns.Server
requestHandler func(context.Context, rpc.Request) rpc.Response requestHandler func(context.Context, rpc.Request) rpc.Response
} }
func Open(ctx context.Context) (*Daemon, error) { func Open(ctx context.Context) (d *Daemon, err error) {
layout, err := paths.Resolve() layout, err := paths.Resolve()
if err != nil { if err != nil {
return nil, err return nil, err
@ -59,7 +61,7 @@ func Open(ctx context.Context) (*Daemon, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
d := &Daemon{ d = &Daemon{
layout: layout, layout: layout,
config: cfg, config: cfg,
store: db, store: db,
@ -69,11 +71,20 @@ func Open(ctx context.Context) (*Daemon, error) {
pid: os.Getpid(), pid: os.Getpid(),
} }
d.logger.Info("daemon opened", "socket", layout.SocketPath, "state_dir", layout.StateDir, "runtime_dir", cfg.RuntimeDir, "log_level", cfg.LogLevel) d.logger.Info("daemon opened", "socket", layout.SocketPath, "state_dir", layout.StateDir, "runtime_dir", cfg.RuntimeDir, "log_level", cfg.LogLevel)
if err := d.ensureDefaultImage(ctx); err != nil { if err = d.startVMDNS(vmdns.DefaultListenAddr); err != nil {
d.logger.Error("daemon open failed", "stage", "start_vm_dns", "error", err.Error())
return nil, err
}
defer func() {
if err != nil {
_ = d.stopVMDNS()
}
}()
if err = d.ensureDefaultImage(ctx); err != nil {
d.logger.Error("daemon open failed", "stage", "ensure_default_image", "error", err.Error()) d.logger.Error("daemon open failed", "stage", "ensure_default_image", "error", err.Error())
return nil, err return nil, err
} }
if err := d.reconcile(ctx); err != nil { if err = d.reconcile(ctx); err != nil {
d.logger.Error("daemon open failed", "stage", "reconcile", "error", err.Error()) d.logger.Error("daemon open failed", "stage", "reconcile", "error", err.Error())
return nil, err return nil, err
} }
@ -90,7 +101,7 @@ func (d *Daemon) Close() error {
if d.listener != nil { if d.listener != nil {
_ = d.listener.Close() _ = d.listener.Close()
} }
err = d.store.Close() err = errors.Join(d.stopVMDNS(), d.store.Close())
}) })
return err return err
} }
@ -358,6 +369,27 @@ func (d *Daemon) backgroundLoop() {
} }
} }
func (d *Daemon) startVMDNS(addr string) error {
server, err := vmdns.New(addr, d.logger)
if err != nil {
return err
}
d.vmDNS = server
if d.logger != nil {
d.logger.Info("vm dns serving", "dns_addr", server.Addr())
}
return nil
}
func (d *Daemon) stopVMDNS() error {
if d.vmDNS == nil {
return nil
}
err := d.vmDNS.Close()
d.vmDNS = nil
return err
}
func (d *Daemon) ensureDefaultImage(ctx context.Context) error { func (d *Daemon) ensureDefaultImage(ctx context.Context) error {
if d.config.DefaultImageName == "" { if d.config.DefaultImageName == "" {
return nil return nil
@ -477,6 +509,9 @@ func (d *Daemon) reconcile(ctx context.Context) error {
return op.fail(err, vmLogAttrs(vm)...) return op.fail(err, vmLogAttrs(vm)...)
} }
} }
if err := d.rebuildDNS(ctx); err != nil {
return op.fail(err)
}
op.done() op.done()
return nil return nil
} }

View file

@ -283,58 +283,36 @@ func writeDefaultImageArtifacts(t *testing.T, dir string) (rootfs, kernel, initr
return rootfs, kernel, initrd, modulesDir, packages return rootfs, kernel, initrd, modulesDir, packages
} }
func TestSetDNSUsesConfiguredMapDNSDataFile(t *testing.T) { func TestStartVMDNSFailsWhenAddressBusy(t *testing.T) {
t.Parallel() t.Parallel()
dataFile := filepath.Join(t.TempDir(), "mapdns", "records.json") packetConn, err := net.ListenPacket("udp", "127.0.0.1:0")
runner := &scriptedRunner{ if err != nil {
t: t, t.Fatalf("ListenPacket: %v", err)
steps: []runnerStep{
{
call: runnerCall{
name: "custom-mapdns",
args: []string{"set", "--data-file", dataFile, "devbox.vm", "172.16.0.8"},
},
},
},
}
d := &Daemon{
runner: runner,
config: model.DaemonConfig{
MapDNSBin: "custom-mapdns",
MapDNSDataFile: dataFile,
},
} }
defer packetConn.Close()
if err := d.setDNS(context.Background(), "devbox", "172.16.0.8"); err != nil { d := &Daemon{}
t.Fatalf("setDNS: %v", err) if err := d.startVMDNS(packetConn.LocalAddr().String()); err == nil {
t.Fatal("startVMDNS() succeeded on occupied address, want failure")
} }
runner.assertExhausted()
} }
func TestSetDNSUsesMapDNSDefaultsWhenDataFileUnset(t *testing.T) { func TestSetDNSPublishesIntoDaemonServer(t *testing.T) {
t.Parallel() t.Parallel()
runner := &scriptedRunner{ d := &Daemon{}
t: t, if err := d.startVMDNS("127.0.0.1:0"); err != nil {
steps: []runnerStep{ t.Fatalf("startVMDNS: %v", err)
{
call: runnerCall{
name: "mapdns",
args: []string{"set", "devbox.vm", "172.16.0.8"},
},
},
},
}
d := &Daemon{
runner: runner,
config: model.DaemonConfig{},
} }
defer d.stopVMDNS()
if err := d.setDNS(context.Background(), "devbox", "172.16.0.8"); err != nil { if err := d.setDNS(context.Background(), "devbox", "172.16.0.8"); err != nil {
t.Fatalf("setDNS: %v", err) t.Fatalf("setDNS: %v", err)
} }
runner.assertExhausted() if _, ok := d.vmDNS.Lookup("devbox.vm"); !ok {
t.Fatal("devbox.vm missing after setDNS")
}
} }
func TestDispatchUsesPassedContext(t *testing.T) { func TestDispatchUsesPassedContext(t *testing.T) {

View file

@ -109,12 +109,6 @@ func (d *Daemon) BuildImage(ctx context.Context, params api.ImageBuildParams) (i
"BANGER_RUNTIME_DIR="+d.config.RuntimeDir, "BANGER_RUNTIME_DIR="+d.config.RuntimeDir,
"BANGER_STATE_DIR="+filepath.Join(d.layout.StateDir, "image-build"), "BANGER_STATE_DIR="+filepath.Join(d.layout.StateDir, "image-build"),
) )
if d.config.MapDNSBin != "" {
cmd.Env = append(cmd.Env, "BANGER_MAPDNS_BIN="+d.config.MapDNSBin)
}
if d.config.MapDNSDataFile != "" {
cmd.Env = append(cmd.Env, "BANGER_MAPDNS_DATA_FILE="+d.config.MapDNSDataFile)
}
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
_ = os.RemoveAll(artifactDir) _ = os.RemoveAll(artifactDir)
return model.Image{}, err return model.Image{}, err

View file

@ -48,7 +48,7 @@ func TestStartVMLockedLogsBridgeFailure(t *testing.T) {
for _, name := range []string{ for _, name := range []string{
"sudo", "ip", "dmsetup", "losetup", "blockdev", "truncate", "pgrep", "ps", "sudo", "ip", "dmsetup", "losetup", "blockdev", "truncate", "pgrep", "ps",
"chown", "chmod", "kill", "e2cp", "e2rm", "debugfs", "mkfs.ext4", "mount", "chown", "chmod", "kill", "e2cp", "e2rm", "debugfs", "mkfs.ext4", "mount",
"umount", "cp", "mapdns", "umount", "cp",
} { } {
writeFakeExecutable(t, filepath.Join(binDir, name)) writeFakeExecutable(t, filepath.Join(binDir, name))
} }
@ -98,7 +98,6 @@ func TestStartVMLockedLogsBridgeFailure(t *testing.T) {
BridgeIP: model.DefaultBridgeIP, BridgeIP: model.DefaultBridgeIP,
DefaultDNS: model.DefaultDNS, DefaultDNS: model.DefaultDNS,
FirecrackerBin: firecrackerBin, FirecrackerBin: firecrackerBin,
MapDNSBin: "mapdns",
StatsPollInterval: model.DefaultStatsPollInterval, StatsPollInterval: model.DefaultStatsPollInterval,
}, },
runner: runner, runner: runner,
@ -130,7 +129,7 @@ func TestBuildImagePreservesBuildLogOnFailure(t *testing.T) {
} }
binDir := t.TempDir() binDir := t.TempDir()
for _, name := range []string{"sudo", "ip", "curl", "ssh", "jq", "sha256sum", "e2fsck", "resize2fs", "mapdns"} { for _, name := range []string{"sudo", "ip", "curl", "ssh", "jq", "sha256sum", "e2fsck", "resize2fs"} {
writeFakeExecutable(t, filepath.Join(binDir, name)) writeFakeExecutable(t, filepath.Join(binDir, name))
} }
bashPath, err := exec.LookPath("bash") bashPath, err := exec.LookPath("bash")
@ -169,7 +168,6 @@ func TestBuildImagePreservesBuildLogOnFailure(t *testing.T) {
config: model.DaemonConfig{ config: model.DaemonConfig{
RuntimeDir: t.TempDir(), RuntimeDir: t.TempDir(),
CustomizeScript: script, CustomizeScript: script,
MapDNSBin: "mapdns",
DefaultImageName: "default", DefaultImageName: "default",
}, },
store: store, store: store,

View file

@ -2,8 +2,6 @@ package daemon
import ( import (
"context" "context"
"os"
"path/filepath"
"strings" "strings"
"banger/internal/model" "banger/internal/model"
@ -19,7 +17,6 @@ func (d *Daemon) validateStartPrereqs(ctx context.Context, vm model.VMRecord, im
checks.RequireCommand(command, toolHint(command)) checks.RequireCommand(command, toolHint(command))
} }
checks.RequireExecutable(d.config.FirecrackerBin, "firecracker binary", hint) checks.RequireExecutable(d.config.FirecrackerBin, "firecracker binary", hint)
checks.RequireExecutable(d.config.MapDNSBin, "mapdns binary", `install mapdns or set "mapdns_bin" / BANGER_MAPDNS_BIN`)
checks.RequireFile(image.RootfsPath, "rootfs image", "select a valid image or rebuild the runtime bundle") checks.RequireFile(image.RootfsPath, "rootfs image", "select a valid image or rebuild the runtime bundle")
checks.RequireFile(image.KernelPath, "kernel image", `set "default_kernel" or refresh the runtime bundle`) checks.RequireFile(image.KernelPath, "kernel image", `set "default_kernel" or refresh the runtime bundle`)
if strings.TrimSpace(image.InitrdPath) != "" { if strings.TrimSpace(image.InitrdPath) != "" {
@ -33,14 +30,6 @@ func (d *Daemon) validateStartPrereqs(ctx context.Context, vm model.VMRecord, im
if vm.Spec.NATEnabled { if vm.Spec.NATEnabled {
d.addNATPrereqs(ctx, checks) d.addNATPrereqs(ctx, checks)
} }
if dataFile := strings.TrimSpace(d.config.MapDNSDataFile); dataFile != "" {
parent := filepath.Dir(dataFile)
if parent != "." && parent != "" {
if _, err := os.Stat(parent); err != nil && !os.IsNotExist(err) {
checks.Addf("mapdns data directory %s is not accessible (%v)", parent, err)
}
}
}
return checks.Err("vm start preflight failed") return checks.Err("vm start preflight failed")
} }
@ -52,7 +41,6 @@ func (d *Daemon) validateImageBuildPrereqs(ctx context.Context, baseRootfs, kern
checks.RequireCommand(command, toolHint(command)) checks.RequireCommand(command, toolHint(command))
} }
checks.RequireExecutable(d.config.CustomizeScript, "customize.sh helper", hint) checks.RequireExecutable(d.config.CustomizeScript, "customize.sh helper", hint)
checks.RequireExecutable(d.config.MapDNSBin, "mapdns binary", `install mapdns or set "mapdns_bin" / BANGER_MAPDNS_BIN`)
checks.RequireFile(baseRootfs, "base rootfs image", `pass --base-rootfs or set "default_base_rootfs"`) checks.RequireFile(baseRootfs, "base rootfs image", `pass --base-rootfs or set "default_base_rootfs"`)
checks.RequireFile(kernelPath, "kernel image", `pass --kernel or set "default_kernel"`) checks.RequireFile(kernelPath, "kernel image", `pass --kernel or set "default_kernel"`)
if strings.TrimSpace(initrdPath) != "" { if strings.TrimSpace(initrdPath) != "" {
@ -109,8 +97,6 @@ func toolHint(command string) string {
return "install jq" return "install jq"
case "sha256sum": case "sha256sum":
return "install coreutils" return "install coreutils"
case "mapdns":
return `install mapdns or set "mapdns_bin" / BANGER_MAPDNS_BIN`
case "ssh": case "ssh":
return "install openssh-client" return "install openssh-client"
case "bash": case "bash":

View file

@ -15,6 +15,7 @@ import (
"banger/internal/model" "banger/internal/model"
"banger/internal/paths" "banger/internal/paths"
"banger/internal/system" "banger/internal/system"
"banger/internal/vmdns"
) )
func (d *Daemon) CreateVM(ctx context.Context, params api.VMCreateParams) (vm model.VMRecord, err error) { func (d *Daemon) CreateVM(ctx context.Context, params api.VMCreateParams) (vm model.VMRecord, err error) {
@ -100,7 +101,7 @@ func (d *Daemon) CreateVM(ctx context.Context, params api.VMCreateParams) (vm mo
Runtime: model.VMRuntime{ Runtime: model.VMRuntime{
State: model.VMStateCreated, State: model.VMStateCreated,
GuestIP: guestIP, GuestIP: guestIP,
DNSName: name + ".vm", DNSName: vmdns.RecordName(name),
VMDir: vmDir, VMDir: vmDir,
SystemOverlay: filepath.Join(vmDir, "system.cow"), SystemOverlay: filepath.Join(vmDir, "system.cow"),
WorkDiskPath: filepath.Join(vmDir, "root.ext4"), WorkDiskPath: filepath.Join(vmDir, "root.ext4"),
@ -859,30 +860,44 @@ func clearRuntimeHandles(vm *model.VMRecord) {
} }
func (d *Daemon) setDNS(ctx context.Context, vmName, guestIP string) error { func (d *Daemon) setDNS(ctx context.Context, vmName, guestIP string) error {
if dataFile := strings.TrimSpace(d.config.MapDNSDataFile); dataFile != "" { if d.vmDNS == nil {
if err := os.MkdirAll(filepath.Dir(dataFile), 0o755); err != nil { return nil
return err
}
} }
_, err := d.runner.Run(ctx, d.mapdnsBinary(), d.mapdnsArgs("set", vmName+".vm", guestIP)...) return d.vmDNS.Set(vmdns.RecordName(vmName), guestIP)
if err == nil && d.logger != nil {
d.logger.Debug("dns record set", "dns_name", vmName+".vm", "guest_ip", guestIP)
}
return err
} }
func (d *Daemon) removeDNS(ctx context.Context, dnsName string) error { func (d *Daemon) removeDNS(ctx context.Context, dnsName string) error {
if dnsName == "" { if dnsName == "" {
return nil return nil
} }
_, err := d.runner.Run(ctx, d.mapdnsBinary(), d.mapdnsArgs("rm", dnsName)...) if d.vmDNS == nil {
if err != nil && strings.Contains(err.Error(), "not found") {
return nil return nil
} }
if err == nil && d.logger != nil { return d.vmDNS.Remove(dnsName)
d.logger.Debug("dns record removed", "dns_name", dnsName) }
func (d *Daemon) rebuildDNS(ctx context.Context) error {
if d.vmDNS == nil {
return nil
} }
return err vms, err := d.store.ListVMs(ctx)
if err != nil {
return err
}
records := make(map[string]string)
for _, vm := range vms {
if vm.State != model.VMStateRunning {
continue
}
if !system.ProcessRunning(vm.Runtime.PID, vm.Runtime.APISockPath) {
continue
}
if strings.TrimSpace(vm.Runtime.GuestIP) == "" {
continue
}
records[vmdns.RecordName(vm.Name)] = vm.Runtime.GuestIP
}
return d.vmDNS.Replace(records)
} }
func (d *Daemon) killVMProcess(ctx context.Context, pid int) error { func (d *Daemon) killVMProcess(ctx context.Context, pid int) error {
@ -927,19 +942,3 @@ func validateOptionalPositiveSetting(label string, value *int) error {
} }
return nil return nil
} }
func (d *Daemon) mapdnsBinary() string {
if value := strings.TrimSpace(d.config.MapDNSBin); value != "" {
return value
}
return "mapdns"
}
func (d *Daemon) mapdnsArgs(subcommand string, args ...string) []string {
out := []string{subcommand}
if value := strings.TrimSpace(d.config.MapDNSDataFile); value != "" {
out = append(out, "--data-file", value)
}
out = append(out, args...)
return out
}

View file

@ -15,6 +15,7 @@ import (
"banger/internal/model" "banger/internal/model"
"banger/internal/paths" "banger/internal/paths"
"banger/internal/store" "banger/internal/store"
"banger/internal/vmdns"
) )
func TestFindVMPrefixResolution(t *testing.T) { func TestFindVMPrefixResolution(t *testing.T) {
@ -143,6 +144,65 @@ func TestReconcileStopsStaleRunningVMAndClearsRuntimeHandles(t *testing.T) {
} }
} }
func TestRebuildDNSIncludesOnlyLiveRunningVMs(t *testing.T) {
t.Parallel()
ctx := context.Background()
db := openDaemonStore(t)
liveSock := filepath.Join(t.TempDir(), "live.sock")
liveCmd := startFakeFirecrackerProcess(t, liveSock)
t.Cleanup(func() {
_ = liveCmd.Process.Kill()
_ = liveCmd.Wait()
})
live := testVM("live", "image-live", "172.16.0.21")
live.State = model.VMStateRunning
live.Runtime.State = model.VMStateRunning
live.Runtime.PID = liveCmd.Process.Pid
live.Runtime.APISockPath = liveSock
stale := testVM("stale", "image-stale", "172.16.0.22")
stale.State = model.VMStateRunning
stale.Runtime.State = model.VMStateRunning
stale.Runtime.PID = 999999
stale.Runtime.APISockPath = filepath.Join(t.TempDir(), "stale.sock")
stopped := testVM("stopped", "image-stopped", "172.16.0.23")
for _, vm := range []model.VMRecord{live, stale, stopped} {
if err := db.UpsertVM(ctx, vm); err != nil {
t.Fatalf("UpsertVM(%s): %v", vm.Name, err)
}
}
server, err := vmdns.New("127.0.0.1:0", nil)
if err != nil {
t.Fatalf("vmdns.New: %v", err)
}
t.Cleanup(func() {
if err := server.Close(); err != nil {
t.Fatalf("server.Close: %v", err)
}
})
d := &Daemon{store: db, vmDNS: server}
if err := d.rebuildDNS(ctx); err != nil {
t.Fatalf("rebuildDNS: %v", err)
}
if _, ok := server.Lookup("live.vm"); !ok {
t.Fatal("live.vm missing after rebuildDNS")
}
if _, ok := server.Lookup("stale.vm"); ok {
t.Fatal("stale.vm should not be published")
}
if _, ok := server.Lookup("stopped.vm"); ok {
t.Fatal("stopped.vm should not be published")
}
}
func TestSetVMRejectsStoppedOnlyChangesForRunningVM(t *testing.T) { func TestSetVMRejectsStoppedOnlyChangesForRunningVM(t *testing.T) {
t.Parallel() t.Parallel()
@ -316,7 +376,7 @@ func TestValidateStartPrereqsReportsNATUplinkFailure(t *testing.T) {
for _, name := range []string{ for _, name := range []string{
"sudo", "ip", "dmsetup", "losetup", "blockdev", "truncate", "pgrep", "ps", "sudo", "ip", "dmsetup", "losetup", "blockdev", "truncate", "pgrep", "ps",
"chown", "chmod", "kill", "e2cp", "e2rm", "debugfs", "mkfs.ext4", "mount", "chown", "chmod", "kill", "e2cp", "e2rm", "debugfs", "mkfs.ext4", "mount",
"umount", "cp", "iptables", "sysctl", "mapdns", "umount", "cp", "iptables", "sysctl",
} { } {
writeFakeExecutable(t, filepath.Join(binDir, name)) writeFakeExecutable(t, filepath.Join(binDir, name))
} }
@ -339,7 +399,6 @@ func TestValidateStartPrereqsReportsNATUplinkFailure(t *testing.T) {
runner: runner, runner: runner,
config: model.DaemonConfig{ config: model.DaemonConfig{
FirecrackerBin: firecrackerBin, FirecrackerBin: firecrackerBin,
MapDNSBin: "mapdns",
}, },
} }
vm := testVM("nat", "image-nat", "172.16.0.12") vm := testVM("nat", "image-nat", "172.16.0.12")

View file

@ -38,8 +38,6 @@ type DaemonConfig struct {
RuntimeDir string RuntimeDir string
LogLevel string LogLevel string
FirecrackerBin string FirecrackerBin string
MapDNSBin string
MapDNSDataFile string
SSHKeyPath string SSHKeyPath string
NamegenPath string NamegenPath string
CustomizeScript string CustomizeScript string

View file

@ -22,7 +22,6 @@ func TestBootstrapExtractsBundleAndValidatesChecksum(t *testing.T) {
"runtime/namegen": "namegen", "runtime/namegen": "namegen",
"runtime/customize.sh": "#!/bin/bash\n", "runtime/customize.sh": "#!/bin/bash\n",
"runtime/packages.sh": "#!/bin/bash\n", "runtime/packages.sh": "#!/bin/bash\n",
"runtime/dns.sh": "#!/bin/bash\n",
"runtime/packages.apt": "vim\n", "runtime/packages.apt": "vim\n",
"runtime/rootfs-docker.ext4": "rootfs", "runtime/rootfs-docker.ext4": "rootfs",
"runtime/wtf/root/boot/vmlinux-6.8.0-94-generic": "kernel", "runtime/wtf/root/boot/vmlinux-6.8.0-94-generic": "kernel",

257
internal/vmdns/server.go Normal file
View file

@ -0,0 +1,257 @@
package vmdns
import (
"errors"
"fmt"
"log/slog"
"net"
"net/netip"
"strings"
"sync"
"time"
"github.com/miekg/dns"
)
const (
DefaultListenAddr = "127.0.0.1:42069"
recordTTLSeconds = 5
vmZoneSuffix = ".vm."
)
type Server struct {
logger *slog.Logger
mu sync.RWMutex
records map[string]netip.Addr
addr string
server *dns.Server
conn net.PacketConn
done chan error
}
func New(addr string, logger *slog.Logger) (*Server, error) {
packetConn, err := net.ListenPacket("udp", addr)
if err != nil {
return nil, err
}
s := &Server{
logger: logger,
records: make(map[string]netip.Addr),
addr: packetConn.LocalAddr().String(),
conn: packetConn,
done: make(chan error, 1),
}
s.server = &dns.Server{
PacketConn: packetConn,
Handler: dns.HandlerFunc(s.handleDNS),
}
go func() {
s.done <- s.server.ActivateAndServe()
close(s.done)
}()
return s, nil
}
func (s *Server) Addr() string {
if s == nil {
return ""
}
return s.addr
}
func (s *Server) Close() error {
if s == nil || s.server == nil {
return nil
}
connErr := error(nil)
if s.conn != nil {
connErr = s.conn.Close()
s.conn = nil
}
shutdownErr := s.server.Shutdown()
if isIgnorableCloseErr(shutdownErr) {
shutdownErr = nil
}
var serveErr error
select {
case serveErr = <-s.done:
case <-time.After(2 * time.Second):
serveErr = errors.New("timed out waiting for vm dns server shutdown")
}
if isClosedServeErr(serveErr) {
serveErr = nil
}
s.server = nil
s.done = nil
return errors.Join(connErr, shutdownErr, serveErr)
}
func (s *Server) Set(name, guestIP string) error {
if s == nil {
return nil
}
addr, err := netip.ParseAddr(strings.TrimSpace(guestIP))
if err != nil {
return fmt.Errorf("parse guest IP %q: %w", guestIP, err)
}
if !addr.Is4() {
return fmt.Errorf("guest IP must be IPv4: %q", guestIP)
}
fqdn, err := normalizeVMName(name)
if err != nil {
return err
}
s.mu.Lock()
s.records[fqdn] = addr
s.mu.Unlock()
if s.logger != nil {
s.logger.Debug("vm dns record set", "dns_name", displayName(fqdn), "guest_ip", addr.String())
}
return nil
}
func (s *Server) Remove(name string) error {
if s == nil {
return nil
}
fqdn, err := normalizeVMName(name)
if err != nil {
return nil
}
s.mu.Lock()
delete(s.records, fqdn)
s.mu.Unlock()
if s.logger != nil {
s.logger.Debug("vm dns record removed", "dns_name", displayName(fqdn))
}
return nil
}
func (s *Server) Replace(records map[string]string) error {
if s == nil {
return nil
}
next := make(map[string]netip.Addr, len(records))
for name, guestIP := range records {
fqdn, err := normalizeVMName(name)
if err != nil {
return err
}
addr, err := netip.ParseAddr(strings.TrimSpace(guestIP))
if err != nil {
return fmt.Errorf("parse guest IP for %s: %w", name, err)
}
if !addr.Is4() {
return fmt.Errorf("guest IP for %s must be IPv4: %q", name, guestIP)
}
next[fqdn] = addr
}
s.mu.Lock()
s.records = next
s.mu.Unlock()
return nil
}
func (s *Server) Lookup(name string) (netip.Addr, bool) {
if s == nil {
return netip.Addr{}, false
}
fqdn, err := normalizeVMName(name)
if err != nil {
return netip.Addr{}, false
}
s.mu.RLock()
defer s.mu.RUnlock()
addr, ok := s.records[fqdn]
return addr, ok
}
func RecordName(vmName string) string {
name := strings.TrimSpace(strings.ToLower(vmName))
name = strings.TrimSuffix(name, ".")
if strings.HasSuffix(name, ".vm") {
return name
}
return name + ".vm"
}
func normalizeVMName(name string) (string, error) {
name = strings.TrimSpace(name)
if name == "" {
return "", errors.New("dns name is required")
}
fqdn := strings.ToLower(dns.Fqdn(name))
if !strings.HasSuffix(fqdn, vmZoneSuffix) {
return "", fmt.Errorf("dns name must end with .vm: %q", name)
}
return fqdn, nil
}
func displayName(fqdn string) string {
return strings.TrimSuffix(fqdn, ".")
}
func isVMQueryName(name string) bool {
return strings.HasSuffix(strings.ToLower(dns.Fqdn(name)), vmZoneSuffix)
}
func (s *Server) handleDNS(w dns.ResponseWriter, req *dns.Msg) {
resp := new(dns.Msg)
resp.SetReply(req)
resp.Authoritative = true
if len(req.Question) == 0 {
resp.Rcode = dns.RcodeFormatError
_ = w.WriteMsg(resp)
return
}
question := req.Question[0]
if !isVMQueryName(question.Name) {
resp.Rcode = dns.RcodeRefused
_ = w.WriteMsg(resp)
return
}
addr, ok := s.Lookup(question.Name)
if !ok {
resp.Rcode = dns.RcodeNameError
_ = w.WriteMsg(resp)
return
}
if question.Qtype == dns.TypeA {
resp.Answer = []dns.RR{
&dns.A{
Hdr: dns.RR_Header{
Name: strings.ToLower(dns.Fqdn(question.Name)),
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: recordTTLSeconds,
},
A: net.IP(addr.AsSlice()),
},
}
}
_ = w.WriteMsg(resp)
}
func isClosedServeErr(err error) bool {
if err == nil {
return true
}
return errors.Is(err, net.ErrClosed) || strings.Contains(strings.ToLower(err.Error()), "closed")
}
func isIgnorableCloseErr(err error) bool {
if err == nil {
return true
}
return strings.Contains(strings.ToLower(err.Error()), "server not started")
}

View file

@ -0,0 +1,126 @@
package vmdns
import (
"net"
"testing"
"github.com/miekg/dns"
)
func TestRecordName(t *testing.T) {
if got := RecordName("DevBox"); got != "devbox.vm" {
t.Fatalf("RecordName = %q, want devbox.vm", got)
}
if got := RecordName("already.vm"); got != "already.vm" {
t.Fatalf("RecordName = %q, want already.vm", got)
}
}
func TestServerAnswersVMQueries(t *testing.T) {
server := startTestServer(t)
if err := server.Set("devbox.vm", "172.16.0.8"); err != nil {
t.Fatalf("Set: %v", err)
}
t.Run("A record", func(t *testing.T) {
resp := exchangeQuery(t, server.Addr(), "devbox.vm.", dns.TypeA)
if resp.Rcode != dns.RcodeSuccess {
t.Fatalf("rcode = %d, want success", resp.Rcode)
}
if len(resp.Answer) != 1 {
t.Fatalf("answer count = %d, want 1", len(resp.Answer))
}
a, ok := resp.Answer[0].(*dns.A)
if !ok {
t.Fatalf("answer type = %T, want *dns.A", resp.Answer[0])
}
if got := a.A.String(); got != "172.16.0.8" {
t.Fatalf("A = %q, want 172.16.0.8", got)
}
})
t.Run("known AAAA returns NODATA", func(t *testing.T) {
resp := exchangeQuery(t, server.Addr(), "devbox.vm.", dns.TypeAAAA)
if resp.Rcode != dns.RcodeSuccess {
t.Fatalf("rcode = %d, want success", resp.Rcode)
}
if len(resp.Answer) != 0 {
t.Fatalf("answer count = %d, want 0", len(resp.Answer))
}
})
t.Run("unknown name returns NXDOMAIN", func(t *testing.T) {
resp := exchangeQuery(t, server.Addr(), "missing.vm.", dns.TypeA)
if resp.Rcode != dns.RcodeNameError {
t.Fatalf("rcode = %d, want NXDOMAIN", resp.Rcode)
}
})
t.Run("outside zone returns REFUSED", func(t *testing.T) {
resp := exchangeQuery(t, server.Addr(), "example.com.", dns.TypeA)
if resp.Rcode != dns.RcodeRefused {
t.Fatalf("rcode = %d, want REFUSED", resp.Rcode)
}
})
}
func TestServerReplaceSwapsRecordSet(t *testing.T) {
server := startTestServer(t)
if err := server.Replace(map[string]string{
"alpha.vm": "172.16.0.2",
"beta.vm": "172.16.0.3",
}); err != nil {
t.Fatalf("Replace: %v", err)
}
if _, ok := server.Lookup("alpha.vm"); !ok {
t.Fatal("alpha.vm missing after replace")
}
if err := server.Replace(map[string]string{"beta.vm": "172.16.0.4"}); err != nil {
t.Fatalf("Replace second set: %v", err)
}
if _, ok := server.Lookup("alpha.vm"); ok {
t.Fatal("alpha.vm should have been removed by replace")
}
addr, ok := server.Lookup("beta.vm")
if !ok || addr.String() != "172.16.0.4" {
t.Fatalf("beta.vm = %v, %v, want 172.16.0.4", addr, ok)
}
}
func TestServerFailsWhenAddressAlreadyInUse(t *testing.T) {
packetConn, err := net.ListenPacket("udp", "127.0.0.1:0")
if err != nil {
t.Fatalf("ListenPacket: %v", err)
}
defer packetConn.Close()
if _, err := New(packetConn.LocalAddr().String(), nil); err == nil {
t.Fatal("New() succeeded on occupied UDP address, want failure")
}
}
func startTestServer(t *testing.T) *Server {
t.Helper()
server, err := New("127.0.0.1:0", nil)
if err != nil {
t.Fatalf("New: %v", err)
}
t.Cleanup(func() {
if err := server.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
})
return server
}
func exchangeQuery(t *testing.T, addr, name string, qtype uint16) *dns.Msg {
t.Helper()
client := &dns.Client{Net: "udp"}
req := new(dns.Msg)
req.SetQuestion(name, qtype)
resp, _, err := client.Exchange(req, addr)
if err != nil {
t.Fatalf("Exchange(%s, %d): %v", name, qtype, err)
}
return resp
}

View file

@ -8,7 +8,6 @@ bundle_root = "runtime"
required_paths = [ required_paths = [
"firecracker", "firecracker",
"customize.sh", "customize.sh",
"dns.sh",
"packages.sh", "packages.sh",
"namegen", "namegen",
"packages.apt", "packages.apt",