diff --git a/scripts/extract-option-help.sh b/scripts/extract-option-help.sh
index 9494e5c..eea8a04 100755
--- a/scripts/extract-option-help.sh
+++ b/scripts/extract-option-help.sh
@@ -122,6 +122,7 @@ OPTION_KEYS = [
"dhcp-hostsfile",
"dhcp-optsfile",
"dhcp-hostsdir",
+ "dhcp-optsdir",
"dhcp-boot",
"dhcp-ignore",
"dhcp-vendorclass",
diff --git a/src/DnsmasqWebUI.Tests/DnsmasqCompileOptionsParserTests.cs b/src/DnsmasqWebUI.Tests/DnsmasqCompileOptionsParserTests.cs
new file mode 100644
index 0000000..1e5ef2b
--- /dev/null
+++ b/src/DnsmasqWebUI.Tests/DnsmasqCompileOptionsParserTests.cs
@@ -0,0 +1,72 @@
+using DnsmasqWebUI.Infrastructure.Helpers.Dnsmasq;
+using DnsmasqWebUI.Models.Dnsmasq;
+
+namespace DnsmasqWebUI.Tests;
+
+///
+/// Tests for parsing "Compile time options: ..." from dnsmasq --version output.
+///
+public class DnsmasqCompileOptionsParserTests
+{
+ [Fact]
+ public void Parse_WhenNoCompileLine_ReturnsAllCapabilitiesFalse()
+ {
+ var result = DnsmasqCompileOptionsParser.Parse("Dnsmasq version 2.89\nCopyright ...", null);
+ Assert.False(result.Dhcp);
+ Assert.False(result.Tftp);
+ Assert.False(result.Dnssec);
+ Assert.False(result.Dbus);
+ Assert.NotNull(result.RawTokens);
+ Assert.Empty(result.RawTokens);
+ }
+
+ [Fact]
+ public void Parse_WhenCompileLineHasDnssec_SetsDnssecTrue()
+ {
+ var stdout = "Dnsmasq version 2.89\nCompile time options: IPv6 GNU-getopt no-DBus no-Idn DHCP DHCPv6 TFTP DNSSEC auth";
+ var result = DnsmasqCompileOptionsParser.Parse(stdout, null);
+ Assert.True(result.Dnssec);
+ Assert.True(result.Dhcp);
+ Assert.True(result.Tftp);
+ Assert.False(result.Dbus);
+ Assert.Contains("DNSSEC", result.RawTokens, StringComparer.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void Parse_WhenCompileLineHasDhcpOnly_SetsDhcpTrueOthersFalse()
+ {
+ var result = DnsmasqCompileOptionsParser.Parse(null, "Compile time options: DHCP no-DBus no-DNSSEC");
+ Assert.True(result.Dhcp);
+ Assert.False(result.Tftp);
+ Assert.False(result.Dnssec);
+ Assert.False(result.Dbus);
+ }
+
+ [Fact]
+ public void Parse_WhenCompileLineHasDhcpv6_SetsDhcpTrue()
+ {
+ var result = DnsmasqCompileOptionsParser.Parse("Compile time options: DHCPv6", null);
+ Assert.True(result.Dhcp);
+ }
+
+ [Fact]
+ public void Parse_WhenCompileLineHasTftp_SetsTftpTrue()
+ {
+ var result = DnsmasqCompileOptionsParser.Parse("Compile time options: TFTP", null);
+ Assert.True(result.Tftp);
+ }
+
+ [Fact]
+ public void Parse_WhenCompileLineHasDbus_SetsDbusTrue()
+ {
+ var result = DnsmasqCompileOptionsParser.Parse("Compile time options: DBus", null);
+ Assert.True(result.Dbus);
+ }
+
+ [Fact]
+ public void Parse_CombinesStdoutAndStderr()
+ {
+ var result = DnsmasqCompileOptionsParser.Parse("Version line\n", "Compile time options: DNSSEC");
+ Assert.True(result.Dnssec);
+ }
+}
diff --git a/src/DnsmasqWebUI.Tests/DnsmasqConfIncludeParserDhcpTftpDnssecTests.cs b/src/DnsmasqWebUI.Tests/DnsmasqConfIncludeParserDhcpTftpDnssecTests.cs
index 450eb4c..c929626 100644
--- a/src/DnsmasqWebUI.Tests/DnsmasqConfIncludeParserDhcpTftpDnssecTests.cs
+++ b/src/DnsmasqWebUI.Tests/DnsmasqConfIncludeParserDhcpTftpDnssecTests.cs
@@ -283,7 +283,7 @@ public class DnsmasqConfIncludeParserDhcpTftpDnssecTests
}
[Fact]
- public void GetFlagFromConfigFiles_DnssecCheckUnsigned_WhenPresent_ReturnsTrue()
+ public void GetLastValueFromConfigFiles_DnssecCheckUnsigned_WhenPresent_ReturnsValue()
{
var dir = Path.Combine(Path.GetTempPath(), "dnsmasq-dnsseccu-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(dir);
@@ -291,8 +291,26 @@ public class DnsmasqConfIncludeParserDhcpTftpDnssecTests
try
{
File.WriteAllText(conf, "dnssec-check-unsigned\n");
- var result = DnsmasqConfIncludeParser.GetFlagFromConfigFiles(new[] { conf }, DnsmasqConfKeys.DnssecCheckUnsigned);
- Assert.True(result);
+ var (value, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(new[] { conf }, DnsmasqConfKeys.DnssecCheckUnsigned);
+ Assert.NotNull(value);
+ }
+ finally
+ {
+ Directory.Delete(dir, recursive: true);
+ }
+ }
+
+ [Fact]
+ public void GetLastValueFromConfigFiles_DnssecCheckUnsigned_WhenEqualsNo_ReturnsNo()
+ {
+ var dir = Path.Combine(Path.GetTempPath(), "dnsmasq-dnsseccu-no-" + Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(dir);
+ var conf = Path.Combine(dir, "dnsmasq.conf");
+ try
+ {
+ File.WriteAllText(conf, "dnssec-check-unsigned=no\n");
+ var (value, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(new[] { conf }, DnsmasqConfKeys.DnssecCheckUnsigned);
+ Assert.Equal("no", value);
}
finally
{
diff --git a/src/DnsmasqWebUI.Tests/DnsmasqConfIncludeParserNewOptionsTests.cs b/src/DnsmasqWebUI.Tests/DnsmasqConfIncludeParserNewOptionsTests.cs
index b665b94..c9c807a 100644
--- a/src/DnsmasqWebUI.Tests/DnsmasqConfIncludeParserNewOptionsTests.cs
+++ b/src/DnsmasqWebUI.Tests/DnsmasqConfIncludeParserNewOptionsTests.cs
@@ -80,9 +80,9 @@ public class DnsmasqConfIncludeParserNewOptionsTests
}
[Fact]
- public void GetFlagFromConfigFiles_ConnmarkAllowlistEnable_WhenPresent_ReturnsTrue()
+ public void GetLastValueFromConfigFiles_ConnmarkAllowlistEnable_WhenPresent_ReturnsValue()
{
- WriteFlagAndAssertTrue(DnsmasqConfKeys.ConnmarkAllowlistEnable);
+ WriteLastWinsKeyOnlyAndAssertPresent(DnsmasqConfKeys.ConnmarkAllowlistEnable);
}
[Fact]
@@ -104,9 +104,32 @@ public class DnsmasqConfIncludeParserNewOptionsTests
}
[Fact]
- public void GetFlagFromConfigFiles_Leasequery_WhenPresent_ReturnsTrue()
+ public void GetMultiValueFromConfigFiles_Leasequery_WhenPresent_ReturnsValues()
{
- WriteFlagAndAssertTrue(DnsmasqConfKeys.Leasequery);
+ var dir = CreateTempDir();
+ var conf = Path.Combine(dir, "dnsmasq.conf");
+ try
+ {
+ File.WriteAllText(conf, "leasequery\n");
+ var result = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(new[] { conf }, DnsmasqConfKeys.Leasequery);
+ Assert.Single(result);
+ Assert.Equal("", result[0]);
+ }
+ finally { Directory.Delete(dir, recursive: true); }
+ }
+
+ [Fact]
+ public void GetMultiValueFromConfigFiles_Leasequery_WithValue_IsParsed()
+ {
+ var dir = CreateTempDir();
+ var conf = Path.Combine(dir, "dnsmasq.conf");
+ try
+ {
+ File.WriteAllText(conf, "leasequery=10.0.0.0/24\n");
+ var result = DnsmasqConfIncludeParser.GetMultiValueFromConfigFiles(new[] { conf }, DnsmasqConfKeys.Leasequery);
+ Assert.Equal("10.0.0.0/24", Assert.Single(result));
+ }
+ finally { Directory.Delete(dir, recursive: true); }
}
[Fact]
@@ -122,15 +145,15 @@ public class DnsmasqConfIncludeParserNewOptionsTests
}
[Fact]
- public void GetFlagFromConfigFiles_DhcpGenerateNames_WhenPresent_ReturnsTrue()
+ public void GetLastValueFromConfigFiles_DhcpGenerateNames_WhenPresent_ReturnsValue()
{
- WriteFlagAndAssertTrue(DnsmasqConfKeys.DhcpGenerateNames);
+ WriteLastWinsKeyOnlyAndAssertPresent(DnsmasqConfKeys.DhcpGenerateNames);
}
[Fact]
- public void GetFlagFromConfigFiles_DhcpBroadcast_WhenPresent_ReturnsTrue()
+ public void GetLastValueFromConfigFiles_DhcpBroadcast_WhenPresent_ReturnsValue()
{
- WriteFlagAndAssertTrue(DnsmasqConfKeys.DhcpBroadcast);
+ WriteLastWinsKeyOnlyAndAssertPresent(DnsmasqConfKeys.DhcpBroadcast);
}
[Fact]
@@ -146,9 +169,9 @@ public class DnsmasqConfIncludeParserNewOptionsTests
}
[Fact]
- public void GetFlagFromConfigFiles_BootpDynamic_WhenPresent_ReturnsTrue()
+ public void GetLastValueFromConfigFiles_BootpDynamic_WhenPresent_ReturnsValue()
{
- WriteFlagAndAssertTrue(DnsmasqConfKeys.BootpDynamic);
+ WriteLastWinsKeyOnlyAndAssertPresent(DnsmasqConfKeys.BootpDynamic);
}
[Fact]
@@ -403,6 +426,19 @@ public class DnsmasqConfIncludeParserNewOptionsTests
finally { Directory.Delete(dir, recursive: true); }
}
+ private static void WriteLastWinsKeyOnlyAndAssertPresent(string key)
+ {
+ var dir = CreateTempDir();
+ var conf = Path.Combine(dir, "dnsmasq.conf");
+ try
+ {
+ File.WriteAllText(conf, key + "\n");
+ var (value, _) = DnsmasqConfIncludeParser.GetLastValueFromConfigFiles(new[] { conf }, key);
+ Assert.NotNull(value);
+ }
+ finally { Directory.Delete(dir, recursive: true); }
+ }
+
private static string CreateTempDir()
{
var dir = Path.Combine(Path.GetTempPath(), "dnsmasq-newopts-" + Guid.NewGuid().ToString("N"));
diff --git a/src/DnsmasqWebUI.Tests/DnsmasqConfigServiceApplyChangesTests.cs b/src/DnsmasqWebUI.Tests/DnsmasqConfigServiceApplyChangesTests.cs
index a04d3df..be7c50e 100644
--- a/src/DnsmasqWebUI.Tests/DnsmasqConfigServiceApplyChangesTests.cs
+++ b/src/DnsmasqWebUI.Tests/DnsmasqConfigServiceApplyChangesTests.cs
@@ -159,6 +159,41 @@ public class DnsmasqConfigServiceApplyChangesTests
}
}
+ [Fact]
+ public async Task ApplyEffectiveConfigChangesAsync_DnssecCheckUnsignedNo_WritesEqualsNo()
+ {
+ var dir = Path.Combine(Path.GetTempPath(), "dnsmasq-apply-" + Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(dir);
+ var mainPath = Path.Combine(dir, "dnsmasq.conf");
+ var managedName = "zz-managed.conf";
+ var managedPath = Path.Combine(dir, managedName);
+ ConfigSetCache? cache = null;
+ try
+ {
+ File.WriteAllText(mainPath, "port=53\n");
+ var options = Options.Create(new DnsmasqOptions { MainConfigPath = mainPath, ManagedFileName = managedName });
+ cache = new ConfigSetCache(options, NullLogger.Instance);
+ var setService = new DnsmasqConfigSetService(cache);
+ var configService = new DnsmasqConfigService(setService, cache, NullLogger.Instance);
+
+ var changes = new List
+ {
+ new(EffectiveConfigSections.SectionDnssec, DnsmasqConfKeys.DnssecCheckUnsigned, null, "no", null)
+ };
+ await configService.ApplyEffectiveConfigChangesAsync(changes);
+
+ Assert.True(File.Exists(managedPath));
+ var text = await File.ReadAllTextAsync(managedPath);
+ Assert.Contains("dnssec-check-unsigned=no", text);
+ }
+ finally
+ {
+ cache?.Dispose();
+ if (Directory.Exists(dir))
+ Directory.Delete(dir, recursive: true);
+ }
+ }
+
[Fact]
public async Task ApplyEffectiveConfigChangesAsync_KeyOnlyOrValue_KeyOnly_WritesBareKey()
{
@@ -475,4 +510,55 @@ public class DnsmasqConfigServiceApplyChangesTests
Directory.Delete(dir, recursive: true);
}
}
+
+ [Fact]
+ public async Task ApplyEffectiveConfigChangesAsync_Leasequery_MultiKeyOnlyOrValue_DoesNotDuplicateReadonlyValues()
+ {
+ var dir = Path.Combine(Path.GetTempPath(), "dnsmasq-apply-" + Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(dir);
+ var mainPath = Path.Combine(dir, "dnsmasq.conf");
+ var readonlyPath = Path.Combine(dir, "readonly.conf");
+ var managedName = "zz-managed.conf";
+ var managedPath = Path.Combine(dir, managedName);
+ ConfigSetCache? cache = null;
+ try
+ {
+ File.WriteAllText(mainPath, $"port=53\nconf-file={readonlyPath}\n");
+ File.WriteAllText(readonlyPath, "leasequery\nleasequery=10.0.0.0/24\n");
+ File.WriteAllText(managedPath, "leasequery=172.16.0.0/16\n");
+
+ var options = Options.Create(new DnsmasqOptions { MainConfigPath = mainPath, ManagedFileName = managedName });
+ cache = new ConfigSetCache(options, NullLogger.Instance);
+ cache.Invalidate();
+ var setService = new DnsmasqConfigSetService(cache);
+ var configService = new DnsmasqConfigService(setService, cache, NullLogger.Instance);
+
+ // Simulate UI effective values (readonly + managed). Save should write only managed-only values.
+ var changes = new List
+ {
+ new(
+ EffectiveConfigSections.SectionDhcp,
+ DnsmasqConfKeys.Leasequery,
+ null,
+ new List { "", "10.0.0.0/24", "192.168.50.0/24" },
+ null)
+ };
+
+ await configService.ApplyEffectiveConfigChangesAsync(changes);
+
+ var lines = (await File.ReadAllLinesAsync(managedPath))
+ .Select(l => l.Trim())
+ .Where(l => l.StartsWith("leasequery", StringComparison.Ordinal))
+ .ToList();
+
+ Assert.Single(lines);
+ Assert.Equal("leasequery=192.168.50.0/24", lines[0]);
+ }
+ finally
+ {
+ cache?.Dispose();
+ if (Directory.Exists(dir))
+ Directory.Delete(dir, recursive: true);
+ }
+ }
}
diff --git a/src/DnsmasqWebUI.Tests/EffectiveConfigFeatureRequirementsTests.cs b/src/DnsmasqWebUI.Tests/EffectiveConfigFeatureRequirementsTests.cs
new file mode 100644
index 0000000..2ac1b6f
--- /dev/null
+++ b/src/DnsmasqWebUI.Tests/EffectiveConfigFeatureRequirementsTests.cs
@@ -0,0 +1,142 @@
+using DnsmasqWebUI.Infrastructure.Helpers.Config;
+using DnsmasqWebUI.Models.Dnsmasq;
+using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
+
+namespace DnsmasqWebUI.Tests;
+
+///
+/// Tests for option -> feature mapping and capability checks.
+///
+public class EffectiveConfigFeatureRequirementsTests
+{
+ [Theory]
+ [InlineData(DnsmasqConfKeys.Dnssec, DnsmasqFeature.Dnssec)]
+ [InlineData(DnsmasqConfKeys.DnssecCheckUnsigned, DnsmasqFeature.Dnssec)]
+ [InlineData(DnsmasqConfKeys.TrustAnchor, DnsmasqFeature.Dnssec)]
+ [InlineData(DnsmasqConfKeys.DhcpRange, DnsmasqFeature.Dhcp)]
+ [InlineData(DnsmasqConfKeys.EnableTftp, DnsmasqFeature.Tftp)]
+ [InlineData(DnsmasqConfKeys.EnableDbus, DnsmasqFeature.Dbus)]
+ public void GetRequiredFeature_ReturnsExpectedFeature(string optionName, DnsmasqFeature expected)
+ {
+ var feature = EffectiveConfigFeatureRequirements.GetRequiredFeature(optionName);
+ Assert.NotNull(feature);
+ Assert.Equal(expected, feature.Value);
+ }
+
+ [Fact]
+ public void GetRequiredFeature_ForUnknownOption_ReturnsNull()
+ {
+ var feature = EffectiveConfigFeatureRequirements.GetRequiredFeature("no-such-option");
+ Assert.Null(feature);
+ }
+
+ [Fact]
+ public void IsSupportedByCapabilities_WhenCapabilityPresent_ReturnsTrue()
+ {
+ var caps = new DnsmasqCompileCapabilities(true, true, true, false,
+ new HashSet(StringComparer.OrdinalIgnoreCase) { "DHCP", "TFTP", "DNSSEC" });
+ Assert.True(EffectiveConfigFeatureRequirements.IsSupportedByCapabilities(DnsmasqConfKeys.DnssecCheckUnsigned, caps));
+ Assert.True(EffectiveConfigFeatureRequirements.IsSupportedByCapabilities(DnsmasqConfKeys.DhcpRange, caps));
+ }
+
+ [Fact]
+ public void IsSupportedByCapabilities_WhenDnssecMissing_ReturnsFalseForDnssecOptions()
+ {
+ var caps = new DnsmasqCompileCapabilities(true, true, false, false,
+ new HashSet(StringComparer.OrdinalIgnoreCase));
+ Assert.False(EffectiveConfigFeatureRequirements.IsSupportedByCapabilities(DnsmasqConfKeys.Dnssec, caps));
+ Assert.False(EffectiveConfigFeatureRequirements.IsSupportedByCapabilities(DnsmasqConfKeys.DnssecCheckUnsigned, caps));
+ }
+
+ [Fact]
+ public void IsSupportedByCapabilities_ForOptionWithNoRequiredFeature_ReturnsTrue()
+ {
+ var caps = new DnsmasqCompileCapabilities(false, false, false, false,
+ new HashSet(StringComparer.OrdinalIgnoreCase));
+ Assert.True(EffectiveConfigFeatureRequirements.IsSupportedByCapabilities(DnsmasqConfKeys.Port, caps));
+ }
+
+ [Fact]
+ public void GetCapabilityDisabled_WhenStatusSupportsFeature_ReturnsNotDisabled()
+ {
+ var status = CreateStatusWithCapabilities(dnssec: true);
+ var (isDisabled, reason) = EffectiveConfigFeatureRequirements.GetCapabilityDisabled(DnsmasqConfKeys.DnssecCheckUnsigned, status);
+ Assert.False(isDisabled);
+ Assert.Null(reason);
+ }
+
+ [Fact]
+ public void GetCapabilityDisabled_WhenStatusMissingDnssec_ReturnsDisabledWithReason()
+ {
+ var status = CreateStatusWithCapabilities(dnssec: false);
+ var (isDisabled, reason) = EffectiveConfigFeatureRequirements.GetCapabilityDisabled(DnsmasqConfKeys.DnssecCheckUnsigned, status);
+ Assert.True(isDisabled);
+ Assert.NotNull(reason);
+ Assert.Contains("DNSSEC", reason, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void GetCapabilityDisabled_WhenStatusNull_ReturnsNotDisabled()
+ {
+ var (isDisabled, _) = EffectiveConfigFeatureRequirements.GetCapabilityDisabled(DnsmasqConfKeys.DnssecCheckUnsigned, null);
+ Assert.False(isDisabled);
+ }
+
+ ///
+ /// Save path uses IsSupportedByCapabilities(optionName, version.Capabilities) to reject changes before write.
+ /// This test asserts that logic: a build without DNSSEC would reject dnssec-check-unsigned.
+ ///
+ [Fact]
+ public void SaveGuard_WhenBuildHasNoDnssec_DnssecOptionChangesWouldBeRejected()
+ {
+ var noDnssecCaps = new DnsmasqCompileCapabilities(true, true, false, false,
+ new HashSet(StringComparer.OrdinalIgnoreCase) { "DHCP", "TFTP" });
+ Assert.False(EffectiveConfigFeatureRequirements.IsSupportedByCapabilities(DnsmasqConfKeys.DnssecCheckUnsigned, noDnssecCaps));
+ var reason = EffectiveConfigFeatureRequirements.GetUnsupportedReason(DnsmasqFeature.Dnssec);
+ Assert.Contains("DNSSEC", reason, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static DnsmasqServiceStatus CreateStatusWithCapabilities(bool dhcp = true, bool tftp = true, bool dnssec = true, bool dbus = false)
+ {
+ return new DnsmasqServiceStatus(
+ SystemHostsPath: null,
+ SystemHostsPathExists: false,
+ ManagedHostsFilePath: null,
+ ManagedHostsPathExists: false,
+ NoHosts: false,
+ AddnHostsPaths: Array.Empty(),
+ EffectiveConfig: null,
+ EffectiveConfigSources: null,
+ MainConfigPath: null,
+ ManagedFilePath: null,
+ LeasesPath: null,
+ MainConfigPathExists: false,
+ ManagedFilePathExists: false,
+ LeasesPathConfigured: false,
+ LeasesPathExists: false,
+ ConfigFiles: null,
+ ReloadCommandConfigured: false,
+ StatusCommandConfigured: false,
+ StatusShowConfigured: false,
+ LogsConfigured: false,
+ LogsPath: null,
+ StatusShowCommand: null,
+ LogsCommand: null,
+ DnsmasqStatus: "active",
+ StatusCommandExitCode: null,
+ StatusCommandStdout: null,
+ StatusCommandStderr: null,
+ StatusShowOutput: null,
+ LogsOutput: null,
+ DhcpRangeStart: null,
+ DhcpRangeEnd: null,
+ DnsmasqVersion: "2.91",
+ MinimumSupportedDnsmasqVersion: "2.91",
+ DnsmasqVersionSupported: true,
+ DnsmasqVersionError: null,
+ DnsmasqSupportsDhcp: dhcp,
+ DnsmasqSupportsTftp: tftp,
+ DnsmasqSupportsDnssec: dnssec,
+ DnsmasqSupportsDbus: dbus);
+ }
+}
diff --git a/src/DnsmasqWebUI.Tests/EffectiveConfigFieldBuilderCapabilityTests.cs b/src/DnsmasqWebUI.Tests/EffectiveConfigFieldBuilderCapabilityTests.cs
new file mode 100644
index 0000000..73f9ca7
--- /dev/null
+++ b/src/DnsmasqWebUI.Tests/EffectiveConfigFieldBuilderCapabilityTests.cs
@@ -0,0 +1,104 @@
+using DnsmasqWebUI.Infrastructure.Helpers.Config;
+using DnsmasqWebUI.Models.Dnsmasq;
+using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
+
+namespace DnsmasqWebUI.Tests;
+
+///
+/// Tests that field descriptors get IsCapabilityDisabled set when the status indicates the option is not supported by the dnsmasq build.
+///
+public class EffectiveConfigFieldBuilderCapabilityTests
+{
+ [Fact]
+ public void GetSingleDescriptor_WhenDnssecOptionAndStatusHasNoDnssec_ReturnsDescriptorWithCapabilityDisabled()
+ {
+ var status = CreateStatusWithCapabilities(dnssec: false);
+ var descriptor = EffectiveConfigFieldBuilder.GetSingleDescriptor(
+ null,
+ DnsmasqConfKeys.DnssecCheckUnsigned,
+ status,
+ _ => null,
+ _ => null,
+ null);
+
+ Assert.True(descriptor.IsCapabilityDisabled);
+ Assert.NotNull(descriptor.CapabilityDisabledReason);
+ Assert.Contains("DNSSEC", descriptor.CapabilityDisabledReason, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void GetSingleDescriptor_WhenDnssecOptionAndStatusHasDnssec_ReturnsDescriptorWithCapabilityNotDisabled()
+ {
+ var status = CreateStatusWithCapabilities(dnssec: true);
+ var descriptor = EffectiveConfigFieldBuilder.GetSingleDescriptor(
+ null,
+ DnsmasqConfKeys.DnssecCheckUnsigned,
+ status,
+ _ => null,
+ _ => null,
+ null);
+
+ Assert.False(descriptor.IsCapabilityDisabled);
+ Assert.Null(descriptor.CapabilityDisabledReason);
+ }
+
+ [Fact]
+ public void GetSingleDescriptor_WhenOptionWithNoFeatureRequirement_ReturnsDescriptorWithCapabilityNotDisabled()
+ {
+ var status = CreateStatusWithCapabilities(dnssec: false);
+ var descriptor = EffectiveConfigFieldBuilder.GetSingleDescriptor(
+ null,
+ DnsmasqConfKeys.Port,
+ status,
+ _ => null,
+ _ => null,
+ null);
+
+ Assert.False(descriptor.IsCapabilityDisabled);
+ Assert.Null(descriptor.CapabilityDisabledReason);
+ }
+
+ private static DnsmasqServiceStatus CreateStatusWithCapabilities(bool dhcp = true, bool tftp = true, bool dnssec = true, bool dbus = false)
+ {
+ return new DnsmasqServiceStatus(
+ SystemHostsPath: null,
+ SystemHostsPathExists: false,
+ ManagedHostsFilePath: null,
+ ManagedHostsPathExists: false,
+ NoHosts: false,
+ AddnHostsPaths: Array.Empty(),
+ EffectiveConfig: null,
+ EffectiveConfigSources: null,
+ MainConfigPath: null,
+ ManagedFilePath: null,
+ LeasesPath: null,
+ MainConfigPathExists: false,
+ ManagedFilePathExists: false,
+ LeasesPathConfigured: false,
+ LeasesPathExists: false,
+ ConfigFiles: null,
+ ReloadCommandConfigured: false,
+ StatusCommandConfigured: false,
+ StatusShowConfigured: false,
+ LogsConfigured: false,
+ LogsPath: null,
+ StatusShowCommand: null,
+ LogsCommand: null,
+ DnsmasqStatus: "active",
+ StatusCommandExitCode: null,
+ StatusCommandStdout: null,
+ StatusCommandStderr: null,
+ StatusShowOutput: null,
+ LogsOutput: null,
+ DhcpRangeStart: null,
+ DhcpRangeEnd: null,
+ DnsmasqVersion: "2.91",
+ MinimumSupportedDnsmasqVersion: "2.91",
+ DnsmasqVersionSupported: true,
+ DnsmasqVersionError: null,
+ DnsmasqSupportsDhcp: dhcp,
+ DnsmasqSupportsTftp: tftp,
+ DnsmasqSupportsDnssec: dnssec,
+ DnsmasqSupportsDbus: dbus);
+ }
+}
diff --git a/src/DnsmasqWebUI.Tests/EffectiveDnsmasqConfigTests.cs b/src/DnsmasqWebUI.Tests/EffectiveDnsmasqConfigTests.cs
index 8b2fdf2..ab39f19 100644
--- a/src/DnsmasqWebUI.Tests/EffectiveDnsmasqConfigTests.cs
+++ b/src/DnsmasqWebUI.Tests/EffectiveDnsmasqConfigTests.cs
@@ -54,6 +54,7 @@ public class EffectiveDnsmasqConfigTests
DhcpHostsfilePaths: Array.Empty(),
DhcpOptsfilePaths: Array.Empty(),
DhcpHostsdirPaths: Array.Empty(),
+ DhcpOptsdirPaths: Array.Empty(),
ConnmarkAllowlistValues: Array.Empty(),
CaaRecordValues: Array.Empty(),
DnsRrValues: Array.Empty(),
@@ -97,7 +98,7 @@ public class EffectiveDnsmasqConfigTests
TftpNoFail: false,
TftpNoBlocksize: false,
Dnssec: false,
- DnssecCheckUnsigned: false,
+ DnssecCheckUnsigned: null,
ReadEthers: false,
DhcpRapidCommit: false,
Localmx: false,
@@ -107,16 +108,16 @@ public class EffectiveDnsmasqConfigTests
KeepInForeground: false,
NoDaemon: false,
ProxyDnssec: false,
- ConnmarkAllowlistEnable: false,
+ ConnmarkAllowlistEnable: null,
NoRoundRobin: false,
DnssecNoTimecheck: false,
DnssecDebug: false,
- Leasequery: false,
- DhcpGenerateNames: false,
- DhcpBroadcast: false,
+ LeasequeryValues: Array.Empty(),
+ DhcpGenerateNames: null,
+ DhcpBroadcast: null,
DhcpSequentialIp: false,
DhcpIgnoreClid: false,
- BootpDynamic: false,
+ BootpDynamic: null,
NoPing: false,
ScriptArp: false,
ScriptOnRenewal: false,
diff --git a/src/DnsmasqWebUI.Tests/SpecialOptionValidatorsTests.cs b/src/DnsmasqWebUI.Tests/SpecialOptionValidatorsTests.cs
new file mode 100644
index 0000000..b502b49
--- /dev/null
+++ b/src/DnsmasqWebUI.Tests/SpecialOptionValidatorsTests.cs
@@ -0,0 +1,48 @@
+using DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
+using Xunit;
+
+namespace DnsmasqWebUI.Tests;
+
+///
+/// Validation tests for SpecialOptionValidators, including malformed numeric and structured values.
+///
+public class SpecialOptionValidatorsTests
+{
+ [Theory]
+ [InlineData("", true)]
+ [InlineData("0xff", true)]
+ [InlineData("255", true)]
+ [InlineData("0", true)]
+ [InlineData("0xZZ", false)]
+ [InlineData("-1", false)]
+ [InlineData("not-a-number", false)]
+ public void ValidateConnmarkAllowlistEnable_AcceptsValid_RejectsInvalid(string input, bool valid)
+ {
+ var err = SpecialOptionValidators.ValidateConnmarkAllowlistEnable(input);
+ Assert.Equal(valid, err is null);
+ }
+
+ [Theory]
+ [InlineData("", true)]
+ [InlineData("no", true)]
+ [InlineData("yes", false)]
+ [InlineData("0", false)]
+ public void ValidateDnssecCheckUnsigned_AcceptsValid_RejectsInvalid(string input, bool valid)
+ {
+ var err = SpecialOptionValidators.ValidateDnssecCheckUnsigned(input);
+ Assert.Equal(valid, err is null);
+ }
+
+ [Theory]
+ [InlineData("", true)]
+ [InlineData("10.0.0.1", true)]
+ [InlineData("10.0.0.0/24", true)]
+ [InlineData("::1", true)]
+ [InlineData("invalid", false)]
+ [InlineData("10.0.0.0/abc", false)]
+ public void ValidateLeasequeryValue_AcceptsValid_RejectsInvalid(string? input, bool valid)
+ {
+ var err = SpecialOptionValidators.ValidateLeasequeryValue(input);
+ Assert.Equal(valid, err is null);
+ }
+}
diff --git a/src/DnsmasqWebUI/Components/EffectiveConfig/Fields/EffectiveConfigFieldDisplay.razor b/src/DnsmasqWebUI/Components/EffectiveConfig/Fields/EffectiveConfigFieldDisplay.razor
index a188cec..109515f 100644
--- a/src/DnsmasqWebUI/Components/EffectiveConfig/Fields/EffectiveConfigFieldDisplay.razor
+++ b/src/DnsmasqWebUI/Components/EffectiveConfig/Fields/EffectiveConfigFieldDisplay.razor
@@ -13,8 +13,8 @@
{
var labelTooltip = DnsmasqOptionTooltips.Get(Descriptor.OptionName);
var isReadonly = Descriptor.GetSource()?.IsReadOnly == true;
- var showEditableBadge = !isReadonly;
- var isActiveEditor = IsEditMode && IsActive;
+ var showEditableBadge = !isReadonly && !Descriptor.IsCapabilityDisabled;
+ var isActiveEditor = IsEditMode && IsActive && !Descriptor.IsCapabilityDisabled;
var fragmentWithCallback = isActiveEditor ? _fragmentWithCallback : null;
var effectiveValue = _effectiveValue;
@@ -30,7 +30,7 @@
-
+
}
else
@@ -38,7 +38,7 @@
@fragment(Descriptor)
-
+
}
@@ -49,17 +49,18 @@ else if (Descriptor.IsMultiValue)
if (customMulti is not null)
{
@* Custom multi editor (e.g. server): same row shell as generic multi, value slot uses custom fragment. *@
-
+
@Descriptor.OptionName:
-
+
@customMulti(Descriptor)
}
@@ -67,8 +68,8 @@ else if (Descriptor.IsMultiValue)
{
@* Multi-value state is computed in OnParametersSet and in HandleMultiValueChanged; we only read cached fields here. *@
}
}
@@ -78,7 +79,8 @@ else
var displayValue = value == null ? "(not set)" : (value is IReadOnlyList list ? (list.Count == 0 ? "(none)" : string.Join(", ", list)) : value.ToString());
var isReadonly = Descriptor.GetSource()?.IsReadOnly == true;
var showReadonlyHint = IsEditMode && isReadonly;
- var isActiveEditor = IsEditMode && IsActive;
+ var isActiveEditor = IsEditMode && IsActive && !Descriptor.IsCapabilityDisabled;
+ var showEditableForDefault = !Descriptor.IsCapabilityDisabled;
@if (showReadonlyHint)
{
@@ -90,8 +92,9 @@ else
{
}
diff --git a/src/DnsmasqWebUI/Components/EffectiveConfig/Fields/EffectiveConfigMultiValueRow.razor b/src/DnsmasqWebUI/Components/EffectiveConfig/Fields/EffectiveConfigMultiValueRow.razor
index bdbb251..de503a4 100644
--- a/src/DnsmasqWebUI/Components/EffectiveConfig/Fields/EffectiveConfigMultiValueRow.razor
+++ b/src/DnsmasqWebUI/Components/EffectiveConfig/Fields/EffectiveConfigMultiValueRow.razor
@@ -19,6 +19,7 @@
}
else
@@ -37,8 +38,8 @@
else
{
(none)
-
+
}
}
diff --git a/src/DnsmasqWebUI/Components/EffectiveConfig/Fields/EffectiveConfigOptionItem.razor b/src/DnsmasqWebUI/Components/EffectiveConfig/Fields/EffectiveConfigOptionItem.razor
index b538559..3d19ba8 100644
--- a/src/DnsmasqWebUI/Components/EffectiveConfig/Fields/EffectiveConfigOptionItem.razor
+++ b/src/DnsmasqWebUI/Components/EffectiveConfig/Fields/EffectiveConfigOptionItem.razor
@@ -3,18 +3,18 @@
@using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig
@Label:
-@if (DisplayMode == EffectiveConfigDisplayMode.Edit && IsActive && Source?.IsReadOnly != true)
+@if (DisplayMode == EffectiveConfigDisplayMode.Edit && IsActive && Source?.IsReadOnly != true && !IsCapabilityDisabled)
{
-
+
}
else
{
@DisplayValue
-
+
}
@@ -38,6 +38,10 @@ else
[Parameter] public string FieldKey { get; set; } = "";
/// When true, add ec-field-editing class and show input when DisplayMode is Edit.
[Parameter] public bool IsActive { get; set; }
+ /// When true, option is disabled because dnsmasq build does not support it; show disabled badge and no editor.
+ [Parameter] public bool IsCapabilityDisabled { get; set; }
+ /// Tooltip when is true.
+ [Parameter] public string? CapabilityDisabledReason { get; set; }
/// When Edit and not readonly: raised when the user changes the value. Not yet wired to save.
[Parameter] public EventCallback DisplayValueChanged { get; set; }
/// When Edit and not readonly: raised when the input loses focus; argument is the current value.
diff --git a/src/DnsmasqWebUI/Components/EffectiveConfig/Popovers/OptionStateBadge.razor b/src/DnsmasqWebUI/Components/EffectiveConfig/Popovers/OptionStateBadge.razor
index 5ee9408..76bcb74 100644
--- a/src/DnsmasqWebUI/Components/EffectiveConfig/Popovers/OptionStateBadge.razor
+++ b/src/DnsmasqWebUI/Components/EffectiveConfig/Popovers/OptionStateBadge.razor
@@ -2,7 +2,14 @@
@using DnsmasqWebUI.Models.Dnsmasq
@using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig
-@if (Source?.IsReadOnly == true)
+@if (IsCapabilityDisabled)
+{
+
+}
+else if (Source?.IsReadOnly == true)
{
var readonlyTitle = !string.IsNullOrEmpty(ReadonlyTitleOverride) ? ReadonlyTitleOverride : Source?.GetReadOnlyTooltip();
if (ReadonlyClickActivatesField && Ui != null && !string.IsNullOrEmpty(FieldKey))
@@ -85,6 +92,10 @@ else if (ShowEditableBadge)
[Parameter] public bool ReadonlyClickActivatesField { get; set; }
/// Optional tooltip for readonly badge when is true (e.g. "New values can still be added. Click to edit.").
[Parameter] public string? ReadonlyTitleOverride { get; set; }
+ /// When true, option is disabled because dnsmasq build does not support it (e.g. no DNSSEC).
+ [Parameter] public bool IsCapabilityDisabled { get; set; }
+ /// Tooltip shown when is true (e.g. "This dnsmasq build does not support DNSSEC; option not written.").
+ [Parameter] public string? CapabilityDisabledReason { get; set; }
[CascadingParameter] public EffectiveConfigUiContext? Ui { get; set; }
private async Task OnConfirmClick()
diff --git a/src/DnsmasqWebUI/Controllers/StatusController.cs b/src/DnsmasqWebUI/Controllers/StatusController.cs
index 6638a51..f95af13 100644
--- a/src/DnsmasqWebUI/Controllers/StatusController.cs
+++ b/src/DnsmasqWebUI/Controllers/StatusController.cs
@@ -110,7 +110,11 @@ public class StatusController : ControllerBase
DnsmasqVersion: versionInfo.InstalledVersion?.ToString(),
MinimumSupportedDnsmasqVersion: versionInfo.MinimumVersion.ToString(),
DnsmasqVersionSupported: versionInfo.IsSupported,
- DnsmasqVersionError: versionInfo.Error
+ DnsmasqVersionError: versionInfo.Error,
+ DnsmasqSupportsDhcp: versionInfo.Capabilities.Dhcp,
+ DnsmasqSupportsTftp: versionInfo.Capabilities.Tftp,
+ DnsmasqSupportsDnssec: versionInfo.Capabilities.Dnssec,
+ DnsmasqSupportsDbus: versionInfo.Capabilities.Dbus
);
return Ok(status);
}
diff --git a/src/DnsmasqWebUI/Infrastructure/Helpers/Config/DnsmasqConfKeys.cs b/src/DnsmasqWebUI/Infrastructure/Helpers/Config/DnsmasqConfKeys.cs
index 87e6bf1..f784a13 100644
--- a/src/DnsmasqWebUI/Infrastructure/Helpers/Config/DnsmasqConfKeys.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Helpers/Config/DnsmasqConfKeys.cs
@@ -25,6 +25,7 @@ public static class DnsmasqConfKeys
public const string DhcpHostsfile = "dhcp-hostsfile";
public const string DhcpOptsfile = "dhcp-optsfile";
public const string DhcpHostsdir = "dhcp-hostsdir";
+ public const string DhcpOptsdir = "dhcp-optsdir";
// --- Multi-value (ARG_DUP) ---
public const string Server = "server";
diff --git a/src/DnsmasqWebUI/Infrastructure/Helpers/Config/DnsmasqOptionTooltips.cs b/src/DnsmasqWebUI/Infrastructure/Helpers/Config/DnsmasqOptionTooltips.cs
index dae5437..3f79b3a 100644
--- a/src/DnsmasqWebUI/Infrastructure/Helpers/Config/DnsmasqOptionTooltips.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Helpers/Config/DnsmasqOptionTooltips.cs
@@ -118,6 +118,7 @@ public static class DnsmasqOptionTooltips
[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.DhcpOptsdir] = "Directory of files read like dhcp-optsfile (watched for changes). Can repeat.",
[DnsmasqConfKeys.DhcpBoot] = "Boot file and server for PXE. Can repeat.",
[DnsmasqConfKeys.DhcpIgnore] = "Ignore DHCP clients (e.g. by tag). Can repeat.",
[DnsmasqConfKeys.DhcpVendorclass] = "Match by DHCP vendor class. Can repeat.",
diff --git a/src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigFeatureRequirements.cs b/src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigFeatureRequirements.cs
new file mode 100644
index 0000000..c1a1f2b
--- /dev/null
+++ b/src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigFeatureRequirements.cs
@@ -0,0 +1,140 @@
+using DnsmasqWebUI.Models.Dnsmasq;
+
+namespace DnsmasqWebUI.Infrastructure.Helpers.Config;
+
+/// Dnsmasq compile-time feature required for certain options.
+public enum DnsmasqFeature
+{
+ Dhcp,
+ Tftp,
+ Dnssec,
+ Dbus
+}
+
+///
+/// Maps effective-config option names to the dnsmasq compile-time feature they require.
+/// Used to disable option editors and block writes when the running dnsmasq binary does not support the feature.
+///
+public static class EffectiveConfigFeatureRequirements
+{
+ private static readonly IReadOnlyDictionary RequiredByOption =
+ new Dictionary(StringComparer.Ordinal)
+ {
+ // DNSSEC
+ [DnsmasqConfKeys.Dnssec] = DnsmasqFeature.Dnssec,
+ [DnsmasqConfKeys.DnssecCheckUnsigned] = DnsmasqFeature.Dnssec,
+ [DnsmasqConfKeys.ProxyDnssec] = DnsmasqFeature.Dnssec,
+ [DnsmasqConfKeys.TrustAnchor] = DnsmasqFeature.Dnssec,
+ [DnsmasqConfKeys.DnssecNoTimecheck] = DnsmasqFeature.Dnssec,
+ [DnsmasqConfKeys.DnssecDebug] = DnsmasqFeature.Dnssec,
+ [DnsmasqConfKeys.DnssecTimestamp] = DnsmasqFeature.Dnssec,
+ [DnsmasqConfKeys.DnssecLimits] = DnsmasqFeature.Dnssec,
+ // DHCP
+ [DnsmasqConfKeys.DhcpLeasefile] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpLeaseMax] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpRange] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpHost] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpOption] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpOptionForce] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpAuthoritative] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpRapidCommit] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpGenerateNames] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpBroadcast] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpBoot] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpScript] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpHostsfile] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpOptsfile] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpHostsdir] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpOptsdir] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.Leasequery] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.LeasefileRo] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpTtl] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpMatch] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpIgnore] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpIgnoreNames] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpRelay] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpSequentialIp] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpIgnoreClid] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.BootpDynamic] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.NoPing] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.ScriptArp] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.ScriptOnRenewal] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpNoOverride] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpAlternatePort] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpDuid] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpLuascript] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpScriptuser] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpMac] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpNameMatch] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpVendorclass] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpUserclass] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.RaParam] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.Slaac] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpCircuitid] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpRemoteid] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpSubscrid] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpProxy] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpPxeVendor] = DnsmasqFeature.Dhcp,
+ [DnsmasqConfKeys.DhcpOptionPxe] = DnsmasqFeature.Dhcp,
+ // TFTP
+ [DnsmasqConfKeys.EnableTftp] = DnsmasqFeature.Tftp,
+ [DnsmasqConfKeys.TftpSecure] = DnsmasqFeature.Tftp,
+ [DnsmasqConfKeys.TftpNoFail] = DnsmasqFeature.Tftp,
+ [DnsmasqConfKeys.TftpNoBlocksize] = DnsmasqFeature.Tftp,
+ [DnsmasqConfKeys.TftpRoot] = DnsmasqFeature.Tftp,
+ [DnsmasqConfKeys.PxePrompt] = DnsmasqFeature.Tftp,
+ [DnsmasqConfKeys.PxeService] = DnsmasqFeature.Tftp,
+ // DBus
+ [DnsmasqConfKeys.EnableDbus] = DnsmasqFeature.Dbus,
+ };
+
+ public static DnsmasqFeature? GetRequiredFeature(string optionName) =>
+ RequiredByOption.TryGetValue(optionName, out var f) ? f : null;
+
+ public static bool IsSupported(DnsmasqFeature feature, DnsmasqServiceStatus? status)
+ {
+ if (status == null) return true;
+ return feature switch
+ {
+ DnsmasqFeature.Dhcp => status.DnsmasqSupportsDhcp,
+ DnsmasqFeature.Tftp => status.DnsmasqSupportsTftp,
+ DnsmasqFeature.Dnssec => status.DnsmasqSupportsDnssec,
+ DnsmasqFeature.Dbus => status.DnsmasqSupportsDbus,
+ _ => true
+ };
+ }
+
+ /// User-facing tooltip when an option is disabled because the running dnsmasq was built without the required feature.
+ public static string GetUnsupportedReason(DnsmasqFeature feature) => feature switch
+ {
+ DnsmasqFeature.Dhcp => "Your dnsmasq was built without DHCP support, so this option can't be used. Install or build dnsmasq with DHCP enabled to change it.",
+ DnsmasqFeature.Tftp => "Your dnsmasq was built without TFTP support, so this option can't be used. Install or build dnsmasq with TFTP enabled to change it.",
+ DnsmasqFeature.Dnssec => "Your dnsmasq was built without DNSSEC support, so this option can't be used. Install or build dnsmasq with DNSSEC enabled to change it.",
+ DnsmasqFeature.Dbus => "Your dnsmasq was built without DBus support, so this option can't be used. Install or build dnsmasq with DBus enabled to change it.",
+ _ => "This option isn't available with your current dnsmasq build."
+ };
+
+ /// Returns (isDisabled, reason) for the option given current status. Used to set descriptor capability flags.
+ public static (bool IsDisabled, string? Reason) GetCapabilityDisabled(string optionName, DnsmasqServiceStatus? status)
+ {
+ var feature = GetRequiredFeature(optionName);
+ if (feature == null || status == null) return (false, null);
+ if (IsSupported(feature.Value, status)) return (false, null);
+ return (true, GetUnsupportedReason(feature.Value));
+ }
+
+ /// True when the option is supported by the given compile-time capabilities (for save-path guard).
+ public static bool IsSupportedByCapabilities(string optionName, DnsmasqCompileCapabilities capabilities)
+ {
+ var feature = GetRequiredFeature(optionName);
+ if (feature == null) return true;
+ return feature.Value switch
+ {
+ DnsmasqFeature.Dhcp => capabilities.Dhcp,
+ DnsmasqFeature.Tftp => capabilities.Tftp,
+ DnsmasqFeature.Dnssec => capabilities.Dnssec,
+ DnsmasqFeature.Dbus => capabilities.Dbus,
+ _ => true
+ };
+ }
+}
diff --git a/src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigFieldBuilder.cs b/src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigFieldBuilder.cs
index f68c24d..b4cdfce 100644
--- a/src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigFieldBuilder.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigFieldBuilder.cs
@@ -44,7 +44,12 @@ public static class EffectiveConfigFieldBuilder
return Array.Empty();
var list = new List();
+ AddAllDescriptors(list, status, registry);
+ return list;
+ }
+ private static void AddAllDescriptors(List list, DnsmasqServiceStatus? status, IEffectiveConfigRenderFragmentRegistry? registry)
+ {
// Hosts
list.AddDescriptor(registry, DnsmasqConfKeys.NoHosts, status, s => Config(s)?.NoHosts, s => Sources(s)?.NoHosts, null);
list.AddDescriptor(registry, DnsmasqConfKeys.AddnHosts, status, null, null, s => ToValueWithSourceList(s?.AddnHostsPaths, Sources(s)?.AddnHostsPaths));
@@ -119,7 +124,7 @@ public static class EffectiveConfigFieldBuilder
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpAuthoritative, status, s => Config(s)?.DhcpAuthoritative, s => Sources(s)?.DhcpAuthoritative, null);
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpRapidCommit, status, s => Config(s)?.DhcpRapidCommit, s => Sources(s)?.DhcpRapidCommit, null);
list.AddDescriptor(registry, DnsmasqConfKeys.LeasefileRo, status, s => Config(s)?.LeasefileRo, s => Sources(s)?.LeasefileRo, null);
- list.AddDescriptor(registry, DnsmasqConfKeys.Leasequery, status, s => Config(s)?.Leasequery, s => Sources(s)?.Leasequery, null);
+ list.AddDescriptor(registry, DnsmasqConfKeys.Leasequery, status, null, null, Items(ec => ec?.LeasequeryValues, src => src?.LeasequeryValues));
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpGenerateNames, status, s => Config(s)?.DhcpGenerateNames, s => Sources(s)?.DhcpGenerateNames, null);
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpBroadcast, status, s => Config(s)?.DhcpBroadcast, s => Sources(s)?.DhcpBroadcast, null);
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpSequentialIp, status, s => Config(s)?.DhcpSequentialIp, s => Sources(s)?.DhcpSequentialIp, null);
@@ -148,6 +153,7 @@ public static class EffectiveConfigFieldBuilder
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpHostsfile, status, null, null, Items(ec => ec?.DhcpHostsfilePaths, src => src?.DhcpHostsfilePaths));
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpOptsfile, status, null, null, Items(ec => ec?.DhcpOptsfilePaths, src => src?.DhcpOptsfilePaths));
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpHostsdir, status, null, null, Items(ec => ec?.DhcpHostsdirPaths, src => src?.DhcpHostsdirPaths));
+ list.AddDescriptor(registry, DnsmasqConfKeys.DhcpOptsdir, status, null, null, Items(ec => ec?.DhcpOptsdirPaths, src => src?.DhcpOptsdirPaths));
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpRelay, status, null, null, Items(ec => ec?.DhcpRelayValues, src => src?.DhcpRelayValues));
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpCircuitid, status, null, null, Items(ec => ec?.DhcpCircuitidValues, src => src?.DhcpCircuitidValues));
list.AddDescriptor(registry, DnsmasqConfKeys.DhcpRemoteid, status, null, null, Items(ec => ec?.DhcpRemoteidValues, src => src?.DhcpRemoteidValues));
@@ -232,8 +238,6 @@ public static class EffectiveConfigFieldBuilder
list.AddDescriptor(registry, DnsmasqConfKeys.QuietDhcp6, status, s => Config(s)?.QuietDhcp6, s => Sources(s)?.QuietDhcp6, null);
list.AddDescriptor(registry, DnsmasqConfKeys.QuietRa, status, s => Config(s)?.QuietRa, s => Sources(s)?.QuietRa, null);
list.AddDescriptor(registry, DnsmasqConfKeys.QuietTftp, status, s => Config(s)?.QuietTftp, s => Sources(s)?.QuietTftp, null);
-
- return list;
}
/// Creates a single-value descriptor; uses registry factory when registered (e.g. integer), otherwise a plain descriptor. Used by .
@@ -247,9 +251,11 @@ public static class EffectiveConfigFieldBuilder
{
var sectionId = EffectiveConfigSections.GetSectionId(optionName);
var factory = registry?.GetDescriptorFactory(sectionId, optionName);
- if (factory != null)
- return (EffectiveConfigFieldDescriptor)factory(sectionId, optionName, status, getValue, getSource, getItems);
- return Single(optionName, status, getValue, getSource, getItems);
+ var descriptor = factory != null
+ ? (EffectiveConfigFieldDescriptor)factory(sectionId, optionName, status, getValue, getSource, getItems)
+ : Single(optionName, status, getValue, getSource, getItems);
+ var (isDisabled, reason) = EffectiveConfigFeatureRequirements.GetCapabilityDisabled(optionName, status);
+ return descriptor with { IsCapabilityDisabled = isDisabled, CapabilityDisabledReason = reason };
}
private static EffectiveConfigFieldDescriptor Single(string optionName, DnsmasqServiceStatus? status,
diff --git a/src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigFieldDescriptorListExtensions.cs b/src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigFieldDescriptorListExtensions.cs
index c48ec11..955b47d 100644
--- a/src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigFieldDescriptorListExtensions.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigFieldDescriptorListExtensions.cs
@@ -55,6 +55,8 @@ public static class EffectiveConfigFieldDescriptorListExtensions
var sectionId = EffectiveConfigSections.GetSectionId(optionName);
var factory = registry?.GetMultiDescriptorFactory(sectionId, optionName)
?? throw new InvalidOperationException($"No multi descriptor factory registered for option '{optionName}' (section '{sectionId}'). Register all multi options via RegisterMultiDescriptor.");
- list.Add(factory(sectionId, optionName, status, getItems));
+ var descriptor = factory(sectionId, optionName, status, getItems);
+ var (isDisabled, reason) = EffectiveConfigFeatureRequirements.GetCapabilityDisabled(optionName, status);
+ list.Add(descriptor with { IsCapabilityDisabled = isDisabled, CapabilityDisabledReason = reason });
}
}
diff --git a/src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigOptionKindMap.cs b/src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigOptionKindMap.cs
index f25bfd6..a460d68 100644
--- a/src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigOptionKindMap.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigOptionKindMap.cs
@@ -102,6 +102,7 @@ public static class EffectiveConfigOptionKindMap
[DnsmasqConfKeys.DhcpHostsfile] = EffectiveConfigFieldKind.Multi,
[DnsmasqConfKeys.DhcpOptsfile] = EffectiveConfigFieldKind.Multi,
[DnsmasqConfKeys.DhcpHostsdir] = EffectiveConfigFieldKind.Multi,
+ [DnsmasqConfKeys.DhcpOptsdir] = EffectiveConfigFieldKind.Multi,
[DnsmasqConfKeys.DhcpBoot] = EffectiveConfigFieldKind.Multi,
[DnsmasqConfKeys.DhcpIgnore] = EffectiveConfigFieldKind.Multi,
[DnsmasqConfKeys.DhcpVendorclass] = EffectiveConfigFieldKind.Multi,
@@ -155,7 +156,7 @@ public static class EffectiveConfigOptionKindMap
[DnsmasqConfKeys.NoRoundRobin] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.DnssecNoTimecheck] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.DnssecDebug] = EffectiveConfigFieldKind.Single,
- [DnsmasqConfKeys.Leasequery] = EffectiveConfigFieldKind.Single,
+ [DnsmasqConfKeys.Leasequery] = EffectiveConfigFieldKind.Multi,
[DnsmasqConfKeys.DhcpGenerateNames] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.DhcpBroadcast] = EffectiveConfigFieldKind.Single,
[DnsmasqConfKeys.DhcpSequentialIp] = EffectiveConfigFieldKind.Single,
@@ -291,6 +292,7 @@ public static class EffectiveConfigParserBehaviorMap
[DnsmasqConfKeys.DhcpHostsfile] = EffectiveConfigParserBehavior.Multi,
[DnsmasqConfKeys.DhcpOptsfile] = EffectiveConfigParserBehavior.Multi,
[DnsmasqConfKeys.DhcpHostsdir] = EffectiveConfigParserBehavior.Multi,
+ [DnsmasqConfKeys.DhcpOptsdir] = EffectiveConfigParserBehavior.Multi,
[DnsmasqConfKeys.DhcpBoot] = EffectiveConfigParserBehavior.Multi,
[DnsmasqConfKeys.DhcpIgnore] = EffectiveConfigParserBehavior.Multi,
[DnsmasqConfKeys.DhcpVendorclass] = EffectiveConfigParserBehavior.Multi,
@@ -305,7 +307,7 @@ public static class EffectiveConfigParserBehaviorMap
[DnsmasqConfKeys.PxePrompt] = EffectiveConfigParserBehavior.LastWins,
[DnsmasqConfKeys.PxeService] = EffectiveConfigParserBehavior.Multi,
[DnsmasqConfKeys.Dnssec] = EffectiveConfigParserBehavior.Flag,
- [DnsmasqConfKeys.DnssecCheckUnsigned] = EffectiveConfigParserBehavior.Flag,
+ [DnsmasqConfKeys.DnssecCheckUnsigned] = EffectiveConfigParserBehavior.LastWins,
[DnsmasqConfKeys.ProxyDnssec] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.TrustAnchor] = EffectiveConfigParserBehavior.Multi,
[DnsmasqConfKeys.CacheRr] = EffectiveConfigParserBehavior.Multi,
@@ -340,16 +342,16 @@ public static class EffectiveConfigParserBehaviorMap
[DnsmasqConfKeys.KeepInForeground] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.NoDaemon] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.Conntrack] = EffectiveConfigParserBehavior.Flag,
- [DnsmasqConfKeys.ConnmarkAllowlistEnable] = EffectiveConfigParserBehavior.Flag,
+ [DnsmasqConfKeys.ConnmarkAllowlistEnable] = EffectiveConfigParserBehavior.LastWins,
[DnsmasqConfKeys.NoRoundRobin] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.DnssecNoTimecheck] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.DnssecDebug] = EffectiveConfigParserBehavior.Flag,
- [DnsmasqConfKeys.Leasequery] = EffectiveConfigParserBehavior.Flag,
- [DnsmasqConfKeys.DhcpGenerateNames] = EffectiveConfigParserBehavior.Flag,
- [DnsmasqConfKeys.DhcpBroadcast] = EffectiveConfigParserBehavior.Flag,
+ [DnsmasqConfKeys.Leasequery] = EffectiveConfigParserBehavior.Multi,
+ [DnsmasqConfKeys.DhcpGenerateNames] = EffectiveConfigParserBehavior.LastWins,
+ [DnsmasqConfKeys.DhcpBroadcast] = EffectiveConfigParserBehavior.LastWins,
[DnsmasqConfKeys.DhcpSequentialIp] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.DhcpIgnoreClid] = EffectiveConfigParserBehavior.Flag,
- [DnsmasqConfKeys.BootpDynamic] = EffectiveConfigParserBehavior.Flag,
+ [DnsmasqConfKeys.BootpDynamic] = EffectiveConfigParserBehavior.LastWins,
[DnsmasqConfKeys.NoPing] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.ScriptArp] = EffectiveConfigParserBehavior.Flag,
[DnsmasqConfKeys.ScriptOnRenewal] = EffectiveConfigParserBehavior.Flag,
diff --git a/src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigSections.cs b/src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigSections.cs
index b064c55..4ea133a 100644
--- a/src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigSections.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigSections.cs
@@ -128,6 +128,7 @@ public static class EffectiveConfigSections
DnsmasqConfKeys.DhcpHostsfile,
DnsmasqConfKeys.DhcpOptsfile,
DnsmasqConfKeys.DhcpHostsdir,
+ DnsmasqConfKeys.DhcpOptsdir,
DnsmasqConfKeys.DhcpBoot,
DnsmasqConfKeys.DhcpIgnore,
DnsmasqConfKeys.DhcpVendorclass,
diff --git a/src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigSpecialOptionSemantics.cs b/src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigSpecialOptionSemantics.cs
index 7ae86ba..f9c6db2 100644
--- a/src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigSpecialOptionSemantics.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigSpecialOptionSemantics.cs
@@ -50,6 +50,36 @@ public static class EffectiveConfigSpecialOptionSemantics
EffectiveConfigParserBehavior.Flag,
EffectiveConfigWriteBehavior.InversePair,
Validator: null),
+ [DnsmasqConfKeys.ConnmarkAllowlistEnable] = new OptionSemantics(
+ DnsmasqConfKeys.ConnmarkAllowlistEnable,
+ EffectiveConfigParserBehavior.LastWins,
+ EffectiveConfigWriteBehavior.KeyOnlyOrValue,
+ SpecialOptionValidators.ValidateConnmarkAllowlistEnable),
+ [DnsmasqConfKeys.DnssecCheckUnsigned] = new OptionSemantics(
+ DnsmasqConfKeys.DnssecCheckUnsigned,
+ EffectiveConfigParserBehavior.LastWins,
+ EffectiveConfigWriteBehavior.KeyOnlyOrValue,
+ SpecialOptionValidators.ValidateDnssecCheckUnsigned),
+ [DnsmasqConfKeys.Leasequery] = new OptionSemantics(
+ DnsmasqConfKeys.Leasequery,
+ EffectiveConfigParserBehavior.Multi,
+ EffectiveConfigWriteBehavior.MultiKeyOnlyOrValue,
+ null),
+ [DnsmasqConfKeys.DhcpGenerateNames] = new OptionSemantics(
+ DnsmasqConfKeys.DhcpGenerateNames,
+ EffectiveConfigParserBehavior.LastWins,
+ EffectiveConfigWriteBehavior.KeyOnlyOrValue,
+ null),
+ [DnsmasqConfKeys.DhcpBroadcast] = new OptionSemantics(
+ DnsmasqConfKeys.DhcpBroadcast,
+ EffectiveConfigParserBehavior.LastWins,
+ EffectiveConfigWriteBehavior.KeyOnlyOrValue,
+ null),
+ [DnsmasqConfKeys.BootpDynamic] = new OptionSemantics(
+ DnsmasqConfKeys.BootpDynamic,
+ EffectiveConfigParserBehavior.LastWins,
+ EffectiveConfigWriteBehavior.KeyOnlyOrValue,
+ null),
};
/// Keys (enabled, disabled) for InversePair options only. Used by write path and readonly hints.
diff --git a/src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigWriteSemantics.cs b/src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigWriteSemantics.cs
index 3c5172b..af684e2 100644
--- a/src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigWriteSemantics.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigWriteSemantics.cs
@@ -10,6 +10,7 @@ public enum EffectiveConfigWriteBehavior
SingleValue,
MultiValue,
KeyOnlyOrValue,
+ MultiKeyOnlyOrValue,
InversePair,
}
diff --git a/src/DnsmasqWebUI/Infrastructure/Helpers/Dnsmasq/DnsmasqCompileOptionsParser.cs b/src/DnsmasqWebUI/Infrastructure/Helpers/Dnsmasq/DnsmasqCompileOptionsParser.cs
new file mode 100644
index 0000000..4da4bb6
--- /dev/null
+++ b/src/DnsmasqWebUI/Infrastructure/Helpers/Dnsmasq/DnsmasqCompileOptionsParser.cs
@@ -0,0 +1,37 @@
+using System.Text.RegularExpressions;
+using DnsmasqWebUI.Models.Dnsmasq;
+
+namespace DnsmasqWebUI.Infrastructure.Helpers.Dnsmasq;
+
+///
+/// Parses "Compile time options: ..." line from dnsmasq --version output
+/// to determine DHCP, TFTP, DNSSEC, DBus support.
+///
+public static class DnsmasqCompileOptionsParser
+{
+ private static readonly Regex CompileLine = new(
+ @"Compile\s+time\s+options\s*:\s*(?.+)$",
+ RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled);
+
+ public static DnsmasqCompileCapabilities Parse(string? stdout, string? stderr)
+ {
+ var text = $"{stdout}\n{stderr}";
+ var m = CompileLine.Match(text);
+ if (!m.Success)
+ return new DnsmasqCompileCapabilities(false, false, false, false, new HashSet(StringComparer.OrdinalIgnoreCase));
+
+ var tokens = m.Groups["opts"].Value
+ .Split(new[] { ' ', '\t', ',' }, StringSplitOptions.RemoveEmptyEntries)
+ .ToHashSet(StringComparer.OrdinalIgnoreCase);
+
+ bool Has(string s) => tokens.Contains(s);
+
+ return new DnsmasqCompileCapabilities(
+ Dhcp: Has("DHCP") || Has("DHCPv4") || Has("DHCPv6"),
+ Tftp: Has("TFTP"),
+ Dnssec: Has("DNSSEC"),
+ Dbus: Has("DBus"),
+ RawTokens: tokens
+ );
+ }
+}
diff --git a/src/DnsmasqWebUI/Infrastructure/Services/Dnsmasq/Config/ConfigSetCache.cs b/src/DnsmasqWebUI/Infrastructure/Services/Dnsmasq/Config/ConfigSetCache.cs
index ea6c51c..aeeb982 100644
--- a/src/DnsmasqWebUI/Infrastructure/Services/Dnsmasq/Config/ConfigSetCache.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/Dnsmasq/Config/ConfigSetCache.cs
@@ -293,6 +293,7 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
var dhcpHostsfilePaths = (IReadOnlyList)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpHostsfile);
var dhcpOptsfilePaths = (IReadOnlyList)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpOptsfile);
var dhcpHostsdirPaths = (IReadOnlyList)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpHostsdir);
+ var dhcpOptsdirPaths = (IReadOnlyList)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpOptsdir);
var connmarkAllowlistValues = (IReadOnlyList)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.ConnmarkAllowlist);
var caaRecordValues = (IReadOnlyList)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.CaaRecord);
var dnsRrValues = (IReadOnlyList)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DnsRr);
@@ -335,7 +336,8 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
var enableTftp = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.EnableTftp);
var tftpSecure = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.TftpSecure);
var dnssec = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.Dnssec);
- var dnssecCheckUnsigned = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DnssecCheckUnsigned);
+ var (dnssecCheckUnsignedVal, _) = ((string?, string?))ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DnssecCheckUnsigned);
+ var dnssecCheckUnsigned = dnssecCheckUnsignedVal == null ? null : (string.IsNullOrWhiteSpace(dnssecCheckUnsignedVal) ? "" : dnssecCheckUnsignedVal.Trim());
var readEthers = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.ReadEthers);
var dhcpRapidCommit = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpRapidCommit);
var tftpNoFail = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.TftpNoFail);
@@ -347,16 +349,20 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
var keepInForeground = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.KeepInForeground);
var noDaemon = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.NoDaemon);
var proxyDnssec = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.ProxyDnssec);
- var connmarkAllowlistEnable = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.ConnmarkAllowlistEnable);
+ var (connmarkAllowlistEnableVal, _) = ((string?, string?))ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.ConnmarkAllowlistEnable);
+ var connmarkAllowlistEnable = connmarkAllowlistEnableVal == null ? null : (string.IsNullOrWhiteSpace(connmarkAllowlistEnableVal) ? "" : connmarkAllowlistEnableVal.Trim());
var noRoundRobin = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.NoRoundRobin);
var dnssecNoTimecheck = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DnssecNoTimecheck);
var dnssecDebug = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DnssecDebug);
- var leasequery = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.Leasequery);
- var dhcpGenerateNames = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpGenerateNames);
- var dhcpBroadcast = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpBroadcast);
+ var leasequeryValues = (IReadOnlyList)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.Leasequery);
+ var (dhcpGenerateNamesVal, _) = ((string?, string?))ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpGenerateNames);
+ var dhcpGenerateNames = dhcpGenerateNamesVal == null ? null : (string.IsNullOrWhiteSpace(dhcpGenerateNamesVal) ? "" : dhcpGenerateNamesVal.Trim());
+ var (dhcpBroadcastVal, _) = ((string?, string?))ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpBroadcast);
+ var dhcpBroadcast = dhcpBroadcastVal == null ? null : (string.IsNullOrWhiteSpace(dhcpBroadcastVal) ? "" : dhcpBroadcastVal.Trim());
var dhcpSequentialIp = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpSequentialIp);
var dhcpIgnoreClid = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpIgnoreClid);
- var bootpDynamic = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.BootpDynamic);
+ var (bootpDynamicVal, _) = ((string?, string?))ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.BootpDynamic);
+ var bootpDynamic = bootpDynamicVal == null ? null : (string.IsNullOrWhiteSpace(bootpDynamicVal) ? "" : bootpDynamicVal.Trim());
var noPing = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.NoPing);
var scriptArp = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.ScriptArp);
var scriptOnRenewal = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.ScriptOnRenewal);
@@ -471,14 +477,14 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
rebindDomainOk, bogusNxdomain, ignoreAddress, alias, filterRr, cacheRr, authServer, noDhcpInterface, noDhcpv4Interface, noDhcpv6Interface,
domainValues, cnameValues, mxHostValues, srvValues, ptrRecordValues, txtRecordValues, naptrRecordValues, hostRecordValues, dynamicHostValues, interfaceNameValues,
dhcpOptionForceLines, ipsetValues, nftsetValues, dhcpMacValues, dhcpNameMatchValues, dhcpIgnoreNamesValues,
- dhcpHostsfilePaths, dhcpOptsfilePaths, dhcpHostsdirPaths,
+ dhcpHostsfilePaths, dhcpOptsfilePaths, dhcpHostsdirPaths, dhcpOptsdirPaths,
connmarkAllowlistValues, caaRecordValues, dnsRrValues, synthDomainValues, authZoneValues, authSoaValues, authSecServersValues, authPeerValues,
dhcpRelayValues, dhcpCircuitidValues, dhcpRemoteidValues, dhcpSubscridValues, dhcpProxyValues, tagIfValues, bridgeInterfaceValues, sharedNetworkValues, dhcpOptionPxeValues,
expandHosts, bogusPriv, strictOrder, allServers, noResolv, domainNeeded, noPoll, bindInterfaces, bindDynamic, noNegcache,
dnsLoopDetect, stopDnsRebind, rebindLocalhostOk, clearOnReload, filterwin2k, filterA, filterAaaa, localiseQueries, logDebug, dhcpAuthoritative, leasefileRo,
enableTftp, tftpSecure, tftpNoFail, tftpNoBlocksize, dnssec, dnssecCheckUnsigned, readEthers, dhcpRapidCommit, localmx, selfmx, enableRa, logDhcp,
keepInForeground, noDaemon, proxyDnssec,
- connmarkAllowlistEnable, noRoundRobin, dnssecNoTimecheck, dnssecDebug, leasequery, dhcpGenerateNames, dhcpBroadcast, dhcpSequentialIp, dhcpIgnoreClid, bootpDynamic, noPing, scriptArp, scriptOnRenewal, dhcpNoOverride, quietDhcp, quietDhcp6, quietRa, quietTftp,
+ connmarkAllowlistEnable, noRoundRobin, dnssecNoTimecheck, dnssecDebug, leasequeryValues, dhcpGenerateNames, dhcpBroadcast, dhcpSequentialIp, dhcpIgnoreClid, bootpDynamic, noPing, scriptArp, scriptOnRenewal, dhcpNoOverride, quietDhcp, quietDhcp6, quietRa, quietTftp,
dhcpLeaseFilePath, cacheSize, port, localTtl, pidFilePath, user, group, logFacility, logQueries,
authTtl, ednsPacketMax, queryPort, portLimit, minPort, maxPort, logAsync, localService, dhcpLeaseMax,
negTtl, maxTtl, maxCacheTtl, minCacheTtl, dhcpTtl, tftpRootPath, pxePrompt, enableDbus, enableUbus, fastDnsRetry,
@@ -541,6 +547,7 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
var dhcpHostsfileWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpHostsfile, managedFilePath);
var dhcpOptsfileWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpOptsfile, managedFilePath);
var dhcpHostsdirWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpHostsdir, managedFilePath);
+ var dhcpOptsdirWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpOptsdir, managedFilePath);
var connmarkAllowlistWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.ConnmarkAllowlist, managedFilePath);
var caaRecordWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.CaaRecord, managedFilePath);
var dnsRrWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DnsRr, managedFilePath);
@@ -583,7 +590,7 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
var (_, enableTftpSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.EnableTftp, managedFilePath);
var (_, tftpSecureSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.TftpSecure, managedFilePath);
var (_, dnssecSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.Dnssec, managedFilePath);
- var (_, dnssecCheckUnsignedSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DnssecCheckUnsigned, managedFilePath);
+ var (_, dnssecCheckUnsignedSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DnssecCheckUnsigned, managedFilePath);
var (_, readEthersSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.ReadEthers, managedFilePath);
var (_, dhcpRapidCommitSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpRapidCommit, managedFilePath);
var (_, tftpNoFailSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.TftpNoFail, managedFilePath);
@@ -595,16 +602,16 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
var (_, keepInForegroundSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.KeepInForeground, managedFilePath);
var (_, noDaemonSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.NoDaemon, managedFilePath);
var (_, proxyDnssecSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.ProxyDnssec, managedFilePath);
- var (_, connmarkAllowlistEnableSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.ConnmarkAllowlistEnable, managedFilePath);
+ var (_, connmarkAllowlistEnableSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.ConnmarkAllowlistEnable, managedFilePath);
var (_, noRoundRobinSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.NoRoundRobin, managedFilePath);
var (_, dnssecNoTimecheckSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DnssecNoTimecheck, managedFilePath);
var (_, dnssecDebugSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DnssecDebug, managedFilePath);
- var (_, leasequerySource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.Leasequery, managedFilePath);
- var (_, dhcpGenerateNamesSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpGenerateNames, managedFilePath);
- var (_, dhcpBroadcastSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpBroadcast, managedFilePath);
+ var leasequeryWithSource = (IReadOnlyList<(string Value, ConfigValueSource Source)>)ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.Leasequery, managedFilePath);
+ var (_, dhcpGenerateNamesSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpGenerateNames, managedFilePath);
+ var (_, dhcpBroadcastSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpBroadcast, managedFilePath);
var (_, dhcpSequentialIpSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpSequentialIp, managedFilePath);
var (_, dhcpIgnoreClidSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpIgnoreClid, managedFilePath);
- var (_, bootpDynamicSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.BootpDynamic, managedFilePath);
+ var (_, bootpDynamicSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.BootpDynamic, managedFilePath);
var (_, noPingSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.NoPing, managedFilePath);
var (_, scriptArpSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.ScriptArp, managedFilePath);
var (_, scriptOnRenewalSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.ScriptOnRenewal, managedFilePath);
@@ -718,6 +725,7 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
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(),
+ dhcpOptsdirWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
connmarkAllowlistWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
caaRecordWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
dnsRrWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
@@ -775,7 +783,7 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
noRoundRobinSource,
dnssecNoTimecheckSource,
dnssecDebugSource,
- leasequerySource,
+ leasequeryWithSource.Select(t => new ValueWithSource(t.Value, t.Source)).ToList(),
dhcpGenerateNamesSource,
dhcpBroadcastSource,
dhcpSequentialIpSource,
@@ -890,7 +898,7 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
HostRecordValues: Array.Empty(), DynamicHostValues: Array.Empty(), InterfaceNameValues: Array.Empty(),
DhcpOptionForceLines: Array.Empty(), IpsetValues: Array.Empty(), NftsetValues: Array.Empty(),
DhcpMacValues: Array.Empty(), DhcpNameMatchValues: Array.Empty(), DhcpIgnoreNamesValues: Array.Empty(),
- DhcpHostsfilePaths: Array.Empty(), DhcpOptsfilePaths: Array.Empty(), DhcpHostsdirPaths: Array.Empty(),
+ DhcpHostsfilePaths: Array.Empty(), DhcpOptsfilePaths: Array.Empty(), DhcpHostsdirPaths: Array.Empty(), DhcpOptsdirPaths: Array.Empty(),
ConnmarkAllowlistValues: Array.Empty(), CaaRecordValues: Array.Empty(), DnsRrValues: Array.Empty(), SynthDomainValues: Array.Empty(),
AuthZoneValues: Array.Empty(), AuthSoaValues: Array.Empty(), AuthSecServersValues: Array.Empty(), AuthPeerValues: Array.Empty(),
DhcpRelayValues: Array.Empty(), DhcpCircuitidValues: Array.Empty(), DhcpRemoteidValues: Array.Empty(), DhcpSubscridValues: Array.Empty(), DhcpProxyValues: Array.Empty(),
@@ -898,10 +906,10 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
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,
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: null,
ReadEthers: false, DhcpRapidCommit: false, Localmx: false, Selfmx: false, EnableRa: false, LogDhcp: false,
KeepInForeground: false, NoDaemon: false, ProxyDnssec: false,
- ConnmarkAllowlistEnable: false, NoRoundRobin: false, DnssecNoTimecheck: false, DnssecDebug: false, Leasequery: false, DhcpGenerateNames: false, DhcpBroadcast: false, DhcpSequentialIp: false, DhcpIgnoreClid: false, BootpDynamic: false, NoPing: false, ScriptArp: false, ScriptOnRenewal: false, DhcpNoOverride: false, QuietDhcp: false, QuietDhcp6: false, QuietRa: false, QuietTftp: false,
+ ConnmarkAllowlistEnable: null, NoRoundRobin: false, DnssecNoTimecheck: false, DnssecDebug: false, LeasequeryValues: Array.Empty(), DhcpGenerateNames: null, DhcpBroadcast: null, DhcpSequentialIp: false, DhcpIgnoreClid: false, BootpDynamic: null, NoPing: false, ScriptArp: false, ScriptOnRenewal: false, DhcpNoOverride: false, QuietDhcp: false, QuietDhcp6: false, QuietRa: false, QuietTftp: false,
DhcpLeaseFilePath: null, CacheSize: null, Port: null, LocalTtl: null, PidFilePath: null, User: null, Group: null,
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,
@@ -931,7 +939,7 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
HostRecordValues: Array.Empty(), DynamicHostValues: Array.Empty(), InterfaceNameValues: Array.Empty(),
DhcpOptionForceLines: Array.Empty(), IpsetValues: Array.Empty(), NftsetValues: Array.Empty(),
DhcpMacValues: Array.Empty(), DhcpNameMatchValues: Array.Empty(), DhcpIgnoreNamesValues: Array.Empty(),
- DhcpHostsfilePaths: Array.Empty(), DhcpOptsfilePaths: Array.Empty(), DhcpHostsdirPaths: Array.Empty(),
+ DhcpHostsfilePaths: Array.Empty(), DhcpOptsfilePaths: Array.Empty(), DhcpHostsdirPaths: Array.Empty(), DhcpOptsdirPaths: Array.Empty(),
ConnmarkAllowlistValues: Array.Empty(), CaaRecordValues: Array.Empty(), DnsRrValues: Array.Empty(), SynthDomainValues: Array.Empty(),
AuthZoneValues: Array.Empty(), AuthSoaValues: Array.Empty(), AuthSecServersValues: Array.Empty(), AuthPeerValues: Array.Empty(),
DhcpRelayValues: Array.Empty(), DhcpCircuitidValues: Array.Empty(), DhcpRemoteidValues: Array.Empty(), DhcpSubscridValues: Array.Empty(), DhcpProxyValues: Array.Empty(),
@@ -942,7 +950,7 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
EnableTftp: null, TftpSecure: null, TftpNoFail: null, TftpNoBlocksize: null, Dnssec: null, DnssecCheckUnsigned: null,
ReadEthers: null, DhcpRapidCommit: null, Localmx: null, Selfmx: null, EnableRa: null, LogDhcp: null,
KeepInForeground: null, NoDaemon: null, ProxyDnssec: null,
- ConnmarkAllowlistEnable: null, NoRoundRobin: null, DnssecNoTimecheck: null, DnssecDebug: null, Leasequery: null, DhcpGenerateNames: null, DhcpBroadcast: null, DhcpSequentialIp: null, DhcpIgnoreClid: null, BootpDynamic: null, NoPing: null, ScriptArp: null, ScriptOnRenewal: null, DhcpNoOverride: null, QuietDhcp: null, QuietDhcp6: null, QuietRa: null, QuietTftp: null,
+ ConnmarkAllowlistEnable: null, NoRoundRobin: null, DnssecNoTimecheck: null, DnssecDebug: null, LeasequeryValues: Array.Empty(), DhcpGenerateNames: null, DhcpBroadcast: null, DhcpSequentialIp: null, DhcpIgnoreClid: null, BootpDynamic: null, NoPing: null, ScriptArp: null, ScriptOnRenewal: null, DhcpNoOverride: null, QuietDhcp: null, QuietDhcp6: null, QuietRa: null, QuietTftp: null,
DhcpLeaseFilePath: null, CacheSize: null, Port: null, LocalTtl: null, PidFilePath: null, User: null, Group: null,
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,
diff --git a/src/DnsmasqWebUI/Infrastructure/Services/Dnsmasq/Config/DnsmasqConfigService.cs b/src/DnsmasqWebUI/Infrastructure/Services/Dnsmasq/Config/DnsmasqConfigService.cs
index 279c55f..64d29fc 100644
--- a/src/DnsmasqWebUI/Infrastructure/Services/Dnsmasq/Config/DnsmasqConfigService.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/Dnsmasq/Config/DnsmasqConfigService.cs
@@ -253,6 +253,19 @@ public class DnsmasqConfigService : IDnsmasqConfigService
continue;
}
+ if (writeBehavior == EffectiveConfigWriteBehavior.MultiKeyOnlyOrValue && TryGetMultiValues(c.NewValue, out var multiKeyOnlyValues))
+ {
+ IReadOnlyList readonlyValues = readonlyByOption.TryGetValue(confKey, out var listValues) ? listValues : Array.Empty();
+ var valuesToWrite = FilterManagedOnly(multiKeyOnlyValues, readonlyValues);
+ RemoveAllMatchingLines(list, MatchesOption);
+ foreach (var v in valuesToWrite)
+ {
+ var raw = string.IsNullOrEmpty(v) ? confKey : $"{confKey}={v.Trim()}";
+ list.Add(new OtherLine { LineNumber = ++maxLineNumber, RawLine = raw });
+ }
+ continue;
+ }
+
if (writeBehavior == EffectiveConfigWriteBehavior.InversePair)
{
var pair = EffectiveConfigWriteSemantics.GetInversePairKeys(confKey);
diff --git a/src/DnsmasqWebUI/Infrastructure/Services/Dnsmasq/Version/DnsmasqVersionService.cs b/src/DnsmasqWebUI/Infrastructure/Services/Dnsmasq/Version/DnsmasqVersionService.cs
index e8feb4c..6479e40 100644
--- a/src/DnsmasqWebUI/Infrastructure/Services/Dnsmasq/Version/DnsmasqVersionService.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/Dnsmasq/Version/DnsmasqVersionService.cs
@@ -29,13 +29,15 @@ public sealed class DnsmasqVersionService : IDnsmasqVersionService
if (string.IsNullOrWhiteSpace(command))
{
+ var emptyCaps = new DnsmasqCompileCapabilities(false, false, false, false, new HashSet(StringComparer.OrdinalIgnoreCase));
return new DnsmasqVersionInfo(
InstalledVersion: null,
MinimumVersion: minimumVersion,
ProbeSucceeded: false,
IsSupported: false,
ProbeCommand: "",
- Error: "Version command is not configured.");
+ Error: "Version command is not configured.",
+ Capabilities: emptyCaps);
}
var result = await _processRunner.RunAsync(command, _options.VersionTimeout, ct);
@@ -52,6 +54,7 @@ public sealed class DnsmasqVersionService : IDnsmasqVersionService
var probeSucceeded = installed != null;
var isSupported = probeSucceeded && installed!.CompareTo(minimumVersion) >= 0;
+ var capabilities = DnsmasqCompileOptionsParser.Parse(result.Stdout, result.Stderr);
return new DnsmasqVersionInfo(
InstalledVersion: installed,
@@ -59,6 +62,7 @@ public sealed class DnsmasqVersionService : IDnsmasqVersionService
ProbeSucceeded: probeSucceeded,
IsSupported: isSupported,
ProbeCommand: command,
- Error: error);
+ Error: error,
+ Capabilities: capabilities);
}
}
diff --git a/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/EffectiveConfigRenderFragmentRegistry.cs b/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/EffectiveConfigRenderFragmentRegistry.cs
index b9c119b..54c35f6 100644
--- a/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/EffectiveConfigRenderFragmentRegistry.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/EffectiveConfigRenderFragmentRegistry.cs
@@ -76,23 +76,17 @@ public class EffectiveConfigRenderFragmentRegistry : IEffectiveConfigRenderFragm
RegisterFlag(EffectiveConfigFieldBuilder.SectionDnsRecords, DnsmasqConfKeys.Localmx);
RegisterFlag(EffectiveConfigFieldBuilder.SectionDnsRecords, DnsmasqConfKeys.Selfmx);
RegisterFlag(EffectiveConfigFieldBuilder.SectionDnssec, DnsmasqConfKeys.Dnssec);
- RegisterFlag(EffectiveConfigFieldBuilder.SectionDnssec, DnsmasqConfKeys.DnssecCheckUnsigned);
RegisterFlag(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.EnableRa);
RegisterFlag(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.LogDhcp);
RegisterFlag(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.KeepInForeground);
RegisterFlag(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.NoDaemon);
RegisterFlag(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.Conntrack);
RegisterFlag(EffectiveConfigFieldBuilder.SectionDnssec, DnsmasqConfKeys.ProxyDnssec);
- RegisterFlag(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.ConnmarkAllowlistEnable);
RegisterFlag(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.NoRoundRobin);
RegisterFlag(EffectiveConfigFieldBuilder.SectionDnssec, DnsmasqConfKeys.DnssecNoTimecheck);
RegisterFlag(EffectiveConfigFieldBuilder.SectionDnssec, DnsmasqConfKeys.DnssecDebug);
- RegisterFlag(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.Leasequery);
- RegisterFlag(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpGenerateNames);
- RegisterFlag(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpBroadcast);
RegisterFlag(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpSequentialIp);
RegisterFlag(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpIgnoreClid);
- RegisterFlag(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.BootpDynamic);
RegisterFlag(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.NoPing);
RegisterFlag(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.ScriptArp);
RegisterFlag(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.ScriptOnRenewal);
@@ -117,6 +111,17 @@ public class EffectiveConfigRenderFragmentRegistry : IEffectiveConfigRenderFragm
RegisterComponent(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.Umbrella, typeof(KeyOnlyOrValueDisplay));
RegisterValidatedSingle(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.Umbrella, SpecialOptionValidators.ValidateUmbrella);
+ // connmark-allowlist-enable, dnssec-check-unsigned: key-only or key=value with validation.
+ RegisterComponent(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.ConnmarkAllowlistEnable, typeof(KeyOnlyOrValueDisplay));
+ RegisterValidatedSingle(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.ConnmarkAllowlistEnable, SpecialOptionValidators.ValidateConnmarkAllowlistEnable);
+ RegisterComponent(EffectiveConfigFieldBuilder.SectionDnssec, DnsmasqConfKeys.DnssecCheckUnsigned, typeof(KeyOnlyOrValueDisplay));
+ RegisterValidatedSingle(EffectiveConfigFieldBuilder.SectionDnssec, DnsmasqConfKeys.DnssecCheckUnsigned, SpecialOptionValidators.ValidateDnssecCheckUnsigned);
+
+ // DHCP key-only-or-value options (optional value; no custom validator).
+ RegisterComponent(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpGenerateNames, typeof(KeyOnlyOrValueDisplay));
+ RegisterComponent(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpBroadcast, typeof(KeyOnlyOrValueDisplay));
+ RegisterComponent(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.BootpDynamic, typeof(KeyOnlyOrValueDisplay));
+
// log-queries: dropdown (Off / On / extra / proto / auth).
RegisterComponent(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.LogQueries, typeof(LogQueriesDisplay));
@@ -176,6 +181,8 @@ public class EffectiveConfigRenderFragmentRegistry : IEffectiveConfigRenderFragm
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpHostsfile);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpOptsfile);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpHostsdir);
+ RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpOptsdir);
+ RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.Leasequery);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpRelay);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpCircuitid);
RegisterMultiDescriptor(EffectiveConfigFieldBuilder.SectionDhcp, DnsmasqConfKeys.DhcpRemoteid);
diff --git a/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/EffectiveConfigSaveService.cs b/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/EffectiveConfigSaveService.cs
index 25ef43a..66da3e9 100644
--- a/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/EffectiveConfigSaveService.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/EffectiveConfigSaveService.cs
@@ -1,3 +1,4 @@
+using DnsmasqWebUI.Infrastructure.Helpers.Config;
using DnsmasqWebUI.Infrastructure.Services.Dnsmasq.Config.Abstractions;
using DnsmasqWebUI.Infrastructure.Services.Dnsmasq.Reload.Abstractions;
using DnsmasqWebUI.Infrastructure.Services.Dnsmasq.Validation.Abstractions;
@@ -94,6 +95,38 @@ public sealed class EffectiveConfigSaveService : IEffectiveConfigSaveService
var managedPath = set.ManagedFilePath!;
var backupPath = BuildBackupPath(managedPath);
+ // Reject changes for options not supported by this dnsmasq build (e.g. DNSSEC when built without DNSSEC).
+ var unsupported = changes
+ .Where(c => !EffectiveConfigFeatureRequirements.IsSupportedByCapabilities(c.OptionName, version.Capabilities))
+ .Select(c => c.OptionName)
+ .Distinct(StringComparer.Ordinal)
+ .ToList();
+ if (unsupported.Count > 0)
+ {
+ var firstReason = EffectiveConfigFeatureRequirements.GetRequiredFeature(unsupported[0]) is { } f
+ ? EffectiveConfigFeatureRequirements.GetUnsupportedReason(f)
+ : "This option isn't available with your current dnsmasq build.";
+ var optionList = string.Join(", ", unsupported);
+ _logger.LogWarning("Save rejected: unsupported options in pending changes: {Options}", optionList);
+ var userMessage = unsupported.Count == 1
+ ? firstReason
+ : $"Some of the options you changed aren't supported by your dnsmasq build. Options not saved: {optionList}.";
+ return new EffectiveConfigSaveResult(
+ BackupCreated: false,
+ BackupPath: null,
+ Saved: false,
+ Validated: false,
+ ValidationExitCode: -1,
+ ValidationStdOut: null,
+ ValidationStdErr: null,
+ Restarted: false,
+ RestartExitCode: -1,
+ RestartStdOut: null,
+ RestartStdErr: null,
+ ErrorCode: EffectiveConfigSaveResult.ErrorCodes.UnsupportedCapabilities,
+ UserMessage: userMessage);
+ }
+
try
{
CreateBackupIfSourceExists(managedPath, backupPath);
diff --git a/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/Validation/SpecialOptionValidators.cs b/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/Validation/SpecialOptionValidators.cs
index 93d143f..7a89a80 100644
--- a/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/Validation/SpecialOptionValidators.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfig/Validation/SpecialOptionValidators.cs
@@ -45,4 +45,35 @@ public static class SpecialOptionValidators
if (s.Length == 0) return null; // key-only
return null; // token parsing can be tightened later
}
+
+ /// connmark-allowlist-enable: key-only or optional mask (decimal uint or 0x hex).
+ public static string? ValidateConnmarkAllowlistEnable(object? value)
+ {
+ if (value is null) return null;
+ var s = value.ToString()?.Trim() ?? "";
+ if (s.Length == 0) return null; // key-only
+ if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
+ return uint.TryParse(s[2..], System.Globalization.NumberStyles.HexNumber, null, out _)
+ ? null
+ : "Mask must be valid hex after 0x.";
+ return uint.TryParse(s, out _) ? null : "Mask must be empty, decimal uint, or hex (0x...).";
+ }
+
+ /// dnssec-check-unsigned: key-only (enable check) or 'no' (disable).
+ public static string? ValidateDnssecCheckUnsigned(object? value)
+ {
+ if (value is null) return null;
+ var s = value.ToString()?.Trim() ?? "";
+ return s is "" or "no" ? null : "Allowed values: empty (enable check) or 'no'.";
+ }
+
+ /// Per-item validation for leasequery: key-only or IP[/prefix].
+ public static string? ValidateLeasequeryValue(string? s)
+ {
+ if (string.IsNullOrWhiteSpace(s)) return null; // key-only
+ var parts = s.Trim().Split('/', 2);
+ if (!System.Net.IPAddress.TryParse(parts[0], out _)) return "Leasequery source must be an IP address.";
+ if (parts.Length == 2 && !int.TryParse(parts[1], out _)) return "Prefix must be numeric.";
+ return null;
+ }
}
diff --git a/src/DnsmasqWebUI/Models/Dnsmasq/DnsmasqCompileCapabilities.cs b/src/DnsmasqWebUI/Models/Dnsmasq/DnsmasqCompileCapabilities.cs
new file mode 100644
index 0000000..f3c3d7e
--- /dev/null
+++ b/src/DnsmasqWebUI/Models/Dnsmasq/DnsmasqCompileCapabilities.cs
@@ -0,0 +1,13 @@
+namespace DnsmasqWebUI.Models.Dnsmasq;
+
+///
+/// Compile-time capabilities parsed from dnsmasq --version (e.g. "Compile time options: ... DHCP ... DNSSEC ...").
+/// Used to disable/hide options in the UI when the running dnsmasq binary does not support them.
+///
+public record DnsmasqCompileCapabilities(
+ bool Dhcp,
+ bool Tftp,
+ bool Dnssec,
+ bool Dbus,
+ IReadOnlySet RawTokens
+);
diff --git a/src/DnsmasqWebUI/Models/Dnsmasq/DnsmasqServiceStatus.cs b/src/DnsmasqWebUI/Models/Dnsmasq/DnsmasqServiceStatus.cs
index b09856e..9f0d51c 100644
--- a/src/DnsmasqWebUI/Models/Dnsmasq/DnsmasqServiceStatus.cs
+++ b/src/DnsmasqWebUI/Models/Dnsmasq/DnsmasqServiceStatus.cs
@@ -41,6 +41,10 @@ namespace DnsmasqWebUI.Models.Dnsmasq;
/// Configured minimum required dnsmasq version (e.g. "2.91").
/// True when version probe succeeded and installed version is at least minimum.
/// Error message when version probe failed or could not parse; null on success.
+/// True when dnsmasq was compiled with DHCP support (from --version compile options).
+/// True when dnsmasq was compiled with TFTP support.
+/// True when dnsmasq was compiled with DNSSEC support.
+/// True when dnsmasq was compiled with DBus support.
public record DnsmasqServiceStatus(
string? SystemHostsPath,
bool SystemHostsPathExists,
@@ -76,5 +80,9 @@ public record DnsmasqServiceStatus(
string? DnsmasqVersion,
string MinimumSupportedDnsmasqVersion,
bool DnsmasqVersionSupported,
- string? DnsmasqVersionError
+ string? DnsmasqVersionError,
+ bool DnsmasqSupportsDhcp,
+ bool DnsmasqSupportsTftp,
+ bool DnsmasqSupportsDnssec,
+ bool DnsmasqSupportsDbus
);
diff --git a/src/DnsmasqWebUI/Models/Dnsmasq/DnsmasqVersionInfo.cs b/src/DnsmasqWebUI/Models/Dnsmasq/DnsmasqVersionInfo.cs
index cb9f235..31d3689 100644
--- a/src/DnsmasqWebUI/Models/Dnsmasq/DnsmasqVersionInfo.cs
+++ b/src/DnsmasqWebUI/Models/Dnsmasq/DnsmasqVersionInfo.cs
@@ -7,10 +7,12 @@ namespace DnsmasqWebUI.Models.Dnsmasq;
/// True when probe succeeded and installed version is at least minimum.
/// The command that was run (e.g. "dnsmasq --version").
/// Error message when probe failed or version could not be parsed; null on success.
+/// Compile-time options parsed from version output (DHCP, TFTP, DNSSEC, DBus). Empty when probe failed or line not found.
public record DnsmasqVersionInfo(
Version? InstalledVersion,
Version MinimumVersion,
bool ProbeSucceeded,
bool IsSupported,
string ProbeCommand,
- string? Error);
+ string? Error,
+ DnsmasqCompileCapabilities Capabilities);
diff --git a/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveConfigFieldDescriptor.cs b/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveConfigFieldDescriptor.cs
index eae0a9a..7fa2308 100644
--- a/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveConfigFieldDescriptor.cs
+++ b/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveConfigFieldDescriptor.cs
@@ -17,7 +17,9 @@ public record EffectiveConfigFieldDescriptor(
Func? ResolveValue,
Func? ResolveSource,
Func?>? ResolveItems,
- EffectiveConfigSingleValueValidator? ValidateValue = null
+ EffectiveConfigSingleValueValidator? ValidateValue = null,
+ bool IsCapabilityDisabled = false,
+ string? CapabilityDisabledReason = null
) : IEffectiveConfigFieldDescriptor
{
public object? GetValue() => ResolveValue?.Invoke(Status);
diff --git a/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveConfigSaveResult.cs b/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveConfigSaveResult.cs
index f372a2a..f32c7f7 100644
--- a/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveConfigSaveResult.cs
+++ b/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveConfigSaveResult.cs
@@ -35,6 +35,8 @@ public record EffectiveConfigSaveResult(
public const string NoChanges = "no_changes";
public const string MissingManagedPath = "missing_managed_path";
public const string WriteFailed = "write_failed";
+ /// Save blocked before write because one or more changed options require dnsmasq capabilities not present in this build.
+ public const string UnsupportedCapabilities = "unsupported_capabilities";
public const string ValidateFailed = "validate_failed";
public const string RestartFailed = "restart_failed";
public const string UnsupportedVersion = "unsupported_version";
diff --git a/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveConfigSources.cs b/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveConfigSources.cs
index d99b252..36d3596 100644
--- a/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveConfigSources.cs
+++ b/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveConfigSources.cs
@@ -65,6 +65,7 @@ public record EffectiveConfigSources(
IReadOnlyList DhcpHostsfilePaths,
IReadOnlyList DhcpOptsfilePaths,
IReadOnlyList DhcpHostsdirPaths,
+ IReadOnlyList DhcpOptsdirPaths,
IReadOnlyList ConnmarkAllowlistValues,
IReadOnlyList CaaRecordValues,
IReadOnlyList DnsRrValues,
@@ -110,7 +111,7 @@ public record EffectiveConfigSources(
ConfigValueSource? TftpNoFail,
ConfigValueSource? TftpNoBlocksize,
ConfigValueSource? Dnssec,
- ConfigValueSource? DnssecCheckUnsigned,
+ ConfigValueSource? DnssecCheckUnsigned, // single value (empty or "no")
ConfigValueSource? ReadEthers,
ConfigValueSource? DhcpRapidCommit,
ConfigValueSource? Localmx,
@@ -120,11 +121,11 @@ public record EffectiveConfigSources(
ConfigValueSource? KeepInForeground,
ConfigValueSource? NoDaemon,
ConfigValueSource? ProxyDnssec,
- ConfigValueSource? ConnmarkAllowlistEnable,
+ ConfigValueSource? ConnmarkAllowlistEnable, // single value (empty or mask)
ConfigValueSource? NoRoundRobin,
ConfigValueSource? DnssecNoTimecheck,
ConfigValueSource? DnssecDebug,
- ConfigValueSource? Leasequery,
+ IReadOnlyList LeasequeryValues,
ConfigValueSource? DhcpGenerateNames,
ConfigValueSource? DhcpBroadcast,
ConfigValueSource? DhcpSequentialIp,
diff --git a/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveDnsmasqConfig.cs b/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveDnsmasqConfig.cs
index 3367f23..163b1f2 100644
--- a/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveDnsmasqConfig.cs
+++ b/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveDnsmasqConfig.cs
@@ -69,6 +69,7 @@ public record EffectiveDnsmasqConfig(
IReadOnlyList DhcpHostsfilePaths,
IReadOnlyList DhcpOptsfilePaths,
IReadOnlyList DhcpHostsdirPaths,
+ IReadOnlyList DhcpOptsdirPaths,
IReadOnlyList ConnmarkAllowlistValues,
IReadOnlyList CaaRecordValues,
IReadOnlyList DnsRrValues,
@@ -114,7 +115,7 @@ public record EffectiveDnsmasqConfig(
bool TftpNoFail,
bool TftpNoBlocksize,
bool Dnssec,
- bool DnssecCheckUnsigned,
+ string? DnssecCheckUnsigned,
bool ReadEthers,
bool DhcpRapidCommit,
bool Localmx,
@@ -124,16 +125,16 @@ public record EffectiveDnsmasqConfig(
bool KeepInForeground,
bool NoDaemon,
bool ProxyDnssec,
- bool ConnmarkAllowlistEnable,
+ string? ConnmarkAllowlistEnable,
bool NoRoundRobin,
bool DnssecNoTimecheck,
bool DnssecDebug,
- bool Leasequery,
- bool DhcpGenerateNames,
- bool DhcpBroadcast,
+ IReadOnlyList LeasequeryValues,
+ string? DhcpGenerateNames,
+ string? DhcpBroadcast,
bool DhcpSequentialIp,
bool DhcpIgnoreClid,
- bool BootpDynamic,
+ string? BootpDynamic,
bool NoPing,
bool ScriptArp,
bool ScriptOnRenewal,
diff --git a/src/DnsmasqWebUI/wwwroot/option-help/dhcp-optsdir.html b/src/DnsmasqWebUI/wwwroot/option-help/dhcp-optsdir.html
new file mode 100644
index 0000000..6888c0c
--- /dev/null
+++ b/src/DnsmasqWebUI/wwwroot/option-help/dhcp-optsdir.html
@@ -0,0 +1,2 @@
+--dhcp-optsdir=<path>
+This is equivalent to --dhcp-optsfile , with the differences noted for --dhcp-hostsdir .
\ No newline at end of file