mcp-xray-pilot 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +502 -0
- package/data/docs/_index.json +533 -0
- package/data/docs/basic__api.md +148 -0
- package/data/docs/basic__dns.md +366 -0
- package/data/docs/basic__fakedns.md +202 -0
- package/data/docs/basic__geodata.md +64 -0
- package/data/docs/basic__inbound.md +159 -0
- package/data/docs/basic__index.md +136 -0
- package/data/docs/basic__log.md +67 -0
- package/data/docs/basic__metrics.md +262 -0
- package/data/docs/basic__observatory.md +115 -0
- package/data/docs/basic__outbound.md +164 -0
- package/data/docs/basic__policy.md +140 -0
- package/data/docs/basic__reverse.md +268 -0
- package/data/docs/basic__routing.md +474 -0
- package/data/docs/basic__stats.md +61 -0
- package/data/docs/basic__transport.md +1283 -0
- package/data/docs/features__features_browser_dialer.md +61 -0
- package/data/docs/features__features_env.md +66 -0
- package/data/docs/features__features_fallback.md +110 -0
- package/data/docs/features__features_index.md +17 -0
- package/data/docs/features__features_multiple.md +144 -0
- package/data/docs/features__features_xtls.md +13 -0
- package/data/docs/inbounds__inbounds_dokodemo.md +11 -0
- package/data/docs/inbounds__inbounds_http.md +80 -0
- package/data/docs/inbounds__inbounds_hysteria.md +60 -0
- package/data/docs/inbounds__inbounds_index.md +22 -0
- package/data/docs/inbounds__inbounds_shadowsocks.md +118 -0
- package/data/docs/inbounds__inbounds_socks.md +87 -0
- package/data/docs/inbounds__inbounds_trojan.md +78 -0
- package/data/docs/inbounds__inbounds_tun.md +47 -0
- package/data/docs/inbounds__inbounds_tunnel.md +86 -0
- package/data/docs/inbounds__inbounds_vless.md +135 -0
- package/data/docs/inbounds__inbounds_vmess.md +95 -0
- package/data/docs/inbounds__inbounds_wireguard.md +78 -0
- package/data/docs/outbounds__outbounds_blackhole.md +42 -0
- package/data/docs/outbounds__outbounds_dns.md +97 -0
- package/data/docs/outbounds__outbounds_freedom.md +170 -0
- package/data/docs/outbounds__outbounds_http.md +70 -0
- package/data/docs/outbounds__outbounds_hysteria.md +39 -0
- package/data/docs/outbounds__outbounds_index.md +24 -0
- package/data/docs/outbounds__outbounds_loopback.md +65 -0
- package/data/docs/outbounds__outbounds_shadowsocks.md +105 -0
- package/data/docs/outbounds__outbounds_socks.md +58 -0
- package/data/docs/outbounds__outbounds_trojan.md +49 -0
- package/data/docs/outbounds__outbounds_vless.md +122 -0
- package/data/docs/outbounds__outbounds_vmess.md +76 -0
- package/data/docs/outbounds__outbounds_wireguard.md +141 -0
- package/data/docs/transports__transports_grpc.md +137 -0
- package/data/docs/transports__transports_h2.md +11 -0
- package/data/docs/transports__transports_http.md +11 -0
- package/data/docs/transports__transports_httpupgrade.md +61 -0
- package/data/docs/transports__transports_hysteria.md +110 -0
- package/data/docs/transports__transports_index.md +19 -0
- package/data/docs/transports__transports_mkcp.md +125 -0
- package/data/docs/transports__transports_quic.md +11 -0
- package/data/docs/transports__transports_raw.md +156 -0
- package/data/docs/transports__transports_splithttp.md +11 -0
- package/data/docs/transports__transports_tcp.md +11 -0
- package/data/docs/transports__transports_websocket.md +75 -0
- package/data/docs/transports__transports_xhttp.md +11 -0
- package/dist/data/compatibility.js +170 -0
- package/dist/data/compatibility.js.map +1 -0
- package/dist/data/geocatalogue.js +191 -0
- package/dist/data/geocatalogue.js.map +1 -0
- package/dist/docs.js +339 -0
- package/dist/docs.js.map +1 -0
- package/dist/handlers.js +217 -0
- package/dist/handlers.js.map +1 -0
- package/dist/index.js +66 -0
- package/dist/index.js.map +1 -0
- package/dist/lint.js +737 -0
- package/dist/lint.js.map +1 -0
- package/dist/schemas/protocols/blackhole.js +16 -0
- package/dist/schemas/protocols/blackhole.js.map +1 -0
- package/dist/schemas/protocols/common.js +32 -0
- package/dist/schemas/protocols/common.js.map +1 -0
- package/dist/schemas/protocols/dns.js +14 -0
- package/dist/schemas/protocols/dns.js.map +1 -0
- package/dist/schemas/protocols/dokodemo.js +17 -0
- package/dist/schemas/protocols/dokodemo.js.map +1 -0
- package/dist/schemas/protocols/freedom.js +45 -0
- package/dist/schemas/protocols/freedom.js.map +1 -0
- package/dist/schemas/protocols/http.js +38 -0
- package/dist/schemas/protocols/http.js.map +1 -0
- package/dist/schemas/protocols/hysteria.js +51 -0
- package/dist/schemas/protocols/hysteria.js.map +1 -0
- package/dist/schemas/protocols/index.js +50 -0
- package/dist/schemas/protocols/index.js.map +1 -0
- package/dist/schemas/protocols/loopback.js +11 -0
- package/dist/schemas/protocols/loopback.js.map +1 -0
- package/dist/schemas/protocols/shadowsocks.js +60 -0
- package/dist/schemas/protocols/shadowsocks.js.map +1 -0
- package/dist/schemas/protocols/socks.js +42 -0
- package/dist/schemas/protocols/socks.js.map +1 -0
- package/dist/schemas/protocols/trojan.js +34 -0
- package/dist/schemas/protocols/trojan.js.map +1 -0
- package/dist/schemas/protocols/tun.js +19 -0
- package/dist/schemas/protocols/tun.js.map +1 -0
- package/dist/schemas/protocols/vless.js +44 -0
- package/dist/schemas/protocols/vless.js.map +1 -0
- package/dist/schemas/protocols/vmess.js +48 -0
- package/dist/schemas/protocols/vmess.js.map +1 -0
- package/dist/schemas/protocols/wireguard.js +34 -0
- package/dist/schemas/protocols/wireguard.js.map +1 -0
- package/dist/schemas/security/index.js +16 -0
- package/dist/schemas/security/index.js.map +1 -0
- package/dist/schemas/security/reality.js +35 -0
- package/dist/schemas/security/reality.js.map +1 -0
- package/dist/schemas/security/tls.js +46 -0
- package/dist/schemas/security/tls.js.map +1 -0
- package/dist/schemas/security/xtls.js +17 -0
- package/dist/schemas/security/xtls.js.map +1 -0
- package/dist/schemas/transports/grpc.js +18 -0
- package/dist/schemas/transports/grpc.js.map +1 -0
- package/dist/schemas/transports/httpupgrade.js +14 -0
- package/dist/schemas/transports/httpupgrade.js.map +1 -0
- package/dist/schemas/transports/hysteria.js +25 -0
- package/dist/schemas/transports/hysteria.js.map +1 -0
- package/dist/schemas/transports/index.js +32 -0
- package/dist/schemas/transports/index.js.map +1 -0
- package/dist/schemas/transports/mkcp.js +34 -0
- package/dist/schemas/transports/mkcp.js.map +1 -0
- package/dist/schemas/transports/raw.js +19 -0
- package/dist/schemas/transports/raw.js.map +1 -0
- package/dist/schemas/transports/websocket.js +15 -0
- package/dist/schemas/transports/websocket.js.map +1 -0
- package/dist/schemas/transports/xhttp.js +34 -0
- package/dist/schemas/transports/xhttp.js.map +1 -0
- package/dist/search.js +78 -0
- package/dist/search.js.map +1 -0
- package/dist/state.js +87 -0
- package/dist/state.js.map +1 -0
- package/dist/tools.js +274 -0
- package/dist/tools.js.map +1 -0
- package/dist/tools_impl/diff.js +55 -0
- package/dist/tools_impl/diff.js.map +1 -0
- package/dist/tools_impl/github.js +416 -0
- package/dist/tools_impl/github.js.map +1 -0
- package/dist/tools_impl/merge.js +181 -0
- package/dist/tools_impl/merge.js.map +1 -0
- package/dist/tools_impl/refresh.js +46 -0
- package/dist/tools_impl/refresh.js.map +1 -0
- package/dist/tools_impl/suggest.js +169 -0
- package/dist/tools_impl/suggest.js.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.js +81 -0
- package/dist/utils.js.map +1 -0
- package/dist/validate.js +408 -0
- package/dist/validate.js.map +1 -0
- package/package.json +62 -0
package/dist/lint.js
ADDED
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Best-practice lint rules for xray configs.
|
|
3
|
+
*
|
|
4
|
+
* Each rule is a pure function (config) => LintIssue[]. They run on the
|
|
5
|
+
* *parsed* JSON; if parsing failed the dispatcher in handlers.ts short-
|
|
6
|
+
* circuits with the parse error from validate.ts and never calls these.
|
|
7
|
+
*
|
|
8
|
+
* Rule families (v0.6):
|
|
9
|
+
* v0.1 base — vless decryption, REALITY shortIds/target, dns/dangling tags,
|
|
10
|
+
* geosite/geoip + domainStrategy, xhttp path, geoip:private
|
|
11
|
+
* block, sniffing on 80/443.
|
|
12
|
+
* v0.4 sec — REALITY pubkey base64url-43, REALITY shortId hex 0..16,
|
|
13
|
+
* REALITY target host:port grammar, XTLS vision flow needs
|
|
14
|
+
* raw + tls/reality, TLS fingerprint enum, ALPN h2/h3 collision.
|
|
15
|
+
* v0.5 geo — unknown geosite/geoip catalogue tag.
|
|
16
|
+
* v0.6 mtx — protocol+security/transport/flow incompatibility.
|
|
17
|
+
*/
|
|
18
|
+
import { tlsFingerprints, alpnValues } from "./schemas/security/index.js";
|
|
19
|
+
import { isKnownGeoTag } from "./data/geocatalogue.js";
|
|
20
|
+
import { checkFlow, isProtocolSecuritySupported, isProtocolTransportSupported, } from "./data/compatibility.js";
|
|
21
|
+
function isObject(x) {
|
|
22
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
23
|
+
}
|
|
24
|
+
function getInbounds(c) {
|
|
25
|
+
if (!isObject(c) || !Array.isArray(c["inbounds"]))
|
|
26
|
+
return [];
|
|
27
|
+
return c["inbounds"].filter(isObject);
|
|
28
|
+
}
|
|
29
|
+
function getOutbounds(c) {
|
|
30
|
+
if (!isObject(c) || !Array.isArray(c["outbounds"]))
|
|
31
|
+
return [];
|
|
32
|
+
return c["outbounds"].filter(isObject);
|
|
33
|
+
}
|
|
34
|
+
function getRoutingRules(c) {
|
|
35
|
+
if (!isObject(c))
|
|
36
|
+
return [];
|
|
37
|
+
const r = c["routing"];
|
|
38
|
+
if (!isObject(r) || !Array.isArray(r["rules"]))
|
|
39
|
+
return [];
|
|
40
|
+
return r["rules"].filter(isObject);
|
|
41
|
+
}
|
|
42
|
+
function inboundsAndOutbounds(c) {
|
|
43
|
+
const out = [];
|
|
44
|
+
getInbounds(c).forEach((n, i) => out.push({ node: n, where: `/inbounds/${i}` }));
|
|
45
|
+
getOutbounds(c).forEach((n, i) => out.push({ node: n, where: `/outbounds/${i}` }));
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
const RULES = [
|
|
49
|
+
/* ---------- v0.1 base rules ---------- */
|
|
50
|
+
{
|
|
51
|
+
id: "vless_decryption_none",
|
|
52
|
+
fn: (c) => {
|
|
53
|
+
const out = [];
|
|
54
|
+
getInbounds(c).forEach((ib, i) => {
|
|
55
|
+
if (ib["protocol"] !== "vless")
|
|
56
|
+
return;
|
|
57
|
+
const settings = ib["settings"];
|
|
58
|
+
if (!isObject(settings))
|
|
59
|
+
return;
|
|
60
|
+
if (settings["decryption"] !== "none") {
|
|
61
|
+
out.push({
|
|
62
|
+
rule: "vless_decryption_none",
|
|
63
|
+
id: "vless_decryption_none",
|
|
64
|
+
severity: "warn",
|
|
65
|
+
message: 'VLESS inbound must have settings.decryption = "none" (xray will refuse to start otherwise).',
|
|
66
|
+
where: `/inbounds/${i}/settings/decryption`,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
return out;
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "reality_short_id_present",
|
|
75
|
+
fn: (c) => {
|
|
76
|
+
const out = [];
|
|
77
|
+
getInbounds(c).forEach((ib, i) => {
|
|
78
|
+
const ss = ib["streamSettings"];
|
|
79
|
+
if (!isObject(ss))
|
|
80
|
+
return;
|
|
81
|
+
if (ss["security"] !== "reality")
|
|
82
|
+
return;
|
|
83
|
+
const reality = ss["realitySettings"];
|
|
84
|
+
if (!isObject(reality))
|
|
85
|
+
return;
|
|
86
|
+
const sids = reality["shortIds"];
|
|
87
|
+
if (!Array.isArray(sids) || sids.length === 0)
|
|
88
|
+
return;
|
|
89
|
+
const hasOnlyEmpty = sids.every((x) => x === "");
|
|
90
|
+
if (hasOnlyEmpty) {
|
|
91
|
+
out.push({
|
|
92
|
+
rule: "reality_short_id_present",
|
|
93
|
+
id: "reality_short_id_present",
|
|
94
|
+
severity: "warn",
|
|
95
|
+
message: "REALITY shortIds contains only empty string. A non-empty shortId is harder for DPI to fingerprint.",
|
|
96
|
+
where: `/inbounds/${i}/streamSettings/realitySettings/shortIds`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
return out;
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "reality_target_resolves",
|
|
105
|
+
fn: (c) => {
|
|
106
|
+
const out = [];
|
|
107
|
+
const re = /^[A-Za-z0-9.\-_]+:\d{1,5}$/;
|
|
108
|
+
getInbounds(c).forEach((ib, i) => {
|
|
109
|
+
const ss = ib["streamSettings"];
|
|
110
|
+
if (!isObject(ss) || ss["security"] !== "reality")
|
|
111
|
+
return;
|
|
112
|
+
const reality = ss["realitySettings"];
|
|
113
|
+
if (!isObject(reality))
|
|
114
|
+
return;
|
|
115
|
+
const target = reality["target"] ?? reality["dest"];
|
|
116
|
+
if (typeof target !== "string" || !re.test(target)) {
|
|
117
|
+
out.push({
|
|
118
|
+
rule: "reality_target_resolves",
|
|
119
|
+
id: "reality_target_resolves",
|
|
120
|
+
severity: "warn",
|
|
121
|
+
message: `REALITY target/dest "${String(target)}" doesn't look like host:port (e.g. "www.microsoft.com:443").`,
|
|
122
|
+
where: `/inbounds/${i}/streamSettings/realitySettings/target`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
return out;
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: "dns_block_missing",
|
|
131
|
+
fn: (c) => {
|
|
132
|
+
const out = [];
|
|
133
|
+
const rules = getRoutingRules(c);
|
|
134
|
+
const referencesDnsOutbound = rules.some((r) => r["outboundTag"] === "dns-out" || r["outboundTag"] === "dns");
|
|
135
|
+
if (!referencesDnsOutbound)
|
|
136
|
+
return out;
|
|
137
|
+
const dns = isObject(c) ? c["dns"] : undefined;
|
|
138
|
+
const servers = isObject(dns) ? dns["servers"] : undefined;
|
|
139
|
+
if (!Array.isArray(servers) || servers.length === 0) {
|
|
140
|
+
out.push({
|
|
141
|
+
rule: "dns_block_missing",
|
|
142
|
+
id: "dns_block_missing",
|
|
143
|
+
severity: "warn",
|
|
144
|
+
message: "routing rules reference a DNS outbound, but dns.servers is empty or missing.",
|
|
145
|
+
where: "/dns/servers",
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: "routing_dangling_outbound",
|
|
153
|
+
fn: (c) => {
|
|
154
|
+
const out = [];
|
|
155
|
+
const tags = new Set(getOutbounds(c)
|
|
156
|
+
.map((o) => o["tag"])
|
|
157
|
+
.filter((t) => typeof t === "string" && !!t));
|
|
158
|
+
getRoutingRules(c).forEach((r, i) => {
|
|
159
|
+
const ot = r["outboundTag"];
|
|
160
|
+
if (typeof ot === "string" && ot && !tags.has(ot)) {
|
|
161
|
+
out.push({
|
|
162
|
+
rule: "routing_dangling_outbound",
|
|
163
|
+
id: "routing_dangling_outbound",
|
|
164
|
+
severity: "error",
|
|
165
|
+
message: `routing.rules[${i}].outboundTag "${ot}" doesn't exist in outbounds[].`,
|
|
166
|
+
where: `/routing/rules/${i}/outboundTag`,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
return out;
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
id: "routing_dangling_inbound",
|
|
175
|
+
fn: (c) => {
|
|
176
|
+
const out = [];
|
|
177
|
+
const tags = new Set(getInbounds(c)
|
|
178
|
+
.map((o) => o["tag"])
|
|
179
|
+
.filter((t) => typeof t === "string" && !!t));
|
|
180
|
+
getRoutingRules(c).forEach((r, i) => {
|
|
181
|
+
const its = r["inboundTag"];
|
|
182
|
+
const arr = Array.isArray(its) ? its : its ? [its] : [];
|
|
183
|
+
arr.forEach((tag, j) => {
|
|
184
|
+
if (typeof tag === "string" && tag && !tags.has(tag)) {
|
|
185
|
+
out.push({
|
|
186
|
+
rule: "routing_dangling_inbound",
|
|
187
|
+
id: "routing_dangling_inbound",
|
|
188
|
+
severity: "error",
|
|
189
|
+
message: `routing.rules[${i}].inboundTag[${j}] "${tag}" doesn't exist in inbounds[].`,
|
|
190
|
+
where: `/routing/rules/${i}/inboundTag/${j}`,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
return out;
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: "geosite_geoip_used_without_strategy",
|
|
200
|
+
fn: (c) => {
|
|
201
|
+
const out = [];
|
|
202
|
+
if (!isObject(c))
|
|
203
|
+
return out;
|
|
204
|
+
const routing = c["routing"];
|
|
205
|
+
if (!isObject(routing))
|
|
206
|
+
return out;
|
|
207
|
+
const strat = routing["domainStrategy"];
|
|
208
|
+
if (strat && strat !== "AsIs")
|
|
209
|
+
return out;
|
|
210
|
+
const usesGeoIp = getRoutingRules(c).some((r) => {
|
|
211
|
+
const ip = r["ip"];
|
|
212
|
+
return Array.isArray(ip) && ip.some((x) => typeof x === "string" && x.startsWith("geoip:"));
|
|
213
|
+
});
|
|
214
|
+
const usesGeoSite = getRoutingRules(c).some((r) => {
|
|
215
|
+
const d = r["domain"];
|
|
216
|
+
return Array.isArray(d) && d.some((x) => typeof x === "string" && x.startsWith("geosite:"));
|
|
217
|
+
});
|
|
218
|
+
if (usesGeoIp && (!strat || strat === "AsIs")) {
|
|
219
|
+
out.push({
|
|
220
|
+
rule: "geosite_geoip_used_without_strategy",
|
|
221
|
+
id: "geosite_geoip_used_without_strategy",
|
|
222
|
+
severity: "info",
|
|
223
|
+
message: "geoip:* used in rules but routing.domainStrategy is AsIs. Consider IPIfNonMatch so domain rules can fall back to IP lookup.",
|
|
224
|
+
where: "/routing/domainStrategy",
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
if (usesGeoSite && !strat) {
|
|
228
|
+
out.push({
|
|
229
|
+
rule: "geosite_geoip_used_without_strategy",
|
|
230
|
+
id: "geosite_geoip_used_without_strategy",
|
|
231
|
+
severity: "info",
|
|
232
|
+
message: "geosite:* used in rules but routing.domainStrategy is unset (defaults to AsIs).",
|
|
233
|
+
where: "/routing/domainStrategy",
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
return out;
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
id: "xhttp_path_leading_slash",
|
|
241
|
+
fn: (c) => {
|
|
242
|
+
const out = [];
|
|
243
|
+
const check = (node, where) => {
|
|
244
|
+
const ss = node["streamSettings"];
|
|
245
|
+
if (!isObject(ss))
|
|
246
|
+
return;
|
|
247
|
+
const xs = ss["xhttpSettings"];
|
|
248
|
+
if (!isObject(xs))
|
|
249
|
+
return;
|
|
250
|
+
const path = xs["path"];
|
|
251
|
+
if (typeof path === "string" && path && !path.startsWith("/")) {
|
|
252
|
+
out.push({
|
|
253
|
+
rule: "xhttp_path_leading_slash",
|
|
254
|
+
id: "xhttp_path_leading_slash",
|
|
255
|
+
severity: "warn",
|
|
256
|
+
message: `xhttpSettings.path "${path}" should start with "/".`,
|
|
257
|
+
where: `${where}/streamSettings/xhttpSettings/path`,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
getInbounds(c).forEach((ib, i) => check(ib, `/inbounds/${i}`));
|
|
262
|
+
getOutbounds(c).forEach((ob, i) => check(ob, `/outbounds/${i}`));
|
|
263
|
+
return out;
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
id: "private_geoip_blocked",
|
|
268
|
+
fn: (c) => {
|
|
269
|
+
const out = [];
|
|
270
|
+
const rules = getRoutingRules(c);
|
|
271
|
+
const blocks = rules.some((r) => {
|
|
272
|
+
const ip = r["ip"];
|
|
273
|
+
const ot = r["outboundTag"];
|
|
274
|
+
if (typeof ot !== "string")
|
|
275
|
+
return false;
|
|
276
|
+
const isBlock = /block|blackhole/i.test(ot);
|
|
277
|
+
if (!isBlock)
|
|
278
|
+
return false;
|
|
279
|
+
return Array.isArray(ip) && ip.some((x) => typeof x === "string" && x.includes("geoip:private"));
|
|
280
|
+
});
|
|
281
|
+
if (!blocks) {
|
|
282
|
+
out.push({
|
|
283
|
+
rule: "private_geoip_blocked",
|
|
284
|
+
id: "private_geoip_blocked",
|
|
285
|
+
severity: "warn",
|
|
286
|
+
message: "No routing rule blocks geoip:private. Without it the VPN can be used to scan the server's LAN — usually undesirable.",
|
|
287
|
+
where: "/routing/rules",
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
return out;
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
id: "sniffing_enabled_recommended",
|
|
295
|
+
fn: (c) => {
|
|
296
|
+
const out = [];
|
|
297
|
+
getInbounds(c).forEach((ib, i) => {
|
|
298
|
+
const port = ib["port"];
|
|
299
|
+
const portMatches = port === 443 || port === 80 || port === "443" || port === "80";
|
|
300
|
+
if (!portMatches)
|
|
301
|
+
return;
|
|
302
|
+
const sniffing = ib["sniffing"];
|
|
303
|
+
if (!isObject(sniffing) || sniffing["enabled"] !== true) {
|
|
304
|
+
out.push({
|
|
305
|
+
rule: "sniffing_enabled_recommended",
|
|
306
|
+
id: "sniffing_enabled_recommended",
|
|
307
|
+
severity: "info",
|
|
308
|
+
message: "sniffing.enabled=true is recommended for inbounds on 80/443 — required for routing by domain/protocol.",
|
|
309
|
+
where: `/inbounds/${i}/sniffing`,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
return out;
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
/* ---------- v0.4 REALITY/XTLS deep ---------- */
|
|
317
|
+
{
|
|
318
|
+
id: "reality_pubkey_format",
|
|
319
|
+
fn: (c) => {
|
|
320
|
+
const out = [];
|
|
321
|
+
const re = /^[A-Za-z0-9_-]{43}$/;
|
|
322
|
+
const check = (node, where) => {
|
|
323
|
+
const ss = node["streamSettings"];
|
|
324
|
+
if (!isObject(ss) || ss["security"] !== "reality")
|
|
325
|
+
return;
|
|
326
|
+
const reality = ss["realitySettings"];
|
|
327
|
+
if (!isObject(reality))
|
|
328
|
+
return;
|
|
329
|
+
for (const key of ["privateKey", "publicKey"]) {
|
|
330
|
+
const k = reality[key];
|
|
331
|
+
if (k !== undefined && (typeof k !== "string" || !re.test(k))) {
|
|
332
|
+
out.push({
|
|
333
|
+
rule: "reality_pubkey_format",
|
|
334
|
+
id: "reality_pubkey_format",
|
|
335
|
+
severity: "error",
|
|
336
|
+
message: `REALITY ${key} must be 43 base64url chars (no padding). Got: "${String(k).slice(0, 20)}…" (${typeof k === "string" ? k.length : "non-string"} chars).`,
|
|
337
|
+
where: `${where}/streamSettings/realitySettings/${key}`,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
getInbounds(c).forEach((ib, i) => check(ib, `/inbounds/${i}`));
|
|
343
|
+
getOutbounds(c).forEach((ob, i) => check(ob, `/outbounds/${i}`));
|
|
344
|
+
return out;
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
id: "reality_shortid_format",
|
|
349
|
+
fn: (c) => {
|
|
350
|
+
const out = [];
|
|
351
|
+
const re = /^([0-9a-fA-F]{2}){0,8}$/;
|
|
352
|
+
const check = (node, where) => {
|
|
353
|
+
const ss = node["streamSettings"];
|
|
354
|
+
if (!isObject(ss) || ss["security"] !== "reality")
|
|
355
|
+
return;
|
|
356
|
+
const reality = ss["realitySettings"];
|
|
357
|
+
if (!isObject(reality))
|
|
358
|
+
return;
|
|
359
|
+
const sids = reality["shortIds"];
|
|
360
|
+
if (!Array.isArray(sids))
|
|
361
|
+
return;
|
|
362
|
+
sids.forEach((s, j) => {
|
|
363
|
+
if (typeof s !== "string" || !re.test(s)) {
|
|
364
|
+
out.push({
|
|
365
|
+
rule: "reality_shortid_format",
|
|
366
|
+
id: "reality_shortid_format",
|
|
367
|
+
severity: "error",
|
|
368
|
+
message: `REALITY shortIds[${j}] must be hex with even length 0..16. Got: ${JSON.stringify(s)}.`,
|
|
369
|
+
where: `${where}/streamSettings/realitySettings/shortIds/${j}`,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
};
|
|
374
|
+
getInbounds(c).forEach((ib, i) => check(ib, `/inbounds/${i}`));
|
|
375
|
+
getOutbounds(c).forEach((ob, i) => check(ob, `/outbounds/${i}`));
|
|
376
|
+
return out;
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
id: "reality_target_format",
|
|
381
|
+
fn: (c) => {
|
|
382
|
+
const out = [];
|
|
383
|
+
const re = /^[A-Za-z0-9.\-_]+:\d{1,5}$/;
|
|
384
|
+
const check = (node, where) => {
|
|
385
|
+
const ss = node["streamSettings"];
|
|
386
|
+
if (!isObject(ss) || ss["security"] !== "reality")
|
|
387
|
+
return;
|
|
388
|
+
const reality = ss["realitySettings"];
|
|
389
|
+
if (!isObject(reality))
|
|
390
|
+
return;
|
|
391
|
+
const t = reality["target"] ?? reality["dest"];
|
|
392
|
+
if (t === undefined)
|
|
393
|
+
return;
|
|
394
|
+
if (typeof t !== "string" || !re.test(t)) {
|
|
395
|
+
out.push({
|
|
396
|
+
rule: "reality_target_format",
|
|
397
|
+
id: "reality_target_format",
|
|
398
|
+
severity: "error",
|
|
399
|
+
message: `REALITY target/dest must match host:port grammar. Got: ${JSON.stringify(t)}.`,
|
|
400
|
+
where: `${where}/streamSettings/realitySettings/target`,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
getInbounds(c).forEach((ib, i) => check(ib, `/inbounds/${i}`));
|
|
405
|
+
getOutbounds(c).forEach((ob, i) => check(ob, `/outbounds/${i}`));
|
|
406
|
+
return out;
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
id: "xtls_flow_requires_vision",
|
|
411
|
+
fn: (c) => {
|
|
412
|
+
const out = [];
|
|
413
|
+
/* Vision flow only works with raw/tcp transport + tls/reality security.
|
|
414
|
+
* Walk each VLESS client/user; if flow=xtls-rprx-vision* then verify. */
|
|
415
|
+
const visit = (node, users, where) => {
|
|
416
|
+
if (!Array.isArray(users))
|
|
417
|
+
return;
|
|
418
|
+
const ss = node["streamSettings"];
|
|
419
|
+
const network = isObject(ss) ? ss["network"] : undefined;
|
|
420
|
+
const security = isObject(ss) ? ss["security"] : undefined;
|
|
421
|
+
users.forEach((u, j) => {
|
|
422
|
+
if (!isObject(u))
|
|
423
|
+
return;
|
|
424
|
+
const flow = u["flow"];
|
|
425
|
+
if (typeof flow !== "string" || !flow.startsWith("xtls-rprx-vision"))
|
|
426
|
+
return;
|
|
427
|
+
const goodTransport = network === "raw" || network === "tcp" || network === undefined;
|
|
428
|
+
const goodSecurity = security === "tls" || security === "reality";
|
|
429
|
+
if (!goodTransport || !goodSecurity) {
|
|
430
|
+
out.push({
|
|
431
|
+
rule: "xtls_flow_requires_vision",
|
|
432
|
+
id: "xtls_flow_requires_vision",
|
|
433
|
+
severity: "error",
|
|
434
|
+
message: `flow="${flow}" requires transport=raw/tcp + security=tls/reality (got network=${String(network)}, security=${String(security)}).`,
|
|
435
|
+
where: `${where}/${j}/flow`,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
};
|
|
440
|
+
getInbounds(c).forEach((ib, i) => {
|
|
441
|
+
if (ib["protocol"] !== "vless")
|
|
442
|
+
return;
|
|
443
|
+
const settings = ib["settings"];
|
|
444
|
+
if (!isObject(settings))
|
|
445
|
+
return;
|
|
446
|
+
visit(ib, settings["clients"], `/inbounds/${i}/settings/clients`);
|
|
447
|
+
});
|
|
448
|
+
getOutbounds(c).forEach((ob, i) => {
|
|
449
|
+
if (ob["protocol"] !== "vless")
|
|
450
|
+
return;
|
|
451
|
+
const settings = ob["settings"];
|
|
452
|
+
if (!isObject(settings))
|
|
453
|
+
return;
|
|
454
|
+
const vnext = settings["vnext"];
|
|
455
|
+
if (Array.isArray(vnext)) {
|
|
456
|
+
vnext.forEach((srv, k) => {
|
|
457
|
+
if (!isObject(srv))
|
|
458
|
+
return;
|
|
459
|
+
visit(ob, srv["users"], `/outbounds/${i}/settings/vnext/${k}/users`);
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
return out;
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
id: "tls_fingerprint_enum",
|
|
468
|
+
fn: (c) => {
|
|
469
|
+
const out = [];
|
|
470
|
+
const allowed = new Set(tlsFingerprints);
|
|
471
|
+
const check = (node, where) => {
|
|
472
|
+
const ss = node["streamSettings"];
|
|
473
|
+
if (!isObject(ss))
|
|
474
|
+
return;
|
|
475
|
+
for (const key of ["tlsSettings", "realitySettings"]) {
|
|
476
|
+
const sec = ss[key];
|
|
477
|
+
if (!isObject(sec))
|
|
478
|
+
continue;
|
|
479
|
+
const fp = sec["fingerprint"];
|
|
480
|
+
if (fp !== undefined && (typeof fp !== "string" || !allowed.has(fp))) {
|
|
481
|
+
out.push({
|
|
482
|
+
rule: "tls_fingerprint_enum",
|
|
483
|
+
id: "tls_fingerprint_enum",
|
|
484
|
+
severity: "warn",
|
|
485
|
+
message: `${key}.fingerprint "${String(fp)}" not in known set: ${[...allowed].join(", ")}.`,
|
|
486
|
+
where: `${where}/streamSettings/${key}/fingerprint`,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
getInbounds(c).forEach((ib, i) => check(ib, `/inbounds/${i}`));
|
|
492
|
+
getOutbounds(c).forEach((ob, i) => check(ob, `/outbounds/${i}`));
|
|
493
|
+
return out;
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
id: "tls_alpn_collision",
|
|
498
|
+
fn: (c) => {
|
|
499
|
+
const out = [];
|
|
500
|
+
const allowed = new Set(alpnValues);
|
|
501
|
+
const check = (node, where) => {
|
|
502
|
+
const ss = node["streamSettings"];
|
|
503
|
+
if (!isObject(ss))
|
|
504
|
+
return;
|
|
505
|
+
const tls = ss["tlsSettings"];
|
|
506
|
+
if (!isObject(tls))
|
|
507
|
+
return;
|
|
508
|
+
const alpn = tls["alpn"];
|
|
509
|
+
if (!Array.isArray(alpn))
|
|
510
|
+
return;
|
|
511
|
+
const network = ss["network"];
|
|
512
|
+
const hasH2 = alpn.includes("h2");
|
|
513
|
+
const hasH3 = alpn.includes("h3");
|
|
514
|
+
const hasH1 = alpn.includes("http/1.1");
|
|
515
|
+
for (const a of alpn) {
|
|
516
|
+
if (typeof a === "string" && !allowed.has(a)) {
|
|
517
|
+
out.push({
|
|
518
|
+
rule: "tls_alpn_collision",
|
|
519
|
+
id: "tls_alpn_collision",
|
|
520
|
+
severity: "warn",
|
|
521
|
+
message: `tlsSettings.alpn contains unknown value "${a}". Known: ${[...allowed].join(", ")}.`,
|
|
522
|
+
where: `${where}/streamSettings/tlsSettings/alpn`,
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (network === "xhttp" && !hasH2 && !hasH3) {
|
|
527
|
+
out.push({
|
|
528
|
+
rule: "tls_alpn_collision",
|
|
529
|
+
id: "tls_alpn_collision",
|
|
530
|
+
severity: "info",
|
|
531
|
+
message: 'xhttp transport usually negotiates h2/h3; consider declaring alpn: ["h2"] explicitly.',
|
|
532
|
+
where: `${where}/streamSettings/tlsSettings/alpn`,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
if (hasH1 && hasH2 && network === "grpc") {
|
|
536
|
+
out.push({
|
|
537
|
+
rule: "tls_alpn_collision",
|
|
538
|
+
id: "tls_alpn_collision",
|
|
539
|
+
severity: "info",
|
|
540
|
+
message: "grpc requires h2; including http/1.1 in alpn lets clients fall back and break.",
|
|
541
|
+
where: `${where}/streamSettings/tlsSettings/alpn`,
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
getInbounds(c).forEach((ib, i) => check(ib, `/inbounds/${i}`));
|
|
546
|
+
getOutbounds(c).forEach((ob, i) => check(ob, `/outbounds/${i}`));
|
|
547
|
+
return out;
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
/* ---------- v0.5 geo catalogue ---------- */
|
|
551
|
+
{
|
|
552
|
+
id: "geo_unknown_category",
|
|
553
|
+
fn: (c) => {
|
|
554
|
+
const out = [];
|
|
555
|
+
getRoutingRules(c).forEach((r, i) => {
|
|
556
|
+
const visit = (arr, field) => {
|
|
557
|
+
if (!Array.isArray(arr))
|
|
558
|
+
return;
|
|
559
|
+
arr.forEach((tag, j) => {
|
|
560
|
+
if (typeof tag !== "string")
|
|
561
|
+
return;
|
|
562
|
+
if (!tag.startsWith("geosite:") && !tag.startsWith("geoip:"))
|
|
563
|
+
return;
|
|
564
|
+
if (!isKnownGeoTag(tag)) {
|
|
565
|
+
out.push({
|
|
566
|
+
rule: "geo_unknown_category",
|
|
567
|
+
id: "geo_unknown_category",
|
|
568
|
+
severity: "warn",
|
|
569
|
+
message: `Unknown ${tag.split(":")[0]} category "${tag}". Use xray_geo_search to find the correct tag.`,
|
|
570
|
+
where: `/routing/rules/${i}/${field}/${j}`,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
};
|
|
575
|
+
visit(r["domain"], "domain");
|
|
576
|
+
visit(r["ip"], "ip");
|
|
577
|
+
});
|
|
578
|
+
return out;
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
/* ---------- v0.6 compatibility matrix ---------- */
|
|
582
|
+
{
|
|
583
|
+
id: "incompatible_protocol_security",
|
|
584
|
+
fn: (c) => {
|
|
585
|
+
const out = [];
|
|
586
|
+
const check = (node, where) => {
|
|
587
|
+
const proto = node["protocol"];
|
|
588
|
+
if (typeof proto !== "string")
|
|
589
|
+
return;
|
|
590
|
+
const ss = node["streamSettings"];
|
|
591
|
+
if (!isObject(ss))
|
|
592
|
+
return;
|
|
593
|
+
const security = ss["security"];
|
|
594
|
+
if (typeof security !== "string" || !security)
|
|
595
|
+
return;
|
|
596
|
+
const r = isProtocolSecuritySupported(proto, security);
|
|
597
|
+
if (!r.ok) {
|
|
598
|
+
out.push({
|
|
599
|
+
rule: "incompatible_protocol_security",
|
|
600
|
+
id: "incompatible_protocol_security",
|
|
601
|
+
severity: "error",
|
|
602
|
+
message: r.reason ?? "incompatible protocol+security",
|
|
603
|
+
where: `${where}/streamSettings/security`,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
getInbounds(c).forEach((ib, i) => check(ib, `/inbounds/${i}`));
|
|
608
|
+
getOutbounds(c).forEach((ob, i) => check(ob, `/outbounds/${i}`));
|
|
609
|
+
return out;
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
id: "incompatible_protocol_transport",
|
|
614
|
+
fn: (c) => {
|
|
615
|
+
const out = [];
|
|
616
|
+
const check = (node, where) => {
|
|
617
|
+
const proto = node["protocol"];
|
|
618
|
+
if (typeof proto !== "string")
|
|
619
|
+
return;
|
|
620
|
+
const ss = node["streamSettings"];
|
|
621
|
+
if (!isObject(ss))
|
|
622
|
+
return;
|
|
623
|
+
const network = ss["network"];
|
|
624
|
+
if (typeof network !== "string" || !network)
|
|
625
|
+
return;
|
|
626
|
+
const r = isProtocolTransportSupported(proto, network);
|
|
627
|
+
if (!r.ok) {
|
|
628
|
+
out.push({
|
|
629
|
+
rule: "incompatible_protocol_transport",
|
|
630
|
+
id: "incompatible_protocol_transport",
|
|
631
|
+
severity: "error",
|
|
632
|
+
message: r.reason ?? "incompatible protocol+transport",
|
|
633
|
+
where: `${where}/streamSettings/network`,
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
getInbounds(c).forEach((ib, i) => check(ib, `/inbounds/${i}`));
|
|
638
|
+
getOutbounds(c).forEach((ob, i) => check(ob, `/outbounds/${i}`));
|
|
639
|
+
return out;
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
id: "flow_requires_specific_transport",
|
|
644
|
+
fn: (c) => {
|
|
645
|
+
const out = [];
|
|
646
|
+
const visit = (node, users, where) => {
|
|
647
|
+
if (!Array.isArray(users))
|
|
648
|
+
return;
|
|
649
|
+
const proto = node["protocol"];
|
|
650
|
+
if (typeof proto !== "string")
|
|
651
|
+
return;
|
|
652
|
+
const ss = node["streamSettings"];
|
|
653
|
+
const network = isObject(ss) && typeof ss["network"] === "string" ? ss["network"] : "";
|
|
654
|
+
const security = isObject(ss) && typeof ss["security"] === "string" ? ss["security"] : "none";
|
|
655
|
+
users.forEach((u, j) => {
|
|
656
|
+
if (!isObject(u))
|
|
657
|
+
return;
|
|
658
|
+
const flow = u["flow"];
|
|
659
|
+
if (typeof flow !== "string" || !flow)
|
|
660
|
+
return;
|
|
661
|
+
const r = checkFlow(proto, flow, network, security);
|
|
662
|
+
if (!r.ok) {
|
|
663
|
+
out.push({
|
|
664
|
+
rule: "flow_requires_specific_transport",
|
|
665
|
+
id: "flow_requires_specific_transport",
|
|
666
|
+
severity: "error",
|
|
667
|
+
message: r.reason ?? "flow requires a specific transport+security",
|
|
668
|
+
where: `${where}/${j}/flow`,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
};
|
|
673
|
+
getInbounds(c).forEach((ib, i) => {
|
|
674
|
+
const settings = ib["settings"];
|
|
675
|
+
if (!isObject(settings))
|
|
676
|
+
return;
|
|
677
|
+
visit(ib, settings["clients"], `/inbounds/${i}/settings/clients`);
|
|
678
|
+
});
|
|
679
|
+
getOutbounds(c).forEach((ob, i) => {
|
|
680
|
+
const settings = ob["settings"];
|
|
681
|
+
if (!isObject(settings))
|
|
682
|
+
return;
|
|
683
|
+
const vnext = settings["vnext"];
|
|
684
|
+
if (Array.isArray(vnext)) {
|
|
685
|
+
vnext.forEach((srv, k) => {
|
|
686
|
+
if (!isObject(srv))
|
|
687
|
+
return;
|
|
688
|
+
visit(ob, srv["users"], `/outbounds/${i}/settings/vnext/${k}/users`);
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
return out;
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
];
|
|
696
|
+
export function lintConfig(jsonText) {
|
|
697
|
+
let parsed;
|
|
698
|
+
try {
|
|
699
|
+
parsed = JSON.parse(jsonText);
|
|
700
|
+
}
|
|
701
|
+
catch (err) {
|
|
702
|
+
return {
|
|
703
|
+
ok: false,
|
|
704
|
+
ranRules: [],
|
|
705
|
+
issues: [
|
|
706
|
+
{
|
|
707
|
+
rule: "_parse",
|
|
708
|
+
id: "bad_json",
|
|
709
|
+
severity: "error",
|
|
710
|
+
message: `Config is not valid JSON: ${err.message}`,
|
|
711
|
+
},
|
|
712
|
+
],
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
const issues = [];
|
|
716
|
+
const ranRules = [];
|
|
717
|
+
for (const r of RULES) {
|
|
718
|
+
ranRules.push(r.id);
|
|
719
|
+
try {
|
|
720
|
+
issues.push(...r.fn(parsed));
|
|
721
|
+
}
|
|
722
|
+
catch (err) {
|
|
723
|
+
issues.push({
|
|
724
|
+
rule: r.id,
|
|
725
|
+
id: "rule_threw",
|
|
726
|
+
severity: "warn",
|
|
727
|
+
message: `Lint rule "${r.id}" threw: ${err.message}`,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return {
|
|
732
|
+
ok: !issues.some((i) => i.severity === "error"),
|
|
733
|
+
issues,
|
|
734
|
+
ranRules,
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
//# sourceMappingURL=lint.js.map
|