diff --git a/src/DnsmasqWebUI/Infrastructure/DnsmasqFileEncoding.cs b/src/DnsmasqWebUI/Infrastructure/DnsmasqFileEncoding.cs
new file mode 100644
index 0000000..c9886cd
--- /dev/null
+++ b/src/DnsmasqWebUI/Infrastructure/DnsmasqFileEncoding.cs
@@ -0,0 +1,9 @@
+using System.Text;
+
+namespace DnsmasqWebUI.Infrastructure;
+
+/// Encoding for dnsmasq config and hosts files. UTF-8 without BOM so dnsmasq does not see a BOM as "bad option" on line 1.
+public static class DnsmasqFileEncoding
+{
+ public static readonly Encoding Utf8NoBom = new UTF8Encoding(false);
+}
diff --git a/src/DnsmasqWebUI/Services/DnsmasqConfigService.cs b/src/DnsmasqWebUI/Services/DnsmasqConfigService.cs
index 5759386..0bfae0f 100644
--- a/src/DnsmasqWebUI/Services/DnsmasqConfigService.cs
+++ b/src/DnsmasqWebUI/Services/DnsmasqConfigService.cs
@@ -1,4 +1,5 @@
using System.Text;
+using DnsmasqWebUI.Infrastructure;
using DnsmasqWebUI.Models.Config;
using DnsmasqWebUI.Models.Dhcp;
using DnsmasqWebUI.Models.EffectiveConfig;
@@ -82,7 +83,7 @@ public class DnsmasqConfigService : IDnsmasqConfigService
var dir = Path.GetDirectoryName(managedHostsPath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
- File.WriteAllText(managedHostsPath, "");
+ File.WriteAllText(managedHostsPath, "", DnsmasqFileEncoding.Utf8NoBom);
}
public async Task WriteDhcpHostsAsync(IReadOnlyList entries, CancellationToken ct = default)
@@ -110,7 +111,10 @@ public class DnsmasqConfigService : IDnsmasqConfigService
IReadOnlyList rawLines;
if (File.Exists(path))
- rawLines = await File.ReadAllLinesAsync(path, Encoding.UTF8, ct);
+ {
+ var read = await File.ReadAllLinesAsync(path, DnsmasqFileEncoding.Utf8NoBom, ct);
+ rawLines = StripBomFromFirstLine(read);
+ }
else
rawLines = Array.Empty();
@@ -136,7 +140,7 @@ public class DnsmasqConfigService : IDnsmasqConfigService
output.Add(DnsmasqConfDhcpHostLineParser.ToLine(entry));
var tmpPath = path + ".tmp";
- await File.WriteAllLinesAsync(tmpPath, output, Encoding.UTF8, ct);
+ await File.WriteAllLinesAsync(tmpPath, output, DnsmasqFileEncoding.Utf8NoBom, ct);
File.Move(tmpPath, path, overwrite: true);
EnsureManagedHostsFileExists(set.ManagedHostsFilePath);
var effectiveHostsPathDhcp = configLines.OfType().FirstOrDefault()?.AddnHostsPath ?? "";
@@ -152,7 +156,7 @@ public class DnsmasqConfigService : IDnsmasqConfigService
foreach (var file in set.Files.Where(f => !f.IsManaged && !string.Equals(f.Path, managedPath, StringComparison.Ordinal)))
{
if (!File.Exists(file.Path)) continue;
- var lines = await File.ReadAllLinesAsync(file.Path, Encoding.UTF8, ct);
+ var lines = await File.ReadAllLinesAsync(file.Path, DnsmasqFileEncoding.Utf8NoBom, ct);
var configLines = DnsmasqConfFileLineParser.ParseFile(lines);
foreach (var dhcp in configLines.OfType().Select(c => c.DhcpHost))
{
@@ -191,11 +195,25 @@ public class DnsmasqConfigService : IDnsmasqConfigService
var output = list.Select(DnsmasqConfFileLineParser.ToLine).ToList();
var tmpPath = path + ".tmp";
- await File.WriteAllLinesAsync(tmpPath, output, Encoding.UTF8, ct);
+ await File.WriteAllLinesAsync(tmpPath, output, DnsmasqFileEncoding.Utf8NoBom, ct);
File.Move(tmpPath, path, overwrite: true);
EnsureManagedHostsFileExists(set.ManagedHostsFilePath);
var effectiveHostsPath = list.OfType().FirstOrDefault()?.AddnHostsPath ?? "";
_configSetCache.NotifyWeWroteManagedConfig(new ManagedConfigContent(list, effectiveHostsPath));
_logger.LogInformation("Wrote managed config file: {Path}", path);
}
+
+ /// Strip UTF-8 BOM from first line if present.
+ private static IReadOnlyList StripBomFromFirstLine(IReadOnlyList lines)
+ {
+ if (lines.Count == 0) return lines;
+ var first = lines[0];
+ if (first.Length > 0 && first[0] == '\uFEFF')
+ {
+ var list = lines.ToList();
+ list[0] = first[1..];
+ return list;
+ }
+ return lines;
+ }
}
diff --git a/src/DnsmasqWebUI/Services/EnsureManagedConfigHostedService.cs b/src/DnsmasqWebUI/Services/EnsureManagedConfigHostedService.cs
index 36e6307..3161fe2 100644
--- a/src/DnsmasqWebUI/Services/EnsureManagedConfigHostedService.cs
+++ b/src/DnsmasqWebUI/Services/EnsureManagedConfigHostedService.cs
@@ -1,4 +1,5 @@
using System.Text;
+using DnsmasqWebUI.Infrastructure;
using DnsmasqWebUI.Models.Config;
using DnsmasqWebUI.Models.EffectiveConfig;
using DnsmasqWebUI.Configuration;
@@ -48,8 +49,9 @@ public class EnsureManagedConfigHostedService : IApplicationHostedService
var confFileLine = "conf-file=" + Path.GetRelativePath(mainDir, managedPath);
var lines = File.Exists(mainFull)
- ? (await File.ReadAllLinesAsync(mainFull, Encoding.UTF8, cancellationToken)).ToList()
+ ? (await File.ReadAllLinesAsync(mainFull, DnsmasqFileEncoding.Utf8NoBom, cancellationToken)).ToList()
: new List();
+ StripBomFromFirstLine(lines);
// Remove any conf-file= line that points to our managed path (we will add it as the last line).
for (var i = lines.Count - 1; i >= 0; i--)
@@ -75,12 +77,12 @@ public class EnsureManagedConfigHostedService : IApplicationHostedService
if (lines.Count > 0 && !string.IsNullOrWhiteSpace(lines[^1]))
lines.Add("");
lines.Add(confFileLine);
- await File.WriteAllLinesAsync(mainFull, lines, Encoding.UTF8, cancellationToken);
+ await File.WriteAllLinesAsync(mainFull, lines, DnsmasqFileEncoding.Utf8NoBom, cancellationToken);
_logger.LogInformation("Set {Line} as the last line of main config {Path} so the managed file is included only by conf-file=.", confFileLine, mainFull);
}
else
{
- await File.WriteAllLinesAsync(mainFull, lines, Encoding.UTF8, cancellationToken);
+ await File.WriteAllLinesAsync(mainFull, lines, DnsmasqFileEncoding.Utf8NoBom, cancellationToken);
}
Directory.CreateDirectory(managedDir);
@@ -92,5 +94,14 @@ public class EnsureManagedConfigHostedService : IApplicationHostedService
await configService.WriteManagedConfigAsync(Array.Empty(), cancellationToken);
}
+ /// Strip UTF-8 BOM from first line if present, so we do not perpetuate it when writing.
+ private static void StripBomFromFirstLine(List lines)
+ {
+ if (lines.Count == 0) return;
+ var first = lines[0];
+ if (first.Length > 0 && first[0] == '\uFEFF')
+ lines[0] = first[1..];
+ }
+
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
diff --git a/src/DnsmasqWebUI/Services/HostsFileService.cs b/src/DnsmasqWebUI/Services/HostsFileService.cs
index 840cc5f..78fe5e4 100644
--- a/src/DnsmasqWebUI/Services/HostsFileService.cs
+++ b/src/DnsmasqWebUI/Services/HostsFileService.cs
@@ -1,4 +1,4 @@
-using System.Text;
+using DnsmasqWebUI.Infrastructure;
using DnsmasqWebUI.Models.Hosts;
using DnsmasqWebUI.Parsers;
using DnsmasqWebUI.Services.Abstractions;
@@ -36,7 +36,7 @@ public class HostsFileService : IHostsFileService
var tmpPath = path + ".tmp";
var lines = entries.Select(HostsFileLineParser.ToLine).ToList();
- await File.WriteAllLinesAsync(tmpPath, lines, Encoding.UTF8, ct);
+ await File.WriteAllLinesAsync(tmpPath, lines, DnsmasqFileEncoding.Utf8NoBom, ct);
File.Move(tmpPath, path, overwrite: true);
_hostsCache.NotifyWeWroteManagedHosts(entries);
_logger.LogInformation("Wrote managed hosts file: {Path}", path);