}
-else if (_status.NoHosts && !_pathInAddnHosts)
+else if (string.IsNullOrEmpty(_status.ManagedHostsFilePath))
{
-
Hosts editing is disabled: no-hosts is set in dnsmasq config (dnsmasq only uses addn-hosts files) and the configured path is not in the effective addn-hosts list, so dnsmasq would not read the file.
- }
-}
-else if (string.IsNullOrEmpty(_status.SystemHostsPath))
-{
-
No system hosts file configured. Set Dnsmasq:SystemHostsPath (e.g. /etc/hosts) to edit a hosts file from this app.
+
Hosts editing is unavailable: managed hosts path is not configured. Set Dnsmasq:MainConfigPath (and optionally Dnsmasq:ManagedHostsFileName) so the app can create and edit the managed hosts file.
@if (_status.AddnHostsPaths?.Count > 0)
{
Dnsmasq loads these addn-hosts files: @RenderAddnHostsList()
}
}
-else if (!_status.SystemHostsPathExists)
-{
-
System hosts file not found: @_status.SystemHostsPath
-
-
- @foreach (var e in _entries.Where(x => !x.IsPassthrough))
- {
-
-
@e.LineNumber
-
@e.Address
-
@string.Join(", ", e.Names)
-
- }
-
-
-
-
- @foreach (var e in _entries.Where(x => !x.IsPassthrough))
- {
-
-
-
# @e.LineNumber
-
Address @e.Address
-
Names @string.Join(", ", e.Names)
-
-
- }
-
-
-
Path: @_status.SystemHostsPath
+
+
Each hosts file below is rendered with the same component; only the managed file is editable. System hosts is excluded when no-hosts is set (dnsmasq does not read it then).
+
+
+
@_status.ManagedHostsFilePath @(!_status.ManagedHostsPathExists ? "(not created yet; will be created on first save)" : "")
+
+ @if (_readOnlyHosts?.Count > 0)
+ {
+ @foreach (var ro in _readOnlyHosts)
+ {
+
+ }
+ }
}
@code {
private string AppTitle => AppOptions!.Value!.EffectiveTitle;
private DnsmasqServiceStatus? _status;
private List _entries = new();
+ private IReadOnlyList? _readOnlyHosts;
private string? _error;
private string? _message;
private bool _saving;
- /// True when SystemHostsPath is in the effective addn-hosts list (editing allowed when no-hosts is set).
- private bool _pathInAddnHosts => _status != null
- && !string.IsNullOrEmpty(_status.SystemHostsPath)
- && _status.AddnHostsPaths?.Any(p => string.Equals(p, Path.GetFullPath(_status.SystemHostsPath!.Trim()), StringComparison.Ordinal)) == true;
-
/// Returns MarkupString so Blazor renders raw HTML (path + source label).
private MarkupString RenderAddnHostsList()
{
@@ -130,6 +95,7 @@ else
_status = await StatusClient.GetStatusAsync();
var entries = await HostsClient.GetHostsAsync();
_entries = entries?.ToList() ?? new List();
+ _readOnlyHosts = await HostsClient.GetReadOnlyHostsAsync();
}
catch (Exception ex)
{
@@ -151,6 +117,7 @@ else
_error = "Saved but dnsmasq reload failed: " + result.Reload.StdErr;
else
_message = "Saved.";
+ _status = await StatusClient.GetStatusAsync();
}
catch (Exception ex)
{
diff --git a/src/DnsmasqWebUI/Components/_Imports.razor b/src/DnsmasqWebUI/Components/_Imports.razor
index c823be8..5bdbf7a 100644
--- a/src/DnsmasqWebUI/Components/_Imports.razor
+++ b/src/DnsmasqWebUI/Components/_Imports.razor
@@ -12,6 +12,7 @@
@using DnsmasqWebUI.Components
@using DnsmasqWebUI.Components.Modals
@using DnsmasqWebUI.Components.EffectiveConfig
+@using DnsmasqWebUI.Components.Hosts
@using DnsmasqWebUI.Components.Dnsmasq
@using DnsmasqWebUI.Models
@using DnsmasqWebUI.Configuration
diff --git a/src/DnsmasqWebUI/Configuration/DnsmasqOptions.cs b/src/DnsmasqWebUI/Configuration/DnsmasqOptions.cs
index da97008..05d5c58 100644
--- a/src/DnsmasqWebUI/Configuration/DnsmasqOptions.cs
+++ b/src/DnsmasqWebUI/Configuration/DnsmasqOptions.cs
@@ -23,7 +23,10 @@ public class DnsmasqOptions
/// Filename of the managed config (e.g. zz-dnsmasq-webui.conf), created in the same directory as the main config and included only via a conf-file= directive as the last line of the main config. Managed file content parsed with DnsmasqConfFileLineParser.
public string ManagedFileName { get; set; } = "zz-dnsmasq-webui.conf";
- /// Optional path to the hosts file the app can edit (e.g. /etc/hosts). When set, the app can read/write it. When no-hosts is not set, the app adds addn-hosts= in the managed file so dnsmasq loads it; when no-hosts is set, dnsmasq only uses addn-hosts files, so this path must be one of the effective addn-hosts paths for editing to take effect. When unset, hosts UI is disabled.
+ /// Filename of the managed hosts file (e.g. zz-dnsmasq-webui.hosts), created in the same directory as the main config. The app adds addn-hosts=<this path> in the managed config so dnsmasq loads it last. This is the only hosts file the app writes to.
+ public string ManagedHostsFileName { get; set; } = "zz-dnsmasq-webui.hosts";
+
+ /// Optional path to the system hosts file (e.g. /etc/hosts). When set, shown in the UI as read-only so users can see those entries. The app never writes to it; editing is via the managed hosts file only. When unset, the system hosts row is not shown.
public string? SystemHostsPath { get; set; }
/// Command to run after config changes (e.g. "systemctl reload dnsmasq" or "pkill -HUP -x dnsmasq"). Runs in the same environment as the app; if app is in a container and dnsmasq is on the host, this runs in the container and will not reload host dnsmasq unless you use a host-side relay.
diff --git a/src/DnsmasqWebUI/Configuration/DnsmasqOptionsValidator.cs b/src/DnsmasqWebUI/Configuration/DnsmasqOptionsValidator.cs
index 3027af3..3c2a31e 100644
--- a/src/DnsmasqWebUI/Configuration/DnsmasqOptionsValidator.cs
+++ b/src/DnsmasqWebUI/Configuration/DnsmasqOptionsValidator.cs
@@ -28,9 +28,9 @@ public sealed class DnsmasqOptionsValidator : IValidateOptions
}
}
- // SystemHostsPath is optional. When set, the app can edit that hosts file. Hosts UI is disabled when
- // SystemHostsPath is unset, or when no-hosts is set and SystemHostsPath is not in the effective addn-hosts
- // list (dnsmasq only uses addn-hosts when no-hosts is set, so the path must be in addn-hosts for editing to take effect).
+ // SystemHostsPath is optional. When set, the UI shows system hosts as read-only. The app only writes to the
+ // managed hosts file (ManagedHostsFileName in the same directory as main config). Hosts UI is available when
+ // MainConfigPath is set (managed hosts path is then derived from it and ManagedHostsFileName).
if (failures.Count == 0)
return ValidateOptionsResult.Success;
diff --git a/src/DnsmasqWebUI/Controllers/HostsController.cs b/src/DnsmasqWebUI/Controllers/HostsController.cs
index 3c50322..a2d6232 100644
--- a/src/DnsmasqWebUI/Controllers/HostsController.cs
+++ b/src/DnsmasqWebUI/Controllers/HostsController.cs
@@ -1,7 +1,10 @@
-using DnsmasqWebUI.Models;
+using System.Text;
using DnsmasqWebUI.Configuration;
+using DnsmasqWebUI.Models;
+using DnsmasqWebUI.Parsers;
using DnsmasqWebUI.Services.Abstractions;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
namespace DnsmasqWebUI.Controllers;
@@ -14,11 +17,7 @@ public class HostsController : ControllerBase
private readonly IDnsmasqConfigSetService _configSetService;
private readonly DnsmasqOptions _options;
- public HostsController(
- IHostsFileService hostsService,
- IReloadService reloadService,
- IDnsmasqConfigSetService configSetService,
- Microsoft.Extensions.Options.IOptions options)
+ public HostsController(IHostsFileService hostsService, IReloadService reloadService, IDnsmasqConfigSetService configSetService, IOptions options)
{
_hostsService = hostsService;
_reloadService = reloadService;
@@ -40,25 +39,96 @@ public class HostsController : ControllerBase
}
}
+ /// Returns entries from each read-only hosts file: system hosts (when SystemHostsPath set and no-hosts is false), then addn-hosts that are not the managed file. System hosts is excluded when no-hosts is set because dnsmasq does not read it then.
+ [HttpGet("readonly")]
+ public async Task>> GetReadOnly(CancellationToken ct)
+ {
+ try
+ {
+ var set = await _configSetService.GetConfigSetAsync(ct);
+ var effectiveConfig = _configSetService.GetEffectiveConfig();
+ var addnPaths = effectiveConfig.AddnHostsPaths ?? Array.Empty();
+ var managedPath = set.ManagedHostsFilePath != null ? Path.GetFullPath(set.ManagedHostsFilePath) : null;
+
+ var result = new List();
+
+ // System hosts: only when configured and no-hosts is false (dnsmasq reads it then).
+ var systemPath = _options.SystemHostsPath?.Trim();
+ if (!string.IsNullOrEmpty(systemPath) && !effectiveConfig.NoHosts)
+ {
+ var fullPath = Path.GetFullPath(systemPath);
+ if (System.IO.File.Exists(fullPath))
+ {
+ try
+ {
+ var lines = await System.IO.File.ReadAllLinesAsync(fullPath, Encoding.UTF8, ct);
+ var entries = new List();
+ for (var i = 0; i < lines.Length; i++)
+ {
+ var entry = HostsFileLineParser.ParseLine(lines[i], i + 1);
+ if (entry != null)
+ entries.Add(entry);
+ }
+ result.Add(new ReadOnlyHostsFile(fullPath, entries));
+ }
+ catch
+ {
+ // Skip unreadable
+ }
+ }
+ }
+
+ var systemPathFull = !string.IsNullOrEmpty(systemPath) ? Path.GetFullPath(systemPath) : null;
+
+ foreach (var p in addnPaths)
+ {
+ var fullPath = Path.GetFullPath(p);
+ if (managedPath != null && string.Equals(fullPath, managedPath, StringComparison.Ordinal))
+ continue;
+ if (systemPathFull != null && string.Equals(fullPath, systemPathFull, StringComparison.Ordinal))
+ continue;
+ if (!System.IO.File.Exists(fullPath))
+ continue;
+ try
+ {
+ var lines = await System.IO.File.ReadAllLinesAsync(fullPath, Encoding.UTF8, ct);
+ var entries = new List();
+ for (var i = 0; i < lines.Length; i++)
+ {
+ var entry = HostsFileLineParser.ParseLine(lines[i], i + 1);
+ if (entry != null)
+ entries.Add(entry);
+ }
+ result.Add(new ReadOnlyHostsFile(fullPath, entries));
+ }
+ catch
+ {
+ // Skip unreadable files
+ }
+ }
+ return Ok(result);
+ }
+ catch (Exception ex)
+ {
+ return StatusCode(500, new { error = ex.Message });
+ }
+ }
+
[HttpPut]
public async Task> Put([FromBody] List? entries, CancellationToken ct)
{
if (entries == null)
return BadRequest(new { error = "Body required" });
- var effectiveConfig = _configSetService.GetEffectiveConfig();
- if (string.IsNullOrWhiteSpace(_options.SystemHostsPath))
- return BadRequest(new { error = "No system hosts file configured. Set Dnsmasq:SystemHostsPath to enable hosts editing." });
- // When no-hosts is set, dnsmasq only uses addn-hosts files; allow editing only if our path is in that list.
- var systemPath = Path.GetFullPath(_options.SystemHostsPath.Trim());
- var pathInAddnHosts = effectiveConfig.AddnHostsPaths?.Any(p => string.Equals(p, systemPath, StringComparison.Ordinal)) == true;
- if (effectiveConfig.NoHosts && !pathInAddnHosts)
- return BadRequest(new { error = "Hosts are disabled by no-hosts in dnsmasq config, and the configured path is not in addn-hosts, so dnsmasq would not read it." });
try
{
await _hostsService.WriteAsync(entries, ct);
var reload = await _reloadService.ReloadAsync(ct);
return Ok(new SaveWithReloadResult(true, reload));
}
+ catch (InvalidOperationException ex)
+ {
+ return BadRequest(new { error = ex.Message });
+ }
catch (Exception ex)
{
return StatusCode(500, new { error = ex.Message });
diff --git a/src/DnsmasqWebUI/Controllers/StatusController.cs b/src/DnsmasqWebUI/Controllers/StatusController.cs
index 94c71ec..9fff292 100644
--- a/src/DnsmasqWebUI/Controllers/StatusController.cs
+++ b/src/DnsmasqWebUI/Controllers/StatusController.cs
@@ -62,6 +62,8 @@ public class StatusController : ControllerBase
var status = new DnsmasqServiceStatus(
SystemHostsPath: string.IsNullOrEmpty(systemHostsPath) ? null : systemHostsPath,
SystemHostsPathExists: !string.IsNullOrEmpty(systemHostsPath) && System.IO.File.Exists(systemHostsPath),
+ ManagedHostsFilePath: set.ManagedHostsFilePath,
+ ManagedHostsPathExists: !string.IsNullOrEmpty(set.ManagedHostsFilePath) && System.IO.File.Exists(set.ManagedHostsFilePath),
NoHosts: effectiveConfig.NoHosts,
AddnHostsPaths: effectiveConfig.AddnHostsPaths,
EffectiveConfig: effectiveConfig,
diff --git a/src/DnsmasqWebUI/Models/DnsmasqConfigSet.cs b/src/DnsmasqWebUI/Models/DnsmasqConfigSet.cs
index 0624fec..97776ff 100644
--- a/src/DnsmasqWebUI/Models/DnsmasqConfigSet.cs
+++ b/src/DnsmasqWebUI/Models/DnsmasqConfigSet.cs
@@ -1,8 +1,9 @@
namespace DnsmasqWebUI.Models;
-/// Ordered set of dnsmasq config files (main + conf-file + conf-dir). ManagedFilePath is the single file we read/write.
+/// Ordered set of dnsmasq config files (main + conf-file + conf-dir). ManagedFilePath is the single config file we read/write; ManagedHostsFilePath is the single hosts file we read/write.
public record DnsmasqConfigSet(
string MainConfigPath,
string ManagedFilePath,
+ string? ManagedHostsFilePath,
IReadOnlyList Files
);
diff --git a/src/DnsmasqWebUI/Models/DnsmasqServiceStatus.cs b/src/DnsmasqWebUI/Models/DnsmasqServiceStatus.cs
index 0da0345..0700129 100644
--- a/src/DnsmasqWebUI/Models/DnsmasqServiceStatus.cs
+++ b/src/DnsmasqWebUI/Models/DnsmasqServiceStatus.cs
@@ -4,8 +4,10 @@ namespace DnsmasqWebUI.Models;
/// Dnsmasq service and config status returned by GET api/status.
/// Includes paths from config, command configuration flags, and live output from status/logs commands.
///
-/// Path to the hosts file the app can edit (e.g. /etc/hosts). Null when hosts UI is disabled.
+/// Optional path to the system hosts file (e.g. /etc/hosts). When set, shown in UI as read-only. Null when not configured.
/// True if is set and the file exists on disk.
+/// Path to the app's managed hosts file (the only hosts file the app writes to). Null when main config path is empty.
+/// True if is set and the file exists on disk.
/// True when dnsmasq has no-hosts set (hosts files disabled).
/// Effective addn-hosts paths dnsmasq loads. Empty when none configured.
/// Effective dnsmasq config (single-value, flags, multi-value) after parsing all config files.
@@ -35,6 +37,8 @@ namespace DnsmasqWebUI.Models;
public record DnsmasqServiceStatus(
string? SystemHostsPath,
bool SystemHostsPathExists,
+ string? ManagedHostsFilePath,
+ bool ManagedHostsPathExists,
bool NoHosts,
IReadOnlyList AddnHostsPaths,
EffectiveDnsmasqConfig? EffectiveConfig,
diff --git a/src/DnsmasqWebUI/Models/ReadOnlyHostsFile.cs b/src/DnsmasqWebUI/Models/ReadOnlyHostsFile.cs
new file mode 100644
index 0000000..3f065d5
--- /dev/null
+++ b/src/DnsmasqWebUI/Models/ReadOnlyHostsFile.cs
@@ -0,0 +1,4 @@
+namespace DnsmasqWebUI.Models;
+
+/// Path and parsed entries for a read-only addn-hosts file (not the managed hosts file).
+public record ReadOnlyHostsFile(string Path, IReadOnlyList Entries);
diff --git a/src/DnsmasqWebUI/Services/DnsmasqConfigService.cs b/src/DnsmasqWebUI/Services/DnsmasqConfigService.cs
index 7828ab3..c710b11 100644
--- a/src/DnsmasqWebUI/Services/DnsmasqConfigService.cs
+++ b/src/DnsmasqWebUI/Services/DnsmasqConfigService.cs
@@ -1,23 +1,19 @@
using System.Text;
using DnsmasqWebUI.Models;
using DnsmasqWebUI.Models.Config;
-using DnsmasqWebUI.Configuration;
using DnsmasqWebUI.Parsers;
using DnsmasqWebUI.Services.Abstractions;
-using Microsoft.Extensions.Options;
namespace DnsmasqWebUI.Services;
public class DnsmasqConfigService : IDnsmasqConfigService
{
private readonly IDnsmasqConfigSetService _configSetService;
- private readonly string _hostsPath;
private readonly ILogger _logger;
- public DnsmasqConfigService(IDnsmasqConfigSetService configSetService, IOptions options, ILogger logger)
+ public DnsmasqConfigService(IDnsmasqConfigSetService configSetService, ILogger logger)
{
_configSetService = configSetService;
- _hostsPath = options.Value.SystemHostsPath?.Trim() ?? "";
_logger = logger;
}
@@ -75,20 +71,31 @@ public class DnsmasqConfigService : IDnsmasqConfigService
return "line:" + e.LineNumber;
}
- /// When hostsPath is set, ensures the managed file has exactly one addn-hosts line pointing to it (replaces the first AddnHosts line in the list or inserts at start). So dnsmasq loads that file; other config files may have other addn-hosts lines.
- private static void EnsureOneAddnHostsLine(List configLines, string hostsPath)
+ /// When managedHostsPath is set, ensures the managed config has exactly one addn-hosts line pointing to it (replaces the first AddnHosts line or inserts at start). So dnsmasq loads our managed hosts file last.
+ private static void EnsureOneAddnHostsLine(List configLines, string? managedHostsPath)
{
- if (string.IsNullOrEmpty(hostsPath))
+ if (string.IsNullOrEmpty(managedHostsPath))
return;
var idx = configLines.FindIndex(c => c.Kind == DnsmasqConfLineKind.AddnHosts);
var lineNumber = idx >= 0 ? configLines[idx].LineNumber : 1;
- var line = new AddnHostsLine { LineNumber = lineNumber, AddnHostsPath = hostsPath };
+ var line = new AddnHostsLine { LineNumber = lineNumber, AddnHostsPath = managedHostsPath };
if (idx >= 0)
configLines[idx] = line;
else
configLines.Insert(0, line);
}
+ /// Creates the managed hosts file empty if it does not exist, so dnsmasq does not error when we add addn-hosts=<path> to the managed config.
+ private static void EnsureManagedHostsFileExists(string? managedHostsPath)
+ {
+ if (string.IsNullOrEmpty(managedHostsPath) || File.Exists(managedHostsPath))
+ return;
+ var dir = Path.GetDirectoryName(managedHostsPath);
+ if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
+ Directory.CreateDirectory(dir);
+ File.WriteAllText(managedHostsPath, "");
+ }
+
public async Task WriteDhcpHostsAsync(IReadOnlyList entries, CancellationToken ct = default)
{
var set = await _configSetService.GetConfigSetAsync(ct);
@@ -119,7 +126,7 @@ public class DnsmasqConfigService : IDnsmasqConfigService
rawLines = Array.Empty();
var configLines = DnsmasqConfFileLineParser.ParseFile(rawLines).ToList();
- EnsureOneAddnHostsLine(configLines, _hostsPath);
+ EnsureOneAddnHostsLine(configLines, set.ManagedHostsFilePath);
var fileEntries = configLines.OfType().Select(c => c.DhcpHost).ToList();
AssignStableIds(fileEntries);
@@ -142,6 +149,7 @@ public class DnsmasqConfigService : IDnsmasqConfigService
var tmpPath = path + ".tmp";
await File.WriteAllLinesAsync(tmpPath, output, Encoding.UTF8, ct);
File.Move(tmpPath, path, overwrite: true);
+ EnsureManagedHostsFileExists(set.ManagedHostsFilePath);
_logger.LogInformation("Wrote managed config file: {Path}", path);
}
@@ -188,7 +196,8 @@ public class DnsmasqConfigService : IDnsmasqConfigService
public async Task WriteManagedConfigAsync(IReadOnlyList lines, CancellationToken ct = default)
{
- var path = await GetManagedFilePathAsync(ct);
+ var set = await _configSetService.GetConfigSetAsync(ct);
+ var path = set.ManagedFilePath;
if (string.IsNullOrEmpty(path))
throw new InvalidOperationException("No managed file path (main config has no conf-dir). Cannot write managed config.");
@@ -197,12 +206,13 @@ public class DnsmasqConfigService : IDnsmasqConfigService
Directory.CreateDirectory(dir);
var list = lines.ToList();
- EnsureOneAddnHostsLine(list, _hostsPath);
+ EnsureOneAddnHostsLine(list, set.ManagedHostsFilePath);
var output = list.Select(DnsmasqConfFileLineParser.ToLine).ToList();
var tmpPath = path + ".tmp";
await File.WriteAllLinesAsync(tmpPath, output, Encoding.UTF8, ct);
File.Move(tmpPath, path, overwrite: true);
+ EnsureManagedHostsFileExists(set.ManagedHostsFilePath);
_logger.LogInformation("Wrote managed config file: {Path}", path);
}
}
diff --git a/src/DnsmasqWebUI/Services/DnsmasqConfigSetService.cs b/src/DnsmasqWebUI/Services/DnsmasqConfigSetService.cs
index 0a47387..3419394 100644
--- a/src/DnsmasqWebUI/Services/DnsmasqConfigSetService.cs
+++ b/src/DnsmasqWebUI/Services/DnsmasqConfigSetService.cs
@@ -251,11 +251,15 @@ public class DnsmasqConfigSetService : IDnsmasqConfigSetService
{
var mainPath = _options.MainConfigPath;
if (string.IsNullOrEmpty(mainPath))
- return new DnsmasqConfigSet("", "", Array.Empty());
+ return new DnsmasqConfigSet("", "", null, Array.Empty());
var mainFull = Path.GetFullPath(mainPath);
var mainDir = Path.GetDirectoryName(mainFull) ?? "";
var managedFilePath = Path.Combine(mainDir, _options.ManagedFileName);
+ var managedHostsFileName = _options.ManagedHostsFileName?.Trim();
+ if (string.IsNullOrEmpty(managedHostsFileName))
+ managedHostsFileName = "zz-dnsmasq-webui.hosts";
+ var managedHostsFilePath = Path.Combine(mainDir, managedHostsFileName);
var withSource = DnsmasqConfIncludeParser.GetIncludedPathsWithSource(mainPath);
var files = withSource.Select(p => new DnsmasqConfigSetEntry(
@@ -268,6 +272,6 @@ public class DnsmasqConfigSetService : IDnsmasqConfigSetService
if (files.All(e => !string.Equals(e.Path, managedFilePath, StringComparison.Ordinal)))
files.Add(new DnsmasqConfigSetEntry(managedFilePath, Path.GetFileName(managedFilePath), DnsmasqConfFileSource.ConfFile, IsManaged: true));
- return new DnsmasqConfigSet(mainFull, managedFilePath, files);
+ return new DnsmasqConfigSet(mainFull, managedFilePath, managedHostsFilePath, files);
}
}
diff --git a/src/DnsmasqWebUI/Services/HostsFileService.cs b/src/DnsmasqWebUI/Services/HostsFileService.cs
index 86737f1..7ad69fe 100644
--- a/src/DnsmasqWebUI/Services/HostsFileService.cs
+++ b/src/DnsmasqWebUI/Services/HostsFileService.cs
@@ -1,33 +1,33 @@
using System.Text;
using DnsmasqWebUI.Models;
-using DnsmasqWebUI.Configuration;
using DnsmasqWebUI.Parsers;
using DnsmasqWebUI.Services.Abstractions;
-using Microsoft.Extensions.Options;
namespace DnsmasqWebUI.Services;
public class HostsFileService : IHostsFileService
{
- private readonly string _path;
+ private readonly IDnsmasqConfigSetService _configSetService;
private readonly ILogger _logger;
- public HostsFileService(IOptions options, ILogger logger)
+ public HostsFileService(IDnsmasqConfigSetService configSetService, ILogger logger)
{
- _path = options.Value.SystemHostsPath?.Trim() ?? "";
+ _configSetService = configSetService;
_logger = logger;
}
public async Task> ReadAsync(CancellationToken ct = default)
{
- if (string.IsNullOrEmpty(_path))
+ var set = await _configSetService.GetConfigSetAsync(ct);
+ var path = set.ManagedHostsFilePath;
+ if (string.IsNullOrEmpty(path))
return Array.Empty();
- if (!File.Exists(_path))
+ if (!File.Exists(path))
{
- _logger.LogWarning("Hosts file not found: {Path}", _path);
+ _logger.LogDebug("Managed hosts file not found: {Path}", path);
return Array.Empty();
}
- var lines = await File.ReadAllLinesAsync(_path, Encoding.UTF8, ct);
+ var lines = await File.ReadAllLinesAsync(path, Encoding.UTF8, ct);
var entries = new List();
var seenContentIds = new HashSet(StringComparer.Ordinal);
for (var i = 0; i < lines.Length; i++)
@@ -48,16 +48,18 @@ public class HostsFileService : IHostsFileService
public async Task WriteAsync(IReadOnlyList entries, CancellationToken ct = default)
{
- if (string.IsNullOrEmpty(_path))
- throw new InvalidOperationException("No system hosts file configured. Set Dnsmasq:SystemHostsPath to enable hosts editing.");
- var dir = Path.GetDirectoryName(_path);
+ var set = await _configSetService.GetConfigSetAsync(ct);
+ var path = set.ManagedHostsFilePath;
+ if (string.IsNullOrEmpty(path))
+ throw new InvalidOperationException("No managed hosts path configured. Set Dnsmasq:MainConfigPath and Dnsmasq:ManagedHostsFileName to enable hosts editing.");
+ var dir = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
- var tmpPath = _path + ".tmp";
+ var tmpPath = path + ".tmp";
var lines = entries.Select(HostsFileLineParser.ToLine).ToList();
await File.WriteAllLinesAsync(tmpPath, lines, Encoding.UTF8, ct);
- File.Move(tmpPath, _path, overwrite: true);
- _logger.LogInformation("Wrote hosts file: {Path}", _path);
+ File.Move(tmpPath, path, overwrite: true);
+ _logger.LogInformation("Wrote managed hosts file: {Path}", path);
}
}
diff --git a/src/DnsmasqWebUI/appsettings.json b/src/DnsmasqWebUI/appsettings.json
index ee33647..e43a338 100644
--- a/src/DnsmasqWebUI/appsettings.json
+++ b/src/DnsmasqWebUI/appsettings.json
@@ -26,6 +26,7 @@
"Dnsmasq": {
"MainConfigPath": "/etc/dnsmasq.conf",
"ManagedFileName": "zz-dnsmasq-webui.conf",
+ "ManagedHostsFileName": "zz-dnsmasq-webui.hosts",
"SystemHostsPath": "/etc/hosts",
"ReloadCommand": "systemctl reload dnsmasq",
"StatusCommand": "systemctl is-active dnsmasq",