mirror of
https://github.com/alexhopeoconnor/dnsmasq-webui.git
synced 2026-10-03 22:12:00 +10:00
Effective config: no-0x20 precedence, write semantics, validators, strip-mac/strip-subnet
- ConfigSetCache: no-0x20-encode wins per dnsmasq docs; effective state and source attribution updated - ReadonlyConfigCommandGenerator: use EffectiveConfigWriteSemantics for inverse-pair and key-only-or-value - SpecialOptionValidators for UseStaleCache, AddMac, AddSubnet, Umbrella; wired via RegisterValidatedSingle - OptionSemantics + EffectiveConfigWriteSemantics (GetBehavior/GetInversePairKeys); remove EffectiveConfigWriteBehaviorMap - strip-mac / strip-subnet: full support in EffectiveDnsmasqConfig, sources, cache, sections, field builder, registry, tests - Multi-value editor: toasts and block commit on validation error; 2s toast dedupe - Do0x20 view label comment + Do0x20EncodeDisplayLabelTests - DnsmasqOptionTooltips + extract-option-help.sh for strip-mac/strip-subnet; regenerated option-help HTML
This commit is contained in:
@@ -216,7 +216,9 @@ OPTION_KEYS = [
|
|||||||
"dumpmask",
|
"dumpmask",
|
||||||
"use-stale-cache",
|
"use-stale-cache",
|
||||||
"add-mac",
|
"add-mac",
|
||||||
|
"strip-mac",
|
||||||
"add-subnet",
|
"add-subnet",
|
||||||
|
"strip-subnet",
|
||||||
"umbrella",
|
"umbrella",
|
||||||
"quiet-dhcp",
|
"quiet-dhcp",
|
||||||
"quiet-dhcp6",
|
"quiet-dhcp6",
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
using DnsmasqWebUI.Infrastructure.Services.Dnsmasq.Config;
|
||||||
|
using DnsmasqWebUI.Models.Config;
|
||||||
|
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests effective-config resolution in ConfigSetCache (BuildEffectiveConfig / BuildEffectiveConfigSources).
|
||||||
|
/// Verifies no-0x20-encode precedence and source attribution per dnsmasq docs.
|
||||||
|
/// </summary>
|
||||||
|
public class ConfigSetCacheEffectiveConfigTests
|
||||||
|
{
|
||||||
|
private static (string Dir, string MainPath, string ManagedPath, ConfigSetCache Cache) CreateCacheWithManaged(string managedContent)
|
||||||
|
{
|
||||||
|
var dir = Path.Combine(Path.GetTempPath(), "dnsmasq-cache-" + 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);
|
||||||
|
File.WriteAllText(mainPath, "port=53\n");
|
||||||
|
File.WriteAllText(managedPath, managedContent);
|
||||||
|
var options = Options.Create(new DnsmasqOptions { MainConfigPath = mainPath, ManagedFileName = managedName });
|
||||||
|
var cache = new ConfigSetCache(options, NullLogger<ConfigSetCache>.Instance);
|
||||||
|
return (dir, mainPath, managedPath, cache);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Do0x20_BothPresent_No0x20Wins()
|
||||||
|
{
|
||||||
|
var (dir, _, managedPath, cache) = CreateCacheWithManaged("do-0x20-encode\nno-0x20-encode\n");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var snapshot = await cache.GetSnapshotAsync();
|
||||||
|
Assert.Equal(ExplicitToggleState.Disabled, snapshot.Config.Do0x20EncodeState);
|
||||||
|
Assert.NotNull(snapshot.Sources.Do0x20Encode);
|
||||||
|
Assert.Equal(Path.GetFullPath(managedPath), snapshot.Sources.Do0x20Encode.FilePath, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
cache.Dispose();
|
||||||
|
if (Directory.Exists(dir)) Directory.Delete(dir, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Do0x20_OnlyDo_Enabled()
|
||||||
|
{
|
||||||
|
var (dir, _, _, cache) = CreateCacheWithManaged("do-0x20-encode\n");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var snapshot = await cache.GetSnapshotAsync();
|
||||||
|
Assert.Equal(ExplicitToggleState.Enabled, snapshot.Config.Do0x20EncodeState);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
cache.Dispose();
|
||||||
|
if (Directory.Exists(dir)) Directory.Delete(dir, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Do0x20_OnlyNo_Disabled()
|
||||||
|
{
|
||||||
|
var (dir, _, managedPath, cache) = CreateCacheWithManaged("no-0x20-encode\n");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var snapshot = await cache.GetSnapshotAsync();
|
||||||
|
Assert.Equal(ExplicitToggleState.Disabled, snapshot.Config.Do0x20EncodeState);
|
||||||
|
Assert.NotNull(snapshot.Sources.Do0x20Encode);
|
||||||
|
Assert.Equal(Path.GetFullPath(managedPath), snapshot.Sources.Do0x20Encode.FilePath, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
cache.Dispose();
|
||||||
|
if (Directory.Exists(dir)) Directory.Delete(dir, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Do0x20_Neither_Default()
|
||||||
|
{
|
||||||
|
var (dir, _, _, cache) = CreateCacheWithManaged("port=53\n");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var snapshot = await cache.GetSnapshotAsync();
|
||||||
|
Assert.Equal(ExplicitToggleState.Default, snapshot.Config.Do0x20EncodeState);
|
||||||
|
Assert.Null(snapshot.Sources.Do0x20Encode);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
cache.Dispose();
|
||||||
|
if (Directory.Exists(dir)) Directory.Delete(dir, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Do0x20_SourceAttribution_WhenBothPresent_IsNo0x20Line()
|
||||||
|
{
|
||||||
|
var (dir, _, managedPath, cache) = CreateCacheWithManaged("no-0x20-encode\ndo-0x20-encode\n");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var snapshot = await cache.GetSnapshotAsync();
|
||||||
|
Assert.Equal(ExplicitToggleState.Disabled, snapshot.Config.Do0x20EncodeState);
|
||||||
|
Assert.NotNull(snapshot.Sources.Do0x20Encode);
|
||||||
|
Assert.Equal(Path.GetFullPath(managedPath), snapshot.Sources.Do0x20Encode.FilePath, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
cache.Dispose();
|
||||||
|
if (Directory.Exists(dir)) Directory.Delete(dir, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task StripFlags_ParseState_And_Source()
|
||||||
|
{
|
||||||
|
var (dir, _, managedPath, cache) = CreateCacheWithManaged("strip-mac\nstrip-subnet\n");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var snapshot = await cache.GetSnapshotAsync();
|
||||||
|
Assert.True(snapshot.Config.StripMac);
|
||||||
|
Assert.True(snapshot.Config.StripSubnet);
|
||||||
|
Assert.NotNull(snapshot.Sources.StripMac);
|
||||||
|
Assert.NotNull(snapshot.Sources.StripSubnet);
|
||||||
|
Assert.Equal(Path.GetFullPath(managedPath), snapshot.Sources.StripMac!.FilePath, StringComparer.OrdinalIgnoreCase);
|
||||||
|
Assert.Equal(Path.GetFullPath(managedPath), snapshot.Sources.StripSubnet!.FilePath, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
cache.Dispose();
|
||||||
|
if (Directory.Exists(dir)) Directory.Delete(dir, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -86,6 +86,79 @@ public class DnsmasqConfigServiceApplyChangesTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ApplyEffectiveConfigChangesAsync_StripMacTrue_WritesBareFlagLine()
|
||||||
|
{
|
||||||
|
var dir = Path.Combine(Path.GetTempPath(), "dnsmasq-apply-" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
var mainPath = Path.Combine(dir, "dnsmasq.conf");
|
||||||
|
var managedName = "zz-managed.conf";
|
||||||
|
var managedPath = Path.Combine(dir, managedName);
|
||||||
|
ConfigSetCache? cache = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(mainPath, "port=53\n");
|
||||||
|
var options = Options.Create(new DnsmasqOptions { MainConfigPath = mainPath, ManagedFileName = managedName });
|
||||||
|
cache = new ConfigSetCache(options, NullLogger<ConfigSetCache>.Instance);
|
||||||
|
var setService = new DnsmasqConfigSetService(cache);
|
||||||
|
var configService = new DnsmasqConfigService(setService, cache, NullLogger<DnsmasqConfigService>.Instance);
|
||||||
|
|
||||||
|
var changes = new List<PendingEffectiveConfigChange>
|
||||||
|
{
|
||||||
|
new(EffectiveConfigSections.SectionCache, DnsmasqConfKeys.StripMac, false, true, null)
|
||||||
|
};
|
||||||
|
await configService.ApplyEffectiveConfigChangesAsync(changes);
|
||||||
|
|
||||||
|
Assert.True(File.Exists(managedPath));
|
||||||
|
var content = await File.ReadAllTextAsync(managedPath);
|
||||||
|
var lines = content.TrimEnd().Split('\n').Select(l => l.Trim()).Where(l => l.Length > 0).ToList();
|
||||||
|
Assert.Contains(lines, l => l == DnsmasqConfKeys.StripMac);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
cache?.Dispose();
|
||||||
|
if (Directory.Exists(dir))
|
||||||
|
Directory.Delete(dir, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ApplyEffectiveConfigChangesAsync_StripSubnetFalse_RemovesFlagLine()
|
||||||
|
{
|
||||||
|
var dir = Path.Combine(Path.GetTempPath(), "dnsmasq-apply-" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
var mainPath = Path.Combine(dir, "dnsmasq.conf");
|
||||||
|
var managedName = "zz-managed.conf";
|
||||||
|
var managedPath = Path.Combine(dir, managedName);
|
||||||
|
ConfigSetCache? cache = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(mainPath, "port=53\n");
|
||||||
|
File.WriteAllText(managedPath, "strip-subnet\n");
|
||||||
|
var options = Options.Create(new DnsmasqOptions { MainConfigPath = mainPath, ManagedFileName = managedName });
|
||||||
|
cache = new ConfigSetCache(options, NullLogger<ConfigSetCache>.Instance);
|
||||||
|
cache.Invalidate();
|
||||||
|
var setService = new DnsmasqConfigSetService(cache);
|
||||||
|
var configService = new DnsmasqConfigService(setService, cache, NullLogger<DnsmasqConfigService>.Instance);
|
||||||
|
|
||||||
|
var changes = new List<PendingEffectiveConfigChange>
|
||||||
|
{
|
||||||
|
new(EffectiveConfigSections.SectionCache, DnsmasqConfKeys.StripSubnet, true, false, null)
|
||||||
|
};
|
||||||
|
await configService.ApplyEffectiveConfigChangesAsync(changes);
|
||||||
|
|
||||||
|
var content = await File.ReadAllTextAsync(managedPath);
|
||||||
|
var lines = content.TrimEnd().Split('\n').Select(l => l.Trim()).Where(l => l.Length > 0).ToList();
|
||||||
|
Assert.DoesNotContain(lines, l => l == DnsmasqConfKeys.StripSubnet || l.StartsWith($"{DnsmasqConfKeys.StripSubnet}=", StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
cache?.Dispose();
|
||||||
|
if (Directory.Exists(dir))
|
||||||
|
Directory.Delete(dir, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ApplyEffectiveConfigChangesAsync_KeyOnlyOrValue_KeyOnly_WritesBareKey()
|
public async Task ApplyEffectiveConfigChangesAsync_KeyOnlyOrValue_KeyOnly_WritesBareKey()
|
||||||
{
|
{
|
||||||
@@ -157,4 +230,249 @@ public class DnsmasqConfigServiceApplyChangesTests
|
|||||||
Directory.Delete(dir, recursive: true);
|
Directory.Delete(dir, recursive: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ApplyChanges_Do0x20_Default_RemovesBothLines()
|
||||||
|
{
|
||||||
|
var dir = Path.Combine(Path.GetTempPath(), "dnsmasq-apply-" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
var mainPath = Path.Combine(dir, "dnsmasq.conf");
|
||||||
|
var managedName = "zz-managed.conf";
|
||||||
|
var managedPath = Path.Combine(dir, managedName);
|
||||||
|
ConfigSetCache? cache = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(mainPath, "port=53\n");
|
||||||
|
File.WriteAllText(managedPath, "do-0x20-encode\nno-0x20-encode\n");
|
||||||
|
var options = Options.Create(new DnsmasqOptions { MainConfigPath = mainPath, ManagedFileName = managedName });
|
||||||
|
cache = new ConfigSetCache(options, NullLogger<ConfigSetCache>.Instance);
|
||||||
|
cache.Invalidate();
|
||||||
|
var setService = new DnsmasqConfigSetService(cache);
|
||||||
|
var configService = new DnsmasqConfigService(setService, cache, NullLogger<DnsmasqConfigService>.Instance);
|
||||||
|
|
||||||
|
var changes = new List<PendingEffectiveConfigChange>
|
||||||
|
{
|
||||||
|
new(EffectiveConfigSections.SectionResolver, DnsmasqConfKeys.Do0x20Encode, ExplicitToggleState.Disabled, ExplicitToggleState.Default, null)
|
||||||
|
};
|
||||||
|
await configService.ApplyEffectiveConfigChangesAsync(changes);
|
||||||
|
|
||||||
|
var content = await File.ReadAllTextAsync(managedPath);
|
||||||
|
var lines = content.TrimEnd().Split('\n').Select(l => l.Trim()).Where(l => l.Length > 0).ToList();
|
||||||
|
Assert.DoesNotContain(lines, l => l == "do-0x20-encode" || l == "no-0x20-encode");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
cache?.Dispose();
|
||||||
|
if (Directory.Exists(dir))
|
||||||
|
Directory.Delete(dir, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ApplyChanges_Do0x20_Disabled_WritesNo0x20Encode()
|
||||||
|
{
|
||||||
|
var dir = Path.Combine(Path.GetTempPath(), "dnsmasq-apply-" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
var mainPath = Path.Combine(dir, "dnsmasq.conf");
|
||||||
|
var managedName = "zz-managed.conf";
|
||||||
|
var managedPath = Path.Combine(dir, managedName);
|
||||||
|
ConfigSetCache? cache = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(mainPath, "port=53\n");
|
||||||
|
var options = Options.Create(new DnsmasqOptions { MainConfigPath = mainPath, ManagedFileName = managedName });
|
||||||
|
cache = new ConfigSetCache(options, NullLogger<ConfigSetCache>.Instance);
|
||||||
|
var setService = new DnsmasqConfigSetService(cache);
|
||||||
|
var configService = new DnsmasqConfigService(setService, cache, NullLogger<DnsmasqConfigService>.Instance);
|
||||||
|
|
||||||
|
var changes = new List<PendingEffectiveConfigChange>
|
||||||
|
{
|
||||||
|
new(EffectiveConfigSections.SectionResolver, DnsmasqConfKeys.Do0x20Encode, ExplicitToggleState.Default, ExplicitToggleState.Disabled, null)
|
||||||
|
};
|
||||||
|
await configService.ApplyEffectiveConfigChangesAsync(changes);
|
||||||
|
|
||||||
|
var content = await File.ReadAllTextAsync(managedPath);
|
||||||
|
Assert.Contains("no-0x20-encode", content);
|
||||||
|
Assert.DoesNotContain("do-0x20-encode", content);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
cache?.Dispose();
|
||||||
|
if (Directory.Exists(dir))
|
||||||
|
Directory.Delete(dir, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ApplyChanges_UseStaleCache_Value_WritesKeyEqualsValue()
|
||||||
|
{
|
||||||
|
var dir = Path.Combine(Path.GetTempPath(), "dnsmasq-apply-" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
var mainPath = Path.Combine(dir, "dnsmasq.conf");
|
||||||
|
var managedName = "zz-managed.conf";
|
||||||
|
var managedPath = Path.Combine(dir, managedName);
|
||||||
|
ConfigSetCache? cache = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(mainPath, "port=53\n");
|
||||||
|
var options = Options.Create(new DnsmasqOptions { MainConfigPath = mainPath, ManagedFileName = managedName });
|
||||||
|
cache = new ConfigSetCache(options, NullLogger<ConfigSetCache>.Instance);
|
||||||
|
var setService = new DnsmasqConfigSetService(cache);
|
||||||
|
var configService = new DnsmasqConfigService(setService, cache, NullLogger<DnsmasqConfigService>.Instance);
|
||||||
|
|
||||||
|
var changes = new List<PendingEffectiveConfigChange>
|
||||||
|
{
|
||||||
|
new(EffectiveConfigSections.SectionCache, DnsmasqConfKeys.UseStaleCache, null, "60", null)
|
||||||
|
};
|
||||||
|
await configService.ApplyEffectiveConfigChangesAsync(changes);
|
||||||
|
|
||||||
|
var content = await File.ReadAllTextAsync(managedPath);
|
||||||
|
Assert.Contains("use-stale-cache=60", content);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
cache?.Dispose();
|
||||||
|
if (Directory.Exists(dir))
|
||||||
|
Directory.Delete(dir, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ApplyChanges_AddMac_KeyOnly_WritesBareKey()
|
||||||
|
{
|
||||||
|
var dir = Path.Combine(Path.GetTempPath(), "dnsmasq-apply-" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
var mainPath = Path.Combine(dir, "dnsmasq.conf");
|
||||||
|
var managedName = "zz-managed.conf";
|
||||||
|
var managedPath = Path.Combine(dir, managedName);
|
||||||
|
ConfigSetCache? cache = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(mainPath, "port=53\n");
|
||||||
|
var options = Options.Create(new DnsmasqOptions { MainConfigPath = mainPath, ManagedFileName = managedName });
|
||||||
|
cache = new ConfigSetCache(options, NullLogger<ConfigSetCache>.Instance);
|
||||||
|
var setService = new DnsmasqConfigSetService(cache);
|
||||||
|
var configService = new DnsmasqConfigService(setService, cache, NullLogger<DnsmasqConfigService>.Instance);
|
||||||
|
|
||||||
|
var changes = new List<PendingEffectiveConfigChange>
|
||||||
|
{
|
||||||
|
new(EffectiveConfigSections.SectionCache, DnsmasqConfKeys.AddMac, null, "", null)
|
||||||
|
};
|
||||||
|
await configService.ApplyEffectiveConfigChangesAsync(changes);
|
||||||
|
|
||||||
|
var content = await File.ReadAllTextAsync(managedPath);
|
||||||
|
Assert.Contains("add-mac", content);
|
||||||
|
Assert.DoesNotContain("add-mac=", content);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
cache?.Dispose();
|
||||||
|
if (Directory.Exists(dir))
|
||||||
|
Directory.Delete(dir, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ApplyChanges_AddMac_ValueBase64_WritesKeyEqualsValue()
|
||||||
|
{
|
||||||
|
var dir = Path.Combine(Path.GetTempPath(), "dnsmasq-apply-" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
var mainPath = Path.Combine(dir, "dnsmasq.conf");
|
||||||
|
var managedName = "zz-managed.conf";
|
||||||
|
var managedPath = Path.Combine(dir, managedName);
|
||||||
|
ConfigSetCache? cache = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(mainPath, "port=53\n");
|
||||||
|
var options = Options.Create(new DnsmasqOptions { MainConfigPath = mainPath, ManagedFileName = managedName });
|
||||||
|
cache = new ConfigSetCache(options, NullLogger<ConfigSetCache>.Instance);
|
||||||
|
var setService = new DnsmasqConfigSetService(cache);
|
||||||
|
var configService = new DnsmasqConfigService(setService, cache, NullLogger<DnsmasqConfigService>.Instance);
|
||||||
|
|
||||||
|
var changes = new List<PendingEffectiveConfigChange>
|
||||||
|
{
|
||||||
|
new(EffectiveConfigSections.SectionCache, DnsmasqConfKeys.AddMac, null, "base64", null)
|
||||||
|
};
|
||||||
|
await configService.ApplyEffectiveConfigChangesAsync(changes);
|
||||||
|
|
||||||
|
var content = await File.ReadAllTextAsync(managedPath);
|
||||||
|
Assert.Contains("add-mac=base64", content);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
cache?.Dispose();
|
||||||
|
if (Directory.Exists(dir))
|
||||||
|
Directory.Delete(dir, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ApplyChanges_AddSubnet_KeyOnly_WritesBareKey()
|
||||||
|
{
|
||||||
|
var dir = Path.Combine(Path.GetTempPath(), "dnsmasq-apply-" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
var mainPath = Path.Combine(dir, "dnsmasq.conf");
|
||||||
|
var managedName = "zz-managed.conf";
|
||||||
|
var managedPath = Path.Combine(dir, managedName);
|
||||||
|
ConfigSetCache? cache = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(mainPath, "port=53\n");
|
||||||
|
var options = Options.Create(new DnsmasqOptions { MainConfigPath = mainPath, ManagedFileName = managedName });
|
||||||
|
cache = new ConfigSetCache(options, NullLogger<ConfigSetCache>.Instance);
|
||||||
|
var setService = new DnsmasqConfigSetService(cache);
|
||||||
|
var configService = new DnsmasqConfigService(setService, cache, NullLogger<DnsmasqConfigService>.Instance);
|
||||||
|
|
||||||
|
var changes = new List<PendingEffectiveConfigChange>
|
||||||
|
{
|
||||||
|
new(EffectiveConfigSections.SectionCache, DnsmasqConfKeys.AddSubnet, null, "", null)
|
||||||
|
};
|
||||||
|
await configService.ApplyEffectiveConfigChangesAsync(changes);
|
||||||
|
|
||||||
|
var content = await File.ReadAllTextAsync(managedPath);
|
||||||
|
Assert.Contains("add-subnet", content);
|
||||||
|
Assert.DoesNotContain("add-subnet=", content);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
cache?.Dispose();
|
||||||
|
if (Directory.Exists(dir))
|
||||||
|
Directory.Delete(dir, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ApplyChanges_Umbrella_KeyOnly_WritesBareKey()
|
||||||
|
{
|
||||||
|
var dir = Path.Combine(Path.GetTempPath(), "dnsmasq-apply-" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
var mainPath = Path.Combine(dir, "dnsmasq.conf");
|
||||||
|
var managedName = "zz-managed.conf";
|
||||||
|
var managedPath = Path.Combine(dir, managedName);
|
||||||
|
ConfigSetCache? cache = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(mainPath, "port=53\n");
|
||||||
|
var options = Options.Create(new DnsmasqOptions { MainConfigPath = mainPath, ManagedFileName = managedName });
|
||||||
|
cache = new ConfigSetCache(options, NullLogger<ConfigSetCache>.Instance);
|
||||||
|
var setService = new DnsmasqConfigSetService(cache);
|
||||||
|
var configService = new DnsmasqConfigService(setService, cache, NullLogger<DnsmasqConfigService>.Instance);
|
||||||
|
|
||||||
|
var changes = new List<PendingEffectiveConfigChange>
|
||||||
|
{
|
||||||
|
new(EffectiveConfigSections.SectionCache, DnsmasqConfKeys.Umbrella, null, "", null)
|
||||||
|
};
|
||||||
|
await configService.ApplyEffectiveConfigChangesAsync(changes);
|
||||||
|
|
||||||
|
var content = await File.ReadAllTextAsync(managedPath);
|
||||||
|
Assert.Contains("umbrella", content);
|
||||||
|
Assert.DoesNotContain("umbrella=", content);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
cache?.Dispose();
|
||||||
|
if (Directory.Exists(dir))
|
||||||
|
Directory.Delete(dir, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Documents and guards the Do0x20 display label rule: Default and Disabled both show "Disabled" (product choice).
|
||||||
|
/// Do not "fix" the display to show "Default" for unset without updating this test and the component comment.
|
||||||
|
/// </summary>
|
||||||
|
public class Do0x20EncodeDisplayLabelTests
|
||||||
|
{
|
||||||
|
/// <summary>Same logic as Do0x20EncodeDisplay.razor view mode: Enabled → "Enabled", else "Disabled".</summary>
|
||||||
|
private static string GetDisplayLabel(ExplicitToggleState state) =>
|
||||||
|
state == ExplicitToggleState.Enabled ? "Enabled" : "Disabled";
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Do0x20_Default_ShowsDisabled()
|
||||||
|
{
|
||||||
|
Assert.Equal("Disabled", GetDisplayLabel(ExplicitToggleState.Default));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Do0x20_Disabled_ShowsDisabled()
|
||||||
|
{
|
||||||
|
Assert.Equal("Disabled", GetDisplayLabel(ExplicitToggleState.Disabled));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Do0x20_Enabled_ShowsEnabled()
|
||||||
|
{
|
||||||
|
Assert.Equal("Enabled", GetDisplayLabel(ExplicitToggleState.Enabled));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures effective-config editable options list includes supported cache flags.
|
||||||
|
/// </summary>
|
||||||
|
public class EffectiveConfigEditableOptionsTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// strip-mac and strip-subnet are supported in the effective-config UI and should be listed in cache options.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void EditableOptions_Include_StripMac_And_StripSubnet()
|
||||||
|
{
|
||||||
|
var allOptionNames = EffectiveConfigSections.GetSectionsInOrder()
|
||||||
|
.SelectMany(t => EffectiveConfigSections.GetOptionsInSection(t.SectionId))
|
||||||
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
Assert.Contains(DnsmasqConfKeys.StripMac, allOptionNames);
|
||||||
|
Assert.Contains(DnsmasqConfKeys.StripSubnet, allOptionNames);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -168,7 +168,9 @@ public class EffectiveDnsmasqConfigTests
|
|||||||
DhcpPxeVendor: null,
|
DhcpPxeVendor: null,
|
||||||
UseStaleCache: null,
|
UseStaleCache: null,
|
||||||
AddMac: null,
|
AddMac: null,
|
||||||
|
StripMac: false,
|
||||||
AddSubnet: null,
|
AddSubnet: null,
|
||||||
|
StripSubnet: false,
|
||||||
Umbrella: null,
|
Umbrella: null,
|
||||||
Do0x20EncodeState: ExplicitToggleState.Default,
|
Do0x20EncodeState: ExplicitToggleState.Default,
|
||||||
Conntrack: false
|
Conntrack: false
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@* Intentionally show both Default and Disabled as "Disabled" (product choice: collapse unset and off). Do not "fix" to show "Default" for unset. *@
|
||||||
@(GetState() == ExplicitToggleState.Enabled ? "Enabled" : "Disabled")
|
@(GetState() == ExplicitToggleState.Enabled ? "Enabled" : "Disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+30
-43
@@ -3,7 +3,9 @@
|
|||||||
@using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig
|
@using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig
|
||||||
@using Microsoft.AspNetCore.Components.Web
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
@using DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation
|
@using DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation
|
||||||
|
@using DnsmasqWebUI.Infrastructure.Services.UI.Notifications.Abstractions
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
|
@inject INotificationService NotificationService
|
||||||
|
|
||||||
<div class="ec-field-value ec-field-value-multi ec-multi-list-editor" @onclick:stopPropagation="true">
|
<div class="ec-field-value ec-field-value-multi ec-multi-list-editor" @onclick:stopPropagation="true">
|
||||||
<ol class="ec-field-value-list">
|
<ol class="ec-field-value-list">
|
||||||
@@ -16,20 +18,15 @@
|
|||||||
var itemReadonly = item.Source?.IsReadOnly == true;
|
var itemReadonly = item.Source?.IsReadOnly == true;
|
||||||
var rowIndex = i;
|
var rowIndex = i;
|
||||||
var rowValue = item.Value;
|
var rowValue = item.Value;
|
||||||
var rowInlineError = GetInlineErrorForIndex(rowIndex);
|
|
||||||
|
|
||||||
<li class="ec-multi-list-row @(_editingIndex == rowIndex && !itemReadonly ? "ec-multi-list-row-with-controls" : "")" @key="anchorId">
|
<li class="ec-multi-list-row @(_editingIndex == rowIndex && !itemReadonly ? "ec-multi-list-row-with-controls" : "")" @key="anchorId">
|
||||||
@if (_editingIndex == rowIndex && !itemReadonly)
|
@if (_editingIndex == rowIndex && !itemReadonly)
|
||||||
{
|
{
|
||||||
<div class="ec-multi-list-row-controls">
|
<div class="ec-multi-list-row-controls">
|
||||||
<div class="ec-multi-list-input-group">
|
<div class="ec-multi-list-input-group">
|
||||||
<input class="form-control form-control-sm ec-multi-list-input @(rowInlineError != null ? "is-invalid" : "")"
|
<input class="form-control form-control-sm ec-multi-list-input"
|
||||||
placeholder="@(Behavior?.Placeholder ?? "")"
|
placeholder="@(Behavior?.Placeholder ?? "")"
|
||||||
@bind="_draftValue" @bind:event="oninput" />
|
@bind="_draftValue" @bind:event="oninput" />
|
||||||
@if (rowInlineError != null)
|
|
||||||
{
|
|
||||||
<div class="ec-multi-list-invalid-feedback invalid-feedback d-block">@rowInlineError</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<span class="status-badge-inline">
|
<span class="status-badge-inline">
|
||||||
<StatusBadge Kind="StatusBadgeKind.ActionOk" Text="save" Clickable="true"
|
<StatusBadge Kind="StatusBadgeKind.ActionOk" Text="save" Clickable="true"
|
||||||
@@ -81,13 +78,9 @@
|
|||||||
{
|
{
|
||||||
<div class="ec-multi-list-row-controls">
|
<div class="ec-multi-list-row-controls">
|
||||||
<div class="ec-multi-list-input-group">
|
<div class="ec-multi-list-input-group">
|
||||||
<input @ref="_addNewInputRef" class="form-control form-control-sm ec-multi-list-input @(AddInlineError != null ? "is-invalid" : "")"
|
<input @ref="_addNewInputRef" class="form-control form-control-sm ec-multi-list-input"
|
||||||
placeholder="@(Behavior?.Placeholder ?? "New value")"
|
placeholder="@(Behavior?.Placeholder ?? "New value")"
|
||||||
@bind="_draftValue" @bind:event="oninput" />
|
@bind="_draftValue" @bind:event="oninput" />
|
||||||
@if (AddInlineError != null)
|
|
||||||
{
|
|
||||||
<div class="ec-multi-list-invalid-feedback invalid-feedback d-block">@AddInlineError</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<span class="status-badge-inline">
|
<span class="status-badge-inline">
|
||||||
<StatusBadge Kind="StatusBadgeKind.ActionOk" Text="add" Clickable="true"
|
<StatusBadge Kind="StatusBadgeKind.ActionOk" Text="add" Clickable="true"
|
||||||
@@ -125,24 +118,20 @@
|
|||||||
[Parameter] public EventCallback<IReadOnlyList<string>> OnValuesChanged { get; set; }
|
[Parameter] public EventCallback<IReadOnlyList<string>> OnValuesChanged { get; set; }
|
||||||
|
|
||||||
private const string AddItemKey = "add-item";
|
private const string AddItemKey = "add-item";
|
||||||
|
private static readonly TimeSpan ValidationToastDedupeWindow = TimeSpan.FromSeconds(2);
|
||||||
|
|
||||||
private int? _editingIndex;
|
private int? _editingIndex;
|
||||||
private bool _addingNew;
|
private bool _addingNew;
|
||||||
private string _draftValue = "";
|
private string _draftValue = "";
|
||||||
private bool _didAutoOpenAdd;
|
private bool _didAutoOpenAdd;
|
||||||
private bool _shouldFocusAddInput;
|
private bool _shouldFocusAddInput;
|
||||||
private ElementReference _addNewInputRef;
|
private ElementReference _addNewInputRef;
|
||||||
private int? _inlineErrorIndex;
|
private string? _lastValidationErrorMessage;
|
||||||
private string? _inlineErrorMessage;
|
private DateTime _lastValidationErrorUtc;
|
||||||
|
|
||||||
private List<ValueWithSource> _displayItems = new();
|
private List<ValueWithSource> _displayItems = new();
|
||||||
private IReadOnlyList<string> _currentValues => _displayItems.Select(x => x.Value).ToList();
|
private IReadOnlyList<string> _currentValues => _displayItems.Select(x => x.Value).ToList();
|
||||||
|
|
||||||
private string? AddInlineError => _addingNew && _inlineErrorIndex is null ? _inlineErrorMessage : null;
|
|
||||||
private string? GetInlineErrorForIndex(int? editIndex)
|
|
||||||
{
|
|
||||||
return _editingIndex == editIndex && _inlineErrorIndex == editIndex ? _inlineErrorMessage : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnParametersSet()
|
protected override void OnParametersSet()
|
||||||
{
|
{
|
||||||
_displayItems = (SourceItems ?? Array.Empty<ValueWithSource>()).ToList();
|
_displayItems = (SourceItems ?? Array.Empty<ValueWithSource>()).ToList();
|
||||||
@@ -176,7 +165,6 @@
|
|||||||
_addingNew = false;
|
_addingNew = false;
|
||||||
_editingIndex = index;
|
_editingIndex = index;
|
||||||
_draftValue = current;
|
_draftValue = current;
|
||||||
ClearInlineError();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BeginAdd()
|
private void BeginAdd()
|
||||||
@@ -185,7 +173,6 @@
|
|||||||
_addingNew = true;
|
_addingNew = true;
|
||||||
_draftValue = "";
|
_draftValue = "";
|
||||||
_shouldFocusAddInput = true;
|
_shouldFocusAddInput = true;
|
||||||
ClearInlineError();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CancelInline()
|
private void CancelInline()
|
||||||
@@ -193,7 +180,18 @@
|
|||||||
_editingIndex = null;
|
_editingIndex = null;
|
||||||
_addingNew = false;
|
_addingNew = false;
|
||||||
_draftValue = "";
|
_draftValue = "";
|
||||||
ClearInlineError();
|
_lastValidationErrorMessage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowValidationError(string message)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if (string.Equals(_lastValidationErrorMessage, message, StringComparison.Ordinal) &&
|
||||||
|
(now - _lastValidationErrorUtc) < ValidationToastDedupeWindow)
|
||||||
|
return;
|
||||||
|
_lastValidationErrorMessage = message;
|
||||||
|
_lastValidationErrorUtc = now;
|
||||||
|
NotificationService.ShowError(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SaveEdit(int index)
|
private async Task SaveEdit(int index)
|
||||||
@@ -206,24 +204,23 @@
|
|||||||
var normalized = Behavior?.Normalize(raw) ?? raw.Trim();
|
var normalized = Behavior?.Normalize(raw) ?? raw.Trim();
|
||||||
if (normalized.Length == 0)
|
if (normalized.Length == 0)
|
||||||
{
|
{
|
||||||
SetInlineError(index, "Value cannot be empty.");
|
ShowValidationError("Value cannot be empty.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var validationError = Validator?.ValidateItem(normalized, current, index);
|
var validationError = Validator?.ValidateItem(normalized, current, index);
|
||||||
if (validationError != null)
|
if (validationError != null)
|
||||||
{
|
{
|
||||||
SetInlineError(index, validationError);
|
ShowValidationError(validationError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Behavior?.AllowDuplicates == false && next.Where((v, i) => i != index).Any(v => string.Equals(v, normalized, StringComparison.Ordinal)))
|
if (Behavior?.AllowDuplicates == false && next.Where((v, i) => i != index).Any(v => string.Equals(v, normalized, StringComparison.Ordinal)))
|
||||||
{
|
{
|
||||||
SetInlineError(index, "Duplicate values are not allowed.");
|
ShowValidationError("Duplicate values are not allowed.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ClearInlineError();
|
|
||||||
next[index] = normalized;
|
next[index] = normalized;
|
||||||
_displayItems[index] = _displayItems[index] with { Value = normalized };
|
_displayItems[index] = _displayItems[index] with { Value = normalized };
|
||||||
await OnValuesChanged.InvokeAsync(next);
|
await OnValuesChanged.InvokeAsync(next);
|
||||||
@@ -234,23 +231,26 @@
|
|||||||
{
|
{
|
||||||
var raw = _draftValue ?? "";
|
var raw = _draftValue ?? "";
|
||||||
var normalized = Behavior?.Normalize(raw) ?? raw.Trim();
|
var normalized = Behavior?.Normalize(raw) ?? raw.Trim();
|
||||||
if (normalized.Length == 0) return;
|
if (normalized.Length == 0)
|
||||||
|
{
|
||||||
|
ShowValidationError("Value cannot be empty.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var current = _currentValues;
|
var current = _currentValues;
|
||||||
var validationError = Validator?.ValidateItem(normalized, current, null);
|
var validationError = Validator?.ValidateItem(normalized, current, null);
|
||||||
if (validationError != null)
|
if (validationError != null)
|
||||||
{
|
{
|
||||||
SetInlineError(null, validationError);
|
ShowValidationError(validationError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Behavior?.AllowDuplicates == false && current.Contains(normalized, StringComparer.Ordinal))
|
if (Behavior?.AllowDuplicates == false && current.Contains(normalized, StringComparer.Ordinal))
|
||||||
{
|
{
|
||||||
SetInlineError(null, "Duplicate values are not allowed.");
|
ShowValidationError("Duplicate values are not allowed.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ClearInlineError();
|
|
||||||
var next = current.ToList();
|
var next = current.ToList();
|
||||||
next.Add(normalized);
|
next.Add(normalized);
|
||||||
_displayItems.Add(new ValueWithSource(normalized, null));
|
_displayItems.Add(new ValueWithSource(normalized, null));
|
||||||
@@ -258,19 +258,6 @@
|
|||||||
CancelInline();
|
CancelInline();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetInlineError(int? itemIndex, string message)
|
|
||||||
{
|
|
||||||
_inlineErrorIndex = itemIndex;
|
|
||||||
_inlineErrorMessage = message;
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ClearInlineError()
|
|
||||||
{
|
|
||||||
_inlineErrorIndex = null;
|
|
||||||
_inlineErrorMessage = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RemoveAt(int index)
|
private async Task RemoveAt(int index)
|
||||||
{
|
{
|
||||||
if (index < 0 || index >= _displayItems.Count) return;
|
if (index < 0 || index >= _displayItems.Count) return;
|
||||||
|
|||||||
@@ -284,7 +284,7 @@
|
|||||||
|
|
||||||
private static string FormatValue(object? v, string? optionName = null)
|
private static string FormatValue(object? v, string? optionName = null)
|
||||||
{
|
{
|
||||||
if (optionName != null && EffectiveConfigWriteBehaviorMap.GetBehavior(optionName) == EffectiveConfigWriteBehavior.KeyOnlyOrValue)
|
if (optionName != null && EffectiveConfigWriteSemantics.GetBehavior(optionName) == EffectiveConfigWriteBehavior.KeyOnlyOrValue)
|
||||||
return FormatKeyOnlyOrValue(v);
|
return FormatKeyOnlyOrValue(v);
|
||||||
if (v == null) return "(not set)";
|
if (v == null) return "(not set)";
|
||||||
if (v is bool b) return b ? "on" : "off";
|
if (v is bool b) return b ? "on" : "off";
|
||||||
|
|||||||
@@ -166,7 +166,9 @@ public static class DnsmasqOptionTooltips
|
|||||||
[DnsmasqConfKeys.Dumpmask] = "Mask for cache dump (which data to include).",
|
[DnsmasqConfKeys.Dumpmask] = "Mask for cache dump (which data to include).",
|
||||||
[DnsmasqConfKeys.UseStaleCache] = "Use stale cache when upstream is unavailable. Off, On (key-only), or custom seconds.",
|
[DnsmasqConfKeys.UseStaleCache] = "Use stale cache when upstream is unavailable. Off, On (key-only), or custom seconds.",
|
||||||
[DnsmasqConfKeys.AddMac] = "Add MAC address to DNS queries. Off, On (key-only), or custom value.",
|
[DnsmasqConfKeys.AddMac] = "Add MAC address to DNS queries. Off, On (key-only), or custom value.",
|
||||||
|
[DnsmasqConfKeys.StripMac] = "Strip MAC address information from forwarded DNS queries.",
|
||||||
[DnsmasqConfKeys.AddSubnet] = "Add subnet to DNS queries. Off, On (key-only), or custom value.",
|
[DnsmasqConfKeys.AddSubnet] = "Add subnet to DNS queries. Off, On (key-only), or custom value.",
|
||||||
|
[DnsmasqConfKeys.StripSubnet] = "Strip subnet (ECS) information from forwarded DNS queries.",
|
||||||
[DnsmasqConfKeys.Umbrella] = "Cisco Umbrella device identity. Off, On (key-only), or custom value.",
|
[DnsmasqConfKeys.Umbrella] = "Cisco Umbrella device identity. Off, On (key-only), or custom value.",
|
||||||
|
|
||||||
// --- Process & networking ---
|
// --- Process & networking ---
|
||||||
|
|||||||
@@ -194,7 +194,9 @@ public static class EffectiveConfigFieldBuilder
|
|||||||
list.AddDescriptor(registry, DnsmasqConfKeys.Dumpmask, status, s => Config(s)?.Dumpmask, s => Sources(s)?.Dumpmask, null);
|
list.AddDescriptor(registry, DnsmasqConfKeys.Dumpmask, status, s => Config(s)?.Dumpmask, s => Sources(s)?.Dumpmask, null);
|
||||||
list.AddDescriptor(registry, DnsmasqConfKeys.UseStaleCache, status, s => Config(s)?.UseStaleCache, s => Sources(s)?.UseStaleCache, null);
|
list.AddDescriptor(registry, DnsmasqConfKeys.UseStaleCache, status, s => Config(s)?.UseStaleCache, s => Sources(s)?.UseStaleCache, null);
|
||||||
list.AddDescriptor(registry, DnsmasqConfKeys.AddMac, status, s => Config(s)?.AddMac, s => Sources(s)?.AddMac, null);
|
list.AddDescriptor(registry, DnsmasqConfKeys.AddMac, status, s => Config(s)?.AddMac, s => Sources(s)?.AddMac, null);
|
||||||
|
list.AddDescriptor(registry, DnsmasqConfKeys.StripMac, status, s => Config(s)?.StripMac, s => Sources(s)?.StripMac, null);
|
||||||
list.AddDescriptor(registry, DnsmasqConfKeys.AddSubnet, status, s => Config(s)?.AddSubnet, s => Sources(s)?.AddSubnet, null);
|
list.AddDescriptor(registry, DnsmasqConfKeys.AddSubnet, status, s => Config(s)?.AddSubnet, s => Sources(s)?.AddSubnet, null);
|
||||||
|
list.AddDescriptor(registry, DnsmasqConfKeys.StripSubnet, status, s => Config(s)?.StripSubnet, s => Sources(s)?.StripSubnet, null);
|
||||||
list.AddDescriptor(registry, DnsmasqConfKeys.Umbrella, status, s => Config(s)?.Umbrella, s => Sources(s)?.Umbrella, null);
|
list.AddDescriptor(registry, DnsmasqConfKeys.Umbrella, status, s => Config(s)?.Umbrella, s => Sources(s)?.Umbrella, null);
|
||||||
list.AddDescriptor(registry, DnsmasqConfKeys.NegTtl, status, s => Config(s)?.NegTtl, s => Sources(s)?.NegTtl, null);
|
list.AddDescriptor(registry, DnsmasqConfKeys.NegTtl, status, s => Config(s)?.NegTtl, s => Sources(s)?.NegTtl, null);
|
||||||
list.AddDescriptor(registry, DnsmasqConfKeys.MaxTtl, status, s => Config(s)?.MaxTtl, s => Sources(s)?.MaxTtl, null);
|
list.AddDescriptor(registry, DnsmasqConfKeys.MaxTtl, status, s => Config(s)?.MaxTtl, s => Sources(s)?.MaxTtl, null);
|
||||||
|
|||||||
@@ -199,7 +199,9 @@ public static class EffectiveConfigOptionKindMap
|
|||||||
[DnsmasqConfKeys.DhcpOptionPxe] = EffectiveConfigFieldKind.Multi,
|
[DnsmasqConfKeys.DhcpOptionPxe] = EffectiveConfigFieldKind.Multi,
|
||||||
[DnsmasqConfKeys.UseStaleCache] = EffectiveConfigFieldKind.Single,
|
[DnsmasqConfKeys.UseStaleCache] = EffectiveConfigFieldKind.Single,
|
||||||
[DnsmasqConfKeys.AddMac] = EffectiveConfigFieldKind.Single,
|
[DnsmasqConfKeys.AddMac] = EffectiveConfigFieldKind.Single,
|
||||||
|
[DnsmasqConfKeys.StripMac] = EffectiveConfigFieldKind.Single,
|
||||||
[DnsmasqConfKeys.AddSubnet] = EffectiveConfigFieldKind.Single,
|
[DnsmasqConfKeys.AddSubnet] = EffectiveConfigFieldKind.Single,
|
||||||
|
[DnsmasqConfKeys.StripSubnet] = EffectiveConfigFieldKind.Single,
|
||||||
[DnsmasqConfKeys.Umbrella] = EffectiveConfigFieldKind.Single,
|
[DnsmasqConfKeys.Umbrella] = EffectiveConfigFieldKind.Single,
|
||||||
[DnsmasqConfKeys.Do0x20Encode] = EffectiveConfigFieldKind.Single,
|
[DnsmasqConfKeys.Do0x20Encode] = EffectiveConfigFieldKind.Single,
|
||||||
};
|
};
|
||||||
@@ -386,7 +388,9 @@ public static class EffectiveConfigParserBehaviorMap
|
|||||||
[DnsmasqConfKeys.DhcpOptionPxe] = EffectiveConfigParserBehavior.Multi,
|
[DnsmasqConfKeys.DhcpOptionPxe] = EffectiveConfigParserBehavior.Multi,
|
||||||
[DnsmasqConfKeys.UseStaleCache] = EffectiveConfigParserBehavior.LastWins,
|
[DnsmasqConfKeys.UseStaleCache] = EffectiveConfigParserBehavior.LastWins,
|
||||||
[DnsmasqConfKeys.AddMac] = EffectiveConfigParserBehavior.LastWins,
|
[DnsmasqConfKeys.AddMac] = EffectiveConfigParserBehavior.LastWins,
|
||||||
|
[DnsmasqConfKeys.StripMac] = EffectiveConfigParserBehavior.Flag,
|
||||||
[DnsmasqConfKeys.AddSubnet] = EffectiveConfigParserBehavior.LastWins,
|
[DnsmasqConfKeys.AddSubnet] = EffectiveConfigParserBehavior.LastWins,
|
||||||
|
[DnsmasqConfKeys.StripSubnet] = EffectiveConfigParserBehavior.Flag,
|
||||||
[DnsmasqConfKeys.Umbrella] = EffectiveConfigParserBehavior.LastWins,
|
[DnsmasqConfKeys.Umbrella] = EffectiveConfigParserBehavior.LastWins,
|
||||||
[DnsmasqConfKeys.Do0x20Encode] = EffectiveConfigParserBehavior.Flag,
|
[DnsmasqConfKeys.Do0x20Encode] = EffectiveConfigParserBehavior.Flag,
|
||||||
[DnsmasqConfKeys.No0x20Encode] = EffectiveConfigParserBehavior.Flag,
|
[DnsmasqConfKeys.No0x20Encode] = EffectiveConfigParserBehavior.Flag,
|
||||||
@@ -397,48 +401,3 @@ public static class EffectiveConfigParserBehaviorMap
|
|||||||
BehaviorByOptionName.TryGetValue(optionName, out var b) ? b : EffectiveConfigParserBehavior.LastWins;
|
BehaviorByOptionName.TryGetValue(optionName, out var b) ? b : EffectiveConfigParserBehavior.LastWins;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// How to serialize an option when writing the managed config file.
|
|
||||||
/// Used by DnsmasqConfigService so KeyOnlyOrValue and InversePair options serialize correctly.
|
|
||||||
/// </summary>
|
|
||||||
public enum EffectiveConfigWriteBehavior
|
|
||||||
{
|
|
||||||
Flag,
|
|
||||||
SingleValue,
|
|
||||||
MultiValue,
|
|
||||||
KeyOnlyOrValue,
|
|
||||||
InversePair,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Maps dnsmasq option names to write behavior. Options not in the map default to SingleValue
|
|
||||||
/// (or are inferred from parser behavior in the service). Add entries for KeyOnlyOrValue and InversePair options.
|
|
||||||
/// </summary>
|
|
||||||
public static class EffectiveConfigWriteBehaviorMap
|
|
||||||
{
|
|
||||||
private static readonly IReadOnlyDictionary<string, EffectiveConfigWriteBehavior> BehaviorByOptionName =
|
|
||||||
new Dictionary<string, EffectiveConfigWriteBehavior>(StringComparer.Ordinal)
|
|
||||||
{
|
|
||||||
[DnsmasqConfKeys.Conntrack] = EffectiveConfigWriteBehavior.Flag,
|
|
||||||
[DnsmasqConfKeys.UseStaleCache] = EffectiveConfigWriteBehavior.KeyOnlyOrValue,
|
|
||||||
[DnsmasqConfKeys.AddMac] = EffectiveConfigWriteBehavior.KeyOnlyOrValue,
|
|
||||||
[DnsmasqConfKeys.AddSubnet] = EffectiveConfigWriteBehavior.KeyOnlyOrValue,
|
|
||||||
[DnsmasqConfKeys.Umbrella] = EffectiveConfigWriteBehavior.KeyOnlyOrValue,
|
|
||||||
[DnsmasqConfKeys.Do0x20Encode] = EffectiveConfigWriteBehavior.InversePair,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>For InversePair options: (key for "enabled", key for "disabled"). Null if not an InversePair or unknown.</summary>
|
|
||||||
private static readonly IReadOnlyDictionary<string, (string KeyA, string KeyB)> InversePairKeysByOptionName =
|
|
||||||
new Dictionary<string, (string, string)>(StringComparer.Ordinal)
|
|
||||||
{
|
|
||||||
[DnsmasqConfKeys.Do0x20Encode] = (DnsmasqConfKeys.Do0x20Encode, DnsmasqConfKeys.No0x20Encode),
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>Returns write behavior for the option; defaults to SingleValue if unknown.</summary>
|
|
||||||
public static EffectiveConfigWriteBehavior GetBehavior(string optionName) =>
|
|
||||||
BehaviorByOptionName.TryGetValue(optionName, out var b) ? b : EffectiveConfigWriteBehavior.SingleValue;
|
|
||||||
|
|
||||||
/// <summary>Returns the pair of config keys (enabled, disabled) for an InversePair option; null otherwise.</summary>
|
|
||||||
public static (string KeyA, string KeyB)? GetInversePairKeys(string optionName) =>
|
|
||||||
InversePairKeysByOptionName.TryGetValue(optionName, out var pair) ? pair : null;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -178,7 +178,9 @@ public static class EffectiveConfigSections
|
|||||||
DnsmasqConfKeys.Dumpmask,
|
DnsmasqConfKeys.Dumpmask,
|
||||||
DnsmasqConfKeys.UseStaleCache,
|
DnsmasqConfKeys.UseStaleCache,
|
||||||
DnsmasqConfKeys.AddMac,
|
DnsmasqConfKeys.AddMac,
|
||||||
|
DnsmasqConfKeys.StripMac,
|
||||||
DnsmasqConfKeys.AddSubnet,
|
DnsmasqConfKeys.AddSubnet,
|
||||||
|
DnsmasqConfKeys.StripSubnet,
|
||||||
DnsmasqConfKeys.Umbrella,
|
DnsmasqConfKeys.Umbrella,
|
||||||
]),
|
]),
|
||||||
new SectionDef(SectionProcess, "Process & networking", [
|
new SectionDef(SectionProcess, "Process & networking", [
|
||||||
|
|||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||||
|
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Single source of truth for options with special parse/write/validation semantics.
|
||||||
|
/// Prevents drift between parse, write, hint, and validation for UseStaleCache, AddMac, AddSubnet, Umbrella, Do0x20Encode.
|
||||||
|
/// Inverse-pair key names are not stored here; see <see cref="EffectiveConfigSpecialOptionSemantics.GetInversePairKeys"/>.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record OptionSemantics(
|
||||||
|
string OptionName,
|
||||||
|
EffectiveConfigParserBehavior ParserBehavior,
|
||||||
|
EffectiveConfigWriteBehavior WriteBehavior,
|
||||||
|
EffectiveConfigSingleValueValidator? Validator
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lookup for special-option semantics. Used by EffectiveConfigWriteSemantics and registry for validators.
|
||||||
|
/// Inverse-pair options (e.g. Do0x20Encode) have their (enabled key, disabled key) in a separate table.
|
||||||
|
/// </summary>
|
||||||
|
public static class EffectiveConfigSpecialOptionSemantics
|
||||||
|
{
|
||||||
|
private static readonly IReadOnlyDictionary<string, OptionSemantics> ByOptionName =
|
||||||
|
new Dictionary<string, OptionSemantics>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
[DnsmasqConfKeys.UseStaleCache] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.UseStaleCache,
|
||||||
|
EffectiveConfigParserBehavior.LastWins,
|
||||||
|
EffectiveConfigWriteBehavior.KeyOnlyOrValue,
|
||||||
|
SpecialOptionValidators.ValidateUseStaleCache),
|
||||||
|
[DnsmasqConfKeys.AddMac] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.AddMac,
|
||||||
|
EffectiveConfigParserBehavior.LastWins,
|
||||||
|
EffectiveConfigWriteBehavior.KeyOnlyOrValue,
|
||||||
|
SpecialOptionValidators.ValidateAddMac),
|
||||||
|
[DnsmasqConfKeys.AddSubnet] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.AddSubnet,
|
||||||
|
EffectiveConfigParserBehavior.LastWins,
|
||||||
|
EffectiveConfigWriteBehavior.KeyOnlyOrValue,
|
||||||
|
SpecialOptionValidators.ValidateAddSubnet),
|
||||||
|
[DnsmasqConfKeys.Umbrella] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.Umbrella,
|
||||||
|
EffectiveConfigParserBehavior.LastWins,
|
||||||
|
EffectiveConfigWriteBehavior.KeyOnlyOrValue,
|
||||||
|
SpecialOptionValidators.ValidateUmbrella),
|
||||||
|
[DnsmasqConfKeys.Do0x20Encode] = new OptionSemantics(
|
||||||
|
DnsmasqConfKeys.Do0x20Encode,
|
||||||
|
EffectiveConfigParserBehavior.Flag,
|
||||||
|
EffectiveConfigWriteBehavior.InversePair,
|
||||||
|
Validator: null),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>Keys (enabled, disabled) for InversePair options only. Used by write path and readonly hints.</summary>
|
||||||
|
private static readonly IReadOnlyDictionary<string, (string KeyA, string KeyB)> InversePairKeysByOptionName =
|
||||||
|
new Dictionary<string, (string, string)>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
[DnsmasqConfKeys.Do0x20Encode] = (DnsmasqConfKeys.Do0x20Encode, DnsmasqConfKeys.No0x20Encode),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>Returns semantics for a special option; null if not a special option.</summary>
|
||||||
|
public static OptionSemantics? TryGetSemantics(string optionName) =>
|
||||||
|
ByOptionName.TryGetValue(optionName, out var s) ? s : null;
|
||||||
|
|
||||||
|
/// <summary>Returns write behavior from semantics for special options; otherwise null (caller uses general map).</summary>
|
||||||
|
public static EffectiveConfigWriteBehavior? GetWriteBehavior(string optionName) =>
|
||||||
|
TryGetSemantics(optionName)?.WriteBehavior;
|
||||||
|
|
||||||
|
/// <summary>Returns the pair of config keys (enabled, disabled) for an InversePair option; null otherwise.</summary>
|
||||||
|
public static (string KeyA, string KeyB)? GetInversePairKeys(string optionName) =>
|
||||||
|
InversePairKeysByOptionName.TryGetValue(optionName, out var pair) ? pair : null;
|
||||||
|
|
||||||
|
/// <summary>Returns validator from semantics for special options; otherwise null.</summary>
|
||||||
|
public static EffectiveConfigSingleValueValidator? GetValidator(string optionName) =>
|
||||||
|
TryGetSemantics(optionName)?.Validator;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
namespace DnsmasqWebUI.Infrastructure.Helpers.Config;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How to serialize an option when writing the managed config file.
|
||||||
|
/// This models write intent, not parse behavior.
|
||||||
|
/// </summary>
|
||||||
|
public enum EffectiveConfigWriteBehavior
|
||||||
|
{
|
||||||
|
Flag,
|
||||||
|
SingleValue,
|
||||||
|
MultiValue,
|
||||||
|
KeyOnlyOrValue,
|
||||||
|
InversePair,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Source of truth for write semantics used by managed-config writing and readonly hints.
|
||||||
|
/// Special options come from <see cref="EffectiveConfigSpecialOptionSemantics"/>; all others
|
||||||
|
/// derive from parser behavior (Flag/Multi/LastWins).
|
||||||
|
/// </summary>
|
||||||
|
public static class EffectiveConfigWriteSemantics
|
||||||
|
{
|
||||||
|
/// <summary>Returns write behavior for an option.</summary>
|
||||||
|
public static EffectiveConfigWriteBehavior GetBehavior(string optionName)
|
||||||
|
{
|
||||||
|
if (EffectiveConfigSpecialOptionSemantics.GetWriteBehavior(optionName) is { } special)
|
||||||
|
return special;
|
||||||
|
return EffectiveConfigParserBehaviorMap.GetBehavior(optionName) switch
|
||||||
|
{
|
||||||
|
EffectiveConfigParserBehavior.Flag => EffectiveConfigWriteBehavior.Flag,
|
||||||
|
EffectiveConfigParserBehavior.Multi => EffectiveConfigWriteBehavior.MultiValue,
|
||||||
|
_ => EffectiveConfigWriteBehavior.SingleValue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns inverse-pair keys (enabled, disabled) for InversePair options; null otherwise.</summary>
|
||||||
|
public static (string KeyA, string KeyB)? GetInversePairKeys(string optionName) =>
|
||||||
|
EffectiveConfigSpecialOptionSemantics.GetInversePairKeys(optionName);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -5,22 +5,31 @@ namespace DnsmasqWebUI.Infrastructure.Helpers.Config;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generates copyable commands or hints for readonly effective-config values:
|
/// Generates copyable commands or hints for readonly effective-config values:
|
||||||
/// remove or edit the line in the source file, or override by adding to managed config.
|
/// remove or edit the line in the source file, or override by adding to managed config.
|
||||||
|
/// Uses EffectiveConfigWriteSemantics so inverse-pair and key-only-or-value options are handled correctly.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class ReadonlyConfigCommandGenerator
|
public static class ReadonlyConfigCommandGenerator
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Suggested command to remove the option line from the readonly file (sed). User can copy and run with appropriate privileges.
|
/// Suggested command to remove the option line from the readonly file (sed). User can copy and run with appropriate privileges.
|
||||||
/// Escapes the option name for a basic sed pattern (^option=\?.*$ or ^option$ for flags).
|
/// For InversePair options, removes both keys (e.g. do-0x20-encode and no-0x20-encode).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string GetRemoveLineCommand(ConfigValueSource source, string optionName)
|
public static string GetRemoveLineCommand(ConfigValueSource source, string optionName)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(source.FilePath)) return "";
|
if (string.IsNullOrEmpty(source.FilePath)) return "";
|
||||||
|
var pathEscaped = source.FilePath.Replace("|", "\\|");
|
||||||
|
var writeBehavior = EffectiveConfigWriteSemantics.GetBehavior(optionName);
|
||||||
|
if (writeBehavior == EffectiveConfigWriteBehavior.InversePair)
|
||||||
|
{
|
||||||
|
var pair = EffectiveConfigWriteSemantics.GetInversePairKeys(optionName);
|
||||||
|
if (pair is null) return "";
|
||||||
|
var escapedA = EscapeForSedPattern(pair.Value.KeyA);
|
||||||
|
var escapedB = EscapeForSedPattern(pair.Value.KeyB);
|
||||||
|
return $"sed -i -e '|^{escapedA}$|d' -e '|^{escapedB}$|d' {pathEscaped}";
|
||||||
|
}
|
||||||
var escaped = EscapeForSedPattern(optionName);
|
var escaped = EscapeForSedPattern(optionName);
|
||||||
var behavior = EffectiveConfigParserBehaviorMap.GetBehavior(optionName);
|
var pattern = writeBehavior == EffectiveConfigWriteBehavior.Flag
|
||||||
var pattern = behavior == EffectiveConfigParserBehavior.Flag
|
|
||||||
? $"^{escaped}$"
|
? $"^{escaped}$"
|
||||||
: $"^{escaped}=.*$";
|
: $"^{escaped}=.*$";
|
||||||
var pathEscaped = source.FilePath.Replace("|", "\\|");
|
|
||||||
return $"sed -i '|{pattern}|d' {pathEscaped}";
|
return $"sed -i '|{pattern}|d' {pathEscaped}";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,12 +43,31 @@ public static class ReadonlyConfigCommandGenerator
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The line to add to managed config to override (e.g. "port=53" or "expand-hosts"). Empty if managed path not set.
|
/// The line to add to managed config to override (e.g. "port=53" or "expand-hosts"). Empty if managed path not set.
|
||||||
|
/// Consults EffectiveConfigWriteSemantics: InversePair uses ExplicitToggleState (Enabled→KeyA, Disabled→KeyB, Default→remove);
|
||||||
|
/// KeyOnlyOrValue uses key-only or key=value; Flag uses key-only when true; other options use key=value fallback.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string GetOverrideLine(string optionName, object? value, string? managedFilePath)
|
public static string GetOverrideLine(string optionName, object? value, string? managedFilePath)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(managedFilePath)) return "";
|
if (string.IsNullOrEmpty(managedFilePath)) return "";
|
||||||
var behavior = EffectiveConfigParserBehaviorMap.GetBehavior(optionName);
|
var writeBehavior = EffectiveConfigWriteSemantics.GetBehavior(optionName);
|
||||||
if (behavior == EffectiveConfigParserBehavior.Flag)
|
if (writeBehavior == EffectiveConfigWriteBehavior.InversePair)
|
||||||
|
{
|
||||||
|
var pair = EffectiveConfigWriteSemantics.GetInversePairKeys(optionName);
|
||||||
|
if (pair is null || value is not ExplicitToggleState s) return "";
|
||||||
|
return s switch
|
||||||
|
{
|
||||||
|
ExplicitToggleState.Enabled => pair.Value.KeyA,
|
||||||
|
ExplicitToggleState.Disabled => pair.Value.KeyB,
|
||||||
|
_ => "" // Default means remove/unset
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (writeBehavior == EffectiveConfigWriteBehavior.KeyOnlyOrValue)
|
||||||
|
{
|
||||||
|
if (value is null) return "";
|
||||||
|
var str = value.ToString() ?? "";
|
||||||
|
return str.Length == 0 ? optionName : $"{optionName}={str}";
|
||||||
|
}
|
||||||
|
if (writeBehavior == EffectiveConfigWriteBehavior.Flag)
|
||||||
return value is bool b && b ? optionName : "";
|
return value is bool b && b ? optionName : "";
|
||||||
var v = ValueToConfString(value);
|
var v = ValueToConfString(value);
|
||||||
return string.IsNullOrEmpty(v) ? optionName : $"{optionName}={v}";
|
return string.IsNullOrEmpty(v) ? optionName : $"{optionName}={v}";
|
||||||
|
|||||||
@@ -452,13 +452,16 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
|
|||||||
var useStaleCache = useStaleCacheVal == null ? null : (string.IsNullOrWhiteSpace(useStaleCacheVal) ? "" : useStaleCacheVal.Trim());
|
var useStaleCache = useStaleCacheVal == null ? null : (string.IsNullOrWhiteSpace(useStaleCacheVal) ? "" : useStaleCacheVal.Trim());
|
||||||
var (addMacVal, _) = ((string?, string?))ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.AddMac);
|
var (addMacVal, _) = ((string?, string?))ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.AddMac);
|
||||||
var addMac = addMacVal == null ? null : (string.IsNullOrWhiteSpace(addMacVal) ? "" : addMacVal.Trim());
|
var addMac = addMacVal == null ? null : (string.IsNullOrWhiteSpace(addMacVal) ? "" : addMacVal.Trim());
|
||||||
|
var stripMac = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.StripMac);
|
||||||
var (addSubnetVal, _) = ((string?, string?))ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.AddSubnet);
|
var (addSubnetVal, _) = ((string?, string?))ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.AddSubnet);
|
||||||
var addSubnet = addSubnetVal == null ? null : (string.IsNullOrWhiteSpace(addSubnetVal) ? "" : addSubnetVal.Trim());
|
var addSubnet = addSubnetVal == null ? null : (string.IsNullOrWhiteSpace(addSubnetVal) ? "" : addSubnetVal.Trim());
|
||||||
|
var stripSubnet = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.StripSubnet);
|
||||||
var (umbrellaVal, _) = ((string?, string?))ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.Umbrella);
|
var (umbrellaVal, _) = ((string?, string?))ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.Umbrella);
|
||||||
var umbrella = umbrellaVal == null ? null : (string.IsNullOrWhiteSpace(umbrellaVal) ? "" : umbrellaVal.Trim());
|
var umbrella = umbrellaVal == null ? null : (string.IsNullOrWhiteSpace(umbrellaVal) ? "" : umbrellaVal.Trim());
|
||||||
var do0x20 = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.Do0x20Encode);
|
var do0x20 = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.Do0x20Encode);
|
||||||
var no0x20 = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.No0x20Encode);
|
var no0x20 = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.No0x20Encode);
|
||||||
var do0x20EncodeState = do0x20 ? ExplicitToggleState.Enabled : (no0x20 ? ExplicitToggleState.Disabled : ExplicitToggleState.Default);
|
// dnsmasq docs: no-0x20-encode overrides do-0x20-encode
|
||||||
|
var do0x20EncodeState = no0x20 ? ExplicitToggleState.Disabled : (do0x20 ? ExplicitToggleState.Enabled : ExplicitToggleState.Default);
|
||||||
var conntrack = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.Conntrack);
|
var conntrack = (bool)ParseOptionValue(paths, pathToLines, DnsmasqConfKeys.Conntrack);
|
||||||
|
|
||||||
return new EffectiveDnsmasqConfig(
|
return new EffectiveDnsmasqConfig(
|
||||||
@@ -480,7 +483,7 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
|
|||||||
authTtl, ednsPacketMax, queryPort, portLimit, minPort, maxPort, logAsync, localService, dhcpLeaseMax,
|
authTtl, ednsPacketMax, queryPort, portLimit, minPort, maxPort, logAsync, localService, dhcpLeaseMax,
|
||||||
negTtl, maxTtl, maxCacheTtl, minCacheTtl, dhcpTtl, tftpRootPath, pxePrompt, enableDbus, enableUbus, fastDnsRetry,
|
negTtl, maxTtl, maxCacheTtl, minCacheTtl, dhcpTtl, tftpRootPath, pxePrompt, enableDbus, enableUbus, fastDnsRetry,
|
||||||
dhcpScriptPath, mxTarget, dnsForwardMax, dumpfilePath, dumpmask, addCpeId, dnssecTimestamp, dnssecLimits, dhcpAlternatePort, dhcpDuid, dhcpLuascriptPath, dhcpScriptuser, dhcpPxeVendor,
|
dhcpScriptPath, mxTarget, dnsForwardMax, dumpfilePath, dumpmask, addCpeId, dnssecTimestamp, dnssecLimits, dhcpAlternatePort, dhcpDuid, dhcpLuascriptPath, dhcpScriptuser, dhcpPxeVendor,
|
||||||
useStaleCache, addMac, addSubnet, umbrella, do0x20EncodeState, conntrack
|
useStaleCache, addMac, stripMac, addSubnet, stripSubnet, umbrella, do0x20EncodeState, conntrack
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -654,11 +657,14 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
|
|||||||
var (_, dhcpPxeVendorSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpPxeVendor, managedFilePath);
|
var (_, dhcpPxeVendorSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.DhcpPxeVendor, managedFilePath);
|
||||||
var (_, useStaleCacheSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.UseStaleCache, managedFilePath);
|
var (_, useStaleCacheSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.UseStaleCache, managedFilePath);
|
||||||
var (_, addMacSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.AddMac, managedFilePath);
|
var (_, addMacSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.AddMac, managedFilePath);
|
||||||
|
var (_, stripMacSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.StripMac, managedFilePath);
|
||||||
var (_, addSubnetSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.AddSubnet, managedFilePath);
|
var (_, addSubnetSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.AddSubnet, managedFilePath);
|
||||||
|
var (_, stripSubnetSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.StripSubnet, managedFilePath);
|
||||||
var (_, umbrellaSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.Umbrella, managedFilePath);
|
var (_, umbrellaSource) = ((string?, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.Umbrella, managedFilePath);
|
||||||
var (do0x20Set, do0x20EncodeSourceA) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.Do0x20Encode, managedFilePath);
|
var (do0x20Set, do0x20EncodeSourceA) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.Do0x20Encode, managedFilePath);
|
||||||
var (no0x20Set, no0x20EncodeSourceB) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.No0x20Encode, managedFilePath);
|
var (no0x20Set, no0x20EncodeSourceB) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.No0x20Encode, managedFilePath);
|
||||||
var do0x20EncodeSource = do0x20Set ? do0x20EncodeSourceA : (no0x20Set ? no0x20EncodeSourceB : null);
|
// no-0x20-encode overrides do-0x20-encode; source reflects the effective directive
|
||||||
|
var do0x20EncodeSource = no0x20Set ? no0x20EncodeSourceB : (do0x20Set ? do0x20EncodeSourceA : null);
|
||||||
var (_, conntrackSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.Conntrack, managedFilePath);
|
var (_, conntrackSource) = ((bool, ConfigValueSource?))ParseOptionWithSource(paths, pathToLines, DnsmasqConfKeys.Conntrack, managedFilePath);
|
||||||
|
|
||||||
return new EffectiveConfigSources(
|
return new EffectiveConfigSources(
|
||||||
@@ -826,7 +832,9 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
|
|||||||
dhcpPxeVendorSource,
|
dhcpPxeVendorSource,
|
||||||
useStaleCacheSource,
|
useStaleCacheSource,
|
||||||
addMacSource,
|
addMacSource,
|
||||||
|
stripMacSource,
|
||||||
addSubnetSource,
|
addSubnetSource,
|
||||||
|
stripSubnetSource,
|
||||||
umbrellaSource,
|
umbrellaSource,
|
||||||
do0x20EncodeSource,
|
do0x20EncodeSource,
|
||||||
conntrackSource
|
conntrackSource
|
||||||
@@ -899,7 +907,7 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
|
|||||||
DhcpLeaseMax: null, NegTtl: null, MaxTtl: null, MaxCacheTtl: null, MinCacheTtl: null, DhcpTtl: null,
|
DhcpLeaseMax: null, NegTtl: null, MaxTtl: null, MaxCacheTtl: null, MinCacheTtl: null, DhcpTtl: null,
|
||||||
TftpRootPath: null, PxePrompt: null, EnableDbus: null, EnableUbus: null, FastDnsRetry: null,
|
TftpRootPath: null, PxePrompt: null, EnableDbus: null, EnableUbus: null, FastDnsRetry: null,
|
||||||
DhcpScriptPath: null, MxTarget: null, DnsForwardMax: null, DumpfilePath: null, Dumpmask: null, AddCpeId: null, DnssecTimestamp: null, DnssecLimits: null, DhcpAlternatePort: null, DhcpDuid: null, DhcpLuascriptPath: null, DhcpScriptuser: null, DhcpPxeVendor: null,
|
DhcpScriptPath: null, MxTarget: null, DnsForwardMax: null, DumpfilePath: null, Dumpmask: null, AddCpeId: null, DnssecTimestamp: null, DnssecLimits: null, DhcpAlternatePort: null, DhcpDuid: null, DhcpLuascriptPath: null, DhcpScriptuser: null, DhcpPxeVendor: null,
|
||||||
UseStaleCache: null, AddMac: null, AddSubnet: null, Umbrella: null, Do0x20EncodeState: ExplicitToggleState.Default, Conntrack: false
|
UseStaleCache: null, AddMac: null, StripMac: false, AddSubnet: null, StripSubnet: false, Umbrella: null, Do0x20EncodeState: ExplicitToggleState.Default, Conntrack: false
|
||||||
);
|
);
|
||||||
|
|
||||||
private static EffectiveConfigSources CreateDefaultEffectiveConfigSources() =>
|
private static EffectiveConfigSources CreateDefaultEffectiveConfigSources() =>
|
||||||
@@ -940,7 +948,7 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
|
|||||||
DhcpLeaseMax: null, NegTtl: null, MaxTtl: null, MaxCacheTtl: null, MinCacheTtl: null, DhcpTtl: null,
|
DhcpLeaseMax: null, NegTtl: null, MaxTtl: null, MaxCacheTtl: null, MinCacheTtl: null, DhcpTtl: null,
|
||||||
TftpRootPath: null, PxePrompt: null, EnableDbus: null, EnableUbus: null, FastDnsRetry: null,
|
TftpRootPath: null, PxePrompt: null, EnableDbus: null, EnableUbus: null, FastDnsRetry: null,
|
||||||
DhcpScriptPath: null, MxTarget: null, DnsForwardMax: null, DumpfilePath: null, Dumpmask: null, AddCpeId: null, DnssecTimestamp: null, DnssecLimits: null, DhcpAlternatePort: null, DhcpDuid: null, DhcpLuascriptPath: null, DhcpScriptuser: null, DhcpPxeVendor: null,
|
DhcpScriptPath: null, MxTarget: null, DnsForwardMax: null, DumpfilePath: null, Dumpmask: null, AddCpeId: null, DnssecTimestamp: null, DnssecLimits: null, DhcpAlternatePort: null, DhcpDuid: null, DhcpLuascriptPath: null, DhcpScriptuser: null, DhcpPxeVendor: null,
|
||||||
UseStaleCache: null, AddMac: null, AddSubnet: null, Umbrella: null, Do0x20Encode: null, Conntrack: null
|
UseStaleCache: null, AddMac: null, StripMac: null, AddSubnet: null, StripSubnet: null, Umbrella: null, Do0x20Encode: null, Conntrack: null
|
||||||
);
|
);
|
||||||
|
|
||||||
private static int? TryParseInt(string? value)
|
private static int? TryParseInt(string? value)
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ public class DnsmasqConfigService : IDnsmasqConfigService
|
|||||||
var maxLineNumber = list.Count > 0 ? list.Max(l => l.LineNumber) : 0;
|
var maxLineNumber = list.Count > 0 ? list.Max(l => l.LineNumber) : 0;
|
||||||
foreach (var c in changes)
|
foreach (var c in changes)
|
||||||
{
|
{
|
||||||
var writeBehavior = EffectiveConfigWriteBehaviorMap.GetBehavior(c.OptionName);
|
var writeBehavior = EffectiveConfigWriteSemantics.GetBehavior(c.OptionName);
|
||||||
var confKey = c.OptionName;
|
var confKey = c.OptionName;
|
||||||
|
|
||||||
bool MatchesOption(DnsmasqConfLine line)
|
bool MatchesOption(DnsmasqConfLine line)
|
||||||
@@ -255,7 +255,7 @@ public class DnsmasqConfigService : IDnsmasqConfigService
|
|||||||
|
|
||||||
if (writeBehavior == EffectiveConfigWriteBehavior.InversePair)
|
if (writeBehavior == EffectiveConfigWriteBehavior.InversePair)
|
||||||
{
|
{
|
||||||
var pair = EffectiveConfigWriteBehaviorMap.GetInversePairKeys(confKey);
|
var pair = EffectiveConfigWriteSemantics.GetInversePairKeys(confKey);
|
||||||
if (pair is null)
|
if (pair is null)
|
||||||
continue;
|
continue;
|
||||||
var (pairKeyA, pairKeyB) = pair.Value;
|
var (pairKeyA, pairKeyB) = pair.Value;
|
||||||
@@ -275,10 +275,9 @@ public class DnsmasqConfigService : IDnsmasqConfigService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var behavior = EffectiveConfigParserBehaviorMap.GetBehavior(c.OptionName);
|
var isFlag = writeBehavior == EffectiveConfigWriteBehavior.Flag;
|
||||||
var isFlag = behavior == EffectiveConfigParserBehavior.Flag;
|
|
||||||
|
|
||||||
if (behavior == EffectiveConfigParserBehavior.Multi && TryGetMultiValues(c.NewValue, out var values))
|
if (writeBehavior == EffectiveConfigWriteBehavior.MultiValue && TryGetMultiValues(c.NewValue, out var values))
|
||||||
{
|
{
|
||||||
IReadOnlyList<string> readonlyValues = readonlyByOption.TryGetValue(confKey, out var listValues) ? listValues : Array.Empty<string>();
|
IReadOnlyList<string> readonlyValues = readonlyByOption.TryGetValue(confKey, out var listValues) ? listValues : Array.Empty<string>();
|
||||||
var valuesToWrite = FilterManagedOnly(values, readonlyValues);
|
var valuesToWrite = FilterManagedOnly(values, readonlyValues);
|
||||||
|
|||||||
+7
-1
@@ -101,15 +101,21 @@ public class EffectiveConfigRenderFragmentRegistry : IEffectiveConfigRenderFragm
|
|||||||
RegisterFlag(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.QuietDhcp6);
|
RegisterFlag(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.QuietDhcp6);
|
||||||
RegisterFlag(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.QuietRa);
|
RegisterFlag(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.QuietRa);
|
||||||
RegisterFlag(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.QuietTftp);
|
RegisterFlag(EffectiveConfigFieldBuilder.SectionProcess, DnsmasqConfKeys.QuietTftp);
|
||||||
|
RegisterFlag(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.StripMac);
|
||||||
|
RegisterFlag(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.StripSubnet);
|
||||||
|
|
||||||
// do-0x20-encode / no-0x20-encode: tri-state dropdown (Default / Enabled / Disabled).
|
// do-0x20-encode / no-0x20-encode: tri-state dropdown (Default / Enabled / Disabled).
|
||||||
RegisterComponent(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.Do0x20Encode, typeof(Do0x20EncodeDisplay));
|
RegisterComponent(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.Do0x20Encode, typeof(Do0x20EncodeDisplay));
|
||||||
|
|
||||||
// Key-only or key=value options: checkbox (On) + optional value input.
|
// Key-only or key=value options: checkbox (On) + optional value input; with semantic validators.
|
||||||
RegisterComponent(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.UseStaleCache, typeof(KeyOnlyOrValueDisplay));
|
RegisterComponent(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.UseStaleCache, typeof(KeyOnlyOrValueDisplay));
|
||||||
|
RegisterValidatedSingle(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.UseStaleCache, SpecialOptionValidators.ValidateUseStaleCache);
|
||||||
RegisterComponent(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.AddMac, typeof(KeyOnlyOrValueDisplay));
|
RegisterComponent(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.AddMac, typeof(KeyOnlyOrValueDisplay));
|
||||||
|
RegisterValidatedSingle(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.AddMac, SpecialOptionValidators.ValidateAddMac);
|
||||||
RegisterComponent(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.AddSubnet, typeof(KeyOnlyOrValueDisplay));
|
RegisterComponent(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.AddSubnet, typeof(KeyOnlyOrValueDisplay));
|
||||||
|
RegisterValidatedSingle(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.AddSubnet, SpecialOptionValidators.ValidateAddSubnet);
|
||||||
RegisterComponent(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.Umbrella, typeof(KeyOnlyOrValueDisplay));
|
RegisterComponent(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.Umbrella, typeof(KeyOnlyOrValueDisplay));
|
||||||
|
RegisterValidatedSingle(EffectiveConfigFieldBuilder.SectionCache, DnsmasqConfKeys.Umbrella, SpecialOptionValidators.ValidateUmbrella);
|
||||||
|
|
||||||
// log-queries: dropdown (Off / On / extra / proto / auth).
|
// log-queries: dropdown (Off / On / extra / proto / auth).
|
||||||
RegisterComponent(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.LogQueries, typeof(LogQueriesDisplay));
|
RegisterComponent(EffectiveConfigFieldBuilder.SectionResolver, DnsmasqConfKeys.LogQueries, typeof(LogQueriesDisplay));
|
||||||
|
|||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
|
||||||
|
|
||||||
|
namespace DnsmasqWebUI.Infrastructure.Services.EffectiveConfig.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Single-value validators for effective-config options with special semantics (key-only or key=value, allowed values).
|
||||||
|
/// Used by the descriptor factory so field-level validation shows friendly errors in the edit flow.
|
||||||
|
/// </summary>
|
||||||
|
public static class SpecialOptionValidators
|
||||||
|
{
|
||||||
|
/// <summary>use-stale-cache: unset, key-only, or non-negative integer.</summary>
|
||||||
|
public static string? ValidateUseStaleCache(object? value)
|
||||||
|
{
|
||||||
|
if (value is null) return null;
|
||||||
|
var s = value.ToString()?.Trim() ?? "";
|
||||||
|
if (s.Length == 0) return null; // key-only
|
||||||
|
return int.TryParse(s, out var n) && n >= 0
|
||||||
|
? null
|
||||||
|
: "use-stale-cache must be empty or a non-negative integer.";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>add-mac: unset, key-only, or 'base64' or 'text'.</summary>
|
||||||
|
public static string? ValidateAddMac(object? value)
|
||||||
|
{
|
||||||
|
if (value is null) return null;
|
||||||
|
var s = value.ToString()?.Trim() ?? "";
|
||||||
|
if (s.Length == 0) return null; // key-only
|
||||||
|
return s is "base64" or "text"
|
||||||
|
? null
|
||||||
|
: "add-mac must be empty, 'base64', or 'text'.";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>add-subnet: unset, key-only, or value (permissive; format not strictly validated).</summary>
|
||||||
|
public static string? ValidateAddSubnet(object? value)
|
||||||
|
{
|
||||||
|
if (value is null) return null;
|
||||||
|
return null; // accept any string for now
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>umbrella: unset, key-only, or token list (permissive).</summary>
|
||||||
|
public static string? ValidateUmbrella(object? value)
|
||||||
|
{
|
||||||
|
if (value is null) return null;
|
||||||
|
var s = value.ToString()?.Trim() ?? "";
|
||||||
|
if (s.Length == 0) return null; // key-only
|
||||||
|
return null; // token parsing can be tightened later
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -183,7 +183,9 @@ public record EffectiveConfigSources(
|
|||||||
ConfigValueSource? DhcpPxeVendor,
|
ConfigValueSource? DhcpPxeVendor,
|
||||||
ConfigValueSource? UseStaleCache,
|
ConfigValueSource? UseStaleCache,
|
||||||
ConfigValueSource? AddMac,
|
ConfigValueSource? AddMac,
|
||||||
|
ConfigValueSource? StripMac,
|
||||||
ConfigValueSource? AddSubnet,
|
ConfigValueSource? AddSubnet,
|
||||||
|
ConfigValueSource? StripSubnet,
|
||||||
ConfigValueSource? Umbrella,
|
ConfigValueSource? Umbrella,
|
||||||
ConfigValueSource? Do0x20Encode,
|
ConfigValueSource? Do0x20Encode,
|
||||||
ConfigValueSource? Conntrack
|
ConfigValueSource? Conntrack
|
||||||
|
|||||||
@@ -187,7 +187,9 @@ public record EffectiveDnsmasqConfig(
|
|||||||
string? DhcpPxeVendor,
|
string? DhcpPxeVendor,
|
||||||
string? UseStaleCache,
|
string? UseStaleCache,
|
||||||
string? AddMac,
|
string? AddMac,
|
||||||
|
bool StripMac,
|
||||||
string? AddSubnet,
|
string? AddSubnet,
|
||||||
|
bool StripSubnet,
|
||||||
string? Umbrella,
|
string? Umbrella,
|
||||||
ExplicitToggleState Do0x20EncodeState,
|
ExplicitToggleState Do0x20EncodeState,
|
||||||
bool Conntrack
|
bool Conntrack
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<dt><b>--strip-mac</b></dt>
|
||||||
|
<dd>Remove any MAC address information already in downstream queries before forwarding upstream.</dd>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<dt><b>--strip-subnet</b></dt>
|
||||||
|
<dd>Remove any subnet address already present in a downstream query before forwarding it upstream. If --add-subnet is set this also
|
||||||
|
ensures that any downstream-provided subnet is replaced by the one added by dnsmasq. Otherwise, dnsmasq will NOT replace an
|
||||||
|
existing subnet in the query.</dd>
|
||||||
Reference in New Issue
Block a user