From cc0db60cff4e4ba16c2bd11fadd198f5e28e6dfc Mon Sep 17 00:00:00 2001 From: Anton Adamson Date: Mon, 7 Sep 2026 12:52:29 +0600 Subject: [PATCH] docs: document the HAPP client routing profile and its override The subscription endpoint ships a client-side routing profile in the routing: response header, and the managed default tunnels everything. The override file that changes it was mentioned in one line of the architecture docs, without a schema, an example, or the difference from server-side bypass routing. Bypass routing only changes where the server sends a request; the client still tunnels it, so a Russian site keeps seeing the VPS address. On a node without a cascade the catch-all outbound is already direct, so bypass cannot help there. Split routing is only reachable through the client profile, which made this the most common thing operators had to rediscover on their own. Adds a Configuration section in en/ru/zh-CN covering the two environment variables, GlobalProxy semantics, a complete split-tunnel example, and the three mistakes that are easy to make: Telegram matching by IP rather than domain, LastUpdated needing to grow, and geo database URLs being per-subscriber. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0121QZ51XSKaiKcRrtoVQTDg --- docs/configuration.md | 69 +++++++++++++++++++++++++++++++++++++ docs/ru/configuration.md | 69 +++++++++++++++++++++++++++++++++++++ docs/zh-CN/configuration.md | 62 +++++++++++++++++++++++++++++++++ 3 files changed, 200 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index e0ec86b..9d3fc3d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -118,6 +118,75 @@ Default bundle groups: The menu is interactive: arrows move the selection, space toggles a group, Enter applies. +## HAPP client routing profile + +The subscription endpoint returns a client-side routing profile to HAPP in the `routing:` response +header. The managed default, `xrayebator-default`, sets `GlobalProxy: "true"`, so every destination +except private IPv4 ranges is tunnelled. + +That is a different knob from [Bypass routing](#bypass-routing). Bypass changes where **the server** +sends a request; the client still tunnels it, so the destination sees the VPS address. On a node +without a cascade the catch-all outbound is already `direct`, so bypass cannot change what a Russian +site sees. Keeping domestic traffic out of the tunnel is only possible in the client profile. + +### Override file + +| Variable | Default | Meaning | +|---|---|---| +| `HAPP_ROUTING_ENABLED` | `true` | Set to `false` to stop sending the `routing:` header | +| `HAPP_ROUTING_JSON_FILE` | `/usr/local/etc/xray/.happ_routing.json` | Operator override | + +If the override file exists and passes HAPP schema validation, it replaces the managed default for +every subscriber. Malformed JSON is ignored in favour of the default rather than broadcast. Restart +`xrayebator-sub.service` is not required: the file is read per request. + +Verdicts are evaluated per destination. `GlobalProxy: "false"` makes `direct` the default and sends +only `ProxySites` and `ProxyIp` through the tunnel; `GlobalProxy: "true"` inverts that and treats +`DirectSites` and `DirectIp` as the exceptions. + +### Split-tunnel example + +Domestic traffic stays on the mobile carrier, blocked services go through the VPS: + +```json +{ + "Name": "xrayebator-split", + "GlobalProxy": "false", + "RemoteDNSType": "DoH", + "RemoteDNSDomain": "https://cloudflare-dns.com/dns-query", + "RemoteDNSIP": "1.1.1.1", + "DomesticDNSType": "DoU", + "DomesticDNSDomain": "", + "DomesticDNSIP": "77.88.8.8", + "Geoipurl": "https://example.com/geo/geoip.dat", + "Geositeurl": "https://example.com/geo/geosite.dat", + "LastUpdated": "1788700000", + "DnsHosts": { "cloudflare-dns.com": "1.1.1.1" }, + "DirectSites": ["domain:ru", "domain:xn--p1ai"], + "DirectIp": ["geoip:private", "geoip:ru"], + "ProxySites": ["domain:google.com", "domain:youtube.com", "domain:instagram.com"], + "ProxyIp": ["149.154.160.0/20", "91.108.4.0/22", "91.105.192.0/23"], + "BlockSites": [], + "BlockIp": [], + "DomainStrategy": "IPIfNonMatch", + "FakeDNS": "false" +} +``` + +Three things are easy to get wrong: + +- **Telegram travels to IP addresses, not domains.** Domain rules never match it. Take the ranges + from instead of writing them from memory; the list + changes, and a missing range silently degrades media downloads while chat still works. +- **`LastUpdated` must grow.** HAPP re-imports a profile only when the value is higher than the one + it already stored. +- **Geo databases must be reachable by the client.** The managed default points at + `/sub//geoip.dat`, which is per-subscriber. An operator-wide override cannot embed one + subscriber's token, so host the databases somewhere every client can fetch them. + +While a client downloads new geo databases the previous profile keeps running, so a failed download +leaves routing unchanged rather than broken. + ## Cascade and upstream nodes The cascade is a server-side outbound and routing mode, not a new client profile. The client keeps diff --git a/docs/ru/configuration.md b/docs/ru/configuration.md index 5b69102..5fd3938 100644 --- a/docs/ru/configuration.md +++ b/docs/ru/configuration.md @@ -119,6 +119,75 @@ Bypass добавляет правила в Xray routing, чтобы выбра Меню интерактивное: стрелки двигают выбор, пробел включает и выключает группу, Enter применяет. +## Профиль маршрутизации клиента HAPP + +Эндпоинт подписки возвращает клиенту профиль маршрутизации в заголовке ответа `routing:`. +Управляемый профиль по умолчанию, `xrayebator-default`, задаёт `GlobalProxy: "true"`, поэтому в +туннель уходит всё, кроме приватных диапазонов IPv4. + +Это не то же самое, что [Bypass routing](#bypass-routing). Bypass меняет то, куда запрос отправляет +**сервер**; клиент всё равно заворачивает его в туннель, и адресат видит адрес VPS. На ноде без +каскада общий outbound и так `direct`, поэтому bypass не меняет то, что видит российский сайт. +Не пускать домашний трафик в туннель можно только профилем клиента. + +### Файл переопределения + +| Переменная | По умолчанию | Значение | +|---|---|---| +| `HAPP_ROUTING_ENABLED` | `true` | `false` полностью отключает заголовок `routing:` | +| `HAPP_ROUTING_JSON_FILE` | `/usr/local/etc/xray/.happ_routing.json` | Переопределение оператором | + +Если файл существует и проходит проверку схемы HAPP, он заменяет управляемый профиль для всех +подписчиков. Некорректный JSON игнорируется в пользу профиля по умолчанию, а не рассылается +клиентам. Перезапуск `xrayebator-sub.service` не нужен: файл читается на каждый запрос. + +Вердикт выносится для каждого адресата. При `GlobalProxy: "false"` поведением по умолчанию +становится `direct`, а в туннель уходят только `ProxySites` и `ProxyIp`. При `GlobalProxy: "true"` +всё наоборот, и исключениями работают `DirectSites` и `DirectIp`. + +### Пример раздельной маршрутизации + +Домашний трафик остаётся у оператора, заблокированные сервисы идут через VPS: + +```json +{ + "Name": "xrayebator-split", + "GlobalProxy": "false", + "RemoteDNSType": "DoH", + "RemoteDNSDomain": "https://cloudflare-dns.com/dns-query", + "RemoteDNSIP": "1.1.1.1", + "DomesticDNSType": "DoU", + "DomesticDNSDomain": "", + "DomesticDNSIP": "77.88.8.8", + "Geoipurl": "https://example.com/geo/geoip.dat", + "Geositeurl": "https://example.com/geo/geosite.dat", + "LastUpdated": "1788700000", + "DnsHosts": { "cloudflare-dns.com": "1.1.1.1" }, + "DirectSites": ["domain:ru", "domain:xn--p1ai"], + "DirectIp": ["geoip:private", "geoip:ru"], + "ProxySites": ["domain:google.com", "domain:youtube.com", "domain:instagram.com"], + "ProxyIp": ["149.154.160.0/20", "91.108.4.0/22", "91.105.192.0/23"], + "BlockSites": [], + "BlockIp": [], + "DomainStrategy": "IPIfNonMatch", + "FakeDNS": "false" +} +``` + +Три места, где легко ошибиться: + +- **Telegram ходит по IP-адресам, а не по доменам.** Доменные правила его не ловят. Диапазоны нужно + брать со страницы , а не по памяти: список меняется, + а пропущенный диапазон тихо портит загрузку медиа, пока переписка продолжает работать. +- **`LastUpdated` обязан расти.** HAPP переимпортирует профиль только когда значение больше уже + сохранённого. +- **Geo-базы должны быть доступны клиенту.** Управляемый профиль ссылается на + `/sub//geoip.dat`, а этот путь привязан к конкретному подписчику. Общее переопределение не + может содержать чужой токен, поэтому базы нужно выложить туда, откуда их скачает любой клиент. + +Пока клиент скачивает новые geo-базы, продолжает работать предыдущий профиль, поэтому неудачная +загрузка оставляет маршрутизацию прежней, а не ломает её. + ## Каскад и upstream-ноды Каскад — серверный режим outbound и routing, а не новый профиль клиента. Клиент продолжает diff --git a/docs/zh-CN/configuration.md b/docs/zh-CN/configuration.md index c4c8bd3..ef8ac6e 100644 --- a/docs/zh-CN/configuration.md +++ b/docs/zh-CN/configuration.md @@ -110,6 +110,68 @@ UFW 由安装脚本自行管理:安装 `ufw` 包,若 UFW 未启用则执行 菜单是交互式的:方向键移动光标,空格切换分组,回车应用。 +## HAPP 客户端路由配置 + +订阅端点通过响应头 `routing:` 向 HAPP 下发客户端路由配置。托管的默认配置 `xrayebator-default` +设置了 `GlobalProxy: "true"`,因此除私有 IPv4 段之外的所有目标都会走隧道。 + +这与[分流路由](#分流路由)不是同一个开关。分流路由改变的是**服务端**把请求发往何处;客户端仍然会把它 +送进隧道,目标站点看到的依旧是 VPS 的地址。在没有级联的节点上,兜底 outbound 本来就是 `direct`, +所以分流路由无法改变俄罗斯站点看到的结果。只有客户端配置才能让本国流量不进入隧道。 + +### 覆盖文件 + +| 变量 | 默认值 | 含义 | +|---|---|---| +| `HAPP_ROUTING_ENABLED` | `true` | 设为 `false` 则不再下发 `routing:` 响应头 | +| `HAPP_ROUTING_JSON_FILE` | `/usr/local/etc/xray/.happ_routing.json` | 运维方的覆盖文件 | + +如果覆盖文件存在并通过 HAPP 结构校验,它会对所有订阅者取代托管的默认配置。格式错误的 JSON 会被忽略 +并回退到默认配置,而不会下发给客户端。无需重启 `xrayebator-sub.service`:该文件在每次请求时读取。 + +判定是按目标逐个进行的。`GlobalProxy: "false"` 时默认行为是 `direct`,只有 `ProxySites` 和 +`ProxyIp` 走隧道;`GlobalProxy: "true"` 则相反,`DirectSites` 和 `DirectIp` 成为例外。 + +### 分流示例 + +本国流量留在运营商网络,被封锁的服务经由 VPS: + +```json +{ + "Name": "xrayebator-split", + "GlobalProxy": "false", + "RemoteDNSType": "DoH", + "RemoteDNSDomain": "https://cloudflare-dns.com/dns-query", + "RemoteDNSIP": "1.1.1.1", + "DomesticDNSType": "DoU", + "DomesticDNSDomain": "", + "DomesticDNSIP": "77.88.8.8", + "Geoipurl": "https://example.com/geo/geoip.dat", + "Geositeurl": "https://example.com/geo/geosite.dat", + "LastUpdated": "1788700000", + "DnsHosts": { "cloudflare-dns.com": "1.1.1.1" }, + "DirectSites": ["domain:ru", "domain:xn--p1ai"], + "DirectIp": ["geoip:private", "geoip:ru"], + "ProxySites": ["domain:google.com", "domain:youtube.com", "domain:instagram.com"], + "ProxyIp": ["149.154.160.0/20", "91.108.4.0/22", "91.105.192.0/23"], + "BlockSites": [], + "BlockIp": [], + "DomainStrategy": "IPIfNonMatch", + "FakeDNS": "false" +} +``` + +三个容易出错的地方: + +- **Telegram 按 IP 地址通信,而不是域名。** 域名规则匹配不到它。请从 + 获取网段,不要凭记忆填写:该列表会变化,遗漏一个 + 网段会悄悄拖慢媒体下载,而聊天看起来仍然正常。 +- **`LastUpdated` 必须递增。** 只有当该值大于已保存的值时,HAPP 才会重新导入配置。 +- **geo 数据库必须能被客户端访问。** 托管的默认配置指向 `/sub//geoip.dat`,该路径与具体订阅者 + 绑定。面向全体的覆盖文件无法内嵌某一个订阅者的 token,因此请把数据库放在任何客户端都能下载的位置。 + +客户端下载新的 geo 数据库期间,先前的配置继续生效,因此下载失败只会保持路由不变,而不会使其损坏。 + ## 级联与上游节点 级联是服务端的出站与路由模式,而不是新的客户端配置档。客户端仍然连接当前 VPS: