我只是想上个网,为什么还要研究这些?
折腾代理客户端,经常会遇到一种很微妙的体验:
软件装好了,节点也有了,但离“正常使用”还差一段距离。
下载客户端
↓
导入订阅
↓
订阅格式不兼容,找个转换服务
↓
选核心、换版本
↓
开系统代理,发现有些程序不跟
↓
开 TUN,发现国内网站变慢
↓
调整 DNS、FakeIP、分流规则
↓
换一台电脑,再来一遍
不是这些客户端没有功能。恰恰相反,很多客户端的功能已经很丰富。
但对我来说,问题在于:功能都有,不代表它们已经组成了一条完整的使用路径。
下载核心是一件事,转换订阅是一件事,注册系统服务是一件事,处理 DNS 又是另一件事。每个环节都能找到工具,最后负责把它们接起来的人,还是用户。
GUI 把配置项变成了按钮和下拉框,但用户仍然需要理解这些配置项之间的关系。
我想要的东西比较直接:
- 换一台机器,一行命令或者双击安装包,就能把运行环境准备好。
- 订阅是什么格式,尽量交给程序识别和转换。
- 核心可以升级,也可以固定版本,不和界面绑死。
- 国内访问尽量保持原来的网络路径,需要代理的流量自动进入核心。
- 出问题时,能看出卡在哪一层,而不是面对一个没有解释的“连接失败”。
于是有了 Sempre。
它不是一个新的代理协议实现,而是一个跨平台的代理核心管理平台。
先用起来:一行命令,或者双击 bundle
Linux 和 macOS:
curl -fsSL https://sempre.run/install | sh
Windows PowerShell:
irm https://sempre.run/install.ps1 | iex
如果希望第一次安装时就配置好核心和订阅,也可以一起传进去:
curl -fsSL https://sempre.run/install | sh -s -- \
--core='sing-box@stable' \
--subscription='https://example.com/your-subscription'
官网提供了命令生成器,不需要自己记参数。
这条命令背后做的事情,不只是下载一个可执行文件:
识别操作系统和架构
→ 定位发布版本
→ 下载对应 bundle
→ 校验 SHA-256
→ 安装程序与资源
→ 注册并启动原生系统服务
→ 打开 Web 控制台
带订阅安装时,还会继续完成订阅更新、配置生成和核心启动。没有订阅、也没有已有配置时,服务先保持待配置状态。
这里的“一键”,是把安装编排交给程序,不是省掉必要的管理员授权,也不是凭空提供代理节点。
不想运行在线脚本,就下载完整包
从 Releases 下载对应平台的 bundle,解压后运行安装入口:
| 平台 | 安装入口 |
|---|---|
| Windows | 双击 install.cmd |
| macOS | 双击 install.command |
| Linux | 运行 install.sh,桌面环境也提供 install.desktop |
bundle 带着程序、官方 Web UI 和核心资源,适合不想从零下载依赖的场景。订阅更新和实际联网仍然需要网络。
在线脚本和 bundle 最终进入的是同一套安装逻辑。重复执行可以用于安装、修复或升级,正常安装路径会保留已有配置。
安装之后,真正长期运行的是系统服务:
| 平台 | 服务管理 |
|---|---|
| Windows | Windows SCM |
| Linux | systemd |
| macOS | launchd |
浏览器只是控制台。关闭网页,不等于关闭代理;停止代理核心,也不等于失去管理界面。
这一点很重要。管理工具不能在被管理的东西坏掉时,也一起消失。
Sempre 管配置和生命周期,核心负责真正转发
先把边界说清楚:
Sempre 不实现自己的 TUN,也不重新实现 sing-box 的代理协议。
TUN、协议握手、连接转发、业务流量路由,仍然由外部核心负责。Sempre 负责把它们安装好、配置好、启动起来,并管理外围的系统集成。
flowchart TB
UI["浏览器 / CLI"] --> Manager["Sempre 管理服务"]
OS["SCM / systemd / launchd"] --> Manager
subgraph Control["Sempre"]
Manager --> Profile["订阅、规则与 Profile"]
Profile --> Compiler["配置编译器"]
Compiler --> Validate["使用目标核心校验配置"]
Validate --> Supervisor["暂存、启动与运行监督"]
Manager --> Integration["系统 DNS 与路由集成"]
end
Supervisor --> Core["sing-box / Mihomo"]
subgraph Data["外部核心"]
Core --> Inbound["TUN / TProxy / 本地代理入口"]
Inbound --> Route["业务流量路由"]
Route --> Outbound["直连 / 代理 / 私网连接器 / 拒绝"]
end
Sempre 自带一个 Rust DNS 前置解析器,后面会展开讲。但它处理 DNS,不接管代理核心的数据转发职责。
这种分工带来一个很实际的结果:界面、配置模型和核心版本,可以分别演进。
订阅格式,不应该决定你必须用哪个客户端
Sempre 的转换逻辑并不是从零凭空长出来的。它整合了我之前在 OhMyWrt Toolbox 中积累的订阅转换能力,后来统一到了 Rust 转换核心。
现在,一份 Profile 可以组合多个订阅地址、原始订阅文本和手工节点。
输入可以是 Clash YAML/JSON 代理列表,也可以是常见的 URI 或 Base64 订阅;节点协议覆盖 VMess、VLESS、Shadowsocks、Trojan、Hysteria、Hysteria 2、TUIC、AnyTLS 等。
转换过程可以理解成:
不同来源、不同格式的订阅
↓
统一的节点与配置模型
↓
合并规则、代理组和 DNS 策略
↓
根据核心、版本和平台生成配置
↓
用实际核心二进制校验
不是拿到一份 YAML,再机械地换成 JSON。
节点能不能表达、字段在目标版本里有没有意义、某个功能在当前平台能不能用,都属于转换的一部分。
也需要区分两种“支持”:
| 能力 | 含义 |
|---|---|
| 配置转换 | 能生成某种核心或客户端需要的格式 |
| 完整托管 | 能安装、校验、启动、停止、控制和诊断这个核心 |
目前完整托管的稳定适配器是 sing-box 和 Mihomo。转换输出覆盖的格式更多,但不能因此宣称所有对应核心都已经完整托管。
对 sing-box 的适配,不能停留在“支持 JSON”
sing-box 的灵活性很强,版本之间的配置语义也会变化。
比如 DNS 的旧版写法、新版独立 DNS server,以及 1.14 的响应匹配语义,并不是同一份模板换个版本号就能解决的。
Sempre 按核心版本和平台选择编译目标,覆盖 sing-box 1.11~1.14 的配置代际。新版能力和旧版兼容分别处理,再交给实际安装的核心检查。
核心管理也有明确的版本语义:
sempre core install sing-box@stable
sempre core use sing-box@stable
stable 是最新正式发布,不会偷偷切到预发布版本。需要测试版时,可以明确指定版本;需要保留某个版本时,也可以固定它。
安装新核心与切换正在使用的核心,是两个动作。
我不希望“下载了一个新版本”,就等于“现在立刻拿它接管我的网络”。
分流首先要回答:这个连接究竟经过哪里?
讨论自动分流,很容易从“国内直连,国外代理”开始。
这句话作为目标没问题,作为实现还远远不够。
因为访问一个网站,至少涉及两条链路:
| 链路 | 回答的问题 |
|---|---|
| DNS 解析 | 这个名字对应什么地址,由谁来解析? |
| 业务连接 | 拿到地址之后,这个连接从哪里出去? |
DNS 查询经过代理,不代表业务连接一定经过代理。
DNS 返回真实 IP,也不代表业务连接一定直连。
两条链路需要配合,但不能混为一谈。
先看已经进入 sing-box 的流量
在当前托管配置中,私网连接器、私有地址、优先规则集和用户显式规则,有各自的优先级。不能因为增加了“自动国内分流”,就把这些已有规则挤到后面。
排除 DNS 接管等入口处理后,可以把主要业务决策简化成:
flowchart TD
Input["进入 sing-box 的业务连接"] --> Explicit{"命中更高优先级规则?"}
Explicit -- 是 --> Selected["使用对应的直连、代理、私网或拒绝动作"]
Explicit -- 否 --> Domain{"命中国内域名集?"}
Domain -- 是 --> Direct["直连"]
Domain -- 否 --> Resolve["尚未解析的域名<br/>通过 remote DNS 获取真实地址"]
Resolve --> GeoIP{"命中国内 IP 集?"}
GeoIP -- 是 --> Direct
GeoIP -- 否 --> Other{"命中后续普通规则集?"}
Other -- 是 --> RuleOutbound["使用规则指定的出口"]
Other -- 否 --> Final["使用配置的最终出口"]
这里有两层判断:
- 域名判断:已经知道是国内域名,不需要先绕去远程解析一遍。
- 地址判断:域名表没有覆盖,但真实地址属于国内,仍然有机会直连。
这让分流不必完全依赖一份永远追不完的域名清单。
FakeIP 不能直接拿来判断 GeoIP
这是最近一次修复里很值得讲的细节。
假设应用拿到的是:
some-site.example → 198.18.0.42
这个地址只是核心分配的 FakeIP,不是网站真实服务器地址。
核心可以从映射中恢复出 some-site.example,但:
恢复出了域名 ≠ 已经拿到了真实 IP
如果直接把 FakeIP 拿去匹配国内 IP 集,当然判断不出网站真正的位置。
所以,对走到这一阶段、仍未解析的域名,需要先解析真实地址,再执行 GeoIP 判断。
为什么这里不能随手用本地 DNS?
因为 sing-box 的 resolve 不是一个纯粹的“查询辅助信息”动作。
它会把请求目的地从域名解析成 IP。显式指定 server 时,还会绕过普通 DNS 路由选择,直接使用这个解析器。sing-box 的动作定义
如果为了判断国内地址,先向不可信的本地解析器查询:
未知域名
→ 本地 DNS 返回错误地址
→ 没命中国内规则
→ 最后选择了代理
表面上看,最后还是代理。
问题是,代理可能拿着前面那个错误地址去连接。
“出口选对了”,并不能纠正一个已经被污染的目的地址。
因此当前设计是:
已知国内域名 → 直接处理
剩余域名 → resolve(remote) → GeoIP 判断
并且让不同 DNS server 使用独立缓存,避免旧版核心把本地解析器缓存过的答案,拿来满足一次明确要求远程解析的查询。
这两个条件要一起成立:
解析器选对,缓存也不能串。
代价也很明确:未知的国内域名,经过远程 DNS 后可能得到境外 CDN 地址,最终仍然走代理。
它不是完美的地理位置识别器。但比起为了提高“国内命中率”,把未知域名的本地答案直接变成连接目标,我更愿意保留这个边界。具体顺序与约束记录在分流设计文档里。
DNS 前置:能在核心外解决的,就不要先送进核心
前面的分流图有一个前提:
流量已经进入了 sing-box。
但国内网站为什么必须先进核心,再由核心决定直连?
这是 Sempre 后来调整 DNS 架构的起点。
当时我重新看了自己 OpenWrt 网关上的方案。值得借鉴的不是堆了多少组件,而是一个很朴素的顺序:
先识别明确的国内域名
→ 使用原始网络 DNS
其余域名
→ 再交给代理核心
Sempre 最终没有要求用户再安装一套 AdGuard Home 或 mosdns,而是复用、整理已有的 Rust DNS 能力,做成共享的前置解析器。
查询先到前置层,再决定交给谁
下面先看普通公网域名的处理,私网 DNS 留到后面:
flowchart TD
Desktop["桌面应用"] --> System["操作系统 DNS"]
System --> Platform["平台 DNS 接管"]
Platform --> Front["Sempre DNS 前置层"]
LAN["LAN 客户端"] --> Gateway["网关 DNS 入口与重定向"]
Gateway --> Front
Front --> Rewrite{"命中 DNS 重写?"}
Rewrite -- 是 --> Answer["返回配置的答案"]
Rewrite -- 否 --> HTTPS{"命中 HTTPS 记录拒绝策略?"}
HTTPS -- 是 --> Reject["返回策略拒绝响应"]
HTTPS -- 否 --> Custom{"命中自定义分流规则?"}
Custom -- 直连 --> Original["原始网络 DNS"]
Custom -- 代理 --> Core["当前核心 DNS"]
Custom -- 未命中 --> Domestic{"命中内置国内域名表?"}
Domestic -- 是 --> Original
Domestic -- 否 --> Core
Original --> Real["返回真实 IP"]
Core --> Policy["按 Profile 处理<br/>FakeIP、远程解析等"]
内置国内域名表来自 OhMyWrt 积累的 domains-min 数据,以经过校验的快照随程序分发。
这里的“原始 DNS”,指接管之前发现或显式配置的网络解析器,不是再调用已经指回 Sempre 的系统 DNS,否则就绕成环了。
而且,前置层自己发出的解析请求,也必须有正确的物理网络出口。仅仅把目标写成某个 DNS 地址,不代表请求真的绕过了 TUN。
自定义规则,要同时影响两条链路
假设一个域名在国内表里,但你明确希望它使用代理。
如果只在 sing-box 里加业务路由,而前置 DNS 仍然给它返回真实地址,那么在后面要讲的受限 FakeIP 捕获模式里,这个连接可能根本不会进入核心。
核心里的规则写得再漂亮,也没有机会执行。
所以 Sempre 的“分流规则”会同时生成两部分:
| 规则模式 | DNS 前置层 | 核心业务路由 |
|---|---|---|
| 直连 | 交给原始 DNS | 生成直连规则 |
| 代理 | 交给核心 DNS | 生成对应的独立代理选择组与路由 |
这些自定义 DNS 分流规则优先于内置国内域名表。
同一份域名策略,分别落到 DNS 和业务路由中,而不是让用户维护两份容易打架的名单。
核心应用新配置后,还可以为对应规则组切换节点。
这里说的是 Sempre 专门的分流规则功能,不是把任意核心原生规则都自动翻译成 DNS 规则。
FakeIP:让 DNS 的判断,接上操作系统的路由
现在把两条链路接起来。
FakeIP 可以理解成一个占位地址:
example.com ↔ 198.18.0.42
它不代表网站真的部署在那里,而是让应用先拿到一个可用于发起连接的地址。连接进入核心后,核心再恢复对应域名并继续处理。
Sempre 的常用 FakeIP 地址范围是:
IPv4:198.18.0.0/15
IPv6:fc00::/18
这也是 sing-box 新版 FakeIP DNS server 使用的典型配置范围。FakeIP 配置说明
单独看,FakeIP 只是一个 DNS 技术。
配合前置分流和 TUN 捕获范围,它才变成一套完整的自动分流方案。
桌面 FakeIP 模式的完整路径
下面以托管桌面 sing-box TUN、普通公网域名为例:
flowchart TD
Query["应用查询域名"] --> Front{"前置 DNS 判断"}
Front -- 明确直连或国内域名 --> Local["原始 DNS"]
Local --> Real["返回真实 IP"]
Real --> Normal["应用连接真实地址"]
Normal --> Physical["按普通系统路由访问<br/>绕过代理核心"]
Front -- 代理或默认路径 --> CoreDNS["sing-box DNS"]
CoreDNS --> Fake["返回 FakeIP"]
Fake --> Connection["应用连接 FakeIP"]
Connection --> TUN["FakeIP 网段路由进入 TUN"]
TUN --> Restore["核心恢复域名"]
Restore --> Rules["执行核心分流规则"]
Rules --> Exit["代理 / 直连 / 拒绝"]
这里最关键的不是“国内直连”四个字,而是:
对前置层已经明确直连的普通公网域名,DNS 查询和后续业务连接,都可以不经过代理核心。
另一方面,拿到 FakeIP 也不等于最终一定代理。
一个域名没有命中国内域名表,先拿到了 FakeIP;连接进入核心后,又通过真实 IP 判断为国内,仍然可以选择直连。
区别是:这种直连发生在核心内部,而不是完全绕过核心。
关闭 FakeIP,前置 DNS 仍然有价值
容易产生的误解是:
“没有 FakeIP,那前置 DNS 就没用了吧?”
不是。
没有 FakeIP 时,前置层仍然可以决定:
- 国内域名交给原始 DNS,尽量保留本地 CDN 解析结果。
- 其他域名交给核心的远程 DNS。
改变的是后续流量如何捕获:
| 托管桌面模式 | 国内域名解析 | 其他域名解析 | 普通业务流量捕获 |
|---|---|---|---|
| FakeIP | 原始 DNS,真实 IP | 核心返回 FakeIP | 主要捕获 FakeIP 网段 |
| Real-IP | 原始 DNS,真实 IP | 核心远程解析,真实 IP | 全量进入 TUN,再由核心分流 |
Real-IP 模式下,DNS 答案都是“真实地址”,操作系统不能仅凭它们判断哪个来自直连策略、哪个来自代理策略。
因此仍然保留前置 DNS,但让业务流量进入核心统一决策。
DNS 前置分流一直存在;FakeIP 决定能否方便地把一部分业务流量也提前分出去。
自动分流也有前提
这不是一个能猜出所有应用意图的魔法。
应用绕过系统 DNS、自带 DoH、直接连接字面 IP,或者使用切换模式前留下的 DNS 缓存,都可能不走上面这条标准路径。
在受限 FakeIP 捕获模式下,一个没有落入捕获范围的真实 IP,不会因为核心里存在一条规则,就自动被送进去。
需要更完整的流量接管时,Real-IP 全量 TUN 或相应网关模式,承担的是另一种取舍。
Linux TProxy 网关也不能照搬“真实 IP 全部绕过核心”的桌面结论:LAN 业务流量可能仍被透明捕获,再由核心判断出口。
私网访问:能解析,不代表已经进了隧道
除了公网分流,Sempre 还需要处理 WireGuard 等私网连接器。
这里同样有两条独立链路。
例如:
git.internal.example
↓ 私网 DNS
10.20.0.8
DNS 成功了,能不能访问?
还不知道。
如果桌面 TUN 只捕获 FakeIP 网段,10.20.0.8 是真实私网地址,它不会自动进入核心。
所以私网访问至少要接通三件事:
flowchart TD
Name["git.internal.example"] --> DNSRule["命中私网 DNS 后缀"]
DNSRule --> PrivateDNS["核心经连接器访问私网 DNS"]
PrivateDNS --> IP["返回 10.20.0.8"]
IP --> Capture["私网 CIDR 纳入 TUN 捕获范围"]
Capture --> Route["核心路由匹配私网 CIDR"]
Route --> Connector["选择对应私网连接器"]
Connector --> Service["访问内网服务"]
Sempre 会把连接器配置中的私网 CIDR 同时用于系统捕获路由和核心业务路由。
一个设置,两处用途,但职责不同:
- 系统路由负责把连接送进核心。
- 核心规则负责把连接送进正确的隧道。
只配置私网 DNS 后缀,不等于配置了私网访问。
只添加核心域名规则,也不等于操作系统已经知道要把那个真实私网地址交给 TUN。
很多“明明解析正常,为什么还是不通”的问题,就出在这两步之间。
好用不只是能连上,还包括出问题时能改回来
回头看 Sempre 的开发过程,很多重要调整并不是什么新功能,而是在拆掉不合理的依赖。
保存配置,不能要求网络先正常
曾经遇到过一个非常典型的问题:
网络坏了,需要修改配置
→ 保存配置时要求联网拉订阅
→ 拉取失败,保存不了
→ 新配置无法启动
→ 网络继续坏着
这是一个闭环死锁。
因此 Sempre 把“保存”和“应用”拆开:
保存 Profile
→ 持久化本地配置
更新订阅 / 启动运行时
→ 获取来源
→ 编译配置
→ 核心校验
→ 暂存并应用
保存不以远程订阅可达为前提。
同样,修改配置不应该悄悄中断当前连接。待应用的变化需要能够查看,真正替换运行中的部署,也要有明确的动作和状态。
一键安装是减少初次使用的步骤,不是把后续所有危险操作都变成自动执行。
看见红色日志,也要知道它拒绝了什么
比如 DNS 日志里的:
HTTPS → reject
它很容易被理解成“HTTPS 网站被拦了”。
但这里的 HTTPS 是 DNS 记录类型,和 A、AAAA 并列:
| 类型 | 主要用途 |
|---|---|
| A | 查询 IPv4 地址 |
| AAAA | 查询 IPv6 地址 |
| HTTPS | 查询 HTTPS 服务的连接参数 |
HTTPS 记录可以携带协议和地址提示等信息,不是地址栏里的 https:// 本身。标准说明
Sempre 提供拒绝这类记录的策略,可以减少额外连接提示对预期 FakeIP 路径的干扰,但它有兼容性与功能上的代价,不能理解成“越多拒绝越安全”。
当前前置实现使用 NXDOMAIN 响应,还需要注意客户端的负缓存行为。
所以,一条 DNS 拒绝日志不能直接证明网站连接失败;看到域名返回了地址,也不能直接证明出口正确。
诊断应该能把这些阶段拆开看:
DNS 由谁回答?
→ 返回真实 IP 还是 FakeIP?
→ 连接有没有进入核心?
→ 命中了哪条规则?
→ 最终选择哪个出口?
→ TCP、TLS、HTTP 是否成功?
这比一个笼统的“节点可用”更接近真实网络。
最后:不是再增加一个需要研究的客户端
Sempre 想解决的,不是“代理工具还不够多”。
而是从安装到长期运行,中间仍然有太多需要人来拼接的部分:
- 订阅与核心之间的格式差异。
- 核心版本与配置语义之间的差异。
- 操作系统服务、DNS 与路由之间的差异。
- DNS 决策与实际连接路径之间的差异。
这些差异不会因为做了一个 GUI 就消失。
我的做法是把它们放回各自应该负责的位置:
安装器负责准备环境,转换器负责生成配置,管理服务负责生命周期,DNS 前置层负责提前判断解析路径,代理核心负责真正转发。
然后让这些部分配合起来。
普通用户看到的是一行命令、一个安装入口、一份订阅。
需要深入调整的人,仍然能看到规则、配置和实际运行链路。
我觉得这才是“一键”真正应该意味着的东西:不是把复杂度藏起来,而是把复杂度处理好。
项目:tinymins/sempre
官网与安装入口:sempre.run
完整安装包:GitHub Releases
版权属于:一名宅。
本文链接:https://zhaiyiming.com/archives/82.html
转载时须注明出处及本声明