Effective config: do-0x20-encode revert fix, validation UI, custom displays, option help

- Fix do-0x20-encode: clearing pending when toggling back to original (Disabled/Default)
  - Do0x20EncodeDisplay: uncheck uses pending.OldValue when present
  - EffectiveConfigFieldDisplay: use pending.OldValue as _valueAtActivation when re-entering with pending
- Effective config: validation errors/warnings in toolbar and save modal, revert from modal
- Custom displays: Do0x20EncodeDisplay (tri-state), KeyOnlyOrValueDisplay
- ExplicitToggleState, config/apply support for do-0x20-encode, option help and tests
This commit is contained in:
2026-03-08 12:52:46 +10:00
parent 6cf4c07695
commit a03e93bcd3
89 changed files with 2229 additions and 67 deletions
+30 -3
View File
@@ -2,7 +2,15 @@
# Both run as root; file permissions work because config dirs are in the container. # Both run as root; file permissions work because config dirs are in the container.
# For dnsmasq on the host, use the self-contained publish (scripts/publish-self-contained.sh) # For dnsmasq on the host, use the self-contained publish (scripts/publish-self-contained.sh)
# or run the app on the host; see DnsmasqOptions XML doc for permissions and ReloadCommand scope. # or run the app on the host; see DnsmasqOptions XML doc for permissions and ReloadCommand scope.
#
# Dnsmasq install mode for the test harness:
# - DNSMASQ_VERSION=latest (default): build the latest stable upstream dnsmasq release.
# - DNSMASQ_VERSION=<version> : build that exact upstream release (e.g. 2.91).
# - DNSMASQ_VERSION=distro : use the distro package from apt.
#
# This keeps day-to-day harness use close to upstream while still allowing pinned-version testing.
ARG DNSMASQ_VERSION=
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app WORKDIR /app
EXPOSE 8080 EXPOSE 8080
@@ -20,11 +28,30 @@ FROM build AS publish
RUN dotnet publish "DnsmasqWebUI.csproj" -c Release -o /app/publish /p:UseAppHost=false RUN dotnet publish "DnsmasqWebUI.csproj" -c Release -o /app/publish /p:UseAppHost=false
FROM base AS final FROM base AS final
ARG DNSMASQ_VERSION=latest
WORKDIR /app WORKDIR /app
ENV ASPNETCORE_URLS=http://+:8080 ENV ASPNETCORE_URLS=http://+:8080
# Slow, rarely-changing steps first (stay cached when only .NET code changes) # Install dnsmasq either from the distro package or by building an upstream release.
RUN apt-get update && apt-get install -y --no-install-recommends dnsmasq procps \ RUN if [ "$DNSMASQ_VERSION" = "distro" ]; then \
&& rm -rf /var/lib/apt/lists/* apt-get update && apt-get install -y --no-install-recommends dnsmasq procps \
&& rm -rf /var/lib/apt/lists/*; \
else \
apt-get update && apt-get install -y --no-install-recommends procps curl build-essential xz-utils ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
&& if [ "$DNSMASQ_VERSION" = "latest" ]; then \
DNSMASQ_VERSION="$(curl -fsSL https://thekelleys.org.uk/dnsmasq/ | grep -o 'dnsmasq-[0-9][0-9.]*\.tar\.xz' | sed 's/^dnsmasq-//; s/\.tar\.xz$//' | sort -V | tail -n1)"; \
fi \
&& test -n "$DNSMASQ_VERSION" \
&& echo "Building dnsmasq $DNSMASQ_VERSION from upstream source" \
&& curl -fsSL "https://thekelleys.org.uk/dnsmasq/dnsmasq-${DNSMASQ_VERSION}.tar.xz" -o /tmp/dnsmasq.tar.xz \
&& tar -C /tmp -xJf /tmp/dnsmasq.tar.xz \
&& make -C /tmp/dnsmasq-${DNSMASQ_VERSION} -j"$(nproc)" \
&& make -C /tmp/dnsmasq-${DNSMASQ_VERSION} install PREFIX=/usr \
&& rm -rf /tmp/dnsmasq-${DNSMASQ_VERSION} /tmp/dnsmasq.tar.xz \
&& apt-get purge -y build-essential curl xz-utils \
&& apt-get autoremove -y --purge \
&& rm -rf /var/lib/apt/lists/*; \
fi
COPY scripts/entrypoint.sh scripts/dnsmasq-status.sh . COPY scripts/entrypoint.sh scripts/dnsmasq-status.sh .
RUN chmod +x entrypoint.sh dnsmasq-status.sh RUN chmod +x entrypoint.sh dnsmasq-status.sh
# .NET publish last - only this layer invalidates when code changes # .NET publish last - only this layer invalidates when code changes
+32 -5
View File
@@ -391,7 +391,7 @@ sudo ./scripts/install.sh --uninstall --purge --system
### scripts/prepare-test-mount.sh ### 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`). **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] [--]` **Usage:** `./scripts/prepare-test-mount.sh [OPTIONS] [--]`
@@ -401,9 +401,11 @@ sudo ./scripts/install.sh --uninstall --purge --system
|--------|-------------| |--------|-------------|
| `--source DIR` | Source to copy from (default: `testdata`). | | `--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`. | | `--mount DIR` | Target mount directory (default: `testdata-mount`). Compose uses `TESTDATA_MOUNT`; script exports it when you use `--mount`. |
| `--no-clear` | Do not clear mount dir; only sync over existing contents (e.g. preserve leases between runs). | | `--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. | | `--prepare-only` | Only prepare the mount; do not run docker compose. |
| `--build` | Pass `--build` to docker compose (rebuild images). Default: use existing images. | | `--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). | | `--no-build` | Do not rebuild (default). |
| `--recreate` | Pass `--force-recreate` to docker compose. | | `--recreate` | Pass `--force-recreate` to docker compose. |
| `--stop` | Stop the harness: `docker compose down` (no prepare, no start). | | `--stop` | Stop the harness: `docker compose down` (no prepare, no start). |
@@ -419,12 +421,22 @@ sudo ./scripts/install.sh --uninstall --purge --system
# Rebuild images then start # Rebuild images then start
./scripts/prepare-test-mount.sh --build ./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 # Only prepare the mount; start manually later
./scripts/prepare-test-mount.sh --prepare-only ./scripts/prepare-test-mount.sh --prepare-only
# Then: TESTDATA_MOUNT=./testdata-mount docker compose -f docker-compose.test.yml up -d # Then use the version printed by the script, e.g.:
# TESTDATA_MOUNT=./testdata-mount DNSMASQ_VERSION=<resolved-version> docker compose -f docker-compose.test.yml up -d
# Preserve mount contents, sync over it, start # Preserve mount contents, sync over it, start (default behavior)
./scripts/prepare-test-mount.sh --no-clear ./scripts/prepare-test-mount.sh
# Stop the harness # Stop the harness
./scripts/prepare-test-mount.sh --stop ./scripts/prepare-test-mount.sh --stop
@@ -449,6 +461,21 @@ sudo ./scripts/install.sh --uninstall --purge --system
**Usage:** Set `DNSMASQ_CONF` to the path of the dnsmasq config file inside the container (e.g. `/data/dnsmasq-test.conf`). No CLI arguments. **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.
<!-- SUGGESTED SCREENSHOT: TERMINAL WITH PREPARE-TEST-MOUNT.SH USAGE OR DOCKER COMPOSE UP OUTPUT --> <!-- SUGGESTED SCREENSHOT: TERMINAL WITH PREPARE-TEST-MOUNT.SH USAGE OR DOCKER COMPOSE UP OUTPUT -->
--- ---
+18 -6
View File
@@ -17,15 +17,18 @@ From the repo root:
This will: This will:
1. Clear `testdata-mount/` (unless you pass `--no-clear`) 1. Sync `testdata/` to `testdata-mount/` (preserving existing contents unless you pass `--clear`)
2. Sync `testdata/` → `testdata-mount/` 2. Remove any `*dnsmasq-webui*.conf` in the mount so dnsmasq starts clean
3. Remove any `*dnsmasq-webui*.conf` in the mount so dnsmasq starts clean 3. Resolve the requested dnsmasq version (`latest` by default) to a concrete value when needed
4. Run `docker compose -f docker-compose.test.yml up -d --build` 4. Run `docker compose -f docker-compose.test.yml up -d --build`
- **Quick restart (no image rebuild):** `./scripts/prepare-test-mount.sh --no-build` - **Quick restart (no image rebuild):** `./scripts/prepare-test-mount.sh --no-build`
- **Force a fresh image build:** `./scripts/prepare-test-mount.sh --no-cache-build`
- **Pin dnsmasq version:** `./scripts/prepare-test-mount.sh --dnsmasq-version 2.91 --build`
- **Use distro package:** `./scripts/prepare-test-mount.sh --dnsmasq-version distro --build`
- **Prepare mount only (no start):** `./scripts/prepare-test-mount.sh --prepare-only` - **Prepare mount only (no start):** `./scripts/prepare-test-mount.sh --prepare-only`
Then start manually: `TESTDATA_MOUNT=./testdata-mount docker compose -f docker-compose.test.yml up -d [--build]` Then start manually with the version printed by the script: `TESTDATA_MOUNT=./testdata-mount DNSMASQ_VERSION=<resolved-version> docker compose -f docker-compose.test.yml up -d [--build]`
- **Preserve mount (e.g. keep leases):** `./scripts/prepare-test-mount.sh --no-clear --no-build` - **Preserve mount (e.g. keep leases):** `./scripts/prepare-test-mount.sh --no-build` (default mount behavior)
App is at **http://localhost:8080**. Main config path in the container is `/data/dnsmasq-test.conf`; managed file is `zz-dnsmasq-webui.conf` in the same directory. App is at **http://localhost:8080**. Main config path in the container is `/data/dnsmasq-test.conf`; managed file is `zz-dnsmasq-webui.conf` in the same directory.
@@ -49,6 +52,8 @@ Stops the harness and deletes the contents of `testdata-mount/` so the next run
- `--source DIR` — Source to sync from (default: `testdata`) - `--source DIR` — Source to sync from (default: `testdata`)
- `--mount DIR` — Mount directory (default: `testdata-mount`). The script exports `TESTDATA_MOUNT=./DIR` when you use `--mount`, so compose uses it. - `--mount DIR` — Mount directory (default: `testdata-mount`). The script exports `TESTDATA_MOUNT=./DIR` when you use `--mount`, so compose uses it.
- `--dnsmasq-version V` — Dnsmasq version for the harness image: `latest` (default), `distro`, or an exact upstream version like `2.91`.
- `--no-cache-build` — Force a fresh image build with `docker compose build --pull --no-cache` before start.
Example: sync from a custom dir and start: Example: sync from a custom dir and start:
@@ -76,5 +81,12 @@ Build only: `dotnet build`
- **testdata/** — Source for the harness mount. Synced to `testdata-mount/` by `prepare-test-mount.sh`. Unit tests read from the copy in the test output dir. See **testdata/README** for the full layout. - **testdata/** — Source for the harness mount. Synced to `testdata-mount/` by `prepare-test-mount.sh`. Unit tests read from the copy in the test output dir. See **testdata/README** for the full layout.
- **testdata/dnsmasq-test.conf** — Harness main config (`/data/dnsmasq-test.conf`). Richer config: multiple `server=`, `addn-hosts=`, `address=`, `listen-address=`, `conf-dir=/data/dnsmasq.d`. - **testdata/dnsmasq-test.conf** — Harness main config (`/data/dnsmasq-test.conf`). Richer config: multiple `server=`, `addn-hosts=`, `address=`, `listen-address=`, `conf-dir=/data/dnsmasq.d`.
- **testdata/dnsmasq.d/** — Included configs (01-other.conf, 02-servers.conf, dhcp.conf). App creates `zz-dnsmasq-webui.conf` here when running. - **testdata/dnsmasq.d/** — Included configs (01-other.conf, 02-servers.conf, 03-more.conf, dhcp.conf). App creates `zz-dnsmasq-webui.conf` here when running.
- **testdata/hosts**, **testdata/hosts.extra** — Addn-hosts files so effective config has multiple addn-hosts; UI can show source per path. - **testdata/hosts**, **testdata/hosts.extra** — Addn-hosts files so effective config has multiple addn-hosts; UI can show source per path.
## When adding new effective-config options
When adding a new dnsmasq option to the Effective Config UI (new key in `DnsmasqConfKeys`, section, kind map, parser/write behavior, tooltip, etc.):
1. **Option help:** Add the option key (lowercase, e.g. `no-round-robin`) to the `OPTION_KEYS` list in `scripts/extract-option-help.sh` (keep in sync with `EffectiveConfigSections`). Then run `./scripts/extract-option-help.sh` so `wwwroot/option-help/<key>.html` is generated if the option exists in the dnsmasq man page.
2. **Testdata (optional):** To show the option in the UI when using the harness or testdata, add a line to e.g. `testdata/dnsmasq.d/03-more.conf` and update `testdata/README` if needed.
+2
View File
@@ -14,6 +14,8 @@ services:
app: app:
build: build:
context: . context: .
args:
DNSMASQ_VERSION: ${DNSMASQ_VERSION:-latest}
ports: ports:
- "8080:8080" - "8080:8080"
cap_add: cap_add:
+51
View File
@@ -171,6 +171,57 @@ OPTION_KEYS = [
"keep-in-foreground", "keep-in-foreground",
"no-daemon", "no-daemon",
"conntrack", "conntrack",
"connmark-allowlist-enable",
"no-round-robin",
"dns-forward-max",
"connmark-allowlist",
"do-0x20-encode",
"caa-record",
"dns-rr",
"synth-domain",
"auth-zone",
"auth-soa",
"auth-sec-servers",
"auth-peer",
"leasequery",
"dhcp-generate-names",
"dhcp-broadcast",
"dhcp-sequential-ip",
"dhcp-ignore-clid",
"bootp-dynamic",
"no-ping",
"script-arp",
"script-on-renewal",
"dhcp-no-override",
"dhcp-alternate-port",
"dhcp-duid",
"dhcp-luascript",
"dhcp-scriptuser",
"dhcp-relay",
"dhcp-circuitid",
"dhcp-remoteid",
"dhcp-subscrid",
"dhcp-proxy",
"tag-if",
"bridge-interface",
"shared-network",
"dhcp-pxe-vendor",
"dhcp-option-pxe",
"dnssec-no-timecheck",
"dnssec-debug",
"add-cpe-id",
"dnssec-timestamp",
"dnssec-limits",
"dumpfile",
"dumpmask",
"use-stale-cache",
"add-mac",
"add-subnet",
"umbrella",
"quiet-dhcp",
"quiet-dhcp6",
"quiet-ra",
"quiet-tftp",
] ]
long_opt_re = re.compile(r"--([a-z][a-z0-9-]*)(?:=[^,\s]*)?", re.IGNORECASE) long_opt_re = re.compile(r"--([a-z][a-z0-9-]*)(?:=[^,\s]*)?", re.IGNORECASE)
+89 -3
View File
@@ -15,8 +15,10 @@ COMPOSE_FILE="docker-compose.test.yml"
SOURCE_DIR="" SOURCE_DIR=""
MOUNT_DIR="" MOUNT_DIR=""
DNSMASQ_VERSION=""
PREPARE_ONLY=false PREPARE_ONLY=false
BUILD=false BUILD=false
NO_CACHE_BUILD=false
RECREATE=false RECREATE=false
CLEAR=false CLEAR=false
STOP=false STOP=false
@@ -38,6 +40,9 @@ usage() {
echo " --source DIR Source to copy from (default: testdata)" echo " --source DIR Source to copy from (default: testdata)"
echo " --mount DIR Target mount directory (default: testdata-mount)" echo " --mount DIR Target mount directory (default: testdata-mount)"
echo " Compose uses TESTDATA_MOUNT; script exports it if you use --mount." echo " Compose uses TESTDATA_MOUNT; script exports it if you use --mount."
echo " --dnsmasq-version V Dnsmasq version for the harness image:"
echo " latest (default), distro, or an exact upstream version like 2.91."
echo " Non-default values require --build or --no-cache-build; script fails with --no-build."
echo "" echo ""
echo "Mount behaviour:" echo "Mount behaviour:"
echo " (default) Preserve mount dir; sync source over existing contents." echo " (default) Preserve mount dir; sync source over existing contents."
@@ -50,7 +55,10 @@ usage() {
echo " Use to inspect or edit the mount before starting containers." echo " Use to inspect or edit the mount before starting containers."
echo " --build Pass --build to docker compose (rebuild images before starting)." echo " --build Pass --build to docker compose (rebuild images before starting)."
echo " Use after changing the app or Dockerfile. Default: use existing images." echo " Use after changing the app or Dockerfile. Default: use existing images."
echo " --no-cache-build Rebuild images with --pull --no-cache before starting."
echo " Use to force a fresh image build and refresh 'latest' dnsmasq."
echo " --no-build Do not rebuild (default). Use existing images for a quick restart." echo " --no-build Do not rebuild (default). Use existing images for a quick restart."
echo " With --no-build, 'latest' is not resolved (no network)."
echo " --recreate Pass --force-recreate to docker compose (recreate containers)." echo " --recreate Pass --force-recreate to docker compose (recreate containers)."
echo " Use to ensure fresh container state and mounts." echo " Use to ensure fresh container state and mounts."
echo "" echo ""
@@ -72,6 +80,9 @@ usage() {
echo " $0 --recreate" echo " $0 --recreate"
echo " Sync, then up -d --force-recreate (fresh containers)." echo " Sync, then up -d --force-recreate (fresh containers)."
echo "" echo ""
echo " $0 --no-cache-build"
echo " Force a fresh image build (no Docker build cache), then start containers."
echo ""
echo " $0 --clear" echo " $0 --clear"
echo " Clear mount, sync testdata, start (clean run, no rebuild)." echo " Clear mount, sync testdata, start (clean run, no rebuild)."
echo "" echo ""
@@ -91,6 +102,35 @@ usage() {
echo "" echo ""
echo " $0 --minimal-conf" echo " $0 --minimal-conf"
echo " Use dnsmasq-test-minimal.conf (single file, few options) and start." echo " Use dnsmasq-test-minimal.conf (single file, few options) and start."
echo ""
echo " $0 --dnsmasq-version 2.91 --build"
echo " Rebuild the harness image with dnsmasq 2.91."
}
fetch_url() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL "$1"
return $?
fi
if command -v wget >/dev/null 2>&1; then
wget -qO- "$1"
return $?
fi
echo "Error: need curl or wget to resolve the latest upstream dnsmasq version." >&2
return 1
}
resolve_dnsmasq_version() {
if [ "$DNSMASQ_VERSION" != "latest" ]; then
return 0
fi
RESOLVED_VERSION="$(fetch_url "https://thekelleys.org.uk/dnsmasq/" | sed -n 's/.*LATEST_IS_\([0-9][0-9.]*\).*/\1/p' | head -n1)"
if [ -z "$RESOLVED_VERSION" ]; then
echo "Error: could not resolve the latest upstream dnsmasq version." >&2
exit 1
fi
DNSMASQ_VERSION="$RESOLVED_VERSION"
} }
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
@@ -111,6 +151,12 @@ while [ $# -gt 0 ]; do
MOUNT_DIR="$1" MOUNT_DIR="$1"
shift shift
;; ;;
--dnsmasq-version)
shift
[ $# -gt 0 ] || { echo "Error: --dnsmasq-version requires VERSION" >&2; exit 1; }
DNSMASQ_VERSION="$1"
shift
;;
--clear) --clear)
CLEAR=true CLEAR=true
shift shift
@@ -123,8 +169,14 @@ while [ $# -gt 0 ]; do
BUILD=true BUILD=true
shift shift
;; ;;
--no-cache-build)
NO_CACHE_BUILD=true
BUILD=true
shift
;;
--no-build) --no-build)
BUILD=false BUILD=false
NO_CACHE_BUILD=false
shift shift
;; ;;
--recreate) --recreate)
@@ -170,6 +222,7 @@ cd "$REPO_ROOT"
: "${SOURCE_DIR:=testdata}" : "${SOURCE_DIR:=testdata}"
: "${MOUNT_DIR:=testdata-mount}" : "${MOUNT_DIR:=testdata-mount}"
: "${DNSMASQ_VERSION:=latest}"
# Stop and/or tidy: no prepare, no start # Stop and/or tidy: no prepare, no start
if [ "$STOP" = true ] || [ "$TIDY" = true ]; then if [ "$STOP" = true ] || [ "$TIDY" = true ]; then
@@ -192,6 +245,17 @@ if [ ! -d "$SOURCE_DIR" ]; then
exit 1 exit 1
fi fi
# Resolve "latest" only when we will build; avoid network when doing a no-build run.
WILL_BUILD=false
[ "$BUILD" = true ] || [ "$NO_CACHE_BUILD" = true ] && WILL_BUILD=true
if [ "$WILL_BUILD" = false ] && [ "$DNSMASQ_VERSION" != "latest" ] && [ "$DNSMASQ_VERSION" != "distro" ]; then
echo "Error: --dnsmasq-version $DNSMASQ_VERSION has no effect without --build or --no-cache-build. Add --build or omit --dnsmasq-version for a no-build run." >&2
exit 1
fi
if [ "$WILL_BUILD" = true ]; then
resolve_dnsmasq_version
fi
# Take the harness down so all containers (including one-shot DHCP clients) are recreated on up. # Take the harness down so all containers (including one-shot DHCP clients) are recreated on up.
echo "Stopping test harness: docker compose -f $COMPOSE_FILE down" echo "Stopping test harness: docker compose -f $COMPOSE_FILE down"
docker compose -f "$COMPOSE_FILE" down docker compose -f "$COMPOSE_FILE" down
@@ -220,9 +284,14 @@ if [ "$PREPARE_ONLY" = true ]; then
/*) MOUNT_EXPORT="$MOUNT_DIR" ;; /*) MOUNT_EXPORT="$MOUNT_DIR" ;;
*) MOUNT_EXPORT="./$MOUNT_DIR" ;; *) MOUNT_EXPORT="./$MOUNT_DIR" ;;
esac esac
START_CMD="TESTDATA_MOUNT=$MOUNT_EXPORT docker compose -f $COMPOSE_FILE up -d [--build]" START_PREFIX="TESTDATA_MOUNT=$MOUNT_EXPORT DNSMASQ_VERSION=$DNSMASQ_VERSION"
if [ "$MINIMAL_CONF" = true ]; then if [ "$MINIMAL_CONF" = true ]; then
START_CMD="TESTDATA_MOUNT=$MOUNT_EXPORT TEST_DNSMASQ_CONF=/data/dnsmasq-test-minimal.conf docker compose -f $COMPOSE_FILE up -d [--build]" START_PREFIX="$START_PREFIX TEST_DNSMASQ_CONF=/data/dnsmasq-test-minimal.conf"
fi
if [ "$NO_CACHE_BUILD" = true ]; then
START_CMD="$START_PREFIX docker compose -f $COMPOSE_FILE build --pull --no-cache && $START_PREFIX docker compose -f $COMPOSE_FILE up -d"
else
START_CMD="$START_PREFIX docker compose -f $COMPOSE_FILE up -d [--build]"
fi fi
echo "To start the harness: $START_CMD" echo "To start the harness: $START_CMD"
exit 0 exit 0
@@ -234,13 +303,30 @@ case "$MOUNT_DIR" in
*) export TESTDATA_MOUNT="./$MOUNT_DIR" ;; *) export TESTDATA_MOUNT="./$MOUNT_DIR" ;;
esac esac
export DNSMASQ_VERSION
case "$DNSMASQ_VERSION" in
distro)
echo "Using distro-packaged dnsmasq for the harness image."
;;
*)
echo "Using dnsmasq $DNSMASQ_VERSION for the harness image."
;;
esac
if [ "$MINIMAL_CONF" = true ]; then if [ "$MINIMAL_CONF" = true ]; then
export TEST_DNSMASQ_CONF="/data/dnsmasq-test-minimal.conf" export TEST_DNSMASQ_CONF="/data/dnsmasq-test-minimal.conf"
echo "Using minimal config: $TEST_DNSMASQ_CONF" echo "Using minimal config: $TEST_DNSMASQ_CONF"
fi fi
if [ "$NO_CACHE_BUILD" = true ]; then
BUILD_CMD="docker compose -f $COMPOSE_FILE build --pull --no-cache"
echo "Running: $BUILD_CMD"
$BUILD_CMD
fi
COMPOSE_CMD="docker compose -f $COMPOSE_FILE up -d" COMPOSE_CMD="docker compose -f $COMPOSE_FILE up -d"
if [ "$BUILD" = true ]; then if [ "$BUILD" = true ] && [ "$NO_CACHE_BUILD" != true ]; then
COMPOSE_CMD="$COMPOSE_CMD --build" COMPOSE_CMD="$COMPOSE_CMD --build"
fi fi
if [ "$RECREATE" = true ]; then if [ "$RECREATE" = true ]; then
@@ -79,6 +79,114 @@ public class DnsmasqConfIncludeParserNewOptionsTests
WriteFlagAndAssertTrue(DnsmasqConfKeys.ProxyDnssec); WriteFlagAndAssertTrue(DnsmasqConfKeys.ProxyDnssec);
} }
[Fact]
public void GetFlagFromConfigFiles_ConnmarkAllowlistEnable_WhenPresent_ReturnsTrue()
{
WriteFlagAndAssertTrue(DnsmasqConfKeys.ConnmarkAllowlistEnable);
}
[Fact]
public void GetFlagFromConfigFiles_NoRoundRobin_WhenPresent_ReturnsTrue()
{
WriteFlagAndAssertTrue(DnsmasqConfKeys.NoRoundRobin);
}
[Fact]
public void GetFlagFromConfigFiles_DnssecNoTimecheck_WhenPresent_ReturnsTrue()
{
WriteFlagAndAssertTrue(DnsmasqConfKeys.DnssecNoTimecheck);
}
[Fact]
public void GetFlagFromConfigFiles_DnssecDebug_WhenPresent_ReturnsTrue()
{
WriteFlagAndAssertTrue(DnsmasqConfKeys.DnssecDebug);
}
[Fact]
public void GetFlagFromConfigFiles_Leasequery_WhenPresent_ReturnsTrue()
{
WriteFlagAndAssertTrue(DnsmasqConfKeys.Leasequery);
}
[Fact]
public void GetFlagFromConfigFiles_QuietDhcp_WhenPresent_ReturnsTrue()
{
WriteFlagAndAssertTrue(DnsmasqConfKeys.QuietDhcp);
}
[Fact]
public void GetFlagFromConfigFiles_QuietTftp_WhenPresent_ReturnsTrue()
{
WriteFlagAndAssertTrue(DnsmasqConfKeys.QuietTftp);
}
[Fact]
public void GetFlagFromConfigFiles_DhcpGenerateNames_WhenPresent_ReturnsTrue()
{
WriteFlagAndAssertTrue(DnsmasqConfKeys.DhcpGenerateNames);
}
[Fact]
public void GetFlagFromConfigFiles_DhcpBroadcast_WhenPresent_ReturnsTrue()
{
WriteFlagAndAssertTrue(DnsmasqConfKeys.DhcpBroadcast);
}
[Fact]
public void GetFlagFromConfigFiles_DhcpSequentialIp_WhenPresent_ReturnsTrue()
{
WriteFlagAndAssertTrue(DnsmasqConfKeys.DhcpSequentialIp);
}
[Fact]
public void GetFlagFromConfigFiles_DhcpIgnoreClid_WhenPresent_ReturnsTrue()
{
WriteFlagAndAssertTrue(DnsmasqConfKeys.DhcpIgnoreClid);
}
[Fact]
public void GetFlagFromConfigFiles_BootpDynamic_WhenPresent_ReturnsTrue()
{
WriteFlagAndAssertTrue(DnsmasqConfKeys.BootpDynamic);
}
[Fact]
public void GetFlagFromConfigFiles_NoPing_WhenPresent_ReturnsTrue()
{
WriteFlagAndAssertTrue(DnsmasqConfKeys.NoPing);
}
[Fact]
public void GetFlagFromConfigFiles_ScriptArp_WhenPresent_ReturnsTrue()
{
WriteFlagAndAssertTrue(DnsmasqConfKeys.ScriptArp);
}
[Fact]
public void GetFlagFromConfigFiles_ScriptOnRenewal_WhenPresent_ReturnsTrue()
{
WriteFlagAndAssertTrue(DnsmasqConfKeys.ScriptOnRenewal);
}
[Fact]
public void GetFlagFromConfigFiles_DhcpNoOverride_WhenPresent_ReturnsTrue()
{
WriteFlagAndAssertTrue(DnsmasqConfKeys.DhcpNoOverride);
}
[Fact]
public void GetFlagFromConfigFiles_QuietDhcp6_WhenPresent_ReturnsTrue()
{
WriteFlagAndAssertTrue(DnsmasqConfKeys.QuietDhcp6);
}
[Fact]
public void GetFlagFromConfigFiles_QuietRa_WhenPresent_ReturnsTrue()
{
WriteFlagAndAssertTrue(DnsmasqConfKeys.QuietRa);
}
// --- Multi-value (6): key=value line(s) → GetMultiValue returns value(s) --- // --- Multi-value (6): key=value line(s) → GetMultiValue returns value(s) ---
[Fact] [Fact]
@@ -258,16 +366,27 @@ public class DnsmasqConfIncludeParserNewOptionsTests
} }
[Fact] [Fact]
public void GetLastValueFromConfigFiles_Conntrack_WhenSet_ReturnsValue() public void GetFlagFromConfigFiles_Conntrack_WhenPresent_ReturnsTrue()
{ {
var dir = CreateTempDir(); var dir = CreateTempDir();
var conf = Path.Combine(dir, "dnsmasq.conf"); var conf = Path.Combine(dir, "dnsmasq.conf");
var value = "42";
try try
{ {
File.WriteAllText(conf, $"conntrack={value}\n"); File.WriteAllText(conf, "conntrack\n");
var (result, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(new[] { conf }, DnsmasqConfKeys.Conntrack); Assert.True(DnsmasqConfIncludeParser.GetFlagFromConfigFiles(new[] { conf }, DnsmasqConfKeys.Conntrack));
Assert.Equal(value, result); }
finally { Directory.Delete(dir, recursive: true); }
}
[Fact]
public void GetFlagFromConfigFiles_Conntrack_WhenAbsent_ReturnsFalse()
{
var dir = CreateTempDir();
var conf = Path.Combine(dir, "dnsmasq.conf");
try
{
File.WriteAllText(conf, "port=53\n");
Assert.False(DnsmasqConfIncludeParser.GetFlagFromConfigFiles(new[] { conf }, DnsmasqConfKeys.Conntrack));
} }
finally { Directory.Delete(dir, recursive: true); } finally { Directory.Delete(dir, recursive: true); }
} }
@@ -0,0 +1,160 @@
using DnsmasqWebUI.Infrastructure.Helpers.Config;
using DnsmasqWebUI.Infrastructure.Services.Dnsmasq.Config;
using DnsmasqWebUI.Models.Config;
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
namespace DnsmasqWebUI.Tests;
/// <summary>
/// Save-path tests for DnsmasqConfigService.ApplyEffectiveConfigChangesAsync.
/// Verifies that pending changes produce the correct managed config lines.
/// </summary>
public class DnsmasqConfigServiceApplyChangesTests
{
[Fact]
public async Task ApplyEffectiveConfigChangesAsync_ConntrackTrue_WritesBareFlagLine()
{
var dir = Path.Combine(Path.GetTempPath(), "dnsmasq-apply-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(dir);
var mainPath = Path.Combine(dir, "dnsmasq.conf");
var managedName = "zz-managed.conf";
var managedPath = Path.Combine(dir, managedName);
ConfigSetCache? cache = null;
try
{
File.WriteAllText(mainPath, "port=53\n");
var options = Options.Create(new DnsmasqOptions { MainConfigPath = mainPath, ManagedFileName = managedName });
cache = new ConfigSetCache(options, NullLogger<ConfigSetCache>.Instance);
var setService = new DnsmasqConfigSetService(cache);
var configService = new DnsmasqConfigService(setService, cache, NullLogger<DnsmasqConfigService>.Instance);
var changes = new List<PendingEffectiveConfigChange>
{
new(EffectiveConfigSections.SectionResolver, DnsmasqConfKeys.Conntrack, false, true, null)
};
await configService.ApplyEffectiveConfigChangesAsync(changes);
Assert.True(File.Exists(managedPath));
var content = await File.ReadAllTextAsync(managedPath);
var lines = content.TrimEnd().Split('\n').Select(l => l.Trim()).Where(l => l.Length > 0).ToList();
Assert.Contains(lines, l => l == "conntrack");
}
finally
{
cache?.Dispose();
if (Directory.Exists(dir))
Directory.Delete(dir, recursive: true);
}
}
[Fact]
public async Task ApplyEffectiveConfigChangesAsync_ConntrackFalse_RemovesFlagLine()
{
var dir = Path.Combine(Path.GetTempPath(), "dnsmasq-apply-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(dir);
var mainPath = Path.Combine(dir, "dnsmasq.conf");
var managedName = "zz-managed.conf";
var managedPath = Path.Combine(dir, managedName);
ConfigSetCache? cache = null;
try
{
File.WriteAllText(mainPath, "port=53\n");
File.WriteAllText(managedPath, "conntrack\n");
var options = Options.Create(new DnsmasqOptions { MainConfigPath = mainPath, ManagedFileName = managedName });
cache = new ConfigSetCache(options, NullLogger<ConfigSetCache>.Instance);
cache.Invalidate();
var setService = new DnsmasqConfigSetService(cache);
var configService = new DnsmasqConfigService(setService, cache, NullLogger<DnsmasqConfigService>.Instance);
var changes = new List<PendingEffectiveConfigChange>
{
new(EffectiveConfigSections.SectionResolver, DnsmasqConfKeys.Conntrack, true, false, null)
};
await configService.ApplyEffectiveConfigChangesAsync(changes);
var content = await File.ReadAllTextAsync(managedPath);
var lines = content.TrimEnd().Split('\n').Select(l => l.Trim()).Where(l => l.Length > 0).ToList();
Assert.DoesNotContain(lines, l => l == "conntrack" || l.StartsWith("conntrack=", StringComparison.Ordinal));
}
finally
{
cache?.Dispose();
if (Directory.Exists(dir))
Directory.Delete(dir, recursive: true);
}
}
[Fact]
public async Task ApplyEffectiveConfigChangesAsync_KeyOnlyOrValue_KeyOnly_WritesBareKey()
{
var dir = Path.Combine(Path.GetTempPath(), "dnsmasq-apply-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(dir);
var mainPath = Path.Combine(dir, "dnsmasq.conf");
var managedName = "zz-managed.conf";
var managedPath = Path.Combine(dir, managedName);
ConfigSetCache? cache = null;
try
{
File.WriteAllText(mainPath, "port=53\n");
var options = Options.Create(new DnsmasqOptions { MainConfigPath = mainPath, ManagedFileName = managedName });
cache = new ConfigSetCache(options, NullLogger<ConfigSetCache>.Instance);
var setService = new DnsmasqConfigSetService(cache);
var configService = new DnsmasqConfigService(setService, cache, NullLogger<DnsmasqConfigService>.Instance);
var changes = new List<PendingEffectiveConfigChange>
{
new(EffectiveConfigSections.SectionCache, DnsmasqConfKeys.UseStaleCache, null, "", null)
};
await configService.ApplyEffectiveConfigChangesAsync(changes);
Assert.True(File.Exists(managedPath));
var content = await File.ReadAllTextAsync(managedPath);
Assert.Contains("use-stale-cache", content);
Assert.DoesNotContain("use-stale-cache=", content);
}
finally
{
cache?.Dispose();
if (Directory.Exists(dir))
Directory.Delete(dir, recursive: true);
}
}
[Fact]
public async Task ApplyEffectiveConfigChangesAsync_InversePair_Enabled_WritesDo0x20Encode()
{
var dir = Path.Combine(Path.GetTempPath(), "dnsmasq-apply-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(dir);
var mainPath = Path.Combine(dir, "dnsmasq.conf");
var managedName = "zz-managed.conf";
var managedPath = Path.Combine(dir, managedName);
ConfigSetCache? cache = null;
try
{
File.WriteAllText(mainPath, "port=53\n");
var options = Options.Create(new DnsmasqOptions { MainConfigPath = mainPath, ManagedFileName = managedName });
cache = new ConfigSetCache(options, NullLogger<ConfigSetCache>.Instance);
var setService = new DnsmasqConfigSetService(cache);
var configService = new DnsmasqConfigService(setService, cache, NullLogger<DnsmasqConfigService>.Instance);
var changes = new List<PendingEffectiveConfigChange>
{
new(EffectiveConfigSections.SectionResolver, DnsmasqConfKeys.Do0x20Encode, ExplicitToggleState.Default, ExplicitToggleState.Enabled, null)
};
await configService.ApplyEffectiveConfigChangesAsync(changes);
Assert.True(File.Exists(managedPath));
var content = await File.ReadAllTextAsync(managedPath);
Assert.Contains("do-0x20-encode", content);
Assert.DoesNotContain("no-0x20-encode", content);
}
finally
{
cache?.Dispose();
if (Directory.Exists(dir))
Directory.Delete(dir, recursive: true);
}
}
}
@@ -54,6 +54,23 @@ public class EffectiveDnsmasqConfigTests
DhcpHostsfilePaths: Array.Empty<string>(), DhcpHostsfilePaths: Array.Empty<string>(),
DhcpOptsfilePaths: Array.Empty<string>(), DhcpOptsfilePaths: Array.Empty<string>(),
DhcpHostsdirPaths: Array.Empty<string>(), DhcpHostsdirPaths: Array.Empty<string>(),
ConnmarkAllowlistValues: Array.Empty<string>(),
CaaRecordValues: Array.Empty<string>(),
DnsRrValues: Array.Empty<string>(),
SynthDomainValues: Array.Empty<string>(),
AuthZoneValues: Array.Empty<string>(),
AuthSoaValues: Array.Empty<string>(),
AuthSecServersValues: Array.Empty<string>(),
AuthPeerValues: Array.Empty<string>(),
DhcpRelayValues: Array.Empty<string>(),
DhcpCircuitidValues: Array.Empty<string>(),
DhcpRemoteidValues: Array.Empty<string>(),
DhcpSubscridValues: Array.Empty<string>(),
DhcpProxyValues: Array.Empty<string>(),
TagIfValues: Array.Empty<string>(),
BridgeInterfaceValues: Array.Empty<string>(),
SharedNetworkValues: Array.Empty<string>(),
DhcpOptionPxeValues: Array.Empty<string>(),
ExpandHosts: false, ExpandHosts: false,
BogusPriv: false, BogusPriv: false,
StrictOrder: false, StrictOrder: false,
@@ -90,6 +107,24 @@ public class EffectiveDnsmasqConfigTests
KeepInForeground: false, KeepInForeground: false,
NoDaemon: false, NoDaemon: false,
ProxyDnssec: false, ProxyDnssec: false,
ConnmarkAllowlistEnable: false,
NoRoundRobin: false,
DnssecNoTimecheck: false,
DnssecDebug: false,
Leasequery: false,
DhcpGenerateNames: false,
DhcpBroadcast: false,
DhcpSequentialIp: false,
DhcpIgnoreClid: false,
BootpDynamic: false,
NoPing: false,
ScriptArp: false,
ScriptOnRenewal: false,
DhcpNoOverride: false,
QuietDhcp: false,
QuietDhcp6: false,
QuietRa: false,
QuietTftp: false,
DhcpLeaseFilePath: null, DhcpLeaseFilePath: null,
CacheSize: null, CacheSize: null,
Port: null, Port: null,
@@ -120,6 +155,22 @@ public class EffectiveDnsmasqConfigTests
FastDnsRetry: null, FastDnsRetry: null,
DhcpScriptPath: null, DhcpScriptPath: null,
MxTarget: null, MxTarget: null,
DnsForwardMax: null,
DumpfilePath: null,
Dumpmask: null,
AddCpeId: null,
DnssecTimestamp: null,
DnssecLimits: null,
DhcpAlternatePort: null,
DhcpDuid: null,
DhcpLuascriptPath: null,
DhcpScriptuser: null,
DhcpPxeVendor: null,
UseStaleCache: null,
AddMac: null,
AddSubnet: null,
Umbrella: null,
Do0x20EncodeState: ExplicitToggleState.Default,
Conntrack: false Conntrack: false
); );
@@ -24,7 +24,21 @@
{ {
<pre class="pre-block mb-0" style="max-height: 22rem;">@_status.StatusShowOutput</pre> <pre class="pre-block mb-0" style="max-height: 22rem;">@_status.StatusShowOutput</pre>
} }
<div class="mt-2"> <div class="mt-2 d-flex align-items-center gap-2 flex-wrap">
<button class="btn btn-primary btn-sm" @onclick="RefreshAsync" disabled="@_refreshing"><i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>Refresh</button> <button class="btn btn-primary btn-sm" @onclick="RefreshAsync" disabled="@_refreshing"><i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>Refresh</button>
@if (ReloadCommandConfigured && OnReloadRequested.HasDelegate)
{
<button class="btn btn-primary btn-sm" @onclick="InvokeReloadAsync" disabled="@IsReloading" title="Reload dnsmasq config (e.g. SIGHUP)">
@if (IsReloading)
{
<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
}
else
{
<i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>
}
Reload config
</button>
}
</div> </div>
} }
@@ -19,6 +19,11 @@ public partial class StatusSection : IDisposable
[Parameter] public bool IsReloading { get; set; } [Parameter] public bool IsReloading { get; set; }
/// <summary>When set and reload is configured, show a "Reload config" button next to Refresh.</summary>
[Parameter] public EventCallback OnReloadRequested { get; set; }
[Parameter] public bool ReloadCommandConfigured { get; set; }
[Parameter] public EventCallback OnOpenSettings { get; set; } [Parameter] public EventCallback OnOpenSettings { get; set; }
[Inject] private IStatusClient StatusClient { get; set; } = null!; [Inject] private IStatusClient StatusClient { get; set; } = null!;
@@ -55,6 +60,8 @@ public partial class StatusSection : IDisposable
private static readonly TimeSpan MinUpdatingDuration = TimeSpan.FromSeconds(1); private static readonly TimeSpan MinUpdatingDuration = TimeSpan.FromSeconds(1);
private Task InvokeReloadAsync() => OnReloadRequested.InvokeAsync();
private async Task RefreshAsync() private async Task RefreshAsync()
{ {
var started = DateTime.UtcNow; var started = DateTime.UtcNow;
@@ -18,7 +18,7 @@
} }
<EffectiveConfigToolbar ShowSearchBox="@ShowSearchBox" SearchTerm="@_searchTerm" SearchTermChanged="@OnSearchTermChanged" <EffectiveConfigToolbar ShowSearchBox="@ShowSearchBox" SearchTerm="@_searchTerm" SearchTermChanged="@OnSearchTermChanged"
IsEditMode="@Session.IsEditMode" PendingChangesCount="@Session.PendingChanges.Count" IsEditMode="@Session.IsEditMode" PendingChangesCount="@Session.PendingChanges.Count"
ValidationErrorCount="@_validationErrorCount" ValidationWarningCount="@_validationWarningCount" ValidationSummary="@Session.GetValidationSummary()"
AllSectionsExpanded="@_allSectionsExpanded" AllSectionsExpanded="@_allSectionsExpanded"
OnExpandAll="ExpandAll" OnCollapseAll="CollapseAll" OnEnterEditMode="EnterEditMode" OnExitEditModeWithConfirm="ExitEditModeWithConfirmAsync" OnOpenSaveModal="OpenSaveModal" /> OnExpandAll="ExpandAll" OnCollapseAll="CollapseAll" OnEnterEditMode="EnterEditMode" OnExitEditModeWithConfirm="ExitEditModeWithConfirmAsync" OnOpenSaveModal="OpenSaveModal" />
<CascadingValue Value="@Session"> <CascadingValue Value="@Session">
@@ -28,7 +28,7 @@
</CascadingValue> </CascadingValue>
</CascadingValue> </CascadingValue>
<EffectiveConfigSaveFlow ShowModal="@_showSaveModal" PendingChanges="@Session.PendingChanges" Status="@Status" Session="@Session" <EffectiveConfigSaveFlow ShowModal="@_showSaveModal" PendingChanges="@Session.PendingChanges" Status="@Status" Session="@Session"
OnClose="CloseSaveModal" OnSaveCompleted="OnSaveCompleted" OnContinueEditingAfterSavedFailure="ContinueEditingAfterSavedFailureAsync" OnCloseAndEndEdit="CloseSaveModalAndEndEdit" /> OnClose="CloseSaveModal" OnRevertedNotify="OnRevertedFromSaveModal" OnSaveCompleted="OnSaveCompleted" OnContinueEditingAfterSavedFailure="ContinueEditingAfterSavedFailureAsync" OnCloseAndEndEdit="CloseSaveModalAndEndEdit" />
</div> </div>
} }
@@ -42,9 +42,6 @@
private HashSet<string> _openPanels = new(); private HashSet<string> _openPanels = new();
private bool _showSaveModal; private bool _showSaveModal;
private int _validationErrorCount => Session.GetValidationSummary().Count(i => i.Severity == FieldIssueSeverity.Error);
private int _validationWarningCount => Session.GetValidationSummary().Count(i => i.Severity == FieldIssueSeverity.Warning);
private string? _optionHelpKey; private string? _optionHelpKey;
private string? _optionHelpAnchorId; private string? _optionHelpAnchorId;
private string _optionHelpTitle = "Option help"; private string _optionHelpTitle = "Option help";
@@ -199,6 +196,12 @@
Session.SetCrossOptionIssues(issues); Session.SetCrossOptionIssues(issues);
} }
private void OnRevertedFromSaveModal()
{
RunCrossOptionEvaluator();
StateHasChanged();
}
private void OpenSaveModal() private void OpenSaveModal()
{ {
if (Session.PendingChanges.Count == 0) return; if (Session.PendingChanges.Count == 0) return;
@@ -187,11 +187,23 @@
gap: 0.25rem; gap: 0.25rem;
} }
/* Edit wrapper (checkbox/input) shrinks to content so the editable badge stays on the same line */ /* When a field is active: value + badges in one block so any click inside (checkbox, input, ok/cancel) doesn't deactivate */
::deep .ec-field-value-when-editing {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: 0.25rem;
min-width: 0;
max-width: 100%;
}
/* Edit wrapper (checkbox/input) shrinks to content */
::deep .ec-field-edit-wrapper { ::deep .ec-field-edit-wrapper {
flex: 0 0 auto; flex: 0 1 auto;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
min-width: 0;
max-width: 100%;
} }
/* Input/select in value area can shrink so the editable badge stays on the same line */ /* Input/select in value area can shrink so the editable badge stays on the same line */
@@ -253,6 +265,21 @@
line-height: 1; line-height: 1;
} }
/* Only when field is in edit mode: push ok/cancel/revert to end and allow them to wrap first on narrow widths */
::deep .ec-field-value-when-editing > .status-badge-inline {
flex: 0 0 auto;
white-space: nowrap;
margin-left: auto;
}
@media (max-width: 991.98px) {
::deep .ec-field-value-when-editing > .status-badge-inline {
flex-basis: 100%;
margin-left: 0;
margin-top: 0.15rem;
}
}
@media (max-width: 767.98px) { @media (max-width: 767.98px) {
::deep .ec-section-header { ::deep .ec-section-header {
padding-left: 0.75rem; padding-left: 0.75rem;
@@ -35,12 +35,24 @@
<span class="small ec-toolbar-validation-count" aria-live="polite"> <span class="small ec-toolbar-validation-count" aria-live="polite">
@if (ValidationErrorCount > 0) @if (ValidationErrorCount > 0)
{ {
<span class="text-danger fw-medium">errors: @ValidationErrorCount</span> <span class="text-danger fw-medium ec-toolbar-changes-link" role="button" tabindex="0"
title="@ValidationErrorTooltip"
aria-label="Open save dialog"
@onclick="OnOpenSaveModal"
@onkeydown="OnValidationLinkKeyDown">
@ValidationErrorCount error(s)
</span>
} }
@if (ValidationErrorCount > 0 && ValidationWarningCount > 0) { <span class="text-muted">·</span> } @if (ValidationErrorCount > 0 && ValidationWarningCount > 0) { <span class="text-muted">·</span> }
@if (ValidationWarningCount > 0) @if (ValidationWarningCount > 0)
{ {
<span class="text-warning">warnings: @ValidationWarningCount</span> <span class="text-warning ec-toolbar-changes-link" role="button" tabindex="0"
title="@ValidationWarningTooltip"
aria-label="Open save dialog"
@onclick="OnOpenSaveModal"
@onkeydown="OnValidationLinkKeyDown">
@ValidationWarningCount warning(s)
</span>
} }
</span> </span>
} }
@@ -60,8 +72,11 @@
[Parameter] public EventCallback<string> SearchTermChanged { get; set; } [Parameter] public EventCallback<string> SearchTermChanged { get; set; }
[Parameter] public bool IsEditMode { get; set; } [Parameter] public bool IsEditMode { get; set; }
[Parameter] public int PendingChangesCount { get; set; } [Parameter] public int PendingChangesCount { get; set; }
[Parameter] public int ValidationErrorCount { get; set; } [Parameter] public IReadOnlyList<FieldIssue>? ValidationSummary { get; set; }
[Parameter] public int ValidationWarningCount { get; set; } private int ValidationErrorCount => ValidationSummary?.Count(i => i.Severity == FieldIssueSeverity.Error) ?? 0;
private int ValidationWarningCount => ValidationSummary?.Count(i => i.Severity == FieldIssueSeverity.Warning) ?? 0;
private string ValidationErrorTooltip => ValidationSummary == null ? "" : string.Join("\n", ValidationSummary.Where(i => i.Severity == FieldIssueSeverity.Error).Select(i => i.Message));
private string ValidationWarningTooltip => ValidationSummary == null ? "" : string.Join("\n", ValidationSummary.Where(i => i.Severity == FieldIssueSeverity.Warning).Select(i => i.Message));
[Parameter] public bool AllSectionsExpanded { get; set; } [Parameter] public bool AllSectionsExpanded { get; set; }
[Parameter] public EventCallback OnExpandAll { get; set; } [Parameter] public EventCallback OnExpandAll { get; set; }
[Parameter] public EventCallback OnCollapseAll { get; set; } [Parameter] public EventCallback OnCollapseAll { get; set; }
@@ -85,8 +100,12 @@
private async Task OnChangesLinkKeyDown(KeyboardEventArgs e) private async Task OnChangesLinkKeyDown(KeyboardEventArgs e)
{ {
if (e.Key == "Enter" || e.Key == " ") if (e.Key == "Enter" || e.Key == " ")
{
await OnOpenSaveModal.InvokeAsync(); await OnOpenSaveModal.InvokeAsync();
} }
private async Task OnValidationLinkKeyDown(KeyboardEventArgs e)
{
if (e.Key == "Enter" || e.Key == " ")
await OnOpenSaveModal.InvokeAsync();
} }
} }
@@ -0,0 +1,59 @@
@namespace DnsmasqWebUI.Components.EffectiveConfig.CustomDisplays
@using DnsmasqWebUI.Models.Dnsmasq
@using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig
@inherits EffectiveConfigValueDisplayBase
@* do-0x20-encode / no-0x20-encode: tri-state. Checkbox shows Enabled vs not-Enabled; uncheck maps back to Default when that was the value at activation, else Disabled. *@
@if (IsActiveEditor && Descriptor.GetSource()?.IsReadOnly != true)
{
var isOn = GetState() == ExplicitToggleState.Enabled;
<input type="checkbox" class="form-check-input" checked="@isOn" @onchange="OnCheckedChange" @onfocusout="OnCommitRequested" />
}
else
{
@(GetState() == ExplicitToggleState.Enabled ? "Enabled" : "Disabled")
}
@code {
private ExplicitToggleState _stateAtActivation;
private bool _wasActiveEditor;
private ExplicitToggleState GetState()
{
var v = GetEffectiveDisplayValue();
if (v is ExplicitToggleState s) return s;
return ExplicitToggleState.Default;
}
protected override void OnParametersSet()
{
if (!IsActiveEditor)
_stateAtActivation = GetState();
else if (!_wasActiveEditor)
_stateAtActivation = GetState();
_wasActiveEditor = IsActiveEditor;
}
private async Task OnCheckedChange(ChangeEventArgs e)
{
var isChecked = e.Value is true;
if (isChecked)
{
await NotifyValueChanged(ExplicitToggleState.Enabled);
return;
}
var pending = Session?.PendingChanges.FirstOrDefault(c =>
string.Equals(c.SectionId, Descriptor.SectionId, StringComparison.OrdinalIgnoreCase) &&
string.Equals(c.OptionName, Descriptor.OptionName, StringComparison.OrdinalIgnoreCase));
if (pending?.OldValue is ExplicitToggleState previousState)
{
await NotifyValueChanged(previousState);
return;
}
// No existing pending edit for this field: map unchecked based on the state at activation.
// Default and Disabled both display as "Disabled" in view mode, so we preserve true intent here.
await NotifyValueChanged(_stateAtActivation == ExplicitToggleState.Default ? ExplicitToggleState.Default : ExplicitToggleState.Disabled);
}
}
@@ -0,0 +1,84 @@
@namespace DnsmasqWebUI.Components.EffectiveConfig.CustomDisplays
@using DnsmasqWebUI.Models.Dnsmasq
@inherits EffectiveConfigValueDisplayBase
@* Options that can be: not set, key-only (e.g. use-stale-cache), or key=value (e.g. use-stale-cache=60). Checkbox to enable; when enabled, optional value input. *@
@if (IsActiveEditor && Descriptor.GetSource()?.IsReadOnly != true)
{
var isEnabled = _editorValue != null;
var valuePart = _editorValue is { Length: > 0 } ? _editorValue : "";
<div class="ec-key-only-or-value-editor">
<div class="form-check form-switch mb-0 ec-key-only-or-value-switch">
<input type="checkbox"
class="form-check-input"
checked="@isEnabled"
aria-label="Enable @Descriptor.OptionName"
title="Set @Descriptor.OptionName"
@onchange="OnCheckedChange" />
</div>
@if (isEnabled)
{
<input type="text"
class="form-control form-control-sm ec-key-only-or-value-input"
placeholder="Optional value"
value="@valuePart"
@oninput="OnValueInput" />
}
</div>
}
else
{
var v = GetEffectiveDisplayValue() as string;
if (v is null)
{
@("(not set)")
}
else if (v.Length == 0)
{
@("Enabled")
}
else
{
@v
}
}
@code {
private string? _editorValue;
private bool _wasActiveEditor;
protected override void OnParametersSet()
{
var effective = GetEffectiveDisplayValue() as string;
if (!IsActiveEditor)
{
_editorValue = effective;
}
else if (!_wasActiveEditor)
{
_editorValue = effective;
}
_wasActiveEditor = IsActiveEditor;
}
private async Task OnCheckedChange(ChangeEventArgs e)
{
var isChecked = e.Value switch
{
bool b => b,
string s when bool.TryParse(s, out var parsed) => parsed,
_ => false
};
_editorValue = isChecked ? "" : null;
await NotifyValueChanged(_editorValue);
}
private async Task OnValueInput(ChangeEventArgs e)
{
var raw = e.Value?.ToString()?.Trim();
_editorValue = string.IsNullOrEmpty(raw) ? "" : raw;
await NotifyValueChanged(_editorValue);
}
}
@@ -0,0 +1,31 @@
.ec-key-only-or-value-editor {
display: inline-flex;
align-items: center;
gap: 0.5rem;
min-width: 0;
max-width: 100%;
white-space: nowrap;
}
.ec-key-only-or-value-switch {
display: inline-flex;
align-items: center;
flex: 0 0 auto;
min-height: 1.25rem;
padding-left: 0;
}
.ec-key-only-or-value-switch .form-check-input {
float: none;
margin: 0;
cursor: pointer;
position: relative;
top: 2px;
}
.ec-key-only-or-value-input {
flex: 1 1 7rem;
min-width: 0;
width: 7rem;
max-width: 9rem;
}
@@ -22,12 +22,15 @@
<span class="ec-field-value"> <span class="ec-field-value">
@if (isActiveEditor) @if (isActiveEditor)
{ {
<div class="ec-field-edit-wrapper" @onfocusout="OnBlur" @onclick:stopPropagation="true"> <div class="ec-field-value-when-editing" @onclick:stopPropagation="true">
<CascadingValue Value="@true"> <div class="ec-field-edit-wrapper">
<CascadingValue Value="@(EventCallback.Factory.Create<FocusEventArgs>(this, OnBlur))"> <CascadingValue Value="@true">
@((fragmentWithCallback ?? fragment)(Descriptor)) <CascadingValue Value="@(EventCallback.Factory.Create<FocusEventArgs>(this, OnBlur))">
@((fragmentWithCallback ?? fragment)(Descriptor))
</CascadingValue>
</CascadingValue> </CascadingValue>
</CascadingValue> </div>
<OptionStateBadge AnchorId="@_readonlyBadgeAnchorId" Source="@Descriptor.GetSource()" OptionName="@Descriptor.OptionName" Value="@effectiveValue" IsInteractive="@IsEditMode" ShowEditableBadge="@showEditableBadge" HasPendingChange="@_hasPendingChange" EditableAsButton="true" FieldKey="@FieldKey" IsActiveEditor="@isActiveEditor" OnConfirmRequested="@HandleConfirmEdit" OnCancelRequested="@HandleCancelEdit" OnRevertRequested="@HandleRevertEdit" />
</div> </div>
} }
else else
@@ -35,8 +38,8 @@
<CascadingValue Value="@false"> <CascadingValue Value="@false">
@fragment(Descriptor) @fragment(Descriptor)
</CascadingValue> </CascadingValue>
<OptionStateBadge AnchorId="@_readonlyBadgeAnchorId" Source="@Descriptor.GetSource()" OptionName="@Descriptor.OptionName" Value="@effectiveValue" IsInteractive="@IsEditMode" ShowEditableBadge="@showEditableBadge" HasPendingChange="@_hasPendingChange" EditableAsButton="true" FieldKey="@FieldKey" IsActiveEditor="@isActiveEditor" OnConfirmRequested="@HandleConfirmEdit" OnCancelRequested="@HandleCancelEdit" OnRevertRequested="@HandleRevertEdit" />
} }
<OptionStateBadge AnchorId="@_readonlyBadgeAnchorId" Source="@Descriptor.GetSource()" OptionName="@Descriptor.OptionName" Value="@effectiveValue" IsInteractive="@IsEditMode" ShowEditableBadge="@showEditableBadge" HasPendingChange="@_hasPendingChange" EditableAsButton="true" FieldKey="@FieldKey" IsActiveEditor="@isActiveEditor" OnConfirmRequested="@HandleConfirmEdit" OnCancelRequested="@HandleCancelEdit" OnRevertRequested="@HandleRevertEdit" />
</span> </span>
</li> </li>
} }
@@ -111,6 +114,7 @@ else
private bool _hasPendingChange; private bool _hasPendingChange;
private object? _valueAtActivation; private object? _valueAtActivation;
private object? _draftValue; private object? _draftValue;
private bool _hasDraftValue;
private bool _wasActive; private bool _wasActive;
private ExitAction _exitAction = ExitAction.None; private ExitAction _exitAction = ExitAction.None;
@@ -140,7 +144,12 @@ else
if (Descriptor.IsMultiValue) if (Descriptor.IsMultiValue)
ComputeMultiValueState(Descriptor.GetItems(), pending?.NewValue as IReadOnlyList<string>, _draftValue as IReadOnlyList<string>); ComputeMultiValueState(Descriptor.GetItems(), pending?.NewValue as IReadOnlyList<string>, _draftValue as IReadOnlyList<string>);
if (!_wasActive && IsActive) if (!_wasActive && IsActive)
_valueAtActivation = descriptorValue; {
// Use pending.OldValue when present so "revert to original" clears the pending change (e.g. do-0x20-encode: Disabled → Enable → Disable).
_valueAtActivation = pending != null ? pending.OldValue : descriptorValue;
_hasDraftValue = false;
_draftValue = null;
}
if (_wasActive && !IsActive && _exitAction == ExitAction.None) if (_wasActive && !IsActive && _exitAction == ExitAction.None)
_ = InvokeAsync(CommitIfChangedAsync); _ = InvokeAsync(CommitIfChangedAsync);
if (_wasActive && !IsActive) if (_wasActive && !IsActive)
@@ -190,8 +199,7 @@ else
private void HandleValueChanged(object? value) private void HandleValueChanged(object? value)
{ {
_draftValue = value; _draftValue = value;
if (_hasPendingChange && ValuesEqual(_valueAtActivation, value)) _hasDraftValue = true;
_ = InvokeAsync(CommitIfChangedAsync);
} }
private async Task OnBlur(FocusEventArgs _) private async Task OnBlur(FocusEventArgs _)
@@ -202,10 +210,11 @@ else
private async Task<bool> CommitIfChangedAsync() private async Task<bool> CommitIfChangedAsync()
{ {
var newValue = _draftValue ?? _effectiveValue; var newValue = _hasDraftValue ? _draftValue : _effectiveValue;
if (ValuesEqual(_valueAtActivation, newValue)) if (ValuesEqual(_valueAtActivation, newValue))
{ {
_draftValue = null; _draftValue = null;
_hasDraftValue = false;
if (_hasPendingChange && Ui?.RevertFieldAsync != null) if (_hasPendingChange && Ui?.RevertFieldAsync != null)
await Ui.RevertFieldAsync(Descriptor.SectionId, Descriptor.OptionName); await Ui.RevertFieldAsync(Descriptor.SectionId, Descriptor.OptionName);
return true; return true;
@@ -228,6 +237,7 @@ else
if (Ui != null) if (Ui != null)
await Ui.CommitFieldAsync(args); await Ui.CommitFieldAsync(args);
_draftValue = null; _draftValue = null;
_hasDraftValue = false;
return true; return true;
} }
@@ -236,12 +246,14 @@ else
private void HandleDefaultValueChanged(string value) private void HandleDefaultValueChanged(string value)
{ {
_draftValue = value; _draftValue = value;
_hasDraftValue = true;
} }
private async Task HandleDefaultBlur(string? value) private async Task HandleDefaultBlur(string? value)
{ {
if (!IsActive) return; if (!IsActive) return;
_draftValue = value; _draftValue = value;
_hasDraftValue = true;
if (!await CommitIfChangedAsync() && Ui != null) if (!await CommitIfChangedAsync() && Ui != null)
await Ui.ActivateFieldAsync(FieldKey); await Ui.ActivateFieldAsync(FieldKey);
} }
@@ -249,6 +261,7 @@ else
private void HandleMultiValueChanged(IReadOnlyList<string> values) private void HandleMultiValueChanged(IReadOnlyList<string> values)
{ {
_draftValue = values.ToList(); _draftValue = values.ToList();
_hasDraftValue = true;
if (Descriptor.IsMultiValue) if (Descriptor.IsMultiValue)
ComputeMultiValueState(Descriptor.GetItems(), _effectiveValue as IReadOnlyList<string>, _draftValue as IReadOnlyList<string>); ComputeMultiValueState(Descriptor.GetItems(), _effectiveValue as IReadOnlyList<string>, _draftValue as IReadOnlyList<string>);
StateHasChanged(); StateHasChanged();
@@ -266,6 +279,7 @@ else
private async Task HandleCancelEdit() private async Task HandleCancelEdit()
{ {
_draftValue = null; _draftValue = null;
_hasDraftValue = false;
_exitAction = ExitAction.Cancel; _exitAction = ExitAction.Cancel;
if (Ui != null) if (Ui != null)
await Ui.DeactivateFieldAsync(); await Ui.DeactivateFieldAsync();
@@ -274,6 +288,7 @@ else
private async Task HandleRevertEdit() private async Task HandleRevertEdit()
{ {
_draftValue = null; _draftValue = null;
_hasDraftValue = false;
Session?.RevertChange(Descriptor.SectionId, Descriptor.OptionName); Session?.RevertChange(Descriptor.SectionId, Descriptor.OptionName);
_exitAction = ExitAction.Revert; _exitAction = ExitAction.Revert;
if (Ui != null) if (Ui != null)
@@ -39,6 +39,8 @@
[Parameter] public IReadOnlyList<PendingEffectiveConfigChange> PendingChanges { get; set; } = null!; [Parameter] public IReadOnlyList<PendingEffectiveConfigChange> PendingChanges { get; set; } = null!;
[Parameter] public DnsmasqServiceStatus? Status { get; set; } [Parameter] public DnsmasqServiceStatus? Status { get; set; }
[Parameter] public EventCallback OnClose { get; set; } [Parameter] public EventCallback OnClose { get; set; }
/// <summary>Invoked when the modal reverts a change so the parent can re-run validation (e.g. cross-option evaluator).</summary>
[Parameter] public EventCallback OnRevertedNotify { get; set; }
[Parameter] public EventCallback OnSaveCompleted { get; set; } [Parameter] public EventCallback OnSaveCompleted { get; set; }
[Parameter] public EventCallback OnContinueEditingAfterSavedFailure { get; set; } [Parameter] public EventCallback OnContinueEditingAfterSavedFailure { get; set; }
[Parameter] public EventCallback OnCloseAndEndEdit { get; set; } [Parameter] public EventCallback OnCloseAndEndEdit { get; set; }
@@ -60,6 +62,8 @@
private async Task HandleReverted() private async Task HandleReverted()
{ {
if (OnRevertedNotify.HasDelegate)
await OnRevertedNotify.InvokeAsync();
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
@@ -2,6 +2,7 @@
@using DnsmasqWebUI.Models.Dnsmasq @using DnsmasqWebUI.Models.Dnsmasq
@using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig @using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig
@using DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Abstractions @using DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Abstractions
@using DnsmasqWebUI.Infrastructure.Helpers.Config
<div class="modal fade show" style="display: block;" tabindex="-1" role="dialog" aria-labelledby="ec-save-modal-title" aria-modal="true"> <div class="modal fade show" style="display: block;" tabindex="-1" role="dialog" aria-labelledby="ec-save-modal-title" aria-modal="true">
<div class="modal-dialog modal-dialog-scrollable" role="document"> <div class="modal-dialog modal-dialog-scrollable" role="document">
@@ -85,6 +86,35 @@
} }
else else
{ {
@if (ValidationErrors.Count > 0 || ValidationWarnings.Count > 0)
{
<div class="ec-save-modal-validation mb-3">
@if (ValidationErrors.Count > 0)
{
<div class="alert alert-danger py-2 mb-2">
<strong>Validation errors</strong>
<ul class="mb-0 mt-1 ps-3">
@foreach (var issue in ValidationErrors)
{
<li>@issue.Message</li>
}
</ul>
</div>
}
@if (ValidationWarnings.Count > 0)
{
<div class="alert alert-warning py-2 mb-2">
<strong>Validation warnings</strong>
<ul class="mb-0 mt-1 ps-3">
@foreach (var issue in ValidationWarnings)
{
<li>@issue.Message</li>
}
</ul>
</div>
}
</div>
}
<p class="text-muted mb-3">These changes will be written to your config. Remove any you don't want to keep.</p> <p class="text-muted mb-3">These changes will be written to your config. Remove any you don't want to keep.</p>
@if (ManagedFilePath != null) @if (ManagedFilePath != null)
{ {
@@ -114,9 +144,9 @@
@if (!isMultiValue) @if (!isMultiValue)
{ {
<span class="ec-save-modal-diff text-body"> <span class="ec-save-modal-diff text-body">
<span class="text-muted text-decoration-line-through">@FormatValue(change.OldValue)</span> <span class="text-muted text-decoration-line-through">@FormatValue(change.OldValue, change.OptionName)</span>
<span class="text-muted mx-1" aria-hidden="true">→</span> <span class="text-muted mx-1" aria-hidden="true">→</span>
<span>@FormatValue(change.NewValue)</span> <span>@FormatValue(change.NewValue, change.OptionName)</span>
</span> </span>
} }
</div> </div>
@@ -180,7 +210,7 @@
else if (SaveState != EffectiveConfigSaveFlow.SaveUiState.Saving && SaveState != EffectiveConfigSaveFlow.SaveUiState.RestoreInProgress) else if (SaveState != EffectiveConfigSaveFlow.SaveUiState.Saving && SaveState != EffectiveConfigSaveFlow.SaveUiState.RestoreInProgress)
{ {
<button type="button" class="btn btn-secondary" @onclick="Close"><i class="bi bi-x me-1" aria-hidden="true"></i>Cancel</button> <button type="button" class="btn btn-secondary" @onclick="Close"><i class="bi bi-x me-1" aria-hidden="true"></i>Cancel</button>
<button type="button" class="btn btn-primary" @onclick="Save" disabled="@(PendingChanges.Count == 0)"><i class="bi bi-floppy-fill me-1" aria-hidden="true"></i>Save</button> <button type="button" class="btn btn-primary" @onclick="Save" disabled="@(PendingChanges.Count == 0 || HasBlockingValidationErrors)"><i class="bi bi-floppy-fill me-1" aria-hidden="true"></i>Save</button>
} }
</div> </div>
</div> </div>
@@ -206,6 +236,10 @@
private string? ManagedFilePath => Status?.ManagedFilePath; private string? ManagedFilePath => Status?.ManagedFilePath;
private IReadOnlyList<FieldIssue> ValidationErrors => Session.GetValidationSummary().Where(i => i.Severity == FieldIssueSeverity.Error).ToList();
private IReadOnlyList<FieldIssue> ValidationWarnings => Session.GetValidationSummary().Where(i => i.Severity == FieldIssueSeverity.Warning).ToList();
private bool HasBlockingValidationErrors => Session.HasBlockingValidationErrors();
private static string GetValidationFailedOutput(EffectiveConfigSaveResult result) private static string GetValidationFailedOutput(EffectiveConfigSaveResult result)
{ {
var err = (result.ValidationStdErr ?? "").Trim(); var err = (result.ValidationStdErr ?? "").Trim();
@@ -248,8 +282,10 @@
return null; return null;
} }
private static string FormatValue(object? v) private static string FormatValue(object? v, string? optionName = null)
{ {
if (optionName != null && EffectiveConfigWriteBehaviorMap.GetBehavior(optionName) == EffectiveConfigWriteBehavior.KeyOnlyOrValue)
return FormatKeyOnlyOrValue(v);
if (v == null) return "(not set)"; if (v == null) return "(not set)";
if (v is bool b) return b ? "on" : "off"; if (v is bool b) return b ? "on" : "off";
var list = AsStringList(v); var list = AsStringList(v);
@@ -257,6 +293,14 @@
return v.ToString() ?? "(not set)"; return v.ToString() ?? "(not set)";
} }
private static string FormatKeyOnlyOrValue(object? v)
{
if (v == null) return "(not set)";
var s = v.ToString();
if (string.IsNullOrEmpty(s)) return "(set)";
return s;
}
private async Task Revert(PendingEffectiveConfigChange change) private async Task Revert(PendingEffectiveConfigChange change)
{ {
Session.RevertChange(change.SectionId, change.OptionName); Session.RevertChange(change.SectionId, change.OptionName);
@@ -76,7 +76,7 @@ else if (_status != null)
</div> </div>
} }
<StatusSection RefreshIntervalSeconds="@ClientSettingsFields.ServiceStatusPollingInterval.Value" IsReloading="_reloading" OnOpenSettings="OpenServiceStatusSettings" /> <StatusSection RefreshIntervalSeconds="@ClientSettingsFields.ServiceStatusPollingInterval.Value" IsReloading="_reloading" OnReloadRequested="ReloadDnsmasq" ReloadCommandConfigured="_status.ReloadCommandConfigured" OnOpenSettings="OpenServiceStatusSettings" />
<LogsSection Status="_status" RefreshIntervalSeconds="@ClientSettingsFields.RecentLogsPollingInterval.Value" LogsMaxLines="@ClientSettingsFields.RecentLogsMaxLines.Value" LogsAutoScroll="@ClientSettingsFields.RecentLogsAutoScroll.Value" OnOpenSettings="OpenRecentLogsSettings" /> <LogsSection Status="_status" RefreshIntervalSeconds="@ClientSettingsFields.RecentLogsPollingInterval.Value" LogsMaxLines="@ClientSettingsFields.RecentLogsMaxLines.Value" LogsAutoScroll="@ClientSettingsFields.RecentLogsAutoScroll.Value" OnOpenSettings="OpenRecentLogsSettings" />
<AppLogsSection RefreshIntervalSeconds="@ClientSettingsFields.AppLogsPollingInterval.Value" LogsMaxLines="@ClientSettingsFields.AppLogsMaxLines.Value" LogsAutoScroll="@ClientSettingsFields.AppLogsAutoScroll.Value" OnOpenSettings="OpenAppLogsSettings" /> <AppLogsSection RefreshIntervalSeconds="@ClientSettingsFields.AppLogsPollingInterval.Value" LogsMaxLines="@ClientSettingsFields.AppLogsMaxLines.Value" LogsAutoScroll="@ClientSettingsFields.AppLogsAutoScroll.Value" OnOpenSettings="OpenAppLogsSettings" />
</section> </section>
@@ -1,3 +1,4 @@
using DnsmasqWebUI.Infrastructure.Services.Dnsmasq.Config.Abstractions;
using DnsmasqWebUI.Infrastructure.Services.Dnsmasq.Reload.Abstractions; using DnsmasqWebUI.Infrastructure.Services.Dnsmasq.Reload.Abstractions;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -9,11 +10,13 @@ namespace DnsmasqWebUI.Controllers;
public class ReloadController : ControllerBase public class ReloadController : ControllerBase
{ {
private readonly IReloadService _reloadService; private readonly IReloadService _reloadService;
private readonly IConfigSetCache _configSetCache;
private readonly ILogger<ReloadController> _logger; private readonly ILogger<ReloadController> _logger;
public ReloadController(IReloadService reloadService, ILogger<ReloadController> logger) public ReloadController(IReloadService reloadService, IConfigSetCache configSetCache, ILogger<ReloadController> logger)
{ {
_reloadService = reloadService; _reloadService = reloadService;
_configSetCache = configSetCache;
_logger = logger; _logger = logger;
} }
@@ -24,6 +27,8 @@ public class ReloadController : ControllerBase
{ {
var result = await _reloadService.ReloadAsync(ct); var result = await _reloadService.ReloadAsync(ct);
_logger.LogInformation("Reload requested, success={Success}", result.Success); _logger.LogInformation("Reload requested, success={Success}", result.Success);
if (result.Success)
_configSetCache.Invalidate();
return Ok(result); return Ok(result);
} }
catch (Exception ex) catch (Exception ex)
@@ -148,4 +148,63 @@ public static class DnsmasqConfKeys
// --- Niche / platform (Linux conntrack mark for UBus/query filtering) --- // --- Niche / platform (Linux conntrack mark for UBus/query filtering) ---
public const string Conntrack = "conntrack"; public const string Conntrack = "conntrack";
public const string ConnmarkAllowlistEnable = "connmark-allowlist-enable";
public const string NoRoundRobin = "no-round-robin";
public const string DnssecNoTimecheck = "dnssec-no-timecheck";
public const string DnssecDebug = "dnssec-debug";
public const string Leasequery = "leasequery";
public const string DhcpGenerateNames = "dhcp-generate-names";
public const string DhcpBroadcast = "dhcp-broadcast";
public const string DhcpSequentialIp = "dhcp-sequential-ip";
public const string DhcpIgnoreClid = "dhcp-ignore-clid";
public const string BootpDynamic = "bootp-dynamic";
public const string NoPing = "no-ping";
public const string ScriptArp = "script-arp";
public const string ScriptOnRenewal = "script-on-renewal";
public const string DhcpNoOverride = "dhcp-no-override";
public const string QuietDhcp = "quiet-dhcp";
public const string QuietDhcp6 = "quiet-dhcp6";
public const string QuietRa = "quiet-ra";
public const string QuietTftp = "quiet-tftp";
public const string DnsForwardMax = "dns-forward-max";
public const string Dumpfile = "dumpfile";
public const string Dumpmask = "dumpmask";
public const string AddCpeId = "add-cpe-id";
public const string DnssecTimestamp = "dnssec-timestamp";
public const string DnssecLimits = "dnssec-limits";
public const string DhcpAlternatePort = "dhcp-alternate-port";
public const string DhcpDuid = "dhcp-duid";
public const string DhcpLuascript = "dhcp-luascript";
public const string DhcpScriptuser = "dhcp-scriptuser";
public const string DhcpPxeVendor = "dhcp-pxe-vendor";
public const string ConnmarkAllowlist = "connmark-allowlist";
public const string CaaRecord = "caa-record";
public const string DnsRr = "dns-rr";
public const string SynthDomain = "synth-domain";
public const string AuthZone = "auth-zone";
public const string AuthSoa = "auth-soa";
public const string AuthSecServers = "auth-sec-servers";
public const string AuthPeer = "auth-peer";
public const string DhcpRelay = "dhcp-relay";
public const string DhcpCircuitid = "dhcp-circuitid";
public const string DhcpRemoteid = "dhcp-remoteid";
public const string DhcpSubscrid = "dhcp-subscrid";
public const string DhcpProxy = "dhcp-proxy";
public const string TagIf = "tag-if";
public const string BridgeInterface = "bridge-interface";
public const string SharedNetwork = "shared-network";
public const string DhcpOptionPxe = "dhcp-option-pxe";
public const string UseStaleCache = "use-stale-cache";
public const string AddMac = "add-mac";
public const string StripMac = "strip-mac";
public const string AddSubnet = "add-subnet";
public const string StripSubnet = "strip-subnet";
public const string Umbrella = "umbrella";
public const string Do0x20Encode = "do-0x20-encode";
public const string No0x20Encode = "no-0x20-encode";
} }
@@ -57,6 +57,11 @@ public static class DnsmasqOptionTooltips
[DnsmasqConfKeys.NoDhcpv6Interface] = "No DHCPv6/RA on these interfaces. Can repeat.", [DnsmasqConfKeys.NoDhcpv6Interface] = "No DHCPv6/RA on these interfaces. Can repeat.",
[DnsmasqConfKeys.Ipset] = "Add resolved IPs to ipset (domain, set names). Can repeat.", [DnsmasqConfKeys.Ipset] = "Add resolved IPs to ipset (domain, set names). Can repeat.",
[DnsmasqConfKeys.Nftset] = "Add resolved IPs to nftables sets. Can repeat.", [DnsmasqConfKeys.Nftset] = "Add resolved IPs to nftables sets. Can repeat.",
[DnsmasqConfKeys.ConnmarkAllowlistEnable] = "Use conntrack marks to allowlist which queries are forwarded (with connmark-allowlist).",
[DnsmasqConfKeys.NoRoundRobin] = "Do not rotate the order of upstream servers for each query.",
[DnsmasqConfKeys.DnsForwardMax] = "Maximum number of concurrent DNS forwards (default 150).",
[DnsmasqConfKeys.ConnmarkAllowlist] = "Conntrack marks that allowlist which queries are forwarded (with connmark-allowlist-enable). Can repeat.",
[DnsmasqConfKeys.Do0x20Encode] = "Use 0x20 encoding in queries to detect DNS poisoning (Enabled), disable it (Disabled), or default (not set).",
// --- DNS records --- // --- DNS records ---
[DnsmasqConfKeys.Domain] = "Local domain(s); optional IP. Can repeat (e.g. domain=home,192.168.1.1).", [DnsmasqConfKeys.Domain] = "Local domain(s); optional IP. Can repeat (e.g. domain=home,192.168.1.1).",
@@ -72,12 +77,33 @@ public static class DnsmasqOptionTooltips
[DnsmasqConfKeys.HostRecord] = "A/AAAA host records (name, IP). Can repeat.", [DnsmasqConfKeys.HostRecord] = "A/AAAA host records (name, IP). Can repeat.",
[DnsmasqConfKeys.DynamicHost] = "Dynamic host entries. Can repeat.", [DnsmasqConfKeys.DynamicHost] = "Dynamic host entries. Can repeat.",
[DnsmasqConfKeys.InterfaceName] = "Name per interface (for localise-queries). Can repeat.", [DnsmasqConfKeys.InterfaceName] = "Name per interface (for localise-queries). Can repeat.",
[DnsmasqConfKeys.CaaRecord] = "CAA record (domain, flags, tag, value). Can repeat.",
[DnsmasqConfKeys.DnsRr] = "Arbitrary DNS resource record. Can repeat.",
[DnsmasqConfKeys.SynthDomain] = "Synthesize answers for a domain. Can repeat.",
[DnsmasqConfKeys.AuthZone] = "Authoritative zone (domain, subnet). Can repeat.",
[DnsmasqConfKeys.AuthSoa] = "SOA record for authoritative zone. Can repeat.",
[DnsmasqConfKeys.AuthSecServers] = "Secondary servers for zone transfer. Can repeat.",
[DnsmasqConfKeys.AuthPeer] = "Peers for zone transfer. Can repeat.",
// --- DHCP --- // --- DHCP ---
[DnsmasqConfKeys.DhcpAuthoritative] = "DHCP server is authoritative; required for DHCP to work.", [DnsmasqConfKeys.DhcpAuthoritative] = "DHCP server is authoritative; required for DHCP to work.",
[DnsmasqConfKeys.DhcpRapidCommit] = "DHCPv4 Rapid Commit (RFC 4039); use only if single server or all commit.", [DnsmasqConfKeys.DhcpRapidCommit] = "DHCPv4 Rapid Commit (RFC 4039); use only if single server or all commit.",
[DnsmasqConfKeys.Leasequery] = "Respond to DHCPLEASEQUERY messages (e.g. from relay).",
[DnsmasqConfKeys.DhcpGenerateNames] = "Generate names for DHCP clients from their hostname or MAC.",
[DnsmasqConfKeys.DhcpBroadcast] = "Use broadcast to communicate with DHCP clients when unicast is not possible.",
[DnsmasqConfKeys.DhcpSequentialIp] = "Allocate DHCP addresses sequentially from the range.",
[DnsmasqConfKeys.DhcpIgnoreClid] = "Ignore client identifier (use MAC only) for DHCP.",
[DnsmasqConfKeys.BootpDynamic] = "Allow dynamic allocation for BOOTP clients.",
[DnsmasqConfKeys.NoPing] = "Do not ping the gateway before offering a DHCP lease.",
[DnsmasqConfKeys.ScriptArp] = "Call dhcp-script with extra 'arp' or 'arp-old arp-new' for ARP table updates.",
[DnsmasqConfKeys.ScriptOnRenewal] = "Call dhcp-script on lease renewal (in addition to first assignment).",
[DnsmasqConfKeys.DhcpNoOverride] = "Do not replace existing DNS/DHCP data for a client (e.g. from /etc/hosts).",
[DnsmasqConfKeys.DhcpScript] = "Script run on lease add/delete (add|del, MAC, IP, hostname).", [DnsmasqConfKeys.DhcpScript] = "Script run on lease add/delete (add|del, MAC, IP, hostname).",
[DnsmasqConfKeys.LeasefileRo] = "Do not create or truncate the DHCP lease file.", [DnsmasqConfKeys.LeasefileRo] = "Do not create or truncate the DHCP lease file.",
[DnsmasqConfKeys.DhcpAlternatePort] = "Use alternate port for DHCP (e.g. 67).",
[DnsmasqConfKeys.DhcpDuid] = "DHCP unique identifier (DUID) for the server.",
[DnsmasqConfKeys.DhcpLuascript] = "Lua script for DHCP (e.g. for custom logic).",
[DnsmasqConfKeys.DhcpScriptuser] = "User to run dhcp-script as (requires root).",
[DnsmasqConfKeys.DhcpLeasefile] = "Path to the DHCP lease file.", [DnsmasqConfKeys.DhcpLeasefile] = "Path to the DHCP lease file.",
[DnsmasqConfKeys.DhcpLeaseMax] = "Maximum number of DHCP leases to hold.", [DnsmasqConfKeys.DhcpLeaseMax] = "Maximum number of DHCP leases to hold.",
[DnsmasqConfKeys.DhcpTtl] = "TTL for DHCP names in the DNS cache.", [DnsmasqConfKeys.DhcpTtl] = "TTL for DHCP names in the DNS cache.",
@@ -98,6 +124,14 @@ public static class DnsmasqOptionTooltips
[DnsmasqConfKeys.DhcpUserclass] = "Match by DHCP user class. Can repeat.", [DnsmasqConfKeys.DhcpUserclass] = "Match by DHCP user class. Can repeat.",
[DnsmasqConfKeys.RaParam] = "Router advertisement parameters (DHCPv6/RA). Can repeat.", [DnsmasqConfKeys.RaParam] = "Router advertisement parameters (DHCPv6/RA). Can repeat.",
[DnsmasqConfKeys.Slaac] = "SLAAC host part for DHCPv6/RA. Can repeat.", [DnsmasqConfKeys.Slaac] = "SLAAC host part for DHCPv6/RA. Can repeat.",
[DnsmasqConfKeys.DhcpRelay] = "Relay DHCP to another server. Can repeat.",
[DnsmasqConfKeys.DhcpCircuitid] = "Circuit ID for DHCP relay. Can repeat.",
[DnsmasqConfKeys.DhcpRemoteid] = "Remote ID for DHCP relay. Can repeat.",
[DnsmasqConfKeys.DhcpSubscrid] = "Subscriber ID for DHCP relay. Can repeat.",
[DnsmasqConfKeys.DhcpProxy] = "DHCP proxy (relay) configuration. Can repeat.",
[DnsmasqConfKeys.TagIf] = "Set tag when request from interface. Can repeat.",
[DnsmasqConfKeys.BridgeInterface] = "Bridge interfaces for DHCP. Can repeat.",
[DnsmasqConfKeys.SharedNetwork] = "Shared network for DHCP. Can repeat.",
// --- TFTP / PXE --- // --- TFTP / PXE ---
[DnsmasqConfKeys.EnableTftp] = "Enable the built-in TFTP server.", [DnsmasqConfKeys.EnableTftp] = "Enable the built-in TFTP server.",
@@ -107,10 +141,17 @@ public static class DnsmasqOptionTooltips
[DnsmasqConfKeys.TftpRoot] = "Root directory for TFTP files.", [DnsmasqConfKeys.TftpRoot] = "Root directory for TFTP files.",
[DnsmasqConfKeys.PxePrompt] = "PXE boot prompt (e.g. timeout,0 to disable).", [DnsmasqConfKeys.PxePrompt] = "PXE boot prompt (e.g. timeout,0 to disable).",
[DnsmasqConfKeys.PxeService] = "PXE service definitions. Can repeat.", [DnsmasqConfKeys.PxeService] = "PXE service definitions. Can repeat.",
[DnsmasqConfKeys.DhcpPxeVendor] = "PXE vendor class string (e.g. for PXE matching).",
[DnsmasqConfKeys.DhcpOptionPxe] = "PXE option (e.g. option 43). Can repeat.",
// --- DNSSEC --- // --- DNSSEC ---
[DnsmasqConfKeys.Dnssec] = "Validate DNSSEC on replies from upstream.", [DnsmasqConfKeys.Dnssec] = "Validate DNSSEC on replies from upstream.",
[DnsmasqConfKeys.DnssecCheckUnsigned] = "Treat unsigned zones as bogus.", [DnsmasqConfKeys.DnssecCheckUnsigned] = "Treat unsigned zones as bogus.",
[DnsmasqConfKeys.DnssecNoTimecheck] = "Do not validate DNSSEC signatures against the current time (e.g. for testing).",
[DnsmasqConfKeys.DnssecDebug] = "Log DNSSEC validation failures at debug level.",
[DnsmasqConfKeys.AddCpeId] = "Add CPE-ID to DHCP requests (for identification).",
[DnsmasqConfKeys.DnssecTimestamp] = "Path to file for DNSSEC timestamp (e.g. for validation).",
[DnsmasqConfKeys.DnssecLimits] = "DNSSEC validation limits (e.g. max signed zones).",
[DnsmasqConfKeys.TrustAnchor] = "DNSSEC trust anchors. Can repeat.", [DnsmasqConfKeys.TrustAnchor] = "DNSSEC trust anchors. Can repeat.",
// --- Cache --- // --- Cache ---
@@ -121,6 +162,12 @@ public static class DnsmasqOptionTooltips
[DnsmasqConfKeys.MaxTtl] = "Maximum TTL for cached responses (cap on upstream TTL).", [DnsmasqConfKeys.MaxTtl] = "Maximum TTL for cached responses (cap on upstream TTL).",
[DnsmasqConfKeys.MaxCacheTtl] = "Maximum TTL for positive cache entries.", [DnsmasqConfKeys.MaxCacheTtl] = "Maximum TTL for positive cache entries.",
[DnsmasqConfKeys.MinCacheTtl] = "Minimum TTL for positive cache entries.", [DnsmasqConfKeys.MinCacheTtl] = "Minimum TTL for positive cache entries.",
[DnsmasqConfKeys.Dumpfile] = "Dump cache to this file (for debugging).",
[DnsmasqConfKeys.Dumpmask] = "Mask for cache dump (which data to include).",
[DnsmasqConfKeys.UseStaleCache] = "Use stale cache when upstream is unavailable. Off, On (key-only), or custom seconds.",
[DnsmasqConfKeys.AddMac] = "Add MAC address to DNS queries. Off, On (key-only), or custom value.",
[DnsmasqConfKeys.AddSubnet] = "Add subnet to DNS queries. Off, On (key-only), or custom value.",
[DnsmasqConfKeys.Umbrella] = "Cisco Umbrella device identity. Off, On (key-only), or custom value.",
// --- Process & networking --- // --- Process & networking ---
[DnsmasqConfKeys.NoPoll] = "Do not poll /etc/resolv.conf for changes.", [DnsmasqConfKeys.NoPoll] = "Do not poll /etc/resolv.conf for changes.",
@@ -143,6 +190,10 @@ public static class DnsmasqOptionTooltips
[DnsmasqConfKeys.KeepInForeground] = "Do not daemonise; stay in foreground (e.g. under systemd).", [DnsmasqConfKeys.KeepInForeground] = "Do not daemonise; stay in foreground (e.g. under systemd).",
[DnsmasqConfKeys.NoDaemon] = "Debug mode: no fork, no pid file, log to stderr.", [DnsmasqConfKeys.NoDaemon] = "Debug mode: no fork, no pid file, log to stderr.",
[DnsmasqConfKeys.Conntrack] = "Linux connection-tracking mark for UBus/query filtering (platform-dependent).", [DnsmasqConfKeys.Conntrack] = "Linux connection-tracking mark for UBus/query filtering (platform-dependent).",
[DnsmasqConfKeys.QuietDhcp] = "Suppress routine DHCP logging.",
[DnsmasqConfKeys.QuietDhcp6] = "Suppress routine DHCPv6 logging.",
[DnsmasqConfKeys.QuietRa] = "Suppress routine router advertisement logging.",
[DnsmasqConfKeys.QuietTftp] = "Suppress routine TFTP logging.",
}.ToFrozenDictionary(); }.ToFrozenDictionary();
/// <summary>Returns a short tooltip for the option, or null if none is defined.</summary> /// <summary>Returns a short tooltip for the option, or null if none is defined.</summary>
@@ -87,6 +87,11 @@ public static class EffectiveConfigFieldBuilder
list.AddDescriptor(registry, DnsmasqConfKeys.FastDnsRetry, status, s => Config(s)?.FastDnsRetry, s => Sources(s)?.FastDnsRetry, null); list.AddDescriptor(registry, DnsmasqConfKeys.FastDnsRetry, status, s => Config(s)?.FastDnsRetry, s => Sources(s)?.FastDnsRetry, null);
list.AddDescriptor(registry, DnsmasqConfKeys.Ipset, status, null, null, Items(ec => ec?.IpsetValues, src => src?.IpsetValues)); list.AddDescriptor(registry, DnsmasqConfKeys.Ipset, status, null, null, Items(ec => ec?.IpsetValues, src => src?.IpsetValues));
list.AddDescriptor(registry, DnsmasqConfKeys.Nftset, status, null, null, Items(ec => ec?.NftsetValues, src => src?.NftsetValues)); list.AddDescriptor(registry, DnsmasqConfKeys.Nftset, status, null, null, Items(ec => ec?.NftsetValues, src => src?.NftsetValues));
list.AddDescriptor(registry, DnsmasqConfKeys.ConnmarkAllowlistEnable, status, s => Config(s)?.ConnmarkAllowlistEnable, s => Sources(s)?.ConnmarkAllowlistEnable, null);
list.AddDescriptor(registry, DnsmasqConfKeys.NoRoundRobin, status, s => Config(s)?.NoRoundRobin, s => Sources(s)?.NoRoundRobin, null);
list.AddDescriptor(registry, DnsmasqConfKeys.DnsForwardMax, status, s => Config(s)?.DnsForwardMax, s => Sources(s)?.DnsForwardMax, null);
list.AddDescriptor(registry, DnsmasqConfKeys.ConnmarkAllowlist, status, null, null, Items(ec => ec?.ConnmarkAllowlistValues, src => src?.ConnmarkAllowlistValues));
list.AddDescriptor(registry, DnsmasqConfKeys.Do0x20Encode, status, s => Config(s)?.Do0x20EncodeState, s => Sources(s)?.Do0x20Encode, null);
// DNS records // DNS records
list.AddDescriptor(registry, DnsmasqConfKeys.Domain, status, null, null, Items(ec => ec?.DomainValues, src => src?.DomainValues)); list.AddDescriptor(registry, DnsmasqConfKeys.Domain, status, null, null, Items(ec => ec?.DomainValues, src => src?.DomainValues));
@@ -99,6 +104,13 @@ public static class EffectiveConfigFieldBuilder
list.AddDescriptor(registry, DnsmasqConfKeys.HostRecord, status, null, null, Items(ec => ec?.HostRecordValues, src => src?.HostRecordValues)); list.AddDescriptor(registry, DnsmasqConfKeys.HostRecord, status, null, null, Items(ec => ec?.HostRecordValues, src => src?.HostRecordValues));
list.AddDescriptor(registry, DnsmasqConfKeys.DynamicHost, status, null, null, Items(ec => ec?.DynamicHostValues, src => src?.DynamicHostValues)); list.AddDescriptor(registry, DnsmasqConfKeys.DynamicHost, status, null, null, Items(ec => ec?.DynamicHostValues, src => src?.DynamicHostValues));
list.AddDescriptor(registry, DnsmasqConfKeys.InterfaceName, status, null, null, Items(ec => ec?.InterfaceNameValues, src => src?.InterfaceNameValues)); list.AddDescriptor(registry, DnsmasqConfKeys.InterfaceName, status, null, null, Items(ec => ec?.InterfaceNameValues, src => src?.InterfaceNameValues));
list.AddDescriptor(registry, DnsmasqConfKeys.CaaRecord, status, null, null, Items(ec => ec?.CaaRecordValues, src => src?.CaaRecordValues));
list.AddDescriptor(registry, DnsmasqConfKeys.DnsRr, status, null, null, Items(ec => ec?.DnsRrValues, src => src?.DnsRrValues));
list.AddDescriptor(registry, DnsmasqConfKeys.SynthDomain, status, null, null, Items(ec => ec?.SynthDomainValues, src => src?.SynthDomainValues));
list.AddDescriptor(registry, DnsmasqConfKeys.AuthZone, status, null, null, Items(ec => ec?.AuthZoneValues, src => src?.AuthZoneValues));
list.AddDescriptor(registry, DnsmasqConfKeys.AuthSoa, status, null, null, Items(ec => ec?.AuthSoaValues, src => src?.AuthSoaValues));
list.AddDescriptor(registry, DnsmasqConfKeys.AuthSecServers, status, null, null, Items(ec => ec?.AuthSecServersValues, src => src?.AuthSecServersValues));
list.AddDescriptor(registry, DnsmasqConfKeys.AuthPeer, status, null, null, Items(ec => ec?.AuthPeerValues, src => src?.AuthPeerValues));
list.AddDescriptor(registry, DnsmasqConfKeys.MxTarget, status, s => Config(s)?.MxTarget, s => Sources(s)?.MxTarget, null); list.AddDescriptor(registry, DnsmasqConfKeys.MxTarget, status, s => Config(s)?.MxTarget, s => Sources(s)?.MxTarget, null);
list.AddDescriptor(registry, DnsmasqConfKeys.Localmx, status, s => Config(s)?.Localmx, s => Sources(s)?.Localmx, null); list.AddDescriptor(registry, DnsmasqConfKeys.Localmx, status, s => Config(s)?.Localmx, s => Sources(s)?.Localmx, null);
list.AddDescriptor(registry, DnsmasqConfKeys.Selfmx, status, s => Config(s)?.Selfmx, s => Sources(s)?.Selfmx, null); list.AddDescriptor(registry, DnsmasqConfKeys.Selfmx, status, s => Config(s)?.Selfmx, s => Sources(s)?.Selfmx, null);
@@ -107,7 +119,21 @@ public static class EffectiveConfigFieldBuilder
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpAuthoritative, status, s => Config(s)?.DhcpAuthoritative, s => Sources(s)?.DhcpAuthoritative, null); list.AddDescriptor(registry, DnsmasqConfKeys.DhcpAuthoritative, status, s => Config(s)?.DhcpAuthoritative, s => Sources(s)?.DhcpAuthoritative, null);
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpRapidCommit, status, s => Config(s)?.DhcpRapidCommit, s => Sources(s)?.DhcpRapidCommit, null); list.AddDescriptor(registry, DnsmasqConfKeys.DhcpRapidCommit, status, s => Config(s)?.DhcpRapidCommit, s => Sources(s)?.DhcpRapidCommit, null);
list.AddDescriptor(registry, DnsmasqConfKeys.LeasefileRo, status, s => Config(s)?.LeasefileRo, s => Sources(s)?.LeasefileRo, null); list.AddDescriptor(registry, DnsmasqConfKeys.LeasefileRo, status, s => Config(s)?.LeasefileRo, s => Sources(s)?.LeasefileRo, null);
list.AddDescriptor(registry, DnsmasqConfKeys.Leasequery, status, s => Config(s)?.Leasequery, s => Sources(s)?.Leasequery, null);
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpGenerateNames, status, s => Config(s)?.DhcpGenerateNames, s => Sources(s)?.DhcpGenerateNames, null);
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpBroadcast, status, s => Config(s)?.DhcpBroadcast, s => Sources(s)?.DhcpBroadcast, null);
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpSequentialIp, status, s => Config(s)?.DhcpSequentialIp, s => Sources(s)?.DhcpSequentialIp, null);
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpIgnoreClid, status, s => Config(s)?.DhcpIgnoreClid, s => Sources(s)?.DhcpIgnoreClid, null);
list.AddDescriptor(registry, DnsmasqConfKeys.BootpDynamic, status, s => Config(s)?.BootpDynamic, s => Sources(s)?.BootpDynamic, null);
list.AddDescriptor(registry, DnsmasqConfKeys.NoPing, status, s => Config(s)?.NoPing, s => Sources(s)?.NoPing, null);
list.AddDescriptor(registry, DnsmasqConfKeys.ScriptArp, status, s => Config(s)?.ScriptArp, s => Sources(s)?.ScriptArp, null);
list.AddDescriptor(registry, DnsmasqConfKeys.ScriptOnRenewal, status, s => Config(s)?.ScriptOnRenewal, s => Sources(s)?.ScriptOnRenewal, null);
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpNoOverride, status, s => Config(s)?.DhcpNoOverride, s => Sources(s)?.DhcpNoOverride, null);
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpScript, status, s => Config(s)?.DhcpScriptPath, s => Sources(s)?.DhcpScriptPath, null); list.AddDescriptor(registry, DnsmasqConfKeys.DhcpScript, status, s => Config(s)?.DhcpScriptPath, s => Sources(s)?.DhcpScriptPath, null);
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpAlternatePort, status, s => Config(s)?.DhcpAlternatePort, s => Sources(s)?.DhcpAlternatePort, null);
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpDuid, status, s => Config(s)?.DhcpDuid, s => Sources(s)?.DhcpDuid, null);
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpLuascript, status, s => Config(s)?.DhcpLuascriptPath, s => Sources(s)?.DhcpLuascriptPath, null);
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpScriptuser, status, s => Config(s)?.DhcpScriptuser, s => Sources(s)?.DhcpScriptuser, null);
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpLeasefile, status, s => Config(s)?.DhcpLeaseFilePath, s => Sources(s)?.DhcpLeaseFilePath, null); list.AddDescriptor(registry, DnsmasqConfKeys.DhcpLeasefile, status, s => Config(s)?.DhcpLeaseFilePath, s => Sources(s)?.DhcpLeaseFilePath, null);
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpLeaseMax, status, s => Config(s)?.DhcpLeaseMax, s => Sources(s)?.DhcpLeaseMax, null); list.AddDescriptor(registry, DnsmasqConfKeys.DhcpLeaseMax, status, s => Config(s)?.DhcpLeaseMax, s => Sources(s)?.DhcpLeaseMax, null);
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpTtl, status, s => Config(s)?.DhcpTtl, s => Sources(s)?.DhcpTtl, null); list.AddDescriptor(registry, DnsmasqConfKeys.DhcpTtl, status, s => Config(s)?.DhcpTtl, s => Sources(s)?.DhcpTtl, null);
@@ -122,6 +148,14 @@ public static class EffectiveConfigFieldBuilder
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpHostsfile, status, null, null, Items(ec => ec?.DhcpHostsfilePaths, src => src?.DhcpHostsfilePaths)); list.AddDescriptor(registry, DnsmasqConfKeys.DhcpHostsfile, status, null, null, Items(ec => ec?.DhcpHostsfilePaths, src => src?.DhcpHostsfilePaths));
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpOptsfile, status, null, null, Items(ec => ec?.DhcpOptsfilePaths, src => src?.DhcpOptsfilePaths)); list.AddDescriptor(registry, DnsmasqConfKeys.DhcpOptsfile, status, null, null, Items(ec => ec?.DhcpOptsfilePaths, src => src?.DhcpOptsfilePaths));
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpHostsdir, status, null, null, Items(ec => ec?.DhcpHostsdirPaths, src => src?.DhcpHostsdirPaths)); list.AddDescriptor(registry, DnsmasqConfKeys.DhcpHostsdir, status, null, null, Items(ec => ec?.DhcpHostsdirPaths, src => src?.DhcpHostsdirPaths));
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpRelay, status, null, null, Items(ec => ec?.DhcpRelayValues, src => src?.DhcpRelayValues));
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpCircuitid, status, null, null, Items(ec => ec?.DhcpCircuitidValues, src => src?.DhcpCircuitidValues));
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpRemoteid, status, null, null, Items(ec => ec?.DhcpRemoteidValues, src => src?.DhcpRemoteidValues));
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpSubscrid, status, null, null, Items(ec => ec?.DhcpSubscridValues, src => src?.DhcpSubscridValues));
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpProxy, status, null, null, Items(ec => ec?.DhcpProxyValues, src => src?.DhcpProxyValues));
list.AddDescriptor(registry, DnsmasqConfKeys.TagIf, status, null, null, Items(ec => ec?.TagIfValues, src => src?.TagIfValues));
list.AddDescriptor(registry, DnsmasqConfKeys.BridgeInterface, status, null, null, Items(ec => ec?.BridgeInterfaceValues, src => src?.BridgeInterfaceValues));
list.AddDescriptor(registry, DnsmasqConfKeys.SharedNetwork, status, null, null, Items(ec => ec?.SharedNetworkValues, src => src?.SharedNetworkValues));
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpBoot, status, null, null, Items(ec => ec?.DhcpBootValues, src => src?.DhcpBootValues)); list.AddDescriptor(registry, DnsmasqConfKeys.DhcpBoot, status, null, null, Items(ec => ec?.DhcpBootValues, src => src?.DhcpBootValues));
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpIgnore, status, null, null, Items(ec => ec?.DhcpIgnoreValues, src => src?.DhcpIgnoreValues)); list.AddDescriptor(registry, DnsmasqConfKeys.DhcpIgnore, status, null, null, Items(ec => ec?.DhcpIgnoreValues, src => src?.DhcpIgnoreValues));
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpVendorclass, status, null, null, Items(ec => ec?.DhcpVendorclassValues, src => src?.DhcpVendorclassValues)); list.AddDescriptor(registry, DnsmasqConfKeys.DhcpVendorclass, status, null, null, Items(ec => ec?.DhcpVendorclassValues, src => src?.DhcpVendorclassValues));
@@ -137,11 +171,18 @@ public static class EffectiveConfigFieldBuilder
list.AddDescriptor(registry, DnsmasqConfKeys.TftpRoot, status, s => Config(s)?.TftpRootPath, s => Sources(s)?.TftpRootPath, null); list.AddDescriptor(registry, DnsmasqConfKeys.TftpRoot, status, s => Config(s)?.TftpRootPath, s => Sources(s)?.TftpRootPath, null);
list.AddDescriptor(registry, DnsmasqConfKeys.PxePrompt, status, s => Config(s)?.PxePrompt, s => Sources(s)?.PxePrompt, null); list.AddDescriptor(registry, DnsmasqConfKeys.PxePrompt, status, s => Config(s)?.PxePrompt, s => Sources(s)?.PxePrompt, null);
list.AddDescriptor(registry, DnsmasqConfKeys.PxeService, status, null, null, Items(ec => ec?.PxeServiceValues, src => src?.PxeServiceValues)); list.AddDescriptor(registry, DnsmasqConfKeys.PxeService, status, null, null, Items(ec => ec?.PxeServiceValues, src => src?.PxeServiceValues));
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpPxeVendor, status, s => Config(s)?.DhcpPxeVendor, s => Sources(s)?.DhcpPxeVendor, null);
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpOptionPxe, status, null, null, Items(ec => ec?.DhcpOptionPxeValues, src => src?.DhcpOptionPxeValues));
// DNSSEC // DNSSEC
list.AddDescriptor(registry, DnsmasqConfKeys.Dnssec, status, s => Config(s)?.Dnssec, s => Sources(s)?.Dnssec, null); list.AddDescriptor(registry, DnsmasqConfKeys.Dnssec, status, s => Config(s)?.Dnssec, s => Sources(s)?.Dnssec, null);
list.AddDescriptor(registry, DnsmasqConfKeys.DnssecCheckUnsigned, status, s => Config(s)?.DnssecCheckUnsigned, s => Sources(s)?.DnssecCheckUnsigned, null); list.AddDescriptor(registry, DnsmasqConfKeys.DnssecCheckUnsigned, status, s => Config(s)?.DnssecCheckUnsigned, s => Sources(s)?.DnssecCheckUnsigned, null);
list.AddDescriptor(registry, DnsmasqConfKeys.DnssecNoTimecheck, status, s => Config(s)?.DnssecNoTimecheck, s => Sources(s)?.DnssecNoTimecheck, null);
list.AddDescriptor(registry, DnsmasqConfKeys.DnssecDebug, status, s => Config(s)?.DnssecDebug, s => Sources(s)?.DnssecDebug, null);
list.AddDescriptor(registry, DnsmasqConfKeys.ProxyDnssec, status, s => Config(s)?.ProxyDnssec, s => Sources(s)?.ProxyDnssec, null); list.AddDescriptor(registry, DnsmasqConfKeys.ProxyDnssec, status, s => Config(s)?.ProxyDnssec, s => Sources(s)?.ProxyDnssec, null);
list.AddDescriptor(registry, DnsmasqConfKeys.AddCpeId, status, s => Config(s)?.AddCpeId, s => Sources(s)?.AddCpeId, null);
list.AddDescriptor(registry, DnsmasqConfKeys.DnssecTimestamp, status, s => Config(s)?.DnssecTimestamp, s => Sources(s)?.DnssecTimestamp, null);
list.AddDescriptor(registry, DnsmasqConfKeys.DnssecLimits, status, s => Config(s)?.DnssecLimits, s => Sources(s)?.DnssecLimits, null);
list.AddDescriptor(registry, DnsmasqConfKeys.TrustAnchor, status, null, null, Items(ec => ec?.TrustAnchorValues, src => src?.TrustAnchorValues)); list.AddDescriptor(registry, DnsmasqConfKeys.TrustAnchor, status, null, null, Items(ec => ec?.TrustAnchorValues, src => src?.TrustAnchorValues));
// Cache // Cache
@@ -149,6 +190,12 @@ public static class EffectiveConfigFieldBuilder
list.AddDescriptor(registry, DnsmasqConfKeys.CacheSize, status, s => Config(s)?.CacheSize, s => Sources(s)?.CacheSize, null); list.AddDescriptor(registry, DnsmasqConfKeys.CacheSize, status, s => Config(s)?.CacheSize, s => Sources(s)?.CacheSize, null);
list.AddDescriptor(registry, DnsmasqConfKeys.LocalTtl, status, s => Config(s)?.LocalTtl, s => Sources(s)?.LocalTtl, null); list.AddDescriptor(registry, DnsmasqConfKeys.LocalTtl, status, s => Config(s)?.LocalTtl, s => Sources(s)?.LocalTtl, null);
list.AddDescriptor(registry, DnsmasqConfKeys.NoNegcache, status, s => Config(s)?.NoNegcache, s => Sources(s)?.NoNegcache, null); list.AddDescriptor(registry, DnsmasqConfKeys.NoNegcache, status, s => Config(s)?.NoNegcache, s => Sources(s)?.NoNegcache, null);
list.AddDescriptor(registry, DnsmasqConfKeys.Dumpfile, status, s => Config(s)?.DumpfilePath, s => Sources(s)?.DumpfilePath, null);
list.AddDescriptor(registry, DnsmasqConfKeys.Dumpmask, status, s => Config(s)?.Dumpmask, s => Sources(s)?.Dumpmask, null);
list.AddDescriptor(registry, DnsmasqConfKeys.UseStaleCache, status, s => Config(s)?.UseStaleCache, s => Sources(s)?.UseStaleCache, null);
list.AddDescriptor(registry, DnsmasqConfKeys.AddMac, status, s => Config(s)?.AddMac, s => Sources(s)?.AddMac, null);
list.AddDescriptor(registry, DnsmasqConfKeys.AddSubnet, status, s => Config(s)?.AddSubnet, s => Sources(s)?.AddSubnet, null);
list.AddDescriptor(registry, DnsmasqConfKeys.Umbrella, status, s => Config(s)?.Umbrella, s => Sources(s)?.Umbrella, null);
list.AddDescriptor(registry, DnsmasqConfKeys.NegTtl, status, s => Config(s)?.NegTtl, s => Sources(s)?.NegTtl, null); list.AddDescriptor(registry, DnsmasqConfKeys.NegTtl, status, s => Config(s)?.NegTtl, s => Sources(s)?.NegTtl, null);
list.AddDescriptor(registry, DnsmasqConfKeys.MaxTtl, status, s => Config(s)?.MaxTtl, s => Sources(s)?.MaxTtl, null); list.AddDescriptor(registry, DnsmasqConfKeys.MaxTtl, status, s => Config(s)?.MaxTtl, s => Sources(s)?.MaxTtl, null);
list.AddDescriptor(registry, DnsmasqConfKeys.MaxCacheTtl, status, s => Config(s)?.MaxCacheTtl, s => Sources(s)?.MaxCacheTtl, null); list.AddDescriptor(registry, DnsmasqConfKeys.MaxCacheTtl, status, s => Config(s)?.MaxCacheTtl, s => Sources(s)?.MaxCacheTtl, null);
@@ -179,6 +226,10 @@ public static class EffectiveConfigFieldBuilder
list.AddDescriptor(registry, DnsmasqConfKeys.KeepInForeground, status, s => Config(s)?.KeepInForeground, s => Sources(s)?.KeepInForeground, null); list.AddDescriptor(registry, DnsmasqConfKeys.KeepInForeground, status, s => Config(s)?.KeepInForeground, s => Sources(s)?.KeepInForeground, null);
list.AddDescriptor(registry, DnsmasqConfKeys.NoDaemon, status, s => Config(s)?.NoDaemon, s => Sources(s)?.NoDaemon, null); list.AddDescriptor(registry, DnsmasqConfKeys.NoDaemon, status, s => Config(s)?.NoDaemon, s => Sources(s)?.NoDaemon, null);
list.AddDescriptor(registry, DnsmasqConfKeys.Conntrack, status, s => Config(s)?.Conntrack, s => Sources(s)?.Conntrack, null); list.AddDescriptor(registry, DnsmasqConfKeys.Conntrack, status, s => Config(s)?.Conntrack, s => Sources(s)?.Conntrack, null);
list.AddDescriptor(registry, DnsmasqConfKeys.QuietDhcp, status, s => Config(s)?.QuietDhcp, s => Sources(s)?.QuietDhcp, null);
list.AddDescriptor(registry, DnsmasqConfKeys.QuietDhcp6, status, s => Config(s)?.QuietDhcp6, s => Sources(s)?.QuietDhcp6, null);
list.AddDescriptor(registry, DnsmasqConfKeys.QuietRa, status, s => Config(s)?.QuietRa, s => Sources(s)?.QuietRa, null);
list.AddDescriptor(registry, DnsmasqConfKeys.QuietTftp, status, s => Config(s)?.QuietTftp, s => Sources(s)?.QuietTftp, null);
return list; return list;
} }
@@ -151,6 +151,57 @@ public static class EffectiveConfigOptionKindMap
[DnsmasqConfKeys.KeepInForeground] = EffectiveConfigFieldKind.Single, [DnsmasqConfKeys.KeepInForeground] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.NoDaemon] = EffectiveConfigFieldKind.Single, [DnsmasqConfKeys.NoDaemon] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.Conntrack] = EffectiveConfigFieldKind.Single, [DnsmasqConfKeys.Conntrack] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.ConnmarkAllowlistEnable] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.NoRoundRobin] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.DnssecNoTimecheck] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.DnssecDebug] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.Leasequery] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.DhcpGenerateNames] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.DhcpBroadcast] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.DhcpSequentialIp] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.DhcpIgnoreClid] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.BootpDynamic] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.NoPing] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.ScriptArp] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.ScriptOnRenewal] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.DhcpNoOverride] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.QuietDhcp] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.QuietDhcp6] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.QuietRa] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.QuietTftp] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.DnsForwardMax] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.Dumpfile] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.Dumpmask] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.AddCpeId] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.DnssecTimestamp] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.DnssecLimits] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.DhcpAlternatePort] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.DhcpDuid] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.DhcpLuascript] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.DhcpScriptuser] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.DhcpPxeVendor] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.ConnmarkAllowlist] = EffectiveConfigFieldKind.Multi,
[DnsmasqConfKeys.CaaRecord] = EffectiveConfigFieldKind.Multi,
[DnsmasqConfKeys.DnsRr] = EffectiveConfigFieldKind.Multi,
[DnsmasqConfKeys.SynthDomain] = EffectiveConfigFieldKind.Multi,
[DnsmasqConfKeys.AuthZone] = EffectiveConfigFieldKind.Multi,
[DnsmasqConfKeys.AuthSoa] = EffectiveConfigFieldKind.Multi,
[DnsmasqConfKeys.AuthSecServers] = EffectiveConfigFieldKind.Multi,
[DnsmasqConfKeys.AuthPeer] = EffectiveConfigFieldKind.Multi,
[DnsmasqConfKeys.DhcpRelay] = EffectiveConfigFieldKind.Multi,
[DnsmasqConfKeys.DhcpCircuitid] = EffectiveConfigFieldKind.Multi,
[DnsmasqConfKeys.DhcpRemoteid] = EffectiveConfigFieldKind.Multi,
[DnsmasqConfKeys.DhcpSubscrid] = EffectiveConfigFieldKind.Multi,
[DnsmasqConfKeys.DhcpProxy] = EffectiveConfigFieldKind.Multi,
[DnsmasqConfKeys.TagIf] = EffectiveConfigFieldKind.Multi,
[DnsmasqConfKeys.BridgeInterface] = EffectiveConfigFieldKind.Multi,
[DnsmasqConfKeys.SharedNetwork] = EffectiveConfigFieldKind.Multi,
[DnsmasqConfKeys.DhcpOptionPxe] = EffectiveConfigFieldKind.Multi,
[DnsmasqConfKeys.UseStaleCache] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.AddMac] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.AddSubnet] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.Umbrella] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.Do0x20Encode] = EffectiveConfigFieldKind.Single,
}; };
/// <summary>Returns Single or Multi for the given option name; defaults to Single if unknown.</summary> /// <summary>Returns Single or Multi for the given option name; defaults to Single if unknown.</summary>
@@ -287,9 +338,107 @@ public static class EffectiveConfigParserBehaviorMap
[DnsmasqConfKeys.KeepInForeground] = EffectiveConfigParserBehavior.Flag, [DnsmasqConfKeys.KeepInForeground] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.NoDaemon] = EffectiveConfigParserBehavior.Flag, [DnsmasqConfKeys.NoDaemon] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.Conntrack] = EffectiveConfigParserBehavior.Flag, [DnsmasqConfKeys.Conntrack] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.ConnmarkAllowlistEnable] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.NoRoundRobin] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.DnssecNoTimecheck] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.DnssecDebug] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.Leasequery] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.DhcpGenerateNames] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.DhcpBroadcast] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.DhcpSequentialIp] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.DhcpIgnoreClid] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.BootpDynamic] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.NoPing] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.ScriptArp] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.ScriptOnRenewal] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.DhcpNoOverride] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.QuietDhcp] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.QuietDhcp6] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.QuietRa] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.QuietTftp] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.DnsForwardMax] = EffectiveConfigParserBehavior.LastWins,
[DnsmasqConfKeys.Dumpfile] = EffectiveConfigParserBehavior.LastWins,
[DnsmasqConfKeys.Dumpmask] = EffectiveConfigParserBehavior.LastWins,
[DnsmasqConfKeys.AddCpeId] = EffectiveConfigParserBehavior.LastWins,
[DnsmasqConfKeys.DnssecTimestamp] = EffectiveConfigParserBehavior.LastWins,
[DnsmasqConfKeys.DnssecLimits] = EffectiveConfigParserBehavior.LastWins,
[DnsmasqConfKeys.DhcpAlternatePort] = EffectiveConfigParserBehavior.LastWins,
[DnsmasqConfKeys.DhcpDuid] = EffectiveConfigParserBehavior.LastWins,
[DnsmasqConfKeys.DhcpLuascript] = EffectiveConfigParserBehavior.LastWins,
[DnsmasqConfKeys.DhcpScriptuser] = EffectiveConfigParserBehavior.LastWins,
[DnsmasqConfKeys.DhcpPxeVendor] = EffectiveConfigParserBehavior.LastWins,
[DnsmasqConfKeys.ConnmarkAllowlist] = EffectiveConfigParserBehavior.Multi,
[DnsmasqConfKeys.CaaRecord] = EffectiveConfigParserBehavior.Multi,
[DnsmasqConfKeys.DnsRr] = EffectiveConfigParserBehavior.Multi,
[DnsmasqConfKeys.SynthDomain] = EffectiveConfigParserBehavior.Multi,
[DnsmasqConfKeys.AuthZone] = EffectiveConfigParserBehavior.Multi,
[DnsmasqConfKeys.AuthSoa] = EffectiveConfigParserBehavior.Multi,
[DnsmasqConfKeys.AuthSecServers] = EffectiveConfigParserBehavior.Multi,
[DnsmasqConfKeys.AuthPeer] = EffectiveConfigParserBehavior.Multi,
[DnsmasqConfKeys.DhcpRelay] = EffectiveConfigParserBehavior.Multi,
[DnsmasqConfKeys.DhcpCircuitid] = EffectiveConfigParserBehavior.Multi,
[DnsmasqConfKeys.DhcpRemoteid] = EffectiveConfigParserBehavior.Multi,
[DnsmasqConfKeys.DhcpSubscrid] = EffectiveConfigParserBehavior.Multi,
[DnsmasqConfKeys.DhcpProxy] = EffectiveConfigParserBehavior.Multi,
[DnsmasqConfKeys.TagIf] = EffectiveConfigParserBehavior.Multi,
[DnsmasqConfKeys.BridgeInterface] = EffectiveConfigParserBehavior.Multi,
[DnsmasqConfKeys.SharedNetwork] = EffectiveConfigParserBehavior.Multi,
[DnsmasqConfKeys.DhcpOptionPxe] = EffectiveConfigParserBehavior.Multi,
[DnsmasqConfKeys.UseStaleCache] = EffectiveConfigParserBehavior.LastWins,
[DnsmasqConfKeys.AddMac] = EffectiveConfigParserBehavior.LastWins,
[DnsmasqConfKeys.AddSubnet] = EffectiveConfigParserBehavior.LastWins,
[DnsmasqConfKeys.Umbrella] = EffectiveConfigParserBehavior.LastWins,
[DnsmasqConfKeys.Do0x20Encode] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.No0x20Encode] = EffectiveConfigParserBehavior.Flag,
}; };
/// <summary>Returns LastWins, Flag, or Multi for the given option name; defaults to LastWins if unknown.</summary> /// <summary>Returns LastWins, Flag, or Multi for the given option name; defaults to LastWins if unknown.</summary>
public static EffectiveConfigParserBehavior GetBehavior(string optionName) => public static EffectiveConfigParserBehavior GetBehavior(string optionName) =>
BehaviorByOptionName.TryGetValue(optionName, out var b) ? b : EffectiveConfigParserBehavior.LastWins; BehaviorByOptionName.TryGetValue(optionName, out var b) ? b : EffectiveConfigParserBehavior.LastWins;
} }
/// <summary>
/// How to serialize an option when writing the managed config file.
/// Used by DnsmasqConfigService so KeyOnlyOrValue and InversePair options serialize correctly.
/// </summary>
public enum EffectiveConfigWriteBehavior
{
Flag,
SingleValue,
MultiValue,
KeyOnlyOrValue,
InversePair,
}
/// <summary>
/// Maps dnsmasq option names to write behavior. Options not in the map default to SingleValue
/// (or are inferred from parser behavior in the service). Add entries for KeyOnlyOrValue and InversePair options.
/// </summary>
public static class EffectiveConfigWriteBehaviorMap
{
private static readonly IReadOnlyDictionary<string, EffectiveConfigWriteBehavior> BehaviorByOptionName =
new Dictionary<string, EffectiveConfigWriteBehavior>(StringComparer.Ordinal)
{
[DnsmasqConfKeys.Conntrack] = EffectiveConfigWriteBehavior.Flag,
[DnsmasqConfKeys.UseStaleCache] = EffectiveConfigWriteBehavior.KeyOnlyOrValue,
[DnsmasqConfKeys.AddMac] = EffectiveConfigWriteBehavior.KeyOnlyOrValue,
[DnsmasqConfKeys.AddSubnet] = EffectiveConfigWriteBehavior.KeyOnlyOrValue,
[DnsmasqConfKeys.Umbrella] = EffectiveConfigWriteBehavior.KeyOnlyOrValue,
[DnsmasqConfKeys.Do0x20Encode] = EffectiveConfigWriteBehavior.InversePair,
};
/// <summary>For InversePair options: (key for "enabled", key for "disabled"). Null if not an InversePair or unknown.</summary>
private static readonly IReadOnlyDictionary<string, (string KeyA, string KeyB)> InversePairKeysByOptionName =
new Dictionary<string, (string, string)>(StringComparer.Ordinal)
{
[DnsmasqConfKeys.Do0x20Encode] = (DnsmasqConfKeys.Do0x20Encode, DnsmasqConfKeys.No0x20Encode),
};
/// <summary>Returns write behavior for the option; defaults to SingleValue if unknown.</summary>
public static EffectiveConfigWriteBehavior GetBehavior(string optionName) =>
BehaviorByOptionName.TryGetValue(optionName, out var b) ? b : EffectiveConfigWriteBehavior.SingleValue;
/// <summary>Returns the pair of config keys (enabled, disabled) for an InversePair option; null otherwise.</summary>
public static (string KeyA, string KeyB)? GetInversePairKeys(string optionName) =>
InversePairKeysByOptionName.TryGetValue(optionName, out var pair) ? pair : null;
}
@@ -67,6 +67,11 @@ public static class EffectiveConfigSections
DnsmasqConfKeys.FastDnsRetry, DnsmasqConfKeys.FastDnsRetry,
DnsmasqConfKeys.Ipset, DnsmasqConfKeys.Ipset,
DnsmasqConfKeys.Nftset, DnsmasqConfKeys.Nftset,
DnsmasqConfKeys.ConnmarkAllowlistEnable,
DnsmasqConfKeys.NoRoundRobin,
DnsmasqConfKeys.DnsForwardMax,
DnsmasqConfKeys.ConnmarkAllowlist,
DnsmasqConfKeys.Do0x20Encode,
]), ]),
new SectionDef(SectionDnsRecords, "DNS records", [ new SectionDef(SectionDnsRecords, "DNS records", [
DnsmasqConfKeys.Domain, DnsmasqConfKeys.Domain,
@@ -79,6 +84,13 @@ public static class EffectiveConfigSections
DnsmasqConfKeys.HostRecord, DnsmasqConfKeys.HostRecord,
DnsmasqConfKeys.DynamicHost, DnsmasqConfKeys.DynamicHost,
DnsmasqConfKeys.InterfaceName, DnsmasqConfKeys.InterfaceName,
DnsmasqConfKeys.CaaRecord,
DnsmasqConfKeys.DnsRr,
DnsmasqConfKeys.SynthDomain,
DnsmasqConfKeys.AuthZone,
DnsmasqConfKeys.AuthSoa,
DnsmasqConfKeys.AuthSecServers,
DnsmasqConfKeys.AuthPeer,
DnsmasqConfKeys.MxTarget, DnsmasqConfKeys.MxTarget,
DnsmasqConfKeys.Localmx, DnsmasqConfKeys.Localmx,
DnsmasqConfKeys.Selfmx, DnsmasqConfKeys.Selfmx,
@@ -87,7 +99,21 @@ public static class EffectiveConfigSections
DnsmasqConfKeys.DhcpAuthoritative, DnsmasqConfKeys.DhcpAuthoritative,
DnsmasqConfKeys.DhcpRapidCommit, DnsmasqConfKeys.DhcpRapidCommit,
DnsmasqConfKeys.LeasefileRo, DnsmasqConfKeys.LeasefileRo,
DnsmasqConfKeys.Leasequery,
DnsmasqConfKeys.DhcpGenerateNames,
DnsmasqConfKeys.DhcpBroadcast,
DnsmasqConfKeys.DhcpSequentialIp,
DnsmasqConfKeys.DhcpIgnoreClid,
DnsmasqConfKeys.BootpDynamic,
DnsmasqConfKeys.NoPing,
DnsmasqConfKeys.ScriptArp,
DnsmasqConfKeys.ScriptOnRenewal,
DnsmasqConfKeys.DhcpNoOverride,
DnsmasqConfKeys.DhcpScript, DnsmasqConfKeys.DhcpScript,
DnsmasqConfKeys.DhcpAlternatePort,
DnsmasqConfKeys.DhcpDuid,
DnsmasqConfKeys.DhcpLuascript,
DnsmasqConfKeys.DhcpScriptuser,
DnsmasqConfKeys.DhcpLeasefile, DnsmasqConfKeys.DhcpLeasefile,
DnsmasqConfKeys.DhcpLeaseMax, DnsmasqConfKeys.DhcpLeaseMax,
DnsmasqConfKeys.DhcpTtl, DnsmasqConfKeys.DhcpTtl,
@@ -106,6 +132,14 @@ public static class EffectiveConfigSections
DnsmasqConfKeys.DhcpIgnore, DnsmasqConfKeys.DhcpIgnore,
DnsmasqConfKeys.DhcpVendorclass, DnsmasqConfKeys.DhcpVendorclass,
DnsmasqConfKeys.DhcpUserclass, DnsmasqConfKeys.DhcpUserclass,
DnsmasqConfKeys.DhcpRelay,
DnsmasqConfKeys.DhcpCircuitid,
DnsmasqConfKeys.DhcpRemoteid,
DnsmasqConfKeys.DhcpSubscrid,
DnsmasqConfKeys.DhcpProxy,
DnsmasqConfKeys.TagIf,
DnsmasqConfKeys.BridgeInterface,
DnsmasqConfKeys.SharedNetwork,
DnsmasqConfKeys.RaParam, DnsmasqConfKeys.RaParam,
DnsmasqConfKeys.Slaac, DnsmasqConfKeys.Slaac,
]), ]),
@@ -117,11 +151,18 @@ public static class EffectiveConfigSections
DnsmasqConfKeys.TftpRoot, DnsmasqConfKeys.TftpRoot,
DnsmasqConfKeys.PxePrompt, DnsmasqConfKeys.PxePrompt,
DnsmasqConfKeys.PxeService, DnsmasqConfKeys.PxeService,
DnsmasqConfKeys.DhcpPxeVendor,
DnsmasqConfKeys.DhcpOptionPxe,
]), ]),
new SectionDef(SectionDnssec, "DNSSEC", [ new SectionDef(SectionDnssec, "DNSSEC", [
DnsmasqConfKeys.Dnssec, DnsmasqConfKeys.Dnssec,
DnsmasqConfKeys.DnssecCheckUnsigned, DnsmasqConfKeys.DnssecCheckUnsigned,
DnsmasqConfKeys.DnssecNoTimecheck,
DnsmasqConfKeys.DnssecDebug,
DnsmasqConfKeys.ProxyDnssec, DnsmasqConfKeys.ProxyDnssec,
DnsmasqConfKeys.AddCpeId,
DnsmasqConfKeys.DnssecTimestamp,
DnsmasqConfKeys.DnssecLimits,
DnsmasqConfKeys.TrustAnchor, DnsmasqConfKeys.TrustAnchor,
]), ]),
new SectionDef(SectionCache, "Cache", [ new SectionDef(SectionCache, "Cache", [
@@ -133,6 +174,12 @@ public static class EffectiveConfigSections
DnsmasqConfKeys.MaxTtl, DnsmasqConfKeys.MaxTtl,
DnsmasqConfKeys.MaxCacheTtl, DnsmasqConfKeys.MaxCacheTtl,
DnsmasqConfKeys.MinCacheTtl, DnsmasqConfKeys.MinCacheTtl,
DnsmasqConfKeys.Dumpfile,
DnsmasqConfKeys.Dumpmask,
DnsmasqConfKeys.UseStaleCache,
DnsmasqConfKeys.AddMac,
DnsmasqConfKeys.AddSubnet,
DnsmasqConfKeys.Umbrella,
]), ]),
new SectionDef(SectionProcess, "Process & networking", [ new SectionDef(SectionProcess, "Process & networking", [
DnsmasqConfKeys.NoPoll, DnsmasqConfKeys.NoPoll,
@@ -159,6 +206,10 @@ public static class EffectiveConfigSections
DnsmasqConfKeys.KeepInForeground, DnsmasqConfKeys.KeepInForeground,
DnsmasqConfKeys.NoDaemon, DnsmasqConfKeys.NoDaemon,
DnsmasqConfKeys.Conntrack, DnsmasqConfKeys.Conntrack,
DnsmasqConfKeys.QuietDhcp,
DnsmasqConfKeys.QuietDhcp6,
DnsmasqConfKeys.QuietRa,
DnsmasqConfKeys.QuietTftp,
]), ]),
]; ];
@@ -293,6 +293,23 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
var dhcpHostsfilePaths = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpHostsfile); var dhcpHostsfilePaths = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpHostsfile);
var dhcpOptsfilePaths = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpOptsfile); var dhcpOptsfilePaths = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpOptsfile);
var dhcpHostsdirPaths = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpHostsdir); var dhcpHostsdirPaths = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpHostsdir);
var connmarkAllowlistValues = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.ConnmarkAllowlist);
var caaRecordValues = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.CaaRecord);
var dnsRrValues = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DnsRr);
var synthDomainValues = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.SynthDomain);
var authZoneValues = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.AuthZone);
var authSoaValues = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.AuthSoa);
var authSecServersValues = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.AuthSecServers);
var authPeerValues = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.AuthPeer);
var dhcpRelayValues = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpRelay);
var dhcpCircuitidValues = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpCircuitid);
var dhcpRemoteidValues = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpRemoteid);
var dhcpSubscridValues = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpSubscrid);
var dhcpProxyValues = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpProxy);
var tagIfValues = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.TagIf);
var bridgeInterfaceValues = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.BridgeInterface);
var sharedNetworkValues = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.SharedNetwork);
var dhcpOptionPxeValues = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpOptionPxe);
var expandHosts = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.ExpandHosts); var expandHosts = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.ExpandHosts);
var bogusPriv = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.BogusPriv); var bogusPriv = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.BogusPriv);
@@ -330,6 +347,24 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
var keepInForeground = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.KeepInForeground); var keepInForeground = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.KeepInForeground);
var noDaemon = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.NoDaemon); var noDaemon = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.NoDaemon);
var proxyDnssec = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.ProxyDnssec); var proxyDnssec = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.ProxyDnssec);
var connmarkAllowlistEnable = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.ConnmarkAllowlistEnable);
var noRoundRobin = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.NoRoundRobin);
var dnssecNoTimecheck = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DnssecNoTimecheck);
var dnssecDebug = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DnssecDebug);
var leasequery = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.Leasequery);
var dhcpGenerateNames = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpGenerateNames);
var dhcpBroadcast = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpBroadcast);
var dhcpSequentialIp = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpSequentialIp);
var dhcpIgnoreClid = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpIgnoreClid);
var bootpDynamic = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.BootpDynamic);
var noPing = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.NoPing);
var scriptArp = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.ScriptArp);
var scriptOnRenewal = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.ScriptOnRenewal);
var dhcpNoOverride = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpNoOverride);
var quietDhcp = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.QuietDhcp);
var quietDhcp6 = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.QuietDhcp6);
var quietRa = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.QuietRa);
var quietTftp = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.QuietTftp);
var dhcpLeaseFilePath = DnsmasqConfIncludeParser.GetDhcpLeaseFilePathFromConfigFiles(paths, pathToLines); var dhcpLeaseFilePath = DnsmasqConfIncludeParser.GetDhcpLeaseFilePathFromConfigFiles(paths, pathToLines);
@@ -391,6 +426,39 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
var dhcpScriptPath = string.IsNullOrWhiteSpace(dhcpScriptVal) ? null : DnsmasqConfIncludeParser.ResolvePath(dhcpScriptVal?.Trim(), dhcpScriptDir); var dhcpScriptPath = string.IsNullOrWhiteSpace(dhcpScriptVal) ? null : DnsmasqConfIncludeParser.ResolvePath(dhcpScriptVal?.Trim(), dhcpScriptDir);
var (mxTargetVal, _) = ((string?, string?))ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.MxTarget); var (mxTargetVal, _) = ((string?, string?))ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.MxTarget);
var mxTarget = string.IsNullOrWhiteSpace(mxTargetVal) ? null : mxTargetVal.Trim(); var mxTarget = string.IsNullOrWhiteSpace(mxTargetVal) ? null : mxTargetVal.Trim();
var (dnsForwardMaxVal, _) = ((string?, string?))ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DnsForwardMax);
var dnsForwardMax = TryParseInt(dnsForwardMaxVal);
var (dumpfileVal, dumpfileDir) = ((string?, string?))ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.Dumpfile);
var dumpfilePath = string.IsNullOrWhiteSpace(dumpfileVal) ? null : DnsmasqConfIncludeParser.ResolvePath(dumpfileVal.Trim(), dumpfileDir);
var (dumpmaskVal, _) = ((string?, string?))ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.Dumpmask);
var dumpmask = string.IsNullOrWhiteSpace(dumpmaskVal) ? null : dumpmaskVal.Trim();
var (addCpeIdVal, _) = ((string?, string?))ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.AddCpeId);
var addCpeId = string.IsNullOrWhiteSpace(addCpeIdVal) ? null : addCpeIdVal.Trim();
var (dnssecTimestampVal, _) = ((string?, string?))ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DnssecTimestamp);
var dnssecTimestamp = string.IsNullOrWhiteSpace(dnssecTimestampVal) ? null : dnssecTimestampVal.Trim();
var (dnssecLimitsVal, _) = ((string?, string?))ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DnssecLimits);
var dnssecLimits = string.IsNullOrWhiteSpace(dnssecLimitsVal) ? null : dnssecLimitsVal.Trim();
var (dhcpAlternatePortVal, _) = ((string?, string?))ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpAlternatePort);
var dhcpAlternatePort = string.IsNullOrWhiteSpace(dhcpAlternatePortVal) ? null : dhcpAlternatePortVal.Trim();
var (dhcpDuidVal, _) = ((string?, string?))ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpDuid);
var dhcpDuid = string.IsNullOrWhiteSpace(dhcpDuidVal) ? null : dhcpDuidVal.Trim();
var (dhcpLuascriptVal, dhcpLuascriptDir) = ((string?, string?))ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpLuascript);
var dhcpLuascriptPath = string.IsNullOrWhiteSpace(dhcpLuascriptVal) ? null : DnsmasqConfIncludeParser.ResolvePath(dhcpLuascriptVal?.Trim(), dhcpLuascriptDir);
var (dhcpScriptuserVal, _) = ((string?, string?))ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpScriptuser);
var dhcpScriptuser = string.IsNullOrWhiteSpace(dhcpScriptuserVal) ? null : dhcpScriptuserVal.Trim();
var (dhcpPxeVendorVal, _) = ((string?, string?))ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpPxeVendor);
var dhcpPxeVendor = string.IsNullOrWhiteSpace(dhcpPxeVendorVal) ? null : dhcpPxeVendorVal.Trim();
var (useStaleCacheVal, _) = ((string?, string?))ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.UseStaleCache);
var useStaleCache = useStaleCacheVal == null ? null : (string.IsNullOrWhiteSpace(useStaleCacheVal) ? "" : useStaleCacheVal.Trim());
var (addMacVal, _) = ((string?, string?))ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.AddMac);
var addMac = addMacVal == null ? null : (string.IsNullOrWhiteSpace(addMacVal) ? "" : addMacVal.Trim());
var (addSubnetVal, _) = ((string?, string?))ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.AddSubnet);
var addSubnet = addSubnetVal == null ? null : (string.IsNullOrWhiteSpace(addSubnetVal) ? "" : addSubnetVal.Trim());
var (umbrellaVal, _) = ((string?, string?))ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.Umbrella);
var umbrella = umbrellaVal == null ? null : (string.IsNullOrWhiteSpace(umbrellaVal) ? "" : umbrellaVal.Trim());
var do0x20 = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.Do0x20Encode);
var no0x20 = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.No0x20Encode);
var do0x20EncodeState = do0x20 ? ExplicitToggleState.Enabled : (no0x20 ? ExplicitToggleState.Disabled : ExplicitToggleState.Default);
var conntrack = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.Conntrack); var conntrack = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.Conntrack);
return new EffectiveDnsmasqConfig( return new EffectiveDnsmasqConfig(
@@ -401,14 +469,18 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
domainValues, cnameValues, mxHostValues, srvValues, ptrRecordValues, txtRecordValues, naptrRecordValues, hostRecordValues, dynamicHostValues, interfaceNameValues, domainValues, cnameValues, mxHostValues, srvValues, ptrRecordValues, txtRecordValues, naptrRecordValues, hostRecordValues, dynamicHostValues, interfaceNameValues,
dhcpOptionForceLines, ipsetValues, nftsetValues, dhcpMacValues, dhcpNameMatchValues, dhcpIgnoreNamesValues, dhcpOptionForceLines, ipsetValues, nftsetValues, dhcpMacValues, dhcpNameMatchValues, dhcpIgnoreNamesValues,
dhcpHostsfilePaths, dhcpOptsfilePaths, dhcpHostsdirPaths, dhcpHostsfilePaths, dhcpOptsfilePaths, dhcpHostsdirPaths,
connmarkAllowlistValues, caaRecordValues, dnsRrValues, synthDomainValues, authZoneValues, authSoaValues, authSecServersValues, authPeerValues,
dhcpRelayValues, dhcpCircuitidValues, dhcpRemoteidValues, dhcpSubscridValues, dhcpProxyValues, tagIfValues, bridgeInterfaceValues, sharedNetworkValues, dhcpOptionPxeValues,
expandHosts, bogusPriv, strictOrder, allServers, noResolv, domainNeeded, noPoll, bindInterfaces, bindDynamic, noNegcache, expandHosts, bogusPriv, strictOrder, allServers, noResolv, domainNeeded, noPoll, bindInterfaces, bindDynamic, noNegcache,
dnsLoopDetect, stopDnsRebind, rebindLocalhostOk, clearOnReload, filterwin2k, filterA, filterAaaa, localiseQueries, logDebug, dhcpAuthoritative, leasefileRo, dnsLoopDetect, stopDnsRebind, rebindLocalhostOk, clearOnReload, filterwin2k, filterA, filterAaaa, localiseQueries, logDebug, dhcpAuthoritative, leasefileRo,
enableTftp, tftpSecure, tftpNoFail, tftpNoBlocksize, dnssec, dnssecCheckUnsigned, readEthers, dhcpRapidCommit, localmx, selfmx, enableRa, logDhcp, enableTftp, tftpSecure, tftpNoFail, tftpNoBlocksize, dnssec, dnssecCheckUnsigned, readEthers, dhcpRapidCommit, localmx, selfmx, enableRa, logDhcp,
keepInForeground, noDaemon, proxyDnssec, keepInForeground, noDaemon, proxyDnssec,
connmarkAllowlistEnable, noRoundRobin, dnssecNoTimecheck, dnssecDebug, leasequery, dhcpGenerateNames, dhcpBroadcast, dhcpSequentialIp, dhcpIgnoreClid, bootpDynamic, noPing, scriptArp, scriptOnRenewal, dhcpNoOverride, quietDhcp, quietDhcp6, quietRa, quietTftp,
dhcpLeaseFilePath, cacheSize, port, localTtl, pidFilePath, user, group, logFacility, logQueries, dhcpLeaseFilePath, cacheSize, port, localTtl, pidFilePath, user, group, logFacility, logQueries,
authTtl, ednsPacketMax, queryPort, portLimit, minPort, maxPort, logAsync, localService, dhcpLeaseMax, authTtl, ednsPacketMax, queryPort, portLimit, minPort, maxPort, logAsync, localService, dhcpLeaseMax,
negTtl, maxTtl, maxCacheTtl, minCacheTtl, dhcpTtl, tftpRootPath, pxePrompt, enableDbus, enableUbus, fastDnsRetry, negTtl, maxTtl, maxCacheTtl, minCacheTtl, dhcpTtl, tftpRootPath, pxePrompt, enableDbus, enableUbus, fastDnsRetry,
dhcpScriptPath, mxTarget, conntrack dhcpScriptPath, mxTarget, dnsForwardMax, dumpfilePath, dumpmask, addCpeId, dnssecTimestamp, dnssecLimits, dhcpAlternatePort, dhcpDuid, dhcpLuascriptPath, dhcpScriptuser, dhcpPxeVendor,
useStaleCache, addMac, addSubnet, umbrella, do0x20EncodeState, conntrack
); );
} }
@@ -466,6 +538,23 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
var dhcpHostsfileWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpHostsfile, managedFilePath); var dhcpHostsfileWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpHostsfile, managedFilePath);
var dhcpOptsfileWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpOptsfile, managedFilePath); var dhcpOptsfileWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpOptsfile, managedFilePath);
var dhcpHostsdirWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpHostsdir, managedFilePath); var dhcpHostsdirWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpHostsdir, managedFilePath);
var connmarkAllowlistWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.ConnmarkAllowlist, managedFilePath);
var caaRecordWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.CaaRecord, managedFilePath);
var dnsRrWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DnsRr, managedFilePath);
var synthDomainWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.SynthDomain, managedFilePath);
var authZoneWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.AuthZone, managedFilePath);
var authSoaWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.AuthSoa, managedFilePath);
var authSecServersWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.AuthSecServers, managedFilePath);
var authPeerWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.AuthPeer, managedFilePath);
var dhcpRelayWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpRelay, managedFilePath);
var dhcpCircuitidWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpCircuitid, managedFilePath);
var dhcpRemoteidWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpRemoteid, managedFilePath);
var dhcpSubscridWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpSubscrid, managedFilePath);
var dhcpProxyWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpProxy, managedFilePath);
var tagIfWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.TagIf, managedFilePath);
var bridgeInterfaceWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.BridgeInterface, managedFilePath);
var sharedNetworkWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.SharedNetwork, managedFilePath);
var dhcpOptionPxeWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpOptionPxe, managedFilePath);
var (_, expandHostsSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.ExpandHosts, managedFilePath); var (_, expandHostsSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.ExpandHosts, managedFilePath);
var (_, bogusPrivSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.BogusPriv, managedFilePath); var (_, bogusPrivSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.BogusPriv, managedFilePath);
@@ -503,6 +592,24 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
var (_, keepInForegroundSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.KeepInForeground, managedFilePath); var (_, keepInForegroundSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.KeepInForeground, managedFilePath);
var (_, noDaemonSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.NoDaemon, managedFilePath); var (_, noDaemonSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.NoDaemon, managedFilePath);
var (_, proxyDnssecSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.ProxyDnssec, managedFilePath); var (_, proxyDnssecSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.ProxyDnssec, managedFilePath);
var (_, connmarkAllowlistEnableSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.ConnmarkAllowlistEnable, managedFilePath);
var (_, noRoundRobinSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.NoRoundRobin, managedFilePath);
var (_, dnssecNoTimecheckSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DnssecNoTimecheck, managedFilePath);
var (_, dnssecDebugSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DnssecDebug, managedFilePath);
var (_, leasequerySource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.Leasequery, managedFilePath);
var (_, dhcpGenerateNamesSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpGenerateNames, managedFilePath);
var (_, dhcpBroadcastSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpBroadcast, managedFilePath);
var (_, dhcpSequentialIpSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpSequentialIp, managedFilePath);
var (_, dhcpIgnoreClidSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpIgnoreClid, managedFilePath);
var (_, bootpDynamicSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.BootpDynamic, managedFilePath);
var (_, noPingSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.NoPing, managedFilePath);
var (_, scriptArpSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.ScriptArp, managedFilePath);
var (_, scriptOnRenewalSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.ScriptOnRenewal, managedFilePath);
var (_, dhcpNoOverrideSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpNoOverride, managedFilePath);
var (_, quietDhcpSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.QuietDhcp, managedFilePath);
var (_, quietDhcp6Source) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.QuietDhcp6, managedFilePath);
var (_, quietRaSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.QuietRa, managedFilePath);
var (_, quietTftpSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.QuietTftp, managedFilePath);
var (_, dhcpLeaseFilePathSource) = DnsmasqConfIncludeParser.GetDhcpLeaseFilePathFromConfigFilesWithSource(paths, pathToLines, managedFilePath); var (_, dhcpLeaseFilePathSource) = DnsmasqConfIncludeParser.GetDhcpLeaseFilePathFromConfigFilesWithSource(paths, pathToLines, managedFilePath);
var (_, cacheSizeSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.CacheSize, managedFilePath); var (_, cacheSizeSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.CacheSize, managedFilePath);
@@ -534,6 +641,24 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
var (_, fastDnsRetrySource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.FastDnsRetry, managedFilePath); var (_, fastDnsRetrySource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.FastDnsRetry, managedFilePath);
var (_, dhcpScriptPathSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpScript, managedFilePath); var (_, dhcpScriptPathSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpScript, managedFilePath);
var (_, mxTargetSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.MxTarget, managedFilePath); var (_, mxTargetSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.MxTarget, managedFilePath);
var (_, dnsForwardMaxSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DnsForwardMax, managedFilePath);
var (_, dumpfilePathSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.Dumpfile, managedFilePath);
var (_, dumpmaskSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.Dumpmask, managedFilePath);
var (_, addCpeIdSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.AddCpeId, managedFilePath);
var (_, dnssecTimestampSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DnssecTimestamp, managedFilePath);
var (_, dnssecLimitsSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DnssecLimits, managedFilePath);
var (_, dhcpAlternatePortSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpAlternatePort, managedFilePath);
var (_, dhcpDuidSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpDuid, managedFilePath);
var (_, dhcpLuascriptPathSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpLuascript, managedFilePath);
var (_, dhcpScriptuserSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpScriptuser, managedFilePath);
var (_, dhcpPxeVendorSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpPxeVendor, managedFilePath);
var (_, useStaleCacheSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.UseStaleCache, managedFilePath);
var (_, addMacSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.AddMac, managedFilePath);
var (_, addSubnetSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.AddSubnet, managedFilePath);
var (_, umbrellaSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.Umbrella, managedFilePath);
var (do0x20Set, do0x20EncodeSourceA) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.Do0x20Encode, managedFilePath);
var (no0x20Set, no0x20EncodeSourceB) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.No0x20Encode, managedFilePath);
var do0x20EncodeSource = do0x20Set ? do0x20EncodeSourceA : (no0x20Set ? no0x20EncodeSourceB : null);
var (_, conntrackSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.Conntrack, managedFilePath); var (_, conntrackSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.Conntrack, managedFilePath);
return new EffectiveConfigSources( return new EffectiveConfigSources(
@@ -587,6 +712,23 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
dhcpHostsfileWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(), dhcpHostsfileWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
dhcpOptsfileWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(), dhcpOptsfileWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
dhcpHostsdirWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(), dhcpHostsdirWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
connmarkAllowlistWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
caaRecordWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
dnsRrWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
synthDomainWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
authZoneWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
authSoaWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
authSecServersWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
authPeerWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
dhcpRelayWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
dhcpCircuitidWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
dhcpRemoteidWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
dhcpSubscridWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
dhcpProxyWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
tagIfWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
bridgeInterfaceWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
sharedNetworkWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
dhcpOptionPxeWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
expandHostsSource, expandHostsSource,
bogusPrivSource, bogusPrivSource,
strictOrderSource, strictOrderSource,
@@ -623,6 +765,24 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
keepInForegroundSource, keepInForegroundSource,
noDaemonSource, noDaemonSource,
proxyDnssecSource, proxyDnssecSource,
connmarkAllowlistEnableSource,
noRoundRobinSource,
dnssecNoTimecheckSource,
dnssecDebugSource,
leasequerySource,
dhcpGenerateNamesSource,
dhcpBroadcastSource,
dhcpSequentialIpSource,
dhcpIgnoreClidSource,
bootpDynamicSource,
noPingSource,
scriptArpSource,
scriptOnRenewalSource,
dhcpNoOverrideSource,
quietDhcpSource,
quietDhcp6Source,
quietRaSource,
quietTftpSource,
dhcpLeaseFilePathSource, dhcpLeaseFilePathSource,
cacheSizeSource, cacheSizeSource,
portSource, portSource,
@@ -653,6 +813,22 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
fastDnsRetrySource, fastDnsRetrySource,
dhcpScriptPathSource, dhcpScriptPathSource,
mxTargetSource, mxTargetSource,
dnsForwardMaxSource,
dumpfilePathSource,
dumpmaskSource,
addCpeIdSource,
dnssecTimestampSource,
dnssecLimitsSource,
dhcpAlternatePortSource,
dhcpDuidSource,
dhcpLuascriptPathSource,
dhcpScriptuserSource,
dhcpPxeVendorSource,
useStaleCacheSource,
addMacSource,
addSubnetSource,
umbrellaSource,
do0x20EncodeSource,
conntrackSource conntrackSource
); );
} }
@@ -707,17 +883,23 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
DhcpOptionForceLines: Array.Empty<string>(), IpsetValues: Array.Empty<string>(), NftsetValues: Array.Empty<string>(), DhcpOptionForceLines: Array.Empty<string>(), IpsetValues: Array.Empty<string>(), NftsetValues: Array.Empty<string>(),
DhcpMacValues: Array.Empty<string>(), DhcpNameMatchValues: Array.Empty<string>(), DhcpIgnoreNamesValues: Array.Empty<string>(), DhcpMacValues: Array.Empty<string>(), DhcpNameMatchValues: Array.Empty<string>(), DhcpIgnoreNamesValues: Array.Empty<string>(),
DhcpHostsfilePaths: Array.Empty<string>(), DhcpOptsfilePaths: Array.Empty<string>(), DhcpHostsdirPaths: Array.Empty<string>(), DhcpHostsfilePaths: Array.Empty<string>(), DhcpOptsfilePaths: Array.Empty<string>(), DhcpHostsdirPaths: Array.Empty<string>(),
ConnmarkAllowlistValues: Array.Empty<string>(), CaaRecordValues: Array.Empty<string>(), DnsRrValues: Array.Empty<string>(), SynthDomainValues: Array.Empty<string>(),
AuthZoneValues: Array.Empty<string>(), AuthSoaValues: Array.Empty<string>(), AuthSecServersValues: Array.Empty<string>(), AuthPeerValues: Array.Empty<string>(),
DhcpRelayValues: Array.Empty<string>(), DhcpCircuitidValues: Array.Empty<string>(), DhcpRemoteidValues: Array.Empty<string>(), DhcpSubscridValues: Array.Empty<string>(), DhcpProxyValues: Array.Empty<string>(),
TagIfValues: Array.Empty<string>(), BridgeInterfaceValues: Array.Empty<string>(), SharedNetworkValues: Array.Empty<string>(), DhcpOptionPxeValues: Array.Empty<string>(),
ExpandHosts: false, BogusPriv: false, StrictOrder: false, AllServers: false, NoResolv: false, DomainNeeded: false, NoPoll: false, ExpandHosts: false, BogusPriv: false, StrictOrder: false, AllServers: false, NoResolv: false, DomainNeeded: false, NoPoll: false,
BindInterfaces: false, BindDynamic: false, NoNegcache: false, DnsLoopDetect: false, StopDnsRebind: false, RebindLocalhostOk: false, ClearOnReload: false, BindInterfaces: false, BindDynamic: false, NoNegcache: false, DnsLoopDetect: false, StopDnsRebind: false, RebindLocalhostOk: false, ClearOnReload: false,
Filterwin2k: false, FilterA: false, FilterAaaa: false, LocaliseQueries: false, LogDebug: false, DhcpAuthoritative: false, LeasefileRo: false, Filterwin2k: false, FilterA: false, FilterAaaa: false, LocaliseQueries: false, LogDebug: false, DhcpAuthoritative: false, LeasefileRo: false,
EnableTftp: false, TftpSecure: false, TftpNoFail: false, TftpNoBlocksize: false, Dnssec: false, DnssecCheckUnsigned: false, EnableTftp: false, TftpSecure: false, TftpNoFail: false, TftpNoBlocksize: false, Dnssec: false, DnssecCheckUnsigned: false,
ReadEthers: false, DhcpRapidCommit: false, Localmx: false, Selfmx: false, EnableRa: false, LogDhcp: false, ReadEthers: false, DhcpRapidCommit: false, Localmx: false, Selfmx: false, EnableRa: false, LogDhcp: false,
KeepInForeground: false, NoDaemon: false, ProxyDnssec: false, KeepInForeground: false, NoDaemon: false, ProxyDnssec: false,
ConnmarkAllowlistEnable: false, NoRoundRobin: false, DnssecNoTimecheck: false, DnssecDebug: false, Leasequery: false, DhcpGenerateNames: false, DhcpBroadcast: false, DhcpSequentialIp: false, DhcpIgnoreClid: false, BootpDynamic: false, NoPing: false, ScriptArp: false, ScriptOnRenewal: false, DhcpNoOverride: false, QuietDhcp: false, QuietDhcp6: false, QuietRa: false, QuietTftp: false,
DhcpLeaseFilePath: null, CacheSize: null, Port: null, LocalTtl: null, PidFilePath: null, User: null, Group: null, DhcpLeaseFilePath: null, CacheSize: null, Port: null, LocalTtl: null, PidFilePath: null, User: null, Group: null,
LogFacility: null, LogQueries: null, AuthTtl: null, EdnsPacketMax: null, QueryPort: null, PortLimit: null, MinPort: null, MaxPort: null, LogAsync: null, LocalService: null, LogFacility: null, LogQueries: null, AuthTtl: null, EdnsPacketMax: null, QueryPort: null, PortLimit: null, MinPort: null, MaxPort: null, LogAsync: null, LocalService: null,
DhcpLeaseMax: null, NegTtl: null, MaxTtl: null, MaxCacheTtl: null, MinCacheTtl: null, DhcpTtl: null, DhcpLeaseMax: null, NegTtl: null, MaxTtl: null, MaxCacheTtl: null, MinCacheTtl: null, DhcpTtl: null,
TftpRootPath: null, PxePrompt: null, EnableDbus: null, EnableUbus: null, FastDnsRetry: null, TftpRootPath: null, PxePrompt: null, EnableDbus: null, EnableUbus: null, FastDnsRetry: null,
DhcpScriptPath: null, MxTarget: null, Conntrack: false DhcpScriptPath: null, MxTarget: null, DnsForwardMax: null, DumpfilePath: null, Dumpmask: null, AddCpeId: null, DnssecTimestamp: null, DnssecLimits: null, DhcpAlternatePort: null, DhcpDuid: null, DhcpLuascriptPath: null, DhcpScriptuser: null, DhcpPxeVendor: null,
UseStaleCache: null, AddMac: null, AddSubnet: null, Umbrella: null, Do0x20EncodeState: ExplicitToggleState.Default, Conntrack: false
); );
private static EffectiveConfigSources CreateDefaultEffectiveConfigSources() => private static EffectiveConfigSources CreateDefaultEffectiveConfigSources() =>
@@ -742,17 +924,23 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
DhcpOptionForceLines: Array.Empty<ValueWithSource>(), IpsetValues: Array.Empty<ValueWithSource>(), NftsetValues: Array.Empty<ValueWithSource>(), DhcpOptionForceLines: Array.Empty<ValueWithSource>(), IpsetValues: Array.Empty<ValueWithSource>(), NftsetValues: Array.Empty<ValueWithSource>(),
DhcpMacValues: Array.Empty<ValueWithSource>(), DhcpNameMatchValues: Array.Empty<ValueWithSource>(), DhcpIgnoreNamesValues: Array.Empty<ValueWithSource>(), DhcpMacValues: Array.Empty<ValueWithSource>(), DhcpNameMatchValues: Array.Empty<ValueWithSource>(), DhcpIgnoreNamesValues: Array.Empty<ValueWithSource>(),
DhcpHostsfilePaths: Array.Empty<ValueWithSource>(), DhcpOptsfilePaths: Array.Empty<ValueWithSource>(), DhcpHostsdirPaths: Array.Empty<ValueWithSource>(), DhcpHostsfilePaths: Array.Empty<ValueWithSource>(), DhcpOptsfilePaths: Array.Empty<ValueWithSource>(), DhcpHostsdirPaths: Array.Empty<ValueWithSource>(),
ConnmarkAllowlistValues: Array.Empty<ValueWithSource>(), CaaRecordValues: Array.Empty<ValueWithSource>(), DnsRrValues: Array.Empty<ValueWithSource>(), SynthDomainValues: Array.Empty<ValueWithSource>(),
AuthZoneValues: Array.Empty<ValueWithSource>(), AuthSoaValues: Array.Empty<ValueWithSource>(), AuthSecServersValues: Array.Empty<ValueWithSource>(), AuthPeerValues: Array.Empty<ValueWithSource>(),
DhcpRelayValues: Array.Empty<ValueWithSource>(), DhcpCircuitidValues: Array.Empty<ValueWithSource>(), DhcpRemoteidValues: Array.Empty<ValueWithSource>(), DhcpSubscridValues: Array.Empty<ValueWithSource>(), DhcpProxyValues: Array.Empty<ValueWithSource>(),
TagIfValues: Array.Empty<ValueWithSource>(), BridgeInterfaceValues: Array.Empty<ValueWithSource>(), SharedNetworkValues: Array.Empty<ValueWithSource>(), DhcpOptionPxeValues: Array.Empty<ValueWithSource>(),
ExpandHosts: null, BogusPriv: null, StrictOrder: null, AllServers: null, NoResolv: null, DomainNeeded: null, NoPoll: null, ExpandHosts: null, BogusPriv: null, StrictOrder: null, AllServers: null, NoResolv: null, DomainNeeded: null, NoPoll: null,
BindInterfaces: null, BindDynamic: null, NoNegcache: null, DnsLoopDetect: null, StopDnsRebind: null, RebindLocalhostOk: null, ClearOnReload: null, BindInterfaces: null, BindDynamic: null, NoNegcache: null, DnsLoopDetect: null, StopDnsRebind: null, RebindLocalhostOk: null, ClearOnReload: null,
Filterwin2k: null, FilterA: null, FilterAaaa: null, LocaliseQueries: null, LogDebug: null, DhcpAuthoritative: null, LeasefileRo: null, Filterwin2k: null, FilterA: null, FilterAaaa: null, LocaliseQueries: null, LogDebug: null, DhcpAuthoritative: null, LeasefileRo: null,
EnableTftp: null, TftpSecure: null, TftpNoFail: null, TftpNoBlocksize: null, Dnssec: null, DnssecCheckUnsigned: null, EnableTftp: null, TftpSecure: null, TftpNoFail: null, TftpNoBlocksize: null, Dnssec: null, DnssecCheckUnsigned: null,
ReadEthers: null, DhcpRapidCommit: null, Localmx: null, Selfmx: null, EnableRa: null, LogDhcp: null, ReadEthers: null, DhcpRapidCommit: null, Localmx: null, Selfmx: null, EnableRa: null, LogDhcp: null,
KeepInForeground: null, NoDaemon: null, ProxyDnssec: null, KeepInForeground: null, NoDaemon: null, ProxyDnssec: null,
ConnmarkAllowlistEnable: null, NoRoundRobin: null, DnssecNoTimecheck: null, DnssecDebug: null, Leasequery: null, DhcpGenerateNames: null, DhcpBroadcast: null, DhcpSequentialIp: null, DhcpIgnoreClid: null, BootpDynamic: null, NoPing: null, ScriptArp: null, ScriptOnRenewal: null, DhcpNoOverride: null, QuietDhcp: null, QuietDhcp6: null, QuietRa: null, QuietTftp: null,
DhcpLeaseFilePath: null, CacheSize: null, Port: null, LocalTtl: null, PidFilePath: null, User: null, Group: null, DhcpLeaseFilePath: null, CacheSize: null, Port: null, LocalTtl: null, PidFilePath: null, User: null, Group: null,
LogFacility: null, LogQueries: null, AuthTtl: null, EdnsPacketMax: null, QueryPort: null, PortLimit: null, MinPort: null, MaxPort: null, LogAsync: null, LocalService: null, LogFacility: null, LogQueries: null, AuthTtl: null, EdnsPacketMax: null, QueryPort: null, PortLimit: null, MinPort: null, MaxPort: null, LogAsync: null, LocalService: null,
DhcpLeaseMax: null, NegTtl: null, MaxTtl: null, MaxCacheTtl: null, MinCacheTtl: null, DhcpTtl: null, DhcpLeaseMax: null, NegTtl: null, MaxTtl: null, MaxCacheTtl: null, MinCacheTtl: null, DhcpTtl: null,
TftpRootPath: null, PxePrompt: null, EnableDbus: null, EnableUbus: null, FastDnsRetry: null, TftpRootPath: null, PxePrompt: null, EnableDbus: null, EnableUbus: null, FastDnsRetry: null,
DhcpScriptPath: null, MxTarget: null, Conntrack: null DhcpScriptPath: null, MxTarget: null, DnsForwardMax: null, DumpfilePath: null, Dumpmask: null, AddCpeId: null, DnssecTimestamp: null, DnssecLimits: null, DhcpAlternatePort: null, DhcpDuid: null, DhcpLuascriptPath: null, DhcpScriptuser: null, DhcpPxeVendor: null,
UseStaleCache: null, AddMac: null, AddSubnet: null, Umbrella: null, Do0x20Encode: null, Conntrack: null
); );
private static int? TryParseInt(string? value) private static int? TryParseInt(string? value)
@@ -231,8 +231,7 @@ public class DnsmasqConfigService : IDnsmasqConfigService
var maxLineNumber = list.Count > 0 ? list.Max(l => l.LineNumber) : 0; var maxLineNumber = list.Count > 0 ? list.Max(l => l.LineNumber) : 0;
foreach (var c in changes) foreach (var c in changes)
{ {
var behavior = EffectiveConfigParserBehaviorMap.GetBehavior(c.OptionName); var writeBehavior = EffectiveConfigWriteBehaviorMap.GetBehavior(c.OptionName);
var isFlag = behavior == EffectiveConfigParserBehavior.Flag;
var confKey = c.OptionName; var confKey = c.OptionName;
bool MatchesOption(DnsmasqConfLine line) bool MatchesOption(DnsmasqConfLine line)
@@ -242,6 +241,43 @@ public class DnsmasqConfigService : IDnsmasqConfigService
return kv != null && string.Equals(kv.Value.key, confKey, StringComparison.Ordinal); return kv != null && string.Equals(kv.Value.key, confKey, StringComparison.Ordinal);
} }
if (writeBehavior == EffectiveConfigWriteBehavior.KeyOnlyOrValue)
{
RemoveAllMatchingLines(list, MatchesOption);
var value = c.NewValue as string;
if (value is null)
continue;
var lineText = value.Length == 0 ? confKey : $"{confKey}={value.Trim()}";
list.Add(new OtherLine { LineNumber = maxLineNumber + 1, RawLine = lineText });
maxLineNumber++;
continue;
}
if (writeBehavior == EffectiveConfigWriteBehavior.InversePair)
{
var pair = EffectiveConfigWriteBehaviorMap.GetInversePairKeys(confKey);
if (pair is null)
continue;
var (pairKeyA, pairKeyB) = pair.Value;
bool MatchesPair(DnsmasqConfLine line)
{
var raw = DnsmasqConfFileLineParser.ToLine(line);
var kv = DnsmasqConfDirectiveParser.TryParseKeyValue(raw);
return kv != null && (string.Equals(kv.Value.key, pairKeyA, StringComparison.Ordinal) || string.Equals(kv.Value.key, pairKeyB, StringComparison.Ordinal));
}
RemoveAllMatchingLines(list, MatchesPair);
if (c.NewValue is ExplicitToggleState state && state != ExplicitToggleState.Default)
{
var lineKey = state == ExplicitToggleState.Enabled ? pairKeyA : pairKeyB;
list.Add(new OtherLine { LineNumber = maxLineNumber + 1, RawLine = lineKey });
maxLineNumber++;
}
continue;
}
var behavior = EffectiveConfigParserBehaviorMap.GetBehavior(c.OptionName);
var isFlag = behavior == EffectiveConfigParserBehavior.Flag;
if (behavior == EffectiveConfigParserBehavior.Multi && TryGetMultiValues(c.NewValue, out var values)) if (behavior == EffectiveConfigParserBehavior.Multi && TryGetMultiValues(c.NewValue, out var values))
{ {
IReadOnlyList<string> readonlyValues = readonlyByOption.TryGetValue(confKey, out var listValues) ? listValues : Array.Empty<string>(); IReadOnlyList<string> readonlyValues = readonlyByOption.TryGetValue(confKey, out var listValues) ? listValues : Array.Empty<string>();
@@ -299,6 +335,15 @@ public class DnsmasqConfigService : IDnsmasqConfigService
await WriteManagedConfigAsync(list, ct); await WriteManagedConfigAsync(list, ct);
} }
private static void RemoveAllMatchingLines(List<DnsmasqConfLine> list, Predicate<DnsmasqConfLine> match)
{
for (var i = list.Count - 1; i >= 0; i--)
{
if (match(list[i]))
list.RemoveAt(i);
}
}
/// <summary> /// <summary>
/// Keeps only values that should be written to managed config by subtracting values provided by /// Keeps only values that should be written to managed config by subtracting values provided by
/// non-managed files (multiset subtraction, order preserved). /// non-managed files (multiset subtraction, order preserved).
@@ -34,6 +34,7 @@ public class EffectiveConfigRenderFragmentRegistry : IEffectiveConfigRenderFragm
RegisterInteger(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.PortLimit, max: 65535); RegisterInteger(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.PortLimit, max: 65535);
RegisterInteger(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.MinPort, min: 1, max: 65535); RegisterInteger(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.MinPort, min: 1, max: 65535);
RegisterInteger(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.MaxPort, min: 1, max: 65535); RegisterInteger(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.MaxPort, min: 1, max: 65535);
RegisterInteger(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.DnsForwardMax, min: 1);
RegisterInteger(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.CacheSize, min: 0); RegisterInteger(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.CacheSize, min: 0);
RegisterInteger(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.LocalTtl, unit: "seconds"); RegisterInteger(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.LocalTtl, unit: "seconds");
RegisterInteger(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.NegTtl, unit: "seconds"); RegisterInteger(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.NegTtl, unit: "seconds");
@@ -82,6 +83,33 @@ public class EffectiveConfigRenderFragmentRegistry : IEffectiveConfigRenderFragm
RegisterFlag(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.NoDaemon); RegisterFlag(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.NoDaemon);
RegisterFlag(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.Conntrack); RegisterFlag(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.Conntrack);
RegisterFlag(EffectiveConfigFieldBuilder.SectionDnssec, DnsmasqConfKeys.ProxyDnssec); RegisterFlag(EffectiveConfigFieldBuilder.SectionDnssec, DnsmasqConfKeys.ProxyDnssec);
RegisterFlag(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.ConnmarkAllowlistEnable);
RegisterFlag(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.NoRoundRobin);
RegisterFlag(EffectiveConfigFieldBuilder.SectionDnssec, DnsmasqConfKeys.DnssecNoTimecheck);
RegisterFlag(EffectiveConfigFieldBuilder.SectionDnssec, DnsmasqConfKeys.DnssecDebug);
RegisterFlag(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.Leasequery);
RegisterFlag(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpGenerateNames);
RegisterFlag(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpBroadcast);
RegisterFlag(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpSequentialIp);
RegisterFlag(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpIgnoreClid);
RegisterFlag(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.BootpDynamic);
RegisterFlag(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.NoPing);
RegisterFlag(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.ScriptArp);
RegisterFlag(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.ScriptOnRenewal);
RegisterFlag(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpNoOverride);
RegisterFlag(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.QuietDhcp);
RegisterFlag(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.QuietDhcp6);
RegisterFlag(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.QuietRa);
RegisterFlag(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.QuietTftp);
// do-0x20-encode / no-0x20-encode: tri-state dropdown (Default / Enabled / Disabled).
RegisterComponent(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.Do0x20Encode, typeof(Do0x20EncodeDisplay));
// Key-only or key=value options: checkbox (On) + optional value input.
RegisterComponent(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.UseStaleCache, typeof(KeyOnlyOrValueDisplay));
RegisterComponent(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.AddMac, typeof(KeyOnlyOrValueDisplay));
RegisterComponent(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.AddSubnet, typeof(KeyOnlyOrValueDisplay));
RegisterComponent(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.Umbrella, typeof(KeyOnlyOrValueDisplay));
// log-queries: dropdown (Off / On / extra / proto / auth). // log-queries: dropdown (Off / On / extra / proto / auth).
RegisterComponent(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.LogQueries, typeof(LogQueriesDisplay)); RegisterComponent(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.LogQueries, typeof(LogQueriesDisplay));
@@ -113,6 +141,7 @@ public class EffectiveConfigRenderFragmentRegistry : IEffectiveConfigRenderFragm
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.FilterRr); RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.FilterRr);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.Ipset); RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.Ipset);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.Nftset); RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.Nftset);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.ConnmarkAllowlist);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDnsRecords, DnsmasqConfKeys.Domain); RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDnsRecords, DnsmasqConfKeys.Domain);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDnsRecords, DnsmasqConfKeys.Cname); RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDnsRecords, DnsmasqConfKeys.Cname);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDnsRecords, DnsmasqConfKeys.MxHost); RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDnsRecords, DnsmasqConfKeys.MxHost);
@@ -123,6 +152,13 @@ public class EffectiveConfigRenderFragmentRegistry : IEffectiveConfigRenderFragm
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDnsRecords, DnsmasqConfKeys.HostRecord); RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDnsRecords, DnsmasqConfKeys.HostRecord);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDnsRecords, DnsmasqConfKeys.DynamicHost); RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDnsRecords, DnsmasqConfKeys.DynamicHost);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDnsRecords, DnsmasqConfKeys.InterfaceName); RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDnsRecords, DnsmasqConfKeys.InterfaceName);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDnsRecords, DnsmasqConfKeys.CaaRecord);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDnsRecords, DnsmasqConfKeys.DnsRr);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDnsRecords, DnsmasqConfKeys.SynthDomain);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDnsRecords, DnsmasqConfKeys.AuthZone);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDnsRecords, DnsmasqConfKeys.AuthSoa);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDnsRecords, DnsmasqConfKeys.AuthSecServers);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDnsRecords, DnsmasqConfKeys.AuthPeer);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpRange); RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpRange);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpHost); RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpHost);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpOption); RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpOption);
@@ -134,6 +170,14 @@ public class EffectiveConfigRenderFragmentRegistry : IEffectiveConfigRenderFragm
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpHostsfile); RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpHostsfile);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpOptsfile); RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpOptsfile);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpHostsdir); RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpHostsdir);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpRelay);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpCircuitid);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpRemoteid);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpSubscrid);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpProxy);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.TagIf);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.BridgeInterface);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.SharedNetwork);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpBoot); RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpBoot);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpIgnore); RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpIgnore);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpVendorclass); RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpVendorclass);
@@ -141,6 +185,7 @@ public class EffectiveConfigRenderFragmentRegistry : IEffectiveConfigRenderFragm
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.RaParam); RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.RaParam);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.Slaac); RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.Slaac);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionTftpPxe, DnsmasqConfKeys.PxeService); RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionTftpPxe, DnsmasqConfKeys.PxeService);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionTftpPxe, DnsmasqConfKeys.DhcpOptionPxe);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDnssec, DnsmasqConfKeys.TrustAnchor); RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDnssec, DnsmasqConfKeys.TrustAnchor);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.CacheRr); RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.CacheRr);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.Interface); RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.Interface);
@@ -19,7 +19,7 @@ public static class EffectiveConfigCrossOptionEvaluator
{ {
var issues = new List<FieldIssue>(); var issues = new List<FieldIssue>();
var noResolv = GetEffectiveNoResolv(status, pending); var noResolv = GetEffectiveBool(status, pending, DnsmasqConfKeys.NoResolv, s => s?.EffectiveConfig?.NoResolv ?? false);
var serverValues = GetEffectiveServerValues(status, pending); var serverValues = GetEffectiveServerValues(status, pending);
// no-resolv set with no upstream servers: DNS may not work // no-resolv set with no upstream servers: DNS may not work
@@ -33,17 +33,51 @@ public static class EffectiveConfigCrossOptionEvaluator
ItemIndex: null)); ItemIndex: null));
} }
// conntrack cannot be combined with query-port (dnsmasq man page)
var conntrack = GetEffectiveBool(status, pending, DnsmasqConfKeys.Conntrack, s => s?.EffectiveConfig?.Conntrack ?? false);
var queryPort = GetEffectiveInt(status, pending, DnsmasqConfKeys.QueryPort, s => s?.EffectiveConfig?.QueryPort);
if (conntrack && queryPort is not null)
{
issues.Add(new FieldIssue(
$"{EffectiveConfigSections.SectionResolver}:{DnsmasqConfKeys.QueryPort}",
"query-port cannot be combined with conntrack.",
FieldIssueSeverity.Error,
null));
}
return issues; return issues;
} }
private static bool GetEffectiveNoResolv(DnsmasqServiceStatus? status, IReadOnlyList<PendingEffectiveConfigChange> pending) private static bool GetEffectiveBool(
DnsmasqServiceStatus? status,
IReadOnlyList<PendingEffectiveConfigChange>? pending,
string optionName,
Func<DnsmasqServiceStatus?, bool> fromConfig)
{ {
var fromConfig = status?.EffectiveConfig?.NoResolv ?? false; var pendingChange = pending?.FirstOrDefault(c => string.Equals(c.OptionName, optionName, StringComparison.Ordinal));
var pendingChange = pending?.FirstOrDefault(c => if (pendingChange != null)
string.Equals(c.OptionName, DnsmasqConfKeys.NoResolv, StringComparison.Ordinal)); {
if (pendingChange?.NewValue is bool b) if (pendingChange.NewValue is bool b)
return b; return b;
return fromConfig; return false; // pending change to clear or invalid; treat as off for cross-option purposes
}
return fromConfig(status);
}
private static int? GetEffectiveInt(
DnsmasqServiceStatus? status,
IReadOnlyList<PendingEffectiveConfigChange>? pending,
string optionName,
Func<DnsmasqServiceStatus?, int?> fromConfig)
{
var pendingChange = pending?.FirstOrDefault(c => string.Equals(c.OptionName, optionName, StringComparison.Ordinal));
if (pendingChange != null)
{
if (pendingChange.NewValue is int i)
return i;
return null; // pending change to clear the value; use null so conntrack+query-port rule no longer blocks
}
return fromConfig(status);
} }
private static IReadOnlyList<string>? GetEffectiveServerValues(DnsmasqServiceStatus? status, IReadOnlyList<PendingEffectiveConfigChange> pending) private static IReadOnlyList<string>? GetEffectiveServerValues(DnsmasqServiceStatus? status, IReadOnlyList<PendingEffectiveConfigChange> pending)
@@ -65,6 +65,23 @@ public record EffectiveConfigSources(
IReadOnlyList<ValueWithSource> DhcpHostsfilePaths, IReadOnlyList<ValueWithSource> DhcpHostsfilePaths,
IReadOnlyList<ValueWithSource> DhcpOptsfilePaths, IReadOnlyList<ValueWithSource> DhcpOptsfilePaths,
IReadOnlyList<ValueWithSource> DhcpHostsdirPaths, IReadOnlyList<ValueWithSource> DhcpHostsdirPaths,
IReadOnlyList<ValueWithSource> ConnmarkAllowlistValues,
IReadOnlyList<ValueWithSource> CaaRecordValues,
IReadOnlyList<ValueWithSource> DnsRrValues,
IReadOnlyList<ValueWithSource> SynthDomainValues,
IReadOnlyList<ValueWithSource> AuthZoneValues,
IReadOnlyList<ValueWithSource> AuthSoaValues,
IReadOnlyList<ValueWithSource> AuthSecServersValues,
IReadOnlyList<ValueWithSource> AuthPeerValues,
IReadOnlyList<ValueWithSource> DhcpRelayValues,
IReadOnlyList<ValueWithSource> DhcpCircuitidValues,
IReadOnlyList<ValueWithSource> DhcpRemoteidValues,
IReadOnlyList<ValueWithSource> DhcpSubscridValues,
IReadOnlyList<ValueWithSource> DhcpProxyValues,
IReadOnlyList<ValueWithSource> TagIfValues,
IReadOnlyList<ValueWithSource> BridgeInterfaceValues,
IReadOnlyList<ValueWithSource> SharedNetworkValues,
IReadOnlyList<ValueWithSource> DhcpOptionPxeValues,
// --- Flags (set if any occurrence; source = first file that set it, so we know if readonly) --- // --- Flags (set if any occurrence; source = first file that set it, so we know if readonly) ---
ConfigValueSource? ExpandHosts, ConfigValueSource? ExpandHosts,
@@ -103,6 +120,24 @@ public record EffectiveConfigSources(
ConfigValueSource? KeepInForeground, ConfigValueSource? KeepInForeground,
ConfigValueSource? NoDaemon, ConfigValueSource? NoDaemon,
ConfigValueSource? ProxyDnssec, ConfigValueSource? ProxyDnssec,
ConfigValueSource? ConnmarkAllowlistEnable,
ConfigValueSource? NoRoundRobin,
ConfigValueSource? DnssecNoTimecheck,
ConfigValueSource? DnssecDebug,
ConfigValueSource? Leasequery,
ConfigValueSource? DhcpGenerateNames,
ConfigValueSource? DhcpBroadcast,
ConfigValueSource? DhcpSequentialIp,
ConfigValueSource? DhcpIgnoreClid,
ConfigValueSource? BootpDynamic,
ConfigValueSource? NoPing,
ConfigValueSource? ScriptArp,
ConfigValueSource? ScriptOnRenewal,
ConfigValueSource? DhcpNoOverride,
ConfigValueSource? QuietDhcp,
ConfigValueSource? QuietDhcp6,
ConfigValueSource? QuietRa,
ConfigValueSource? QuietTftp,
// --- Single-value (last occurrence wins; source = file that set the last value) --- // --- Single-value (last occurrence wins; source = file that set the last value) ---
ConfigValueSource? DhcpLeaseFilePath, ConfigValueSource? DhcpLeaseFilePath,
@@ -135,5 +170,21 @@ public record EffectiveConfigSources(
ConfigValueSource? FastDnsRetry, ConfigValueSource? FastDnsRetry,
ConfigValueSource? DhcpScriptPath, ConfigValueSource? DhcpScriptPath,
ConfigValueSource? MxTarget, ConfigValueSource? MxTarget,
ConfigValueSource? DnsForwardMax,
ConfigValueSource? DumpfilePath,
ConfigValueSource? Dumpmask,
ConfigValueSource? AddCpeId,
ConfigValueSource? DnssecTimestamp,
ConfigValueSource? DnssecLimits,
ConfigValueSource? DhcpAlternatePort,
ConfigValueSource? DhcpDuid,
ConfigValueSource? DhcpLuascriptPath,
ConfigValueSource? DhcpScriptuser,
ConfigValueSource? DhcpPxeVendor,
ConfigValueSource? UseStaleCache,
ConfigValueSource? AddMac,
ConfigValueSource? AddSubnet,
ConfigValueSource? Umbrella,
ConfigValueSource? Do0x20Encode,
ConfigValueSource? Conntrack ConfigValueSource? Conntrack
); );
@@ -69,6 +69,23 @@ public record EffectiveDnsmasqConfig(
IReadOnlyList<string> DhcpHostsfilePaths, IReadOnlyList<string> DhcpHostsfilePaths,
IReadOnlyList<string> DhcpOptsfilePaths, IReadOnlyList<string> DhcpOptsfilePaths,
IReadOnlyList<string> DhcpHostsdirPaths, IReadOnlyList<string> DhcpHostsdirPaths,
IReadOnlyList<string> ConnmarkAllowlistValues,
IReadOnlyList<string> CaaRecordValues,
IReadOnlyList<string> DnsRrValues,
IReadOnlyList<string> SynthDomainValues,
IReadOnlyList<string> AuthZoneValues,
IReadOnlyList<string> AuthSoaValues,
IReadOnlyList<string> AuthSecServersValues,
IReadOnlyList<string> AuthPeerValues,
IReadOnlyList<string> DhcpRelayValues,
IReadOnlyList<string> DhcpCircuitidValues,
IReadOnlyList<string> DhcpRemoteidValues,
IReadOnlyList<string> DhcpSubscridValues,
IReadOnlyList<string> DhcpProxyValues,
IReadOnlyList<string> TagIfValues,
IReadOnlyList<string> BridgeInterfaceValues,
IReadOnlyList<string> SharedNetworkValues,
IReadOnlyList<string> DhcpOptionPxeValues,
// --- Boolean flags (set if any file contains the option) --- // --- Boolean flags (set if any file contains the option) ---
bool ExpandHosts, bool ExpandHosts,
@@ -107,6 +124,24 @@ public record EffectiveDnsmasqConfig(
bool KeepInForeground, bool KeepInForeground,
bool NoDaemon, bool NoDaemon,
bool ProxyDnssec, bool ProxyDnssec,
bool ConnmarkAllowlistEnable,
bool NoRoundRobin,
bool DnssecNoTimecheck,
bool DnssecDebug,
bool Leasequery,
bool DhcpGenerateNames,
bool DhcpBroadcast,
bool DhcpSequentialIp,
bool DhcpIgnoreClid,
bool BootpDynamic,
bool NoPing,
bool ScriptArp,
bool ScriptOnRenewal,
bool DhcpNoOverride,
bool QuietDhcp,
bool QuietDhcp6,
bool QuietRa,
bool QuietTftp,
// --- Single-value options (last occurrence wins; null = not set, dnsmasq default) --- // --- Single-value options (last occurrence wins; null = not set, dnsmasq default) ---
string? DhcpLeaseFilePath, string? DhcpLeaseFilePath,
@@ -139,6 +174,22 @@ public record EffectiveDnsmasqConfig(
string? FastDnsRetry, string? FastDnsRetry,
string? DhcpScriptPath, string? DhcpScriptPath,
string? MxTarget, string? MxTarget,
int? DnsForwardMax,
string? DumpfilePath,
string? Dumpmask,
string? AddCpeId,
string? DnssecTimestamp,
string? DnssecLimits,
string? DhcpAlternatePort,
string? DhcpDuid,
string? DhcpLuascriptPath,
string? DhcpScriptuser,
string? DhcpPxeVendor,
string? UseStaleCache,
string? AddMac,
string? AddSubnet,
string? Umbrella,
ExplicitToggleState Do0x20EncodeState,
bool Conntrack bool Conntrack
) )
{ {
@@ -0,0 +1,12 @@
namespace DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
/// <summary>
/// Tri-state for inverse-pair options (e.g. do-0x20-encode / no-0x20-encode).
/// Default = not set (remove both lines); Enabled = write the positive key; Disabled = write the negative key.
/// </summary>
public enum ExplicitToggleState
{
Default,
Enabled,
Disabled,
}
@@ -0,0 +1,3 @@
<dt><b>--add-cpe-id=&lt;string&gt;</b></dt>
<dd>Add an arbitrary identifying string to DNS queries which are
forwarded upstream.</dd>
@@ -0,0 +1,10 @@
<dt><b>--add-mac[=base64|text]</b></dt>
<dd>Add the MAC address of the requestor to DNS queries which are
forwarded upstream. This may be used to DNS filtering by the upstream
server. The MAC address can only be added if the requestor is on the same
subnet as the dnsmasq server. Note that the mechanism used to achieve this (an EDNS0 option)
is not yet standardised, so this should be considered
experimental. Also note that exposing MAC addresses in this way may
have security and privacy implications. The warning about caching
given for <b>--add-subnet</b> applies to <b>--add-mac</b> too. An alternative encoding of the
MAC, as base64, is enabled by adding the "base64" parameter and a human-readable encoding of hex-and-colons is enabled by added the "text" parameter.</dd>
@@ -0,0 +1,23 @@
<dt><b>--add-subnet[[=[&lt;IPv4 address&gt;/]&lt;IPv4 prefix length&gt;][,[&lt;IPv6 address&gt;/]&lt;IPv6 prefix length&gt;]]</b></dt>
<dd>Add a subnet address to the DNS queries which are forwarded
upstream. If an address is specified in the flag, it will be used,
otherwise, the address of the requestor will be used. The amount of
the address forwarded depends on the prefix length parameter: 32 (128
for IPv6) forwards the whole address, zero forwards none of it but
still marks the request so that no upstream nameserver will add client
address information either. The default is zero for both IPv4 and
IPv6. Note that upstream nameservers may be configured to return
different results based on this information, but the dnsmasq cache
does not take account. Caching is therefore disabled for such replies,
unless the subnet address being added is constant.
<p>
For example,
<b>--add-subnet=24,96</b>
will add the /24 and /96 subnets of the requestor for IPv4 and IPv6 requestors, respectively.
<b>--add-subnet=1.2.3.4/24</b>
will add 1.2.3.0/24 for IPv4 requestors and ::/0 for IPv6 requestors.
<b>--add-subnet=1.2.3.4/24,1.2.3.4/24</b>
will add 1.2.3.0/24 for both IPv4 and IPv6 requestors.</dd>
@@ -0,0 +1,12 @@
<dt><b>--auth-peer=&lt;ip-address&gt;[,&lt;ip-address&gt;[,&lt;ip-address&gt;...]]</b></dt>
<dd>Specify the addresses of secondary servers which are allowed to
initiate zone transfer (AXFR) requests for zones for which dnsmasq is
authoritative. If this option is not given but --auth-sec-servers is,
then AXFR requests will be
accepted from any secondary. Specifying
<b>--auth-peer</b>
without
<b>--auth-sec-servers</b>
enables zone transfer but does not advertise the secondary in NS records returned by dnsmasq.</dd>
@@ -0,0 +1,5 @@
<dt><b>--auth-sec-servers=&lt;domain&gt;[,&lt;domain&gt;[,&lt;domain&gt;...]]</b></dt>
<dd>Specify any secondary servers for a zone for which dnsmasq is
authoritative. These servers must be configured to get zone data from
dnsmasq by zone transfer, and answer queries for the same
authoritative zones as dnsmasq.</dd>
@@ -0,0 +1,3 @@
<dt><b>--auth-soa=&lt;serial&gt;[,&lt;hostmaster&gt;[,&lt;refresh&gt;[,&lt;retry&gt;[,&lt;expiry&gt;]]]]</b></dt>
<dd>Specify fields in the SOA record associated with authoritative
zones. Note that this is optional, all the values are set to sane defaults.</dd>
@@ -0,0 +1,28 @@
<dt><b>--auth-zone=&lt;domain&gt;[,&lt;subnet&gt;[/&lt;prefix length&gt;][,&lt;subnet&gt;[/&lt;prefix length&gt;].....][,exclude:&lt;subnet&gt;[/&lt;prefix length&gt;]].....]</b></dt>
<dd>Define a DNS zone for which dnsmasq acts as authoritative server. Locally defined DNS records which are in the domain
will be served. If subnet(s) are given, A and AAAA records must be in one of the
specified subnets.
<p>
As alternative to directly specifying the subnets, it's possible to
give the name of an interface, in which case the subnets implied by
that interface's configured addresses and netmask/prefix-length are
used; this is useful when using constructed DHCP ranges as the actual
address is dynamic and not known when configuring dnsmasq. The
interface addresses may be confined to only IPv6 addresses using
&lt;interface&gt;/6 or to only IPv4 using &lt;interface&gt;/4. This is useful when
an interface has dynamically determined global IPv6 addresses which should
appear in the zone, but RFC1918 IPv4 addresses which should not.
Interface-name and address-literal subnet specifications may be used
freely in the same <b>--auth-zone</b> declaration.
<p>
It's possible to exclude certain IP addresses from responses. It can be
used, to make sure that answers contain only global routeable IP
addresses (by excluding loopback, RFC1918 and ULA addresses).
<p>
The subnet(s) are also used to define in-addr.arpa and
ip6.arpa domains which are served for reverse-DNS queries. If not
specified, the prefix length defaults to 24 for IPv4 and 64 for IPv6.
For IPv4 subnets, the prefix length should be have the value 8, 16 or 24
unless you are familiar with RFC 2317 and have arranged the
in-addr.arpa delegation accordingly. Note that if no subnets are
specified, then no reverse queries are answered.</dd>
@@ -0,0 +1,7 @@
<dt><b>-3, --bootp-dynamic[=&lt;network-id&gt;[,&lt;network-id&gt;]]</b></dt>
<dd>(IPv4 only) Enable dynamic allocation of IP addresses to BOOTP clients. Use this
with care, since each address allocated to a BOOTP client is leased
forever, and therefore becomes permanently unavailable for re-use by
other hosts. if this is given without tags, then it unconditionally
enables dynamic allocation. With tags, only when the tags are all
set. It may be repeated with different tag sets.</dd>
@@ -0,0 +1,12 @@
<dt><b>--bridge-interface=&lt;interface&gt;,&lt;alias&gt;[,&lt;alias&gt;]</b></dt>
<dd>Treat DHCP (v4 and v6) requests and IPv6 Router Solicit packets
arriving at any of the &lt;alias&gt; interfaces as if they had arrived at
&lt;interface&gt;. This option allows dnsmasq to provide DHCP and RA
service over unaddressed and unbridged Ethernet interfaces, e.g. on an
OpenStack compute host where each such interface is a TAP interface to
a VM, or as in "old style bridging" on BSD platforms. A trailing '*'
wildcard can be used in each &lt;alias&gt;.
<p>
It is permissible to add more than one alias using more than one <b>--bridge-interface</b> option since
<b>--bridge-interface=int1,alias1,alias2</b> is exactly equivalent to
<b>--bridge-interface=int1,alias1 --bridge-interface=int1,alias2</b></dd>
@@ -0,0 +1,2 @@
<dt><b>--caa-record=&lt;name&gt;,&lt;flags&gt;,&lt;tag&gt;,&lt;value&gt;</b></dt>
<dd>Return a CAA DNS record, as specified in RFC6844.</dd>
@@ -0,0 +1,10 @@
<dt><b>--connmark-allowlist-enable[=&lt;mask&gt;]</b></dt>
<dd>Enables filtering of incoming DNS queries with associated Linux connection track marks
according to individual allowlists configured via a series of <b>--connmark-allowlist</b>
options. Disallowed queries are not forwarded; they are rejected with a REFUSED error code.
DNS queries are only allowed if they do not have an associated Linux connection
track mark, or if the queried domains match the configured DNS patterns for the
associated Linux connection track mark. If no allowlist is configured for a
Linux connection track mark, all DNS queries associated with that mark are rejected.
If a mask is specified, Linux connection track marks are first bitwise ANDed
with the given mask before being processed.</dd>
@@ -0,0 +1,14 @@
<dt><b>--connmark-allowlist=&lt;connmark&gt;[/&lt;mask&gt;][,&lt;pattern&gt;[/&lt;pattern&gt;...]]</b></dt>
<dd>Configures the DNS patterns that are allowed in DNS queries associated with
the given Linux connection track mark.
If a mask is specified, Linux connection track marks are first bitwise ANDed
with the given mask before they are compared to the given connection track mark.
Patterns follow the syntax of DNS names, but additionally allow the wildcard
character "*" to be used up to twice per label to match 0 or more characters
within that label. Note that the wildcard never matches a dot (e.g., "*.example.com"
matches "api.example.com" but not "api.us.example.com"). Patterns must be
fully qualified, i.e., consist of at least two labels. The final label must not be
fully numeric, and must not be the "local" pseudo-TLD. A pattern must end with at least
two literal (non-wildcard) labels.
Instead of a pattern, "*" can be specified to disable allowlist filtering
for a given Linux connection track mark entirely.</dd>
@@ -0,0 +1,7 @@
<dt><b>--dhcp-alternate-port[=&lt;server port&gt;[,&lt;client port&gt;]]</b></dt>
<dd>(IPv4 only) Change the ports used for DHCP from the default. If this option is
given alone, without arguments, it changes the ports used for DHCP
from 67 and 68 to 1067 and 1068. If a single argument is given, that
port number is used for the server and the port number plus one used
for the client. Finally, two port numbers allows arbitrary
specification of both server and client ports for DHCP.</dd>
@@ -0,0 +1,6 @@
<dt><b>--dhcp-broadcast[=tag:&lt;tag&gt;[,tag:&lt;tag&gt;]]</b></dt>
<dd>(IPv4 only) When all the given tags appear in the tag set, always use broadcast to
communicate with the host when it is unconfigured. It is permissible
to supply no tags, in which case this is unconditional. Most DHCP clients which
need broadcast replies set a flag in their requests so that this
happens automatically, some old BOOTP clients do not.</dd>
@@ -0,0 +1,10 @@
<dt><b>--dhcp-circuitid=set:&lt;tag&gt;,&lt;circuit-id&gt;, --dhcp-remoteid=set:&lt;tag&gt;,&lt;remote-id&gt;</b></dt>
<dd>Map from RFC3046 relay agent options to tags. This data may
be provided by DHCP relay agents. The circuit-id or remote-id is
normally given as colon-separated hex, but is also allowed to be a
simple string. If an exact match is achieved between the circuit or
agent ID and one provided by a relay agent, the tag is set.
<p>
<b>--dhcp-remoteid</b>
(but not <b>--dhcp-circuitid</b>) is supported in IPv6.</dd>
@@ -0,0 +1,9 @@
<dt><b>--dhcp-duid=&lt;enterprise-id&gt;,&lt;uid&gt;</b></dt>
<dd>(IPv6 only) Specify the server persistent UID which the DHCPv6 server
will use. This option is not normally required as dnsmasq creates a
DUID automatically when it is first needed. When given, this option
provides dnsmasq the data required to create a DUID-EN type DUID. Note
that once set, the DUID is stored in the lease database, so to change between DUID-EN and
automatically created DUIDs or vice-versa, the lease database must be
re-initialised. The enterprise-id is assigned by IANA, and the uid is a
string of hex octets unique to a particular device.</dd>
@@ -0,0 +1,8 @@
<dt><b>--dhcp-generate-names=tag:&lt;tag&gt;[,tag:&lt;tag&gt;]</b></dt>
<dd>(IPv4 only) Generate a name for DHCP clients which do not otherwise have one,
using the MAC address expressed in hex, separated by dashes. Note that
if a host provides a name, it will be used by preference to this,
unless
<b>--dhcp-ignore-names </b>
is set.</dd>
@@ -0,0 +1,5 @@
<dt><b>--dhcp-ignore-clid</b></dt>
<dd>Dnsmasq is reading 'client identifier' (RFC 2131) option sent by clients
(if available) to identify clients. This allow one to serve same IP address
for a host using several interfaces. Use this option to disable 'client identifier'
reading, i.e. to always identify a host using the MAC address.</dd>
@@ -0,0 +1,73 @@
<dt><b>--dhcp-luascript=&lt;path&gt;</b></dt>
<dd>Specify a script written in Lua, to be run when leases are created,
destroyed or changed. To use this option, dnsmasq must be compiled
with the correct support. The Lua interpreter is initialised once, when
dnsmasq starts, so that global variables persist between lease
events. The Lua code must define a
<b>lease</b>
function, and may provide
<b>init</b>
and
<b>shutdown</b>
functions, which are called, without arguments when dnsmasq starts up
and terminates. It may also provide a
<b>tftp</b>
function.
<p>
The
<b>lease</b>
function receives the information detailed in
<b>--dhcp-script. </b>
It gets two arguments, firstly the action, which is a string
containing, "add", "old" or "del", and secondly a table of tag value
pairs. The tags mostly correspond to the environment variables
detailed above, for instance the tag "domain" holds the same data as
the environment variable DNSMASQ_DOMAIN. There are a few extra tags
which hold the data supplied as arguments to
<b>--dhcp-script. </b>
These are
<b>mac_address, ip_address</b>
and
<b>hostname</b>
for IPv4, and
<b>client_duid, ip_address</b>
and
<b>hostname</b>
for IPv6.
<p>
The
<b>tftp</b>
function is called in the same way as the lease function, and the
table holds the tags
<b>destination_address,</b>
<b>file_name</b>
and
<b>file_size.</b>
<p>
The
<b>arp</b>
and
<b>arp-old</b>
functions are called only when enabled with
<b>--script-arp</b>
and have a table which holds the tags
<b>mac_address</b>
and
<b>client_address.</b></dd>
@@ -0,0 +1,7 @@
<dt><b>--dhcp-no-override</b></dt>
<dd>(IPv4 only) Disable re-use of the DHCP servername and filename fields as extra
option space. If it can, dnsmasq moves the boot server and filename
information (from <b>--dhcp-boot</b>) out of their dedicated fields into
DHCP options. This make extra space available in the DHCP packet for
options but can, rarely, confuse old or broken clients. This flag
forces "simple and safe" behaviour to avoid problems in such a case.</dd>
@@ -0,0 +1,3 @@
<dt><b>--dhcp-option-pxe=[tag:&lt;tag&gt;,[tag:&lt;tag&gt;,]][encap:&lt;opt&gt;,][vi-encap:&lt;enterprise&gt;,]&lt;opt&gt;,[&lt;value&gt;[,&lt;value&gt;]]</b></dt>
<dd>A variant of --dhcp-option which defines options only sent in reply to PXE clients. In addition, such options are
sent in reply to PXE clients when dnsmasq is acting as a PXE proxy, unlike other options. A typical use-case is option 175, sent to iPXE.</dd>
@@ -0,0 +1,18 @@
<dt><b>--dhcp-proxy[=&lt;ip addr&gt;]......</b></dt>
<dd>(IPv4 only) A normal DHCP relay agent is only used to forward the initial parts of
a DHCP interaction to the DHCP server. Once a client is configured, it
communicates directly with the server. This is undesirable if the
relay agent is adding extra information to the DHCP packets, such as
that used by
<b>--dhcp-circuitid</b>
and
<b>--dhcp-remoteid.</b>
A full relay implementation can use the RFC 5107 serverid-override
option to force the DHCP server to use the relay as a full proxy, with all
packets passing through it. This flag provides an alternative method
of doing the same thing, for relays which don't support RFC
5107. Given alone, it manipulates the server-id for all interactions
via relays. If a list of IP addresses is given, only interactions via
relays at those addresses are affected.</dd>
@@ -0,0 +1,16 @@
<dt><b>--dhcp-pxe-vendor=&lt;vendor&gt;[,...]</b></dt>
<dd>According to UEFI and PXE specifications, DHCP packets between PXE clients and
proxy PXE servers should have
<i>PXEClient </i>
in their vendor-class field. However, the firmware of computers from a few
vendors is customized to carry a different identifier in that field. This option
is used to consider such identifiers valid for identifying PXE clients. For
instance
<p>
<b>--dhcp-pxe-vendor=PXEClient,HW-Client</b>
<p>
will enable dnsmasq to also provide proxy PXE service to those PXE clients with
<i>HW-Client</i>
in as their identifier.</dd>
@@ -0,0 +1,39 @@
<dt><b>--dhcp-relay=&lt;local address&gt;[,&lt;server address&gt;[#&lt;server port&gt;]][,&lt;interface]</b></dt>
<dd>Configure dnsmasq to do DHCP relay. The local address is an address
allocated to an interface on the host running dnsmasq. All DHCP
requests arriving on that interface will be relayed to a remote DHCP
server at the server address. It is possible to relay from a single local
address to multiple remote servers by using multiple <b>--dhcp-relay</b>
configs with the same local address and different server
addresses. A server address must be an IP literal address, not a
domain name. If the server address is omitted, the request will be
forwarded by broadcast (IPv4) or multicast (IPv6). In this case the interface
must be given and not be wildcard. The server address may specify a non-standard
port to relay to. If this is used then <b>--dhcp-proxy</b> should likely also be set,
otherwise parts of the DHCP conversation which do not pass through the relay
will be delivered to the wrong port.
<p>
Access control for DHCP clients has the same rules as for the DHCP
server, see <b>--interface</b>, <b>--except-interface</b>, etc. The optional
interface name in the <b>--dhcp-relay</b> config has a different function: it
controls on which interface DHCP replies from the server will be
accepted. This is intended for configurations which have three
interfaces: one being relayed from, a second connecting the DHCP
server, and a third untrusted network, typically the wider
internet. It avoids the possibility of spoof replies arriving via this
third interface.
<p>
It is allowed to have dnsmasq act as a DHCP server on one set of
interfaces and relay from a disjoint set of interfaces. Note that
whilst it is quite possible to write configurations which appear to
act as a server and a relay on the same interface, this is not
supported: the relay function will take precedence.
<p>
Both DHCPv4 and DHCPv6 relay is supported. It's not possible to relay
DHCPv4 to a DHCPv6 server or vice-versa.
<p>
The DHCP relay function for IPv6 includes the ability to snoop
prefix-delegation from relayed DHCP transactions. See
<b>--dhcp-script</b>
for details.</dd>
@@ -0,0 +1,10 @@
<dt><b>--dhcp-circuitid=set:&lt;tag&gt;,&lt;circuit-id&gt;, --dhcp-remoteid=set:&lt;tag&gt;,&lt;remote-id&gt;</b></dt>
<dd>Map from RFC3046 relay agent options to tags. This data may
be provided by DHCP relay agents. The circuit-id or remote-id is
normally given as colon-separated hex, but is also allowed to be a
simple string. If an exact match is achieved between the circuit or
agent ID and one provided by a relay agent, the tag is set.
<p>
<b>--dhcp-remoteid</b>
(but not <b>--dhcp-circuitid</b>) is supported in IPv6.</dd>
@@ -0,0 +1,2 @@
<dt><b>--dhcp-scriptuser</b></dt>
<dd>Specify the user as which to run the lease-change script or Lua script. This defaults to root, but can be changed to another user using this flag.</dd>
@@ -0,0 +1,12 @@
<dt><b>--dhcp-sequential-ip</b></dt>
<dd>Dnsmasq is designed to choose IP addresses for DHCP clients using a
hash of the client's MAC address. This normally allows a client's
address to remain stable long-term, even if the client sometimes allows its DHCP
lease to expire. In this default mode IP addresses are distributed
pseudo-randomly over the entire available address range. There are
sometimes circumstances (typically server deployment) where it is more
convenient to have IP
addresses allocated sequentially, starting from the lowest available
address, and setting this flag enables this mode. Note that in the
sequential mode, clients which allow a lease to expire are much more
likely to move IP address; for this reason it should not be generally used.</dd>
@@ -0,0 +1,2 @@
<dt><b>--dhcp-subscrid=set:&lt;tag&gt;,&lt;subscriber-id&gt;</b></dt>
<dd>(IPv4 and IPv6) Map from RFC3993 subscriber-id relay agent options to tags.</dd>
@@ -0,0 +1,6 @@
<dt><b>-0, --dns-forward-max=&lt;queries&gt;</b></dt>
<dd>Set the maximum number of concurrent DNS queries. The default value is
150, which should be fine for most setups. The only known situation
where this needs to be increased is when using web-server log file
resolvers, which can generate large numbers of concurrent queries. This
parameter actually controls the number of concurrent queries per server group, where a server group is the set of server(s) associated with a single domain. So if a domain has its own server via --server=/example.com/1.2.3.4 and 1.2.3.4 is not responding, but queries for *.example.com cannot go elsewhere, then other queries will not be affected. On configurations with many such server groups and tight resources, this value may need to be reduced.</dd>
@@ -0,0 +1,5 @@
<dt><b>--dns-rr=&lt;name&gt;,&lt;RR-number&gt;,[&lt;hex data&gt;]</b></dt>
<dd>Return an arbitrary DNS Resource Record. The number is the type of the
record (which is always in the C_IN class). The value of the record is
given by the hex data, which may be of the form 01:23:45 or 01 23 45 or
012345 or any mixture of these.</dd>
@@ -0,0 +1,6 @@
<dt><b>--dnssec-debug</b></dt>
<dd>Set debugging mode for the DNSSEC validation, set the Checking Disabled bit on upstream queries,
and don't convert replies which do not validate to responses with
a return code of SERVFAIL. Note that
setting this may affect DNS behaviour in bad ways, it is not an
extra-logging flag and should not be set in production.</dd>
@@ -0,0 +1,8 @@
<dt><b>--dnssec-limits=&lt;limit&gt;[,&lt;limit&gt;.......]</b></dt>
<dd>Override the default resource limits applied to DNSSEC validation. Cryptographic operations are expensive and crafted domains
can DoS a DNSSEC validator by forcing it to do hundreds of thousands of such operations. To avoid this, the dnsmasq validation code
applies limits on how much work will be expended in validation. If any of the limits are exceeded, the validation will fail and the
domain treated as BOGUS. There are four limits, in order(default values in parens): number a signature validation fails per RRset(20), number of signature validations and
hash computations per query(200), number of sub-queries to fetch DS and DNSKEY RRsets per query(40), and the number of iterations in a NSEC3 record(150).
The maximum values reached during validation are stored, and dumped as part of the stats generated by SIGUSR1. Supplying a limit value of 0 leaves the default in place, so
<b>--dnssec-limits=0,0,20</b> sets the number of sub-queries to 20 whilst leaving the other limits at default values.</dd>
@@ -0,0 +1,12 @@
<dt><b>--dnssec-no-timecheck</b></dt>
<dd>DNSSEC signatures are only valid for specified time windows, and should be rejected outside those windows. This generates an
interesting chicken-and-egg problem for machines which don't have a hardware real time clock. For these machines to determine the correct
time typically requires use of NTP and therefore DNS, but validating DNS requires that the correct time is already known. Setting this flag
removes the time-window checks (but not other DNSSEC validation.) only until the dnsmasq process receives SIGINT. The intention is
that dnsmasq should be started with this flag when the platform determines that reliable time is not currently available. As soon as
reliable time is established, a SIGINT should be sent to dnsmasq, which enables time checking, and purges the cache of DNS records
which have not been thoroughly checked.
<p>
Earlier versions of dnsmasq overloaded SIGHUP (which re-reads much configuration) to also enable time validation.
<p>
If dnsmasq is run in debug mode (<b>--no-daemon</b> flag) then SIGINT retains its usual meaning of terminating the dnsmasq process.</dd>
@@ -0,0 +1,6 @@
<dt><b>--dnssec-timestamp=&lt;path&gt;</b></dt>
<dd>Enables an alternative way of checking the validity of the system time for DNSSEC (see <b>--dnssec-no-timecheck</b>). In this case, the
system time is considered to be valid once it becomes later than the timestamp on the specified file. The file is created and
its timestamp set automatically by dnsmasq. The file must be stored on a persistent filesystem, so that it and its mtime are carried
over system restarts. The timestamp file is created after dnsmasq has dropped root, so it must be in a location writable by the
unprivileged user that dnsmasq runs as.</dd>
@@ -0,0 +1,11 @@
<dt><b>--do-0x20-encode, --no-0x20-encode</b></dt>
<dd>Dnsmasq can scramble the case of letters in DNS queries it sends upstream as a security feature.
This technique can interact badly with rare broken DNS servers which don't preserve the case
of the query in their reply. The first time a reply is returned
which matches the query in all respects except case, a warning
will be logged. If this coincides with DNS not functioning, it
is necessary to disable the feature. As at version 2.91, 0x20 encoding
is disabled by default, and must be enabled with --do-0x20-encode. The default
may change in the future, so to be sure of its status after an upgrade, set --do-0x20-encode
or --no-0x20-encode in your config. --no-0x20-encode overrides --do-x20-encode or a future default
0x20-encode enable.</dd>
@@ -0,0 +1,2 @@
<dt><b>--dumpfile=&lt;path/to/file&gt;</b></dt>
<dd>Specify the location of a pcap-format file which dnsmasq uses to dump copies of network packets for debugging purposes. If the file exists when dnsmasq starts, it is not deleted; new packets are added to the end. The file may be a named-pipe which Wireshark is listening to.</dd>
@@ -0,0 +1,3 @@
<dt><b>--dumpmask=&lt;mask&gt;</b></dt>
<dd>Specify which types of packets should be added to the dumpfile. The argument should be the OR of the bitmasks for each type of packet to be dumped: it can be specified in hex by preceding the number with 0x in the normal way. Each time a packet is written to the dumpfile, dnsmasq logs the packet sequence and the mask
representing its type. The current types are: 0x0001 - DNS queries from clients, 0x0002 DNS replies to clients, 0x0004 - DNS queries to upstream, 0x0008 - DNS replies from upstream, 0x0010 - queries send upstream for DNSSEC validation, 0x0020 - replies to queries for DNSSEC validation, 0x0040 - replies to client queries which fail DNSSEC validation, 0x0080 replies to queries for DNSSEC validation which fail validation, 0x1000 - DHCPv4, 0x2000 - DHCPv6, 0x4000 - Router advertisement, 0x8000 - TFTP.</dd>
@@ -0,0 +1,11 @@
<dt><b>--leasequery[=&lt;addr&gt;[/&lt;prefix&gt;]]</b></dt>
<dd>Enable RFC 4388 leasequery. The dnsmasq DHCP server will answer LEASEQUERY messages from DHCP relays
when this option is given. If an address or subnet is given, only queries from a relay at that source are allowed. Repeat
the --leasequery option to specify multiple allowed sources.
To correctly answer lease queries it is necessary to store extra data in
the lease database, and this is also enabled by the <b>--leasequery</b> option. The extra fields
(agent-info and vendorclass) are stored in the leases file in a somewhat backwards compatible manner.
Enabling and then disabling leasequery will not cause problems; the extra information will be aged
out of the database. However, enabling leasequery in release 2.92 or later, storing leases
which come via DHCP relays which add option-82 agent-info data, and then moving back to a pre-2.92 dnsmasq
binary may cause problems reading the leases file. As of release 2.92, leasequery is only supported for DHCPv4.</dd>
@@ -0,0 +1,5 @@
<dt><b>-L, --localmx</b></dt>
<dd>Return an MX record pointing to the host given by <b>--mx-target</b> (or the
machine on which dnsmasq is running) for each
local machine. Local machines are those in /etc/hosts or with DHCP
leases.</dd>
@@ -0,0 +1,6 @@
<dt><b>-5, --no-ping</b></dt>
<dd>(IPv4 only) By default, the DHCP server will attempt to ensure that an address is
not in use before allocating it to a host. It does this by sending an
ICMP echo request (aka "ping") to the address in question. If it gets
a reply, then the address must already be in use, and another is
tried. This flag disables this check. Use with caution.</dd>
@@ -0,0 +1,5 @@
<dt><b>--no-round-robin</b></dt>
<dd>Dnsmasq normally permutes the order of A or AAAA records for the same
name on successive queries, for load-balancing. This turns off that
behaviour, so that the records are always returned in the order
that they are received from upstream.</dd>
@@ -0,0 +1,5 @@
<dt><b>--quiet-dhcp, --quiet-dhcp6, --quiet-ra, --quiet-tftp</b></dt>
<dd>Suppress logging of the routine operation of these protocols. Errors and
problems will still be logged. <b>--quiet-tftp</b> does not consider file not
found to be an error. <b>--quiet-dhcp</b> and quiet-dhcp6 are over-ridden by
<b>--log-dhcp</b>.</dd>
@@ -0,0 +1,5 @@
<dt><b>--quiet-dhcp, --quiet-dhcp6, --quiet-ra, --quiet-tftp</b></dt>
<dd>Suppress logging of the routine operation of these protocols. Errors and
problems will still be logged. <b>--quiet-tftp</b> does not consider file not
found to be an error. <b>--quiet-dhcp</b> and quiet-dhcp6 are over-ridden by
<b>--log-dhcp</b>.</dd>
@@ -0,0 +1,5 @@
<dt><b>--quiet-dhcp, --quiet-dhcp6, --quiet-ra, --quiet-tftp</b></dt>
<dd>Suppress logging of the routine operation of these protocols. Errors and
problems will still be logged. <b>--quiet-tftp</b> does not consider file not
found to be an error. <b>--quiet-dhcp</b> and quiet-dhcp6 are over-ridden by
<b>--log-dhcp</b>.</dd>
@@ -0,0 +1,5 @@
<dt><b>--quiet-dhcp, --quiet-dhcp6, --quiet-ra, --quiet-tftp</b></dt>
<dd>Suppress logging of the routine operation of these protocols. Errors and
problems will still be logged. <b>--quiet-tftp</b> does not consider file not
found to be an error. <b>--quiet-dhcp</b> and quiet-dhcp6 are over-ridden by
<b>--log-dhcp</b>.</dd>
@@ -0,0 +1,2 @@
<dt><b>--script-arp</b></dt>
<dd>Enable the "arp" and "arp-old" functions in the <b>--dhcp-script</b> and <b>--dhcp-luascript</b>.</dd>
@@ -0,0 +1,3 @@
<dt><b>--script-on-renewal</b></dt>
<dd>Call the DHCP script when the lease expiry time changes, for instance when the
lease is renewed.</dd>
@@ -0,0 +1,3 @@
<dt><b>-e, --selfmx</b></dt>
<dd>Return an MX record pointing to itself for each local
machine. Local machines are those in /etc/hosts or with DHCP leases.</dd>
@@ -0,0 +1,2 @@
<dt><b>--shared-network=&lt;interface&gt;,&lt;addr&gt;</b></dt>
<dd></dd>
@@ -0,0 +1,22 @@
<dt><b>--synth-domain=&lt;domain&gt;,&lt;address range&gt;[,&lt;prefix&gt;[*]]</b></dt>
<dd>Create artificial A/AAAA and PTR records for an address range. The
records either seqential numbers or the address, with periods (or colons for IPv6) replaced with dashes.
<p>
An examples should make this clearer. First sequential numbers.
<b>--synth-domain=thekelleys.org.uk,192.168.0.50,192.168.0.70,internal-*</b>
results in the name internal-0.thekelleys.org.uk. returning 192.168.0.50, internal-1.thekelleys.org.uk returning 192.168.0.51 and so on. (note the *) The same principle applies to IPv6 addresses (where the numbers may be very large). Reverse lookups from address to name behave as expected.
<p>
Second,
<b>--synth-domain=thekelleys.org.uk,192.168.0.0/24,internal- (no *)</b>
will result in a query for internal-192-168-0-56.thekelleys.org.uk returning
192.168.0.56 and a reverse query vice versa. The same applies to IPv6;
the representation doesn't use the :: compression feature or
the special representation of V4 mapped IPv6 addresses as these
can generate illegal domain names, so all domains are of the form
internal-1000-0000-0000-0000-0000-0000-0000-0008.example.com
<p>
The address range can be of the form
&lt;start address&gt;,&lt;end address&gt; or &lt;ip address&gt;/&lt;prefix-length&gt; in both forms of the option. For IPv6 the start and end addresses
must fall in the same /64 network, or prefix-length must be greater than or equal to 64 except that shorter prefix lengths than 64 are allowed only if non-sequential names are in use.</dd>
@@ -0,0 +1,16 @@
<dt><b>--tag-if=set:&lt;tag&gt;[,set:&lt;tag&gt;[,tag:&lt;tag&gt;[,tag:&lt;tag&gt;]]]</b></dt>
<dd>Perform boolean operations on tags. All <b>set:&lt;tag&gt;</b> tags are set if
all <b>tag:&lt;tag&gt;</b> are already set, (or unset when <b>tag:!&lt;tag&gt;</b> is used).
If no <b>tag:&lt;tag&gt;</b> appears, <b>set:&lt;tag&gt;</b> tags are set unconditionally.
Any number of <b>set:</b> and <b>tag:</b> forms may appear, in any order.
<b>--tag-if</b> lines are executed in order, so if the tag in <b>tag:&lt;tag&gt;</b> is a
tag set by another
<b>--tag-if,</b>
the line which sets the tag must precede the line which tests it.
<p>
As an extension, the <b>tag:&lt;tag&gt;</b> clauses support limited wildcard matching,
similar to the matching in the <b>--interface</b> directive. This allows the
example <b>--tag-if=set:ppp,tag:ppp*</b> to set the tag 'ppp' for all requests
received on any matching interface (ppp0, ppp1, etc). This can be used in conjunction
with the <b>tag:!&lt;tag&gt;</b> format meaning that no tag matching the wildcard may be set.</dd>
@@ -0,0 +1,7 @@
<dt><b>--umbrella[=[deviceid:&lt;deviceid&gt;][,orgid:&lt;orgid&gt;][,assetid:&lt;id&gt;]]</b></dt>
<dd>Embeds the requestor's IP address in DNS queries forwarded upstream.
If device id or, asset id or organization id are specified, the information is
included in the forwarded queries and may be able to be used in
filtering policies and reporting. The order of the id
attributes is irrelevant, but they must be separated by a comma. Deviceid is
a sixteen digit hexadecimal number, org and asset ids are decimal numbers.</dd>
@@ -0,0 +1,6 @@
<dt><b>--use-stale-cache[=&lt;max TTL excess in s&gt;]</b></dt>
<dd>When set, if a DNS name exists in the cache, but its time-to-live has expired, dnsmasq will return the data anyway. (It attempts to refresh the
data with an upstream query after returning the stale data.) This can improve speed and reliability. It comes at the expense
of sometimes returning out-of-date data and less efficient cache utilisation, since old data cannot be flushed when its TTL expires, so the cache becomes
mostly least-recently-used. To mitigate issues caused by massively outdated DNS replies, the maximum overaging of cached records can be specified in seconds
(defaulting to not serve anything older than one day). Setting the TTL excess time to zero will serve stale cache data regardless how long it has expired.</dd>
+1 -1
View File
@@ -13,7 +13,7 @@ Source for the Docker test harness: `scripts/prepare-test-mount.sh` syncs this d
| **dnsmasq.d/** | Included configs (conf-dir; alphabetical order). In the harness, "Config files (load order)" should list main, then these, then zz-dnsmasq-webui.conf. | | **dnsmasq.d/** | Included configs (conf-dir; alphabetical order). In the harness, "Config files (load order)" should list main, then these, then zz-dnsmasq-webui.conf. |
| **dnsmasq.d/01-other.conf** | `domain=local`. Not managed. | | **dnsmasq.d/01-other.conf** | `domain=local`. Not managed. |
| **dnsmasq.d/02-servers.conf** | Extra `server=` / `local=` (readonly in UI). | | **dnsmasq.d/02-servers.conf** | Extra `server=` / `local=` (readonly in UI). |
| **dnsmasq.d/03-more.conf** | `resolv-file=`, `dhcp-option=`. Not managed. | | **dnsmasq.d/03-more.conf** | `resolv-file=`, `dhcp-option=`, `no-round-robin`, `quiet-dhcp`, `dns-forward-max=`, `use-stale-cache=`. Not managed. |
| **dnsmasq.d/dhcp.conf** | `dhcp-host=`; managed by app in real use, used as-is in harness. | | **dnsmasq.d/dhcp.conf** | `dhcp-host=`; managed by app in real use, used as-is in harness. |
| **hosts** | First addn-hosts file; system hosts when `Dnsmasq:SystemHostsPath` points here. | | **hosts** | First addn-hosts file; system hosts when `Dnsmasq:SystemHostsPath` points here. |
| **hosts.extra** | Second addn-hosts file. | | **hosts.extra** | Second addn-hosts file. |
+6 -1
View File
@@ -1,4 +1,9 @@
# Extra options so effective config has resolv-file and dhcp-option (multi-value). Not managed by app. # Extra options so effective config has resolv-file, dhcp-option (multi-value), and newer options. Not managed by app.
# resolv-file must point to a file that exists in the container (/data is the mount). # resolv-file must point to a file that exists in the container (/data is the mount).
resolv-file=/data/resolv.dnsmasq resolv-file=/data/resolv.dnsmasq
dhcp-option=3,172.28.0.1 dhcp-option=3,172.28.0.1
# New options (readonly in UI when using this testdata)
no-round-robin
quiet-dhcp
dns-forward-max=150
use-stale-cache=60