mirror of
https://github.com/alexhopeoconnor/dnsmasq-webui.git
synced 2026-10-03 22:12:00 +10:00
dnsmasq options: dhcp-optsdir, key-only-or-value semantics, capability detection
- Add dhcp-optsdir support (model, sections, parser, UI multi-value). - Model leasequery, connmark-allowlist-enable, dhcp-generate-names, dhcp-broadcast, bootp-dynamic, dnssec-check-unsigned as key-only-or-value with validation. - Parse compile-time capabilities from dnsmasq --version (DHCP, TFTP, DNSSEC, DBus). - Expose capabilities via DnsmasqVersionInfo and status API (DnsmasqSupports*). - Map options to required features; disable unsupported option editors with badge + tooltip. - Save guard: reject changes for unsupported options; use UnsupportedCapabilities error code. - MultiKeyOnlyOrValue write path: filter out readonly values to avoid duplicating in managed file. - User-friendly tooltips for disabled options; dedicated UnsupportedCapabilities vs WriteFailed.
This commit is contained in:
@@ -122,6 +122,7 @@ OPTION_KEYS = [
|
||||
"dhcp-hostsfile",
|
||||
"dhcp-optsfile",
|
||||
"dhcp-hostsdir",
|
||||
"dhcp-optsdir",
|
||||
"dhcp-boot",
|
||||
"dhcp-ignore",
|
||||
"dhcp-vendorclass",
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
using DnsmasqWebUI.Infrastructure.Helpers.Dnsmasq;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
|
||||
namespace DnsmasqWebUI.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for parsing "Compile time options: ..." from dnsmasq --version output.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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<ConfigSetCache>.Instance);
|
||||
var setService = new DnsmasqConfigSetService(cache);
|
||||
var configService = new DnsmasqConfigService(setService, cache, NullLogger<DnsmasqConfigService>.Instance);
|
||||
|
||||
var changes = new List<PendingEffectiveConfigChange>
|
||||
{
|
||||
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<ConfigSetCache>.Instance);
|
||||
cache.Invalidate();
|
||||
var setService = new DnsmasqConfigSetService(cache);
|
||||
var configService = new DnsmasqConfigService(setService, cache, NullLogger<DnsmasqConfigService>.Instance);
|
||||
|
||||
// Simulate UI effective values (readonly + managed). Save should write only managed-only values.
|
||||
var changes = new List<PendingEffectiveConfigChange>
|
||||
{
|
||||
new(
|
||||
EffectiveConfigSections.SectionDhcp,
|
||||
DnsmasqConfKeys.Leasequery,
|
||||
null,
|
||||
new List<string> { "", "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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
|
||||
namespace DnsmasqWebUI.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for option -> feature mapping and capability checks.
|
||||
/// </summary>
|
||||
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<string>(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<string>(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<string>(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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SaveGuard_WhenBuildHasNoDnssec_DnssecOptionChangesWouldBeRejected()
|
||||
{
|
||||
var noDnssecCaps = new DnsmasqCompileCapabilities(true, true, false, false,
|
||||
new HashSet<string>(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<string>(),
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||
|
||||
namespace DnsmasqWebUI.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests that field descriptors get IsCapabilityDisabled set when the status indicates the option is not supported by the dnsmasq build.
|
||||
/// </summary>
|
||||
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<string>(),
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,7 @@ public class EffectiveDnsmasqConfigTests
|
||||
DhcpHostsfilePaths: Array.Empty<string>(),
|
||||
DhcpOptsfilePaths: Array.Empty<string>(),
|
||||
DhcpHostsdirPaths: Array.Empty<string>(),
|
||||
DhcpOptsdirPaths: Array.Empty<string>(),
|
||||
ConnmarkAllowlistValues: Array.Empty<string>(),
|
||||
CaaRecordValues: Array.Empty<string>(),
|
||||
DnsRrValues: Array.Empty<string>(),
|
||||
@@ -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<string>(),
|
||||
DhcpGenerateNames: null,
|
||||
DhcpBroadcast: null,
|
||||
DhcpSequentialIp: false,
|
||||
DhcpIgnoreClid: false,
|
||||
BootpDynamic: false,
|
||||
BootpDynamic: null,
|
||||
NoPing: false,
|
||||
ScriptArp: false,
|
||||
ScriptOnRenewal: false,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
using DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||
using Xunit;
|
||||
|
||||
namespace DnsmasqWebUI.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for SpecialOptionValidators, including malformed numeric and structured values.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
+14
-11
@@ -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;
|
||||
<li class="ec-field @(isActiveEditor ? "ec-field-editing" : "")" title="@(Descriptor.GetSource()?.GetReadOnlyTooltip())">
|
||||
@@ -30,7 +30,7 @@
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
</div>
|
||||
<OptionStateBadge AnchorId="@_readonlyBadgeAnchorId" Source="@Descriptor.GetSource()" OptionName="@Descriptor.OptionName" Value="@effectiveValue" IsInteractive="@IsEditMode" ShowEditableBadge="@showEditableBadge" HasPendingChange="@_hasPendingChange" EditableAsButton="true" FieldKey="@FieldKey" IsActiveEditor="@isActiveEditor" OnConfirmRequested="@HandleConfirmEdit" OnCancelRequested="@HandleCancelEdit" OnRevertRequested="@HandleRevertEdit" />
|
||||
<OptionStateBadge AnchorId="@_readonlyBadgeAnchorId" Source="@Descriptor.GetSource()" OptionName="@Descriptor.OptionName" Value="@effectiveValue" IsInteractive="@IsEditMode" ShowEditableBadge="@showEditableBadge" HasPendingChange="@_hasPendingChange" EditableAsButton="true" FieldKey="@FieldKey" IsActiveEditor="@isActiveEditor" IsCapabilityDisabled="@Descriptor.IsCapabilityDisabled" CapabilityDisabledReason="@Descriptor.CapabilityDisabledReason" OnConfirmRequested="@HandleConfirmEdit" OnCancelRequested="@HandleCancelEdit" OnRevertRequested="@HandleRevertEdit" />
|
||||
</div>
|
||||
}
|
||||
else
|
||||
@@ -38,7 +38,7 @@
|
||||
<CascadingValue Value="@false">
|
||||
@fragment(Descriptor)
|
||||
</CascadingValue>
|
||||
<OptionStateBadge AnchorId="@_readonlyBadgeAnchorId" Source="@Descriptor.GetSource()" OptionName="@Descriptor.OptionName" Value="@effectiveValue" IsInteractive="@IsEditMode" ShowEditableBadge="@showEditableBadge" HasPendingChange="@_hasPendingChange" EditableAsButton="true" FieldKey="@FieldKey" IsActiveEditor="@isActiveEditor" OnConfirmRequested="@HandleConfirmEdit" OnCancelRequested="@HandleCancelEdit" OnRevertRequested="@HandleRevertEdit" />
|
||||
<OptionStateBadge AnchorId="@_readonlyBadgeAnchorId" Source="@Descriptor.GetSource()" OptionName="@Descriptor.OptionName" Value="@effectiveValue" IsInteractive="@IsEditMode" ShowEditableBadge="@showEditableBadge" HasPendingChange="@_hasPendingChange" EditableAsButton="true" FieldKey="@FieldKey" IsActiveEditor="@isActiveEditor" IsCapabilityDisabled="@Descriptor.IsCapabilityDisabled" CapabilityDisabledReason="@Descriptor.CapabilityDisabledReason" OnConfirmRequested="@HandleConfirmEdit" OnCancelRequested="@HandleCancelEdit" OnRevertRequested="@HandleRevertEdit" />
|
||||
}
|
||||
</span>
|
||||
</li>
|
||||
@@ -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. *@
|
||||
<li class="ec-field @(IsEditMode && IsActive ? "ec-field-editing" : "")">
|
||||
<li class="ec-field @(IsEditMode && IsActive && !Descriptor.IsCapabilityDisabled ? "ec-field-editing" : "")">
|
||||
<OptionHelpLabel AnchorId="@_anchorId" HelpKey="@_optionHelpKey" OptionLabel="@Descriptor.OptionName" Title="@(_labelTooltip ?? Descriptor.GetSource()?.GetReadOnlyTooltip())"><strong>@Descriptor.OptionName:</strong></OptionHelpLabel>
|
||||
<span class="ec-field-value">
|
||||
<CascadingValue Value="@(IsEditMode && IsActive)">
|
||||
<CascadingValue Value="@(IsEditMode && IsActive && !Descriptor.IsCapabilityDisabled)">
|
||||
<CascadingValue Value="@IsEditMode">
|
||||
@customMulti(Descriptor)
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
</span>
|
||||
<OptionStateBadge AnchorId="@_readonlyBadgeAnchorId" Source="@_multiValueRowSource" OptionName="@Descriptor.OptionName" Value="@_multiValueItems"
|
||||
IsInteractive="@IsEditMode" ShowEditableBadge="@IsEditMode" HasPendingChange="@_multiValueHasPendingOrDraft" EditableAsButton="true" FieldKey="@FieldKey" IsActiveEditor="@(IsEditMode && IsActive)"
|
||||
IsInteractive="@(IsEditMode && !Descriptor.IsCapabilityDisabled)" ShowEditableBadge="@(IsEditMode && !Descriptor.IsCapabilityDisabled)" HasPendingChange="@_multiValueHasPendingOrDraft" EditableAsButton="true" FieldKey="@FieldKey" IsActiveEditor="@(IsEditMode && IsActive && !Descriptor.IsCapabilityDisabled)"
|
||||
IsCapabilityDisabled="@Descriptor.IsCapabilityDisabled" CapabilityDisabledReason="@Descriptor.CapabilityDisabledReason"
|
||||
OnConfirmRequested="@HandleConfirmEdit" OnCancelRequested="@HandleCancelEdit" OnRevertRequested="@HandleRevertEdit" />
|
||||
</li>
|
||||
}
|
||||
@@ -67,8 +68,8 @@ else if (Descriptor.IsMultiValue)
|
||||
{
|
||||
@* Multi-value state is computed in OnParametersSet and in HandleMultiValueChanged; we only read cached fields here. *@
|
||||
<EffectiveConfigMultiValueRow Descriptor="@Descriptor" Label="@Descriptor.OptionName" Items="@_multiValueItems" LabelTooltip="@_labelTooltip" OptionHelpKey="@_optionHelpKey" OptionHelpAnchorId="@_anchorId"
|
||||
IsActiveEditor="@(IsEditMode && IsActive)" IsEditMode="@IsEditMode" ReadonlyBadgeAnchorIdBase="@_readonlyBadgeAnchorId"
|
||||
Source="@_multiValueRowSource" ShowEditableBadge="@IsEditMode" HasPendingChange="@_multiValueHasPendingOrDraft" FieldKey="@FieldKey"
|
||||
IsActiveEditor="@(IsEditMode && IsActive && !Descriptor.IsCapabilityDisabled)" IsEditMode="@IsEditMode" ReadonlyBadgeAnchorIdBase="@_readonlyBadgeAnchorId"
|
||||
Source="@_multiValueRowSource" ShowEditableBadge="@(IsEditMode && !Descriptor.IsCapabilityDisabled)" HasPendingChange="@_multiValueHasPendingOrDraft" FieldKey="@FieldKey"
|
||||
ItemsChanged="@HandleMultiValueChanged" OnConfirmRequested="@HandleConfirmEdit" OnCancelRequested="@HandleCancelEdit" OnRevertRequested="@HandleRevertEdit" />
|
||||
}
|
||||
}
|
||||
@@ -78,7 +79,8 @@ else
|
||||
var displayValue = value == null ? "(not set)" : (value is IReadOnlyList<string> 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)
|
||||
{
|
||||
<li class="ec-field" title="@(Descriptor.GetSource()?.GetReadOnlyTooltip())">
|
||||
@@ -90,8 +92,9 @@ else
|
||||
{
|
||||
<li class="ec-field @(isActiveEditor ? "ec-field-editing" : "")" title="@(Descriptor.GetSource()?.GetReadOnlyTooltip())">
|
||||
<EffectiveConfigOptionItem Label="@Descriptor.OptionName" DisplayValue="@(displayValue ?? "(not set)")" Source="@Descriptor.GetSource()" LabelTooltip="@_labelTooltip" OptionHelpKey="@_optionHelpKey" OptionHelpAnchorId="@_anchorId"
|
||||
ShowEditableBadge="true" FieldKey="@FieldKey" IsActive="@IsActive"
|
||||
ShowEditableBadge="@showEditableForDefault" FieldKey="@FieldKey" IsActive="@IsActive"
|
||||
ReadonlyBadgeAnchorId="@_readonlyBadgeAnchorId" RawValue="@value" HasPendingChange="@_hasPendingChange"
|
||||
IsCapabilityDisabled="@Descriptor.IsCapabilityDisabled" CapabilityDisabledReason="@Descriptor.CapabilityDisabledReason"
|
||||
DisplayValueChanged="@HandleDefaultValueChanged" OnBlur="@HandleDefaultBlur" OnConfirmRequested="@HandleConfirmEdit" OnCancelRequested="@HandleCancelEdit" OnRevertRequested="@HandleRevertEdit" />
|
||||
</li>
|
||||
}
|
||||
|
||||
+3
-2
@@ -19,6 +19,7 @@
|
||||
<OptionStateBadge AnchorId="@(ReadonlyBadgeAnchorIdBase ?? "")" Source="@Source" OptionName="@Label" Value="@Items"
|
||||
IsInteractive="@IsEditMode" ShowEditableBadge="@ShowEditableBadge" HasPendingChange="@HasPendingChange"
|
||||
EditableAsButton="true" FieldKey="@FieldKey" IsActiveEditor="@IsActiveEditor"
|
||||
IsCapabilityDisabled="@(Descriptor?.IsCapabilityDisabled == true)" CapabilityDisabledReason="@Descriptor?.CapabilityDisabledReason"
|
||||
OnConfirmRequested="@OnConfirmRequested" OnCancelRequested="@OnCancelRequested" OnRevertRequested="@OnRevertRequested" />
|
||||
}
|
||||
else
|
||||
@@ -37,8 +38,8 @@
|
||||
else
|
||||
{
|
||||
<span class="text-muted">(none)</span>
|
||||
<OptionStateBadge OptionName="@Label" ShowEditableBadge="true" EditableAsButton="true"
|
||||
FieldKey="@FieldKey" IsActiveEditor="false" IsInteractive="true" />
|
||||
<OptionStateBadge OptionName="@Label" ShowEditableBadge="@(!(Descriptor?.IsCapabilityDisabled == true))" EditableAsButton="true"
|
||||
FieldKey="@FieldKey" IsActiveEditor="false" IsInteractive="true" IsCapabilityDisabled="@(Descriptor?.IsCapabilityDisabled == true)" CapabilityDisabledReason="@Descriptor?.CapabilityDisabledReason" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -3,18 +3,18 @@
|
||||
@using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig
|
||||
|
||||
<OptionHelpLabel AnchorId="@(OptionHelpAnchorId ?? "")" HelpKey="@OptionHelpKey" OptionLabel="@Label" Title="@(LabelTooltip ?? Source?.GetReadOnlyTooltip())"><strong>@Label:</strong></OptionHelpLabel>
|
||||
@if (DisplayMode == EffectiveConfigDisplayMode.Edit && IsActive && Source?.IsReadOnly != true)
|
||||
@if (DisplayMode == EffectiveConfigDisplayMode.Edit && IsActive && Source?.IsReadOnly != true && !IsCapabilityDisabled)
|
||||
{
|
||||
<span class="ec-field-value" @onclick:stopPropagation="true">
|
||||
<input type="text" class="form-control form-control-sm" value="@(_editValue ?? DisplayValue)" @oninput="OnValueInput" @onfocusout="OnInputBlur" />
|
||||
<OptionStateBadge AnchorId="@(ReadonlyBadgeAnchorId ?? "")" Source="@Source" OptionName="@Label" Value="@(RawValue ?? DisplayValue)" IsInteractive="@(DisplayMode == EffectiveConfigDisplayMode.Edit && !string.IsNullOrEmpty(ReadonlyBadgeAnchorId))" ShowEditableBadge="@ShowEditableBadge" HasPendingChange="@HasPendingChange" EditableAsButton="true" FieldKey="@FieldKey" IsActiveEditor="true" OnConfirmRequested="@OnConfirmRequested" OnCancelRequested="@OnCancelRequested" OnRevertRequested="@OnRevertRequested" />
|
||||
<OptionStateBadge AnchorId="@(ReadonlyBadgeAnchorId ?? "")" Source="@Source" OptionName="@Label" Value="@(RawValue ?? DisplayValue)" IsInteractive="@(DisplayMode == EffectiveConfigDisplayMode.Edit && !string.IsNullOrEmpty(ReadonlyBadgeAnchorId))" ShowEditableBadge="@ShowEditableBadge" HasPendingChange="@HasPendingChange" EditableAsButton="true" FieldKey="@FieldKey" IsActiveEditor="true" IsCapabilityDisabled="@IsCapabilityDisabled" CapabilityDisabledReason="@CapabilityDisabledReason" OnConfirmRequested="@OnConfirmRequested" OnCancelRequested="@OnCancelRequested" OnRevertRequested="@OnRevertRequested" />
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="ec-field-value">
|
||||
@DisplayValue
|
||||
<OptionStateBadge AnchorId="@(ReadonlyBadgeAnchorId ?? "")" Source="@Source" OptionName="@Label" Value="@(RawValue ?? DisplayValue)" IsInteractive="@(DisplayMode == EffectiveConfigDisplayMode.Edit && !string.IsNullOrEmpty(ReadonlyBadgeAnchorId))" ShowEditableBadge="@ShowEditableBadge" HasPendingChange="@HasPendingChange" EditableAsButton="true" FieldKey="@FieldKey" IsActiveEditor="false" OnConfirmRequested="@OnConfirmRequested" OnCancelRequested="@OnCancelRequested" OnRevertRequested="@OnRevertRequested" />
|
||||
<OptionStateBadge AnchorId="@(ReadonlyBadgeAnchorId ?? "")" Source="@Source" OptionName="@Label" Value="@(RawValue ?? DisplayValue)" IsInteractive="@(DisplayMode == EffectiveConfigDisplayMode.Edit && !string.IsNullOrEmpty(ReadonlyBadgeAnchorId))" ShowEditableBadge="@ShowEditableBadge" HasPendingChange="@HasPendingChange" EditableAsButton="true" FieldKey="@FieldKey" IsActiveEditor="false" IsCapabilityDisabled="@IsCapabilityDisabled" CapabilityDisabledReason="@CapabilityDisabledReason" OnConfirmRequested="@OnConfirmRequested" OnCancelRequested="@OnCancelRequested" OnRevertRequested="@OnRevertRequested" />
|
||||
</span>
|
||||
}
|
||||
|
||||
@@ -38,6 +38,10 @@ else
|
||||
[Parameter] public string FieldKey { get; set; } = "";
|
||||
/// <summary>When true, add ec-field-editing class and show input when DisplayMode is Edit.</summary>
|
||||
[Parameter] public bool IsActive { get; set; }
|
||||
/// <summary>When true, option is disabled because dnsmasq build does not support it; show disabled badge and no editor.</summary>
|
||||
[Parameter] public bool IsCapabilityDisabled { get; set; }
|
||||
/// <summary>Tooltip when <see cref="IsCapabilityDisabled"/> is true.</summary>
|
||||
[Parameter] public string? CapabilityDisabledReason { get; set; }
|
||||
/// <summary>When Edit and not readonly: raised when the user changes the value. Not yet wired to save.</summary>
|
||||
[Parameter] public EventCallback<string> DisplayValueChanged { get; set; }
|
||||
/// <summary>When Edit and not readonly: raised when the input loses focus; argument is the current value.</summary>
|
||||
|
||||
@@ -2,7 +2,14 @@
|
||||
@using DnsmasqWebUI.Models.Dnsmasq
|
||||
@using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig
|
||||
|
||||
@if (Source?.IsReadOnly == true)
|
||||
@if (IsCapabilityDisabled)
|
||||
{
|
||||
<StatusBadge Kind="StatusBadgeKind.ReadOnly"
|
||||
Text="disabled"
|
||||
Title="@CapabilityDisabledReason"
|
||||
Class="status-badge-inline" />
|
||||
}
|
||||
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; }
|
||||
/// <summary>Optional tooltip for readonly badge when <see cref="ReadonlyClickActivatesField"/> is true (e.g. "New values can still be added. Click to edit.").</summary>
|
||||
[Parameter] public string? ReadonlyTitleOverride { get; set; }
|
||||
/// <summary>When true, option is disabled because dnsmasq build does not support it (e.g. no DNSSEC).</summary>
|
||||
[Parameter] public bool IsCapabilityDisabled { get; set; }
|
||||
/// <summary>Tooltip shown when <see cref="IsCapabilityDisabled"/> is true (e.g. "This dnsmasq build does not support DNSSEC; option not written.").</summary>
|
||||
[Parameter] public string? CapabilityDisabledReason { get; set; }
|
||||
[CascadingParameter] public EffectiveConfigUiContext? Ui { get; set; }
|
||||
|
||||
private async Task OnConfirmClick()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
|
||||
namespace DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||
|
||||
/// <summary>Dnsmasq compile-time feature required for certain options.</summary>
|
||||
public enum DnsmasqFeature
|
||||
{
|
||||
Dhcp,
|
||||
Tftp,
|
||||
Dnssec,
|
||||
Dbus
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static class EffectiveConfigFeatureRequirements
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<string, DnsmasqFeature> RequiredByOption =
|
||||
new Dictionary<string, DnsmasqFeature>(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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>User-facing tooltip when an option is disabled because the running dnsmasq was built without the required feature.</summary>
|
||||
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."
|
||||
};
|
||||
|
||||
/// <summary>Returns (isDisabled, reason) for the option given current status. Used to set descriptor capability flags.</summary>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>True when the option is supported by the given compile-time capabilities (for save-path guard).</summary>
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,12 @@ public static class EffectiveConfigFieldBuilder
|
||||
return Array.Empty<EffectiveConfigFieldDescriptor>();
|
||||
|
||||
var list = new List<EffectiveConfigFieldDescriptor>();
|
||||
AddAllDescriptors(list, status, registry);
|
||||
return list;
|
||||
}
|
||||
|
||||
private static void AddAllDescriptors(List<EffectiveConfigFieldDescriptor> 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;
|
||||
}
|
||||
|
||||
/// <summary>Creates a single-value descriptor; uses registry factory when registered (e.g. integer), otherwise a plain descriptor. Used by <see cref="EffectiveConfigFieldDescriptorListExtensions.AddSingleDescriptor"/>.</summary>
|
||||
@@ -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,
|
||||
|
||||
+3
-1
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -128,6 +128,7 @@ public static class EffectiveConfigSections
|
||||
DnsmasqConfKeys.DhcpHostsfile,
|
||||
DnsmasqConfKeys.DhcpOptsfile,
|
||||
DnsmasqConfKeys.DhcpHostsdir,
|
||||
DnsmasqConfKeys.DhcpOptsdir,
|
||||
DnsmasqConfKeys.DhcpBoot,
|
||||
DnsmasqConfKeys.DhcpIgnore,
|
||||
DnsmasqConfKeys.DhcpVendorclass,
|
||||
|
||||
+30
@@ -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),
|
||||
};
|
||||
|
||||
/// <summary>Keys (enabled, disabled) for InversePair options only. Used by write path and readonly hints.</summary>
|
||||
|
||||
@@ -10,6 +10,7 @@ public enum EffectiveConfigWriteBehavior
|
||||
SingleValue,
|
||||
MultiValue,
|
||||
KeyOnlyOrValue,
|
||||
MultiKeyOnlyOrValue,
|
||||
InversePair,
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using DnsmasqWebUI.Models.Dnsmasq;
|
||||
|
||||
namespace DnsmasqWebUI.Infrastructure.Helpers.Dnsmasq;
|
||||
|
||||
/// <summary>
|
||||
/// Parses "Compile time options: ..." line from dnsmasq --version output
|
||||
/// to determine DHCP, TFTP, DNSSEC, DBus support.
|
||||
/// </summary>
|
||||
public static class DnsmasqCompileOptionsParser
|
||||
{
|
||||
private static readonly Regex CompileLine = new(
|
||||
@"Compile\s+time\s+options\s*:\s*(?<opts>.+)$",
|
||||
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<string>(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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -293,6 +293,7 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
|
||||
var dhcpHostsfilePaths = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpHostsfile);
|
||||
var dhcpOptsfilePaths = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpOptsfile);
|
||||
var dhcpHostsdirPaths = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpHostsdir);
|
||||
var dhcpOptsdirPaths = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DhcpOptsdir);
|
||||
var connmarkAllowlistValues = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.ConnmarkAllowlist);
|
||||
var caaRecordValues = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.CaaRecord);
|
||||
var dnsRrValues = (IReadOnlyList<string>)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.DnsRr);
|
||||
@@ -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<string>)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<string>(), DynamicHostValues: Array.Empty<string>(), InterfaceNameValues: Array.Empty<string>(),
|
||||
DhcpOptionForceLines: Array.Empty<string>(), IpsetValues: Array.Empty<string>(), NftsetValues: Array.Empty<string>(),
|
||||
DhcpMacValues: Array.Empty<string>(), DhcpNameMatchValues: Array.Empty<string>(), DhcpIgnoreNamesValues: Array.Empty<string>(),
|
||||
DhcpHostsfilePaths: Array.Empty<string>(), DhcpOptsfilePaths: Array.Empty<string>(), DhcpHostsdirPaths: Array.Empty<string>(),
|
||||
DhcpHostsfilePaths: Array.Empty<string>(), DhcpOptsfilePaths: Array.Empty<string>(), DhcpHostsdirPaths: Array.Empty<string>(), DhcpOptsdirPaths: Array.Empty<string>(),
|
||||
ConnmarkAllowlistValues: Array.Empty<string>(), CaaRecordValues: Array.Empty<string>(), DnsRrValues: Array.Empty<string>(), SynthDomainValues: Array.Empty<string>(),
|
||||
AuthZoneValues: Array.Empty<string>(), AuthSoaValues: Array.Empty<string>(), AuthSecServersValues: Array.Empty<string>(), AuthPeerValues: Array.Empty<string>(),
|
||||
DhcpRelayValues: Array.Empty<string>(), DhcpCircuitidValues: Array.Empty<string>(), DhcpRemoteidValues: Array.Empty<string>(), DhcpSubscridValues: Array.Empty<string>(), DhcpProxyValues: Array.Empty<string>(),
|
||||
@@ -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<string>(), 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<ValueWithSource>(), DynamicHostValues: Array.Empty<ValueWithSource>(), InterfaceNameValues: Array.Empty<ValueWithSource>(),
|
||||
DhcpOptionForceLines: Array.Empty<ValueWithSource>(), IpsetValues: Array.Empty<ValueWithSource>(), NftsetValues: Array.Empty<ValueWithSource>(),
|
||||
DhcpMacValues: Array.Empty<ValueWithSource>(), DhcpNameMatchValues: Array.Empty<ValueWithSource>(), DhcpIgnoreNamesValues: Array.Empty<ValueWithSource>(),
|
||||
DhcpHostsfilePaths: Array.Empty<ValueWithSource>(), DhcpOptsfilePaths: Array.Empty<ValueWithSource>(), DhcpHostsdirPaths: Array.Empty<ValueWithSource>(),
|
||||
DhcpHostsfilePaths: Array.Empty<ValueWithSource>(), DhcpOptsfilePaths: Array.Empty<ValueWithSource>(), DhcpHostsdirPaths: Array.Empty<ValueWithSource>(), DhcpOptsdirPaths: Array.Empty<ValueWithSource>(),
|
||||
ConnmarkAllowlistValues: Array.Empty<ValueWithSource>(), CaaRecordValues: Array.Empty<ValueWithSource>(), DnsRrValues: Array.Empty<ValueWithSource>(), SynthDomainValues: Array.Empty<ValueWithSource>(),
|
||||
AuthZoneValues: Array.Empty<ValueWithSource>(), AuthSoaValues: Array.Empty<ValueWithSource>(), AuthSecServersValues: Array.Empty<ValueWithSource>(), AuthPeerValues: Array.Empty<ValueWithSource>(),
|
||||
DhcpRelayValues: Array.Empty<ValueWithSource>(), DhcpCircuitidValues: Array.Empty<ValueWithSource>(), DhcpRemoteidValues: Array.Empty<ValueWithSource>(), DhcpSubscridValues: Array.Empty<ValueWithSource>(), DhcpProxyValues: Array.Empty<ValueWithSource>(),
|
||||
@@ -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<ValueWithSource>(), 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,
|
||||
|
||||
@@ -253,6 +253,19 @@ public class DnsmasqConfigService : IDnsmasqConfigService
|
||||
continue;
|
||||
}
|
||||
|
||||
if (writeBehavior == EffectiveConfigWriteBehavior.MultiKeyOnlyOrValue && TryGetMultiValues(c.NewValue, out var multiKeyOnlyValues))
|
||||
{
|
||||
IReadOnlyList<string> readonlyValues = readonlyByOption.TryGetValue(confKey, out var listValues) ? listValues : Array.Empty<string>();
|
||||
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);
|
||||
|
||||
@@ -29,13 +29,15 @@ public sealed class DnsmasqVersionService : IDnsmasqVersionService
|
||||
|
||||
if (string.IsNullOrWhiteSpace(command))
|
||||
{
|
||||
var emptyCaps = new DnsmasqCompileCapabilities(false, false, false, false, new HashSet<string>(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);
|
||||
}
|
||||
}
|
||||
|
||||
+13
-6
@@ -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);
|
||||
|
||||
+33
@@ -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);
|
||||
|
||||
+31
@@ -45,4 +45,35 @@ public static class SpecialOptionValidators
|
||||
if (s.Length == 0) return null; // key-only
|
||||
return null; // token parsing can be tightened later
|
||||
}
|
||||
|
||||
/// <summary>connmark-allowlist-enable: key-only or optional mask (decimal uint or 0x hex).</summary>
|
||||
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...).";
|
||||
}
|
||||
|
||||
/// <summary>dnssec-check-unsigned: key-only (enable check) or 'no' (disable).</summary>
|
||||
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'.";
|
||||
}
|
||||
|
||||
/// <summary>Per-item validation for leasequery: key-only or IP[/prefix].</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace DnsmasqWebUI.Models.Dnsmasq;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public record DnsmasqCompileCapabilities(
|
||||
bool Dhcp,
|
||||
bool Tftp,
|
||||
bool Dnssec,
|
||||
bool Dbus,
|
||||
IReadOnlySet<string> RawTokens
|
||||
);
|
||||
@@ -41,6 +41,10 @@ namespace DnsmasqWebUI.Models.Dnsmasq;
|
||||
/// <param name="MinimumSupportedDnsmasqVersion">Configured minimum required dnsmasq version (e.g. "2.91").</param>
|
||||
/// <param name="DnsmasqVersionSupported">True when version probe succeeded and installed version is at least minimum.</param>
|
||||
/// <param name="DnsmasqVersionError">Error message when version probe failed or could not parse; null on success.</param>
|
||||
/// <param name="DnsmasqSupportsDhcp">True when dnsmasq was compiled with DHCP support (from --version compile options).</param>
|
||||
/// <param name="DnsmasqSupportsTftp">True when dnsmasq was compiled with TFTP support.</param>
|
||||
/// <param name="DnsmasqSupportsDnssec">True when dnsmasq was compiled with DNSSEC support.</param>
|
||||
/// <param name="DnsmasqSupportsDbus">True when dnsmasq was compiled with DBus support.</param>
|
||||
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
|
||||
);
|
||||
|
||||
@@ -7,10 +7,12 @@ namespace DnsmasqWebUI.Models.Dnsmasq;
|
||||
/// <param name="IsSupported">True when probe succeeded and installed version is at least minimum.</param>
|
||||
/// <param name="ProbeCommand">The command that was run (e.g. "dnsmasq --version").</param>
|
||||
/// <param name="Error">Error message when probe failed or version could not be parsed; null on success.</param>
|
||||
/// <param name="Capabilities">Compile-time options parsed from version output (DHCP, TFTP, DNSSEC, DBus). Empty when probe failed or line not found.</param>
|
||||
public record DnsmasqVersionInfo(
|
||||
Version? InstalledVersion,
|
||||
Version MinimumVersion,
|
||||
bool ProbeSucceeded,
|
||||
bool IsSupported,
|
||||
string ProbeCommand,
|
||||
string? Error);
|
||||
string? Error,
|
||||
DnsmasqCompileCapabilities Capabilities);
|
||||
|
||||
@@ -17,7 +17,9 @@ public record EffectiveConfigFieldDescriptor(
|
||||
Func<DnsmasqServiceStatus?, object?>? ResolveValue,
|
||||
Func<DnsmasqServiceStatus?, ConfigValueSource?>? ResolveSource,
|
||||
Func<DnsmasqServiceStatus?, IReadOnlyList<ValueWithSource>?>? ResolveItems,
|
||||
EffectiveConfigSingleValueValidator? ValidateValue = null
|
||||
EffectiveConfigSingleValueValidator? ValidateValue = null,
|
||||
bool IsCapabilityDisabled = false,
|
||||
string? CapabilityDisabledReason = null
|
||||
) : IEffectiveConfigFieldDescriptor
|
||||
{
|
||||
public object? GetValue() => ResolveValue?.Invoke(Status);
|
||||
|
||||
@@ -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";
|
||||
/// <summary>Save blocked before write because one or more changed options require dnsmasq capabilities not present in this build.</summary>
|
||||
public const string UnsupportedCapabilities = "unsupported_capabilities";
|
||||
public const string ValidateFailed = "validate_failed";
|
||||
public const string RestartFailed = "restart_failed";
|
||||
public const string UnsupportedVersion = "unsupported_version";
|
||||
|
||||
@@ -65,6 +65,7 @@ public record EffectiveConfigSources(
|
||||
IReadOnlyList<ValueWithSource> DhcpHostsfilePaths,
|
||||
IReadOnlyList<ValueWithSource> DhcpOptsfilePaths,
|
||||
IReadOnlyList<ValueWithSource> DhcpHostsdirPaths,
|
||||
IReadOnlyList<ValueWithSource> DhcpOptsdirPaths,
|
||||
IReadOnlyList<ValueWithSource> ConnmarkAllowlistValues,
|
||||
IReadOnlyList<ValueWithSource> CaaRecordValues,
|
||||
IReadOnlyList<ValueWithSource> 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<ValueWithSource> LeasequeryValues,
|
||||
ConfigValueSource? DhcpGenerateNames,
|
||||
ConfigValueSource? DhcpBroadcast,
|
||||
ConfigValueSource? DhcpSequentialIp,
|
||||
|
||||
@@ -69,6 +69,7 @@ public record EffectiveDnsmasqConfig(
|
||||
IReadOnlyList<string> DhcpHostsfilePaths,
|
||||
IReadOnlyList<string> DhcpOptsfilePaths,
|
||||
IReadOnlyList<string> DhcpHostsdirPaths,
|
||||
IReadOnlyList<string> DhcpOptsdirPaths,
|
||||
IReadOnlyList<string> ConnmarkAllowlistValues,
|
||||
IReadOnlyList<string> CaaRecordValues,
|
||||
IReadOnlyList<string> 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<string> LeasequeryValues,
|
||||
string? DhcpGenerateNames,
|
||||
string? DhcpBroadcast,
|
||||
bool DhcpSequentialIp,
|
||||
bool DhcpIgnoreClid,
|
||||
bool BootpDynamic,
|
||||
string? BootpDynamic,
|
||||
bool NoPing,
|
||||
bool ScriptArp,
|
||||
bool ScriptOnRenewal,
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
<dt><b>--dhcp-optsdir=<path></b></dt>
|
||||
<dd>This is equivalent to <b>--dhcp-optsfile</b>, with the differences noted for <b>--dhcp-hostsdir</b>.</dd>
|
||||
Reference in New Issue
Block a user