diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 3736ab9..7ae2c12 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -172,7 +172,8 @@ Key services:
- `DnsService` — manages DNS server configuration via PowerShell
`Set-DnsClientServerAddress` with preset support (Google, Cloudflare, etc.).
- `HostsFileService` — parses and edits the Windows hosts file with
- add/remove/toggle operations.
+ add/remove/toggle operations; keeps a one-time pristine backup and can
+ restore it (`HasBackup` / `RestoreBackup`).
- `ContextMenuService` — scans and toggles Explorer context menu shell
extensions via registry enumeration.
- `SystemReportService` — generates comprehensive system info reports
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5fdabbf..108dccc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,18 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+## [1.20.0] - 2026-06-08
+
+### Added
+- **Restore original hosts file.** A new "Restore original" button on the DNS & Hosts tab reverts the system hosts file to the pristine backup taken before SysManager first modified it.
+
+### Fixed
+- **Hosts file backup no longer destroys the pristine original.** `SaveHosts` previously copied the current hosts file over `hosts.bak` on **every** save with `overwrite: true`, so after the first save the backup already held SysManager's own output — the real original was lost and restore was impossible. The backup is now written only once (when none exists), preserving the true pre-SysManager file.
+- **DNS and hosts changes now require confirmation.** Applying a DNS preset and overwriting the system hosts file each prompt with `DialogService.Confirm` first, stating exactly what will change and how to revert. Declining makes no system change.
+
+### Changed
+- `HostsFileService` gained a path-injection constructor (used only for testing) and `HasBackup` / `RestoreBackup` members backing the new restore flow.
+
## [1.19.4] - 2026-06-08
### Fixed
diff --git a/SysManager/SysManager.Tests/HostsFileServiceTests.cs b/SysManager/SysManager.Tests/HostsFileServiceTests.cs
new file mode 100644
index 0000000..bd6a05f
--- /dev/null
+++ b/SysManager/SysManager.Tests/HostsFileServiceTests.cs
@@ -0,0 +1,96 @@
+// SysManager · HostsFileServiceTests
+// Author: laurentiu021 · https://github.com/laurentiu021/SystemManager
+// License: MIT
+
+using System.IO;
+using SysManager.Models;
+using SysManager.Services;
+
+namespace SysManager.Tests;
+
+///
+/// Backup / restore tests for . Uses the path-injection
+/// constructor so the real System32 hosts file is never touched and no admin is needed.
+///
+public class HostsFileServiceTests
+{
+ private static (HostsFileService svc, string hosts, string dir) NewServiceWithTempHosts(string initialContent)
+ {
+ var dir = Path.Combine(Path.GetTempPath(), "smtest_hosts_" + Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(dir);
+ var hosts = Path.Combine(dir, "hosts");
+ File.WriteAllText(hosts, initialContent);
+ return (new HostsFileService(hosts), hosts, dir);
+ }
+
+ [Fact]
+ public void SaveHosts_PreservesPristineOriginal_AcrossMultipleSaves()
+ {
+ var (svc, hosts, dir) = NewServiceWithTempHosts("# ORIGINAL pristine hosts\n127.0.0.1 original\n");
+ var backup = hosts + ".bak";
+ try
+ {
+ // First save backs up the pristine original.
+ svc.SaveHosts(new List
+ {
+ new() { IpAddress = "1.1.1.1", Hostname = "first", IsEnabled = true }
+ });
+ Assert.True(File.Exists(backup));
+ var backupAfterFirst = File.ReadAllText(backup);
+ Assert.Contains("ORIGINAL pristine hosts", backupAfterFirst);
+
+ // Second save must NOT overwrite the backup with SysManager's own output.
+ svc.SaveHosts(new List
+ {
+ new() { IpAddress = "2.2.2.2", Hostname = "second", IsEnabled = true }
+ });
+ var backupAfterSecond = File.ReadAllText(backup);
+ Assert.Equal(backupAfterFirst, backupAfterSecond);
+ Assert.Contains("ORIGINAL pristine hosts", backupAfterSecond);
+ }
+ finally
+ {
+ try { Directory.Delete(dir, recursive: true); } catch { }
+ }
+ }
+
+ [Fact]
+ public void RestoreBackup_RestoresPristineOriginal()
+ {
+ const string original = "# ORIGINAL pristine hosts\n127.0.0.1 original\n";
+ var (svc, hosts, dir) = NewServiceWithTempHosts(original);
+ try
+ {
+ svc.SaveHosts(new List
+ {
+ new() { IpAddress = "9.9.9.9", Hostname = "managed", IsEnabled = true }
+ });
+ Assert.DoesNotContain("ORIGINAL pristine hosts", File.ReadAllText(hosts));
+
+ Assert.True(svc.HasBackup);
+ Assert.True(svc.RestoreBackup());
+
+ // After restore the file content matches the pristine original byte-for-byte.
+ Assert.Equal(original, File.ReadAllText(hosts));
+ }
+ finally
+ {
+ try { Directory.Delete(dir, recursive: true); } catch { }
+ }
+ }
+
+ [Fact]
+ public void RestoreBackup_NoBackup_ReturnsFalse()
+ {
+ var (svc, _, dir) = NewServiceWithTempHosts("127.0.0.1 localhost\n");
+ try
+ {
+ Assert.False(svc.HasBackup);
+ Assert.False(svc.RestoreBackup());
+ }
+ finally
+ {
+ try { Directory.Delete(dir, recursive: true); } catch { }
+ }
+ }
+}
diff --git a/SysManager/SysManager/Services/HostsFileService.cs b/SysManager/SysManager/Services/HostsFileService.cs
index 2bbb826..1eabfdb 100644
--- a/SysManager/SysManager/Services/HostsFileService.cs
+++ b/SysManager/SysManager/Services/HostsFileService.cs
@@ -14,11 +14,23 @@ namespace SysManager.Services;
///
public sealed partial class HostsFileService
{
- private static readonly string HostsPath = Path.Combine(
+ private static readonly string DefaultHostsPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.System),
"drivers", "etc", "hosts");
- private static readonly string BackupPath = HostsPath + ".bak";
+ private readonly string HostsPath;
+ private readonly string BackupPath;
+
+ ///
+ /// Creates the service against the real system hosts file. The optional
+ /// override exists for testing so the backup /
+ /// restore logic can be exercised without touching System32 or needing admin.
+ ///
+ public HostsFileService(string? hostsPath = null)
+ {
+ HostsPath = hostsPath ?? DefaultHostsPath;
+ BackupPath = HostsPath + ".bak";
+ }
[GeneratedRegex(@"^[a-zA-Z0-9]([a-zA-Z0-9\-\.]*[a-zA-Z0-9])?$")]
private static partial Regex HostnameRegex();
@@ -75,15 +87,24 @@ public async Task> ReadHostsAsync(CancellationToken ct = defaul
return entries;
}
+ /// True if a pristine pre-SysManager backup of the hosts file exists.
+ public bool HasBackup => File.Exists(BackupPath);
+
///
/// Saves entries back to the hosts file. Disabled entries are written as commented lines.
- /// Creates a backup before writing.
///
+ ///
+ /// The backup is created ONLY the first time (when no backup yet exists), so it
+ /// preserves the original pre-SysManager hosts file. Previously the backup was
+ /// overwritten on every save, which meant that after the first save the ".bak"
+ /// already contained SysManager's own output — losing the pristine original and
+ /// defeating .
+ ///
public void SaveHosts(List entries)
{
- // Backup existing file
- if (File.Exists(HostsPath))
- File.Copy(HostsPath, BackupPath, overwrite: true);
+ // Preserve the pristine original: back up only if we have never backed up before.
+ if (File.Exists(HostsPath) && !File.Exists(BackupPath))
+ File.Copy(HostsPath, BackupPath, overwrite: false);
var lines = new List
{
@@ -104,6 +125,17 @@ public void SaveHosts(List entries)
File.WriteAllLines(HostsPath, lines);
}
+ ///
+ /// Restores the hosts file from the pristine backup created before SysManager
+ /// first modified it. Returns false if there is no backup to restore from.
+ ///
+ public bool RestoreBackup()
+ {
+ if (!File.Exists(BackupPath)) return false;
+ File.Copy(BackupPath, HostsPath, overwrite: true);
+ return true;
+ }
+
///
/// Validates and adds a new entry. Throws on invalid input.
///
diff --git a/SysManager/SysManager/SysManager.csproj b/SysManager/SysManager/SysManager.csproj
index ba67621..40ee42e 100644
--- a/SysManager/SysManager/SysManager.csproj
+++ b/SysManager/SysManager/SysManager.csproj
@@ -10,9 +10,9 @@
SysManager
true
NU1603;NU1701
- 1.19.4
- 1.19.4.0
- 1.19.4.0
+ 1.20.0
+ 1.20.0.0
+ 1.20.0.0
SysManager
SysManager — Windows system monitoring toolkit by laurentiu021. Network, updates, health, logs, safe deep cleanup.
https://github.com/laurentiu021/SystemManager
diff --git a/SysManager/SysManager/ViewModels/DnsHostsViewModel.cs b/SysManager/SysManager/ViewModels/DnsHostsViewModel.cs
index 19d0112..cb5c05d 100644
--- a/SysManager/SysManager/ViewModels/DnsHostsViewModel.cs
+++ b/SysManager/SysManager/ViewModels/DnsHostsViewModel.cs
@@ -120,6 +120,16 @@ private async Task ApplyDnsAsync()
return;
}
+ if (!DialogService.Instance.Confirm(
+ $"Change this PC's DNS servers to {SelectedPreset.Name} " +
+ $"({SelectedPreset.Primary}, {SelectedPreset.Secondary})?\n\n" +
+ "You can revert any time with \"Reset to automatic (DHCP)\".",
+ "Confirm DNS Change"))
+ {
+ StatusMessage = "DNS change cancelled.";
+ return;
+ }
+
IsDnsApplying = true;
StatusMessage = $"Applying {SelectedPreset.Name} DNS...";
try
@@ -214,10 +224,20 @@ private void SaveHosts()
return;
}
+ if (!DialogService.Instance.Confirm(
+ $"Overwrite the system hosts file with these {HostEntries.Count} entries?\n\n" +
+ "The original hosts file is preserved as hosts.bak (only the first time) " +
+ "and can be restored with \"Restore original\".",
+ "Confirm Hosts File Change"))
+ {
+ HostsStatus = "Save cancelled.";
+ return;
+ }
+
try
{
_hostsService.SaveHosts(HostEntries.ToList());
- HostsStatus = $"Saved {HostEntries.Count} entries. Backup created at hosts.bak.";
+ HostsStatus = $"Saved {HostEntries.Count} entries. Original preserved at hosts.bak.";
Log.Information("Hosts file saved with {Count} entries", HostEntries.Count);
}
catch (UnauthorizedAccessException)
@@ -231,6 +251,54 @@ private void SaveHosts()
}
}
+ [RelayCommand]
+ private async Task RestoreHostsAsync()
+ {
+ if (!IsElevated)
+ {
+ HostsStatus = "Restoring the hosts file requires administrator privileges.";
+ return;
+ }
+
+ if (!_hostsService.HasBackup)
+ {
+ HostsStatus = "No backup found — nothing to restore.";
+ return;
+ }
+
+ if (!DialogService.Instance.Confirm(
+ "Restore the original hosts file from backup? Your current SysManager " +
+ "changes to the hosts file will be discarded.",
+ "Confirm Restore Hosts File"))
+ {
+ HostsStatus = "Restore cancelled.";
+ return;
+ }
+
+ try
+ {
+ if (_hostsService.RestoreBackup())
+ {
+ await LoadHostsAsync();
+ HostsStatus = "Original hosts file restored from backup.";
+ Log.Information("Hosts file restored from backup");
+ }
+ else
+ {
+ HostsStatus = "No backup found — nothing to restore.";
+ }
+ }
+ catch (UnauthorizedAccessException)
+ {
+ HostsStatus = "Access denied — run as administrator to restore hosts file.";
+ }
+ catch (IOException ex)
+ {
+ HostsStatus = $"Error restoring hosts file: {ex.Message}";
+ Log.Error(ex, "Failed to restore hosts file");
+ }
+ }
+
[RelayCommand]
private Task RefreshHostsAsync() => LoadHostsAsync();
diff --git a/SysManager/SysManager/Views/DnsHostsView.xaml b/SysManager/SysManager/Views/DnsHostsView.xaml
index 2cac617..161ab11 100644
--- a/SysManager/SysManager/Views/DnsHostsView.xaml
+++ b/SysManager/SysManager/Views/DnsHostsView.xaml
@@ -100,6 +100,9 @@
+