Repository Wiki
BeyondDimension/SteamTools

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 模式并开启加速时:

  1. ProxyService.Operate 计算出本次脚本对应的 域名 → IP 映射集合;
  2. 调用 IHostsFileService.UpdateHosts(hosts),把这些记录写入系统 hosts 文件;
  3. 写入的内容被包裹在 # Steam++ Start / # Steam++ End 标记块中,写入时若发现与已有记录冲突,原记录会被搬进 # Steam++ Backup Start/End 备份块;
  4. 关闭加速时调用 ContainsHostsByTag() 判断是否存在标记,再调用 RemoveHostsByTag() 移除标记块并还原备份块,保证用户原有配置不被破坏;
  5. 程序退出时通过 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

Loading diagram...

架构说明:

  • 插件层(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:

csharp
1var updateHostsResult = await hostsFileService.UpdateHosts(hosts); 2if (updateHostsResult.ResultType != OperationResultType.Success) 3{ 4 // 写入失败时按 OperationResultType 分支提示/回滚 5}

Source: ProxyService.Operate.cs

关闭加速时进入 case ProxyMode.Hosts: 分支,先判断再清理:

csharp
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 判断"是否需要清理"。

写盘与还原的时序

Loading diagram...

文件内标记块的物理结构

text
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 End

Source: HostsFileServiceImpl.cs

实现中 GetMarkValue 对按空白切分后的行做 3 或 4 段 匹配(3 段对应 # + 名字,例如 # Steam++ Start;OrdinalIgnoreCase 忽略大小写),以识别标记行:

csharp
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 文件助手服务契约在类型级注释中写明了权限前提:

csharp
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 接线都通过它拿实例。

特权转发逻辑在实现内部:

csharp
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 中完成:

csharp
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:

csharp
// hosts 文件助手服务 services.AddHostsFileService();

Source: Startup.cs

防覆盖机制(Windows FileSystemWatcher)

Windows 上部分安全软件或系统组件会重写 hosts 文件,导致加速记录丢失。实现仅在特权进程内对 hosts 文件挂载监听:

csharp
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

监听触发后并不是立即回写,而是经过一层防抖 + 随机退避:

csharp
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

成员签名说明
OpenFilevoid OpenFile()用系统默认编辑器打开 hosts 文件
OpenFileDirvoid OpenFileDir()打开 hosts 所在文件夹
ResetFileTask<bool> ResetFile()重置 hosts 文件为初始状态
ReadHostsAllLinesOperationResult<List<(string ip, string domain)>> ReadHostsAllLines()读取 hosts 全部记录行
UpdateHostsTask<OperationResult> UpdateHosts(string ip, string domain)更新一条记录
UpdateHostsTask<OperationResult> UpdateHosts(IEnumerable<(string ip, string domain)> hosts)批量更新记录(元组序列)
UpdateHostsTask<OperationResult> UpdateHosts(IReadOnlyDictionary<string, string> hosts)批量更新记录(字典,ProxyService.Operate 实际使用的重载)
RemoveHostsTask<OperationResult> RemoveHosts(string ip, string domain)移除一条记录
RemoveHostsTask<OperationResult> RemoveHosts(string domain)按域名移除记录
RemoveHostsByTagTask<OperationResult> RemoveHostsByTag()移除本程序标记块写入的记录,并还原写入时冲突的备份记录
OnExitRestoreHostsTask OnExitRestoreHosts()程序退出时还原 hosts 文件(兜底)
ContainsHostsByTagbool ContainsHostsByTag()当前文件是否包含本程序写入的标记;异常时返回 false 并 Toast 显示异常
OccupyHosts(仅 #if DEBUG)Task<bool> OccupyHosts()调试用途:独占 hosts 文件句柄以模拟占用冲突

所有返回 OperationResult 的方法都通过 ResultType(OperationResultType.Success / 失败枚举值)向调用方表达结果,ProxyService.Operate 据此决定提示与回滚路径。

UI 入口与网络自检

AcceleratorPageViewModel 把 hosts 相关操作包装为 ReactiveUI 命令,供加速器页面按钮绑定:

csharp
EditHostsFileCommand = ReactiveCommand.Create(hostsFileService.OpenFile); OpenHostsDirCommand = ReactiveCommand.Create(hostsFileService.OpenFileDir); ResetHostsFileCommand = ReactiveCommand.CreateFromTask(hostsFileService.ResetFile);

Source: AcceleratorPageViewModel.cs

ProxyService 则以字段方式持有服务实例,避免每次操作都走 IoC 解析:

csharp
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 中分发:

csharp
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 定位占用进程的提示),可视为该能力的可执行测试载体:

csharp
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 用期望值缓存自动重写(见上文)
监听事件风暴一次写入触发多次 ChangedisWatcherOverlapHostsing 重入锁 + 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)是加速方式的选择点,切换到反向代理模式时的证书/端口逻辑属于兄弟页面(代理加速模式)范畴。

Sources

(2 files)
src/BD.WTTS.Client/Services.Implementation/Net
src/BD.WTTS.Client/Services/Net