docs: add SIP-008 subscription provider guide

This commit is contained in:
Qiu Yuzhou
2026-10-22 17:10:35 +08:00
parent 0028254b40
commit ff93e6eeeb
3 changed files with 264 additions and 0 deletions
+2
View File
@@ -4,6 +4,8 @@
## 订阅资料
面向订阅服务提供方的字段格式、嵌套分组扩展和响应要求见 [SIP-008 订阅扩展说明](subscription-provider-extension.zh-CN.md)。
订阅可在 SIP-008 JSON 根级提供 `bytes_used`、`bytes_remaining`(非负整数,最大为 UInt64),以及自定义的 `expires_at`(服务到期时间)、`traffic_reset_at`(流量重置时间)。日期值使用 RFC 3339 字符串,省略或 `null` 表示未提供;无效资料仅忽略该项,不影响有效服务器或分组资料。
成功刷新完整替换白名单订阅资料,省略或无效的字段清除旧值;失败或取消保留最后成功资料与独立成功时间。资料和目录记录同文档原子保存,不另存原始响应或含密码的服务器明文快照;密码和插件参数沿用 Keychain 存储。旧目录文件缺少成功时间时不猜测。
+131
View File
@@ -0,0 +1,131 @@
# SIP-008 Subscription Extensions: Provider Guide
This guide describes the SIP-008 fields and private extensions currently supported by ShadowsocksX-NG. It is intended for subscription providers generating compatible responses. It reflects the current implementation.
[简体中文](subscription-provider-extension.zh-CN.md)
`x_shadowsocksx_ng`, `expires_at`, and `traffic_reset_at` are private extensions recognized by ShadowsocksX-NG; they are not SIP-008 standard fields. Ordinary SIP-008 clients may not recognize them, and clients without extension support should still read the servers from the standard `servers` array. SIP-008 allows custom fields but does not guarantee that all clients support them. See the [SIP-008 Online Configuration Delivery specification](https://shadowsocks.org/doc/sip008.html).
## Response delivery
The subscription endpoint must meet these requirements:
- Use HTTPS. The client validates the system trust chain, does not accept plain HTTP, and does not relax certificate or TLS validation. HTTPS-to-HTTPS redirects are allowed; redirects to non-HTTPS are rejected.
- Return HTTP status `200` for a successful response.
- Send `Content-Type: application/json; charset=utf-8`. The client ignores case and whitespace, but the service should send this exact form. A missing header or a different media type or character set is rejected.
- Return one valid JSON object, not JSON5, Base64, or a list of `ss://` links.
SIP-008 subscription URLs often contain access tokens. Use unpredictable tokens, and avoid recording complete subscription URLs in access logs, error tracking, or analytics systems. The subscription body contains server passwords. HTTPS protects the response in transit; the extension fields do not encrypt the body.
## SIP-008 v1 base document
The root object must contain `version: 1` and a `servers` array. An empty array is valid. SIP-008 requires each server entry to include `id`, `remarks`, `server`, `server_port`, `password`, and `method`; include `plugin` and `plugin_opts` when using a plugin. Each entry must remain a complete Shadowsocks server record. Do not put the only copy of a server configuration in the grouping extension.
| Field | Provider requirement and recommendation | Client behavior |
| --- | --- | --- |
| `version` | Must be the integer `1`. | A missing or different version rejects the entire document. |
| `servers` | Must be an array. Each entry uses the SIP-008 server fields listed below. | A missing value or a non-array rejects the entire document. An empty array is valid. |
| `servers[].id` | Required by SIP-008. Give each server a unique, canonical UUID and preserve it across refreshes for the same logical server. The grouping extension uses this field to reference servers. | The current parser tolerates a missing `id`, but any supplied value must be a valid UUID. Duplicate IDs reject the entire document. Entries without an ID cannot be referenced by the extension tree and follow the flat compatibility path. |
| `servers[].remarks` | Required by SIP-008. Provide a string to use as the server's display name. | The current parser tolerates omission; the name is empty when omitted. |
| `servers[].server` | Must be a non-empty string. | A missing or empty value rejects the entire document. |
| `servers[].server_port` | Must be an integer from `1` through `65535`. | A missing, non-integer, or out-of-range value rejects the entire document. |
| `servers[].password` | Must be a non-empty string. | A missing or empty value rejects the entire document. |
| `servers[].method` | Must be a non-empty string. Provide a Shadowsocks cipher supported by the client. | A missing or empty value rejects the entire document. The proxy runtime determines whether the method is available. |
| `servers[].plugin`, `servers[].plugin_opts` | Omit or leave both empty when no plugin is used. When using a plugin, provide its name and corresponding options as specified by SIP-008. | The client passes the values into the server configuration; subscription parsing does not validate plugin option semantics. |
A server ID represents logical identity, not its array position. Preserve the ID when renaming a server, changing its address or port, or updating its credentials if it remains the same logical server. Assign a new ID to a new logical server. Use the canonical hyphenated UUID form, and keep the exact same string in the server entry and extension references.
## Root-level subscription information
The following traffic fields are defined by SIP-008. Providers should follow SIP-008's rules for these fields. The client displays only values that match the formats below.
| Field | Provider format | Meaning |
| --- | --- | --- |
| `bytes_used` | A non-negative JSON integer from `0` through `18446744073709551615`. | Number of bytes used by the user. Do not send a string, negative number, decimal, or exponent form. |
| `bytes_remaining` | A non-negative JSON integer in the same range. Omit it when there is no data limit; include `bytes_used` as well when a limit is enforced. | Number of bytes remaining. Do not use a private field to repeat the same traffic meaning. |
| `expires_at` | An RFC 3339 date-time string, such as `2027-01-01T00:00:00Z` or `2027-01-01T08:00:00+08:00`. | A ShadowsocksX-NG private field for the service expiration time. |
| `traffic_reset_at` | An RFC 3339 date-time string. | A ShadowsocksX-NG private field for the traffic reset time. |
An omitted date field or `null` means no value is provided. The client parses these fields independently. An invalid value is ignored without discarding valid servers, groups, or other valid information. The client reads this information from the JSON body; it does not parse response headers such as `Subscription-Userinfo`.
## Nested-group extension: `x_shadowsocksx_ng`
To provide nested groups to ShadowsocksX-NG, add the following private extension to the root object. Keep every server in `servers`; the extension carries only group names, hierarchy, order, and server references.
```json
{
"version": 1,
"servers": [
{
"id": "3c5e2af7-9f2c-4f42-bd1c-9e143d5f2d11",
"remarks": "Tokyo 1",
"server": "tokyo.example.com",
"server_port": 443,
"password": "example-secret-1",
"method": "chacha20-ietf-poly1305"
},
{
"id": "b9f08d65-8be7-4b09-bf94-4f2bb53e4199",
"remarks": "Osaka 1",
"server": "osaka.example.com",
"server_port": 443,
"password": "example-secret-2",
"method": "chacha20-ietf-poly1305"
}
],
"bytes_used": 1073741824,
"bytes_remaining": 9663676416,
"expires_at": "2027-01-01T00:00:00Z",
"traffic_reset_at": "2026-11-01T00:00:00Z",
"x_shadowsocksx_ng": {
"schema_version": 1,
"root_group_id": "f6ad0a41-2a1d-4db8-9da0-b6f4f3dc889d",
"groups": [
{
"id": "f6ad0a41-2a1d-4db8-9da0-b6f4f3dc889d",
"name": "My subscription",
"children": [
{ "type": "group", "id": "a1e2b11c-2f53-4e39-bc36-95af6dbb5f1c" },
{ "type": "server", "id": "3c5e2af7-9f2c-4f42-bd1c-9e143d5f2d11" }
]
},
{
"id": "a1e2b11c-2f53-4e39-bc36-95af6dbb5f1c",
"name": "Japan",
"children": [
{ "type": "server", "id": "b9f08d65-8be7-4b09-bf94-4f2bb53e4199" }
]
}
]
}
}
```
| Field | Meaning and constraints |
| --- | --- |
| `x_shadowsocksx_ng` | ShadowsocksX-NG private extension object. This field name is not an official SIP-008 field. |
| `schema_version` | The currently supported extension version must be the integer `1`. |
| `root_group_id` | A non-empty string that must match the `id` of the root group in `groups`. |
| `groups` | An array of group objects. Each `id` must be non-empty and unique; a UUID that remains stable across refreshes is recommended. A group ID must not conflict with a server ID. |
| `groups[].name` | Group display name. Providing it is recommended; the client displays an empty name if it is omitted. |
| `groups[].children` | An ordered array. Its order is the display order in the client. Each entry must be `{ "type": "group", "id": "group ID" }` or `{ "type": "server", "id": "server UUID" }`. |
The provider should make the extension a single-parent, acyclic tree rooted at `root_group_id`: each group has at most one parent, and every group and server reference resolves to an object. Reference each server at most once, and place every group intended for display in the tree reachable from the root. Group and server IDs are stable identities; do not use names, array positions, or mutable configuration values as IDs.
The client handles the extension as follows:
- If the extension version is missing or unsupported, the root group is missing, IDs conflict, a reference is invalid, a child type is unknown, a cycle exists, or a group has multiple parents, the client ignores the entire extension and uses the flat standard `servers` list. Valid servers are not lost because of an invalid extension.
- A server in `servers` that is not referenced by a valid tree is still shown at the end of the root group, in the order it appears in the `servers` array. If the extension is invalid or incomplete, servers may still appear, but their grouping is not preserved.
- Groups unreachable from the root are not shown. Do not rely on client behavior for unreachable groups or duplicate server references; providers should produce a valid tree without orphan groups or repeated server references.
- A record without a server `id` cannot be referenced by the tree. Provide a stable ID for every server to preserve grouping.
## Provider release checklist
- Use HTTPS and return HTTP `200` with `Content-Type: application/json; charset=utf-8`.
- Return valid JSON with root-level `version: 1` and complete server records in `servers`.
- Give every server a unique, stable UUID; use the exact same server ID in `servers` and group references.
- If using the extension, set `schema_version` to `1`, include the root group, resolve every reference, and keep the tree ordered, single-parent, and acyclic.
- Use the SIP-008 standard fields for traffic information; use RFC 3339 strings for expiration and reset dates.
- Verify that a standard SIP-008 client that does not support `x_shadowsocksx_ng` still receives all servers in the flat `servers` list.
For details about how the application presents subscription fields and handles refreshes, see [“Subscription information” in the configuration guide](configuration.md#订阅资料).
@@ -0,0 +1,131 @@
# SIP-008 订阅扩展:供应方说明
本文说明 ShadowsocksX-NG 当前支持的 SIP-008 订阅字段和私有扩展,供订阅服务提供方生成兼容响应时参考。内容以当前实现为准。
[English](subscription-provider-extension.md)
`x_shadowsocksx_ng` 和 `expires_at`、`traffic_reset_at` 是 ShadowsocksX-NG 识别的私有扩展,并非 SIP-008 标准字段。普通 SIP-008 客户端不一定识别这些字段;没有扩展支持的客户端仍应从标准 `servers` 数组读取服务器。SIP-008 允许自定义字段,但不保证所有客户端支持。详见 [SIP-008 Online Configuration Delivery](https://shadowsocks.org/doc/sip008.html)。
## 响应传输
订阅端点必须满足以下条件:
- 使用 HTTPS。客户端验证系统信任链,不接受明文 HTTP,也不放宽证书或 TLS 校验;HTTPS 到 HTTPS 的重定向可以跟随,降级到非 HTTPS 会被拒绝。
- 成功响应状态码为 `200`。
- 响应头使用 `Content-Type: application/json; charset=utf-8`。客户端不区分大小写并忽略空白,但服务端应按此格式发送;缺少该头或使用其他类型、字符集都会被拒绝。
- 响应正文是一个合法的 JSON 对象,不是 JSON5、Base64 或 `ss://` 链接列表。
SIP-008 订阅 URL 通常包含访问令牌。请使用不可猜测的令牌,并避免在服务端访问日志、错误追踪或分析系统中记录完整订阅 URL。订阅正文含服务器密码;HTTPS 负责传输保护,扩展字段本身不加密正文。
## SIP-008 v1 基础对象
根对象必须包含 `version: 1` 和 `servers` 数组。空数组有效。服务器项按 SIP-008 应提供 `id`、`remarks`、`server`、`server_port`、`password` 和 `method`;使用插件时再提供 `plugin`、`plugin_opts`。每个服务器项仍须是完整的 Shadowsocks 服务器记录;不能只在分组扩展里发送服务器配置。
| 字段 | 供应方要求与建议 | 客户端处理 |
| --- | --- | --- |
| `version` | 必须是整数 `1`。 | 缺失或其他版本会拒绝整份文档。 |
| `servers` | 必须是数组;每项使用下表所列的 SIP-008 服务器字段。 | 缺失或不是数组会拒绝整份文档。空数组有效。 |
| `servers[].id` | SIP-008 服务器项必需字段。每台服务器提供唯一、规范格式的 UUID,并在同一逻辑服务器的后续刷新中保持不变。分组扩展依赖此字段引用服务器。 | 当前解析器容忍省略 `id`,但提供后必须是有效 UUID;重复 ID 会拒绝整份文档。无 ID 项不能被扩展树引用,会按扁平兼容路径处理。 |
| `servers[].remarks` | SIP-008 服务器项必需字段,提供服务器显示名称字符串。 | 当前解析器容忍省略;缺省时名称为空。 |
| `servers[].server` | 必须是非空字符串。 | 缺失或为空会拒绝整份文档。 |
| `servers[].server_port` | 必须是 `1` 到 `65535` 范围内的整数。 | 缺失、非整数或越界会拒绝整份文档。 |
| `servers[].password` | 必须是非空字符串。 | 缺失或为空会拒绝整份文档。 |
| `servers[].method` | 必须是非空字符串。请提供客户端可用的 Shadowsocks 加密方法。 | 缺失或为空会拒绝整份文档;方法是否可用由代理运行时决定。 |
| `servers[].plugin`、`servers[].plugin_opts` | 未使用插件时两项省略或留空;使用插件时按 SIP-008 提供插件名及对应选项。 | 客户端将值带入服务器配置,不会在订阅解析时校验插件选项语义。 |
服务器 ID 表示逻辑身份,不是数组位置。改名、改地址、改端口或更新凭据时,如果仍是同一逻辑服务器,应保留原 ID;新增一台逻辑服务器时分配新 ID。UUID 应采用连字符分隔的规范形式,并在正文和扩展引用中保持完全相同的字符串。
## 根级订阅资料
以下流量字段来自 SIP-008。供应方应遵守 SIP-008 对数据量字段的约定;客户端仅把符合下表格式的值显示为订阅资料。
| 字段 | 供应方格式 | 说明 |
| --- | --- | --- |
| `bytes_used` | JSON 非负整数,范围为 `0` 至 `18446744073709551615`。 | 用户已使用的字节数。不要发送字符串、负数、小数或指数形式。 |
| `bytes_remaining` | JSON 非负整数,范围同上。没有流量上限时省略;有限额时同时提供 `bytes_used`。 | 用户剩余可用字节数。不要用私有字段重复表达同一流量含义。 |
| `expires_at` | RFC 3339 日期时间字符串,例如 `2027-01-01T00:00:00Z` 或 `2027-01-01T08:00:00+08:00`。 | ShadowsocksX-NG 私有字段,表示服务到期时间。 |
| `traffic_reset_at` | RFC 3339 日期时间字符串。 | ShadowsocksX-NG 私有字段,表示流量重置时间。 |
日期字段缺失或为 `null` 表示未提供。客户端逐项解析这些资料;某一项格式错误只会忽略该项,不会丢弃有效服务器、分组或其他有效资料。客户端从 JSON 正文读取资料,不解析 HTTP `Subscription-Userinfo` 等响应头。
## 嵌套分组扩展 `x_shadowsocksx_ng`
要向 ShadowsocksX-NG 提供嵌套分组,可在根对象中增加以下私有扩展。`servers` 仍须包含全部服务器;扩展只表达分组名称、层级、顺序和服务器引用。
```json
{
"version": 1,
"servers": [
{
"id": "3c5e2af7-9f2c-4f42-bd1c-9e143d5f2d11",
"remarks": "Tokyo 1",
"server": "tokyo.example.com",
"server_port": 443,
"password": "example-secret-1",
"method": "chacha20-ietf-poly1305"
},
{
"id": "b9f08d65-8be7-4b09-bf94-4f2bb53e4199",
"remarks": "Osaka 1",
"server": "osaka.example.com",
"server_port": 443,
"password": "example-secret-2",
"method": "chacha20-ietf-poly1305"
}
],
"bytes_used": 1073741824,
"bytes_remaining": 9663676416,
"expires_at": "2027-01-01T00:00:00Z",
"traffic_reset_at": "2026-11-01T00:00:00Z",
"x_shadowsocksx_ng": {
"schema_version": 1,
"root_group_id": "f6ad0a41-2a1d-4db8-9da0-b6f4f3dc889d",
"groups": [
{
"id": "f6ad0a41-2a1d-4db8-9da0-b6f4f3dc889d",
"name": "我的订阅",
"children": [
{ "type": "group", "id": "a1e2b11c-2f53-4e39-bc36-95af6dbb5f1c" },
{ "type": "server", "id": "3c5e2af7-9f2c-4f42-bd1c-9e143d5f2d11" }
]
},
{
"id": "a1e2b11c-2f53-4e39-bc36-95af6dbb5f1c",
"name": "日本",
"children": [
{ "type": "server", "id": "b9f08d65-8be7-4b09-bf94-4f2bb53e4199" }
]
}
]
}
}
```
| 字段 | 含义和约束 |
| --- | --- |
| `x_shadowsocksx_ng` | ShadowsocksX-NG 私有扩展对象。该字段名不是 SIP-008 官方标准字段。 |
| `schema_version` | 当前支持的扩展版本必须是整数 `1`。 |
| `root_group_id` | 非空字符串,必须对应 `groups` 中根分组的 `id`。 |
| `groups` | 分组对象数组。每个 `id` 必须非空且唯一;建议使用不随刷新变化的 UUID。分组 ID 不得与服务器 ID 冲突。 |
| `groups[].name` | 分组显示名称。建议始终提供;缺省时客户端显示空名称。 |
| `groups[].children` | 有序数组,数组顺序就是客户端显示顺序。每项必须是 `{ "type": "group", "id": "分组 ID" }` 或 `{ "type": "server", "id": "服务器 UUID" }`。 |
供应方应确保扩展构成从 `root_group_id` 出发的单父、无环树:每个分组最多有一个父分组,所有分组和服务器引用都能解析到相应对象。每个服务器最多引用一次;请将每个预期展示的分组放在根分组可到达的树中。分组与服务器 ID 都是稳定身份,不要把名称、数组下标或可变配置内容当作 ID。
客户端的扩展处理行为如下:
- 扩展版本缺失或不支持、根分组不存在、ID 冲突、引用无效、子项类型未知、出现环或分组被多个父分组引用时,整个扩展会被忽略,客户端改用标准 `servers` 扁平列表;有效服务器不会因此丢失。
- 未被有效树引用但存在于 `servers` 的服务器仍会显示在根分组末尾,顺序沿用其在 `servers` 数组中的顺序。因此扩展失效或不完整时,服务器仍可能显示,但分组关系不会保留。
- 树中不可从根到达的分组不会显示。请勿依赖客户端对不可达分组或重复服务器引用的处理;供应方应输出没有孤立分组、没有重复服务器引用的有效树。
- 缺少服务器 `id` 的记录无法被树引用;为保证分组准确,请给所有服务器提供稳定 ID。
## 提供方发布前检查
- 使用 HTTPS,返回 HTTP `200` 和 `Content-Type: application/json; charset=utf-8`。
- 提供合法 JSON,根级 `version` 为 `1`,`servers` 包含完整服务器记录。
- 为每台服务器提供唯一且稳定的 UUID;服务器 ID 在 `servers` 和分组引用中逐字一致。
- 若使用扩展,`schema_version` 为 `1`,根分组存在,所有引用有效,分组树有序、单父、无环。
- 流量字段使用 SIP-008 标准字段;到期与重置日期使用本文规定的 RFC 3339 字符串。
- 以不支持 `x_shadowsocksx_ng` 的普通 SIP-008 客户端验证时,扁平 `servers` 列表仍包含全部服务器。
订阅字段的应用内呈现与刷新行为见[配置文档中的“订阅资料”](configuration.md#订阅资料)。