Hosts 文件加速模式
Hosts 文件加速模式是 Watt Toolkit(SteamTools)加速器插件的两种底层加速实现之一:通过直接修改系统 hosts 文件,把目标平台域名解析到加速节点 IP,而不经过本地反向代理。整个能力由 IHostsFileService 契约与 HostsFileServiceImpl 实现承载,并通过 IPC 转发到特权(管理员)进程完成真实写盘。
Purpose and Scope
本页覆盖 Hosts 加速模式的完整端到端机制:
- 加速模式枚举中的
ProxyMode.Hosts及其在启停流程中的分支逻辑; IHostsFileService服务契约(读、写、移除、按标签清理、退出还原);HostsFileServiceImpl的实现细节:# Steam++ Start/End标记块、Backup 备份块、Windows 特权进程 IPC 转发、FileSystemWatcher防外部覆盖;- DI 注册(
AddHostsFileService)与 IPC 服务端接线(CreateIpcJoint); - UI 层入口(
AcceleratorPageViewModel的编辑/打开目录/重置命令)与网络环境自检中的HostsFileCheck步骤; - 失败模式、并发保护与运维注意事项。
本页不覆盖以下兄弟主题(它们有独立页面):
- 反向代理加速模式(本地 HTTPS 反代 + 证书信任,
ProxyMode的其他取值)→ 参见代理加速模式页; - 加速脚本
Script的下载与解析(ScriptManager、ScriptRepository)→ 参见加速脚本页; - IPC 传输层与特权进程启动机制的通用实现 → 参见 IPC 服务架构页。
Overview
Watt Toolkit 的加速原理是"改写域名解析"。当用户在加速器页面选择 Hosts 模式并开启加速时:
ProxyService.Operate计算出本次脚本对应的域名 → IP映射集合;- 调用
IHostsFileService.UpdateHosts(hosts),把这些记录写入系统hosts文件; - 写入的内容被包裹在
# Steam++ Start/# Steam++ End标记块中,写入时若发现与已有记录冲突,原记录会被搬进# Steam++ Backup Start/End备份块; - 关闭加速时调用
ContainsHostsByTag()判断是否存在标记,再调用RemoveHostsByTag()移除标记块并还原备份块,保证用户原有配置不被破坏; - 程序退出时通过
OnExitRestoreHosts()兜底还原。
由于修改 hosts 文件需要管理员权限(Windows)或 Root 权限(类 Unix),接口注释明确声明了这一约束。在 Windows 上,客户端主进程若非特权进程,会通过 IPCMainProcessService 把调用转发给以管理员身份运行的特权进程,由它真正落盘——HostsFileServiceImpl 同时是 IPC 客户端代理的消费者与特权进程内的服务端实现。
此外,Windows 上其他程序(杀毒软件、系统组件)可能随时重写 hosts 文件,导致加速记录被覆盖。实现在特权进程内挂载 FileSystemWatcher 监听文件变化,并用防抖 + 期望值缓存(ConcurrentDictionary updateHostsValue)的策略在文件被外部改动后自动重写回期望状态。
关键术语:
| 术语 | 含义 |
|---|---|
| 标记块(Mark) | # Steam++ Start / # Steam++ End 之间的区域,本程序写入的 hosts 记录都放在这里 |
| 备份块(Backup Mark) | # Steam++ Backup Start / # Steam++ Backup End,写入时被冲突覆盖的用户原有记录存放处 |
| TAG | 常量 "HostsFileS",用于识别"当前程序写入过"的标记 |
| 特权进程 | Windows 上以管理员运行的伴随进程,通过 IPC(IpcPublic)对外提供 IHostsFileService |
Architecture
架构说明:
- 插件层(
BD.WTTS.Client.Plugins.Accelerator)不直接触碰hosts文件,全部通过IHostsFileService契约完成。ProxyService(部分类ProxyService.Operate.cs)负责加速开关的状态机;AcceleratorPageViewModel暴露与 hosts 文件相关的三个 UI 命令;NetworkEnvCheckService在网络诊断时做 hosts 检查。 - 核心层(
BD.WTTS.Client)中,IHostsFileService被[IpcPublic]标注,天然可跨进程调用;HostsFileServiceImpl是唯一实现。它持有IPlatformService(获取HostsFilePath与特权进程判断),并在 Windows 主进程非特权时通过IPCMainProcessService.GetServiceAsync<IHostsFileService>(IPlatformService.IPCRoot.moduleName)拿到指向特权进程的 IPC 代理_privilegedThis。 - 操作系统层:真实文件读写与
FileSystemWatcher监听都发生在特权进程内,普通进程视角下一切只是异步 IPC 调用。
Core Flow
开启加速(Hosts 模式)
ProxyService.Operate.cs 中,当 ProxyMode.Hosts 被选中时,会把脚本解析出的 IP/域名集合交给 UpdateHosts:
1var updateHostsResult = await hostsFileService.UpdateHosts(hosts);
2if (updateHostsResult.ResultType != OperationResultType.Success)
3{
4 // 写入失败时按 OperationResultType 分支提示/回滚
5}Source: ProxyService.Operate.cs
关闭加速时进入 case ProxyMode.Hosts: 分支,先判断再清理:
1case ProxyMode.Hosts:
2 var needClear = hostsFileService.ContainsHostsByTag();
3 if (needClear)
4 {
5 var removeHostsResult = await hostsFileService.RemoveHostsByTag();
6 if (removeHostsResult.ResultType != OperationResultType.Success)
7 {
8 // 移除失败处理
9 }
10 }Source: ProxyService.Operate.cs
ContainsHostsByTag() 的契约特意注明:发生异常时也返回 false 并 Toast 显示异常,因此调用方无需再包一层 try/catch 判断"是否需要清理"。
写盘与还原的时序
文件内标记块的物理结构
1# ... 用户原有内容 ...
2# Steam++ Start
3127.0.0.1 example-1.steamserver.net
4127.0.0.1 example-2.steamserver.net
5# Steam++ End
6# Steam++ Backup Start
7# 202.96.x.x example-1.steamserver.net (写入时被覆盖的原记录)
8# Steam++ Backup EndSource: HostsFileServiceImpl.cs
实现中 GetMarkValue 对按空白切分后的行做 3 或 4 段 匹配(3 段对应 # + 名字,例如 # Steam++ Start;OrdinalIgnoreCase 忽略大小写),以识别标记行:
1internal const string MarkStart = "# Steam++ Start";
2internal const string MarkEnd = "# Steam++ End";
3internal const string BackupMarkStart = "# Steam++ Backup Start";
4internal const string BackupMarkEnd = "# Steam++ Backup End";
5
6/// <summary>
7/// 根据行切割数组获取标记值
8/// </summary>
9static string? GetMarkValue(string[] line_split_array)
10{
11 if (line_split_array.Length == 3 || line_split_array.Length == 4)
12 {
13 var value = string.Join(' ', line_split_array);
14 if (line_split_array.Length == 3)
15 {
16 if (string.Equals(value, MarkStart, StringComparison.OrdinalIgnoreCase))
17 {
18 return MarkStart;
19 }
20 if (string.Equals(value, MarkEnd, StringComparison.OrdinalIgnoreCase))Source: HostsFileServiceImpl.cs
这种"标记块 + 备份块"的设计意图是幂等且可逆:程序写入的内容永远只住在自己的标记块内,用户手工内容除被冲突覆盖的部分外原样保留,关停加速即可完全还原。兼容历史名称 Steam++ 而非当前产品名,是为了让老版本写入的块也能被新版本识别清理。
权限与 IPC 转发
hosts 文件助手服务契约在类型级注释中写明了权限前提:
1/// <summary>
2/// hosts 文件助手服务,修改需要管理员权限或 Root 权限
3/// </summary>
4[IpcPublic(Timeout = AssemblyInfo.IpcTimeout, IgnoresIpcException = false)]
5public interface IHostsFileService
6{
7 static class Constants
8 {
9 public static IHostsFileService Instance => Ioc.Get<IHostsFileService>();
10 }Source: IHostsFileService.cs
[IpcPublic]让该接口可被 IPC 管道序列化调用;IgnoresIpcException = false意味着跨进程异常会真实抛回调用方,保证加速启停不会"静默失败"。Constants.Instance是全代码库统一的获取方式:ProxyService字段初始化、AcceleratorPageViewModel构造器、Startup.Commands的 IPC 接线都通过它拿实例。
特权转发逻辑在实现内部:
1readonly IPlatformService s;
2readonly object lockObj = new();
3IHostsFileService? _privilegedThis;
4
5async ValueTask<IHostsFileService?> GetPrivilegedThisAsync()
6{
7#if WINDOWS
8 if (Startup.Instance.IsMainProcess && !WindowsPlatformServiceImpl.IsPrivilegedProcess)
9 {
10 if (_privilegedThis == null)
11 {
12 var ipc = IPCMainProcessService.Instance;
13 _privilegedThis = await ipc.GetServiceAsync<IHostsFileService>(IPlatformService.IPCRoot.moduleName);
14 }
15 return _privilegedThis;
16 }
17#endif
18 return null;
19}Source: HostsFileServiceImpl.cs
设计意图:Windows 上主进程通常是普通权限,直接写 hosts 会抛 UnauthorizedAccessException。实现采用"同一接口、双角色"策略——普通进程内该实例充当 IPC 客户端把调用转给特权进程;特权进程内(WindowsPlatformServiceImpl.IsPrivilegedProcess == true)则自己执行文件操作并启动 FileSystemWatcher。lockObj 用于串行化本地文件操作,_privilegedThis 懒加载缓存代理避免重复 IPC 握手。
服务端接线在 Startup.Commands.cs 中完成:
1var platformService = IPlatformService.Instance;
2IHostsFileService hostsFileService = IHostsFileService.Constants.Instance;
3ipcProvider.CreateIpcJoint<IPCPlatformService>(platformService);
4ipcProvider.CreateIpcJoint(hostsFileService);Source: Startup.Commands.cs
客户端 DI 注册位于 src/BD.WTTS.Client.Avalonia.App/Startup.cs:
// hosts 文件助手服务
services.AddHostsFileService();Source: Startup.cs
防覆盖机制(Windows FileSystemWatcher)
Windows 上部分安全软件或系统组件会重写 hosts 文件,导致加速记录丢失。实现仅在特权进程内对 hosts 文件挂载监听:
1void StartWatcher()
2{
3 if (WindowsPlatformServiceImpl.IsPrivilegedProcess)
4 {
5 watcher?.Dispose();
6 var hostsDirectoryName = Path.GetDirectoryName(s.HostsFilePath);
7 var hostsFileName = Path.GetFileName(s.HostsFilePath);
8 watcher = new FileSystemWatcher(hostsDirectoryName.ThrowIsNull())
9 {
10 EnableRaisingEvents = true,
11 NotifyFilter = NotifyFilters.Attributes
12 | NotifyFilters.CreationTime
13 | NotifyFilters.DirectoryName
14 | NotifyFilters.FileName
15 | NotifyFilters.LastWrite
16 | NotifyFilters.Size,
17 Filter = hostsFileName,
18 };
19 watcher.Changed += Watcher_Changed;
20 watcher.Deleted += Watcher_Deleted;
21 }
22}Source: HostsFileServiceImpl.cs
监听触发后并不是立即回写,而是经过一层防抖 + 随机退避:
1async Task WatcherOverlapHosts()
2{
3 if (isWatcherOverlapHostsing || watcher == null)
4 return;
5
6 isWatcherOverlapHostsing = true;
7 try
8 {
9 if (!updateHostsValue.IsEmpty)
10 {
11 if (lastWatcherOverlapHostsTime != default && (DateTime.Now - lastWatcherOverlapHostsTime) < TimeSpan.FromSeconds(2.65d))
12 {
13 lastWatcherOverlapHostsTimeCTS?.Cancel();
14 lastWatcherOverlapHostsTimeCTS = new();
15 try
16 {
17 await Task.Delay(Random2.Next(550, 850), lastWatcherOverlapHostsTimeCTS.Token);
18 lastWatcherOverlapHostsTimeCTS = null;
19 }
20 catch (TaskCanceledException)
21 {
22 return;
23 }
24 catch (ObjectDisposedException)
25 {
26 return;
27 }
28 }
29
30 lastWatcherOverlapHostsTime = DateTime.Now;
31 if (watcher == null)
32 return;
33 HandleHosts(isUpdateOrRemove: true, updateHostsValue);
34 }
35 }
36 finally
37 {
38 isWatcherOverlapHostsing = false;
39 }
40}Source: HostsFileServiceImpl.cs
关键点与设计意图:
updateHostsValue(ConcurrentDictionary<string, string>)是"期望状态"缓存:加速开启时记录期望的ip → domain集合;一旦外部改动触发Changed/Deleted,就调用HandleHosts(isUpdateOrRemove: true, updateHostsValue)把期望值重新写回,实现"最终一致"。isWatcherOverlapHostsing布尔重入锁:FileSystemWatcher对同一次修改常触发多次事件,此标志保证同一时刻只有一次回写在执行,避免读改写竞态把文件写坏。- 2.65 秒窗口 + 550~850ms 随机延迟:若事件密集到来,先取消上一次的延迟任务再重新等待,抖动结束后才执行一次回写。随机量避免与对方进程形成固定的写-监听循环(相互触发的活锁)。
CancellationTokenSource的取消异常被显式吞掉(TaskCanceledException/ObjectDisposedException直接 return),因为"计划中的取消"不是错误。NotifyFilter覆盖属性、大小、写入时间等几乎所有维度,配合Filter = hostsFileName精确锁定单个文件,减少同目录其他文件带来的噪声事件。
API Reference
IHostsFileService 完整契约(实现类为 internal sealed class HostsFileServiceImpl):
Source: IHostsFileService.cs
| 成员 | 签名 | 说明 |
|---|---|---|
OpenFile | void OpenFile() | 用系统默认编辑器打开 hosts 文件 |
OpenFileDir | void OpenFileDir() | 打开 hosts 所在文件夹 |
ResetFile | Task<bool> ResetFile() | 重置 hosts 文件为初始状态 |
ReadHostsAllLines | OperationResult<List<(string ip, string domain)>> ReadHostsAllLines() | 读取 hosts 全部记录行 |
UpdateHosts | Task<OperationResult> UpdateHosts(string ip, string domain) | 更新一条记录 |
UpdateHosts | Task<OperationResult> UpdateHosts(IEnumerable<(string ip, string domain)> hosts) | 批量更新记录(元组序列) |
UpdateHosts | Task<OperationResult> UpdateHosts(IReadOnlyDictionary<string, string> hosts) | 批量更新记录(字典,ProxyService.Operate 实际使用的重载) |
RemoveHosts | Task<OperationResult> RemoveHosts(string ip, string domain) | 移除一条记录 |
RemoveHosts | Task<OperationResult> RemoveHosts(string domain) | 按域名移除记录 |
RemoveHostsByTag | Task<OperationResult> RemoveHostsByTag() | 移除本程序标记块写入的记录,并还原写入时冲突的备份记录 |
OnExitRestoreHosts | Task OnExitRestoreHosts() | 程序退出时还原 hosts 文件(兜底) |
ContainsHostsByTag | bool ContainsHostsByTag() | 当前文件是否包含本程序写入的标记;异常时返回 false 并 Toast 显示异常 |
OccupyHosts(仅 #if DEBUG) | Task<bool> OccupyHosts() | 调试用途:独占 hosts 文件句柄以模拟占用冲突 |
所有返回 OperationResult 的方法都通过 ResultType(OperationResultType.Success / 失败枚举值)向调用方表达结果,ProxyService.Operate 据此决定提示与回滚路径。
UI 入口与网络自检
AcceleratorPageViewModel 把 hosts 相关操作包装为 ReactiveUI 命令,供加速器页面按钮绑定:
EditHostsFileCommand = ReactiveCommand.Create(hostsFileService.OpenFile);
OpenHostsDirCommand = ReactiveCommand.Create(hostsFileService.OpenFileDir);
ResetHostsFileCommand = ReactiveCommand.CreateFromTask(hostsFileService.ResetFile);Source: AcceleratorPageViewModel.cs
ProxyService 则以字段方式持有服务实例,避免每次操作都走 IoC 解析:
readonly IScriptManager scriptManager = IScriptManager.Instance;
readonly IHostsFileService hostsFileService = IHostsFileService.Constants.Instance;
readonly IPlatformService platformService = IPlatformService.Instance;Source: ProxyService.cs
网络环境自检(用于"网络修复"功能)把 hosts 检查列为第 1 步,枚举定义在 NetworkEnvCheckStep.HostsFileCheck = 1,NetworkEnvCheckService 使用 Parallel.ForAsync 并行跑所有检查步骤,并在 switch 中分发:
1await Parallel.ForAsync(
2 (int)NetworkEnvCheckStep.HostsFileCheck,
3 (int)NetworkEnvCheckStep.LSPCheck + 1,
4 ...);
5
6(int)NetworkEnvCheckStep.HostsFileCheck => await CheckHostsAsync(),
7(int)NetworkEnvCheckStep.NetworkInterfaceCheck => CheckNetworkInterfaces(),Sources:
仓库中还附带一个独立的诊断工具 src/BD.WTTS.Client.Tools.HostsTest/Program.cs,用于在开发期验证 hosts 读写与句柄占用行为(含 handle.exe 定位占用进程的提示),可视为该能力的可执行测试载体:
var hostsFilePath = Path.Combine(Environment.SystemDirectory, "drivers", "etc", "hosts");
Console.WriteLine($"hosts 文件路径:{hostsFilePath}");Source: Program.cs
Failure Modes, Edge Cases & Concurrency
基于已读源码可确认的失败模式与并发防护:
| 场景 | 触发条件 | 源码中的处理 |
|---|---|---|
| 权限不足 | 非特权进程直接写盘(非 Windows 或 IPC 不可用) | Windows 主进程通过 GetPrivilegedThisAsync() 转发到特权进程;接口注释声明需管理员/Root 权限 |
| IPC 异常 | 特权进程未启动 / 管道断开 | [IpcPublic(..., IgnoresIpcException = false)] 使异常真实抛回,UpdateHosts 返回非 Success 的 OperationResult,由 ProxyService.Operate 分支处理 |
| 读取/判断异常 | hosts 文件被锁、被删或格式异常 | ContainsHostsByTag() 契约:异常时返回 false 并 Toast 展示异常,调用方无需额外 try/catch |
| 外部程序覆盖记录 | 杀软/系统重写 hosts | 特权进程内 FileSystemWatcher + WatcherOverlapHosts 用期望值缓存自动重写(见上文) |
| 监听事件风暴 | 一次写入触发多次 Changed | isWatcherOverlapHostsing 重入锁 + 2.65s 窗口 + 550~850ms 随机退避 |
| 文件被删除 | hosts 被整体删除 | watcher.Deleted 同样触发重写期望状态 |
| 并发读写竞态 | 多个调用同时修改文件 | 实例字段 readonly object lockObj 串行化本地文件操作 |
| 代理缓存失效 | 特权进程重启 | _privilegedThis 为普通字段缓存;进程身份判断在每次 GetPrivilegedThisAsync 中重新做,未命中才重新解析 IPC 代理 |
边界条件:
- 平台门控:整个接口与实现都被
#if (WINDOWS || MACCATALYST || MACOS || LINUX) && !(IOS || ANDROID)包裹,移动端不提供该能力;FileSystemWatcher与特权转发逻辑进一步被#if WINDOWS限定。 - 旧版本兼容:标记块沿用历史名称
# Steam++ Start/End(而非 Watt Toolkit),且GetMarkValue使用OrdinalIgnoreCase匹配,保证旧版写入的块可被识别与清理。 - 历史遗留
TAG = "HostsFileS"常量保留在实现中,用于识别本程序写入的标签。
Performance / Operational Notes
- 一次启停 = 一次全文件读改写:标记块的定位与替换需要对
hosts全文做行级扫描(GetMarkValue按切分数组匹配),属 O(行数) 操作。hosts文件通常仅数百行,开销可忽略,但意味着每次更新都是"读全文 → 重排 → 写全文",幂等性由标记块保证。 - IPC 超时:接口级
[IpcPublic(Timeout = AssemblyInfo.IpcTimeout)]统一控制跨进程调用超时,UI 侧应按异步命令处理,不应假设即时返回。 - 退出兜底:
OnExitRestoreHosts()在程序退出时还原文件,是防止"加速开着程序崩了导致 hosts 残留"的运维保险;网络自检中的HostsFileCheck步骤则为用户提供了事后发现残留的手段。 - 运行时观测:
BD.WTTS.Client.Tools.HostsTest控制台工程可在目标机上单独验证 hosts 路径、读写权限与句柄占用(handle.exe),是排障时的第一工具。
Extension Points
- 新增写入口:任何需要域名劫持的功能都应复用
IHostsFileService.Constants.Instance,而不是自己拼文件——这样自动获得标记块隔离、备份还原、IPC 特权转发与防覆盖四项能力。 - 新增清理策略:
RemoveHostsByTag()已实现"移除并还原备份"的完整逆操作;若要扩展部分还原(如按脚本分组还原),需在HandleHosts及标记常量层面扩展,保持MarkStart/MarkEnd对旧版本可读。 - 跨平台:Linux/macOS 路径由
IPlatformService.HostsFilePath提供,StartWatcher目前仅在 Windows 特权进程启用;为其他平台补齐监听只需在对应平台实现处扩展StartWatcher的平台宏。 - 模式切换:
ProxyMode枚举(含Hosts)是加速方式的选择点,切换到反向代理模式时的证书/端口逻辑属于兄弟页面(代理加速模式)范畴。
Related Links
- IHostsFileService.cs — 服务契约(
IpcPublic标注与全部方法签名) - HostsFileServiceImpl.cs — 唯一实现(标记块、备份块、IPC 转发、FileSystemWatcher)
- ProxyService.Operate.cs — 加速启停流程中
ProxyMode.Hosts分支 - ProxyService.cs — 服务实例持有与命令入口
- AcceleratorPageViewModel.cs — hosts 编辑/打开/重置 UI 命令
- NetworkEnvCheckService.cs —
HostsFileCheck网络自检步骤 - NetworkEnvCheckStep.cs — 检查步骤枚举
- ProxyMode.cs — 加速模式枚举
- Startup.cs —
AddHostsFileServiceDI 注册 - Startup.Commands.cs — 特权进程 IPC 服务端
CreateIpcJoint接线 - Program.cs (HostsTest) — hosts 读写/占用诊断工具