# dnsmasq-webui
A self-hosted web UI for managing [dnsmasq](https://thekelleys.org.uk/dnsmasq/doc.html) configuration and hosts. It runs alongside your existing dnsmasq setup and edits the same config files; it does not replace dnsmasq.
---
## Purpose and features
**Purpose:** Point the UI at your dnsmasq config (and optionally hosts). Use the browser to view and edit config, managed hosts, and (if you use DHCP) reservations. Reload dnsmasq after changes via a configurable command.
**Features:**
- View and edit **dnsmasq config** (main config + conf-dir), with per-option source (which file or managed block).
- **Managed hosts file** (addn-hosts) editable in the UI; app writes a single managed hosts file and includes it via the managed config.
- **DHCP host entries** (reservations) if you use DHCP; edit in the UI, written into the managed config.
- **Reload dnsmasq** after config changes (configurable command, e.g. `systemctl reload dnsmasq` or `pkill -HUP -x dnsmasq`).
- Optional **status** and **recent logs** commands (e.g. systemctl/journalctl or custom scripts) shown on the Dnsmasq page.
- **Minimum dnsmasq version** check (configurable; e.g. 2.91). The UI can refuse to start or block config save if the detected version is too old. Version is shown on the Dnsmasq page with a link to release notes.
- **Readiness endpoint** `GET /healthz/ready` for orchestration (Kubernetes, Docker, load balancers). Returns healthy when dnsmasq version meets the minimum and, if configured, dnsmasq is running. The Docker image includes a HEALTHCHECK that uses this endpoint.
- **Self-contained Linux binaries** per OS/arch (RID); no .NET install required. Can run in Docker or directly on the host.
---
## Quick start and install
**Prerequisites:** Linux. dnsmasq (or a dnsmasq container) should already be set up. For the install script: `curl`, `jq`, `unzip`. (WSL may work but is untested.)
### Install from release
The recommended way to get a prebuilt binary is the **install script**. It picks the right build for your OS/arch, installs to a user or system directory, and can create a `dnsmasq-webui` symlink so you can run it from the terminal.
**Quick start:**
```bash
curl -sSL https://raw.githubusercontent.com/alexhopeoconnor/dnsmasq-webui/master/scripts/install.sh | sh
```
**From a git clone:**
```bash
./scripts/install.sh
```
By default the script installs to **`~/.local/share/dnsmasq-webui`** and creates a symlink at `~/.local/bin/dnsmasq-webui` if that directory exists and is writable (so you can run `dnsmasq-webui` if `~/.local/bin` is in your PATH). Then configure (see [Configuration](#configuration)) and run:
```bash
dnsmasq-webui
# or
~/.local/share/dnsmasq-webui/dnsmasq-webui
```
**System-wide install (requires sudo):**
```bash
sudo ./scripts/install.sh --system
```
Installs to `/opt/dnsmasq-webui` and symlinks `/usr/local/bin/dnsmasq-webui`. Run with `dnsmasq-webui` from any terminal.
**Install as a systemd service (start on boot or at login):**
On systemd-based systems you can install a unit so the app runs as a service. Use `--service` alone for a **user** service (no sudo; starts when you log in), or with `--system` for a **system** service (requires sudo; starts at boot).
```bash
./scripts/install.sh --service # User service (no sudo)
sudo ./scripts/install.sh --system --service # System service (starts at boot)
```
After install: configure the app (see [Configuration](#configuration)), then start the service: `systemctl --user start dnsmasq-webui` or `sudo systemctl start dnsmasq-webui`. User services run only when you’re logged in unless you enable linger: `loginctl enable-linger`. If you add `--service` later or switch from user to system service (or vice versa), the script removes the previous service type before installing the new one.
**Configure at install:** You can pass config when installing so the app and service are configured in one step. Use **`--set KEY=VALUE`** (e.g. `--set Application__ApplicationTitle=Tree DNS` or `--set ASPNETCORE_URLS=http://0.0.0.0:8080`) or **env vars** with prefix **`DNSMASQ_WEBUI_`** (e.g. `DNSMASQ_WEBUI_Application__ApplicationTitle=Tree DNS`). With `sudo`, env vars are not passed to the script by default—use `--set` or `sudo -E`. The script writes an env file used by the service (system: `/etc/default/dnsmasq-webui`; user: `install-dir/dnsmasq-webui.env`) and sets the service to listen on **port 8080** by default.
```bash
sudo ./scripts/install.sh --system --service --set Application__ApplicationTitle=Tree\ DNS --set Dnsmasq__MainConfigPath=/etc/dnsmasq.conf --set Dnsmasq__ReloadCommand="systemctl reload dnsmasq"
```
**Update to latest release:**
```bash
./scripts/install.sh --update
```
Reinstalls the latest release into the default user directory (~/.local/share/dnsmasq-webui). Existing env files (`/etc/default/dnsmasq-webui` or `install-dir/dnsmasq-webui.env`) are **not overwritten** on update unless you pass `--set` or `DNSMASQ_WEBUI_*` again.
**Uninstall:**
```bash
./scripts/install.sh --uninstall # Remove services and symlinks only (keeps install dir)
./scripts/install.sh --uninstall --purge # Also remove default install dir (~/.local/share/dnsmasq-webui)
sudo ./scripts/install.sh --uninstall --purge --system # Also remove /opt/dnsmasq-webui
./scripts/install.sh --uninstall --purge --dir /path/to/dir # Purge a specific install dir
```
**Custom directory or specific version:**
```bash
./scripts/install.sh --dir /opt/dnsmasq-webui
./scripts/install.sh --version v1.0.0
```
**If the binary fails to run** (e.g. `TypeLoadException` or glibc errors): use the install script with `--build-from-source` so the app is built for your machine. This requires the .NET SDK and a **git clone** (it does not work when installing via `curl ... | sh`). Clone the repo, then run `./scripts/install.sh --build-from-source`. The script checks that .NET is installed, detects your OS/arch, and builds (on Ubuntu it may use a distro-specific RID for a better match). Other distros use the portable RID and may still hit runtime issues; building from source on the target machine is the most reliable.
**Configuration:** Set at least `Dnsmasq__MainConfigPath` to your main dnsmasq config. See [Configuration](#configuration) for all options.
### Switching to a different release
Re-run the install script with the same `--dir` (or default) and the desired `--version`. The script overwrites that directory with the chosen release. To go back to latest, use `--update` or omit `--version`.
---
## Running with dnsmasq in Docker
If you run dnsmasq in a container and want the UI in the **same** container, you have two main options.
### Option A: Use this repo’s image (app + dnsmasq in one image)
Build the image from this repo’s **Dockerfile**. It builds the app and installs dnsmasq in one image. The **entrypoint** (`scripts/entrypoint.sh`) starts dnsmasq when `DNSMASQ_CONF` is set, then runs the app.
**Build and run:**
```bash
docker build -t dnsmasq-webui .
docker run -d -p 8080:8080 \
-e DNSMASQ_CONF=/data/dnsmasq.conf \
-e Dnsmasq__MainConfigPath=/data/dnsmasq.conf \
-e Dnsmasq__ReloadCommand="pkill -HUP -x dnsmasq" \
-v /path/on/host:/data \
--cap-add=NET_ADMIN \
dnsmasq-webui
```
Mount your dnsmasq config directory as `/data` (or adjust paths and env vars). The app and dnsmasq both use files under `/data`.
### Option B: Build your own image (self-contained binary from a release)
The self-contained publish **works in a container** (it’s a Linux binary). You can add it to your own dnsmasq image by copying a release zip and unzipping into the image (no need to run the install script inside the container).
**Example Dockerfile** (extend your dnsmasq base):
```dockerfile
FROM your-dnsmasq-image:tag
# Install unzip if not present
RUN apt-get update && apt-get install -y --no-install-recommends unzip curl ca-certificates && rm -rf /var/lib/apt/lists/*
# Download and extract the release for linux-x64 (or match your base image: linux-musl-x64 for Alpine, etc.)
ARG RID=linux-x64
ARG VERSION=v1.0.0
RUN curl -sSL "https://github.com/alexhopeoconnor/dnsmasq-webui/releases/download/${VERSION}/dnsmasq-webui-${RID}.zip" -o /tmp/app.zip \
&& unzip -o /tmp/app.zip -d /app && rm /tmp/app.zip
WORKDIR /app
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
# Start dnsmasq (if your image doesn’t), then the app. Set DNSMASQ_CONF and Dnsmasq__* as needed.
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
```
Set `Dnsmasq__MainConfigPath`, `Dnsmasq__ReloadCommand`, and other options via environment variables or an `appsettings.json` in `/app`. Mount your config dir and expose 8080. The image includes a **HEALTHCHECK** that calls `GET /healthz/ready`; orchestration (e.g. Kubernetes readiness probe, or Compose `depends_on: condition: service_healthy`) can use the same endpoint.
**Config summary for containers:**
| Env var | Example | Purpose |
|--------|--------|--------|
| `DNSMASQ_CONF` | `/data/dnsmasq.conf` | Path used by entrypoint to start dnsmasq (optional) |
| `Dnsmasq__MainConfigPath` | `/data/dnsmasq.conf` | Main dnsmasq config path (required) |
| `Dnsmasq__ReloadCommand` | `pkill -HUP -x dnsmasq` | Run after config changes |
| `Dnsmasq__StatusShowCommand` | `/app/dnsmasq-status.sh` | Optional status output (e.g. our script in test harness) |
| `Dnsmasq__MinimumVersion` | `2.91` | Minimum dnsmasq version (optional; default 2.91). Set `Dnsmasq__EnforceMinimumVersion=false` to allow older versions. |
| `ASPNETCORE_URLS` | `http://+:8080` | Port the app listens on |
**Readiness:** `GET http://localhost:8080/healthz/ready` returns JSON `{"status":"ok"}` when dnsmasq version meets the minimum and (if `StatusCommand` is set) dnsmasq is running. Use this for Kubernetes readiness probes, Docker HEALTHCHECK, or load balancer health checks.
See [Configuration](#configuration) for all options.
---
## Configuration
The app is configured via **appsettings.json**, **environment variables**, and **command-line arguments**. Later sources override earlier ones (e.g. CLI overrides env over appsettings).
**Required:** Set at least the main dnsmasq config path.
| Source | Example |
|--------|--------|
| **Environment** | `Dnsmasq__MainConfigPath=/etc/dnsmasq.conf` |
| **appsettings.json** | `"Dnsmasq": { "MainConfigPath": "/etc/dnsmasq.conf" }` |
| **CLI** | `--Dnsmasq:MainConfigPath=/etc/dnsmasq.conf` |
**Dnsmasq options** (use `Dnsmasq__` prefix for env, `Dnsmasq:` section in JSON, `--Dnsmasq:OptionName=value` for CLI):
| Option | Description | Example |
|--------|-------------|---------|
| `MainConfigPath` | Path to main dnsmasq config (required) | `/etc/dnsmasq.conf` |
| `ManagedFileName` | Filename of the managed config file the app writes | `zz-dnsmasq-webui.conf` |
| `ManagedHostsFileName` | Filename of the managed hosts file the app writes | `zz-dnsmasq-webui.hosts` |
| `SystemHostsPath` | Optional path to system hosts (read-only in UI) | `/etc/hosts` |
| `ReloadCommand` | Command run after config changes | `systemctl reload dnsmasq` or `pkill -HUP -x dnsmasq` |
| `StatusCommand` | Optional: check if dnsmasq is running | `pgrep -x dnsmasq` |
| `StatusShowCommand` | Optional: full status output (e.g. systemctl status) | `systemctl status dnsmasq --no-pager` |
| `LogsCommand` | Optional: recent logs (e.g. journalctl) | `journalctl -u dnsmasq -n 100 --no-pager` |
| `VersionCommand` | Command to probe dnsmasq version (used for minimum-version check and UI display) | `dnsmasq --version` |
| `MinimumVersion` | Minimum dnsmasq version required (e.g. 2.91). Some options need newer dnsmasq. | `2.91` |
| `EnforceMinimumVersion` | If true, app fails to start when version probe fails or version is below minimum. If false, only save and readiness checks enforce. | `true` |
**Application options** (use `Application__` prefix for env, `Application` section in JSON):
| Option | Description | Example |
|--------|-------------|---------|
| `ApplicationTitle` | Title shown in the sidebar brand and browser tab (default: "Local DNS") | `"ApplicationTitle": "My DNS"` or `Application__ApplicationTitle=My DNS` |
**Host / URLs:** `ASPNETCORE_URLS=http://0.0.0.0:8080` or `--urls=http://0.0.0.0:8080` to bind to a specific address and port.
Place `appsettings.json` in the same directory as the executable (or use the default project paths when running with `dotnet run`). You can override any option via environment variables using the `Dnsmasq__` prefix (e.g. `Dnsmasq__MainConfigPath=/path/to/dnsmasq.conf`). To use the repo test config:
```bash
Dnsmasq__MainConfigPath=/path/to/dnsmasq-webui/testdata/dnsmasq-test.conf dnsmasq-webui
```
---
## Scripts
All scripts live under `scripts/` and are intended for Linux (WSL may work but is untested).
### scripts/install.sh
**Purpose:** Download and install dnsmasq-webui from a GitHub release for the current OS/arch (RID). Supports install, **update** (reinstall latest to default dir), switch-release (re-run with same `--dir` and different `--version`), and **uninstall** (remove services and symlinks; use `--purge` to also remove the install directory). Default install is user-writable (~/.local/share/dnsmasq-webui) with an optional symlink so you can run `dnsmasq-webui` from the terminal; use `--system` for a system-wide install to /opt (requires sudo). Use `--service` to install a systemd unit (user service without sudo, or system service with `--system`); only supported on systemd-based systems. When installing a service, the script removes the other type first (e.g. switching from user to system service cleans up the user unit).
**Usage:** `./scripts/install.sh [OPTIONS]`
**Options:**
| Option | Description |
|--------|-------------|
| `--list` | List available releases (tag, name, published_at) and exit. |
| `--version TAG` | Install from release TAG (e.g. v1.0.0). Default: latest. |
| `--update` | Reinstall latest into the default user directory (~/.local/share/dnsmasq-webui). |
| `--dir DIR` | Install into DIR instead of default. |
| `--system` | Install to /opt/dnsmasq-webui and symlink /usr/local/bin/dnsmasq-webui. Requires root (run with sudo). |
| `--service` | Install a systemd unit so the app runs as a service. With `--system`: system unit (requires root, starts at boot). Without: user unit (no sudo, starts at login). Removes the other type first if present. Only on systemd-based systems. |
| `--set KEY=VALUE` | Set an env var for the app at install (e.g. `Application__ApplicationTitle=Tree DNS`, `ASPNETCORE_URLS=http://0.0.0.0:8080`). Multiple allowed. Written to the service env file or install-dir `dnsmasq-webui.env` for manual runs. With sudo, prefer `--set` (env is not passed by default). |
| Env `DNSMASQ_WEBUI_*` | Any env var starting with `DNSMASQ_WEBUI_` is passed through (e.g. `DNSMASQ_WEBUI_Application__ApplicationTitle=Tree DNS`). For system install with sudo use `--set` or `sudo -E`. |
| `--uninstall` | Remove systemd units and symlinks (user and, if root, system). Does not remove the install directory. |
| `--purge` | With `--uninstall` only: also remove the install directory. Use `--dir DIR` or `--system` to target a specific location. Errors if used without `--uninstall`. |
| `-h`, `-?`, `--help` | Show help. |
Invalid combinations (script errors with a clear message): `--purge` without `--uninstall`; `--uninstall` with `--version`, `--update`, or `--service`; `--list` with install/uninstall options.
**How to use the script**
1. **Run one command to get a runnable binary for your OS/arch** (no repo or build choice needed).
Script detects RID and default repo (or git origin), downloads the matching zip, extracts it, and creates a symlink when possible.
**Commands:** `curl -sSL https://raw.githubusercontent.com/alexhopeoconnor/dnsmasq-webui/master/scripts/install.sh | sh` or `./scripts/install.sh`
2. **Install into your home directory by default** (no root).
Default is `~/.local/share/dnsmasq-webui`; symlink in `~/.local/bin` if writable.
**Command:** `./scripts/install.sh`
3. **Install to /opt and have `dnsmasq-webui` in PATH for all users.**
Script requires root, then installs to `/opt/dnsmasq-webui` and symlinks `/usr/local/bin/dnsmasq-webui`.
**Command:** `sudo ./scripts/install.sh --system`
4. **Install a systemd unit that starts at boot** so the UI runs as a service.
Script checks root and systemd, installs and enables the system unit, and removes the invoking user's user unit so you do not have both.
**Command:** `sudo ./scripts/install.sh --system --service`
5. **Install a user systemd unit (no sudo)** that starts when you log in.
Script installs and enables the user unit and removes any existing user unit; if a system unit exists it tells you how to remove it.
**Command:** `./scripts/install.sh --service`
6. **Upgrade your existing install to the latest release** without changing where it is installed.
`--update` keeps your current target (default dir, `--dir`, or `--system`) and installs latest there.
**Commands:** `./scripts/install.sh --update` or `sudo ./scripts/install.sh --update --system` or `./scripts/install.sh --update --dir /path`
7. **Switch from user service to system service (or the other way)** without manually removing the old unit.
Installing one type removes the other for that scope (or tells you how to remove the system unit when installing user).
**Commands:** `sudo ./scripts/install.sh --system --service` (user to system) or `./scripts/install.sh --service` (system to user; run `sudo ./scripts/install.sh --uninstall` first if you had a system unit).
8. **Uninstall cleanly:** remove services and symlinks, and optionally delete the install directory.
`--uninstall` removes units and symlinks; `--purge` also removes one install dir, and you must say which (default, `--dir`, or `--system`).
**Commands:** `./scripts/install.sh --uninstall`, `./scripts/install.sh --uninstall --purge`, `sudo ./scripts/install.sh --uninstall --purge --system`, or `./scripts/install.sh --uninstall --purge --dir /path`
9. **List available releases** to choose which version to install.
Script lists release tags and dates; list-only mode cannot be combined with install/uninstall so you do not accidentally change the system.
**Command:** `./scripts/install.sh --list`
10. **Install a specific release tag** (e.g. an older or pinned version) instead of latest.
Script fetches that tag's release and the asset for your RID. If no asset matches your OS/arch it errors and prints the asset names so you can pick another tag or report a missing build.
**Command:** `./scripts/install.sh --version v1.0.0`
**Examples:**
```bash
# Install latest
./scripts/install.sh
# Update to latest release
./scripts/install.sh --update
# Install a specific version to default dir
./scripts/install.sh --version v1.0.0
# System-wide install (requires sudo)
sudo ./scripts/install.sh --system
# Install + systemd service (user service: no sudo; system service: starts at boot)
./scripts/install.sh --service
sudo ./scripts/install.sh --system --service
# Uninstall (services + symlinks only; add --purge to remove install dir)
./scripts/install.sh --uninstall
./scripts/install.sh --uninstall --purge
sudo ./scripts/install.sh --uninstall --purge --system
# Install to custom dir
./scripts/install.sh --dir /opt/dnsmasq-webui
# Switch to another release (overwrites that dir)
./scripts/install.sh --dir ~/.local/share/dnsmasq-webui --version v0.9.0
# List releases
./scripts/install.sh --list
```
**How the script works and avoids mistakes**
1. **Modes are exclusive**
The script does one of: list releases, uninstall, or install (including update/switch-release). It errors if you mix modes, e.g. `--uninstall` with `--update` or `--service`, or `--list` with install/uninstall options. `--purge` is only valid with `--uninstall`; using `--purge` alone errors.
2. **Privilege checks**
`--system` (install to /opt and system-wide symlink) and `--system --service` (system systemd unit) require root. The script checks `id -u` and exits with a clear message (“Run with sudo: sudo $0 --system”) instead of failing partway. Uninstall with `--purge --system` also requires root and is checked before removing anything.
3. **Service switching**
When you install a systemd unit, the script removes the *other* type first: installing a system service stops/disables and removes the invoking user’s user unit (via `SUDO_USER`), and installing a user service leaves any system unit in place but prints a note on how to remove it. You never end up with both user and system units for the same user by accident.
4. **Update and target dir**
`--update` means “reinstall latest.” If you also pass `--dir DIR` or `--system`, that target is used (e.g. `--update --dir /opt/foo` or `sudo ./install.sh --update --system`). So upgrade-in-place is predictable and you don’t overwrite a different install.
5. **Uninstall is explicit**
`--uninstall` removes only systemd units and symlinks unless you add `--purge`. With `--purge`, you must say *which* install to remove: default user dir (no extra flags), `--dir DIR`, or `--system` for /opt. That avoids accidentally deleting the wrong directory.
6. **Repo and release**
If no asset matches your OS/arch (RID), the script prints the available asset names so you can pick a different release or report a missing build.
7. **Dependencies**
The script requires `curl`, `jq`, and `unzip`. It checks for `jq` before calling the GitHub API and errors with an install hint. `--service` requires `systemctl` (systemd) and errors on non-systemd systems instead of writing a unit that won’t be used.
---
### scripts/publish-self-contained.sh
**Purpose:** Build a self-contained folder publish for Linux (no single-file). Used locally or in CI to produce the binaries that get zipped and attached to releases. Auto-detects RID when not specified.
**Usage:** `./scripts/publish-self-contained.sh [OPTIONS] [RID]`
**Options:**
| Option | Description |
|--------|-------------|
| `--trim` | Enable trimming (smaller output; can cause 404/routing issues with Blazor). |
| `--no-clean` | Skip clean before publish (faster; use only if same RID and options as last run). |
| `-h`, `-?`, `--help` | Show help. |
**RID:** Optional. If omitted, script picks one from `/etc/os-release` and `uname -m` (e.g. Ubuntu 24.04 → `ubuntu.24.04-x64`, Alpine → `linux-musl-x64`, else `linux-x64` / `linux-arm64` / `linux-arm`).
**Examples:**
```bash
# Publish for current machine (auto-detect RID)
./scripts/publish-self-contained.sh
# Publish for a specific RID
./scripts/publish-self-contained.sh ubuntu.24.04-x64
./scripts/publish-self-contained.sh linux-arm64
./scripts/publish-self-contained.sh linux-musl-x64
# Smaller build (not recommended for Blazor)
./scripts/publish-self-contained.sh --trim linux-x64
# Skip clean for a faster rebuild
./scripts/publish-self-contained.sh --no-clean ubuntu.24.04-x64
```
**Output:** `src/DnsmasqWebUI/bin/Release/net10.0//publish/`
---
### scripts/prepare-test-mount.sh
**Purpose:** Prepare the testdata mount and optionally start or stop the Docker test harness (app + dnsmasq + DHCP clients). Syncs a source dir (default: `testdata`) into a mount dir (default: `testdata-mount`), cleans up previous test data, then runs `docker compose -f docker-compose.test.yml up -d` (or only prepares the mount with `--prepare-only`). By default the script resolves and uses the **latest stable upstream dnsmasq version**; you can pin a specific version (or use the distro package) for compatibility testing.
**Usage:** `./scripts/prepare-test-mount.sh [OPTIONS] [--]`
**Options:**
| Option | Description |
|--------|-------------|
| `--source DIR` | Source to copy from (default: `testdata`). |
| `--mount DIR` | Target mount directory (default: `testdata-mount`). Compose uses `TESTDATA_MOUNT`; script exports it when you use `--mount`. |
| `--dnsmasq-version V` | Dnsmasq version for the harness image: `latest` (default), `distro`, or an exact upstream version like `2.91`. |
| `--clear` | Clear the mount dir completely before sync for a clean run. Default: preserve existing contents and sync over them. |
| `--prepare-only` | Only prepare the mount; do not run docker compose. |
| `--build` | Pass `--build` to docker compose (rebuild images). Default: use existing images. |
| `--no-cache-build` | Run `docker compose build --pull --no-cache` before start. Use to force a fresh image build and refresh the resolved latest dnsmasq version. |
| `--no-build` | Do not rebuild (default). |
| `--recreate` | Pass `--force-recreate` to docker compose. |
| `--stop` | Stop the harness: `docker compose down` (no prepare, no start). |
| `--tidy` | Stop the harness and clear the mount directory. |
| `-h`, `-?`, `--help` | Show help. |
**Examples:**
```bash
# Full run: clear mount, sync testdata, start containers (no rebuild)
./scripts/prepare-test-mount.sh
# Rebuild images then start
./scripts/prepare-test-mount.sh --build
# Force a fresh image build with no Docker build cache
./scripts/prepare-test-mount.sh --no-cache-build
# Rebuild with a specific dnsmasq version
./scripts/prepare-test-mount.sh --dnsmasq-version 2.91 --build
# Use the distro package instead of an upstream build
./scripts/prepare-test-mount.sh --dnsmasq-version distro --build
# Only prepare the mount; start manually later
./scripts/prepare-test-mount.sh --prepare-only
# Then use the version printed by the script, e.g.:
# TESTDATA_MOUNT=./testdata-mount DNSMASQ_VERSION= docker compose -f docker-compose.test.yml up -d
# Preserve mount contents, sync over it, start (default behavior)
./scripts/prepare-test-mount.sh
# Stop the harness
./scripts/prepare-test-mount.sh --stop
# Stop and clear the mount for a clean next run
./scripts/prepare-test-mount.sh --tidy
```
---
### scripts/dnsmasq-status.sh
**Purpose:** Simulates `systemctl status dnsmasq`-style output when run inside a container that has no systemd. Used by the test harness (and any similar setup) so the Dnsmasq page can show a status block. Uses `pgrep`/`ps` to show dnsmasq process info. No arguments.
**Usage:** Invoked by the app when `Dnsmasq__StatusShowCommand` points at this script (e.g. in `docker-compose.test.yml`).
---
### scripts/entrypoint.sh
**Purpose:** Container entrypoint when the app and dnsmasq run in the same container. If `DNSMASQ_CONF` is set, starts dnsmasq in the background with that config file; then exec’s the app (`dotnet DnsmasqWebUI.dll`). Used by this repo’s Dockerfile and by the test harness.
**Usage:** Set `DNSMASQ_CONF` to the path of the dnsmasq config file inside the container (e.g. `/data/dnsmasq-test.conf`). No CLI arguments.
---
### scripts/extract-option-help.sh
**Purpose:** Fetch the dnsmasq man page HTML and extract per-option help fragments into `src/DnsmasqWebUI/wwwroot/option-help/*.html`. The Effective Config UI loads these when the user clicks an option label (tooltip is from code; full help is from these files). Requires Docker (uses a temporary `python:3-slim` container). When adding new options to the UI, add the option key to the script’s `OPTION_KEYS` list (keep in sync with `EffectiveConfigSections` / `DnsmasqOptionTooltips`), then run this script so help is available for the new option (if it exists in the man page).
**Usage:** `./scripts/extract-option-help.sh [--url URL] [--output-dir DIR]`
| Option | Description |
|--------|-------------|
| `--url URL` | Man page URL (default: https://thekelleys.org.uk/dnsmasq/docs/dnsmasq-man.html). |
| `--output-dir DIR` | Output directory (default: `src/DnsmasqWebUI/wwwroot/option-help`). |
Run from the repo root. Writes one `.html` file per option that appears in the man page OPTIONS section; options in `OPTION_KEYS` but not in the man page are skipped.
---
## Project structure and codebase overview
**Top-level:**
| Path | Description |
|------|-------------|
| `.github/workflows/` | GitHub Actions (e.g. release workflow: build on tag push, attach assets). |
| `scripts/` | Install, publish, test harness, entrypoint, and helper scripts. |
| `src/DnsmasqWebUI/` | Main ASP.NET Core Blazor app. |
| `src/DnsmasqWebUI.Tests/` | Unit tests. |
| `testdata/` | Fixtures and config for the Docker test harness. |
| `Dockerfile` | Multi-stage build: app + dnsmasq in one image. |
| `docker-compose.test.yml` | Test harness: app + dnsmasq + DHCP clients. |
| `DnsmasqWebUI.sln` | Solution file. |
**Codebase overview:**
- **Entry and config:** `Program.cs` wires the host; configuration (e.g. `Dnsmasq__*`) lives in `Models/Config/` (`DnsmasqOptions`, `ApplicationOptions`, `DnsmasqOptionsValidator`). Options are validated at startup.
- **Infrastructure:** Under `Infrastructure/`: **Client/** (HTTP API clients and `Abstractions/`), **Serialization/Parsers/** (Dnsmasq version/compile, DnsmasqConfig conf-line and include parsers, EffectiveConfig syntax helpers), **Services/** (caches, config/hosts/leases services, reload, hosted services; EffectiveConfig has Metadata, Rendering, Editing, Validation, Readonly), **Helpers/Config/** (file encoding for config/hosts), **Helpers/Http/** (HTTP helpers). A **managed** config file and optional managed hosts file are written by the app; the main config must include the managed file (e.g. via a final `conf-file=` line). Caches (`ConfigSetCache`, `HostsCache`) keep in-memory snapshots and refresh on file changes or staleness.
- **Models:** `Models/Config/` (options, validators, `DnsmasqConfLine`); `Models/Contracts/` (snapshots and DTOs such as `ConfigSetSnapshot`, `HostsSnapshot`, `ManagedConfigContent`, `ProcessRunResult`, `ReloadResult`); `Models/Dnsmasq/` (status, `SaveWithReloadResult`, and `EffectiveConfig/` for effective config and sources); `Models/Client/`, `Models/Dhcp/`, `Models/Hosts/` (UI and API DTOs).
- **API and UI:** `Controllers/` expose API endpoints; `Components/` contains Blazor Server components for config editor, hosts, DHCP, Dnsmasq status, and app settings. Static assets in `wwwroot/`.
- **Tests:** `DnsmasqWebUI.Tests` contains unit tests organised to mirror the main project: `Serialization/Parsers/` (Dnsmasq, DnsmasqConfig), `Services/Dnsmasq/Config/`, `Services/EffectiveConfig/`, `Models/`, and `Helpers/` (TestDataHelper). Run with `dotnet test`.
---
## Development and test harness
**Prerequisites:** .NET 9 SDK. Docker (and Docker Compose) for the test harness.
**Build and run locally:**
```bash
cd src/DnsmasqWebUI
dotnet run
```
Use `appsettings.json` or launch settings to set `Dnsmasq__MainConfigPath` and other options for your environment.
**Test harness:** Run the app and dnsmasq (plus DHCP clients) in Docker against a mounted testdata directory so you can develop and test without touching system dnsmasq.
1. From the repo root, run:
```bash
./scripts/prepare-test-mount.sh
```
This clears the mount dir (by default `testdata-mount`), syncs `testdata/` into it, and starts the stack with `docker-compose.test.yml`. Use `--prepare-only` to only prepare the mount, then start compose manually; use `--build` after changing the app or Dockerfile.
2. Open the UI (e.g. http://localhost:8080). The app and dnsmasq use the mounted config; you can edit config/hosts in the UI and trigger reload.
3. Stop: `./scripts/prepare-test-mount.sh --stop`. Clean mount for next run: `./scripts/prepare-test-mount.sh --tidy`.
**Unit tests:**
```bash
dotnet test
```
Run from the repo root or the solution path. Tests do not require Docker.
---
## License
This project is licensed under the **MIT License**. See [LICENSE](LICENSE) for the full text.
**dnsmasq** (the DNS/DHCP server this UI manages) is licensed under the **GNU GPL v2 or v3** (see [dnsmasq](https://thekelleys.org.uk/dnsmasq/doc.html)); this project is independent and does not include dnsmasq code.
To add or change the repository license (e.g. with GitHub CLI): `gh repo license view MIT > LICENSE` (or use the GitHub web UI: Add file → Create new file → Choose a license template).
---
## Disclaimer
This codebase was developed with the help of AI-assisted tooling (including LLM-based development tools). No warranty is provided. See the [LICENSE](LICENSE) for terms.