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:
parent
430f66d5dd
commit
0a0b0b617b
24 changed files with 576 additions and 278 deletions
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
2
Makefile
2
Makefile
|
|
@ -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
|
||||||
|
|
|
||||||
17
README.md
17
README.md
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
104
dns.sh
|
|
@ -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
12
go.mod
|
|
@ -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
30
go.sum
|
|
@ -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=
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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":
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
257
internal/vmdns/server.go
Normal 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")
|
||||||
|
}
|
||||||
126
internal/vmdns/server_test.go
Normal file
126
internal/vmdns/server_test.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue