Repository navigation
Expand file tree
/
Copy pathopenapi.yaml
More file actions
634 lines (631 loc) · 23.3 KB
/
Copy pathopenapi.yaml
File metadata and controls
634 lines (631 loc) · 23.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
openapi: 3.0.3
info:
title: VNet-SSR ↔ 面板 WebAPI 契约
# 这里是契约版本,不是发布号:只在接口本身变动时动它(向后兼容的新增加小版本,破坏互通的加主版本)。
# 发布号看 release/version.json 的 latest,那个号决定新装与升级取哪个包。
version: 2.3.0
description: |
本后端与账号面板之间的双向接口。两个方向各有服务端:
* **面板侧**(`servers: panel`):节点拉配置/用户列表,并上报流量、在线、心跳、审计命中。
本后端按 `ssr/v1` 前缀拼 URL。
* **节点侧**(`servers: node`):面板反向推送用户与节点配置,监听 `node.push_port`。
鉴权:面板侧要求请求头 `key`(节点授权 key)与 `timestamp`(unix 秒,与面板时差 ≥300 秒即拒);
节点侧要求请求头 `secret`(面板下发的节点 secret)。
单位约定:流量与限速一律 **字节**、字节/秒;时间戳一律 unix 秒。
面板数据库里 `speed_limit` 存字节/秒,但其展示 cast 读出来是 Mbps —— 下发给本后端必须是字节/秒,
出现小数(如 `12.5`)即为错误:`UserInfo.speed_limit` 是 `uint64`,绑定失败会让整条请求被拒。
servers:
- url: https://panel.example.com/api
description: "面板宿主(面板给节点用的基址),路径带协议前缀"
- url: http://NODE_HOST:PUSH_PORT/api
description: "本后端宿主(面板逐个地址推送到 push_port)"
tags:
- name: panel
description: 面板实现、本后端调用
- name: node
description: 本后端实现、面板调用。改用户表的推送落地后,本后端会作废手里那份 userList 基线(ETAG),
下一轮周期同步因此必然重新拉一次全量。
paths:
/ssr/v1/node/{node}:
get:
tags: [panel]
summary: 取节点自身配置
operationId: getNodeInfo
parameters:
- $ref: '#/components/parameters/NodePathId'
- $ref: '#/components/parameters/KeyHeader'
- $ref: '#/components/parameters/TimestampHeader'
- $ref: '#/components/parameters/IfNoneMatch'
responses:
'200':
description: 节点配置
headers:
ETAG:
description: sha1(json_encode(data)),下一轮可带 If-None-Match 命中 304
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data:
$ref: '#/components/schemas/NodeInfo'
'304':
description: 内容未变(响应体为空)
'400':
$ref: '#/components/responses/ParameterError'
'401':
$ref: '#/components/responses/ClockSkew'
/ssr/v1/userList/{node}:
get:
tags: [panel]
summary: 取该节点可用的用户集合
description: |
只含面板认为有效的账号(`status <> -1 且 enable = 1`,并按节点等级/用户组过滤),按 `id` 升序。
本后端在启动、reload、以及每 5 分钟的周期同步时拉取;日常增删靠面板推送。
推送落地会作废本后端手里上一份 ETAG,所以下一轮同步必然带不上 `If-None-Match`、拿到 200 全量并重新记基线。
operationId: getUserList
parameters:
- $ref: '#/components/parameters/NodePathId'
- $ref: '#/components/parameters/KeyHeader'
- $ref: '#/components/parameters/TimestampHeader'
- $ref: '#/components/parameters/IfNoneMatch'
responses:
'200':
description: 用户数组;空数组表示「该节点当前无人可用」,与拉取失败必须区分
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/UserInfo'
'304':
description: 内容未变
/ssr/v1/userTraffic/{node}:
post:
tags: [panel]
summary: 上报用户流量增量
description: |
面板按 `node.traffic_rate` 倍率乘算后,在数据库侧 `u = u + …, d = d + …` 累加(不能先读后写)。
失效 uid 会被跳过并记日志,不影响同批其他账号。本后端在发送失败时把**整批**留在待发队列,
下一轮原样重发,绝不因发送失败丢账。
重投会带来重复累加的窗口(面板已入账但响应丢失),所以每批带一个 `report_id`:
同一批内各行相同,重发不变,新批次换新值。面板按 `(node_id, report_id)` **整批**认领即闭环
(这批的写入是单事务全有或全无,「记了一半」的中间态不存在,所以不必按 uid 逐个认领);
未认领前该字段只是一个被忽略的多余键,不影响现有实现。
单行增量不足 50 KiB 时本后端先不发,留在本地继续累加,滞留满 10 分钟强制上报;
因此 `upload`/`download` 可能是跨几分钟的累计值,面板照原样累加即可。
operationId: reportUserTraffic
parameters:
- $ref: '#/components/parameters/NodePathId'
- $ref: '#/components/parameters/KeyHeader'
- $ref: '#/components/parameters/TimestampHeader'
requestBody:
required: true
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/UserTraffic'
responses:
'200':
$ref: '#/components/responses/Ack'
'400':
$ref: '#/components/responses/ParameterError'
/ssr/v1/nodeOnline/{node}:
post:
tags: [panel]
summary: 上报在线账号与其出口地址
description: |
同一账号在一轮里连过多个出口时,`ip` 用逗号拼在一个字符串里(面板的 `node_online_ip.ip` 列注释即为此约定)。
上报后本后端清空本地在线表,所以面板必须整批收下、只跳过失效账号——逐行存在性校验会让一整个分钟的在线数据丢失。
operationId: reportNodeOnline
parameters:
- $ref: '#/components/parameters/NodePathId'
- $ref: '#/components/parameters/KeyHeader'
- $ref: '#/components/parameters/TimestampHeader'
requestBody:
required: true
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/NodeOnline'
responses:
'200':
$ref: '#/components/responses/Ack'
/ssr/v1/nodeStatus/{node}:
post:
tags: [panel]
summary: 上报节点心跳
operationId: reportNodeStatus
parameters:
- $ref: '#/components/parameters/NodePathId'
- $ref: '#/components/parameters/KeyHeader'
- $ref: '#/components/parameters/TimestampHeader'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NodeStatus'
responses:
'200':
$ref: '#/components/responses/Ack'
/ssr/v1/nodeRule/{node}:
get:
tags: [panel]
summary: 取审计规则
description: |
本后端只实现 `reg`/`domain`/`ip` 三类;`protocol` 与 `unknown` 会被忽略。
拉取时机:启动、整节点重载、以及面板推送 `POST /v2/rule/reload`(规则变更的唯一增量通道,无周期轮询)。
operationId: getNodeRule
parameters:
- $ref: '#/components/parameters/NodePathId'
- $ref: '#/components/parameters/KeyHeader'
- $ref: '#/components/parameters/TimestampHeader'
- $ref: '#/components/parameters/IfNoneMatch'
responses:
'200':
description: 规则集
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data:
$ref: '#/components/schemas/Rule'
/ssr/v1/trigger/{node}:
post:
tags: [panel]
summary: 上报命中审计规则的账号
description: |
reason 是被命中的判定对象:目标地址本身命中时是它;首包嗅探(HTTP Host / TLS SNI)命中时是嗅出的域名。
target/sniff_* 字段带完整现场(去向、嗅出的协议/域名/路径),面板落进 rule_log 独立列。
节点侧排队重投(at-least-once,面板不可达时不丢);面板按 (node,uid,rule,reason) 冷却窗口去重
(面板侧配置,默认 30 分钟),窗口内的重复上报只 ack 不二次落档。
只有业务码 400200(参数错误)算「这条载荷永远不会被接受」,队列丢掉这一条并继续投后面的;
HTTP 状态分不开永久与暂时(写库失败同样回 400),所以判据只取响应体的 `code`。
operationId: reportTrigger
parameters:
- $ref: '#/components/parameters/NodePathId'
- $ref: '#/components/parameters/KeyHeader'
- $ref: '#/components/parameters/TimestampHeader'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Trigger'
responses:
'200':
$ref: '#/components/responses/Ack'
/v2/user/add/list:
post:
tags: [node]
summary: 按 uid 落目标状态(幂等)
description: |
`enable=0` 摘掉账号;已存在且内容一致则不动;内容有差异则重建。重复推同一批不会多出一份,
所以面板可以安全重投。任一行失败不会回滚其余行(回滚等于把刚更新的账号删掉),
错误在响应里聚合返回。
operationId: applyUsers
security:
- secretHeader: []
requestBody:
required: true
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/UserInfo'
responses:
'200':
$ref: '#/components/responses/PushResult'
/v2/user/del/list:
post:
tags: [node]
summary: 批量摘除账号
operationId: deleteUsers
security:
- secretHeader: []
requestBody:
required: true
content:
application/json:
schema:
type: array
items:
type: integer
description: user id
responses:
'200':
$ref: '#/components/responses/PushResult'
/user/add:
post:
tags: [node]
summary: 落单个账号的目标状态
operationId: applyUser
security:
- secretHeader: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UserInfo'
responses:
'200':
$ref: '#/components/responses/PushResult'
/user/edit:
post:
tags: [node]
summary: 同上(保留给不消费 enable 的旧面板)
operationId: editUser
security:
- secretHeader: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UserInfo'
responses:
'200':
$ref: '#/components/responses/PushResult'
/user/del/{uid}:
post:
tags: [node]
summary: 摘除单个账号
operationId: deleteUser
security:
- secretHeader: []
parameters:
- name: uid
in: path
required: true
schema:
type: integer
responses:
'200':
$ref: '#/components/responses/PushResult'
/user/list:
get:
tags: [node]
summary: 节点当前的用户集合(对账的「实际集合」)
description: 裸数组、无信封;面板的 `vnet:reconcile` 与 `EditUser` 旧路径以此为准。
operationId: listUsers
security:
- secretHeader: []
responses:
'200':
description: 当前账号数组
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/UserInfo'
/v2/node/reload:
post:
tags: [node]
summary: 用新的节点配置重建监听
description: |
会断掉全部在线连接,因此面板不会自动重投。本后端**先拉用户列表再改配置**:
此刻面板不可达则原样保留当前服务并回错误,不会把已经在跑的端口打空。
operationId: reloadNode
security:
- secretHeader: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NodeInfo'
responses:
'200':
$ref: '#/components/responses/PushResult'
/v2/rule/reload:
post:
tags: [node]
summary: 重拉审计规则(轻量,不断连接)
description: |
面板改完规则/分组后按节点推送这一发,是规则的唯一增量通道(节点无周期轮询)。
拉取失败时保留旧规则集并回错误,面板队列按 $tries 重试;节点重启本来就会拉全量。
operationId: reloadRule
security:
- secretHeader: []
responses:
'200':
$ref: '#/components/responses/PushResult'
components:
securitySchemes:
secretHeader:
type: apiKey
in: header
name: secret
description: 面板下发的节点 secret
parameters:
NodePathId:
name: node
in: path
required: true
schema:
type: integer
description: 节点 ID
KeyHeader:
name: key
in: header
required: true
schema:
type: string
description: 面板发的节点授权 key
TimestampHeader:
name: timestamp
in: header
required: true
schema:
type: integer
description: unix 秒;与面板时差 ≥300 秒会被拒
IfNoneMatch:
name: If-None-Match
in: header
required: false
schema:
type: string
description: 上一轮拿到的 ETAG;命中时面板返回 304 空体
responses:
Ack:
description: 面板收下
content:
application/json:
schema:
$ref: '#/components/schemas/Envelope'
ParameterError:
description: 形状校验失败;HTTP 400
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Envelope'
- type: object
properties:
data:
type: array
items:
type: string
description: 完整错误列表(不是只取首条)
ClockSkew:
description: 时钟不同步或超时;HTTP 401
content:
application/json:
schema:
$ref: '#/components/schemas/Envelope'
PushResult:
description: 本后端处理结果(HTTP 200;注意 success 是字符串)
content:
application/json:
schema:
$ref: '#/components/schemas/PushEnvelope'
schemas:
Envelope:
type: object
required: [status, code, message]
properties:
status:
type: string
enum: [success, fail]
code:
type: integer
description: 六位业务码;HTTP 状态 = intdiv(code, 1000),故业务失败也可能是 200
message:
type: string
data:
description: 各端点不同,未携带内容时为 null
additionalProperties: true
PushEnvelope:
type: object
required: [success, content]
properties:
success:
type: string
enum: ['true', 'false']
description: 字符串而非布尔值
content:
type: string
additionalProperties: true
NodeInfo:
type: object
properties:
id:
type: integer
port:
type: string
description: 单端口模式的监听端口,可为逗号分隔多端口;`"0"` 会绑到随机端口
passwd:
type: string
description: 单端口模式的共享密码
method:
type: string
description: 必须在本后端注册表内(docs/capabilities.json)。分组模式(`aes-128-cbc`)只走 TCP:
字节流按块边界走,不足一块的尾巴等下一批;UDP 每个数据报都是自带 IV 的一次性加解密,
尾巴没有下一批可等,所以 `is_udp=1` 配上分组模式会被拒绝落地
protocol:
type: string
description: 同上
obfs:
type: string
description: 同上
obfs_param:
type: string
protocol_param:
type: string
description: |
并发客户端上限,格式按协议代际取:`auth_sha1_v4` 取整个值为整数;
`auth_aes128_*` 与 `auth_chain_a~f` 取 `#` 前段为整数。`auth_chain_f` 的 `#` 后段是
包长表轮换秒数(缺省一天);`auth_aes128_*` 的 `#` 后段在 SSR 里是 mu 形态的用户表,
本后端不解析它——账号表只有 `user/add` 与 `userList` 这一个来源,两个来源共写一张表
会让已摘的号在下一次连接复活。本后端依托下发方提供端口与账号,不提供本地账号配置。
取不到整数时不改动上限;此时上限只由 `client_limit` 决定(0 即默认 64)
push_port:
type: integer
description: 面板反向推送的端口
secret:
type: string
single:
type: integer
enum: [0, 1]
description: 由面板决定:置 1 就按单端口跑,共享密码走 `passwd`。
单端口只有一个监听端口,账号只能靠鉴权包头那 4 字节区分,因此要求 `protocol` 会携带账号身份
(`auth_aes128_md5`/`auth_aes128_sha1`/`auth_chain_a~f`);配上其余协议(含 `auth_sha1_v4`)
时节点拒绝落地该配置并回错误,而不是跑起来后流量、在线与限速全部无法归属
speed_limit:
type: integer
description: 字节/秒;0 = 不限速(此时改用用户限速)
is_udp:
type: integer
enum: [0, 1]
client_limit:
type: integer
description: 并发客户端上限,仅在带连接跟踪的 auth_sha1_v4/auth_aes128_*/auth_chain_a~f 下生效;0 时本后端用默认 64
redirect_url:
type: string
description: 命中审计规则且流量是 HTTP 语义(目标 80 或首包嗅出 http)时回 302 跳此地址;未配置或非法值时改回 403;其余(含 TLS)只断流
UserInfo:
type: object
required: [uid, port, passwd]
properties:
uid:
type: integer
description: 面板 user.id,也是本后端的账号主键
port:
type: integer
description: |
该用户的监听端口,也是账号编号;面板建号时保证全局唯一。
单端口模式下它同时是 auth 包头那 4 字节的线上身份(密码表按它取账号);
非单端口模式下包头身份不参与——两端按本端口密钥建会话,
流量与在线记账按监听端口→uid 反查
passwd:
type: string
speed_limit:
type: integer
description: 字节/秒(uint64,出现小数会导致整条请求绑定失败)
enable:
type: integer
enum: [0, 1]
description: 0 = 摘掉该账号(面板的禁用/到期就是靠它下发状态)
method:
type: string
description: 面板会带,但本后端只认 NodeInfo 里的节点级配置
protocol:
type: string
obfs:
type: string
obfs_param:
type: string
UserTraffic:
type: object
required: [uid, upload, download]
properties:
uid:
type: integer
upload:
type: integer
description: 字节(自上次上报以来的增量)
download:
type: integer
description: 字节
upspeed:
type: integer
description: 面板不消费
downspeed:
type: integer
description: 面板不消费
report_id:
type: string
description: 同一批增量的批次号,重发不变;面板据此整批认领 (node_id, report_id)
NodeOnline:
type: object
required: [uid, ip]
properties:
uid:
type: integer
ip:
type: string
description: 客户端出口地址;同轮多个用逗号拼接
report_id:
type: string
description: 同一批在线记录的批次号,重投不变;面板据此整批认领 (node_id, report_id),否则响应丢失的那一批会被再插一遍在线行
NodeStatus:
type: object
required: [cpu, mem, disk, uptime]
properties:
cpu:
type: string
description: 带百分号的字符串,面板 intval 后 /100 存进 load
mem:
type: string
disk:
type: string
net:
type: string
description: 面板不消费
uptime:
type: integer
description: 本后端进程存活秒数
Rule:
type: object
properties:
mode:
type: string
enum: [all, allow, reject]
rules:
type: array
items:
type: object
properties:
id:
type: integer
type:
type: string
enum: [reg, domain, ip, protocol, unknown]
description: protocol/unknown 本后端忽略
pattern:
type: string
description: |
reg 按 Go 正则匹配原始 host;domain 为精确域名(不区分大小写)或
`*.example.com` 后缀(含 example.com 本身);ip 为精确 IP 或 CIDR 网段(如 10.0.0.0/8)
Trigger:
type: object
required: [uid, rule_id, reason]
properties:
uid:
type: integer
rule_id:
type: integer
reason:
type: string
description: 被命中的判定对象(目标地址或嗅出的域名)
target:
type: string
description: 连接真实去向 host:port
sniff_protocol:
type: string
enum: [http, tls]
description: 首包嗅探结果,未嗅到则省略
sniff_host:
type: string
description: HTTP Host 头或 TLS SNI
sniff_uri:
type: string
description: HTTP 请求路径(含 query),tls 无