mirror of
https://github.com/alexhopeoconnor/dnsmasq-webui.git
synced 2026-10-03 22:12:00 +10:00
Release v0.0.5: option help modal, update check, effective config sections
- Config option help: modal with dnsmasq docs, label click/mouseleave - Update check: background + manual check, nav footer tooltip with last check time - Effective config: sectioned views (DNS, DHCP, etc.), section toggle - Version bump to 0.0.5
This commit is contained in:
Executable
+253
@@ -0,0 +1,253 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Extract per-option HTML from the dnsmasq man page for use as field help in the web UI.
|
||||||
|
#
|
||||||
|
# Runs a temporary Python (BeautifulSoup) container to fetch the page, parse the OPTIONS
|
||||||
|
# <DL>/<DT>/<DD> blocks, and write one .html file per option. The container is removed
|
||||||
|
# when done. If the page URL or structure changes, pass a new --url.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/extract-option-help.sh [--url URL] [--output-dir DIR]
|
||||||
|
#
|
||||||
|
# Requires: Docker (python:3-slim image will be pulled if missing)
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
DEFAULT_URL="https://thekelleys.org.uk/dnsmasq/docs/dnsmasq-man.html"
|
||||||
|
DEFAULT_OUTPUT_DIR="src/DnsmasqWebUI/wwwroot/option-help"
|
||||||
|
URL="$DEFAULT_URL"
|
||||||
|
OUTPUT_DIR="$DEFAULT_OUTPUT_DIR"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--url) URL="$2"; shift 2 ;;
|
||||||
|
--output-dir) OUTPUT_DIR="$2"; shift 2 ;;
|
||||||
|
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ "$OUTPUT_DIR" != /* ]] && OUTPUT_DIR="$PWD/$OUTPUT_DIR"
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
SCRIPT_FILE="$(mktemp)"
|
||||||
|
trap 'rm -f "$SCRIPT_FILE"' EXIT
|
||||||
|
|
||||||
|
# Embed Python script (uses requests + BeautifulSoup; runs inside container)
|
||||||
|
cat << 'PYTHON_SCRIPT' > "$SCRIPT_FILE"
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
except ImportError:
|
||||||
|
sys.stderr.write("Installing requests and beautifulsoup4...\n")
|
||||||
|
import subprocess
|
||||||
|
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "requests", "beautifulsoup4"])
|
||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
OUT_DIR = "/out"
|
||||||
|
URL = os.environ.get("URL", "https://thekelleys.org.uk/dnsmasq/docs/dnsmasq-man.html")
|
||||||
|
|
||||||
|
# One per line; keep in sync with DnsmasqOptionTooltips / EffectiveConfigSections. server + local = one UI row.
|
||||||
|
OPTION_KEYS = [
|
||||||
|
"no-hosts",
|
||||||
|
"addn-hosts",
|
||||||
|
"hostsdir",
|
||||||
|
"read-ethers",
|
||||||
|
"server",
|
||||||
|
"local",
|
||||||
|
"rev-server",
|
||||||
|
"address",
|
||||||
|
"resolv-file",
|
||||||
|
"no-resolv",
|
||||||
|
"domain-needed",
|
||||||
|
"port",
|
||||||
|
"log-queries",
|
||||||
|
"strict-order",
|
||||||
|
"all-servers",
|
||||||
|
"auth-ttl",
|
||||||
|
"edns-packet-max",
|
||||||
|
"query-port",
|
||||||
|
"port-limit",
|
||||||
|
"min-port",
|
||||||
|
"max-port",
|
||||||
|
"dns-loop-detect",
|
||||||
|
"stop-dns-rebind",
|
||||||
|
"rebind-localhost-ok",
|
||||||
|
"clear-on-reload",
|
||||||
|
"expand-hosts",
|
||||||
|
"bogus-priv",
|
||||||
|
"rebind-domain-ok",
|
||||||
|
"bogus-nxdomain",
|
||||||
|
"ignore-address",
|
||||||
|
"alias",
|
||||||
|
"filter-rr",
|
||||||
|
"filterwin2k",
|
||||||
|
"filter-A",
|
||||||
|
"filter-AAAA",
|
||||||
|
"localise-queries",
|
||||||
|
"fast-dns-retry",
|
||||||
|
"ipset",
|
||||||
|
"nftset",
|
||||||
|
"domain",
|
||||||
|
"cname",
|
||||||
|
"mx-host",
|
||||||
|
"srv-host",
|
||||||
|
"ptr-record",
|
||||||
|
"txt-record",
|
||||||
|
"naptr-record",
|
||||||
|
"host-record",
|
||||||
|
"dynamic-host",
|
||||||
|
"interface-name",
|
||||||
|
"mx-target",
|
||||||
|
"localmx",
|
||||||
|
"selfmx",
|
||||||
|
"dhcp-authoritative",
|
||||||
|
"dhcp-rapid-commit",
|
||||||
|
"leasefile-ro",
|
||||||
|
"dhcp-script",
|
||||||
|
"dhcp-leasefile",
|
||||||
|
"dhcp-lease-max",
|
||||||
|
"dhcp-ttl",
|
||||||
|
"dhcp-range",
|
||||||
|
"dhcp-host",
|
||||||
|
"dhcp-option",
|
||||||
|
"dhcp-option-force",
|
||||||
|
"dhcp-match",
|
||||||
|
"dhcp-mac",
|
||||||
|
"dhcp-name-match",
|
||||||
|
"dhcp-ignore-names",
|
||||||
|
"dhcp-hostsfile",
|
||||||
|
"dhcp-optsfile",
|
||||||
|
"dhcp-hostsdir",
|
||||||
|
"dhcp-boot",
|
||||||
|
"dhcp-ignore",
|
||||||
|
"dhcp-vendorclass",
|
||||||
|
"dhcp-userclass",
|
||||||
|
"ra-param",
|
||||||
|
"slaac",
|
||||||
|
"enable-tftp",
|
||||||
|
"tftp-secure",
|
||||||
|
"tftp-no-fail",
|
||||||
|
"tftp-no-blocksize",
|
||||||
|
"tftp-root",
|
||||||
|
"pxe-prompt",
|
||||||
|
"pxe-service",
|
||||||
|
"dnssec",
|
||||||
|
"dnssec-check-unsigned",
|
||||||
|
"proxy-dnssec",
|
||||||
|
"trust-anchor",
|
||||||
|
"cache-rr",
|
||||||
|
"cache-size",
|
||||||
|
"local-ttl",
|
||||||
|
"no-negcache",
|
||||||
|
"neg-ttl",
|
||||||
|
"max-ttl",
|
||||||
|
"max-cache-ttl",
|
||||||
|
"min-cache-ttl",
|
||||||
|
"no-poll",
|
||||||
|
"bind-interfaces",
|
||||||
|
"bind-dynamic",
|
||||||
|
"log-debug",
|
||||||
|
"log-async",
|
||||||
|
"local-service",
|
||||||
|
"interface",
|
||||||
|
"listen-address",
|
||||||
|
"except-interface",
|
||||||
|
"auth-server",
|
||||||
|
"no-dhcp-interface",
|
||||||
|
"no-dhcpv4-interface",
|
||||||
|
"no-dhcpv6-interface",
|
||||||
|
"pid-file",
|
||||||
|
"user",
|
||||||
|
"group",
|
||||||
|
"log-facility",
|
||||||
|
"enable-dbus",
|
||||||
|
"enable-ubus",
|
||||||
|
"enable-ra",
|
||||||
|
"log-dhcp",
|
||||||
|
"keep-in-foreground",
|
||||||
|
"no-daemon",
|
||||||
|
"conntrack",
|
||||||
|
]
|
||||||
|
|
||||||
|
long_opt_re = re.compile(r"--([a-z][a-z0-9-]*)(?:=[^,\s]*)?", re.IGNORECASE)
|
||||||
|
|
||||||
|
def option_names_from_dt(dt):
|
||||||
|
return [m.group(1).lower() for m in long_opt_re.finditer(dt.get_text())]
|
||||||
|
|
||||||
|
def main():
|
||||||
|
r = requests.get(URL, timeout=30)
|
||||||
|
r.raise_for_status()
|
||||||
|
soup = BeautifulSoup(r.text, "html.parser")
|
||||||
|
h2 = None
|
||||||
|
for el in soup.find_all("h2"):
|
||||||
|
if el.get_text(strip=True).upper() == "OPTIONS":
|
||||||
|
h2 = el
|
||||||
|
break
|
||||||
|
if not h2:
|
||||||
|
sys.stderr.write("Could not find OPTIONS H2\n")
|
||||||
|
sys.exit(1)
|
||||||
|
dl = h2.find_next("dl")
|
||||||
|
if not dl:
|
||||||
|
sys.stderr.write("Could not find DL after OPTIONS\n")
|
||||||
|
sys.exit(1)
|
||||||
|
# Build option -> (dt_html, dd_html) from raw HTML; split by <dt> so we get one block per option.
|
||||||
|
raw_dl = str(dl)
|
||||||
|
option_to_block = {}
|
||||||
|
blocks = re.split(r"<dt\b", raw_dl, flags=re.IGNORECASE)
|
||||||
|
for block in blocks[1:]:
|
||||||
|
p_dt = block.find("</dt>") if "</dt>" in block.lower() else len(block) + 1
|
||||||
|
m_dd = re.search(r"<dd\b", block, re.IGNORECASE)
|
||||||
|
p_dd = m_dd.start() if m_dd else len(block) + 1
|
||||||
|
end_pos = min(p_dt, p_dd)
|
||||||
|
if end_pos > len(block):
|
||||||
|
continue
|
||||||
|
dt_part = block[:end_pos].strip()
|
||||||
|
rest = block[end_pos:]
|
||||||
|
if rest.lower().startswith("</dt>"):
|
||||||
|
rest = rest[6:].lstrip()
|
||||||
|
if ">" in dt_part:
|
||||||
|
dt_part = dt_part.split(">", 1)[1]
|
||||||
|
dt_html = "<dt>" + dt_part + "</dt>"
|
||||||
|
dd_match = re.search(r"<dd\b[^>]*>(.*)", rest, re.DOTALL | re.IGNORECASE)
|
||||||
|
dd_body = dd_match.group(1) if dd_match else ""
|
||||||
|
dd_body = re.split(r"</dd>|<dt\s|<dt>", dd_body, flags=re.IGNORECASE)[0].strip()
|
||||||
|
dd_html = "<dd>" + dd_body + "</dd>" if dd_body else "<dd></dd>"
|
||||||
|
soup_dt = BeautifulSoup(dt_html, "html.parser").find("dt")
|
||||||
|
if not soup_dt:
|
||||||
|
continue
|
||||||
|
for n in option_names_from_dt(soup_dt):
|
||||||
|
if n not in option_to_block:
|
||||||
|
option_to_block[n] = (dt_html, dd_html)
|
||||||
|
os.makedirs(OUT_DIR, exist_ok=True)
|
||||||
|
written = 0
|
||||||
|
for key in OPTION_KEYS:
|
||||||
|
norm = key.lower()
|
||||||
|
if norm not in option_to_block:
|
||||||
|
continue
|
||||||
|
dt_html, dd_html = option_to_block[norm]
|
||||||
|
frag = "\n".join([dt_html, dd_html])
|
||||||
|
safe = re.sub(r"[^a-z0-9-]", "", norm) or key
|
||||||
|
path = os.path.join(OUT_DIR, f"{safe}.html")
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(frag)
|
||||||
|
written += 1
|
||||||
|
print(f"Wrote {written} files to {OUT_DIR}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
PYTHON_SCRIPT
|
||||||
|
|
||||||
|
echo "Fetching man page and extracting option help (Docker python:3-slim) ..."
|
||||||
|
docker run --rm \
|
||||||
|
-v "$OUTPUT_DIR:/out" \
|
||||||
|
-v "$SCRIPT_FILE:/script.py:ro" \
|
||||||
|
-e "URL=$URL" \
|
||||||
|
-e "PIP_ROOT_USER_ACTION=ignore" \
|
||||||
|
python:3-slim \
|
||||||
|
sh -c "pip install -q requests beautifulsoup4 && python /script.py"
|
||||||
|
|
||||||
|
echo "Done. Files in $OUTPUT_DIR"
|
||||||
@@ -5,8 +5,9 @@ using DnsmasqWebUI.Infrastructure.Parsers;
|
|||||||
namespace DnsmasqWebUI.Tests;
|
namespace DnsmasqWebUI.Tests;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Positive parser tests for the 16 added options: key wired and "option present" behaviour.
|
/// Positive parser tests for added options: key wired and "option present" behaviour.
|
||||||
/// Ensures key names in DnsmasqConfKeys match what the parser expects (complement to OfficialExampleTests which assert "all commented → false/empty").
|
/// Ensures key names in DnsmasqConfKeys match what the parser expects (complement to OfficialExampleTests which assert "all commented → false/empty").
|
||||||
|
/// Covers process flags (keep-in-foreground, no-daemon), DHCP include paths (dhcp-hostsfile, dhcp-optsfile, dhcp-hostsdir), proxy-dnssec, conntrack.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DnsmasqConfIncludeParserNewOptionsTests
|
public class DnsmasqConfIncludeParserNewOptionsTests
|
||||||
{
|
{
|
||||||
@@ -60,6 +61,24 @@ public class DnsmasqConfIncludeParserNewOptionsTests
|
|||||||
WriteFlagAndAssertTrue(DnsmasqConfKeys.LogDhcp);
|
WriteFlagAndAssertTrue(DnsmasqConfKeys.LogDhcp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetFlagFromConfigFiles_KeepInForeground_WhenPresent_ReturnsTrue()
|
||||||
|
{
|
||||||
|
WriteFlagAndAssertTrue(DnsmasqConfKeys.KeepInForeground);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetFlagFromConfigFiles_NoDaemon_WhenPresent_ReturnsTrue()
|
||||||
|
{
|
||||||
|
WriteFlagAndAssertTrue(DnsmasqConfKeys.NoDaemon);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetFlagFromConfigFiles_ProxyDnssec_WhenPresent_ReturnsTrue()
|
||||||
|
{
|
||||||
|
WriteFlagAndAssertTrue(DnsmasqConfKeys.ProxyDnssec);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Multi-value (6): key=value line(s) → GetMultiValue returns value(s) ---
|
// --- Multi-value (6): key=value line(s) → GetMultiValue returns value(s) ---
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -158,6 +177,54 @@ public class DnsmasqConfIncludeParserNewOptionsTests
|
|||||||
finally { Directory.Delete(dir, recursive: true); }
|
finally { Directory.Delete(dir, recursive: true); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetMultiValueFromConfigFiles_DhcpHostsfile_WhenSet_ReturnsValues()
|
||||||
|
{
|
||||||
|
var dir = CreateTempDir();
|
||||||
|
var conf = Path.Combine(dir, "dnsmasq.conf");
|
||||||
|
var path = "/etc/dnsmasq.d/hosts.d";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(conf, $"dhcp-hostsfile={path}\n");
|
||||||
|
var result = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(new[] { conf }, DnsmasqConfKeys.DhcpHostsfile);
|
||||||
|
Assert.Single(result);
|
||||||
|
Assert.Equal(path, result[0]);
|
||||||
|
}
|
||||||
|
finally { Directory.Delete(dir, recursive: true); }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetMultiValueFromConfigFiles_DhcpOptsfile_WhenSet_ReturnsValues()
|
||||||
|
{
|
||||||
|
var dir = CreateTempDir();
|
||||||
|
var conf = Path.Combine(dir, "dnsmasq.conf");
|
||||||
|
var path = "/etc/dnsmasq.d/opts.conf";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(conf, $"dhcp-optsfile={path}\n");
|
||||||
|
var result = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(new[] { conf }, DnsmasqConfKeys.DhcpOptsfile);
|
||||||
|
Assert.Single(result);
|
||||||
|
Assert.Equal(path, result[0]);
|
||||||
|
}
|
||||||
|
finally { Directory.Delete(dir, recursive: true); }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetMultiValueFromConfigFiles_DhcpHostsdir_WhenSet_ReturnsValues()
|
||||||
|
{
|
||||||
|
var dir = CreateTempDir();
|
||||||
|
var conf = Path.Combine(dir, "dnsmasq.conf");
|
||||||
|
var path = "/etc/dnsmasq.d/hosts.d";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(conf, $"dhcp-hostsdir={path}\n");
|
||||||
|
var result = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(new[] { conf }, DnsmasqConfKeys.DhcpHostsdir);
|
||||||
|
Assert.Single(result);
|
||||||
|
Assert.Equal(path, result[0]);
|
||||||
|
}
|
||||||
|
finally { Directory.Delete(dir, recursive: true); }
|
||||||
|
}
|
||||||
|
|
||||||
// --- Single-value (2): key=value → GetLastValue returns value ---
|
// --- Single-value (2): key=value → GetLastValue returns value ---
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -190,6 +257,21 @@ public class DnsmasqConfIncludeParserNewOptionsTests
|
|||||||
finally { Directory.Delete(dir, recursive: true); }
|
finally { Directory.Delete(dir, recursive: true); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetLastValueFromConfigFiles_Conntrack_WhenSet_ReturnsValue()
|
||||||
|
{
|
||||||
|
var dir = CreateTempDir();
|
||||||
|
var conf = Path.Combine(dir, "dnsmasq.conf");
|
||||||
|
var value = "42";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(conf, $"conntrack={value}\n");
|
||||||
|
var (result, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(new[] { conf }, DnsmasqConfKeys.Conntrack);
|
||||||
|
Assert.Equal(value, result);
|
||||||
|
}
|
||||||
|
finally { Directory.Delete(dir, recursive: true); }
|
||||||
|
}
|
||||||
|
|
||||||
private static void WriteFlagAndAssertTrue(string key)
|
private static void WriteFlagAndAssertTrue(string key)
|
||||||
{
|
{
|
||||||
var dir = CreateTempDir();
|
var dir = CreateTempDir();
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ public class EffectiveDnsmasqConfigTests
|
|||||||
DhcpMacValues: Array.Empty<string>(),
|
DhcpMacValues: Array.Empty<string>(),
|
||||||
DhcpNameMatchValues: Array.Empty<string>(),
|
DhcpNameMatchValues: Array.Empty<string>(),
|
||||||
DhcpIgnoreNamesValues: Array.Empty<string>(),
|
DhcpIgnoreNamesValues: Array.Empty<string>(),
|
||||||
|
DhcpHostsfilePaths: Array.Empty<string>(),
|
||||||
|
DhcpOptsfilePaths: Array.Empty<string>(),
|
||||||
|
DhcpHostsdirPaths: Array.Empty<string>(),
|
||||||
ExpandHosts: false,
|
ExpandHosts: false,
|
||||||
BogusPriv: false,
|
BogusPriv: false,
|
||||||
StrictOrder: false,
|
StrictOrder: false,
|
||||||
@@ -83,6 +86,9 @@ public class EffectiveDnsmasqConfigTests
|
|||||||
Selfmx: false,
|
Selfmx: false,
|
||||||
EnableRa: false,
|
EnableRa: false,
|
||||||
LogDhcp: false,
|
LogDhcp: false,
|
||||||
|
KeepInForeground: false,
|
||||||
|
NoDaemon: false,
|
||||||
|
ProxyDnssec: false,
|
||||||
DhcpLeaseFilePath: null,
|
DhcpLeaseFilePath: null,
|
||||||
CacheSize: null,
|
CacheSize: null,
|
||||||
Port: null,
|
Port: null,
|
||||||
@@ -112,7 +118,8 @@ public class EffectiveDnsmasqConfigTests
|
|||||||
EnableUbus: null,
|
EnableUbus: null,
|
||||||
FastDnsRetry: null,
|
FastDnsRetry: null,
|
||||||
DhcpScriptPath: null,
|
DhcpScriptPath: null,
|
||||||
MxTarget: null
|
MxTarget: null,
|
||||||
|
Conntrack: null
|
||||||
);
|
);
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<Routes @rendermode="InteractiveServer" />
|
<Routes @rendermode="InteractiveServer" />
|
||||||
|
<script src="js/ec-option-help.js"></script>
|
||||||
<script src="_framework/blazor.web.js"></script>
|
<script src="_framework/blazor.web.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
|
||||||
|
@using DnsmasqWebUI.Extensions
|
||||||
|
@using Microsoft.AspNetCore.Components
|
||||||
|
@using Microsoft.JSInterop
|
||||||
|
@using System.Text.Json.Serialization
|
||||||
|
@inject IHttpClientFactory HttpFactory
|
||||||
|
@inject IJSRuntime JSRuntime
|
||||||
|
@implements IAsyncDisposable
|
||||||
|
|
||||||
|
@if (Visible)
|
||||||
|
{
|
||||||
|
<div class="config-option-help-popover" id="@_popoverId" style="@_positionStyle" @onmouseenter="OnModalMouseEnter" @onmouseleave="OnModalMouseLeave">
|
||||||
|
<div class="config-option-help-popover-content">
|
||||||
|
<div class="config-option-help-popover-header">
|
||||||
|
<span class="config-option-help-popover-title">@Title</span>
|
||||||
|
</div>
|
||||||
|
<div class="config-option-help-popover-body">
|
||||||
|
@if (_loading)
|
||||||
|
{
|
||||||
|
<p class="text-muted small mb-0">Loading…</p>
|
||||||
|
}
|
||||||
|
else if (_error != null)
|
||||||
|
{
|
||||||
|
<p class="text-danger small mb-0">@_error</p>
|
||||||
|
}
|
||||||
|
else if (_helpHtml != null)
|
||||||
|
{
|
||||||
|
<div class="option-help">@((MarkupString)_helpHtml)</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public string? HelpKey { get; set; }
|
||||||
|
[Parameter] public string? AnchorId { get; set; }
|
||||||
|
[Parameter] public bool Visible { get; set; }
|
||||||
|
[Parameter] public string Title { get; set; } = "Option help";
|
||||||
|
[Parameter] public EventCallback OnClose { get; set; }
|
||||||
|
[Parameter] public EventCallback OnMouseEnterModal { get; set; }
|
||||||
|
[Parameter] public EventCallback OnMouseLeaveModal { get; set; }
|
||||||
|
|
||||||
|
private string? _helpHtml;
|
||||||
|
private string? _error;
|
||||||
|
private bool _loading;
|
||||||
|
private string? _lastKey;
|
||||||
|
private string _positionStyle = "";
|
||||||
|
private readonly string _popoverId = "config-option-help-popover-" + Guid.NewGuid().ToString("N")[..8];
|
||||||
|
private DotNetObjectReference<ConfigOptionHelpModal>? _dotNetRef;
|
||||||
|
private bool _scrollListenerAdded;
|
||||||
|
private bool _mouseOverModal;
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (Visible && !_scrollListenerAdded)
|
||||||
|
{
|
||||||
|
_scrollListenerAdded = true;
|
||||||
|
_dotNetRef = DotNetObjectReference.Create(this);
|
||||||
|
await JSRuntime.InvokeVoidAsync("addConfigOptionHelpScrollCloseListener", _dotNetRef);
|
||||||
|
}
|
||||||
|
else if (!Visible && _scrollListenerAdded)
|
||||||
|
{
|
||||||
|
_scrollListenerAdded = false;
|
||||||
|
await JSRuntime.InvokeVoidAsync("removeConfigOptionHelpScrollCloseListener");
|
||||||
|
_dotNetRef?.Dispose();
|
||||||
|
_dotNetRef = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JSInvokable]
|
||||||
|
public async Task OnScrollClose()
|
||||||
|
{
|
||||||
|
await PositionUnderAnchorAsync();
|
||||||
|
StateHasChanged();
|
||||||
|
if (!_mouseOverModal)
|
||||||
|
await OnClose.InvokeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if (_scrollListenerAdded)
|
||||||
|
{
|
||||||
|
_scrollListenerAdded = false;
|
||||||
|
try { await JSRuntime.InvokeVoidAsync("removeConfigOptionHelpScrollCloseListener"); } catch { /* best effort */ }
|
||||||
|
_dotNetRef?.Dispose();
|
||||||
|
_dotNetRef = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnParametersSetAsync()
|
||||||
|
{
|
||||||
|
if (!Visible || string.IsNullOrEmpty(HelpKey))
|
||||||
|
{
|
||||||
|
_helpHtml = null;
|
||||||
|
_error = null;
|
||||||
|
_loading = false;
|
||||||
|
_positionStyle = "";
|
||||||
|
_mouseOverModal = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (HelpKey == _lastKey && _helpHtml != null)
|
||||||
|
{
|
||||||
|
await PositionUnderAnchorAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_lastKey = HelpKey;
|
||||||
|
_loading = true;
|
||||||
|
_error = null;
|
||||||
|
_helpHtml = null;
|
||||||
|
_positionStyle = "visibility: hidden;"; // avoid flash before position is set
|
||||||
|
StateHasChanged();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var http = HttpFactory.CreateClient(ServiceCollectionExtensions.DnsmasqApiClientName);
|
||||||
|
var response = await http.GetAsync($"option-help/{HelpKey}.html");
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
_helpHtml = await response.Content.ReadAsStringAsync();
|
||||||
|
else
|
||||||
|
_error = "Help not available for this option.";
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_error = "Could not load help.";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_loading = false;
|
||||||
|
await PositionUnderAnchorAsync();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PositionUnderAnchorAsync()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(AnchorId)) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var rect = await JSRuntime.InvokeAsync<BoundingRect?>("getConfigOptionHelpAnchorRect", AnchorId);
|
||||||
|
if (rect is { } r)
|
||||||
|
{
|
||||||
|
var gap = 4;
|
||||||
|
var topPx = r.Bottom + gap;
|
||||||
|
var leftPx = r.Left;
|
||||||
|
_positionStyle = $"position: fixed; top: {topPx}px; left: {leftPx}px; visibility: visible;";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_positionStyle = "visibility: visible;";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnModalMouseEnter()
|
||||||
|
{
|
||||||
|
_mouseOverModal = true;
|
||||||
|
await OnMouseEnterModal.InvokeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnModalMouseLeave()
|
||||||
|
{
|
||||||
|
_mouseOverModal = false;
|
||||||
|
await OnMouseLeaveModal.InvokeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record BoundingRect(
|
||||||
|
[property: JsonPropertyName("top")] double Top,
|
||||||
|
[property: JsonPropertyName("left")] double Left,
|
||||||
|
[property: JsonPropertyName("bottom")] double Bottom,
|
||||||
|
[property: JsonPropertyName("width")] double Width,
|
||||||
|
[property: JsonPropertyName("height")] double Height);
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/* Popover anchored under the label (position set in code from anchor rect) */
|
||||||
|
.config-option-help-popover {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1060;
|
||||||
|
max-width: min(420px, calc(100vw - 1rem));
|
||||||
|
max-height: min(70vh, 400px);
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid var(--bs-border-color, #dee2e6);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||||
|
background: var(--bs-body-bg, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-option-help-popover-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-option-help-popover-header {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-bottom: 1px solid var(--bs-border-color-translucent, rgba(0, 0, 0, 0.075));
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-option-help-popover-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-option-help-popover-body {
|
||||||
|
padding: 0.75rem;
|
||||||
|
overflow: auto;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Option help content: man-page fragments (dt/dd, b, p, a) */
|
||||||
|
.config-option-help-popover-body .option-help dt {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
.config-option-help-popover-body .option-help dt:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.config-option-help-popover-body .option-help dd {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
border-left: 2px solid var(--bs-border-color-translucent, rgba(0, 0, 0, 0.1));
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
.config-option-help-popover-body .option-help dd p {
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
}
|
||||||
|
.config-option-help-popover-body .option-help dd p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.config-option-help-popover-body .option-help b {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.config-option-help-popover-body .option-help a {
|
||||||
|
color: var(--bs-link-color, #0d6efd);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
+24
-8
@@ -1,6 +1,8 @@
|
|||||||
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
|
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
|
||||||
@using DnsmasqWebUI.Models.Dnsmasq
|
@using DnsmasqWebUI.Models.Dnsmasq
|
||||||
|
@using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig
|
||||||
@using DnsmasqWebUI.Models.Config
|
@using DnsmasqWebUI.Models.Config
|
||||||
|
@using DnsmasqWebUI.Infrastructure.Helpers.Config
|
||||||
@using DnsmasqWebUI.Infrastructure.Services.Abstractions
|
@using DnsmasqWebUI.Infrastructure.Services.Abstractions
|
||||||
@inject IEffectiveConfigRenderFragmentRegistry RenderFragmentRegistry
|
@inject IEffectiveConfigRenderFragmentRegistry RenderFragmentRegistry
|
||||||
|
|
||||||
@@ -9,36 +11,50 @@
|
|||||||
{
|
{
|
||||||
var labelTooltip = DnsmasqOptionTooltips.Get(Descriptor.OptionName);
|
var labelTooltip = DnsmasqOptionTooltips.Get(Descriptor.OptionName);
|
||||||
<li class="ec-field" title="@(Descriptor.GetSource()?.GetReadOnlyTooltip())">
|
<li class="ec-field" title="@(Descriptor.GetSource()?.GetReadOnlyTooltip())">
|
||||||
<span class="ec-field-label" title="@(labelTooltip ?? Descriptor.GetSource()?.GetReadOnlyTooltip())"><strong>@Descriptor.OptionName:</strong></span>
|
<span id="@_anchorId" class="ec-field-label @(_optionHelpKey != null ? "ec-field-label-help" : null)" title="@(labelTooltip ?? Descriptor.GetSource()?.GetReadOnlyTooltip())" @onclick="RaiseOptionHelpRequested" @onmouseleave="RaiseLabelMouseLeave"><strong>@Descriptor.OptionName:</strong></span>
|
||||||
<span class="ec-field-value">@fragment(Descriptor)</span>
|
<span class="ec-field-value">
|
||||||
@if (Descriptor.GetSource()?.IsReadOnly == true)
|
@fragment(Descriptor)
|
||||||
{
|
@if (Descriptor.GetSource()?.IsReadOnly == true)
|
||||||
<span class="badge bg-secondary ms-1">readonly</span>
|
{
|
||||||
}
|
<span class="badge bg-secondary ec-badge-inline">readonly</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
else if (Descriptor.IsMultiValue)
|
else if (Descriptor.IsMultiValue)
|
||||||
{
|
{
|
||||||
var items = Descriptor.GetItems();
|
var items = Descriptor.GetItems();
|
||||||
<EffectiveConfigMultiValueRow Label="@Descriptor.OptionName" Items="@items" LabelTooltip="@_labelTooltip" />
|
<EffectiveConfigMultiValueRow Label="@Descriptor.OptionName" Items="@items" LabelTooltip="@_labelTooltip" OptionHelpKey="@_optionHelpKey" OptionHelpAnchorId="@_anchorId" OnOptionHelpRequested="@OnOptionHelpRequested" />
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var value = Descriptor.GetValue();
|
var value = Descriptor.GetValue();
|
||||||
var displayValue = value == null ? "(not set)" : (value is IReadOnlyList<string> list ? (list.Count == 0 ? "(none)" : string.Join(", ", list)) : value.ToString());
|
var displayValue = value == null ? "(not set)" : (value is IReadOnlyList<string> list ? (list.Count == 0 ? "(none)" : string.Join(", ", list)) : value.ToString());
|
||||||
<EffectiveConfigOptionItem Label="@Descriptor.OptionName" DisplayValue="@(displayValue ?? "(not set)")" Source="@Descriptor.GetSource()" LabelTooltip="@_labelTooltip" />
|
<EffectiveConfigOptionItem Label="@Descriptor.OptionName" DisplayValue="@(displayValue ?? "(not set)")" Source="@Descriptor.GetSource()" LabelTooltip="@_labelTooltip" OptionHelpKey="@_optionHelpKey" OptionHelpAnchorId="@_anchorId" OnOptionHelpRequested="@OnOptionHelpRequested" />
|
||||||
}
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter] public EffectiveConfigFieldDescriptor Descriptor { get; set; } = null!;
|
[Parameter] public EffectiveConfigFieldDescriptor Descriptor { get; set; } = null!;
|
||||||
[CascadingParameter] public EffectiveConfigDisplayMode DisplayMode { get; set; } = EffectiveConfigDisplayMode.View;
|
[CascadingParameter] public EffectiveConfigDisplayMode DisplayMode { get; set; } = EffectiveConfigDisplayMode.View;
|
||||||
|
[Parameter] public EventCallback<ConfigOptionHelpRequestEventArgs> OnOptionHelpRequested { get; set; }
|
||||||
|
|
||||||
|
private readonly string _anchorId = "ec-help-" + Guid.NewGuid().ToString("N")[..8];
|
||||||
private RenderFragment<EffectiveConfigFieldDescriptor>? _customFragment;
|
private RenderFragment<EffectiveConfigFieldDescriptor>? _customFragment;
|
||||||
private string? _labelTooltip;
|
private string? _labelTooltip;
|
||||||
|
private string? _optionHelpKey;
|
||||||
|
|
||||||
protected override void OnParametersSet()
|
protected override void OnParametersSet()
|
||||||
{
|
{
|
||||||
_customFragment = RenderFragmentRegistry.GetDisplayFragment(Descriptor.SectionId, Descriptor.OptionName);
|
_customFragment = RenderFragmentRegistry.GetDisplayFragment(Descriptor.SectionId, Descriptor.OptionName);
|
||||||
_labelTooltip = DnsmasqOptionTooltips.Get(Descriptor.OptionName);
|
_labelTooltip = DnsmasqOptionTooltips.Get(Descriptor.OptionName);
|
||||||
|
_optionHelpKey = DnsmasqOptionTooltips.GetOptionHelpKey(Descriptor.OptionName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task RaiseOptionHelpRequested()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_optionHelpKey)) return;
|
||||||
|
await OnOptionHelpRequested.InvokeAsync(new ConfigOptionHelpRequestEventArgs { HelpKey = _optionHelpKey, OptionLabel = Descriptor.OptionName, AnchorId = _anchorId });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RaiseLabelMouseLeave() => await OnOptionHelpRequested.InvokeAsync(new ConfigOptionHelpRequestEventArgs { AnchorId = _anchorId, IsLabelMouseLeave = true });
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
@Value
|
@Value
|
||||||
@if (Source?.IsReadOnly == true)
|
@if (Source?.IsReadOnly == true)
|
||||||
{
|
{
|
||||||
<span class="badge bg-secondary ms-1">readonly</span>
|
<span class="badge bg-secondary ec-badge-inline">readonly</span>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|||||||
+17
-1
@@ -1,8 +1,9 @@
|
|||||||
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
|
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
|
||||||
@using DnsmasqWebUI.Models.Dnsmasq
|
@using DnsmasqWebUI.Models.Dnsmasq
|
||||||
|
@using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig
|
||||||
|
|
||||||
<li class="ec-field">
|
<li class="ec-field">
|
||||||
<span class="ec-field-label" title="@LabelTooltip"><strong>@Label:</strong></span>
|
<span id="@OptionHelpAnchorId" class="ec-field-label @(OptionHelpKey != null ? "ec-field-label-help" : null)" title="@LabelTooltip" @onclick="RaiseOptionHelpRequested" @onmouseleave="RaiseLabelMouseLeave"><strong>@Label:</strong></span>
|
||||||
@if (DisplayMode == EffectiveConfigDisplayMode.Edit)
|
@if (DisplayMode == EffectiveConfigDisplayMode.Edit)
|
||||||
{
|
{
|
||||||
<span class="ec-field-value ec-field-value-multi">
|
<span class="ec-field-value ec-field-value-multi">
|
||||||
@@ -34,10 +35,25 @@
|
|||||||
[Parameter] public IReadOnlyList<ValueWithSource>? Items { get; set; }
|
[Parameter] public IReadOnlyList<ValueWithSource>? Items { get; set; }
|
||||||
[Parameter] public EventCallback<IReadOnlyList<string>> ItemsChanged { get; set; }
|
[Parameter] public EventCallback<IReadOnlyList<string>> ItemsChanged { get; set; }
|
||||||
[Parameter] public string? LabelTooltip { get; set; }
|
[Parameter] public string? LabelTooltip { get; set; }
|
||||||
|
[Parameter] public string? OptionHelpKey { get; set; }
|
||||||
|
[Parameter] public string? OptionHelpAnchorId { get; set; }
|
||||||
[CascadingParameter] public EffectiveConfigDisplayMode DisplayMode { get; set; } = EffectiveConfigDisplayMode.View;
|
[CascadingParameter] public EffectiveConfigDisplayMode DisplayMode { get; set; } = EffectiveConfigDisplayMode.View;
|
||||||
|
[Parameter] public EventCallback<ConfigOptionHelpRequestEventArgs> OnOptionHelpRequested { get; set; }
|
||||||
|
|
||||||
private string _editLines = "";
|
private string _editLines = "";
|
||||||
|
|
||||||
|
private async Task RaiseOptionHelpRequested()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(OptionHelpKey) || string.IsNullOrEmpty(OptionHelpAnchorId)) return;
|
||||||
|
await OnOptionHelpRequested.InvokeAsync(new ConfigOptionHelpRequestEventArgs { HelpKey = OptionHelpKey, OptionLabel = Label, AnchorId = OptionHelpAnchorId });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RaiseLabelMouseLeave()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(OptionHelpAnchorId)) return;
|
||||||
|
await OnOptionHelpRequested.InvokeAsync(new ConfigOptionHelpRequestEventArgs { AnchorId = OptionHelpAnchorId, IsLabelMouseLeave = true });
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnParametersSet()
|
protected override void OnParametersSet()
|
||||||
{
|
{
|
||||||
if (Items != null && Items.Count > 0)
|
if (Items != null && Items.Count > 0)
|
||||||
|
|||||||
+24
-6
@@ -1,8 +1,9 @@
|
|||||||
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
|
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
|
||||||
@using DnsmasqWebUI.Models.Dnsmasq
|
@using DnsmasqWebUI.Models.Dnsmasq
|
||||||
|
@using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig
|
||||||
|
|
||||||
<li class="ec-field" title="@(Source?.GetReadOnlyTooltip())">
|
<li class="ec-field" title="@(Source?.GetReadOnlyTooltip())">
|
||||||
<span class="ec-field-label" title="@(LabelTooltip ?? Source?.GetReadOnlyTooltip())"><strong>@Label:</strong></span>
|
<span id="@OptionHelpAnchorId" class="ec-field-label @(OptionHelpKey != null ? "ec-field-label-help" : null)" title="@(LabelTooltip ?? Source?.GetReadOnlyTooltip())" @onclick="RaiseOptionHelpRequested" @onmouseleave="RaiseLabelMouseLeave"><strong>@Label:</strong></span>
|
||||||
@if (DisplayMode == EffectiveConfigDisplayMode.Edit && Source?.IsReadOnly != true)
|
@if (DisplayMode == EffectiveConfigDisplayMode.Edit && Source?.IsReadOnly != true)
|
||||||
{
|
{
|
||||||
<span class="ec-field-value">
|
<span class="ec-field-value">
|
||||||
@@ -11,11 +12,13 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span class="ec-field-value">@DisplayValue</span>
|
<span class="ec-field-value">
|
||||||
@if (Source?.IsReadOnly == true)
|
@DisplayValue
|
||||||
{
|
@if (Source?.IsReadOnly == true)
|
||||||
<span class="badge bg-secondary ms-1">readonly</span>
|
{
|
||||||
}
|
<span class="badge bg-secondary ec-badge-inline">readonly</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
}
|
}
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
@@ -25,9 +28,24 @@
|
|||||||
[Parameter] public ConfigValueSource? Source { get; set; }
|
[Parameter] public ConfigValueSource? Source { get; set; }
|
||||||
/// <summary>Optional explainer shown on hover over the label (e.g. from DnsmasqOptionTooltips).</summary>
|
/// <summary>Optional explainer shown on hover over the label (e.g. from DnsmasqOptionTooltips).</summary>
|
||||||
[Parameter] public string? LabelTooltip { get; set; }
|
[Parameter] public string? LabelTooltip { get; set; }
|
||||||
|
[Parameter] public string? OptionHelpKey { get; set; }
|
||||||
|
[Parameter] public string? OptionHelpAnchorId { get; set; }
|
||||||
/// <summary>When Edit and not readonly: raised when the user changes the value. Not yet wired to save.</summary>
|
/// <summary>When Edit and not readonly: raised when the user changes the value. Not yet wired to save.</summary>
|
||||||
[Parameter] public EventCallback<string> DisplayValueChanged { get; set; }
|
[Parameter] public EventCallback<string> DisplayValueChanged { get; set; }
|
||||||
[CascadingParameter] public EffectiveConfigDisplayMode DisplayMode { get; set; } = EffectiveConfigDisplayMode.View;
|
[CascadingParameter] public EffectiveConfigDisplayMode DisplayMode { get; set; } = EffectiveConfigDisplayMode.View;
|
||||||
|
[Parameter] public EventCallback<ConfigOptionHelpRequestEventArgs> OnOptionHelpRequested { get; set; }
|
||||||
|
|
||||||
|
private async Task RaiseOptionHelpRequested()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(OptionHelpKey) || string.IsNullOrEmpty(OptionHelpAnchorId)) return;
|
||||||
|
await OnOptionHelpRequested.InvokeAsync(new ConfigOptionHelpRequestEventArgs { HelpKey = OptionHelpKey, OptionLabel = Label, AnchorId = OptionHelpAnchorId });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RaiseLabelMouseLeave()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(OptionHelpAnchorId)) return;
|
||||||
|
await OnOptionHelpRequested.InvokeAsync(new ConfigOptionHelpRequestEventArgs { AnchorId = OptionHelpAnchorId, IsLabelMouseLeave = true });
|
||||||
|
}
|
||||||
|
|
||||||
private async Task OnValueInput(ChangeEventArgs e)
|
private async Task OnValueInput(ChangeEventArgs e)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,43 +1,118 @@
|
|||||||
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
|
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
|
||||||
@using DnsmasqWebUI.Models.Dnsmasq
|
@using DnsmasqWebUI.Models.Dnsmasq
|
||||||
|
@using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig
|
||||||
|
@implements IAsyncDisposable
|
||||||
|
|
||||||
@if (Status != null)
|
@if (Status != null)
|
||||||
{
|
{
|
||||||
<div class="ec-sections small">
|
<div class="ec-container">
|
||||||
@{
|
<ConfigOptionHelpModal HelpKey="@_optionHelpKey" AnchorId="@_optionHelpAnchorId" Visible="@_showOptionHelp" Title="@_optionHelpTitle"
|
||||||
var descriptors = EffectiveConfigFieldBuilder.BuildFieldDescriptors(Status);
|
OnClose="CloseOptionHelp" OnMouseEnterModal="CancelCloseTimerAsync" OnMouseLeaveModal="ScheduleCloseAsync" />
|
||||||
var sections = new[]
|
@if (ShowSearchBox)
|
||||||
{
|
|
||||||
(EffectiveConfigFieldBuilder.SectionHosts, "Hosts"),
|
|
||||||
(EffectiveConfigFieldBuilder.SectionResolver, "Resolver / DNS"),
|
|
||||||
(EffectiveConfigFieldBuilder.SectionDnsRecords, "DNS records"),
|
|
||||||
(EffectiveConfigFieldBuilder.SectionDhcp, "DHCP"),
|
|
||||||
(EffectiveConfigFieldBuilder.SectionTftpPxe, "TFTP / PXE"),
|
|
||||||
(EffectiveConfigFieldBuilder.SectionDnssec, "DNSSEC"),
|
|
||||||
(EffectiveConfigFieldBuilder.SectionCache, "Cache"),
|
|
||||||
(EffectiveConfigFieldBuilder.SectionProcess, "Process & networking")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@foreach (var (sectionId, title) in sections)
|
|
||||||
{
|
{
|
||||||
var sectionDescriptors = descriptors.Where(d => d.SectionId == sectionId).ToList();
|
<div class="ec-toolbar">
|
||||||
if (sectionDescriptors.Count > 0)
|
<input type="search" id="ec-search-input" class="form-control form-control-sm" placeholder="Search..." autocomplete="off"
|
||||||
{
|
@bind="_searchTerm" @bind:event="oninput" @bind:after="StateHasChanged" />
|
||||||
<EffectiveConfigSectionPanel SectionId="@sectionId" Title="@title" Descriptors="@sectionDescriptors" IsOpen="@IsOpen(sectionId)" OnToggle="@OnSectionToggle" DisplayMode="@DisplayMode" />
|
<button type="button" class="btn btn-sm btn-outline-secondary" @onclick="CollapseAll" title="Collapse all sections" aria-label="Collapse all sections">
|
||||||
}
|
<i class="bi bi-arrows-collapse" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
<div class="ec-sections small">
|
||||||
|
@{
|
||||||
|
var views = EffectiveConfigViews.GetViewsForContext(Context);
|
||||||
|
var descriptorsBySection = EffectiveConfigViews.GetDescriptorsBySection(Status, views);
|
||||||
|
var hasSearch = !string.IsNullOrWhiteSpace(_searchTerm);
|
||||||
|
var term = _searchTerm?.Trim() ?? "";
|
||||||
|
}
|
||||||
|
@foreach (var view in views)
|
||||||
|
{
|
||||||
|
if (!descriptorsBySection.TryGetValue(view.SectionId, out var contextDescriptors))
|
||||||
|
continue;
|
||||||
|
var sectionDescriptors = hasSearch
|
||||||
|
? contextDescriptors.Where(d => DescriptorMatchesSearch(d, term)).ToList()
|
||||||
|
: contextDescriptors.ToList();
|
||||||
|
if (sectionDescriptors.Count == 0)
|
||||||
|
continue;
|
||||||
|
var isOpen = hasSearch ? true : IsOpen(view.SectionId);
|
||||||
|
<EffectiveConfigSectionPanel SectionId="@view.SectionId" Title="@view.Title" Descriptors="@sectionDescriptors" IsOpen="@isOpen" OnToggle="@OnSectionToggle" DisplayMode="@DisplayMode" OnOptionHelpRequested="@HandleOptionHelpRequested" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter] public DnsmasqServiceStatus? Status { get; set; }
|
[Parameter] public DnsmasqServiceStatus? Status { get; set; }
|
||||||
|
[Parameter] public EffectiveConfigContext Context { get; set; } = EffectiveConfigContext.All;
|
||||||
|
[Parameter] public bool ShowSearchBox { get; set; } = true;
|
||||||
/// <summary>View = read-only; Edit = editable (future). Cascaded to field displays.</summary>
|
/// <summary>View = read-only; Edit = editable (future). Cascaded to field displays.</summary>
|
||||||
[Parameter] public EffectiveConfigDisplayMode DisplayMode { get; set; } = EffectiveConfigDisplayMode.View;
|
[Parameter] public EffectiveConfigDisplayMode DisplayMode { get; set; } = EffectiveConfigDisplayMode.View;
|
||||||
|
|
||||||
|
private string _searchTerm = "";
|
||||||
private HashSet<string> _openPanels = new();
|
private HashSet<string> _openPanels = new();
|
||||||
|
private string? _optionHelpKey;
|
||||||
|
private string? _optionHelpAnchorId;
|
||||||
|
private string _optionHelpTitle = "Option help";
|
||||||
|
private bool _showOptionHelp;
|
||||||
|
private CancellationTokenSource? _closeCts;
|
||||||
|
|
||||||
private bool IsOpen(string id) => _openPanels.Contains(id);
|
private bool IsOpen(string id) => _openPanels.Contains(id);
|
||||||
|
|
||||||
|
private async Task HandleOptionHelpRequested(ConfigOptionHelpRequestEventArgs args)
|
||||||
|
{
|
||||||
|
if (args.IsLabelMouseLeave)
|
||||||
|
{
|
||||||
|
if (args.AnchorId == _optionHelpAnchorId)
|
||||||
|
ScheduleClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_optionHelpKey = args.HelpKey;
|
||||||
|
_optionHelpTitle = args.OptionLabel;
|
||||||
|
_optionHelpAnchorId = args.AnchorId;
|
||||||
|
_showOptionHelp = true;
|
||||||
|
CancelCloseTimer();
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelCloseTimer()
|
||||||
|
{
|
||||||
|
_closeCts?.Cancel();
|
||||||
|
_closeCts = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task CancelCloseTimerAsync() { CancelCloseTimer(); return Task.CompletedTask; }
|
||||||
|
|
||||||
|
private void ScheduleClose()
|
||||||
|
{
|
||||||
|
_closeCts?.Cancel();
|
||||||
|
_closeCts = new CancellationTokenSource();
|
||||||
|
var token = _closeCts.Token;
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(300, token);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { return; }
|
||||||
|
await InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
if (token.IsCancellationRequested) return;
|
||||||
|
_showOptionHelp = false;
|
||||||
|
_closeCts = null;
|
||||||
|
StateHasChanged();
|
||||||
|
});
|
||||||
|
}, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task ScheduleCloseAsync() { ScheduleClose(); return Task.CompletedTask; }
|
||||||
|
|
||||||
|
private void CloseOptionHelp()
|
||||||
|
{
|
||||||
|
CancelCloseTimer();
|
||||||
|
_showOptionHelp = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
private void OnSectionToggle(string id)
|
private void OnSectionToggle(string id)
|
||||||
{
|
{
|
||||||
if (_openPanels.Contains(id))
|
if (_openPanels.Contains(id))
|
||||||
@@ -45,4 +120,25 @@
|
|||||||
else
|
else
|
||||||
_openPanels.Add(id);
|
_openPanels.Add(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CollapseAll()
|
||||||
|
{
|
||||||
|
_openPanels.Clear();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool DescriptorMatchesSearch(EffectiveConfigFieldDescriptor d, string term)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(term)) return true;
|
||||||
|
var searchable = (d.OptionName + " " + (DnsmasqOptionTooltips.Get(d.OptionName) ?? "")).Trim();
|
||||||
|
return searchable.Contains(term, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
_closeCts?.Cancel();
|
||||||
|
_closeCts?.Dispose();
|
||||||
|
_closeCts = null;
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+72
-7
@@ -1,9 +1,35 @@
|
|||||||
/* Effective config sections – mobile-first, touch-friendly.
|
/* Effective config sections – mobile-first, touch-friendly.
|
||||||
Use ::deep so styles apply to EffectiveConfigSectionPanel (child component) markup. */
|
Use ::deep so styles apply to EffectiveConfigSectionPanel (child component) markup. */
|
||||||
|
|
||||||
|
/* Wrapper: border and grouping around all sections. Margin-top separates from config files list above. */
|
||||||
|
.ec-container {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
border: 1px solid var(--bs-border-color, #dee2e6);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: var(--bs-secondary-bg, #f8f9fa);
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||||
|
min-width: 0;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ec-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ec-toolbar input[type="search"] {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.ec-sections {
|
.ec-sections {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
::deep .ec-section {
|
::deep .ec-section {
|
||||||
@@ -14,7 +40,7 @@
|
|||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Touch-friendly header: min 44px tap target (WCAG 2.5.5), breathing room from edges */
|
/* Section header: clear separation from background, touch-friendly (min 44px), distinct so search jumps are obvious */
|
||||||
::deep .ec-section-header {
|
::deep .ec-section-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -24,20 +50,22 @@
|
|||||||
padding: 0.625rem 1rem;
|
padding: 0.625rem 1rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border: none;
|
border: none;
|
||||||
|
border-left: 3px solid var(--bs-border-color, #dee2e6);
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
background: transparent;
|
background-color: var(--bs-body-bg, #fff);
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
transition: background-color 0.15s ease, color 0.15s ease;
|
transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
::deep .ec-section-header:hover {
|
::deep .ec-section-header:hover {
|
||||||
color: var(--bs-primary, #0d6efd);
|
color: var(--bs-primary, #0d6efd);
|
||||||
background-color: var(--bs-secondary-bg, rgba(0, 0, 0, 0.04));
|
background-color: var(--bs-secondary-bg, rgba(0, 0, 0, 0.04));
|
||||||
|
border-left-color: var(--bs-primary, #0d6efd);
|
||||||
}
|
}
|
||||||
|
|
||||||
::deep .ec-section-header:focus-visible {
|
::deep .ec-section-header:focus-visible {
|
||||||
@@ -45,15 +73,17 @@
|
|||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Open state: header reads as "active" and clearly separates from body */
|
/* Open state: header reads as "active", left border accent */
|
||||||
::deep .ec-section-open .ec-section-header {
|
::deep .ec-section-open .ec-section-header {
|
||||||
background-color: var(--bs-secondary-bg, rgba(0, 0, 0, 0.05));
|
background-color: var(--bs-body-bg, #fff);
|
||||||
color: var(--bs-emphasis-color, inherit);
|
color: var(--bs-emphasis-color, inherit);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
border-left-color: var(--bs-primary, #0d6efd);
|
||||||
|
box-shadow: 0 1px 0 var(--bs-border-color, #dee2e6);
|
||||||
}
|
}
|
||||||
|
|
||||||
::deep .ec-section-open .ec-section-header:hover {
|
::deep .ec-section-open .ec-section-header:hover {
|
||||||
background-color: var(--bs-tertiary-bg, rgba(0, 0, 0, 0.08));
|
background-color: var(--bs-secondary-bg, rgba(0, 0, 0, 0.04));
|
||||||
color: var(--bs-primary, #0d6efd);
|
color: var(--bs-primary, #0d6efd);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +109,8 @@
|
|||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
border-left: 2px solid var(--bs-border-color-translucent, rgba(0, 0, 0, 0.075));
|
border-left: 2px solid var(--bs-border-color-translucent, rgba(0, 0, 0, 0.075));
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: visible;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
@@ -112,6 +144,17 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Clickable label: open full option help on click (hover tooltip unchanged) */
|
||||||
|
::deep .ec-field-label-help {
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-style: dotted;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
::deep .ec-field-label-help:hover {
|
||||||
|
color: var(--bs-primary, #0d6efd);
|
||||||
|
}
|
||||||
|
|
||||||
::deep .ec-field-value {
|
::deep .ec-field-value {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -119,21 +162,26 @@
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Multi-value: full width below label, indented ordered list */
|
/* Multi-value: full width below label, indented ordered list. min-width: 0 so flex doesn't prevent wrapping. */
|
||||||
::deep .ec-field-value-multi {
|
::deep .ec-field-value-multi {
|
||||||
flex-basis: 100%;
|
flex-basis: 100%;
|
||||||
|
min-width: 0;
|
||||||
padding-left: 1.25rem;
|
padding-left: 1.25rem;
|
||||||
margin-top: 0.15rem;
|
margin-top: 0.15rem;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
::deep .ec-field-value-list {
|
::deep .ec-field-value-list {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-left: 1.25rem;
|
padding-left: 1.25rem;
|
||||||
list-style-type: decimal;
|
list-style-type: decimal;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
::deep .ec-field-value-list li {
|
::deep .ec-field-value-list li {
|
||||||
margin-bottom: 0.2rem;
|
margin-bottom: 0.2rem;
|
||||||
|
min-width: 0;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
@@ -142,14 +190,31 @@
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Long paths/URLs wrap; allow break anywhere so nothing is clipped */
|
||||||
::deep .ec-field-value-item {
|
::deep .ec-field-value-item {
|
||||||
display: inline;
|
display: inline;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
::deep .ec-field-value-sep {
|
::deep .ec-field-value-sep {
|
||||||
margin-right: 0.25rem;
|
margin-right: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Badge: vertically centered, inline with value so when it wraps it stays part of the value flow */
|
||||||
|
::deep .ec-field .badge,
|
||||||
|
::deep .ec-field-value-item .badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline badge (inside value): gap from text; when it wraps to own line the margin gives a subtle indent */
|
||||||
|
::deep .ec-badge-inline {
|
||||||
|
margin-left: 0.35rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
@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;
|
||||||
|
|||||||
+4
-1
@@ -1,5 +1,6 @@
|
|||||||
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
|
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
|
||||||
@using DnsmasqWebUI.Models.Dnsmasq
|
@using DnsmasqWebUI.Models.Dnsmasq
|
||||||
|
@using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig
|
||||||
|
|
||||||
<CascadingValue Value="DisplayMode">
|
<CascadingValue Value="DisplayMode">
|
||||||
<div class="ec-section @(IsOpen ? "ec-section-open" : "")">
|
<div class="ec-section @(IsOpen ? "ec-section-open" : "")">
|
||||||
@@ -12,7 +13,7 @@
|
|||||||
<ul class="list-unstyled mb-0">
|
<ul class="list-unstyled mb-0">
|
||||||
@foreach (var f in Descriptors)
|
@foreach (var f in Descriptors)
|
||||||
{
|
{
|
||||||
<EffectiveConfigFieldDisplay Descriptor="@f" />
|
<EffectiveConfigFieldDisplay Descriptor="@f" OnOptionHelpRequested="@OnOptionHelpRequested" />
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -26,6 +27,8 @@
|
|||||||
[Parameter] public IReadOnlyList<EffectiveConfigFieldDescriptor> Descriptors { get; set; } = null!;
|
[Parameter] public IReadOnlyList<EffectiveConfigFieldDescriptor> Descriptors { get; set; } = null!;
|
||||||
[Parameter] public bool IsOpen { get; set; }
|
[Parameter] public bool IsOpen { get; set; }
|
||||||
[Parameter] public EventCallback<string> OnToggle { get; set; }
|
[Parameter] public EventCallback<string> OnToggle { get; set; }
|
||||||
|
/// <summary>Raised when user requests option help (click or mouse leave from label). Passed through from Section.</summary>
|
||||||
|
[Parameter] public EventCallback<ConfigOptionHelpRequestEventArgs> OnOptionHelpRequested { get; set; }
|
||||||
/// <summary>Cascaded from EffectiveConfigSection. View = read-only; Edit = editable (future).</summary>
|
/// <summary>Cascaded from EffectiveConfigSection. View = read-only; Edit = editable (future).</summary>
|
||||||
[Parameter] public EffectiveConfigDisplayMode DisplayMode { get; set; } = EffectiveConfigDisplayMode.View;
|
[Parameter] public EffectiveConfigDisplayMode DisplayMode { get; set; } = EffectiveConfigDisplayMode.View;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
@using System.Reflection
|
@using System.Reflection
|
||||||
@inject IOptions<ApplicationOptions> AppOptions
|
@inject IOptions<ApplicationOptions> AppOptions
|
||||||
@inject ISettingsModalService SettingsModalService
|
@inject ISettingsModalService SettingsModalService
|
||||||
|
@inject IUpdateCheckService UpdateCheckService
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
<div class="top-row ps-3 navbar navbar-dark">
|
<div class="top-row ps-3 navbar navbar-dark">
|
||||||
<div class="top-row-inner">
|
<div class="top-row-inner">
|
||||||
@@ -36,10 +38,33 @@
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="nav-footer px-3 py-2" aria-label="Application version">
|
<div class="nav-footer px-3 py-2" aria-label="Links and version">
|
||||||
<a class="nav-footer-link" href="@ReleaseUrl" target="_blank" rel="noopener noreferrer" title="View release on GitHub">
|
<a class="nav-footer-link" href="https://github.com/alexhopeoconnor/dnsmasq-webui" target="_blank" rel="noopener noreferrer" title="dnsmasq-webui project on GitHub">Project</a>
|
||||||
v@(BaseVersion(Version))
|
<span class="nav-footer-sep" aria-hidden="true">·</span>
|
||||||
</a>
|
<a class="nav-footer-link" href="https://thekelleys.org.uk/dnsmasq/doc.html" target="_blank" rel="noopener noreferrer" title="Official dnsmasq documentation">Docs</a>
|
||||||
|
<span class="nav-footer-sep" aria-hidden="true">·</span>
|
||||||
|
<a class="nav-footer-link" href="@ReleaseUrl" target="_blank" rel="noopener noreferrer" title="View release on GitHub">v@(BaseVersion(Version))</a>
|
||||||
|
<span class="nav-footer-version-actions" aria-hidden="true">[
|
||||||
|
@if (UpdateCheckService.NewerVersionAvailable)
|
||||||
|
{
|
||||||
|
<a class="nav-footer-link nav-footer-new-release" href="@UpdateCheckService.NewerVersionUrl" target="_blank" rel="noopener noreferrer" title="New version @(UpdateCheckService.NewerVersionTag ?? "") is available" aria-label="New version @(UpdateCheckService.NewerVersionTag ?? "") is available">
|
||||||
|
<i class="bi bi-download" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<button type="button" class="nav-footer-link nav-footer-check-btn" @onclick="ForceCheckAsync" disabled="@UpdateCheckService.CheckInProgress" title="@GetCheckButtonTooltip()" aria-label="@GetCheckButtonTooltip()">
|
||||||
|
@if (UpdateCheckService.CheckInProgress)
|
||||||
|
{
|
||||||
|
<i class="bi bi-arrow-repeat nav-footer-spin" aria-hidden="true"></i>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<i class="bi bi-arrow-repeat" aria-hidden="true"></i>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
]</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -47,6 +72,7 @@
|
|||||||
private const string ReleasesBaseUrl = "https://github.com/alexhopeoconnor/dnsmasq-webui/releases";
|
private const string ReleasesBaseUrl = "https://github.com/alexhopeoconnor/dnsmasq-webui/releases";
|
||||||
private static readonly string Version = typeof(NavMenu).Assembly
|
private static readonly string Version = typeof(NavMenu).Assembly
|
||||||
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "?";
|
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "?";
|
||||||
|
|
||||||
/// <summary>Base version without build metadata (e.g. 0.0.4+abc123 -> 0.0.4) for GitHub tag URLs.</summary>
|
/// <summary>Base version without build metadata (e.g. 0.0.4+abc123 -> 0.0.4) for GitHub tag URLs.</summary>
|
||||||
private static string BaseVersion(string v)
|
private static string BaseVersion(string v)
|
||||||
{
|
{
|
||||||
@@ -59,5 +85,35 @@
|
|||||||
: ReleasesBaseUrl;
|
: ReleasesBaseUrl;
|
||||||
private string? AppTitle => AppOptions?.Value?.EffectiveTitle;
|
private string? AppTitle => AppOptions?.Value?.EffectiveTitle;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
UpdateCheckService.ResultChanged += OnUpdateCheckResultChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
UpdateCheckService.ResultChanged -= OnUpdateCheckResultChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUpdateCheckResultChanged(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
_ = InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetCheckButtonTooltip()
|
||||||
|
{
|
||||||
|
if (UpdateCheckService.CheckInProgress)
|
||||||
|
return "Checking…";
|
||||||
|
var last = UpdateCheckService.LastCheckTime;
|
||||||
|
if (last.HasValue)
|
||||||
|
return $"No new update available. Checked at {last.Value.ToLocalTime():t}. Check again.";
|
||||||
|
return "Check for updates";
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ForceCheckAsync()
|
||||||
|
{
|
||||||
|
await UpdateCheckService.CheckNowAsync();
|
||||||
|
}
|
||||||
|
|
||||||
private void OpenAllSettings() => SettingsModalService.Open(SettingsModalContext.All, "Settings");
|
private void OpenAllSettings() => SettingsModalService.Open(SettingsModalContext.All, "Settings");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,6 +135,7 @@
|
|||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-footer-link {
|
.nav-footer-link {
|
||||||
@@ -146,6 +147,58 @@
|
|||||||
color: rgba(255, 255, 255, 0.9) !important;
|
color: rgba(255, 255, 255, 0.9) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-footer-sep {
|
||||||
|
color: rgba(255, 255, 255, 0.45);
|
||||||
|
margin: 0 0.35rem;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-footer-version-actions {
|
||||||
|
margin-left: 0.15rem;
|
||||||
|
color: rgba(255, 255, 255, 0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-footer-version-actions .nav-footer-link,
|
||||||
|
.nav-footer-version-actions .nav-footer-check-btn {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-footer-new-release {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-footer-new-release:hover {
|
||||||
|
color: rgba(255, 255, 255, 0.9) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-footer-check-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-footer-check-btn:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-footer-check-btn:hover:not(:disabled) {
|
||||||
|
color: rgba(255, 255, 255, 0.9) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-footer-spin {
|
||||||
|
animation: nav-footer-spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes nav-footer-spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
.nav-scrollable {
|
.nav-scrollable {
|
||||||
display: none;
|
display: none;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -322,6 +322,11 @@ else
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="page-section">
|
||||||
|
<h2 class="page-section-title">Config affecting DHCP</h2>
|
||||||
|
<EffectiveConfigSection Status="_status" Context="EffectiveConfigContext.Dhcp" ShowSearchBox="true" />
|
||||||
|
</section>
|
||||||
}
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ else if (_status != null)
|
|||||||
<li><strong>Managed file:</strong> @(_status.ManagedFilePath ?? "—") @(_status.ManagedFilePathExists ? "✓" : "(not created yet)")</li>
|
<li><strong>Managed file:</strong> @(_status.ManagedFilePath ?? "—") @(_status.ManagedFilePathExists ? "✓" : "(not created yet)")</li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
<EffectiveConfigSection Status="_status" />
|
<EffectiveConfigSection Status="_status" Context="EffectiveConfigContext.All" ShowSearchBox="true" />
|
||||||
@if (_status.ReloadCommandConfigured)
|
@if (_status.ReloadCommandConfigured)
|
||||||
{
|
{
|
||||||
<div class="d-flex align-items-center gap-2 flex-wrap mt-3">
|
<div class="d-flex align-items-center gap-2 flex-wrap mt-3">
|
||||||
|
|||||||
@@ -15,37 +15,44 @@
|
|||||||
Loading...
|
Loading...
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
else if (string.IsNullOrEmpty(_status.ManagedHostsFilePath))
|
|
||||||
{
|
|
||||||
<p class="text-muted">Hosts editing is unavailable: managed hosts path is not configured. Set <strong>Dnsmasq:MainConfigPath</strong> (and optionally <strong>Dnsmasq:ManagedHostsFileName</strong>) so the app can create and edit the managed hosts file.</p>
|
|
||||||
@if (_status.AddnHostsPaths?.Count > 0)
|
|
||||||
{
|
|
||||||
<p class="text-muted small">Dnsmasq loads these addn-hosts files: @RenderAddnHostsList()</p>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@if (_status.DnsmasqStatus == "inactive" || _status.DnsmasqStatus == "unknown")
|
<section class="page-section mb-4">
|
||||||
|
<h2 class="page-section-title">Config affecting hosts</h2>
|
||||||
|
<EffectiveConfigSection Status="_status" Context="EffectiveConfigContext.Hosts" ShowSearchBox="true" />
|
||||||
|
</section>
|
||||||
|
@if (string.IsNullOrEmpty(_status.ManagedHostsFilePath))
|
||||||
{
|
{
|
||||||
<div class="alert alert-warning">Dnsmasq service appears to be <strong>@_status.DnsmasqStatus</strong>. Save will update the file but reload may fail until the service is fixed.</div>
|
<p class="text-muted">Hosts editing is unavailable: managed hosts path is not configured. Set <strong>Dnsmasq:MainConfigPath</strong> (and optionally <strong>Dnsmasq:ManagedHostsFileName</strong>) so the app can create and edit the managed hosts file.</p>
|
||||||
}
|
@if (_status.AddnHostsPaths?.Count > 0)
|
||||||
@if (_error != null)
|
|
||||||
{
|
|
||||||
<div class="alert alert-danger">@_error</div>
|
|
||||||
}
|
|
||||||
@if (_message != null)
|
|
||||||
{
|
|
||||||
<div class="alert alert-success">@_message</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<HostsFileSection Path="@(_status.ManagedHostsFilePath ?? "")" Entries="_entries" IsEditable="true" IsFirst="true" />
|
|
||||||
<button class="btn btn-primary mt-2" @onclick="Save" disabled="@_saving">Save</button>
|
|
||||||
|
|
||||||
@if (_readOnlyHosts?.Count > 0)
|
|
||||||
{
|
|
||||||
@foreach (var ro in _readOnlyHosts)
|
|
||||||
{
|
{
|
||||||
<HostsFileSection Path="@ro.Path" Entries="@ro.Entries" IsEditable="false" IsFirst="false" @key="@ro.Path" />
|
<p class="text-muted small">Dnsmasq loads these addn-hosts files: @RenderAddnHostsList()</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@if (_status.DnsmasqStatus == "inactive" || _status.DnsmasqStatus == "unknown")
|
||||||
|
{
|
||||||
|
<div class="alert alert-warning">Dnsmasq service appears to be <strong>@_status.DnsmasqStatus</strong>. Save will update the file but reload may fail until the service is fixed.</div>
|
||||||
|
}
|
||||||
|
@if (_error != null)
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger">@_error</div>
|
||||||
|
}
|
||||||
|
@if (_message != null)
|
||||||
|
{
|
||||||
|
<div class="alert alert-success">@_message</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<HostsFileSection Path="@(_status.ManagedHostsFilePath ?? "")" Entries="_entries" IsEditable="true" IsFirst="true" />
|
||||||
|
<button class="btn btn-primary mt-2" @onclick="Save" disabled="@_saving">Save</button>
|
||||||
|
|
||||||
|
@if (_readOnlyHosts?.Count > 0)
|
||||||
|
{
|
||||||
|
@foreach (var ro in _readOnlyHosts)
|
||||||
|
{
|
||||||
|
<HostsFileSection Path="@ro.Path" Entries="@ro.Entries" IsEditable="false" IsFirst="false" @key="@ro.Path" />
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<Version>0.0.4</Version>
|
<Version>0.0.5</Version>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<!-- Avoid pre-compressed .br/.gz so MapStaticAssets serves uncompressed files (fixes 0-byte/empty response bug). -->
|
<!-- Avoid pre-compressed .br/.gz so MapStaticAssets serves uncompressed files (fixes 0-byte/empty response bug). -->
|
||||||
<DisableBuildCompression>true</DisableBuildCompression>
|
<DisableBuildCompression>true</DisableBuildCompression>
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ public static class ServiceCollectionExtensions
|
|||||||
/// <summary>Name of the HttpClient used for same-host API calls (status, config, reload, hosts, etc.).</summary>
|
/// <summary>Name of the HttpClient used for same-host API calls (status, config, reload, hosts, etc.).</summary>
|
||||||
public const string DnsmasqApiClientName = "DnsmasqWebUI.Api";
|
public const string DnsmasqApiClientName = "DnsmasqWebUI.Api";
|
||||||
|
|
||||||
|
/// <summary>Name of the HttpClient used for GitHub API (e.g. latest release check).</summary>
|
||||||
|
public const string GitHubClientName = "DnsmasqWebUI.GitHub";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registers the named HttpClient for same-host API calls and all typed API clients
|
/// Registers the named HttpClient for same-host API calls and all typed API clients
|
||||||
/// (IStatusClient, IConfigSetClient, IReloadClient, IHostsClient, IDhcpHostsClient, ILeasesClient).
|
/// (IStatusClient, IConfigSetClient, IReloadClient, IHostsClient, IDhcpHostsClient, ILeasesClient).
|
||||||
@@ -32,6 +35,13 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddHttpClient<ILeasesClient, LeasesClient>(DnsmasqApiClientName);
|
services.AddHttpClient<ILeasesClient, LeasesClient>(DnsmasqApiClientName);
|
||||||
services.AddHttpClient<ILoggingClient, LoggingClient>(DnsmasqApiClientName);
|
services.AddHttpClient<ILoggingClient, LoggingClient>(DnsmasqApiClientName);
|
||||||
|
|
||||||
|
services.AddHttpClient(GitHubClientName, client =>
|
||||||
|
{
|
||||||
|
client.BaseAddress = new Uri("https://api.github.com/", UriKind.Absolute);
|
||||||
|
client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "dnsmasq-webui");
|
||||||
|
client.DefaultRequestHeaders.TryAddWithoutValidation("Accept", "application/vnd.github.v3+json");
|
||||||
|
});
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ public static class DnsmasqConfKeys
|
|||||||
public const string DhcpLeasefile = "dhcp-leasefile";
|
public const string DhcpLeasefile = "dhcp-leasefile";
|
||||||
public const string DhcpLease = "dhcp-lease";
|
public const string DhcpLease = "dhcp-lease";
|
||||||
|
|
||||||
|
// --- DHCP include files/dirs (multi-value paths; lines still come from conf only, we just show paths) ---
|
||||||
|
public const string DhcpHostsfile = "dhcp-hostsfile";
|
||||||
|
public const string DhcpOptsfile = "dhcp-optsfile";
|
||||||
|
public const string DhcpHostsdir = "dhcp-hostsdir";
|
||||||
|
|
||||||
// --- Multi-value (ARG_DUP) ---
|
// --- Multi-value (ARG_DUP) ---
|
||||||
public const string Server = "server";
|
public const string Server = "server";
|
||||||
public const string Local = "local";
|
public const string Local = "local";
|
||||||
@@ -104,6 +109,11 @@ public static class DnsmasqConfKeys
|
|||||||
public const string TftpNoBlocksize = "tftp-no-blocksize";
|
public const string TftpNoBlocksize = "tftp-no-blocksize";
|
||||||
public const string Dnssec = "dnssec";
|
public const string Dnssec = "dnssec";
|
||||||
public const string DnssecCheckUnsigned = "dnssec-check-unsigned";
|
public const string DnssecCheckUnsigned = "dnssec-check-unsigned";
|
||||||
|
public const string ProxyDnssec = "proxy-dnssec";
|
||||||
|
|
||||||
|
// --- Process / daemon behaviour (flags) ---
|
||||||
|
public const string KeepInForeground = "keep-in-foreground";
|
||||||
|
public const string NoDaemon = "no-daemon";
|
||||||
|
|
||||||
// --- Single-value (last wins) ---
|
// --- Single-value (last wins) ---
|
||||||
public const string CacheSize = "cache-size";
|
public const string CacheSize = "cache-size";
|
||||||
@@ -135,6 +145,9 @@ public static class DnsmasqConfKeys
|
|||||||
public const string FastDnsRetry = "fast-dns-retry";
|
public const string FastDnsRetry = "fast-dns-retry";
|
||||||
public const string DhcpScript = "dhcp-script";
|
public const string DhcpScript = "dhcp-script";
|
||||||
|
|
||||||
|
// --- Niche / platform (Linux conntrack mark for UBus/query filtering) ---
|
||||||
|
public const string Conntrack = "conntrack";
|
||||||
|
|
||||||
/// <summary>Keys collected for effective config "server/local" multi-value (order preserved).</summary>
|
/// <summary>Keys collected for effective config "server/local" multi-value (order preserved).</summary>
|
||||||
public static readonly string[] ServerLocalKeys = { Server, Local };
|
public static readonly string[] ServerLocalKeys = { Server, Local };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ public static class DnsmasqOptionTooltips
|
|||||||
/// <summary>Display label used in EffectiveConfigFieldBuilder for server/local multi-value.</summary>
|
/// <summary>Display label used in EffectiveConfigFieldBuilder for server/local multi-value.</summary>
|
||||||
public const string ServerLocalLabel = "server / local";
|
public const string ServerLocalLabel = "server / local";
|
||||||
|
|
||||||
|
/// <summary>Option name to option-help file key. "server / local" → "server"; others unchanged.</summary>
|
||||||
|
public static string GetOptionHelpKey(string optionName) =>
|
||||||
|
string.Equals(optionName, ServerLocalLabel, StringComparison.Ordinal) ? "server" : optionName;
|
||||||
|
|
||||||
private static readonly FrozenDictionary<string, string> Tooltips = new Dictionary<string, string>(StringComparer.Ordinal)
|
private static readonly FrozenDictionary<string, string> Tooltips = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||||
{
|
{
|
||||||
// --- Hosts ---
|
// --- Hosts ---
|
||||||
@@ -91,6 +95,9 @@ public static class DnsmasqOptionTooltips
|
|||||||
[DnsmasqConfKeys.DhcpMac] = "Set tag when client MAC matches pattern. Can repeat.",
|
[DnsmasqConfKeys.DhcpMac] = "Set tag when client MAC matches pattern. Can repeat.",
|
||||||
[DnsmasqConfKeys.DhcpNameMatch] = "Set tag when client name matches. Can repeat.",
|
[DnsmasqConfKeys.DhcpNameMatch] = "Set tag when client name matches. Can repeat.",
|
||||||
[DnsmasqConfKeys.DhcpIgnoreNames] = "Ignore client names when tag set. Can repeat.",
|
[DnsmasqConfKeys.DhcpIgnoreNames] = "Ignore client names when tag set. Can repeat.",
|
||||||
|
[DnsmasqConfKeys.DhcpHostsfile] = "File(s) containing dhcp-host-style lines. Can repeat.",
|
||||||
|
[DnsmasqConfKeys.DhcpOptsfile] = "File containing dhcp-option-style lines.",
|
||||||
|
[DnsmasqConfKeys.DhcpHostsdir] = "Directory of files read like dhcp-hostsfile (watched for changes). Can repeat.",
|
||||||
[DnsmasqConfKeys.DhcpBoot] = "Boot file and server for PXE. Can repeat.",
|
[DnsmasqConfKeys.DhcpBoot] = "Boot file and server for PXE. Can repeat.",
|
||||||
[DnsmasqConfKeys.DhcpIgnore] = "Ignore DHCP clients (e.g. by tag). Can repeat.",
|
[DnsmasqConfKeys.DhcpIgnore] = "Ignore DHCP clients (e.g. by tag). Can repeat.",
|
||||||
[DnsmasqConfKeys.DhcpVendorclass] = "Match by DHCP vendor class. Can repeat.",
|
[DnsmasqConfKeys.DhcpVendorclass] = "Match by DHCP vendor class. Can repeat.",
|
||||||
@@ -139,6 +146,9 @@ public static class DnsmasqOptionTooltips
|
|||||||
[DnsmasqConfKeys.EnableUbus] = "Enable UBus interface (optional service name).",
|
[DnsmasqConfKeys.EnableUbus] = "Enable UBus interface (optional service name).",
|
||||||
[DnsmasqConfKeys.EnableRa] = "Enable router advertisements for DHCPv6 subnets.",
|
[DnsmasqConfKeys.EnableRa] = "Enable router advertisements for DHCPv6 subnets.",
|
||||||
[DnsmasqConfKeys.LogDhcp] = "Log DHCP transactions (leases, options).",
|
[DnsmasqConfKeys.LogDhcp] = "Log DHCP transactions (leases, options).",
|
||||||
|
[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.Conntrack] = "Linux connection-tracking mark for UBus/query filtering (platform-dependent).",
|
||||||
}.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>
|
||||||
|
|||||||
@@ -27,14 +27,15 @@ public static class EffectiveConfigFieldBuilder
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public const string SectionHosts = "hosts";
|
/// <summary>Section IDs for use by registry/views. Prefer EffectiveConfigSections for section list and option→section mapping.</summary>
|
||||||
public const string SectionResolver = "resolver";
|
public static string SectionHosts => EffectiveConfigSections.SectionHosts;
|
||||||
public const string SectionDnsRecords = "dns-records";
|
public static string SectionResolver => EffectiveConfigSections.SectionResolver;
|
||||||
public const string SectionDhcp = "dhcp";
|
public static string SectionDnsRecords => EffectiveConfigSections.SectionDnsRecords;
|
||||||
public const string SectionTftpPxe = "tftp-pxe";
|
public static string SectionDhcp => EffectiveConfigSections.SectionDhcp;
|
||||||
public const string SectionDnssec = "dnssec";
|
public static string SectionTftpPxe => EffectiveConfigSections.SectionTftpPxe;
|
||||||
public const string SectionCache = "cache";
|
public static string SectionDnssec => EffectiveConfigSections.SectionDnssec;
|
||||||
public const string SectionProcess = "process";
|
public static string SectionCache => EffectiveConfigSections.SectionCache;
|
||||||
|
public static string SectionProcess => EffectiveConfigSections.SectionProcess;
|
||||||
|
|
||||||
public static IReadOnlyList<EffectiveConfigFieldDescriptor> BuildFieldDescriptors(DnsmasqServiceStatus? status)
|
public static IReadOnlyList<EffectiveConfigFieldDescriptor> BuildFieldDescriptors(DnsmasqServiceStatus? status)
|
||||||
{
|
{
|
||||||
@@ -43,142 +44,149 @@ public static class EffectiveConfigFieldBuilder
|
|||||||
|
|
||||||
var list = new List<EffectiveConfigFieldDescriptor>();
|
var list = new List<EffectiveConfigFieldDescriptor>();
|
||||||
|
|
||||||
// --- Hosts ---
|
// Hosts
|
||||||
list.Add(Single(SectionHosts, DnsmasqConfKeys.NoHosts, status, s => Config(s)?.NoHosts, s => Sources(s)?.NoHosts, null));
|
list.Add(Single(DnsmasqConfKeys.NoHosts, status, s => Config(s)?.NoHosts, s => Sources(s)?.NoHosts, null));
|
||||||
list.Add(Multi(SectionHosts, DnsmasqConfKeys.AddnHosts, status,
|
list.Add(Multi(DnsmasqConfKeys.AddnHosts, status, s => ToValueWithSourceList(s?.AddnHostsPaths, Sources(s)?.AddnHostsPaths)));
|
||||||
s => ToValueWithSourceList(s?.AddnHostsPaths, Sources(s)?.AddnHostsPaths)));
|
list.Add(Single(DnsmasqConfKeys.Hostsdir, status, s => Config(s)?.HostsdirPath, s => Sources(s)?.HostsdirPath, null));
|
||||||
list.Add(Single(SectionHosts, DnsmasqConfKeys.Hostsdir, status, s => Config(s)?.HostsdirPath, s => Sources(s)?.HostsdirPath, null));
|
list.Add(Single(DnsmasqConfKeys.ReadEthers, status, s => Config(s)?.ReadEthers, s => Sources(s)?.ReadEthers, null));
|
||||||
list.Add(Single(SectionHosts, DnsmasqConfKeys.ReadEthers, status, s => Config(s)?.ReadEthers, s => Sources(s)?.ReadEthers, null));
|
|
||||||
|
|
||||||
// --- Resolver / DNS (most-used first: server/local, rev-server, address, resolv-file) ---
|
// Resolver / DNS
|
||||||
list.Add(Multi(SectionResolver, "server / local", status, Items(ec => ec?.ServerLocalValues, src => src?.ServerLocalValues)));
|
list.Add(Multi(DnsmasqOptionTooltips.ServerLocalLabel, status, Items(ec => ec?.ServerLocalValues, src => src?.ServerLocalValues)));
|
||||||
list.Add(Multi(SectionResolver, DnsmasqConfKeys.RevServer, status, Items(ec => ec?.RevServerValues, src => src?.RevServerValues)));
|
list.Add(Multi(DnsmasqConfKeys.RevServer, status, Items(ec => ec?.RevServerValues, src => src?.RevServerValues)));
|
||||||
list.Add(Multi(SectionResolver, DnsmasqConfKeys.Address, status, Items(ec => ec?.AddressValues, src => src?.AddressValues)));
|
list.Add(Multi(DnsmasqConfKeys.Address, status, Items(ec => ec?.AddressValues, src => src?.AddressValues)));
|
||||||
list.Add(Multi(SectionResolver, DnsmasqConfKeys.ResolvFile, status, Items(ec => ec?.ResolvFiles, src => src?.ResolvFiles)));
|
list.Add(Multi(DnsmasqConfKeys.ResolvFile, status, Items(ec => ec?.ResolvFiles, src => src?.ResolvFiles)));
|
||||||
list.Add(Single(SectionResolver, DnsmasqConfKeys.NoResolv, status, s => Config(s)?.NoResolv, s => Sources(s)?.NoResolv, null));
|
list.Add(Single(DnsmasqConfKeys.NoResolv, status, s => Config(s)?.NoResolv, s => Sources(s)?.NoResolv, null));
|
||||||
list.Add(Single(SectionResolver, DnsmasqConfKeys.DomainNeeded, status, s => Config(s)?.DomainNeeded, s => Sources(s)?.DomainNeeded, null));
|
list.Add(Single(DnsmasqConfKeys.DomainNeeded, status, s => Config(s)?.DomainNeeded, s => Sources(s)?.DomainNeeded, null));
|
||||||
list.Add(Single(SectionResolver, DnsmasqConfKeys.Port, status, s => Config(s)?.Port, s => Sources(s)?.Port, null));
|
list.Add(Single(DnsmasqConfKeys.Port, status, s => Config(s)?.Port, s => Sources(s)?.Port, null));
|
||||||
list.Add(Single(SectionResolver, DnsmasqConfKeys.LogQueries, status, s => Config(s)?.LogQueries, s => Sources(s)?.LogQueries, null));
|
list.Add(Single(DnsmasqConfKeys.LogQueries, status, s => Config(s)?.LogQueries, s => Sources(s)?.LogQueries, null));
|
||||||
list.Add(Single(SectionResolver, DnsmasqConfKeys.StrictOrder, status, s => Config(s)?.StrictOrder, s => Sources(s)?.StrictOrder, null));
|
list.Add(Single(DnsmasqConfKeys.StrictOrder, status, s => Config(s)?.StrictOrder, s => Sources(s)?.StrictOrder, null));
|
||||||
list.Add(Single(SectionResolver, DnsmasqConfKeys.AllServers, status, s => Config(s)?.AllServers, s => Sources(s)?.AllServers, null));
|
list.Add(Single(DnsmasqConfKeys.AllServers, status, s => Config(s)?.AllServers, s => Sources(s)?.AllServers, null));
|
||||||
list.Add(Single(SectionResolver, DnsmasqConfKeys.AuthTtl, status, s => Config(s)?.AuthTtl, s => Sources(s)?.AuthTtl, null));
|
list.Add(Single(DnsmasqConfKeys.AuthTtl, status, s => Config(s)?.AuthTtl, s => Sources(s)?.AuthTtl, null));
|
||||||
list.Add(Single(SectionResolver, DnsmasqConfKeys.EdnsPacketMax, status, s => Config(s)?.EdnsPacketMax, s => Sources(s)?.EdnsPacketMax, null));
|
list.Add(Single(DnsmasqConfKeys.EdnsPacketMax, status, s => Config(s)?.EdnsPacketMax, s => Sources(s)?.EdnsPacketMax, null));
|
||||||
list.Add(Single(SectionResolver, DnsmasqConfKeys.QueryPort, status, s => Config(s)?.QueryPort, s => Sources(s)?.QueryPort, null));
|
list.Add(Single(DnsmasqConfKeys.QueryPort, status, s => Config(s)?.QueryPort, s => Sources(s)?.QueryPort, null));
|
||||||
list.Add(Single(SectionResolver, DnsmasqConfKeys.PortLimit, status, s => Config(s)?.PortLimit, s => Sources(s)?.PortLimit, null));
|
list.Add(Single(DnsmasqConfKeys.PortLimit, status, s => Config(s)?.PortLimit, s => Sources(s)?.PortLimit, null));
|
||||||
list.Add(Single(SectionResolver, DnsmasqConfKeys.MinPort, status, s => Config(s)?.MinPort, s => Sources(s)?.MinPort, null));
|
list.Add(Single(DnsmasqConfKeys.MinPort, status, s => Config(s)?.MinPort, s => Sources(s)?.MinPort, null));
|
||||||
list.Add(Single(SectionResolver, DnsmasqConfKeys.MaxPort, status, s => Config(s)?.MaxPort, s => Sources(s)?.MaxPort, null));
|
list.Add(Single(DnsmasqConfKeys.MaxPort, status, s => Config(s)?.MaxPort, s => Sources(s)?.MaxPort, null));
|
||||||
list.Add(Single(SectionResolver, DnsmasqConfKeys.DnsLoopDetect, status, s => Config(s)?.DnsLoopDetect, s => Sources(s)?.DnsLoopDetect, null));
|
list.Add(Single(DnsmasqConfKeys.DnsLoopDetect, status, s => Config(s)?.DnsLoopDetect, s => Sources(s)?.DnsLoopDetect, null));
|
||||||
list.Add(Single(SectionResolver, DnsmasqConfKeys.StopDnsRebind, status, s => Config(s)?.StopDnsRebind, s => Sources(s)?.StopDnsRebind, null));
|
list.Add(Single(DnsmasqConfKeys.StopDnsRebind, status, s => Config(s)?.StopDnsRebind, s => Sources(s)?.StopDnsRebind, null));
|
||||||
list.Add(Single(SectionResolver, DnsmasqConfKeys.RebindLocalhostOk, status, s => Config(s)?.RebindLocalhostOk, s => Sources(s)?.RebindLocalhostOk, null));
|
list.Add(Single(DnsmasqConfKeys.RebindLocalhostOk, status, s => Config(s)?.RebindLocalhostOk, s => Sources(s)?.RebindLocalhostOk, null));
|
||||||
list.Add(Single(SectionResolver, DnsmasqConfKeys.ClearOnReload, status, s => Config(s)?.ClearOnReload, s => Sources(s)?.ClearOnReload, null));
|
list.Add(Single(DnsmasqConfKeys.ClearOnReload, status, s => Config(s)?.ClearOnReload, s => Sources(s)?.ClearOnReload, null));
|
||||||
list.Add(Single(SectionResolver, DnsmasqConfKeys.ExpandHosts, status, s => Config(s)?.ExpandHosts, s => Sources(s)?.ExpandHosts, null));
|
list.Add(Single(DnsmasqConfKeys.ExpandHosts, status, s => Config(s)?.ExpandHosts, s => Sources(s)?.ExpandHosts, null));
|
||||||
list.Add(Single(SectionResolver, DnsmasqConfKeys.BogusPriv, status, s => Config(s)?.BogusPriv, s => Sources(s)?.BogusPriv, null));
|
list.Add(Single(DnsmasqConfKeys.BogusPriv, status, s => Config(s)?.BogusPriv, s => Sources(s)?.BogusPriv, null));
|
||||||
list.Add(Multi(SectionResolver, DnsmasqConfKeys.RebindDomainOk, status, Items(ec => ec?.RebindDomainOkValues, src => src?.RebindDomainOkValues)));
|
list.Add(Multi(DnsmasqConfKeys.RebindDomainOk, status, Items(ec => ec?.RebindDomainOkValues, src => src?.RebindDomainOkValues)));
|
||||||
list.Add(Multi(SectionResolver, DnsmasqConfKeys.BogusNxdomain, status, Items(ec => ec?.BogusNxdomainValues, src => src?.BogusNxdomainValues)));
|
list.Add(Multi(DnsmasqConfKeys.BogusNxdomain, status, Items(ec => ec?.BogusNxdomainValues, src => src?.BogusNxdomainValues)));
|
||||||
list.Add(Multi(SectionResolver, DnsmasqConfKeys.IgnoreAddress, status, Items(ec => ec?.IgnoreAddressValues, src => src?.IgnoreAddressValues)));
|
list.Add(Multi(DnsmasqConfKeys.IgnoreAddress, status, Items(ec => ec?.IgnoreAddressValues, src => src?.IgnoreAddressValues)));
|
||||||
list.Add(Multi(SectionResolver, DnsmasqConfKeys.Alias, status, Items(ec => ec?.AliasValues, src => src?.AliasValues)));
|
list.Add(Multi(DnsmasqConfKeys.Alias, status, Items(ec => ec?.AliasValues, src => src?.AliasValues)));
|
||||||
list.Add(Multi(SectionResolver, DnsmasqConfKeys.FilterRr, status, Items(ec => ec?.FilterRrValues, src => src?.FilterRrValues)));
|
list.Add(Multi(DnsmasqConfKeys.FilterRr, status, Items(ec => ec?.FilterRrValues, src => src?.FilterRrValues)));
|
||||||
list.Add(Single(SectionResolver, DnsmasqConfKeys.Filterwin2k, status, s => Config(s)?.Filterwin2k, s => Sources(s)?.Filterwin2k, null));
|
list.Add(Single(DnsmasqConfKeys.Filterwin2k, status, s => Config(s)?.Filterwin2k, s => Sources(s)?.Filterwin2k, null));
|
||||||
list.Add(Single(SectionResolver, DnsmasqConfKeys.FilterA, status, s => Config(s)?.FilterA, s => Sources(s)?.FilterA, null));
|
list.Add(Single(DnsmasqConfKeys.FilterA, status, s => Config(s)?.FilterA, s => Sources(s)?.FilterA, null));
|
||||||
list.Add(Single(SectionResolver, DnsmasqConfKeys.FilterAaaa, status, s => Config(s)?.FilterAaaa, s => Sources(s)?.FilterAaaa, null));
|
list.Add(Single(DnsmasqConfKeys.FilterAaaa, status, s => Config(s)?.FilterAaaa, s => Sources(s)?.FilterAaaa, null));
|
||||||
list.Add(Single(SectionResolver, DnsmasqConfKeys.LocaliseQueries, status, s => Config(s)?.LocaliseQueries, s => Sources(s)?.LocaliseQueries, null));
|
list.Add(Single(DnsmasqConfKeys.LocaliseQueries, status, s => Config(s)?.LocaliseQueries, s => Sources(s)?.LocaliseQueries, null));
|
||||||
list.Add(Single(SectionResolver, DnsmasqConfKeys.FastDnsRetry, status, s => Config(s)?.FastDnsRetry, s => Sources(s)?.FastDnsRetry, null));
|
list.Add(Single(DnsmasqConfKeys.FastDnsRetry, status, s => Config(s)?.FastDnsRetry, s => Sources(s)?.FastDnsRetry, null));
|
||||||
list.Add(Multi(SectionResolver, DnsmasqConfKeys.Ipset, status, Items(ec => ec?.IpsetValues, src => src?.IpsetValues)));
|
list.Add(Multi(DnsmasqConfKeys.Ipset, status, Items(ec => ec?.IpsetValues, src => src?.IpsetValues)));
|
||||||
list.Add(Multi(SectionResolver, DnsmasqConfKeys.Nftset, status, Items(ec => ec?.NftsetValues, src => src?.NftsetValues)));
|
list.Add(Multi(DnsmasqConfKeys.Nftset, status, Items(ec => ec?.NftsetValues, src => src?.NftsetValues)));
|
||||||
|
|
||||||
// --- DNS records (authoritative / local) ---
|
// DNS records
|
||||||
list.Add(Multi(SectionDnsRecords, DnsmasqConfKeys.Domain, status, Items(ec => ec?.DomainValues, src => src?.DomainValues)));
|
list.Add(Multi(DnsmasqConfKeys.Domain, status, Items(ec => ec?.DomainValues, src => src?.DomainValues)));
|
||||||
list.Add(Multi(SectionDnsRecords, DnsmasqConfKeys.Cname, status, Items(ec => ec?.CnameValues, src => src?.CnameValues)));
|
list.Add(Multi(DnsmasqConfKeys.Cname, status, Items(ec => ec?.CnameValues, src => src?.CnameValues)));
|
||||||
list.Add(Multi(SectionDnsRecords, DnsmasqConfKeys.MxHost, status, Items(ec => ec?.MxHostValues, src => src?.MxHostValues)));
|
list.Add(Multi(DnsmasqConfKeys.MxHost, status, Items(ec => ec?.MxHostValues, src => src?.MxHostValues)));
|
||||||
list.Add(Multi(SectionDnsRecords, DnsmasqConfKeys.Srv, status, Items(ec => ec?.SrvValues, src => src?.SrvValues)));
|
list.Add(Multi(DnsmasqConfKeys.Srv, status, Items(ec => ec?.SrvValues, src => src?.SrvValues)));
|
||||||
list.Add(Multi(SectionDnsRecords, DnsmasqConfKeys.PtrRecord, status, Items(ec => ec?.PtrRecordValues, src => src?.PtrRecordValues)));
|
list.Add(Multi(DnsmasqConfKeys.PtrRecord, status, Items(ec => ec?.PtrRecordValues, src => src?.PtrRecordValues)));
|
||||||
list.Add(Multi(SectionDnsRecords, DnsmasqConfKeys.TxtRecord, status, Items(ec => ec?.TxtRecordValues, src => src?.TxtRecordValues)));
|
list.Add(Multi(DnsmasqConfKeys.TxtRecord, status, Items(ec => ec?.TxtRecordValues, src => src?.TxtRecordValues)));
|
||||||
list.Add(Multi(SectionDnsRecords, DnsmasqConfKeys.NaptrRecord, status, Items(ec => ec?.NaptrRecordValues, src => src?.NaptrRecordValues)));
|
list.Add(Multi(DnsmasqConfKeys.NaptrRecord, status, Items(ec => ec?.NaptrRecordValues, src => src?.NaptrRecordValues)));
|
||||||
list.Add(Multi(SectionDnsRecords, DnsmasqConfKeys.HostRecord, status, Items(ec => ec?.HostRecordValues, src => src?.HostRecordValues)));
|
list.Add(Multi(DnsmasqConfKeys.HostRecord, status, Items(ec => ec?.HostRecordValues, src => src?.HostRecordValues)));
|
||||||
list.Add(Multi(SectionDnsRecords, DnsmasqConfKeys.DynamicHost, status, Items(ec => ec?.DynamicHostValues, src => src?.DynamicHostValues)));
|
list.Add(Multi(DnsmasqConfKeys.DynamicHost, status, Items(ec => ec?.DynamicHostValues, src => src?.DynamicHostValues)));
|
||||||
list.Add(Multi(SectionDnsRecords, DnsmasqConfKeys.InterfaceName, status, Items(ec => ec?.InterfaceNameValues, src => src?.InterfaceNameValues)));
|
list.Add(Multi(DnsmasqConfKeys.InterfaceName, status, Items(ec => ec?.InterfaceNameValues, src => src?.InterfaceNameValues)));
|
||||||
list.Add(Single(SectionDnsRecords, DnsmasqConfKeys.MxTarget, status, s => Config(s)?.MxTarget, s => Sources(s)?.MxTarget, null));
|
list.Add(Single(DnsmasqConfKeys.MxTarget, status, s => Config(s)?.MxTarget, s => Sources(s)?.MxTarget, null));
|
||||||
list.Add(Single(SectionDnsRecords, DnsmasqConfKeys.Localmx, status, s => Config(s)?.Localmx, s => Sources(s)?.Localmx, null));
|
list.Add(Single(DnsmasqConfKeys.Localmx, status, s => Config(s)?.Localmx, s => Sources(s)?.Localmx, null));
|
||||||
list.Add(Single(SectionDnsRecords, DnsmasqConfKeys.Selfmx, status, s => Config(s)?.Selfmx, s => Sources(s)?.Selfmx, null));
|
list.Add(Single(DnsmasqConfKeys.Selfmx, status, s => Config(s)?.Selfmx, s => Sources(s)?.Selfmx, null));
|
||||||
|
|
||||||
// --- DHCP ---
|
// DHCP
|
||||||
list.Add(Single(SectionDhcp, DnsmasqConfKeys.DhcpAuthoritative, status, s => Config(s)?.DhcpAuthoritative, s => Sources(s)?.DhcpAuthoritative, null));
|
list.Add(Single(DnsmasqConfKeys.DhcpAuthoritative, status, s => Config(s)?.DhcpAuthoritative, s => Sources(s)?.DhcpAuthoritative, null));
|
||||||
list.Add(Single(SectionDhcp, DnsmasqConfKeys.DhcpRapidCommit, status, s => Config(s)?.DhcpRapidCommit, s => Sources(s)?.DhcpRapidCommit, null));
|
list.Add(Single(DnsmasqConfKeys.DhcpRapidCommit, status, s => Config(s)?.DhcpRapidCommit, s => Sources(s)?.DhcpRapidCommit, null));
|
||||||
list.Add(Single(SectionDhcp, DnsmasqConfKeys.LeasefileRo, status, s => Config(s)?.LeasefileRo, s => Sources(s)?.LeasefileRo, null));
|
list.Add(Single(DnsmasqConfKeys.LeasefileRo, status, s => Config(s)?.LeasefileRo, s => Sources(s)?.LeasefileRo, null));
|
||||||
list.Add(Single(SectionDhcp, DnsmasqConfKeys.DhcpScript, status, s => Config(s)?.DhcpScriptPath, s => Sources(s)?.DhcpScriptPath, null));
|
list.Add(Single(DnsmasqConfKeys.DhcpScript, status, s => Config(s)?.DhcpScriptPath, s => Sources(s)?.DhcpScriptPath, null));
|
||||||
list.Add(Single(SectionDhcp, DnsmasqConfKeys.DhcpLeasefile, status, s => Config(s)?.DhcpLeaseFilePath, s => Sources(s)?.DhcpLeaseFilePath, null));
|
list.Add(Single(DnsmasqConfKeys.DhcpLeasefile, status, s => Config(s)?.DhcpLeaseFilePath, s => Sources(s)?.DhcpLeaseFilePath, null));
|
||||||
list.Add(Single(SectionDhcp, DnsmasqConfKeys.DhcpLeaseMax, status, s => Config(s)?.DhcpLeaseMax, s => Sources(s)?.DhcpLeaseMax, null));
|
list.Add(Single(DnsmasqConfKeys.DhcpLeaseMax, status, s => Config(s)?.DhcpLeaseMax, s => Sources(s)?.DhcpLeaseMax, null));
|
||||||
list.Add(Single(SectionDhcp, DnsmasqConfKeys.DhcpTtl, status, s => Config(s)?.DhcpTtl, s => Sources(s)?.DhcpTtl, null));
|
list.Add(Single(DnsmasqConfKeys.DhcpTtl, status, s => Config(s)?.DhcpTtl, s => Sources(s)?.DhcpTtl, null));
|
||||||
list.Add(Multi(SectionDhcp, DnsmasqConfKeys.DhcpRange, status, Items(ec => ec?.DhcpRanges, src => src?.DhcpRanges)));
|
list.Add(Multi(DnsmasqConfKeys.DhcpRange, status, Items(ec => ec?.DhcpRanges, src => src?.DhcpRanges)));
|
||||||
list.Add(Multi(SectionDhcp, DnsmasqConfKeys.DhcpHost, status, Items(ec => ec?.DhcpHostLines, src => src?.DhcpHostLines)));
|
list.Add(Multi(DnsmasqConfKeys.DhcpHost, status, Items(ec => ec?.DhcpHostLines, src => src?.DhcpHostLines)));
|
||||||
list.Add(Multi(SectionDhcp, DnsmasqConfKeys.DhcpOption, status, Items(ec => ec?.DhcpOptionLines, src => src?.DhcpOptionLines)));
|
list.Add(Multi(DnsmasqConfKeys.DhcpOption, status, Items(ec => ec?.DhcpOptionLines, src => src?.DhcpOptionLines)));
|
||||||
list.Add(Multi(SectionDhcp, DnsmasqConfKeys.DhcpOptionForce, status, Items(ec => ec?.DhcpOptionForceLines, src => src?.DhcpOptionForceLines)));
|
list.Add(Multi(DnsmasqConfKeys.DhcpOptionForce, status, Items(ec => ec?.DhcpOptionForceLines, src => src?.DhcpOptionForceLines)));
|
||||||
list.Add(Multi(SectionDhcp, DnsmasqConfKeys.DhcpMatch, status, Items(ec => ec?.DhcpMatchValues, src => src?.DhcpMatchValues)));
|
list.Add(Multi(DnsmasqConfKeys.DhcpMatch, status, Items(ec => ec?.DhcpMatchValues, src => src?.DhcpMatchValues)));
|
||||||
list.Add(Multi(SectionDhcp, DnsmasqConfKeys.DhcpMac, status, Items(ec => ec?.DhcpMacValues, src => src?.DhcpMacValues)));
|
list.Add(Multi(DnsmasqConfKeys.DhcpMac, status, Items(ec => ec?.DhcpMacValues, src => src?.DhcpMacValues)));
|
||||||
list.Add(Multi(SectionDhcp, DnsmasqConfKeys.DhcpNameMatch, status, Items(ec => ec?.DhcpNameMatchValues, src => src?.DhcpNameMatchValues)));
|
list.Add(Multi(DnsmasqConfKeys.DhcpNameMatch, status, Items(ec => ec?.DhcpNameMatchValues, src => src?.DhcpNameMatchValues)));
|
||||||
list.Add(Multi(SectionDhcp, DnsmasqConfKeys.DhcpIgnoreNames, status, Items(ec => ec?.DhcpIgnoreNamesValues, src => src?.DhcpIgnoreNamesValues)));
|
list.Add(Multi(DnsmasqConfKeys.DhcpIgnoreNames, status, Items(ec => ec?.DhcpIgnoreNamesValues, src => src?.DhcpIgnoreNamesValues)));
|
||||||
list.Add(Multi(SectionDhcp, DnsmasqConfKeys.DhcpBoot, status, Items(ec => ec?.DhcpBootValues, src => src?.DhcpBootValues)));
|
list.Add(Multi(DnsmasqConfKeys.DhcpHostsfile, status, Items(ec => ec?.DhcpHostsfilePaths, src => src?.DhcpHostsfilePaths)));
|
||||||
list.Add(Multi(SectionDhcp, DnsmasqConfKeys.DhcpIgnore, status, Items(ec => ec?.DhcpIgnoreValues, src => src?.DhcpIgnoreValues)));
|
list.Add(Multi(DnsmasqConfKeys.DhcpOptsfile, status, Items(ec => ec?.DhcpOptsfilePaths, src => src?.DhcpOptsfilePaths)));
|
||||||
list.Add(Multi(SectionDhcp, DnsmasqConfKeys.DhcpVendorclass, status, Items(ec => ec?.DhcpVendorclassValues, src => src?.DhcpVendorclassValues)));
|
list.Add(Multi(DnsmasqConfKeys.DhcpHostsdir, status, Items(ec => ec?.DhcpHostsdirPaths, src => src?.DhcpHostsdirPaths)));
|
||||||
list.Add(Multi(SectionDhcp, DnsmasqConfKeys.DhcpUserclass, status, Items(ec => ec?.DhcpUserclassValues, src => src?.DhcpUserclassValues)));
|
list.Add(Multi(DnsmasqConfKeys.DhcpBoot, status, Items(ec => ec?.DhcpBootValues, src => src?.DhcpBootValues)));
|
||||||
list.Add(Multi(SectionDhcp, DnsmasqConfKeys.RaParam, status, Items(ec => ec?.RaParamValues, src => src?.RaParamValues)));
|
list.Add(Multi(DnsmasqConfKeys.DhcpIgnore, status, Items(ec => ec?.DhcpIgnoreValues, src => src?.DhcpIgnoreValues)));
|
||||||
list.Add(Multi(SectionDhcp, DnsmasqConfKeys.Slaac, status, Items(ec => ec?.SlaacValues, src => src?.SlaacValues)));
|
list.Add(Multi(DnsmasqConfKeys.DhcpVendorclass, status, Items(ec => ec?.DhcpVendorclassValues, src => src?.DhcpVendorclassValues)));
|
||||||
|
list.Add(Multi(DnsmasqConfKeys.DhcpUserclass, status, Items(ec => ec?.DhcpUserclassValues, src => src?.DhcpUserclassValues)));
|
||||||
|
list.Add(Multi(DnsmasqConfKeys.RaParam, status, Items(ec => ec?.RaParamValues, src => src?.RaParamValues)));
|
||||||
|
list.Add(Multi(DnsmasqConfKeys.Slaac, status, Items(ec => ec?.SlaacValues, src => src?.SlaacValues)));
|
||||||
|
|
||||||
// --- TFTP / PXE ---
|
// TFTP / PXE
|
||||||
list.Add(Single(SectionTftpPxe, DnsmasqConfKeys.EnableTftp, status, s => Config(s)?.EnableTftp, s => Sources(s)?.EnableTftp, null));
|
list.Add(Single(DnsmasqConfKeys.EnableTftp, status, s => Config(s)?.EnableTftp, s => Sources(s)?.EnableTftp, null));
|
||||||
list.Add(Single(SectionTftpPxe, DnsmasqConfKeys.TftpSecure, status, s => Config(s)?.TftpSecure, s => Sources(s)?.TftpSecure, null));
|
list.Add(Single(DnsmasqConfKeys.TftpSecure, status, s => Config(s)?.TftpSecure, s => Sources(s)?.TftpSecure, null));
|
||||||
list.Add(Single(SectionTftpPxe, DnsmasqConfKeys.TftpNoFail, status, s => Config(s)?.TftpNoFail, s => Sources(s)?.TftpNoFail, null));
|
list.Add(Single(DnsmasqConfKeys.TftpNoFail, status, s => Config(s)?.TftpNoFail, s => Sources(s)?.TftpNoFail, null));
|
||||||
list.Add(Single(SectionTftpPxe, DnsmasqConfKeys.TftpNoBlocksize, status, s => Config(s)?.TftpNoBlocksize, s => Sources(s)?.TftpNoBlocksize, null));
|
list.Add(Single(DnsmasqConfKeys.TftpNoBlocksize, status, s => Config(s)?.TftpNoBlocksize, s => Sources(s)?.TftpNoBlocksize, null));
|
||||||
list.Add(Single(SectionTftpPxe, DnsmasqConfKeys.TftpRoot, status, s => Config(s)?.TftpRootPath, s => Sources(s)?.TftpRootPath, null));
|
list.Add(Single(DnsmasqConfKeys.TftpRoot, status, s => Config(s)?.TftpRootPath, s => Sources(s)?.TftpRootPath, null));
|
||||||
list.Add(Single(SectionTftpPxe, DnsmasqConfKeys.PxePrompt, status, s => Config(s)?.PxePrompt, s => Sources(s)?.PxePrompt, null));
|
list.Add(Single(DnsmasqConfKeys.PxePrompt, status, s => Config(s)?.PxePrompt, s => Sources(s)?.PxePrompt, null));
|
||||||
list.Add(Multi(SectionTftpPxe, DnsmasqConfKeys.PxeService, status, Items(ec => ec?.PxeServiceValues, src => src?.PxeServiceValues)));
|
list.Add(Multi(DnsmasqConfKeys.PxeService, status, Items(ec => ec?.PxeServiceValues, src => src?.PxeServiceValues)));
|
||||||
|
|
||||||
// --- DNSSEC ---
|
// DNSSEC
|
||||||
list.Add(Single(SectionDnssec, DnsmasqConfKeys.Dnssec, status, s => Config(s)?.Dnssec, s => Sources(s)?.Dnssec, null));
|
list.Add(Single(DnsmasqConfKeys.Dnssec, status, s => Config(s)?.Dnssec, s => Sources(s)?.Dnssec, null));
|
||||||
list.Add(Single(SectionDnssec, DnsmasqConfKeys.DnssecCheckUnsigned, status, s => Config(s)?.DnssecCheckUnsigned, s => Sources(s)?.DnssecCheckUnsigned, null));
|
list.Add(Single(DnsmasqConfKeys.DnssecCheckUnsigned, status, s => Config(s)?.DnssecCheckUnsigned, s => Sources(s)?.DnssecCheckUnsigned, null));
|
||||||
list.Add(Multi(SectionDnssec, DnsmasqConfKeys.TrustAnchor, status, Items(ec => ec?.TrustAnchorValues, src => src?.TrustAnchorValues)));
|
list.Add(Single(DnsmasqConfKeys.ProxyDnssec, status, s => Config(s)?.ProxyDnssec, s => Sources(s)?.ProxyDnssec, null));
|
||||||
|
list.Add(Multi(DnsmasqConfKeys.TrustAnchor, status, Items(ec => ec?.TrustAnchorValues, src => src?.TrustAnchorValues)));
|
||||||
|
|
||||||
// --- Cache ---
|
// Cache
|
||||||
list.Add(Multi(SectionCache, DnsmasqConfKeys.CacheRr, status, Items(ec => ec?.CacheRrValues, src => src?.CacheRrValues)));
|
list.Add(Multi(DnsmasqConfKeys.CacheRr, status, Items(ec => ec?.CacheRrValues, src => src?.CacheRrValues)));
|
||||||
list.Add(Single(SectionCache, DnsmasqConfKeys.CacheSize, status, s => Config(s)?.CacheSize, s => Sources(s)?.CacheSize, null));
|
list.Add(Single(DnsmasqConfKeys.CacheSize, status, s => Config(s)?.CacheSize, s => Sources(s)?.CacheSize, null));
|
||||||
list.Add(Single(SectionCache, DnsmasqConfKeys.LocalTtl, status, s => Config(s)?.LocalTtl, s => Sources(s)?.LocalTtl, null));
|
list.Add(Single(DnsmasqConfKeys.LocalTtl, status, s => Config(s)?.LocalTtl, s => Sources(s)?.LocalTtl, null));
|
||||||
list.Add(Single(SectionCache, DnsmasqConfKeys.NoNegcache, status, s => Config(s)?.NoNegcache, s => Sources(s)?.NoNegcache, null));
|
list.Add(Single(DnsmasqConfKeys.NoNegcache, status, s => Config(s)?.NoNegcache, s => Sources(s)?.NoNegcache, null));
|
||||||
list.Add(Single(SectionCache, DnsmasqConfKeys.NegTtl, status, s => Config(s)?.NegTtl, s => Sources(s)?.NegTtl, null));
|
list.Add(Single(DnsmasqConfKeys.NegTtl, status, s => Config(s)?.NegTtl, s => Sources(s)?.NegTtl, null));
|
||||||
list.Add(Single(SectionCache, DnsmasqConfKeys.MaxTtl, status, s => Config(s)?.MaxTtl, s => Sources(s)?.MaxTtl, null));
|
list.Add(Single(DnsmasqConfKeys.MaxTtl, status, s => Config(s)?.MaxTtl, s => Sources(s)?.MaxTtl, null));
|
||||||
list.Add(Single(SectionCache, DnsmasqConfKeys.MaxCacheTtl, status, s => Config(s)?.MaxCacheTtl, s => Sources(s)?.MaxCacheTtl, null));
|
list.Add(Single(DnsmasqConfKeys.MaxCacheTtl, status, s => Config(s)?.MaxCacheTtl, s => Sources(s)?.MaxCacheTtl, null));
|
||||||
list.Add(Single(SectionCache, DnsmasqConfKeys.MinCacheTtl, status, s => Config(s)?.MinCacheTtl, s => Sources(s)?.MinCacheTtl, null));
|
list.Add(Single(DnsmasqConfKeys.MinCacheTtl, status, s => Config(s)?.MinCacheTtl, s => Sources(s)?.MinCacheTtl, null));
|
||||||
|
|
||||||
// --- Process & networking ---
|
// Process & networking
|
||||||
list.Add(Single(SectionProcess, DnsmasqConfKeys.NoPoll, status, s => Config(s)?.NoPoll, s => Sources(s)?.NoPoll, null));
|
list.Add(Single(DnsmasqConfKeys.NoPoll, status, s => Config(s)?.NoPoll, s => Sources(s)?.NoPoll, null));
|
||||||
list.Add(Single(SectionProcess, DnsmasqConfKeys.BindInterfaces, status, s => Config(s)?.BindInterfaces, s => Sources(s)?.BindInterfaces, null));
|
list.Add(Single(DnsmasqConfKeys.BindInterfaces, status, s => Config(s)?.BindInterfaces, s => Sources(s)?.BindInterfaces, null));
|
||||||
list.Add(Single(SectionProcess, DnsmasqConfKeys.BindDynamic, status, s => Config(s)?.BindDynamic, s => Sources(s)?.BindDynamic, null));
|
list.Add(Single(DnsmasqConfKeys.BindDynamic, status, s => Config(s)?.BindDynamic, s => Sources(s)?.BindDynamic, null));
|
||||||
list.Add(Single(SectionProcess, DnsmasqConfKeys.LogDebug, status, s => Config(s)?.LogDebug, s => Sources(s)?.LogDebug, null));
|
list.Add(Single(DnsmasqConfKeys.LogDebug, status, s => Config(s)?.LogDebug, s => Sources(s)?.LogDebug, null));
|
||||||
list.Add(Single(SectionProcess, DnsmasqConfKeys.LogAsync, status, s => Config(s)?.LogAsync, s => Sources(s)?.LogAsync, null));
|
list.Add(Single(DnsmasqConfKeys.LogAsync, status, s => Config(s)?.LogAsync, s => Sources(s)?.LogAsync, null));
|
||||||
list.Add(Single(SectionProcess, DnsmasqConfKeys.LocalService, status, s => Config(s)?.LocalService, s => Sources(s)?.LocalService, null));
|
list.Add(Single(DnsmasqConfKeys.LocalService, status, s => Config(s)?.LocalService, s => Sources(s)?.LocalService, null));
|
||||||
list.Add(Multi(SectionProcess, DnsmasqConfKeys.Interface, status, Items(ec => ec?.Interfaces, src => src?.Interfaces)));
|
list.Add(Multi(DnsmasqConfKeys.Interface, status, Items(ec => ec?.Interfaces, src => src?.Interfaces)));
|
||||||
list.Add(Multi(SectionProcess, DnsmasqConfKeys.ListenAddress, status, Items(ec => ec?.ListenAddresses, src => src?.ListenAddresses)));
|
list.Add(Multi(DnsmasqConfKeys.ListenAddress, status, Items(ec => ec?.ListenAddresses, src => src?.ListenAddresses)));
|
||||||
list.Add(Multi(SectionProcess, DnsmasqConfKeys.ExceptInterface, status, Items(ec => ec?.ExceptInterfaces, src => src?.ExceptInterfaces)));
|
list.Add(Multi(DnsmasqConfKeys.ExceptInterface, status, Items(ec => ec?.ExceptInterfaces, src => src?.ExceptInterfaces)));
|
||||||
list.Add(Multi(SectionProcess, DnsmasqConfKeys.AuthServer, status, Items(ec => ec?.AuthServerValues, src => src?.AuthServerValues)));
|
list.Add(Multi(DnsmasqConfKeys.AuthServer, status, Items(ec => ec?.AuthServerValues, src => src?.AuthServerValues)));
|
||||||
list.Add(Multi(SectionProcess, DnsmasqConfKeys.NoDhcpInterface, status, Items(ec => ec?.NoDhcpInterfaceValues, src => src?.NoDhcpInterfaceValues)));
|
list.Add(Multi(DnsmasqConfKeys.NoDhcpInterface, status, Items(ec => ec?.NoDhcpInterfaceValues, src => src?.NoDhcpInterfaceValues)));
|
||||||
list.Add(Multi(SectionProcess, DnsmasqConfKeys.NoDhcpv4Interface, status, Items(ec => ec?.NoDhcpv4InterfaceValues, src => src?.NoDhcpv4InterfaceValues)));
|
list.Add(Multi(DnsmasqConfKeys.NoDhcpv4Interface, status, Items(ec => ec?.NoDhcpv4InterfaceValues, src => src?.NoDhcpv4InterfaceValues)));
|
||||||
list.Add(Multi(SectionProcess, DnsmasqConfKeys.NoDhcpv6Interface, status, Items(ec => ec?.NoDhcpv6InterfaceValues, src => src?.NoDhcpv6InterfaceValues)));
|
list.Add(Multi(DnsmasqConfKeys.NoDhcpv6Interface, status, Items(ec => ec?.NoDhcpv6InterfaceValues, src => src?.NoDhcpv6InterfaceValues)));
|
||||||
list.Add(Single(SectionProcess, DnsmasqConfKeys.PidFile, status, s => Config(s)?.PidFilePath, s => Sources(s)?.PidFilePath, null));
|
list.Add(Single(DnsmasqConfKeys.PidFile, status, s => Config(s)?.PidFilePath, s => Sources(s)?.PidFilePath, null));
|
||||||
list.Add(Single(SectionProcess, DnsmasqConfKeys.User, status, s => Config(s)?.User, s => Sources(s)?.User, null));
|
list.Add(Single(DnsmasqConfKeys.User, status, s => Config(s)?.User, s => Sources(s)?.User, null));
|
||||||
list.Add(Single(SectionProcess, DnsmasqConfKeys.Group, status, s => Config(s)?.Group, s => Sources(s)?.Group, null));
|
list.Add(Single(DnsmasqConfKeys.Group, status, s => Config(s)?.Group, s => Sources(s)?.Group, null));
|
||||||
list.Add(Single(SectionProcess, DnsmasqConfKeys.LogFacility, status, s => Config(s)?.LogFacility, s => Sources(s)?.LogFacility, null));
|
list.Add(Single(DnsmasqConfKeys.LogFacility, status, s => Config(s)?.LogFacility, s => Sources(s)?.LogFacility, null));
|
||||||
list.Add(Single(SectionProcess, DnsmasqConfKeys.EnableDbus, status, s => Config(s)?.EnableDbus, s => Sources(s)?.EnableDbus, null));
|
list.Add(Single(DnsmasqConfKeys.EnableDbus, status, s => Config(s)?.EnableDbus, s => Sources(s)?.EnableDbus, null));
|
||||||
list.Add(Single(SectionProcess, DnsmasqConfKeys.EnableUbus, status, s => Config(s)?.EnableUbus, s => Sources(s)?.EnableUbus, null));
|
list.Add(Single(DnsmasqConfKeys.EnableUbus, status, s => Config(s)?.EnableUbus, s => Sources(s)?.EnableUbus, null));
|
||||||
list.Add(Single(SectionProcess, DnsmasqConfKeys.EnableRa, status, s => Config(s)?.EnableRa, s => Sources(s)?.EnableRa, null));
|
list.Add(Single(DnsmasqConfKeys.EnableRa, status, s => Config(s)?.EnableRa, s => Sources(s)?.EnableRa, null));
|
||||||
list.Add(Single(SectionProcess, DnsmasqConfKeys.LogDhcp, status, s => Config(s)?.LogDhcp, s => Sources(s)?.LogDhcp, null));
|
list.Add(Single(DnsmasqConfKeys.LogDhcp, status, s => Config(s)?.LogDhcp, s => Sources(s)?.LogDhcp, null));
|
||||||
|
list.Add(Single(DnsmasqConfKeys.KeepInForeground, status, s => Config(s)?.KeepInForeground, s => Sources(s)?.KeepInForeground, null));
|
||||||
|
list.Add(Single(DnsmasqConfKeys.NoDaemon, status, s => Config(s)?.NoDaemon, s => Sources(s)?.NoDaemon, null));
|
||||||
|
list.Add(Single(DnsmasqConfKeys.Conntrack, status, s => Config(s)?.Conntrack, s => Sources(s)?.Conntrack, null));
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static EffectiveConfigFieldDescriptor Single(string sectionId, string optionName, DnsmasqServiceStatus? status,
|
private static EffectiveConfigFieldDescriptor Single(string optionName, DnsmasqServiceStatus? status,
|
||||||
Func<DnsmasqServiceStatus?, object?>? getValue,
|
Func<DnsmasqServiceStatus?, object?>? getValue,
|
||||||
Func<DnsmasqServiceStatus?, ConfigValueSource?>? getSource,
|
Func<DnsmasqServiceStatus?, ConfigValueSource?>? getSource,
|
||||||
Func<DnsmasqServiceStatus?, IReadOnlyList<ValueWithSource>?>? getItems)
|
Func<DnsmasqServiceStatus?, IReadOnlyList<ValueWithSource>?>? getItems)
|
||||||
{
|
{
|
||||||
|
var sectionId = EffectiveConfigSections.GetSectionId(optionName);
|
||||||
return new EffectiveConfigFieldDescriptor(sectionId, optionName, false, status, getValue, getSource, getItems);
|
return new EffectiveConfigFieldDescriptor(sectionId, optionName, false, status, getValue, getSource, getItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,9 +198,10 @@ public static class EffectiveConfigFieldBuilder
|
|||||||
: paths.Select(p => new ValueWithSource(p, null)).ToList();
|
: paths.Select(p => new ValueWithSource(p, null)).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static EffectiveConfigFieldDescriptor Multi(string sectionId, string optionName, DnsmasqServiceStatus? status,
|
private static EffectiveConfigFieldDescriptor Multi(string optionName, DnsmasqServiceStatus? status,
|
||||||
Func<DnsmasqServiceStatus?, IReadOnlyList<ValueWithSource>?>? getItems)
|
Func<DnsmasqServiceStatus?, IReadOnlyList<ValueWithSource>?>? getItems)
|
||||||
{
|
{
|
||||||
|
var sectionId = EffectiveConfigSections.GetSectionId(optionName);
|
||||||
return new EffectiveConfigFieldDescriptor(sectionId, optionName, true, status, null, null, getItems);
|
return new EffectiveConfigFieldDescriptor(sectionId, optionName, true, status, null, null, getItems);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
using System.Collections.Frozen;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Single source of truth: which effective-config sections exist, their display order and titles,
|
||||||
|
/// and which option (conf key) belongs to which section. Used by the field builder (section lookup)
|
||||||
|
/// and by the UI (section order, context filtering).
|
||||||
|
/// </summary>
|
||||||
|
public static class EffectiveConfigSections
|
||||||
|
{
|
||||||
|
// --- Section IDs (used by registry, views, etc.) ---
|
||||||
|
public const string SectionHosts = "hosts";
|
||||||
|
public const string SectionResolver = "resolver";
|
||||||
|
public const string SectionDnsRecords = "dns-records";
|
||||||
|
public const string SectionDhcp = "dhcp";
|
||||||
|
public const string SectionTftpPxe = "tftp-pxe";
|
||||||
|
public const string SectionDnssec = "dnssec";
|
||||||
|
public const string SectionCache = "cache";
|
||||||
|
public const string SectionProcess = "process";
|
||||||
|
|
||||||
|
/// <summary>Section definition: id, display title, option names in display order.</summary>
|
||||||
|
public sealed record SectionDef(string SectionId, string Title, string[] OptionNames);
|
||||||
|
|
||||||
|
/// <summary>All sections in display order with their option names. Defines option → section mapping.</summary>
|
||||||
|
public static readonly IReadOnlyList<SectionDef> Sections =
|
||||||
|
[
|
||||||
|
new SectionDef(SectionHosts, "Hosts", [
|
||||||
|
DnsmasqConfKeys.NoHosts,
|
||||||
|
DnsmasqConfKeys.AddnHosts,
|
||||||
|
DnsmasqConfKeys.Hostsdir,
|
||||||
|
DnsmasqConfKeys.ReadEthers,
|
||||||
|
]),
|
||||||
|
new SectionDef(SectionResolver, "Resolver / DNS", [
|
||||||
|
DnsmasqOptionTooltips.ServerLocalLabel,
|
||||||
|
DnsmasqConfKeys.RevServer,
|
||||||
|
DnsmasqConfKeys.Address,
|
||||||
|
DnsmasqConfKeys.ResolvFile,
|
||||||
|
DnsmasqConfKeys.NoResolv,
|
||||||
|
DnsmasqConfKeys.DomainNeeded,
|
||||||
|
DnsmasqConfKeys.Port,
|
||||||
|
DnsmasqConfKeys.LogQueries,
|
||||||
|
DnsmasqConfKeys.StrictOrder,
|
||||||
|
DnsmasqConfKeys.AllServers,
|
||||||
|
DnsmasqConfKeys.AuthTtl,
|
||||||
|
DnsmasqConfKeys.EdnsPacketMax,
|
||||||
|
DnsmasqConfKeys.QueryPort,
|
||||||
|
DnsmasqConfKeys.PortLimit,
|
||||||
|
DnsmasqConfKeys.MinPort,
|
||||||
|
DnsmasqConfKeys.MaxPort,
|
||||||
|
DnsmasqConfKeys.DnsLoopDetect,
|
||||||
|
DnsmasqConfKeys.StopDnsRebind,
|
||||||
|
DnsmasqConfKeys.RebindLocalhostOk,
|
||||||
|
DnsmasqConfKeys.ClearOnReload,
|
||||||
|
DnsmasqConfKeys.ExpandHosts,
|
||||||
|
DnsmasqConfKeys.BogusPriv,
|
||||||
|
DnsmasqConfKeys.RebindDomainOk,
|
||||||
|
DnsmasqConfKeys.BogusNxdomain,
|
||||||
|
DnsmasqConfKeys.IgnoreAddress,
|
||||||
|
DnsmasqConfKeys.Alias,
|
||||||
|
DnsmasqConfKeys.FilterRr,
|
||||||
|
DnsmasqConfKeys.Filterwin2k,
|
||||||
|
DnsmasqConfKeys.FilterA,
|
||||||
|
DnsmasqConfKeys.FilterAaaa,
|
||||||
|
DnsmasqConfKeys.LocaliseQueries,
|
||||||
|
DnsmasqConfKeys.FastDnsRetry,
|
||||||
|
DnsmasqConfKeys.Ipset,
|
||||||
|
DnsmasqConfKeys.Nftset,
|
||||||
|
]),
|
||||||
|
new SectionDef(SectionDnsRecords, "DNS records", [
|
||||||
|
DnsmasqConfKeys.Domain,
|
||||||
|
DnsmasqConfKeys.Cname,
|
||||||
|
DnsmasqConfKeys.MxHost,
|
||||||
|
DnsmasqConfKeys.Srv,
|
||||||
|
DnsmasqConfKeys.PtrRecord,
|
||||||
|
DnsmasqConfKeys.TxtRecord,
|
||||||
|
DnsmasqConfKeys.NaptrRecord,
|
||||||
|
DnsmasqConfKeys.HostRecord,
|
||||||
|
DnsmasqConfKeys.DynamicHost,
|
||||||
|
DnsmasqConfKeys.InterfaceName,
|
||||||
|
DnsmasqConfKeys.MxTarget,
|
||||||
|
DnsmasqConfKeys.Localmx,
|
||||||
|
DnsmasqConfKeys.Selfmx,
|
||||||
|
]),
|
||||||
|
new SectionDef(SectionDhcp, "DHCP", [
|
||||||
|
DnsmasqConfKeys.DhcpAuthoritative,
|
||||||
|
DnsmasqConfKeys.DhcpRapidCommit,
|
||||||
|
DnsmasqConfKeys.LeasefileRo,
|
||||||
|
DnsmasqConfKeys.DhcpScript,
|
||||||
|
DnsmasqConfKeys.DhcpLeasefile,
|
||||||
|
DnsmasqConfKeys.DhcpLeaseMax,
|
||||||
|
DnsmasqConfKeys.DhcpTtl,
|
||||||
|
DnsmasqConfKeys.DhcpRange,
|
||||||
|
DnsmasqConfKeys.DhcpHost,
|
||||||
|
DnsmasqConfKeys.DhcpOption,
|
||||||
|
DnsmasqConfKeys.DhcpOptionForce,
|
||||||
|
DnsmasqConfKeys.DhcpMatch,
|
||||||
|
DnsmasqConfKeys.DhcpMac,
|
||||||
|
DnsmasqConfKeys.DhcpNameMatch,
|
||||||
|
DnsmasqConfKeys.DhcpIgnoreNames,
|
||||||
|
DnsmasqConfKeys.DhcpHostsfile,
|
||||||
|
DnsmasqConfKeys.DhcpOptsfile,
|
||||||
|
DnsmasqConfKeys.DhcpHostsdir,
|
||||||
|
DnsmasqConfKeys.DhcpBoot,
|
||||||
|
DnsmasqConfKeys.DhcpIgnore,
|
||||||
|
DnsmasqConfKeys.DhcpVendorclass,
|
||||||
|
DnsmasqConfKeys.DhcpUserclass,
|
||||||
|
DnsmasqConfKeys.RaParam,
|
||||||
|
DnsmasqConfKeys.Slaac,
|
||||||
|
]),
|
||||||
|
new SectionDef(SectionTftpPxe, "TFTP / PXE", [
|
||||||
|
DnsmasqConfKeys.EnableTftp,
|
||||||
|
DnsmasqConfKeys.TftpSecure,
|
||||||
|
DnsmasqConfKeys.TftpNoFail,
|
||||||
|
DnsmasqConfKeys.TftpNoBlocksize,
|
||||||
|
DnsmasqConfKeys.TftpRoot,
|
||||||
|
DnsmasqConfKeys.PxePrompt,
|
||||||
|
DnsmasqConfKeys.PxeService,
|
||||||
|
]),
|
||||||
|
new SectionDef(SectionDnssec, "DNSSEC", [
|
||||||
|
DnsmasqConfKeys.Dnssec,
|
||||||
|
DnsmasqConfKeys.DnssecCheckUnsigned,
|
||||||
|
DnsmasqConfKeys.ProxyDnssec,
|
||||||
|
DnsmasqConfKeys.TrustAnchor,
|
||||||
|
]),
|
||||||
|
new SectionDef(SectionCache, "Cache", [
|
||||||
|
DnsmasqConfKeys.CacheRr,
|
||||||
|
DnsmasqConfKeys.CacheSize,
|
||||||
|
DnsmasqConfKeys.LocalTtl,
|
||||||
|
DnsmasqConfKeys.NoNegcache,
|
||||||
|
DnsmasqConfKeys.NegTtl,
|
||||||
|
DnsmasqConfKeys.MaxTtl,
|
||||||
|
DnsmasqConfKeys.MaxCacheTtl,
|
||||||
|
DnsmasqConfKeys.MinCacheTtl,
|
||||||
|
]),
|
||||||
|
new SectionDef(SectionProcess, "Process & networking", [
|
||||||
|
DnsmasqConfKeys.NoPoll,
|
||||||
|
DnsmasqConfKeys.BindInterfaces,
|
||||||
|
DnsmasqConfKeys.BindDynamic,
|
||||||
|
DnsmasqConfKeys.LogDebug,
|
||||||
|
DnsmasqConfKeys.LogAsync,
|
||||||
|
DnsmasqConfKeys.LocalService,
|
||||||
|
DnsmasqConfKeys.Interface,
|
||||||
|
DnsmasqConfKeys.ListenAddress,
|
||||||
|
DnsmasqConfKeys.ExceptInterface,
|
||||||
|
DnsmasqConfKeys.AuthServer,
|
||||||
|
DnsmasqConfKeys.NoDhcpInterface,
|
||||||
|
DnsmasqConfKeys.NoDhcpv4Interface,
|
||||||
|
DnsmasqConfKeys.NoDhcpv6Interface,
|
||||||
|
DnsmasqConfKeys.PidFile,
|
||||||
|
DnsmasqConfKeys.User,
|
||||||
|
DnsmasqConfKeys.Group,
|
||||||
|
DnsmasqConfKeys.LogFacility,
|
||||||
|
DnsmasqConfKeys.EnableDbus,
|
||||||
|
DnsmasqConfKeys.EnableUbus,
|
||||||
|
DnsmasqConfKeys.EnableRa,
|
||||||
|
DnsmasqConfKeys.LogDhcp,
|
||||||
|
DnsmasqConfKeys.KeepInForeground,
|
||||||
|
DnsmasqConfKeys.NoDaemon,
|
||||||
|
DnsmasqConfKeys.Conntrack,
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
|
||||||
|
private static FrozenDictionary<string, string>? _optionToSection;
|
||||||
|
|
||||||
|
/// <summary>Maps option name (conf key or display label) to section id. Used by Single/Multi in the builder.</summary>
|
||||||
|
public static string GetSectionId(string optionName)
|
||||||
|
{
|
||||||
|
_optionToSection ??= BuildOptionToSection();
|
||||||
|
return _optionToSection.TryGetValue(optionName, out var sectionId) ? sectionId : SectionResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Section id and title in display order. Use for rendering section headers.</summary>
|
||||||
|
public static IReadOnlyList<(string SectionId, string Title)> GetSectionsInOrder() =>
|
||||||
|
Sections.Select(s => (s.SectionId, s.Title)).ToList();
|
||||||
|
|
||||||
|
/// <summary>Option names in the given section (display order). For context/views that restrict by section.</summary>
|
||||||
|
public static IReadOnlyList<string> GetOptionsInSection(string sectionId)
|
||||||
|
{
|
||||||
|
var def = Sections.FirstOrDefault(s => string.Equals(s.SectionId, sectionId, StringComparison.OrdinalIgnoreCase));
|
||||||
|
return def?.OptionNames ?? Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FrozenDictionary<string, string> BuildOptionToSection()
|
||||||
|
{
|
||||||
|
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var section in Sections)
|
||||||
|
{
|
||||||
|
foreach (var option in section.OptionNames)
|
||||||
|
dict[option] = section.SectionId;
|
||||||
|
}
|
||||||
|
return dict.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using DnsmasqWebUI.Models.Dnsmasq;
|
||||||
|
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Defines which sections and (optionally) which fields are visible per EffectiveConfigContext.
|
||||||
|
/// Single place to add new contexts or change what shows on Hosts/DHCP/etc.
|
||||||
|
/// </summary>
|
||||||
|
public static class EffectiveConfigViews
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Ordered list of sections and optional field allow-lists for the given context.
|
||||||
|
/// </summary>
|
||||||
|
public static IReadOnlyList<EffectiveConfigSectionView> GetViewsForContext(EffectiveConfigContext context)
|
||||||
|
{
|
||||||
|
return context switch
|
||||||
|
{
|
||||||
|
EffectiveConfigContext.All => GetAllSections(),
|
||||||
|
EffectiveConfigContext.Hosts => GetHostsView(),
|
||||||
|
EffectiveConfigContext.Dhcp => GetDhcpView(),
|
||||||
|
_ => [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<EffectiveConfigSectionView> GetAllSections()
|
||||||
|
{
|
||||||
|
return EffectiveConfigSections.Sections
|
||||||
|
.Select(s => new EffectiveConfigSectionView(s.SectionId, s.Title, null))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<EffectiveConfigSectionView> GetHostsView() =>
|
||||||
|
[
|
||||||
|
new(EffectiveConfigSections.SectionHosts, "Hosts", [
|
||||||
|
DnsmasqConfKeys.NoHosts,
|
||||||
|
DnsmasqConfKeys.AddnHosts,
|
||||||
|
DnsmasqConfKeys.Hostsdir,
|
||||||
|
DnsmasqConfKeys.ReadEthers,
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
|
||||||
|
private static IReadOnlyList<EffectiveConfigSectionView> GetDhcpView() =>
|
||||||
|
[
|
||||||
|
new(EffectiveConfigSections.SectionDhcp, "DHCP", null),
|
||||||
|
new(EffectiveConfigSections.SectionTftpPxe, "TFTP / PXE", null),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For each section in the view, returns the context-visible descriptors for that section.
|
||||||
|
/// Does not apply search; component applies search to each section's list and hides sections with no matches.
|
||||||
|
/// </summary>
|
||||||
|
public static IReadOnlyDictionary<string, IReadOnlyList<EffectiveConfigFieldDescriptor>> GetDescriptorsBySection(
|
||||||
|
DnsmasqServiceStatus? status,
|
||||||
|
IReadOnlyList<EffectiveConfigSectionView> views)
|
||||||
|
{
|
||||||
|
if (status == null || views.Count == 0)
|
||||||
|
return new Dictionary<string, IReadOnlyList<EffectiveConfigFieldDescriptor>>();
|
||||||
|
|
||||||
|
var allDescriptors = EffectiveConfigFieldBuilder.BuildFieldDescriptors(status);
|
||||||
|
var result = new Dictionary<string, List<EffectiveConfigFieldDescriptor>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var view in views)
|
||||||
|
{
|
||||||
|
var list = allDescriptors
|
||||||
|
.Where(d => string.Equals(d.SectionId, view.SectionId, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Where(d => view.AllowedOptionNames == null
|
||||||
|
|| view.AllowedOptionNames.Contains(d.OptionName, StringComparer.OrdinalIgnoreCase))
|
||||||
|
.ToList();
|
||||||
|
if (list.Count > 0)
|
||||||
|
result[view.SectionId] = list;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ToDictionary(kv => kv.Key, kv => (IReadOnlyList<EffectiveConfigFieldDescriptor>)kv.Value, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service that checks GitHub for a newer release. Runs on a configurable interval (hosted service)
|
||||||
|
/// and can be triggered manually. UI subscribes to <see cref="ResultChanged"/> to refresh when state updates.
|
||||||
|
/// </summary>
|
||||||
|
public interface IUpdateCheckService
|
||||||
|
{
|
||||||
|
/// <summary>Raised when a check completes (background or manual). Subscribe and call StateHasChanged from the UI.</summary>
|
||||||
|
event EventHandler? ResultChanged;
|
||||||
|
|
||||||
|
/// <summary>True if the latest GitHub release is newer than the current app version.</summary>
|
||||||
|
bool NewerVersionAvailable { get; }
|
||||||
|
|
||||||
|
/// <summary>Tag of the newer release (e.g. v0.0.5), or null if none.</summary>
|
||||||
|
string? NewerVersionTag { get; }
|
||||||
|
|
||||||
|
/// <summary>URL of the newer release page, or null if none.</summary>
|
||||||
|
string? NewerVersionUrl { get; }
|
||||||
|
|
||||||
|
/// <summary>When the last check completed (success or failure), or null if never run.</summary>
|
||||||
|
DateTime? LastCheckTime { get; }
|
||||||
|
|
||||||
|
/// <summary>True while a check is in progress.</summary>
|
||||||
|
bool CheckInProgress { get; }
|
||||||
|
|
||||||
|
/// <summary>Run a check now (e.g. user clicked "check for updates").</summary>
|
||||||
|
Task CheckNowAsync();
|
||||||
|
}
|
||||||
@@ -265,6 +265,9 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
|
|||||||
var dhcpMacValues = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.DhcpMac);
|
var dhcpMacValues = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.DhcpMac);
|
||||||
var dhcpNameMatchValues = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.DhcpNameMatch);
|
var dhcpNameMatchValues = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.DhcpNameMatch);
|
||||||
var dhcpIgnoreNamesValues = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.DhcpIgnoreNames);
|
var dhcpIgnoreNamesValues = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.DhcpIgnoreNames);
|
||||||
|
var dhcpHostsfilePaths = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.DhcpHostsfile);
|
||||||
|
var dhcpOptsfilePaths = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.DhcpOptsfile);
|
||||||
|
var dhcpHostsdirPaths = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.DhcpHostsdir);
|
||||||
|
|
||||||
var expandHosts = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.ExpandHosts);
|
var expandHosts = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.ExpandHosts);
|
||||||
var bogusPriv = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.BogusPriv);
|
var bogusPriv = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.BogusPriv);
|
||||||
@@ -299,6 +302,9 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
|
|||||||
var selfmx = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.Selfmx);
|
var selfmx = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.Selfmx);
|
||||||
var enableRa = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.EnableRa);
|
var enableRa = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.EnableRa);
|
||||||
var logDhcp = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.LogDhcp);
|
var logDhcp = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.LogDhcp);
|
||||||
|
var keepInForeground = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.KeepInForeground);
|
||||||
|
var noDaemon = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.NoDaemon);
|
||||||
|
var proxyDnssec = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.ProxyDnssec);
|
||||||
|
|
||||||
var dhcpLeaseFilePath = DnsmasqConfIncludeParser.GetDhcpLeaseFilePathFromConfigFiles(paths, pathToLines);
|
var dhcpLeaseFilePath = DnsmasqConfIncludeParser.GetDhcpLeaseFilePathFromConfigFiles(paths, pathToLines);
|
||||||
|
|
||||||
@@ -360,6 +366,8 @@ 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, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.MxTarget);
|
var (mxTargetVal, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.MxTarget);
|
||||||
var mxTarget = string.IsNullOrWhiteSpace(mxTargetVal) ? null : mxTargetVal.Trim();
|
var mxTarget = string.IsNullOrWhiteSpace(mxTargetVal) ? null : mxTargetVal.Trim();
|
||||||
|
var (conntrackVal, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(paths, pathToLines, DnsmasqConfKeys.Conntrack);
|
||||||
|
var conntrack = string.IsNullOrWhiteSpace(conntrackVal) ? null : conntrackVal.Trim();
|
||||||
|
|
||||||
return new EffectiveDnsmasqConfig(
|
return new EffectiveDnsmasqConfig(
|
||||||
noHosts, addnHosts, hostsdirPath,
|
noHosts, addnHosts, hostsdirPath,
|
||||||
@@ -368,13 +376,15 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
|
|||||||
rebindDomainOk, bogusNxdomain, ignoreAddress, alias, filterRr, cacheRr, authServer, noDhcpInterface, noDhcpv4Interface, noDhcpv6Interface,
|
rebindDomainOk, bogusNxdomain, ignoreAddress, alias, filterRr, cacheRr, authServer, noDhcpInterface, noDhcpv4Interface, noDhcpv6Interface,
|
||||||
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,
|
||||||
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,
|
||||||
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
|
dhcpScriptPath, mxTarget, conntrack
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,6 +438,9 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
|
|||||||
var dhcpMacWithSource = DnsmasqConfIncludeParser.GetMultiValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpMac, managedFilePath);
|
var dhcpMacWithSource = DnsmasqConfIncludeParser.GetMultiValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpMac, managedFilePath);
|
||||||
var dhcpNameMatchWithSource = DnsmasqConfIncludeParser.GetMultiValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpNameMatch, managedFilePath);
|
var dhcpNameMatchWithSource = DnsmasqConfIncludeParser.GetMultiValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpNameMatch, managedFilePath);
|
||||||
var dhcpIgnoreNamesWithSource = DnsmasqConfIncludeParser.GetMultiValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpIgnoreNames, managedFilePath);
|
var dhcpIgnoreNamesWithSource = DnsmasqConfIncludeParser.GetMultiValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpIgnoreNames, managedFilePath);
|
||||||
|
var dhcpHostsfileWithSource = DnsmasqConfIncludeParser.GetMultiValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpHostsfile, managedFilePath);
|
||||||
|
var dhcpOptsfileWithSource = DnsmasqConfIncludeParser.GetMultiValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpOptsfile, managedFilePath);
|
||||||
|
var dhcpHostsdirWithSource = DnsmasqConfIncludeParser.GetMultiValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpHostsdir, managedFilePath);
|
||||||
|
|
||||||
var (_, expandHostsSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.ExpandHosts, managedFilePath);
|
var (_, expandHostsSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.ExpandHosts, managedFilePath);
|
||||||
var (_, bogusPrivSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.BogusPriv, managedFilePath);
|
var (_, bogusPrivSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.BogusPriv, managedFilePath);
|
||||||
@@ -462,6 +475,9 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
|
|||||||
var (_, selfmxSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.Selfmx, managedFilePath);
|
var (_, selfmxSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.Selfmx, managedFilePath);
|
||||||
var (_, enableRaSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.EnableRa, managedFilePath);
|
var (_, enableRaSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.EnableRa, managedFilePath);
|
||||||
var (_, logDhcpSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.LogDhcp, managedFilePath);
|
var (_, logDhcpSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.LogDhcp, managedFilePath);
|
||||||
|
var (_, keepInForegroundSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.KeepInForeground, managedFilePath);
|
||||||
|
var (_, noDaemonSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.NoDaemon, managedFilePath);
|
||||||
|
var (_, proxyDnssecSource) = DnsmasqConfIncludeParser.GetFlagFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.ProxyDnssec, managedFilePath);
|
||||||
|
|
||||||
var (_, dhcpLeaseFilePathSource) = DnsmasqConfIncludeParser.GetDhcpLeaseFilePathFromConfigFilesWithSource(paths, pathToLines, managedFilePath);
|
var (_, dhcpLeaseFilePathSource) = DnsmasqConfIncludeParser.GetDhcpLeaseFilePathFromConfigFilesWithSource(paths, pathToLines, managedFilePath);
|
||||||
var (_, cacheSizeSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.CacheSize, managedFilePath);
|
var (_, cacheSizeSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.CacheSize, managedFilePath);
|
||||||
@@ -493,6 +509,7 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
|
|||||||
var (_, fastDnsRetrySource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.FastDnsRetry, managedFilePath);
|
var (_, fastDnsRetrySource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.FastDnsRetry, managedFilePath);
|
||||||
var (_, dhcpScriptPathSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpScript, managedFilePath);
|
var (_, dhcpScriptPathSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpScript, managedFilePath);
|
||||||
var (_, mxTargetSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.MxTarget, managedFilePath);
|
var (_, mxTargetSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.MxTarget, managedFilePath);
|
||||||
|
var (_, conntrackSource) = DnsmasqConfIncludeParser.GetLastValueFromConfigFilesWithSource(paths, pathToLines, DnsmasqConfKeys.Conntrack, managedFilePath);
|
||||||
|
|
||||||
return new EffectiveConfigSources(
|
return new EffectiveConfigSources(
|
||||||
noHostsSource, addnHostsWithSource.Select(t => new PathWithSource(t.Path, t.Source)).ToList(), hostsdirPathSource,
|
noHostsSource, addnHostsWithSource.Select(t => new PathWithSource(t.Path, t.Source)).ToList(), hostsdirPathSource,
|
||||||
@@ -541,6 +558,9 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
|
|||||||
dhcpMacWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
|
dhcpMacWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
|
||||||
dhcpNameMatchWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
|
dhcpNameMatchWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
|
||||||
dhcpIgnoreNamesWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
|
dhcpIgnoreNamesWithSource.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(),
|
||||||
|
dhcpHostsdirWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
|
||||||
expandHostsSource,
|
expandHostsSource,
|
||||||
bogusPrivSource,
|
bogusPrivSource,
|
||||||
strictOrderSource,
|
strictOrderSource,
|
||||||
@@ -574,6 +594,9 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
|
|||||||
selfmxSource,
|
selfmxSource,
|
||||||
enableRaSource,
|
enableRaSource,
|
||||||
logDhcpSource,
|
logDhcpSource,
|
||||||
|
keepInForegroundSource,
|
||||||
|
noDaemonSource,
|
||||||
|
proxyDnssecSource,
|
||||||
dhcpLeaseFilePathSource,
|
dhcpLeaseFilePathSource,
|
||||||
cacheSizeSource,
|
cacheSizeSource,
|
||||||
portSource,
|
portSource,
|
||||||
@@ -603,7 +626,8 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
|
|||||||
enableUbusSource,
|
enableUbusSource,
|
||||||
fastDnsRetrySource,
|
fastDnsRetrySource,
|
||||||
dhcpScriptPathSource,
|
dhcpScriptPathSource,
|
||||||
mxTargetSource
|
mxTargetSource,
|
||||||
|
conntrackSource
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -656,16 +680,18 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
|
|||||||
HostRecordValues: Array.Empty<string>(), DynamicHostValues: Array.Empty<string>(), InterfaceNameValues: Array.Empty<string>(),
|
HostRecordValues: Array.Empty<string>(), DynamicHostValues: Array.Empty<string>(), InterfaceNameValues: Array.Empty<string>(),
|
||||||
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>(),
|
||||||
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,
|
||||||
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
|
DhcpScriptPath: null, MxTarget: null, Conntrack: null
|
||||||
);
|
);
|
||||||
|
|
||||||
private static EffectiveConfigSources CreateDefaultEffectiveConfigSources() =>
|
private static EffectiveConfigSources CreateDefaultEffectiveConfigSources() =>
|
||||||
@@ -689,16 +715,18 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
|
|||||||
HostRecordValues: Array.Empty<ValueWithSource>(), DynamicHostValues: Array.Empty<ValueWithSource>(), InterfaceNameValues: Array.Empty<ValueWithSource>(),
|
HostRecordValues: Array.Empty<ValueWithSource>(), DynamicHostValues: Array.Empty<ValueWithSource>(), InterfaceNameValues: Array.Empty<ValueWithSource>(),
|
||||||
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>(),
|
||||||
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,
|
||||||
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
|
DhcpScriptPath: null, MxTarget: null, Conntrack: null
|
||||||
);
|
);
|
||||||
|
|
||||||
private static int? TryParseInt(string? value)
|
private static int? TryParseInt(string? value)
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ public class EffectiveConfigRenderFragmentRegistry : IEffectiveConfigRenderFragm
|
|||||||
RegisterFlag(EffectiveConfigFieldBuilder.SectionDnssec, DnsmasqConfKeys.DnssecCheckUnsigned);
|
RegisterFlag(EffectiveConfigFieldBuilder.SectionDnssec, DnsmasqConfKeys.DnssecCheckUnsigned);
|
||||||
RegisterFlag(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.EnableRa);
|
RegisterFlag(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.EnableRa);
|
||||||
RegisterFlag(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.LogDhcp);
|
RegisterFlag(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.LogDhcp);
|
||||||
|
RegisterFlag(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.KeepInForeground);
|
||||||
|
RegisterFlag(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.NoDaemon);
|
||||||
|
RegisterFlag(EffectiveConfigFieldBuilder.SectionDnssec, DnsmasqConfKeys.ProxyDnssec);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RegisterFlag(string sectionId, string optionName)
|
private void RegisterFlag(string sectionId, string optionName)
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||||
|
using DnsmasqWebUI.Models.Config;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs an update check shortly after startup, then periodically at the configured interval.
|
||||||
|
/// Disabled when <see cref="UpdateCheckOptions.IntervalMinutes"/> is 0.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UpdateCheckHostedService : IApplicationHostedService
|
||||||
|
{
|
||||||
|
private const int StartupDelaySeconds = 30;
|
||||||
|
|
||||||
|
private readonly IUpdateCheckService _updateCheckService;
|
||||||
|
private readonly IOptions<UpdateCheckOptions> _options;
|
||||||
|
|
||||||
|
public UpdateCheckHostedService(IUpdateCheckService updateCheckService, IOptions<UpdateCheckOptions> options)
|
||||||
|
{
|
||||||
|
_updateCheckService = updateCheckService;
|
||||||
|
_options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_ = RunLoopAsync(cancellationToken);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
|
||||||
|
private async Task RunLoopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var intervalMinutes = _options.Value.IntervalMinutes;
|
||||||
|
if (intervalMinutes <= 0) return;
|
||||||
|
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(StartupDelaySeconds), cancellationToken);
|
||||||
|
await _updateCheckService.CheckNowAsync();
|
||||||
|
|
||||||
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
await Task.Delay(TimeSpan.FromMinutes(intervalMinutes), cancellationToken);
|
||||||
|
await _updateCheckService.CheckNowAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Text.Json;
|
||||||
|
using DnsmasqWebUI.Extensions;
|
||||||
|
using DnsmasqWebUI.Infrastructure.Services.Abstractions;
|
||||||
|
using DnsmasqWebUI.Models.Config;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Services;
|
||||||
|
|
||||||
|
public sealed class UpdateCheckService : IUpdateCheckService
|
||||||
|
{
|
||||||
|
private const string ReleasesBaseUrl = "https://github.com/alexhopeoconnor/dnsmasq-webui/releases";
|
||||||
|
private static readonly string CurrentVersion = typeof(UpdateCheckService).Assembly
|
||||||
|
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "?";
|
||||||
|
|
||||||
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
private readonly IOptions<UpdateCheckOptions> _options;
|
||||||
|
|
||||||
|
private readonly object _lock = new();
|
||||||
|
private string? _newerVersionTag;
|
||||||
|
private string? _newerVersionUrl;
|
||||||
|
private DateTime? _lastCheckTime;
|
||||||
|
private bool _checkInProgress;
|
||||||
|
|
||||||
|
public UpdateCheckService(IHttpClientFactory httpClientFactory, IOptions<UpdateCheckOptions> options)
|
||||||
|
{
|
||||||
|
_httpClientFactory = httpClientFactory;
|
||||||
|
_options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler? ResultChanged;
|
||||||
|
|
||||||
|
public bool NewerVersionAvailable => _newerVersionTag != null;
|
||||||
|
public string? NewerVersionTag => _newerVersionTag;
|
||||||
|
public string? NewerVersionUrl => _newerVersionUrl;
|
||||||
|
public DateTime? LastCheckTime => _lastCheckTime;
|
||||||
|
public bool CheckInProgress => _checkInProgress;
|
||||||
|
|
||||||
|
public async Task CheckNowAsync()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (_checkInProgress) return;
|
||||||
|
_checkInProgress = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = _httpClientFactory.CreateClient(ServiceCollectionExtensions.GitHubClientName);
|
||||||
|
var response = await client.GetAsync("repos/alexhopeoconnor/dnsmasq-webui/releases/latest");
|
||||||
|
string? newTag = null;
|
||||||
|
string? newUrl = null;
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var json = await response.Content.ReadAsStringAsync();
|
||||||
|
using var doc = JsonDocument.Parse(json);
|
||||||
|
var root = doc.RootElement;
|
||||||
|
var tagName = root.TryGetProperty("tag_name", out var tagProp) ? tagProp.GetString() : null;
|
||||||
|
var htmlUrl = root.TryGetProperty("html_url", out var urlProp) ? urlProp.GetString() : null;
|
||||||
|
if (!string.IsNullOrEmpty(tagName) && !string.IsNullOrEmpty(htmlUrl))
|
||||||
|
{
|
||||||
|
var latest = tagName.TrimStart('v');
|
||||||
|
var current = BaseVersion(CurrentVersion);
|
||||||
|
if (!string.IsNullOrEmpty(current) && current != "?" && IsNewerVersion(latest, current))
|
||||||
|
{
|
||||||
|
newTag = tagName.StartsWith('v') ? tagName : "v" + tagName;
|
||||||
|
newUrl = htmlUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_newerVersionTag = newTag;
|
||||||
|
_newerVersionUrl = newUrl;
|
||||||
|
_lastCheckTime = DateTime.UtcNow;
|
||||||
|
_checkInProgress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResultChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_lastCheckTime = DateTime.UtcNow;
|
||||||
|
_checkInProgress = false;
|
||||||
|
}
|
||||||
|
ResultChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BaseVersion(string v)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(v) || v == "?") return v;
|
||||||
|
var i = v.IndexOf('+');
|
||||||
|
return i >= 0 ? v[..i] : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsNewerVersion(string latest, string current)
|
||||||
|
{
|
||||||
|
var l = ParseVersionParts(latest);
|
||||||
|
var c = ParseVersionParts(current);
|
||||||
|
for (var i = 0; i < Math.Max(l.Length, c.Length); i++)
|
||||||
|
{
|
||||||
|
var a = i < l.Length ? l[i] : 0;
|
||||||
|
var b = i < c.Length ? c[i] : 0;
|
||||||
|
if (a > b) return true;
|
||||||
|
if (a < b) return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int[] ParseVersionParts(string v)
|
||||||
|
{
|
||||||
|
return v.Split('.', StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Select(s => int.TryParse(s.Trim(), out var n) ? n : 0)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace DnsmasqWebUI.Models.Config;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Options for the periodic update check (GitHub latest release). Bound from "UpdateCheck" config section.
|
||||||
|
/// </summary>
|
||||||
|
public class UpdateCheckOptions
|
||||||
|
{
|
||||||
|
/// <summary>Configuration section name.</summary>
|
||||||
|
public const string SectionName = "UpdateCheck";
|
||||||
|
|
||||||
|
/// <summary>Interval in minutes between checks. Default 1440 (24 hours). Set to 0 to disable periodic checks.</summary>
|
||||||
|
public int IntervalMinutes { get; set; } = 1440;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
namespace DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event args when a child requests to show or dismiss the config option help popover.
|
||||||
|
/// Parent (EffectiveConfigSection) shows ConfigOptionHelpModal at the anchor; when
|
||||||
|
/// <see cref="IsLabelMouseLeave"/> is true, parent schedules close if mouse does not enter the modal.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ConfigOptionHelpRequestEventArgs
|
||||||
|
{
|
||||||
|
/// <summary>Option-help file key (e.g. "server", "no-hosts").</summary>
|
||||||
|
public string HelpKey { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>Display label for the modal title (e.g. "server / local").</summary>
|
||||||
|
public string OptionLabel { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>Id of the label element that triggered the request; used to position the modal and match leave events.</summary>
|
||||||
|
public string AnchorId { get; init; } = "";
|
||||||
|
|
||||||
|
/// <summary>When true, the label lost mouse focus; parent should schedule close unless mouse enters the modal.</summary>
|
||||||
|
public bool IsLabelMouseLeave { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
namespace DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Where the effective config component is shown. Drives which sections and (optionally) which fields are visible.
|
||||||
|
/// </summary>
|
||||||
|
public enum EffectiveConfigContext
|
||||||
|
{
|
||||||
|
/// <summary>Full config: all sections, all fields (e.g. Dnsmasq page).</summary>
|
||||||
|
All,
|
||||||
|
|
||||||
|
/// <summary>Hosts page: only hosts-related section and fields.</summary>
|
||||||
|
Hosts,
|
||||||
|
|
||||||
|
/// <summary>DHCP page: DHCP and TFTP/PXE sections.</summary>
|
||||||
|
Dhcp,
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One section in a context view: section id, display title, and optional allow-list of option names.
|
||||||
|
/// When AllowedOptionNames is null, all fields in that section are shown; otherwise only listed options.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="SectionId">Matches EffectiveConfigSections.SectionXxx.</param>
|
||||||
|
/// <param name="Title">Display title (e.g. "Hosts", "Resolver / DNS").</param>
|
||||||
|
/// <param name="AllowedOptionNames">Null = all fields; otherwise only descriptors whose OptionName is in this list (case-insensitive).</param>
|
||||||
|
public record EffectiveConfigSectionView(
|
||||||
|
string SectionId,
|
||||||
|
string Title,
|
||||||
|
IReadOnlyList<string>? AllowedOptionNames
|
||||||
|
);
|
||||||
@@ -61,6 +61,9 @@ public record EffectiveConfigSources(
|
|||||||
IReadOnlyList<ValueWithSource> DhcpMacValues,
|
IReadOnlyList<ValueWithSource> DhcpMacValues,
|
||||||
IReadOnlyList<ValueWithSource> DhcpNameMatchValues,
|
IReadOnlyList<ValueWithSource> DhcpNameMatchValues,
|
||||||
IReadOnlyList<ValueWithSource> DhcpIgnoreNamesValues,
|
IReadOnlyList<ValueWithSource> DhcpIgnoreNamesValues,
|
||||||
|
IReadOnlyList<ValueWithSource> DhcpHostsfilePaths,
|
||||||
|
IReadOnlyList<ValueWithSource> DhcpOptsfilePaths,
|
||||||
|
IReadOnlyList<ValueWithSource> DhcpHostsdirPaths,
|
||||||
|
|
||||||
// --- 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,
|
||||||
@@ -96,6 +99,9 @@ public record EffectiveConfigSources(
|
|||||||
ConfigValueSource? Selfmx,
|
ConfigValueSource? Selfmx,
|
||||||
ConfigValueSource? EnableRa,
|
ConfigValueSource? EnableRa,
|
||||||
ConfigValueSource? LogDhcp,
|
ConfigValueSource? LogDhcp,
|
||||||
|
ConfigValueSource? KeepInForeground,
|
||||||
|
ConfigValueSource? NoDaemon,
|
||||||
|
ConfigValueSource? ProxyDnssec,
|
||||||
|
|
||||||
// --- 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,
|
||||||
@@ -127,5 +133,6 @@ public record EffectiveConfigSources(
|
|||||||
ConfigValueSource? EnableUbus,
|
ConfigValueSource? EnableUbus,
|
||||||
ConfigValueSource? FastDnsRetry,
|
ConfigValueSource? FastDnsRetry,
|
||||||
ConfigValueSource? DhcpScriptPath,
|
ConfigValueSource? DhcpScriptPath,
|
||||||
ConfigValueSource? MxTarget
|
ConfigValueSource? MxTarget,
|
||||||
|
ConfigValueSource? Conntrack
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ public record EffectiveDnsmasqConfig(
|
|||||||
IReadOnlyList<string> DhcpMacValues,
|
IReadOnlyList<string> DhcpMacValues,
|
||||||
IReadOnlyList<string> DhcpNameMatchValues,
|
IReadOnlyList<string> DhcpNameMatchValues,
|
||||||
IReadOnlyList<string> DhcpIgnoreNamesValues,
|
IReadOnlyList<string> DhcpIgnoreNamesValues,
|
||||||
|
IReadOnlyList<string> DhcpHostsfilePaths,
|
||||||
|
IReadOnlyList<string> DhcpOptsfilePaths,
|
||||||
|
IReadOnlyList<string> DhcpHostsdirPaths,
|
||||||
|
|
||||||
// --- Boolean flags (set if any file contains the option) ---
|
// --- Boolean flags (set if any file contains the option) ---
|
||||||
bool ExpandHosts,
|
bool ExpandHosts,
|
||||||
@@ -100,6 +103,9 @@ public record EffectiveDnsmasqConfig(
|
|||||||
bool Selfmx,
|
bool Selfmx,
|
||||||
bool EnableRa,
|
bool EnableRa,
|
||||||
bool LogDhcp,
|
bool LogDhcp,
|
||||||
|
bool KeepInForeground,
|
||||||
|
bool NoDaemon,
|
||||||
|
bool ProxyDnssec,
|
||||||
|
|
||||||
// --- 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,
|
||||||
@@ -131,7 +137,8 @@ public record EffectiveDnsmasqConfig(
|
|||||||
string? EnableUbus,
|
string? EnableUbus,
|
||||||
string? FastDnsRetry,
|
string? FastDnsRetry,
|
||||||
string? DhcpScriptPath,
|
string? DhcpScriptPath,
|
||||||
string? MxTarget
|
string? MxTarget,
|
||||||
|
string? Conntrack
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ builder.Services.AddOptions<AppLogsOptions>()
|
|||||||
.Bind(builder.Configuration.GetSection(AppLogsOptions.SectionName));
|
.Bind(builder.Configuration.GetSection(AppLogsOptions.SectionName));
|
||||||
builder.Services.AddOptions<RuntimeOverridesOptions>()
|
builder.Services.AddOptions<RuntimeOverridesOptions>()
|
||||||
.Bind(builder.Configuration.GetSection(RuntimeOverridesOptions.SectionName));
|
.Bind(builder.Configuration.GetSection(RuntimeOverridesOptions.SectionName));
|
||||||
|
builder.Services.AddOptions<UpdateCheckOptions>()
|
||||||
|
.Bind(builder.Configuration.GetSection(UpdateCheckOptions.SectionName));
|
||||||
|
|
||||||
// ---- Dnsmasq options (required paths validated at startup) ----
|
// ---- Dnsmasq options (required paths validated at startup) ----
|
||||||
builder.Services.AddOptions<DnsmasqOptions>()
|
builder.Services.AddOptions<DnsmasqOptions>()
|
||||||
@@ -36,6 +38,7 @@ builder.Services.AddSingleton<IValidateOptions<DnsmasqOptions>, DnsmasqOptionsVa
|
|||||||
// ---- Application services ----
|
// ---- Application services ----
|
||||||
builder.Services.AddApplicationServices();
|
builder.Services.AddApplicationServices();
|
||||||
builder.Services.AddScoped<ISettingsModalService, SettingsModalService>();
|
builder.Services.AddScoped<ISettingsModalService, SettingsModalService>();
|
||||||
|
builder.Services.AddSingleton<IUpdateCheckService, UpdateCheckService>();
|
||||||
builder.Services.AddHttpContextAccessor();
|
builder.Services.AddHttpContextAccessor();
|
||||||
builder.Services.AddDnsmasqApiHttpClients();
|
builder.Services.AddDnsmasqApiHttpClients();
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
"Application": {
|
"Application": {
|
||||||
"ApplicationTitle": "Local DNS"
|
"ApplicationTitle": "Local DNS"
|
||||||
},
|
},
|
||||||
|
"UpdateCheck": {
|
||||||
|
"IntervalMinutes": 1440
|
||||||
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Positioning and scroll-close for config option help popover.
|
||||||
|
* Called from ConfigOptionHelpModal.razor via IJSRuntime.InvokeAsync.
|
||||||
|
*/
|
||||||
|
window.getConfigOptionHelpAnchorRect = function (anchorId) {
|
||||||
|
var el = document.getElementById(anchorId);
|
||||||
|
if (!el) return null;
|
||||||
|
var r = el.getBoundingClientRect();
|
||||||
|
return { top: r.top, left: r.left, bottom: r.bottom, width: r.width, height: r.height };
|
||||||
|
};
|
||||||
|
|
||||||
|
var _configOptionHelpScrollHandler = null;
|
||||||
|
|
||||||
|
window.addConfigOptionHelpScrollCloseListener = function (dotNetRef) {
|
||||||
|
if (_configOptionHelpScrollHandler) return;
|
||||||
|
_configOptionHelpScrollHandler = function () {
|
||||||
|
dotNetRef.invokeMethodAsync('OnScrollClose').catch(function () {});
|
||||||
|
};
|
||||||
|
window.addEventListener('scroll', _configOptionHelpScrollHandler, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.removeConfigOptionHelpScrollCloseListener = function () {
|
||||||
|
if (!_configOptionHelpScrollHandler) return;
|
||||||
|
window.removeEventListener('scroll', _configOptionHelpScrollHandler, true);
|
||||||
|
_configOptionHelpScrollHandler = null;
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<dt><b>-H, --addn-hosts=<file></b></dt>
|
||||||
|
<dd>Additional hosts file. Read the specified file as well as /etc/hosts. If <b>--no-hosts</b> is given, read
|
||||||
|
only the specified file. This option may be repeated for more than one
|
||||||
|
additional hosts file. If a directory is given, then read all the files contained in that directory
|
||||||
|
in alphabetical order.</dd>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<dt><b>-A, --address=/<domain>[/<domain>...]/[<ipaddr>]</b></dt>
|
||||||
|
<dd>Specify an IP address to return for any host in the given domains.
|
||||||
|
A (or AAAA) queries in the domains are never forwarded and always replied to
|
||||||
|
with the specified IP address which may be IPv4 or IPv6. To give
|
||||||
|
multiple addresses or both IPv4 and IPv6 addresses for a domain, use repeated <b>--address</b> flags.
|
||||||
|
Note that /etc/hosts and DHCP leases override this for individual
|
||||||
|
names. A common use of this is to redirect the entire doubleclick.net
|
||||||
|
domain to some friendly local web server to avoid banner ads. The
|
||||||
|
domain specification works in the same way as for <b>--server</b>, with
|
||||||
|
the additional facility that <b>/#/</b> matches any domain. Thus
|
||||||
|
<b>--address=/#/1.2.3.4</b> will always return <b>1.2.3.4</b> for any
|
||||||
|
query not answered from <b>/etc/hosts</b> or DHCP and not sent to an
|
||||||
|
upstream nameserver by a more specific <b>--server</b> directive. As for
|
||||||
|
<b>--server</b>, one or more domains with no address returns a
|
||||||
|
no-such-domain answer, so <b>--address=/example.com/</b> is equivalent to
|
||||||
|
<b>--server=/example.com/</b> and returns NXDOMAIN for example.com and
|
||||||
|
all its subdomains. An address specified as '#' translates to the NULL
|
||||||
|
address of 0.0.0.0 and its IPv6 equivalent of :: so
|
||||||
|
<b>--address=/example.com/#</b> will return NULL addresses for example.com and
|
||||||
|
its subdomains. This is partly syntactic sugar for <b>--address=/example.com/0.0.0.0</b>
|
||||||
|
and <b>--address=/example.com/::</b> but is also more efficient than including both
|
||||||
|
as separate configuration lines. Note that NULL addresses normally work in the same way as localhost, so beware that clients looking up these names are likely to end up talking to themselves.
|
||||||
|
<p>
|
||||||
|
Note that the behaviour for queries which don't match the specified address literal changed in version 2.86.
|
||||||
|
Previous versions, configured with (eg) --address=/example.com/1.2.3.4 and then queried for a RR type other than
|
||||||
|
A would return a NoData answer. From 2.86, the query is sent upstream. To restore the pre-2.86 behaviour,
|
||||||
|
use the configuration --address=/example.com/1.2.3.4 --local=/example.com/</dd>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<dt><b>-V, --alias=[<old-ip>]|[<start-ip>-<end-ip>],<new-ip>[,<mask>]</b></dt>
|
||||||
|
<dd>Modify IPv4 addresses returned from upstream nameservers; old-ip is
|
||||||
|
replaced by new-ip. If the optional mask is given then any address
|
||||||
|
which matches the masked old-ip will be re-written. So, for instance
|
||||||
|
<b>--alias=1.2.3.0,6.7.8.0,255.255.255.0 </b>
|
||||||
|
|
||||||
|
will map 1.2.3.56 to 6.7.8.56 and 1.2.3.67 to 6.7.8.67. This is what
|
||||||
|
Cisco PIX routers call "DNS doctoring". If the old IP is given as
|
||||||
|
range, then only addresses in the range, rather than a whole subnet,
|
||||||
|
are re-written. So
|
||||||
|
<b>--alias=192.168.0.10-192.168.0.40,10.0.0.0,255.255.255.0</b>
|
||||||
|
|
||||||
|
maps 192.168.0.10->192.168.0.40 to 10.0.0.10->10.0.0.40</dd>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<dt><b>--all-servers</b></dt>
|
||||||
|
<dd>By default, when dnsmasq has more than one upstream server available,
|
||||||
|
it will send queries to just one server. Setting this flag forces
|
||||||
|
dnsmasq to send all queries to all available servers. The reply from
|
||||||
|
the server which answers first will be returned to the original requester.</dd>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<dt><b>--auth-server=<domain>,[<interface>|<ip-address>...]</b></dt>
|
||||||
|
<dd>Enable DNS authoritative mode for queries arriving at an interface or address. Note that the interface or address
|
||||||
|
need not be mentioned in
|
||||||
|
<b>--interface</b>
|
||||||
|
|
||||||
|
or
|
||||||
|
<b>--listen-address</b>
|
||||||
|
|
||||||
|
configuration, indeed
|
||||||
|
<b>--auth-server</b>
|
||||||
|
|
||||||
|
will override these and provide a different DNS service on the
|
||||||
|
specified interface. The <domain> is the "glue record". It should
|
||||||
|
resolve in the global DNS to an A and/or AAAA record which points to
|
||||||
|
the address dnsmasq is listening on. When an interface is specified,
|
||||||
|
it may be qualified with "/4" or "/6" to specify only the IPv4 or IPv6
|
||||||
|
addresses associated with the interface. Since any defined authoritative zones are also available as part of the normal recusive DNS service supplied by dnsmasq, it can make sense to have an --auth-server declaration with no interfaces or address, but simply specifying the primary external nameserver.</dd>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<dt><b>--auth-ttl=<time></b></dt>
|
||||||
|
<dd>Set the TTL value returned in answers from the authoritative server.</dd>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<dt><b>--bind-dynamic</b></dt>
|
||||||
|
<dd>Enable a network mode which is a hybrid between
|
||||||
|
<b>--bind-interfaces</b>
|
||||||
|
|
||||||
|
and the default. Dnsmasq binds the address of individual interfaces,
|
||||||
|
allowing multiple dnsmasq instances, but if new interfaces or
|
||||||
|
addresses appear, it automatically listens on those (subject to any
|
||||||
|
access-control configuration). This makes dynamically created
|
||||||
|
interfaces work in the same way as the default. Implementing this
|
||||||
|
option requires non-standard networking APIs and it is only available
|
||||||
|
under Linux. On other platforms it falls-back to <b>--bind-interfaces</b> mode.</dd>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<dt><b>-z, --bind-interfaces</b></dt>
|
||||||
|
<dd>On systems which support it, dnsmasq binds the wildcard address,
|
||||||
|
even when it is listening on only some interfaces. It then discards
|
||||||
|
requests that it shouldn't reply to. This has the advantage of
|
||||||
|
working even when interfaces come and go and change address. This
|
||||||
|
option forces dnsmasq to really bind only the interfaces it is
|
||||||
|
listening on. About the only time when this is useful is when
|
||||||
|
running another nameserver (or another instance of dnsmasq) on the
|
||||||
|
same machine. Setting this option also enables multiple instances of
|
||||||
|
dnsmasq which provide DHCP service to run in the same machine.</dd>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<dt><b>-B, --bogus-nxdomain=<ipaddr>[/prefix]</b></dt>
|
||||||
|
<dd>Transform replies which contain the specified address or subnet into "No such
|
||||||
|
domain" replies. IPv4 and IPv6 are supported. This is intended to counteract a devious move made by
|
||||||
|
Verisign in September 2003 when they started returning the address of
|
||||||
|
an advertising web page in response to queries for unregistered names,
|
||||||
|
instead of the correct NXDOMAIN response. This option tells dnsmasq to
|
||||||
|
fake the correct response when it sees this behaviour. As at Sept 2003
|
||||||
|
the IP address being returned by Verisign is 64.94.110.11</dd>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<dt><b>-b, --bogus-priv</b></dt>
|
||||||
|
<dd>Bogus private reverse lookups. All reverse lookups for private IP ranges (ie 192.168.x.x, etc)
|
||||||
|
which are not found in /etc/hosts or the DHCP leases file are answered
|
||||||
|
with "no such domain" rather than being forwarded upstream. The
|
||||||
|
set of prefixes affected is the list given in RFC6303, for IPv4 and IPv6.
|
||||||
|
Enabling this also subtly alters DNSSEC validation for reverse lookups in the
|
||||||
|
private ranges such that a non-secure DS record is accepted as proof that
|
||||||
|
the range is not signed. This works around behaviour by the public DNS services
|
||||||
|
which seem not to return validated proof-of-non-existence for DS records in these domains.</dd>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<dt><b>--cache-rr=<rrtype>[,<rrtype>...]</b></dt>
|
||||||
|
<dd>By default, dnsmasq caches A, AAAA, CNAME and SRV DNS record types.
|
||||||
|
This option adds other record types to the cache. The RR-type can be given
|
||||||
|
as a name such as TXT or MX or a decimal number. A single --cache-rr option
|
||||||
|
can take a comma-separated list of RR-types and more than one --cache-rr option
|
||||||
|
is allowed. Use --cache-rr=ANY to enable caching for all RR-types.</dd>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<dt><b>-c, --cache-size=<cachesize></b></dt>
|
||||||
|
<dd>Set the size of dnsmasq's cache. The default is 150 names. Setting the cache size to zero disables caching. Note: huge cache size impacts performance.</dd>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<dt><b>--clear-on-reload</b></dt>
|
||||||
|
<dd>Whenever /etc/resolv.conf is re-read or the upstream servers are set
|
||||||
|
via DBus, clear the DNS cache.
|
||||||
|
This is useful when new nameservers may have different
|
||||||
|
data than that held in cache.</dd>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<dt><b>--cname=<cname>,[<cname>,]<target>[,<TTL>]</b></dt>
|
||||||
|
<dd>Return a CNAME record which indicates that <cname> is really
|
||||||
|
<target>. There is a significant limitation on the target; it must be a
|
||||||
|
DNS record which is known to dnsmasq and NOT a DNS record which comes from
|
||||||
|
an upstream server. The cname must be unique, but it
|
||||||
|
is permissible to have more than one cname pointing to the same target. Indeed
|
||||||
|
it's possible to declare multiple cnames to a target in a single line, like so:
|
||||||
|
<b>--cname=cname1,cname2,target</b>
|
||||||
|
<p>
|
||||||
|
If the time-to-live is given, it overrides the default, which is zero
|
||||||
|
or the value of <b>--local-ttl</b>. The value is a positive integer and gives
|
||||||
|
the time-to-live in seconds.</dd>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<dt><b>--conntrack</b></dt>
|
||||||
|
<dd>Read the Linux connection track mark associated with incoming DNS
|
||||||
|
queries and set the same mark value on upstream traffic used to answer
|
||||||
|
those queries. This allows traffic generated by dnsmasq to be
|
||||||
|
associated with the queries which cause it, useful for bandwidth
|
||||||
|
accounting and firewalling. Dnsmasq must have conntrack support
|
||||||
|
compiled in and the kernel must have conntrack support
|
||||||
|
included and configured. This option cannot be combined with
|
||||||
|
<b>--query-port.</b></dd>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<dt><b>-K, --dhcp-authoritative</b></dt>
|
||||||
|
<dd>Should be set when dnsmasq is definitely the only DHCP server on a network.
|
||||||
|
For DHCPv4, it changes the behaviour from strict RFC compliance so that DHCP requests on
|
||||||
|
unknown leases from unknown hosts are not ignored. This allows new hosts
|
||||||
|
to get a lease without a tedious timeout under all circumstances. It also
|
||||||
|
allows dnsmasq to rebuild its lease database without each client needing to
|
||||||
|
reacquire a lease, if the database is lost. For DHCPv6 it controls the same
|
||||||
|
behaviour as DHCPv4 with missing leases (except for the RFC uncompliance - the
|
||||||
|
DHCPv6 RFCs allow this behaviour if configured). It also sets the
|
||||||
|
priority in replies to 255 (the maximum) instead of 0 (the minimum).</dd>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<dt><b>-M, --dhcp-boot=[tag:<tag>,]<filename>,[<servername>[,<server address>|<tftp_servername>]]</b></dt>
|
||||||
|
<dd>(IPv4 only) Set BOOTP options to be returned by the DHCP server. Server name and
|
||||||
|
address are optional: if not provided, the name is left empty, and the
|
||||||
|
address set to the address of the machine running dnsmasq. If dnsmasq
|
||||||
|
is providing a TFTP service (see
|
||||||
|
<b>--enable-tftp</b>
|
||||||
|
|
||||||
|
) then only the filename is required here to enable network booting.
|
||||||
|
If the optional tag(s) are given,
|
||||||
|
they must match for this configuration to be sent.
|
||||||
|
Instead of an IP address, the TFTP server address can be given as a domain
|
||||||
|
name which is looked up in /etc/hosts. This name can be associated in
|
||||||
|
/etc/hosts with multiple IP addresses, which are used round-robin.
|
||||||
|
This facility can be used to load balance the tftp load among a set of servers.</dd>
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
<dt><b>-G, --dhcp-host=[<hwaddr>][,id:<client_id>|*][,set:<tag>][,tag:<tag>][,<ipaddr>][,<hostname>][,<lease_time>][,ignore]</b></dt>
|
||||||
|
<dd>Specify per host parameters for the DHCP server. This allows a machine
|
||||||
|
with a particular hardware address to be always allocated the same
|
||||||
|
hostname, IP address and lease time. A hostname specified like this
|
||||||
|
overrides any supplied by the DHCP client on the machine. It is also
|
||||||
|
allowable to omit the hardware address and include the hostname, in
|
||||||
|
which case the IP address and lease times will apply to any machine
|
||||||
|
claiming that name. For example
|
||||||
|
<b>--dhcp-host=00:20:e0:3b:13:af,wap,infinite </b>
|
||||||
|
|
||||||
|
tells dnsmasq to give
|
||||||
|
the machine with hardware address 00:20:e0:3b:13:af the name wap, and
|
||||||
|
an infinite DHCP lease.
|
||||||
|
<b>--dhcp-host=lap,192.168.0.199 </b>
|
||||||
|
|
||||||
|
tells
|
||||||
|
dnsmasq to always allocate the machine lap the IP address
|
||||||
|
192.168.0.199.
|
||||||
|
<p>
|
||||||
|
Addresses allocated like this are not constrained to be
|
||||||
|
in the range given by the <b>--dhcp-range</b> option, but they must be in
|
||||||
|
the same subnet as some valid dhcp-range. For
|
||||||
|
subnets which don't need a pool of dynamically allocated addresses,
|
||||||
|
use the "static" keyword in the <b>--dhcp-range</b> declaration.
|
||||||
|
<p>
|
||||||
|
It is allowed to use client identifiers (called client
|
||||||
|
DUID in IPv6-land) rather than
|
||||||
|
hardware addresses to identify hosts by prefixing with 'id:'. Thus:
|
||||||
|
<b>--dhcp-host=id:01:02:03:04,..... </b>
|
||||||
|
|
||||||
|
refers to the host with client identifier 01:02:03:04. It is also
|
||||||
|
allowed to specify the client ID as text, like this:
|
||||||
|
<b>--dhcp-host=id:clientidastext,..... </b>
|
||||||
|
<p>
|
||||||
|
A single
|
||||||
|
<b>--dhcp-host</b>
|
||||||
|
|
||||||
|
may contain an IPv4 address or one or more IPv6 addresses, or both. IPv6 addresses must be bracketed by square brackets thus:
|
||||||
|
<b>--dhcp-host=laptop,[1234::56]</b>
|
||||||
|
|
||||||
|
IPv6 addresses may contain only the host-identifier part:
|
||||||
|
<b>--dhcp-host=laptop,[::56]</b>
|
||||||
|
|
||||||
|
in which case they act as wildcards in constructed DHCP ranges, with
|
||||||
|
the appropriate network part inserted. For IPv6, an address may include a prefix length:
|
||||||
|
<b>--dhcp-host=laptop,[1234:50/126]</b>
|
||||||
|
|
||||||
|
which (in this case) specifies four addresses, 1234::50 to 1234::53. This (an the ability
|
||||||
|
to specify multiple addresses) is useful
|
||||||
|
when a host presents either a consistent name or hardware-ID, but varying DUIDs, since it allows
|
||||||
|
dnsmasq to honour the static address allocation but assign a different address for each DUID. This
|
||||||
|
typically occurs when chain netbooting, as each stage of the chain gets in turn allocates an address.
|
||||||
|
<p>
|
||||||
|
Note that in IPv6 DHCP, the hardware address may not be
|
||||||
|
available, though it normally is for direct-connected clients, or
|
||||||
|
clients using DHCP relays which support RFC 6939.
|
||||||
|
<p>
|
||||||
|
<p>
|
||||||
|
For DHCPv4, the special option <b>id:*</b> means "ignore any client-id
|
||||||
|
and use MAC addresses only." This is useful when a client presents a client-id sometimes
|
||||||
|
but not others.
|
||||||
|
<p>
|
||||||
|
If a name appears in /etc/hosts, the associated address can be
|
||||||
|
allocated to a DHCP lease, but only if a
|
||||||
|
<b>--dhcp-host</b>
|
||||||
|
|
||||||
|
option specifying the name also exists. Only one hostname can be
|
||||||
|
given in a
|
||||||
|
<b>--dhcp-host</b>
|
||||||
|
|
||||||
|
option, but aliases are possible by using CNAMEs. (See
|
||||||
|
<b>--cname</b>
|
||||||
|
|
||||||
|
). Note that /etc/hosts is NOT used when the DNS server side of dnsmasq
|
||||||
|
is disabled by setting the DNS server port to zero.
|
||||||
|
<p>
|
||||||
|
More than one
|
||||||
|
<b>--dhcp-host</b>
|
||||||
|
|
||||||
|
can be associated (by name, hardware address or UID) with a host. Which one is used
|
||||||
|
(and therefore which address is allocated by DHCP and appears in the DNS) depends
|
||||||
|
on the subnet on which the host last obtained a DHCP lease:
|
||||||
|
the
|
||||||
|
<b>--dhcp-host</b>
|
||||||
|
|
||||||
|
with an address within the subnet is used. If more than one address is within the subnet,
|
||||||
|
the result is undefined. A corollary to this is that the name associated with a host using
|
||||||
|
<b>--dhcp-host</b>
|
||||||
|
|
||||||
|
does not appear in the DNS until the host obtains a DHCP lease.
|
||||||
|
<p>
|
||||||
|
<p>
|
||||||
|
The special keyword <b>ignore</b> tells dnsmasq never to offer a DHCP lease
|
||||||
|
to a machine. The machine
|
||||||
|
can be specified by hardware address, client ID or hostname. For
|
||||||
|
example: <b>--dhcp-host=00:20:e0:3b:13:af,ignore</b>.
|
||||||
|
This can be useful when there is another DHCP server on the network which should
|
||||||
|
be used by some machines.
|
||||||
|
<p>
|
||||||
|
The <b>set:<tag></b> construct sets the tag
|
||||||
|
whenever this <b>--dhcp-host</b> directive is in use. This can be used to
|
||||||
|
selectively send DHCP options just for this host. More than one tag
|
||||||
|
can be set in a <b>--dhcp-host</b> directive (but not in other places where
|
||||||
|
<b>set:<tag></b> is allowed). When a host matches any
|
||||||
|
<b>--dhcp-host</b> directive (or one implied by /etc/ethers) then the special
|
||||||
|
tag <b>known</b> is set. This allows dnsmasq to be configured to
|
||||||
|
ignore requests from unknown machines using
|
||||||
|
<b>--dhcp-ignore=tag:!known</b>.
|
||||||
|
If the host matches only a <b>--dhcp-host</b> directive which cannot
|
||||||
|
be used because it specifies an address on a different subnet, the tag <b>known-othernet</b> is set.
|
||||||
|
<p>
|
||||||
|
The <b>tag:<tag></b> construct filters which dhcp-host directives are used. More than
|
||||||
|
one tag can be provided. In this case, the request must match all tags. Tagged
|
||||||
|
directives are used in preference to untagged ones. Note that one of <hwaddr>,
|
||||||
|
<client_id> or <hostname> still must be specified (can be a wildcard).
|
||||||
|
<p>
|
||||||
|
Ethernet addresses (but not client-ids) may have
|
||||||
|
wildcard bytes, so for example
|
||||||
|
<b>--dhcp-host=00:20:e0:3b:13:*,ignore </b>
|
||||||
|
|
||||||
|
will cause dnsmasq to ignore the given range of hardware addresses. Note that
|
||||||
|
the "*" will need to be escaped or quoted on a command line, but not
|
||||||
|
in the configuration file.
|
||||||
|
<p>
|
||||||
|
Hardware addresses normally match any
|
||||||
|
network (ARP) type, but it is possible to restrict them to a single
|
||||||
|
ARP type by preceding them with the ARP-type (in HEX) and "-". so
|
||||||
|
<b>--dhcp-host=06-00:20:e0:3b:13:af,1.2.3.4 </b>
|
||||||
|
|
||||||
|
will only match a
|
||||||
|
Token-Ring hardware address, since the ARP-address type for token ring
|
||||||
|
is 6.
|
||||||
|
<p>
|
||||||
|
As a special case, in DHCPv4, it is possible to include more than one
|
||||||
|
hardware address. eg:
|
||||||
|
<b>--dhcp-host=11:22:33:44:55:66,12:34:56:78:90:12,192.168.0.2</b>.
|
||||||
|
This allows an IP address to be associated with
|
||||||
|
multiple hardware addresses, and gives dnsmasq permission to abandon a
|
||||||
|
DHCP lease to one of the hardware addresses when another one asks for
|
||||||
|
a lease. Beware that this is a dangerous thing to do: it will only
|
||||||
|
work reliably if only one of the hardware addresses is active at any
|
||||||
|
time and there is no way for dnsmasq to enforce this. It is, for instance,
|
||||||
|
useful to allocate a stable IP address to a laptop which
|
||||||
|
has both wired and wireless interfaces.</dd>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<dt><b>--dhcp-hostsdir=<path></b></dt>
|
||||||
|
<dd>This is equivalent to <b>--dhcp-hostsfile</b>, except for the following. The path MUST be a
|
||||||
|
directory, and not an individual file. Changed or new files within
|
||||||
|
the directory are read automatically, without the need to send SIGHUP.
|
||||||
|
If a file is deleted or changed after it has been read by dnsmasq, then the
|
||||||
|
host record it contained will remain until dnsmasq receives a SIGHUP, or
|
||||||
|
is restarted; ie host records are only added dynamically. The order in which the
|
||||||
|
files in a directory are read is not defined.</dd>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<dt><b>--dhcp-hostsfile=<path></b></dt>
|
||||||
|
<dd>Read DHCP host information from the specified file. If a directory
|
||||||
|
is given, then read all the files contained in that directory in alphabetical order. The file contains
|
||||||
|
information about one host per line. The format of a line is the same
|
||||||
|
as text to the right of '=' in <b>--dhcp-host</b>. The advantage of storing DHCP host information
|
||||||
|
in this file is that it can be changed without re-starting dnsmasq:
|
||||||
|
the file will be re-read when dnsmasq receives SIGHUP.</dd>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<dt><b>--dhcp-ignore-names[=tag:<tag>[,tag:<tag>]]</b></dt>
|
||||||
|
<dd>When all the given tags appear in the tag set, ignore any hostname
|
||||||
|
provided by the host. Note that, unlike <b>--dhcp-ignore</b>, it is permissible
|
||||||
|
to supply no tags, in which case DHCP-client supplied hostnames
|
||||||
|
are always ignored, and DHCP hosts are added to the DNS using only
|
||||||
|
<b>--dhcp-host</b> configuration in dnsmasq and the contents of /etc/hosts and
|
||||||
|
/etc/ethers.</dd>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<dt><b>-J, --dhcp-ignore=tag:<tag>[,tag:<tag>]</b></dt>
|
||||||
|
<dd>When all the given tags appear in the tag set, ignore the host and do
|
||||||
|
not allocate it a DHCP lease.</dd>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<dt><b>-X, --dhcp-lease-max=<number></b></dt>
|
||||||
|
<dd>Limits dnsmasq to the specified maximum number of DHCP leases. The
|
||||||
|
default is 1000. This limit is to prevent DoS attacks from hosts which
|
||||||
|
create thousands of leases and use lots of memory in the dnsmasq
|
||||||
|
process.</dd>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<dt><b>-l, --dhcp-leasefile=<path></b></dt>
|
||||||
|
<dd>Use the specified file to store DHCP lease information.</dd>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<dt><b>-4, --dhcp-mac=set:<tag>,<MAC address></b></dt>
|
||||||
|
<dd>Map from a MAC address to a tag. The MAC address may include
|
||||||
|
wildcards. For example
|
||||||
|
<b>--dhcp-mac=set:3com,01:34:23:*:*:*</b>
|
||||||
|
|
||||||
|
will set the tag "3com" for any host whose MAC address matches the pattern.</dd>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<dt><b>--dhcp-match=set:<tag>,<option number>|option:<option name>|vi-encap:<enterprise>[,<value>]</b></dt>
|
||||||
|
<dd>Without <b><value></b>, set <b><tag></b> if the client sends a DHCP
|
||||||
|
<b>option</b> of the given number or name. With <b><value></b>, set <b><tag></b> only if
|
||||||
|
the client sends the <b>option</b> matching or containing <b><value></b>.
|
||||||
|
<p>
|
||||||
|
The value may be of the form
|
||||||
|
"01:ff:*:02" in which case the value must match (apart from wildcards)
|
||||||
|
but the option sent may have unmatched data past the end of the
|
||||||
|
value. The value may also be of the same form as in
|
||||||
|
<b>--dhcp-option</b>
|
||||||
|
|
||||||
|
in which case the option sent is treated as an array, and one element
|
||||||
|
must match.
|
||||||
|
<b>--dhcp-match=set:efi-ia32,option:client-arch,6</b>
|
||||||
|
|
||||||
|
sets the tag <b>efi-ia32</b> if the number <b>6</b> appears in the list of
|
||||||
|
architectures sent by the client in option 93. (See RFC 4578 for
|
||||||
|
details.) If the value is a string, substring matching is used.
|
||||||
|
<p>
|
||||||
|
The special form with vi-encap:<enterprise number> matches against
|
||||||
|
vendor-identifying vendor classes for the specified enterprise. Please
|
||||||
|
see RFC 3925 for more details of these rare and interesting beasts.</dd>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<dt><b>--dhcp-name-match=set:<tag>,<name>[*]</b></dt>
|
||||||
|
<dd>Set a <b><tag></b> if the given <b><name></b> is supplied by a DHCP client. There may be a single trailing wildcard *, with a glob meaning. Combined with <b>dhcp-ignore</b> or <b>dhcp-ignore-names</b> this gives the ability to ignore certain clients by name, or disallow certain hostnames from being claimed by a client.</dd>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<dt><b>--dhcp-option-force=[tag:<tag>,[tag:<tag>,]][encap:<opt>,][vi-encap:<enterprise>,][vendor:[<vendor-class>],]<opt>,[<value>[,<value>]]</b></dt>
|
||||||
|
<dd>This works in exactly the same way as
|
||||||
|
<b>--dhcp-option</b>
|
||||||
|
|
||||||
|
except that the option will always be sent, even if the client does
|
||||||
|
not ask for it in the parameter request list. This is sometimes
|
||||||
|
needed, for example when sending options to PXELinux.</dd>
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<dt><b>-O, --dhcp-option=[tag:<tag>,[tag:<tag>,]][encap:<opt>,][vi-encap:<enterprise>,][vendor:[<vendor-class>],][<opt>|option:<opt-name>|option6:<opt>|option6:<opt-name>],[<value>[,<value>]]</b></dt>
|
||||||
|
<dd>Send various extra options to DHCP clients. By default,
|
||||||
|
dnsmasq sends some standard options to DHCP clients, the netmask and
|
||||||
|
broadcast address are set to the same as the host running dnsmasq, and
|
||||||
|
the DNS server and default route are set to the address of the machine
|
||||||
|
running dnsmasq. (Equivalent rules apply for IPv6.) If the domain name option has been set, that is sent.
|
||||||
|
This configuration allows these defaults to be overridden,
|
||||||
|
or other options specified. The option to be sent may be given as a
|
||||||
|
decimal number or as <b>option:<option-name></b>.
|
||||||
|
<p>
|
||||||
|
Option numbers are
|
||||||
|
specified in RFC2132 and subsequent RFCs. The set of option-names
|
||||||
|
known by dnsmasq can be discovered by running <b>dnsmasq --help dhcp</b>.
|
||||||
|
For example, to set the default route option to 192.168.4.4, use
|
||||||
|
<b>--dhcp-option=3,192.168.4.4</b> or
|
||||||
|
<b>--dhcp-option=option:router,192.168.4.4</b>
|
||||||
|
and to set the time-server address to 192.168.0.4, use
|
||||||
|
<b>--dhcp-option=42,192.168.0.4</b> or
|
||||||
|
<b>--dhcp-option=option:ntp-server,192.168.0.4</b>.
|
||||||
|
The special address 0.0.0.0 means "the address of the system running dnsmasq".
|
||||||
|
<p>
|
||||||
|
An option without data is valid, and includes just the option without data.
|
||||||
|
(There is only one option with a zero length data field currently defined for DHCPv4, 80:rapid commit, so this feature is not very useful in practice). Options for which dnsmasq normally
|
||||||
|
provides default values can be omitted by defining the option with no data. These are
|
||||||
|
netmask, broadcast, router, DNS server, domainname and hostname. Thus, for DHCPv4
|
||||||
|
<b>--dhcp-option = option:router</b>
|
||||||
|
|
||||||
|
will result in no router option being sent, rather than the default of the host on which dnsmasq is running. For DHCPv6, the same is true of the options DNS server and refresh time.
|
||||||
|
<p>
|
||||||
|
<p>
|
||||||
|
Data types allowed are comma separated
|
||||||
|
dotted-quad IPv4 addresses, []-wrapped IPv6 addresses, a decimal number, colon-separated hex digits
|
||||||
|
and a text string. If the optional tags are given then
|
||||||
|
this option is only sent when all the tags are matched.
|
||||||
|
<p>
|
||||||
|
Special processing is done on a text argument for option 119, to
|
||||||
|
conform with RFC 3397. Text or dotted-quad IP addresses as arguments
|
||||||
|
to option 120 are handled as per RFC 3361. Dotted-quad IP addresses
|
||||||
|
which are followed by a slash and then a netmask size are encoded as
|
||||||
|
described in RFC 3442.
|
||||||
|
<p>
|
||||||
|
IPv6 options are specified using the <b>option6:</b>
|
||||||
|
keyword, followed by the option number or option name. The IPv6 option
|
||||||
|
name space is disjoint from the IPv4 option name space. IPv6 addresses
|
||||||
|
in options must be bracketed with square brackets, eg.
|
||||||
|
<b> --dhcp-option=option6:ntp-server,[1234::56]</b>.
|
||||||
|
For IPv6, [::] means "the global address of
|
||||||
|
the machine running dnsmasq", whilst [fd00::] is replaced with the
|
||||||
|
ULA, if it exists, and [fe80::] with the link-local address.
|
||||||
|
<p>
|
||||||
|
Be careful: data-type suitability for the option number sent is not checked.
|
||||||
|
It is quite possible to persuade dnsmasq to generate illegal DHCP packets with
|
||||||
|
injudicious use of this flag.
|
||||||
|
When the value is a decimal number, dnsmasq must determine how
|
||||||
|
large the data item is. It does this by examining the option number and/or the
|
||||||
|
value, but can be overridden by appending a single letter flag as follows:
|
||||||
|
b = one byte, s = two bytes, i = four bytes. This is mainly useful with
|
||||||
|
encapsulated vendor class options (see below) where dnsmasq cannot
|
||||||
|
determine data size from the option number. Option data which
|
||||||
|
consists solely of periods and digits will be interpreted by dnsmasq
|
||||||
|
as an IP address, and inserted into an option as such. To force a
|
||||||
|
literal string, use quotes. For instance when using option 66 to send
|
||||||
|
a literal IP address as TFTP server name, it is necessary to do
|
||||||
|
<b>--dhcp-option=66,"1.2.3.4"</b>.
|
||||||
|
<p>
|
||||||
|
Encapsulated Vendor-class options may also be specified (IPv4 only) using
|
||||||
|
<b>--dhcp-option</b>: for instance
|
||||||
|
<b>--dhcp-option=vendor:PXEClient,1,0.0.0.0 </b>
|
||||||
|
|
||||||
|
sends the encapsulated vendor
|
||||||
|
class-specific option "mftp-address=0.0.0.0" to any client whose
|
||||||
|
vendor-class matches "PXEClient". The vendor-class matching is
|
||||||
|
substring based (see <b>--dhcp-vendorclass</b> for details). If a
|
||||||
|
vendor-class option (number 60) is sent by dnsmasq, then that is used
|
||||||
|
for selecting encapsulated options in preference to any sent by the
|
||||||
|
client. It is
|
||||||
|
possible to omit the vendorclass completely;
|
||||||
|
<b>--dhcp-option=vendor:,1,0.0.0.0</b>
|
||||||
|
|
||||||
|
in which case the encapsulated option is always sent.
|
||||||
|
<p>
|
||||||
|
Options may be encapsulated (IPv4 only) within other options: for instance
|
||||||
|
<b>--dhcp-option=encap:175, 190, iscsi-client0</b>
|
||||||
|
|
||||||
|
will send option 175, within which is the option 190. If multiple
|
||||||
|
options are given which are encapsulated with the same option number
|
||||||
|
then they will be correctly combined into one encapsulated option.
|
||||||
|
encap: and vendor: may not both be set in the same <b>--dhcp-option</b>.
|
||||||
|
<p>
|
||||||
|
The final variant on encapsulated options is "Vendor-Identifying
|
||||||
|
Vendor Options" as specified by RFC3925. These are denoted like this:
|
||||||
|
<b>--dhcp-option=vi-encap:2, 10, text</b>
|
||||||
|
|
||||||
|
The number in the vi-encap: section is the IANA enterprise number
|
||||||
|
used to identify this option. This form of encapsulation is supported
|
||||||
|
in IPv6.
|
||||||
|
<p>
|
||||||
|
The address 0.0.0.0 is not treated specially in
|
||||||
|
encapsulated options.</dd>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<dt><b>--dhcp-optsfile=<path></b></dt>
|
||||||
|
<dd>Read DHCP option information from the specified file. If a directory
|
||||||
|
is given, then read all the files contained in that directory in alphabetical order. The advantage of
|
||||||
|
using this option is the same as for <b>--dhcp-hostsfile</b>: the
|
||||||
|
<b>--dhcp-optsfile</b> will be re-read when dnsmasq receives SIGHUP. Note that
|
||||||
|
it is possible to encode the information in a
|
||||||
|
<b>--dhcp-boot</b>
|
||||||
|
|
||||||
|
flag as DHCP options, using the options names bootfile-name,
|
||||||
|
server-ip-address and tftp-server. This allows these to be included
|
||||||
|
in a <b>--dhcp-optsfile</b>.</dd>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<dt><b>-F, --dhcp-range=[tag:<tag>[,tag:<tag>],][set:<tag>,]<start-addr>[,<end-addr>|<mode>[,<netmask>[,<broadcast>]]][,<lease time>]</b></dt>
|
||||||
|
<dd></dd>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<dt><b>--dhcp-rapid-commit</b></dt>
|
||||||
|
<dd>Enable DHCPv4 Rapid Commit Option specified in RFC 4039. When enabled, dnsmasq
|
||||||
|
will respond to a DHCPDISCOVER message including a Rapid Commit
|
||||||
|
option with a DHCPACK including a Rapid Commit option and fully committed
|
||||||
|
address and configuration information. Should only be enabled if either the
|
||||||
|
server is the only server for the subnet, or multiple servers are present and they each commit a binding for all clients.</dd>
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
<dt><b>-6 --dhcp-script=<path></b></dt>
|
||||||
|
<dd>Whenever a new DHCP lease is created, or an old one destroyed, or a
|
||||||
|
TFTP file transfer completes, the
|
||||||
|
executable specified by this option is run. <path>
|
||||||
|
must be an absolute pathname, no PATH search occurs.
|
||||||
|
The arguments to the process
|
||||||
|
are "add", "old" or "del", the MAC
|
||||||
|
address of the host (or DUID for IPv6) , the IP address, and the hostname,
|
||||||
|
if known. "add" means a lease has been created, "del" means it has
|
||||||
|
been destroyed, "old" is a notification of an existing lease when
|
||||||
|
dnsmasq starts or a change to MAC address or hostname of an existing
|
||||||
|
lease (also, lease length or expiry and client-id, if <b>--leasefile-ro</b> is set
|
||||||
|
and lease expiry if <b>--script-on-renewal</b> is set).
|
||||||
|
If the MAC address is from a network type other than ethernet,
|
||||||
|
it will have the network type prepended, eg "06-01:23:45:67:89:ab" for
|
||||||
|
token ring. The process is run as root (assuming that dnsmasq was originally run as
|
||||||
|
root) even if dnsmasq is configured to change UID to an unprivileged user.
|
||||||
|
<p>
|
||||||
|
The environment is inherited from the invoker of dnsmasq, with some or
|
||||||
|
all of the following variables added
|
||||||
|
<p>
|
||||||
|
For both IPv4 and IPv6:
|
||||||
|
<p>
|
||||||
|
DNSMASQ_DOMAIN if the fully-qualified domain name of the host is
|
||||||
|
known, this is set to the domain part. (Note that the hostname passed
|
||||||
|
to the script as an argument is never fully-qualified.)
|
||||||
|
<p>
|
||||||
|
If the client provides a hostname, DNSMASQ_SUPPLIED_HOSTNAME
|
||||||
|
<p>
|
||||||
|
If the client provides user-classes, DNSMASQ_USER_CLASS0..DNSMASQ_USER_CLASSn
|
||||||
|
<p>
|
||||||
|
If dnsmasq was compiled with HAVE_BROKEN_RTC, then
|
||||||
|
the length of the lease (in seconds) is stored in
|
||||||
|
DNSMASQ_LEASE_LENGTH, otherwise the time of lease expiry is stored in
|
||||||
|
DNSMASQ_LEASE_EXPIRES. The number of seconds until lease expiry is
|
||||||
|
always stored in DNSMASQ_TIME_REMAINING.
|
||||||
|
<p>
|
||||||
|
DNSMASQ_DATA_MISSING is set to "1" during "old" events for existing
|
||||||
|
leases generated at startup to indicate that data not stored in the
|
||||||
|
persistent lease database will not be present. This comprises everything
|
||||||
|
other than IP address, hostname, MAC address, DUID, IAID and lease length
|
||||||
|
or expiry time.
|
||||||
|
<p>
|
||||||
|
If a lease used to have a hostname, which is
|
||||||
|
removed, an "old" event is generated with the new state of the lease,
|
||||||
|
ie no name, and the former name is provided in the environment
|
||||||
|
variable DNSMASQ_OLD_HOSTNAME.
|
||||||
|
<p>
|
||||||
|
DNSMASQ_INTERFACE stores the name of
|
||||||
|
the interface on which the request arrived; this is not set for "old"
|
||||||
|
actions when dnsmasq restarts.
|
||||||
|
<p>
|
||||||
|
DNSMASQ_RELAY_ADDRESS is set if the client
|
||||||
|
used a DHCP relay to contact dnsmasq and the IP address of the relay
|
||||||
|
is known.
|
||||||
|
<p>
|
||||||
|
DNSMASQ_TAGS contains all the tags set during the
|
||||||
|
DHCP transaction, separated by spaces.
|
||||||
|
<p>
|
||||||
|
DNSMASQ_LOG_DHCP is set if
|
||||||
|
<b>--log-dhcp</b>
|
||||||
|
|
||||||
|
is in effect.
|
||||||
|
<p>
|
||||||
|
DNSMASQ_REQUESTED_OPTIONS a string containing the decimal values in the Parameter Request List option, comma separated, if the parameter request list option is provided by the client.
|
||||||
|
<p>
|
||||||
|
DNSMASQ_MUD_URL the Manufacturer Usage Description URL if provided by the client. (See RFC8520 for details.)
|
||||||
|
<p>
|
||||||
|
<p>
|
||||||
|
For IPv4 only:
|
||||||
|
<p>
|
||||||
|
DNSMASQ_CLIENT_ID if the host provided a client-id.
|
||||||
|
<p>
|
||||||
|
DNSMASQ_CIRCUIT_ID, DNSMASQ_SUBSCRIBER_ID, DNSMASQ_REMOTE_ID if a
|
||||||
|
DHCP relay-agent added any of these options.
|
||||||
|
<br/>
|
||||||
|
If the client provides vendor-class, DNSMASQ_VENDOR_CLASS.
|
||||||
|
<p>
|
||||||
|
For IPv6 only:
|
||||||
|
<p>
|
||||||
|
If the client provides vendor-class, DNSMASQ_VENDOR_CLASS_ID,
|
||||||
|
containing the IANA enterprise id for the class, and
|
||||||
|
DNSMASQ_VENDOR_CLASS0..DNSMASQ_VENDOR_CLASSn for the data.
|
||||||
|
<p>
|
||||||
|
DNSMASQ_SERVER_DUID containing the DUID of the server: this is the same for
|
||||||
|
every call to the script.
|
||||||
|
<p>
|
||||||
|
DNSMASQ_IAID containing the IAID for the lease. If the lease is a
|
||||||
|
temporary allocation, this is prefixed to 'T'.
|
||||||
|
<p>
|
||||||
|
DNSMASQ_MAC containing the MAC address of the client, if known.
|
||||||
|
<p>
|
||||||
|
Note that the supplied hostname, vendorclass and userclass data is
|
||||||
|
only supplied for
|
||||||
|
"add" actions or "old" actions when a host resumes an existing lease,
|
||||||
|
since these data are not held in dnsmasq's lease
|
||||||
|
database.
|
||||||
|
<p>
|
||||||
|
<p>
|
||||||
|
<p>
|
||||||
|
All file descriptors are
|
||||||
|
closed except stdin, which is open to /dev/null, and stdout and stderr which capture output for logging by dnsmasq.
|
||||||
|
(In debug mode, stdio, stdout and stderr file are left as those inherited from the invoker of dnsmasq).
|
||||||
|
<p>
|
||||||
|
The script is not invoked concurrently: at most one instance
|
||||||
|
of the script is ever running (dnsmasq waits for an instance of script to exit
|
||||||
|
before running the next). Changes to the lease database are which
|
||||||
|
require the script to be invoked are queued awaiting exit of a running instance.
|
||||||
|
If this queueing allows multiple state changes occur to a single
|
||||||
|
lease before the script can be run then
|
||||||
|
earlier states are discarded and the current state of that lease is
|
||||||
|
reflected when the script finally runs.
|
||||||
|
<p>
|
||||||
|
At dnsmasq startup, the script will be invoked for
|
||||||
|
all existing leases as they are read from the lease file. Expired
|
||||||
|
leases will be called with "del" and others with "old". When dnsmasq
|
||||||
|
receives a HUP signal, the script will be invoked for existing leases
|
||||||
|
with an "old" event.
|
||||||
|
<p>
|
||||||
|
<p>
|
||||||
|
There are five further actions which may appear as the first argument
|
||||||
|
to the script, "init", "arp-add", "arp-del", "relay-snoop" and "tftp".
|
||||||
|
More may be added in the future, so
|
||||||
|
scripts should be written to ignore unknown actions. "init" is
|
||||||
|
described below in
|
||||||
|
<b>--leasefile-ro</b>
|
||||||
|
<p>
|
||||||
|
The "tftp" action is invoked when a TFTP file transfer completes: the
|
||||||
|
arguments are the file size in bytes, the address to which the file
|
||||||
|
was sent, and the complete pathname of the file.
|
||||||
|
<p>
|
||||||
|
The "relay-snoop" action is invoked when dnsmasq is configured as a DHCP
|
||||||
|
relay for DHCPv6 and it relays a prefx delegation to a client. The arguments
|
||||||
|
are the name of the interface where the client is connected, its (link-local)
|
||||||
|
address on that interface and the delegated prefix. This information is
|
||||||
|
sufficient to install routes to the delegated prefix of a router. See
|
||||||
|
<b>--dhcp-relay</b>
|
||||||
|
|
||||||
|
for more details on configuring DHCP relay.
|
||||||
|
<p>
|
||||||
|
The "arp-add" and "arp-del" actions are only called if enabled with
|
||||||
|
<b>--script-arp</b>
|
||||||
|
|
||||||
|
They are are supplied with a MAC address and IP address as arguments. "arp-add" indicates
|
||||||
|
the arrival of a new entry in the ARP or neighbour table, and "arp-del" indicates the deletion of same.
|
||||||
|
<p></dd>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<dt><b>--dhcp-ttl=<time></b></dt>
|
||||||
|
<dd>As for <b>--local-ttl</b>, but affects only replies with information from DHCP leases. If both are given, <b>--dhcp-ttl</b> applies for DHCP information, and <b>--local-ttl</b> for others. Setting this to zero eliminates the effect of <b>--local-ttl</b> for DHCP.</dd>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<dt><b>-j, --dhcp-userclass=set:<tag>,<user-class></b></dt>
|
||||||
|
<dd>Map from a user-class string to a tag (with substring
|
||||||
|
matching, like vendor classes). Most DHCP clients provide a
|
||||||
|
"user class" which is configurable. This option
|
||||||
|
maps user classes to tags, so that DHCP options may be selectively delivered
|
||||||
|
to different classes of hosts. It is possible, for instance to use
|
||||||
|
this to set a different printer server for hosts in the class
|
||||||
|
"accounts" than for hosts in the class "engineering".</dd>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<dt><b>-U, --dhcp-vendorclass=set:<tag>,[enterprise:<IANA-enterprise number>,]<vendor-class></b></dt>
|
||||||
|
<dd>Map from a vendor-class string to a tag. Most DHCP clients provide a
|
||||||
|
"vendor class" which represents, in some sense, the type of host. This option
|
||||||
|
maps vendor classes to tags, so that DHCP options may be selectively delivered
|
||||||
|
to different classes of hosts. For example
|
||||||
|
<b>--dhcp-vendorclass=set:printers,Hewlett-Packard JetDirect</b>
|
||||||
|
|
||||||
|
will allow options to be set only for HP printers like so:
|
||||||
|
<b>--dhcp-option=tag:printers,3,192.168.4.4 </b>
|
||||||
|
|
||||||
|
The vendor-class string is
|
||||||
|
substring matched against the vendor-class supplied by the client, to
|
||||||
|
allow fuzzy matching. The set: prefix is optional but allowed for
|
||||||
|
consistency.
|
||||||
|
<p>
|
||||||
|
Note that in IPv6 only, vendorclasses are namespaced with an
|
||||||
|
IANA-allocated enterprise number. This is given with enterprise:
|
||||||
|
keyword and specifies that only vendorclasses matching the specified
|
||||||
|
number should be searched.</dd>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<dt><b>--dns-loop-detect</b></dt>
|
||||||
|
<dd>Enable code to detect DNS forwarding loops; ie the situation where a query sent to one
|
||||||
|
of the upstream server eventually returns as a new query to the dnsmasq instance. The
|
||||||
|
process works by generating TXT queries of the form <hex>.test and sending them to
|
||||||
|
each upstream server. The hex is a UID which encodes the instance of dnsmasq sending the query
|
||||||
|
and the upstream server to which it was sent. If the query returns to the server which sent it, then
|
||||||
|
the upstream server through which it was sent is disabled and this event is logged. Each time the
|
||||||
|
set of upstream servers changes, the test is re-run on all of them, including ones which
|
||||||
|
were previously disabled.</dd>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<dt><b>--dnssec-check-unsigned[=no]</b></dt>
|
||||||
|
<dd>As a default, dnsmasq checks that unsigned DNS replies are
|
||||||
|
legitimate: this entails possible extra queries even for the majority of DNS
|
||||||
|
zones which are not, at the moment, signed. If
|
||||||
|
<b>--dnssec-check-unsigned=no</b>
|
||||||
|
|
||||||
|
appears in the configuration, then such replies they are assumed to be valid and passed on (without the
|
||||||
|
"authentic data" bit set, of course). This does not protect against an
|
||||||
|
attacker forging unsigned replies for signed DNS zones, but it is
|
||||||
|
fast.
|
||||||
|
<p>
|
||||||
|
Versions of dnsmasq prior to 2.80 defaulted to not checking unsigned replies, and used
|
||||||
|
<b>--dnssec-check-unsigned</b>
|
||||||
|
|
||||||
|
to switch this on. Such configurations will continue to work as before, but those which used the default of no checking will need to be altered to explicitly select no checking. The new default is because switching off checking for unsigned replies is inherently dangerous. Not only does it open the possibility of forged replies, but it allows everything to appear to be working even when the upstream namesevers do not support DNSSEC, and in this case no DNSSEC validation at all is occurring.</dd>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<dt><b>--dnssec</b></dt>
|
||||||
|
<dd>Validate DNS replies and cache DNSSEC data. When forwarding DNS queries, dnsmasq requests the
|
||||||
|
DNSSEC records needed to validate the replies. The replies are validated and the result returned as
|
||||||
|
the Authenticated Data bit in the DNS packet. In addition the DNSSEC records are stored in the cache, making
|
||||||
|
validation by clients more efficient. Note that validation by clients is the most secure DNSSEC mode, but for
|
||||||
|
clients unable to do validation, use of the AD bit set by dnsmasq is useful, provided that the network between
|
||||||
|
the dnsmasq server and the client is trusted. Dnsmasq must be compiled with HAVE_DNSSEC enabled, and DNSSEC
|
||||||
|
trust anchors provided, see
|
||||||
|
<b>--trust-anchor.</b>
|
||||||
|
|
||||||
|
Because the DNSSEC validation process uses the cache, it is not
|
||||||
|
permitted to reduce the cache size below the default when DNSSEC is
|
||||||
|
enabled. The nameservers upstream of dnsmasq must be DNSSEC-capable,
|
||||||
|
ie capable of returning DNSSEC records with data. If they are not,
|
||||||
|
then dnsmasq will not be able to determine the trusted status of
|
||||||
|
answers and this means that DNS service will be entirely broken.</dd>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<dt><b>-D, --domain-needed</b></dt>
|
||||||
|
<dd>Tells dnsmasq to never forward A or AAAA queries for plain names, without dots
|
||||||
|
or domain parts, to upstream nameservers. If the name is not known
|
||||||
|
from /etc/hosts or DHCP then a "not found" answer is returned.</dd>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<dt><b>-s, --domain=<domain>[[,<address range>[,local]]|<interface>]</b></dt>
|
||||||
|
<dd>Specifies DNS domains for the DHCP server. Domains may be be given
|
||||||
|
unconditionally (without the IP range) or for limited IP ranges. This has two effects;
|
||||||
|
firstly it causes the DHCP server to return the domain to any hosts
|
||||||
|
which request it, and secondly it sets the domain which it is legal
|
||||||
|
for DHCP-configured hosts to claim. The intention is to constrain
|
||||||
|
hostnames so that an untrusted host on the LAN cannot advertise
|
||||||
|
its name via DHCP as e.g. "microsoft.com" and capture traffic not
|
||||||
|
meant for it. If no domain suffix is specified, then any DHCP
|
||||||
|
hostname with a domain part (ie with a period) will be disallowed
|
||||||
|
and logged. If suffix is specified, then hostnames with a domain
|
||||||
|
part are allowed, provided the domain part matches the suffix. In
|
||||||
|
addition, when a suffix is set then hostnames without a domain
|
||||||
|
part have the suffix added as an optional domain part. Eg on my network I can set
|
||||||
|
<b>--domain=thekelleys.org.uk</b>
|
||||||
|
|
||||||
|
and have a machine whose DHCP hostname is "laptop". The IP address for that machine is available from
|
||||||
|
<b>dnsmasq</b>
|
||||||
|
|
||||||
|
both as "laptop" and "laptop.thekelleys.org.uk". If the domain is
|
||||||
|
given as "#" then the domain is read from the first "search" directive
|
||||||
|
in /etc/resolv.conf (or equivalent).
|
||||||
|
<p>
|
||||||
|
The address range can be of the form
|
||||||
|
<ip address>,<ip address> or <ip address>/<netmask> or just a single
|
||||||
|
<ip address>. See
|
||||||
|
<b>--dhcp-fqdn</b>
|
||||||
|
|
||||||
|
which can change the behaviour of dnsmasq with domains.
|
||||||
|
<p>
|
||||||
|
If the address range is given as ip-address/network-size, then a
|
||||||
|
additional flag "local" may be supplied which has the effect of adding
|
||||||
|
<b>--local</b> declarations for forward and reverse DNS queries. Eg.
|
||||||
|
<b>--domain=thekelleys.org.uk,192.168.0.0/24,local</b>
|
||||||
|
|
||||||
|
is identical to
|
||||||
|
<b>--domain=thekelleys.org.uk,192.168.0.0/24</b>
|
||||||
|
<b>--local=/thekelleys.org.uk/ --local=/0.168.192.in-addr.arpa/</b>
|
||||||
|
<p>
|
||||||
|
The address range can also be given as a network interface name, in which case
|
||||||
|
all of the subnets currently assigned to the interface are used in matching the
|
||||||
|
address. This allows hosts on different physical subnets to be given different
|
||||||
|
domains in a way which updates automatically as the interface addresses change.</dd>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<dt><b>--dynamic-host=<name>,[IPv4-address],[IPv6-address],<interface></b></dt>
|
||||||
|
<dd>Add A, AAAA and PTR records to the DNS in the same subnet as the specified interface. The address is derived from the network part of each address associated with the interface, and the host part from the specified address. For example
|
||||||
|
<b>--dynamic-host=example.com,0.0.0.8,eth0</b>
|
||||||
|
|
||||||
|
will, when eth0 has the address 192.168.78.x and netmask 255.255.255.0 give the
|
||||||
|
name example.com an A record for 192.168.78.8. The same principle applies to IPv6 addresses. Note that if an interface has more than one address, more than one A or AAAA record will be created. The TTL of the records is always zero, and any changes to interface addresses will be immediately reflected in them.</dd>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<dt><b>-P, --edns-packet-max=<size></b></dt>
|
||||||
|
<dd>Specify the largest EDNS.0 UDP packet which is supported by the DNS
|
||||||
|
forwarder. Defaults to 1232, which is the recommended size following the
|
||||||
|
DNS flag day in 2020. Only increase if you know what you are doing.</dd>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<dt><b>-1, --enable-dbus[=<service-name>]</b></dt>
|
||||||
|
<dd>Allow dnsmasq configuration to be updated via DBus method calls. The
|
||||||
|
configuration which can be changed is upstream DNS servers (and
|
||||||
|
corresponding domains) and cache clear. Requires that dnsmasq has
|
||||||
|
been built with DBus support. If the service name is given, dnsmasq
|
||||||
|
provides service at that name, rather than the default which is
|
||||||
|
<b>uk.org.thekelleys.dnsmasq</b></dd>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<dt><b>--enable-ra</b></dt>
|
||||||
|
<dd>Enable dnsmasq's IPv6 Router Advertisement feature. DHCPv6 doesn't
|
||||||
|
handle complete network configuration in the same way as DHCPv4. Router
|
||||||
|
discovery and (possibly) prefix discovery for autonomous address
|
||||||
|
creation are handled by a different protocol. When DHCP is in use,
|
||||||
|
only a subset of this is needed, and dnsmasq can handle it, using
|
||||||
|
existing DHCP configuration to provide most data. When RA is enabled,
|
||||||
|
dnsmasq will advertise a prefix for each <b>--dhcp-range</b>, with default
|
||||||
|
router as the relevant link-local address on
|
||||||
|
the machine running dnsmasq. By default, the "managed address" bits are set, and
|
||||||
|
the "use SLAAC" bit is reset. This can be changed for individual
|
||||||
|
subnets with the mode keywords described in
|
||||||
|
<b>--dhcp-range.</b>
|
||||||
|
|
||||||
|
RFC6106 DNS parameters are included in the advertisements. By default,
|
||||||
|
the relevant link-local address of the machine running dnsmasq is sent
|
||||||
|
as recursive DNS server. If provided, the DHCPv6 options dns-server and
|
||||||
|
domain-search are used for the DNS server (RDNSS) and the domain search list (DNSSL).</dd>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<dt><b>--enable-tftp[=<interface>[,<interface>]]</b></dt>
|
||||||
|
<dd>Enable the TFTP server function. This is deliberately limited to that
|
||||||
|
needed to net-boot a client. Only reading is allowed; the tsize and
|
||||||
|
blksize extensions are supported (tsize is only supported in octet
|
||||||
|
mode). Without an argument, the TFTP service is provided to the same set of interfaces as DHCP service.
|
||||||
|
If the list of interfaces is provided, that defines which interfaces receive TFTP service.</dd>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<dt><b>--enable-ubus[=<service-name>]</b></dt>
|
||||||
|
<dd>Enable dnsmasq UBus interface. It sends notifications via UBus on
|
||||||
|
DHCPACK and DHCPRELEASE events. Furthermore it offers metrics
|
||||||
|
and allows configuration of Linux connection track mark based filtering.
|
||||||
|
When DNS query filtering based on Linux connection track marks is enabled
|
||||||
|
UBus notifications are generated for each resolved or filtered DNS query.
|
||||||
|
Requires that dnsmasq has been built with UBus support. If the service
|
||||||
|
name is given, dnsmasq provides service at that namespace, rather than
|
||||||
|
the default which is
|
||||||
|
<b>dnsmasq</b></dd>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<dt><b>-I, --except-interface=<interface name></b></dt>
|
||||||
|
<dd>Do not listen on the specified interface. Note that the order of
|
||||||
|
<b>--listen-address</b>
|
||||||
|
<b>--interface</b>
|
||||||
|
|
||||||
|
and
|
||||||
|
<b>--except-interface</b>
|
||||||
|
|
||||||
|
options does not matter and that
|
||||||
|
<b>--except-interface</b>
|
||||||
|
|
||||||
|
options always override the others. The comments about interface labels for
|
||||||
|
<b>--listen-address</b>
|
||||||
|
|
||||||
|
apply here.</dd>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<dt><b>-E, --expand-hosts</b></dt>
|
||||||
|
<dd>Add the domain to simple names (without a period) in /etc/hosts
|
||||||
|
in the same way as for DHCP-derived names. Note that this does not
|
||||||
|
apply to domain names in cnames, PTR records, TXT records etc.</dd>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<dt><b>--fast-dns-retry=[<initial retry delay in ms>[,<time to continue retries in ms>]]</b></dt>
|
||||||
|
<dd>Under normal circumstances, dnsmasq relies on DNS clients to do retries; it
|
||||||
|
does not generate timeouts itself. Setting this option
|
||||||
|
instructs dnsmasq to generate its own retries starting after a delay
|
||||||
|
which defaults to 1000ms. If the second parameter is given this controls
|
||||||
|
how long the retries will continue for
|
||||||
|
otherwise this defaults to 10000ms. Retries are repeated with exponential
|
||||||
|
backoff. Using this option increases memory usage and
|
||||||
|
network bandwidth. If not otherwise configured, this option is activated
|
||||||
|
with the default parameters when <b>--dnssec</b> is set.</dd>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<dt><b>--filter-A</b></dt>
|
||||||
|
<dd>Remove A records from answers. No IPv4 addresses will be returned.</dd>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<dt><b>--filter-AAAA</b></dt>
|
||||||
|
<dd>Remove AAAA records from answers. No IPv6 addresses will be returned.</dd>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<dt><b>--filter-rr=<rrtype>[,<rrtype>...]</b></dt>
|
||||||
|
<dd>Remove records of the specified type(s) from answers. The otherwise-nonsensical --filter-rr=ANY has
|
||||||
|
a special meaning: it filters replies to queries for type ANY. Everything other than A, AAAA, MX and CNAME
|
||||||
|
records are removed. Since ANY queries with forged source addresses can be used in DNS amplification attacks
|
||||||
|
(replies to ANY queries can be large) this defangs such attacks, whilst still supporting the
|
||||||
|
one remaining possible use of ANY queries. See RFC 8482 para 4.3 for details.</dd>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<dt><b>-f, --filterwin2k</b></dt>
|
||||||
|
<dd>Later versions of windows make periodic DNS requests which don't get sensible answers from
|
||||||
|
the public DNS and can cause problems by triggering dial-on-demand links. This flag turns on an option
|
||||||
|
to filter such requests. The requests blocked are for records of type ANY
|
||||||
|
where the requested name has underscores, to catch LDAP requests, and for
|
||||||
|
<b>all</b> records of types SOA and SRV.</dd>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<dt><b>-g, --group=<groupname> </b></dt>
|
||||||
|
<dd>Specify the group which dnsmasq will run
|
||||||
|
as. The default is "dip", if available, to facilitate access to
|
||||||
|
/etc/ppp/resolv.conf which is not normally world readable.</dd>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<dt><b>--host-record=<name>[,<name>....],[<IPv4-address>],[<IPv6-address>][,<TTL>]</b></dt>
|
||||||
|
<dd>Add A, AAAA and PTR records to the DNS. This adds one or more names to
|
||||||
|
the DNS with associated IPv4 (A) and IPv6 (AAAA) records. A name may
|
||||||
|
appear in more than one
|
||||||
|
<b>--host-record</b>
|
||||||
|
|
||||||
|
and therefore be assigned more than one address. Only the first
|
||||||
|
address creates a PTR record linking the address to the name. This is
|
||||||
|
the same rule as is used reading hosts-files.
|
||||||
|
<b>--host-record</b>
|
||||||
|
|
||||||
|
options are considered to be read before host-files, so a name
|
||||||
|
appearing there inhibits PTR-record creation if it appears in
|
||||||
|
hosts-file also. Unlike hosts-files, names are not expanded, even when
|
||||||
|
<b>--expand-hosts</b>
|
||||||
|
|
||||||
|
is in effect. Short and long names may appear in the same
|
||||||
|
<b>--host-record,</b>
|
||||||
|
|
||||||
|
eg.
|
||||||
|
<b>--host-record=laptop,laptop.thekelleys.org,192.168.0.1,1234::100</b>
|
||||||
|
<p>
|
||||||
|
If the time-to-live is given, it overrides the default, which is zero
|
||||||
|
or the value of <b>--local-ttl</b>. The value is a positive integer and gives
|
||||||
|
the time-to-live in seconds.</dd>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<dt><b>--hostsdir=<path></b></dt>
|
||||||
|
<dd>Read all the hosts files contained in the directory. New or changed files
|
||||||
|
are read automatically and modified and deleted files have removed records
|
||||||
|
automatically deleted.</dd>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<dt><b>--ignore-address=<ipaddr>[/prefix]</b></dt>
|
||||||
|
<dd>Ignore replies to A or AAAA queries which include the specified address or subnet.
|
||||||
|
No error is generated, dnsmasq simply continues to listen for another reply.
|
||||||
|
This is useful to defeat blocking strategies which rely on quickly supplying a
|
||||||
|
forged answer to a DNS request for certain domain, before the correct answer can arrive.</dd>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<dt><b>--interface-name=<name>,<interface>[/4|/6]</b></dt>
|
||||||
|
<dd>Return DNS records associating the name with the address(es) of
|
||||||
|
the given interface. This flag specifies an A or AAAA record for the given
|
||||||
|
name in the same way as an /etc/hosts line, except that the address is
|
||||||
|
not constant, but taken from the given interface. The interface may be
|
||||||
|
followed by "/4" or "/6" to specify that only IPv4 or IPv6 addresses
|
||||||
|
of the interface should be used. If the interface is
|
||||||
|
down, not configured or non-existent, an empty record is returned. The
|
||||||
|
matching PTR record is also created, mapping the interface address to
|
||||||
|
the name. More than one name may be associated with an interface
|
||||||
|
address by repeating the flag; in that case the first instance is used
|
||||||
|
for the reverse address-to-name mapping. Note that a name used in
|
||||||
|
<b>--interface-name</b> may not appear in /etc/hosts.</dd>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<dt><b>-i, --interface=<interface name></b></dt>
|
||||||
|
<dd>Listen only on the specified interface(s). Dnsmasq automatically adds
|
||||||
|
the loopback (local) interface to the list of interfaces to use when
|
||||||
|
the
|
||||||
|
<b>--interface</b>
|
||||||
|
|
||||||
|
option is used. If no
|
||||||
|
<b>--interface</b>
|
||||||
|
|
||||||
|
or
|
||||||
|
<b>--listen-address</b>
|
||||||
|
|
||||||
|
options are given dnsmasq listens on all available interfaces except any
|
||||||
|
given in
|
||||||
|
<b>--except-interface</b>
|
||||||
|
|
||||||
|
options. On Linux, when
|
||||||
|
<b>--bind-interfaces</b>
|
||||||
|
|
||||||
|
or
|
||||||
|
<b>--bind-dynamic</b>
|
||||||
|
|
||||||
|
are in effect, IP alias interface labels (eg "eth1:0") are checked, rather than
|
||||||
|
interface names. In the degenerate case when an interface has one address, this amounts to the same thing but when an interface has multiple addresses it
|
||||||
|
allows control over which of those addresses are accepted.
|
||||||
|
The same effect is achievable in default mode by using
|
||||||
|
<b>--listen-address.</b>
|
||||||
|
|
||||||
|
A simple wildcard, consisting of a trailing '*',
|
||||||
|
can be used in
|
||||||
|
<b>--interface </b>
|
||||||
|
|
||||||
|
and
|
||||||
|
<b>--except-interface</b>
|
||||||
|
|
||||||
|
options.</dd>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<dt><b>--ipset=/<domain>[/<domain>...]/<ipset>[,<ipset>...]</b></dt>
|
||||||
|
<dd>Places the resolved IP addresses of queries for one or more domains in
|
||||||
|
the specified Netfilter IP set. If multiple setnames are given, then the
|
||||||
|
addresses are placed in each of them, subject to the limitations of an
|
||||||
|
IP set (IPv4 addresses cannot be stored in an IPv6 IP set and vice
|
||||||
|
versa). Domains and subdomains are matched in the same way as
|
||||||
|
<b>--address</b>.
|
||||||
|
These IP sets must already exist. See
|
||||||
|
<b>ipset</b>(8)
|
||||||
|
|
||||||
|
for more details.</dd>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user