my-singbox 2.0.0 → 3.0.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/CHANGELOG.md +22 -0
- package/README.md +9 -3
- package/README_CN.md +9 -3
- package/dist/cli.js +396 -560
- package/dist/core/isp-service.d.ts +1 -2
- package/dist/core/isp.d.ts +7 -7
- package/dist/core/render-config.d.ts +1 -1
- package/dist/core/routing.d.ts +13 -0
- package/dist/index.js +318 -377
- package/dist/model.d.ts +3 -2
- package/package.json +3 -2
- package/templates/config.json.template +20 -1
package/dist/index.js
CHANGED
|
@@ -48,10 +48,10 @@ var require_identity = __commonJS({
|
|
|
48
48
|
var NODE_TYPE = /* @__PURE__ */ Symbol.for("yaml.node.type");
|
|
49
49
|
var isAlias = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === ALIAS;
|
|
50
50
|
var isDocument = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === DOC;
|
|
51
|
-
var
|
|
51
|
+
var isMap2 = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === MAP;
|
|
52
52
|
var isPair = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === PAIR;
|
|
53
|
-
var
|
|
54
|
-
var
|
|
53
|
+
var isScalar2 = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === SCALAR;
|
|
54
|
+
var isSeq2 = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === SEQ;
|
|
55
55
|
function isCollection(node) {
|
|
56
56
|
if (node && typeof node === "object")
|
|
57
57
|
switch (node[NODE_TYPE]) {
|
|
@@ -72,7 +72,7 @@ var require_identity = __commonJS({
|
|
|
72
72
|
}
|
|
73
73
|
return false;
|
|
74
74
|
}
|
|
75
|
-
var hasAnchor = (node) => (
|
|
75
|
+
var hasAnchor = (node) => (isScalar2(node) || isCollection(node)) && !!node.anchor;
|
|
76
76
|
exports.ALIAS = ALIAS;
|
|
77
77
|
exports.DOC = DOC;
|
|
78
78
|
exports.MAP = MAP;
|
|
@@ -84,11 +84,11 @@ var require_identity = __commonJS({
|
|
|
84
84
|
exports.isAlias = isAlias;
|
|
85
85
|
exports.isCollection = isCollection;
|
|
86
86
|
exports.isDocument = isDocument;
|
|
87
|
-
exports.isMap =
|
|
87
|
+
exports.isMap = isMap2;
|
|
88
88
|
exports.isNode = isNode;
|
|
89
89
|
exports.isPair = isPair;
|
|
90
|
-
exports.isScalar =
|
|
91
|
-
exports.isSeq =
|
|
90
|
+
exports.isScalar = isScalar2;
|
|
91
|
+
exports.isSeq = isSeq2;
|
|
92
92
|
}
|
|
93
93
|
});
|
|
94
94
|
|
|
@@ -100,7 +100,7 @@ var require_visit = __commonJS({
|
|
|
100
100
|
var BREAK = /* @__PURE__ */ Symbol("break visit");
|
|
101
101
|
var SKIP = /* @__PURE__ */ Symbol("skip children");
|
|
102
102
|
var REMOVE = /* @__PURE__ */ Symbol("remove node");
|
|
103
|
-
function
|
|
103
|
+
function visit2(node, visitor) {
|
|
104
104
|
const visitor_ = initVisitor(visitor);
|
|
105
105
|
if (identity.isDocument(node)) {
|
|
106
106
|
const cd = visit_(null, node.contents, visitor_, Object.freeze([node]));
|
|
@@ -109,9 +109,9 @@ var require_visit = __commonJS({
|
|
|
109
109
|
} else
|
|
110
110
|
visit_(null, node, visitor_, Object.freeze([]));
|
|
111
111
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
visit2.BREAK = BREAK;
|
|
113
|
+
visit2.SKIP = SKIP;
|
|
114
|
+
visit2.REMOVE = REMOVE;
|
|
115
115
|
function visit_(key, node, visitor, path) {
|
|
116
116
|
const ctrl = callVisitor(key, node, visitor, path);
|
|
117
117
|
if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
|
|
@@ -245,7 +245,7 @@ var require_visit = __commonJS({
|
|
|
245
245
|
throw new Error(`Cannot replace node with ${pt} parent`);
|
|
246
246
|
}
|
|
247
247
|
}
|
|
248
|
-
exports.visit =
|
|
248
|
+
exports.visit = visit2;
|
|
249
249
|
exports.visitAsync = visitAsync;
|
|
250
250
|
}
|
|
251
251
|
});
|
|
@@ -255,7 +255,7 @@ var require_directives = __commonJS({
|
|
|
255
255
|
"node_modules/yaml/dist/doc/directives.js"(exports) {
|
|
256
256
|
"use strict";
|
|
257
257
|
var identity = require_identity();
|
|
258
|
-
var
|
|
258
|
+
var visit2 = require_visit();
|
|
259
259
|
var escapeChars = {
|
|
260
260
|
"!": "%21",
|
|
261
261
|
",": "%2C",
|
|
@@ -399,7 +399,7 @@ var require_directives = __commonJS({
|
|
|
399
399
|
let tagNames;
|
|
400
400
|
if (doc && tagEntries.length > 0 && identity.isNode(doc.contents)) {
|
|
401
401
|
const tags = {};
|
|
402
|
-
|
|
402
|
+
visit2.visit(doc.contents, (_key, node) => {
|
|
403
403
|
if (identity.isNode(node) && node.tag)
|
|
404
404
|
tags[node.tag] = true;
|
|
405
405
|
});
|
|
@@ -426,7 +426,7 @@ var require_anchors = __commonJS({
|
|
|
426
426
|
"node_modules/yaml/dist/doc/anchors.js"(exports) {
|
|
427
427
|
"use strict";
|
|
428
428
|
var identity = require_identity();
|
|
429
|
-
var
|
|
429
|
+
var visit2 = require_visit();
|
|
430
430
|
function anchorIsValid(anchor) {
|
|
431
431
|
if (/[\x00-\x19\s,[\]{}]/.test(anchor)) {
|
|
432
432
|
const sa = JSON.stringify(anchor);
|
|
@@ -437,7 +437,7 @@ var require_anchors = __commonJS({
|
|
|
437
437
|
}
|
|
438
438
|
function anchorNames(root) {
|
|
439
439
|
const anchors = /* @__PURE__ */ new Set();
|
|
440
|
-
|
|
440
|
+
visit2.visit(root, {
|
|
441
441
|
Value(_key, node) {
|
|
442
442
|
if (node.anchor)
|
|
443
443
|
anchors.add(node.anchor);
|
|
@@ -617,7 +617,7 @@ var require_Alias = __commonJS({
|
|
|
617
617
|
"node_modules/yaml/dist/nodes/Alias.js"(exports) {
|
|
618
618
|
"use strict";
|
|
619
619
|
var anchors = require_anchors();
|
|
620
|
-
var
|
|
620
|
+
var visit2 = require_visit();
|
|
621
621
|
var identity = require_identity();
|
|
622
622
|
var Node = require_Node();
|
|
623
623
|
var toJS = require_toJS();
|
|
@@ -643,7 +643,7 @@ var require_Alias = __commonJS({
|
|
|
643
643
|
nodes = ctx.aliasResolveCache;
|
|
644
644
|
} else {
|
|
645
645
|
nodes = [];
|
|
646
|
-
|
|
646
|
+
visit2.visit(doc, {
|
|
647
647
|
Node: (_key, node) => {
|
|
648
648
|
if (identity.isAlias(node) || identity.hasAnchor(node))
|
|
649
649
|
nodes.push(node);
|
|
@@ -4196,9 +4196,9 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
4196
4196
|
var blockMsg = "Block collections are not allowed within flow collections";
|
|
4197
4197
|
var isBlock = (token) => token && (token.type === "block-map" || token.type === "block-seq");
|
|
4198
4198
|
function resolveFlowCollection({ composeNode, composeEmptyNode }, ctx, fc, onError, tag) {
|
|
4199
|
-
const
|
|
4200
|
-
const fcName =
|
|
4201
|
-
const NodeClass = tag?.nodeClass ?? (
|
|
4199
|
+
const isMap2 = fc.start.source === "{";
|
|
4200
|
+
const fcName = isMap2 ? "flow map" : "flow sequence";
|
|
4201
|
+
const NodeClass = tag?.nodeClass ?? (isMap2 ? YAMLMap.YAMLMap : YAMLSeq.YAMLSeq);
|
|
4202
4202
|
const coll = new NodeClass(ctx.schema);
|
|
4203
4203
|
coll.flow = true;
|
|
4204
4204
|
const atRoot = ctx.atRoot;
|
|
@@ -4234,7 +4234,7 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
4234
4234
|
offset = props.end;
|
|
4235
4235
|
continue;
|
|
4236
4236
|
}
|
|
4237
|
-
if (!
|
|
4237
|
+
if (!isMap2 && ctx.options.strict && utilContainsNewline.containsNewline(key))
|
|
4238
4238
|
onError(
|
|
4239
4239
|
key,
|
|
4240
4240
|
// checked by containsNewline()
|
|
@@ -4274,7 +4274,7 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
4274
4274
|
}
|
|
4275
4275
|
}
|
|
4276
4276
|
}
|
|
4277
|
-
if (!
|
|
4277
|
+
if (!isMap2 && !sep && !props.found) {
|
|
4278
4278
|
const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep, null, props, onError);
|
|
4279
4279
|
coll.items.push(valueNode);
|
|
4280
4280
|
offset = valueNode.range[2];
|
|
@@ -4297,7 +4297,7 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
4297
4297
|
startOnNewline: false
|
|
4298
4298
|
});
|
|
4299
4299
|
if (valueProps.found) {
|
|
4300
|
-
if (!
|
|
4300
|
+
if (!isMap2 && !props.found && ctx.options.strict) {
|
|
4301
4301
|
if (sep)
|
|
4302
4302
|
for (const st of sep) {
|
|
4303
4303
|
if (st === valueProps.found)
|
|
@@ -4329,7 +4329,7 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
4329
4329
|
const pair = new Pair.Pair(keyNode, valueNode);
|
|
4330
4330
|
if (ctx.options.keepSourceTokens)
|
|
4331
4331
|
pair.srcToken = collItem;
|
|
4332
|
-
if (
|
|
4332
|
+
if (isMap2) {
|
|
4333
4333
|
const map = coll;
|
|
4334
4334
|
if (utilMapIncludes.mapIncludes(ctx, map.items, keyNode))
|
|
4335
4335
|
onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique");
|
|
@@ -4345,7 +4345,7 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
4345
4345
|
offset = valueNode ? valueNode.range[2] : valueProps.end;
|
|
4346
4346
|
}
|
|
4347
4347
|
}
|
|
4348
|
-
const expectedEnd =
|
|
4348
|
+
const expectedEnd = isMap2 ? "}" : "]";
|
|
4349
4349
|
const [ce, ...ee] = fc.end;
|
|
4350
4350
|
let cePos = offset;
|
|
4351
4351
|
if (ce?.source === expectedEnd)
|
|
@@ -5565,15 +5565,15 @@ var require_cst_visit = __commonJS({
|
|
|
5565
5565
|
var BREAK = /* @__PURE__ */ Symbol("break visit");
|
|
5566
5566
|
var SKIP = /* @__PURE__ */ Symbol("skip children");
|
|
5567
5567
|
var REMOVE = /* @__PURE__ */ Symbol("remove item");
|
|
5568
|
-
function
|
|
5568
|
+
function visit2(cst, visitor) {
|
|
5569
5569
|
if ("type" in cst && cst.type === "document")
|
|
5570
5570
|
cst = { start: cst.start, value: cst.value };
|
|
5571
5571
|
_visit(Object.freeze([]), cst, visitor);
|
|
5572
5572
|
}
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5573
|
+
visit2.BREAK = BREAK;
|
|
5574
|
+
visit2.SKIP = SKIP;
|
|
5575
|
+
visit2.REMOVE = REMOVE;
|
|
5576
|
+
visit2.itemAtPath = (cst, path) => {
|
|
5577
5577
|
let item = cst;
|
|
5578
5578
|
for (const [field, index] of path) {
|
|
5579
5579
|
const tok = item?.[field];
|
|
@@ -5584,8 +5584,8 @@ var require_cst_visit = __commonJS({
|
|
|
5584
5584
|
}
|
|
5585
5585
|
return item;
|
|
5586
5586
|
};
|
|
5587
|
-
|
|
5588
|
-
const parent =
|
|
5587
|
+
visit2.parentCollection = (cst, path) => {
|
|
5588
|
+
const parent = visit2.itemAtPath(cst, path.slice(0, -1));
|
|
5589
5589
|
const field = path[path.length - 1][0];
|
|
5590
5590
|
const coll = parent?.[field];
|
|
5591
5591
|
if (coll && "items" in coll)
|
|
@@ -5616,7 +5616,7 @@ var require_cst_visit = __commonJS({
|
|
|
5616
5616
|
}
|
|
5617
5617
|
return typeof ctrl === "function" ? ctrl(item, path) : ctrl;
|
|
5618
5618
|
}
|
|
5619
|
-
exports.visit =
|
|
5619
|
+
exports.visit = visit2;
|
|
5620
5620
|
}
|
|
5621
5621
|
});
|
|
5622
5622
|
|
|
@@ -5632,7 +5632,7 @@ var require_cst = __commonJS({
|
|
|
5632
5632
|
var FLOW_END = "";
|
|
5633
5633
|
var SCALAR = "";
|
|
5634
5634
|
var isCollection = (token) => !!token && "items" in token;
|
|
5635
|
-
var
|
|
5635
|
+
var isScalar2 = (token) => !!token && (token.type === "scalar" || token.type === "single-quoted-scalar" || token.type === "double-quoted-scalar" || token.type === "block-scalar");
|
|
5636
5636
|
function prettyToken(token) {
|
|
5637
5637
|
switch (token) {
|
|
5638
5638
|
case BOM:
|
|
@@ -5716,7 +5716,7 @@ var require_cst = __commonJS({
|
|
|
5716
5716
|
exports.FLOW_END = FLOW_END;
|
|
5717
5717
|
exports.SCALAR = SCALAR;
|
|
5718
5718
|
exports.isCollection = isCollection;
|
|
5719
|
-
exports.isScalar =
|
|
5719
|
+
exports.isScalar = isScalar2;
|
|
5720
5720
|
exports.prettyToken = prettyToken;
|
|
5721
5721
|
exports.tokenType = tokenType;
|
|
5722
5722
|
}
|
|
@@ -7246,7 +7246,7 @@ var require_public_api = __commonJS({
|
|
|
7246
7246
|
return docs;
|
|
7247
7247
|
return Object.assign([], { empty: true }, composer$1.streamInfo());
|
|
7248
7248
|
}
|
|
7249
|
-
function
|
|
7249
|
+
function parseDocument2(source, options = {}) {
|
|
7250
7250
|
const { lineCounter: lineCounter2, prettyErrors } = parseOptions(options);
|
|
7251
7251
|
const parser$1 = new parser.Parser(lineCounter2?.addNewLine);
|
|
7252
7252
|
const composer$1 = new composer.Composer(options);
|
|
@@ -7272,7 +7272,7 @@ var require_public_api = __commonJS({
|
|
|
7272
7272
|
} else if (options === void 0 && reviver && typeof reviver === "object") {
|
|
7273
7273
|
options = reviver;
|
|
7274
7274
|
}
|
|
7275
|
-
const doc =
|
|
7275
|
+
const doc = parseDocument2(src, options);
|
|
7276
7276
|
if (!doc)
|
|
7277
7277
|
return null;
|
|
7278
7278
|
doc.warnings.forEach((warning) => log.warn(doc.options.logLevel, warning));
|
|
@@ -7308,7 +7308,7 @@ var require_public_api = __commonJS({
|
|
|
7308
7308
|
}
|
|
7309
7309
|
exports.parse = parse;
|
|
7310
7310
|
exports.parseAllDocuments = parseAllDocuments;
|
|
7311
|
-
exports.parseDocument =
|
|
7311
|
+
exports.parseDocument = parseDocument2;
|
|
7312
7312
|
exports.stringify = stringify;
|
|
7313
7313
|
}
|
|
7314
7314
|
});
|
|
@@ -7332,7 +7332,7 @@ var require_dist = __commonJS({
|
|
|
7332
7332
|
var lineCounter = require_line_counter();
|
|
7333
7333
|
var parser = require_parser();
|
|
7334
7334
|
var publicApi = require_public_api();
|
|
7335
|
-
var
|
|
7335
|
+
var visit2 = require_visit();
|
|
7336
7336
|
exports.Composer = composer.Composer;
|
|
7337
7337
|
exports.Document = Document.Document;
|
|
7338
7338
|
exports.Schema = Schema.Schema;
|
|
@@ -7360,8 +7360,8 @@ var require_dist = __commonJS({
|
|
|
7360
7360
|
exports.parseAllDocuments = publicApi.parseAllDocuments;
|
|
7361
7361
|
exports.parseDocument = publicApi.parseDocument;
|
|
7362
7362
|
exports.stringify = publicApi.stringify;
|
|
7363
|
-
exports.visit =
|
|
7364
|
-
exports.visitAsync =
|
|
7363
|
+
exports.visit = visit2.visit;
|
|
7364
|
+
exports.visitAsync = visit2.visitAsync;
|
|
7365
7365
|
}
|
|
7366
7366
|
});
|
|
7367
7367
|
|
|
@@ -7433,7 +7433,7 @@ async function dockerAccessGuidance(s) {
|
|
|
7433
7433
|
const already = groups.trim().split(/\s+/).includes("docker");
|
|
7434
7434
|
return [
|
|
7435
7435
|
"This tool never uses sudo and never escalates privileges itself.",
|
|
7436
|
-
"Open firewall / cloud security group ports: 8388/tcp, 8388/udp, 8443/tcp
|
|
7436
|
+
"Open firewall / cloud security group ports: 8388/tcp, 8388/udp, 8443/tcp. ISP routing uses the same listening ports.",
|
|
7437
7437
|
...already ? [
|
|
7438
7438
|
"The Docker daemon may not be running, or your session predates the group change. Log out and back in (or run `newgrp docker`)."
|
|
7439
7439
|
] : [
|
|
@@ -7838,20 +7838,20 @@ function buildModel(env, opts = {}) {
|
|
|
7838
7838
|
publicKey,
|
|
7839
7839
|
shortId: v.REALITY_SHORT_ID
|
|
7840
7840
|
};
|
|
7841
|
-
const node = (name, protocol, tag, port
|
|
7841
|
+
const node = (name, protocol, tag, port) => ({
|
|
7842
7842
|
name,
|
|
7843
7843
|
protocol,
|
|
7844
7844
|
inbound: { tag, port },
|
|
7845
|
-
egress,
|
|
7846
7845
|
creds: protocol === "shadowsocks" ? ss : reality
|
|
7847
7846
|
});
|
|
7848
7847
|
const model = {
|
|
7849
7848
|
server,
|
|
7850
7849
|
nodes: [
|
|
7851
|
-
node(NODE_SS, "shadowsocks", SS_TAG, SS_PORT
|
|
7852
|
-
node(NODE_VLESS, "vless-reality", VLESS_TAG, VLESS_PORT
|
|
7850
|
+
node(NODE_SS, "shadowsocks", SS_TAG, SS_PORT),
|
|
7851
|
+
node(NODE_VLESS, "vless-reality", VLESS_TAG, VLESS_PORT)
|
|
7853
7852
|
]
|
|
7854
7853
|
};
|
|
7854
|
+
if (target === "client") return model;
|
|
7855
7855
|
const {
|
|
7856
7856
|
ISP_SOCKS_ADDR: rawAddr = "",
|
|
7857
7857
|
ISP_SOCKS_USER: rawUser = "",
|
|
@@ -7874,16 +7874,6 @@ function buildModel(env, opts = {}) {
|
|
|
7874
7874
|
pass,
|
|
7875
7875
|
...ip ? { egressIp: ip } : {}
|
|
7876
7876
|
};
|
|
7877
|
-
model.nodes.push(
|
|
7878
|
-
node(NODE_SS_ISP, "shadowsocks", SS_ISP_TAG, SS_ISP_PORT, "isp-socks"),
|
|
7879
|
-
node(
|
|
7880
|
-
NODE_VLESS_ISP,
|
|
7881
|
-
"vless-reality",
|
|
7882
|
-
VLESS_ISP_TAG,
|
|
7883
|
-
VLESS_ISP_PORT,
|
|
7884
|
-
"isp-socks"
|
|
7885
|
-
)
|
|
7886
|
-
);
|
|
7887
7877
|
return model;
|
|
7888
7878
|
}
|
|
7889
7879
|
|
|
@@ -7916,6 +7906,64 @@ function readTemplate(name, projectDir) {
|
|
|
7916
7906
|
return readFileSync2(findTemplate(name, projectDir), "utf8");
|
|
7917
7907
|
}
|
|
7918
7908
|
|
|
7909
|
+
// src/core/routing.ts
|
|
7910
|
+
function isRecord(value) {
|
|
7911
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
7912
|
+
}
|
|
7913
|
+
function isIspRule(value) {
|
|
7914
|
+
return isRecord(value) && value.outbound === OUT_TAG;
|
|
7915
|
+
}
|
|
7916
|
+
function isLegacyIspRule(value) {
|
|
7917
|
+
if (!isIspRule(value)) return false;
|
|
7918
|
+
const inbound = typeof value.inbound === "string" ? [value.inbound] : value.inbound;
|
|
7919
|
+
return Array.isArray(inbound) && inbound.length > 0 && inbound.every((tag) => tag === SS_ISP_TAG || tag === VLESS_ISP_TAG) && (value.action === void 0 || value.action === "route") && Object.keys(value).every((key) => ["inbound", "outbound", "action"].includes(key));
|
|
7920
|
+
}
|
|
7921
|
+
function checkNoIspReferences(config) {
|
|
7922
|
+
const referenceKeys = /* @__PURE__ */ new Set(["outbound", "outbounds", "detour", "final", "default"]);
|
|
7923
|
+
const walk = (value, path, key = "") => {
|
|
7924
|
+
if (value === OUT_TAG && referenceKeys.has(key))
|
|
7925
|
+
throw new Error(`Unsupported ISP reference at ${path}. Remove this reference before disabling ISP.`);
|
|
7926
|
+
if (Array.isArray(value))
|
|
7927
|
+
value.forEach((item, i) => walk(item, `${path}[${i}]`, key));
|
|
7928
|
+
else if (isRecord(value))
|
|
7929
|
+
for (const [name, item] of Object.entries(value)) walk(item, `${path}.${name}`, name);
|
|
7930
|
+
};
|
|
7931
|
+
walk(config, "config");
|
|
7932
|
+
}
|
|
7933
|
+
function upsertIspOutbound(config, isp) {
|
|
7934
|
+
const outbounds = config.outbounds ??= [];
|
|
7935
|
+
const matches = outbounds.filter((o) => o.tag === OUT_TAG);
|
|
7936
|
+
if (matches.length > 1) throw new Error(`Duplicate outbound tag: ${OUT_TAG}.`);
|
|
7937
|
+
const fields = {
|
|
7938
|
+
type: "socks",
|
|
7939
|
+
tag: OUT_TAG,
|
|
7940
|
+
server: isp.host,
|
|
7941
|
+
server_port: isp.port,
|
|
7942
|
+
version: "5",
|
|
7943
|
+
username: isp.user,
|
|
7944
|
+
password: isp.pass
|
|
7945
|
+
};
|
|
7946
|
+
if (matches[0]) {
|
|
7947
|
+
if (matches[0].type !== "socks")
|
|
7948
|
+
throw new Error(`Outbound ${OUT_TAG} must have type socks.`);
|
|
7949
|
+
Object.assign(matches[0], fields);
|
|
7950
|
+
} else outbounds.push(fields);
|
|
7951
|
+
}
|
|
7952
|
+
function omitIsp(config) {
|
|
7953
|
+
if (config.outbounds) config.outbounds = config.outbounds.filter((o) => o.tag !== OUT_TAG);
|
|
7954
|
+
if (config.route?.rules) config.route.rules = config.route.rules.filter((r) => !isIspRule(r));
|
|
7955
|
+
checkNoIspReferences(config);
|
|
7956
|
+
}
|
|
7957
|
+
function ensureRoute(config, defaults) {
|
|
7958
|
+
const route = config.route ??= {};
|
|
7959
|
+
const rules = route.rules ??= [];
|
|
7960
|
+
if (!rules.some((r) => isRecord(r) && r.action === "sniff"))
|
|
7961
|
+
rules.unshift({ action: "sniff" });
|
|
7962
|
+
if (!rules.some(isIspRule) && defaults?.rules)
|
|
7963
|
+
rules.push(...structuredClone(defaults.rules.filter(isIspRule)));
|
|
7964
|
+
route.final ??= "direct";
|
|
7965
|
+
}
|
|
7966
|
+
|
|
7919
7967
|
// src/core/render-config.ts
|
|
7920
7968
|
function renderSingbox(model, template) {
|
|
7921
7969
|
const ss = model.nodes.find((n) => n.protocol === "shadowsocks").creds;
|
|
@@ -7933,7 +7981,7 @@ function renderSingbox(model, template) {
|
|
|
7933
7981
|
const prototypes = /* @__PURE__ */ new Map();
|
|
7934
7982
|
for (const protocol of ["shadowsocks", "vless-reality"]) {
|
|
7935
7983
|
const base = model.nodes.find(
|
|
7936
|
-
(n) => n.protocol === protocol
|
|
7984
|
+
(n) => n.protocol === protocol
|
|
7937
7985
|
);
|
|
7938
7986
|
const proto = config.inbounds.find(
|
|
7939
7987
|
(i) => i.type === (protocol === "shadowsocks" ? "shadowsocks" : "vless")
|
|
@@ -7947,27 +7995,9 @@ function renderSingbox(model, template) {
|
|
|
7947
7995
|
tag: n.inbound.tag,
|
|
7948
7996
|
listen_port: n.inbound.port
|
|
7949
7997
|
}));
|
|
7950
|
-
|
|
7951
|
-
if (
|
|
7952
|
-
|
|
7953
|
-
config.outbounds = [
|
|
7954
|
-
...config.outbounds ?? [],
|
|
7955
|
-
{
|
|
7956
|
-
type: "socks",
|
|
7957
|
-
tag: OUT_TAG,
|
|
7958
|
-
server: isp.host,
|
|
7959
|
-
server_port: isp.port,
|
|
7960
|
-
version: "5",
|
|
7961
|
-
username: isp.user,
|
|
7962
|
-
password: isp.pass
|
|
7963
|
-
}
|
|
7964
|
-
];
|
|
7965
|
-
config.route ??= {};
|
|
7966
|
-
config.route.rules = [
|
|
7967
|
-
...config.route.rules ?? [],
|
|
7968
|
-
{ inbound: ispNodes.map((n) => n.inbound.tag), outbound: OUT_TAG }
|
|
7969
|
-
];
|
|
7970
|
-
}
|
|
7998
|
+
ensureRoute(config);
|
|
7999
|
+
if (model.isp) upsertIspOutbound(config, model.isp);
|
|
8000
|
+
else omitIsp(config);
|
|
7971
8001
|
if (JSON.stringify(config).includes("${"))
|
|
7972
8002
|
throw new Error("Unresolved placeholder left in config.json.");
|
|
7973
8003
|
return config;
|
|
@@ -8015,11 +8045,9 @@ function renderClash(model, templateText) {
|
|
|
8015
8045
|
}
|
|
8016
8046
|
proxies.items = model.nodes.map((n) => {
|
|
8017
8047
|
const proto = prototypes.get(n.protocol);
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
clone.set("port", n.inbound.port);
|
|
8022
|
-
return clone;
|
|
8048
|
+
proto.set("name", n.name);
|
|
8049
|
+
proto.set("port", n.inbound.port);
|
|
8050
|
+
return proto;
|
|
8023
8051
|
});
|
|
8024
8052
|
const group = groups.items.find((g) => (0, import_yaml.isMap)(g) && g.get("name") === "Proxy");
|
|
8025
8053
|
const list = (0, import_yaml.isMap)(group) ? group.get("proxies", true) : null;
|
|
@@ -8038,19 +8066,154 @@ function renderClash(model, templateText) {
|
|
|
8038
8066
|
// src/core/isp-service.ts
|
|
8039
8067
|
import { spawn } from "node:child_process";
|
|
8040
8068
|
import { createConnection } from "node:net";
|
|
8069
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
8070
|
+
import { join as join5 } from "node:path";
|
|
8071
|
+
|
|
8072
|
+
// src/core/start.ts
|
|
8073
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
8074
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
8075
|
+
import { connect } from "node:net";
|
|
8076
|
+
import { join as join3 } from "node:path";
|
|
8077
|
+
function parseListenPorts(configText) {
|
|
8078
|
+
const ports = /* @__PURE__ */ new Set();
|
|
8079
|
+
for (const m of configText.matchAll(/"listen_port"\s*:\s*([0-9]+)/g)) {
|
|
8080
|
+
const port = Number(m[1]);
|
|
8081
|
+
if (port < 1 || port > 65535)
|
|
8082
|
+
throw new Error(`Invalid listen_port in config.json: ${m[1]}`);
|
|
8083
|
+
ports.add(port);
|
|
8084
|
+
}
|
|
8085
|
+
if (!ports.size) throw new Error("config.json has no listen_port.");
|
|
8086
|
+
return [...ports];
|
|
8087
|
+
}
|
|
8088
|
+
function secretValues(envText) {
|
|
8089
|
+
return [...envText.matchAll(/^(?:REALITY_PRIVATE_KEY|SS_PASSWORD|VLESS_UUID|ISP_SOCKS_USER|ISP_SOCKS_PASS)=([^\r\n]*)\r?$/gm)].map((m) => envUnquote(m[1])).filter((value) => value !== null && value !== "");
|
|
8090
|
+
}
|
|
8091
|
+
function redact(text, secrets) {
|
|
8092
|
+
for (const secret of [...secrets].sort((a, b) => b.length - a.length))
|
|
8093
|
+
text = text.replaceAll(secret, "***");
|
|
8094
|
+
return text;
|
|
8095
|
+
}
|
|
8096
|
+
function composeImageTag(text) {
|
|
8097
|
+
const m = /^\s*image:\s*ghcr\.io\/sagernet\/sing-box:([^\s#]+)/m.exec(text);
|
|
8098
|
+
if (!m) throw new Error("sing-box image tag not found in compose.yaml.");
|
|
8099
|
+
return m[1];
|
|
8100
|
+
}
|
|
8101
|
+
function firewallPorts(configText) {
|
|
8102
|
+
const config = JSON.parse(configText);
|
|
8103
|
+
return config.inbounds.flatMap(
|
|
8104
|
+
(i) => i.type === "shadowsocks" ? [`${i.listen_port}/tcp`, `${i.listen_port}/udp`] : [`${i.listen_port}/tcp`]
|
|
8105
|
+
);
|
|
8106
|
+
}
|
|
8107
|
+
function realStartDeps() {
|
|
8108
|
+
return {
|
|
8109
|
+
run: (file, args) => new Promise((resolve) => {
|
|
8110
|
+
execFile2(file, args, (error, stdout, stderr) => {
|
|
8111
|
+
const code = error ? typeof error.code === "number" ? error.code : error.code === "ENOENT" ? 127 : 1 : 0;
|
|
8112
|
+
resolve({ code, stdout, stderr });
|
|
8113
|
+
});
|
|
8114
|
+
}),
|
|
8115
|
+
tcpOpen: (port) => new Promise((resolve) => {
|
|
8116
|
+
const socket = connect(port, "127.0.0.1");
|
|
8117
|
+
const finish = (open) => {
|
|
8118
|
+
socket.destroy();
|
|
8119
|
+
resolve(open);
|
|
8120
|
+
};
|
|
8121
|
+
socket.setTimeout(500, () => finish(false));
|
|
8122
|
+
socket.once("connect", () => finish(true));
|
|
8123
|
+
socket.once("error", () => finish(false));
|
|
8124
|
+
}),
|
|
8125
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
8126
|
+
};
|
|
8127
|
+
}
|
|
8128
|
+
async function startNode(opts) {
|
|
8129
|
+
const { project, deps } = opts;
|
|
8130
|
+
if (!existsSync2(join3(project, "compose.yaml")))
|
|
8131
|
+
throw new Error("compose.yaml does not exist.");
|
|
8132
|
+
if (!existsSync2(join3(project, "config.json")))
|
|
8133
|
+
throw new Error("config.json does not exist. Run my-singbox gen-config first.");
|
|
8134
|
+
const ports = parseListenPorts(readFileSync3(join3(project, "config.json"), "utf8"));
|
|
8135
|
+
if (!await opts.dockerAvailable()) throw new Error("docker is not installed.");
|
|
8136
|
+
if ((await deps.run("docker", ["compose", "version"])).code)
|
|
8137
|
+
throw new Error("docker compose is not available.");
|
|
8138
|
+
if (!await opts.dockerDaemonUp()) throw new Error("Docker daemon is not running.");
|
|
8139
|
+
const envPath = join3(project, ".env");
|
|
8140
|
+
const secrets = secretValues(existsSync2(envPath) ? readFileSync3(envPath, "utf8") : "");
|
|
8141
|
+
const compose = (...args) => deps.run("docker", ["compose", "--project-directory", project, ...args]);
|
|
8142
|
+
const forward = (output) => {
|
|
8143
|
+
opts.stdout(redact(output.stdout, secrets));
|
|
8144
|
+
opts.stderr(redact(output.stderr, secrets));
|
|
8145
|
+
};
|
|
8146
|
+
const fail = async (message) => {
|
|
8147
|
+
const logs = await compose("logs", "--tail", "50", "sing-box");
|
|
8148
|
+
opts.stderr(redact(logs.stdout, secrets));
|
|
8149
|
+
opts.stderr(redact(logs.stderr, secrets));
|
|
8150
|
+
throw new Error(redact(message, secrets));
|
|
8151
|
+
};
|
|
8152
|
+
const status = async () => {
|
|
8153
|
+
const output = await deps.run("docker", ["inspect", "-f", "{{.State.Status}}", "sing-box"]);
|
|
8154
|
+
return output.code ? "missing" : output.stdout.trim();
|
|
8155
|
+
};
|
|
8156
|
+
opts.stdout("Starting sing-box...\n");
|
|
8157
|
+
const up = await compose("up", "-d");
|
|
8158
|
+
forward(up);
|
|
8159
|
+
if (up.code) throw new Error("docker compose up failed.");
|
|
8160
|
+
let state = "missing";
|
|
8161
|
+
for (let i = 0; i < 15; i++) {
|
|
8162
|
+
state = await status();
|
|
8163
|
+
if (state === "running") break;
|
|
8164
|
+
await deps.sleep(1e3);
|
|
8165
|
+
}
|
|
8166
|
+
if (state !== "running") await fail(`sing-box did not stay running (status: ${state}).`);
|
|
8167
|
+
const check = await compose("exec", "-T", "sing-box", "sing-box", "check", "-c", "/etc/sing-box/config.json");
|
|
8168
|
+
forward(check);
|
|
8169
|
+
if (check.code) await fail("sing-box config check failed.");
|
|
8170
|
+
for (const port of ports) {
|
|
8171
|
+
let open = false;
|
|
8172
|
+
for (let i = 0; i < 10; i++) {
|
|
8173
|
+
open = await deps.tcpOpen(port);
|
|
8174
|
+
if (open) break;
|
|
8175
|
+
await deps.sleep(1e3);
|
|
8176
|
+
}
|
|
8177
|
+
if (!open) await fail(`Port ${port} is not accepting connections.`);
|
|
8178
|
+
}
|
|
8179
|
+
if (ports.includes(8388)) {
|
|
8180
|
+
let listening = false;
|
|
8181
|
+
for (let i = 0; i < 10; i++) {
|
|
8182
|
+
const udp = await deps.run("ss", ["-uln"]);
|
|
8183
|
+
if (udp.code === 127) {
|
|
8184
|
+
opts.stderr("Warning: ss is not installed. Skipping UDP listener check for port 8388.\n");
|
|
8185
|
+
listening = true;
|
|
8186
|
+
break;
|
|
8187
|
+
}
|
|
8188
|
+
if (udp.code === 0 && /:8388\s/.test(udp.stdout)) {
|
|
8189
|
+
const owners = await deps.run("ss", ["-ulnp"]);
|
|
8190
|
+
const lines = owners.stdout.split("\n").filter((l) => /:8388\s/.test(l)).join("\n");
|
|
8191
|
+
if (lines.includes("users:") && !lines.includes("sing-box"))
|
|
8192
|
+
await fail("UDP port 8388 is not owned by sing-box.");
|
|
8193
|
+
listening = true;
|
|
8194
|
+
break;
|
|
8195
|
+
}
|
|
8196
|
+
await deps.sleep(1e3);
|
|
8197
|
+
}
|
|
8198
|
+
if (!listening) await fail("UDP port 8388 is not listening.");
|
|
8199
|
+
}
|
|
8200
|
+
state = await status();
|
|
8201
|
+
if (state !== "running") await fail(`sing-box did not stay running (status: ${state}).`);
|
|
8202
|
+
opts.stdout(`Self-check passed. Ports: ${ports.join(" ")}
|
|
8203
|
+
`);
|
|
8204
|
+
}
|
|
8041
8205
|
|
|
8042
8206
|
// src/core/isp.ts
|
|
8043
|
-
var import_yaml2 = __toESM(require_dist(), 1);
|
|
8044
8207
|
import {
|
|
8045
8208
|
chmodSync,
|
|
8046
8209
|
chownSync,
|
|
8047
8210
|
closeSync as closeSync2,
|
|
8048
8211
|
copyFileSync,
|
|
8049
|
-
existsSync as
|
|
8212
|
+
existsSync as existsSync3,
|
|
8050
8213
|
fchmodSync as fchmodSync2,
|
|
8051
8214
|
mkdirSync,
|
|
8052
8215
|
openSync as openSync2,
|
|
8053
|
-
readFileSync as
|
|
8216
|
+
readFileSync as readFileSync4,
|
|
8054
8217
|
readdirSync,
|
|
8055
8218
|
renameSync as renameSync2,
|
|
8056
8219
|
rmSync as rmSync2,
|
|
@@ -8058,14 +8221,13 @@ import {
|
|
|
8058
8221
|
utimesSync,
|
|
8059
8222
|
writeFileSync as writeFileSync2
|
|
8060
8223
|
} from "node:fs";
|
|
8061
|
-
import { join as
|
|
8224
|
+
import { join as join4 } from "node:path";
|
|
8062
8225
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
8063
|
-
import { isDeepStrictEqual } from "node:util";
|
|
8064
8226
|
var ISP_BACKUP_DIR = ".isp-backup";
|
|
8065
8227
|
var ISP_LOCK_FILE = ".isp.lock";
|
|
8066
8228
|
var ISP_TMP_PREFIX = ".isp-tmp.";
|
|
8067
8229
|
var GROUP_MAIN = "Proxy";
|
|
8068
|
-
var MANAGED_FILES = ["config.json", "
|
|
8230
|
+
var MANAGED_FILES = ["config.json", ".env"];
|
|
8069
8231
|
var IspError = class extends Error {
|
|
8070
8232
|
name = "IspError";
|
|
8071
8233
|
};
|
|
@@ -8078,116 +8240,63 @@ function envHasIsp(text) {
|
|
|
8078
8240
|
return /^ISP_SOCKS_[^=]*=/.test(line) || line === ENV_HEADER || line === ENV_HEADER_NOEOL;
|
|
8079
8241
|
});
|
|
8080
8242
|
}
|
|
8081
|
-
function isRecord(value) {
|
|
8082
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8083
|
-
}
|
|
8084
8243
|
function checkIspConfig(config) {
|
|
8085
8244
|
if (!isRecord(config)) throw new IspError("config.json is not valid JSON.");
|
|
8086
8245
|
const inbounds = config.inbounds;
|
|
8087
|
-
if (!Array.isArray(inbounds) || ![SS_TAG, VLESS_TAG].every((tag) => inbounds.some((i) =>
|
|
8246
|
+
if (!Array.isArray(inbounds) || !inbounds.every(isRecord) || ![SS_TAG, VLESS_TAG].every((tag) => inbounds.some((i) => i.tag === tag)))
|
|
8088
8247
|
throw new IspError("config.json has no inbounds tagged ss-in and vless-reality-in.");
|
|
8089
|
-
if (
|
|
8090
|
-
|
|
8248
|
+
if (config.outbounds !== void 0 && (!Array.isArray(config.outbounds) || !config.outbounds.every(isRecord))) throw new IspError("config.json has invalid outbounds.");
|
|
8249
|
+
if (config.route !== void 0 && (!isRecord(config.route) || config.route.rules !== void 0 && !Array.isArray(config.route.rules)))
|
|
8250
|
+
throw new IspError("config.json has invalid route rules.");
|
|
8091
8251
|
}
|
|
8092
8252
|
function configHasIsp(config) {
|
|
8093
8253
|
return [...config.inbounds ?? [], ...config.outbounds ?? []].some(
|
|
8094
8254
|
(i) => isRecord(i) && [SS_ISP_TAG, VLESS_ISP_TAG, OUT_TAG].includes(i.tag)
|
|
8095
|
-
);
|
|
8255
|
+
) || (config.route?.rules ?? []).some((r) => isRecord(r) && r.outbound === OUT_TAG);
|
|
8096
8256
|
}
|
|
8097
|
-
function
|
|
8098
|
-
checkIspConfig(config);
|
|
8257
|
+
function withoutLegacyListeners(config) {
|
|
8099
8258
|
const next = structuredClone(config);
|
|
8100
|
-
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
8106
|
-
|
|
8107
|
-
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
|
|
8111
|
-
|
|
8112
|
-
|
|
8113
|
-
|
|
8114
|
-
|
|
8115
|
-
|
|
8116
|
-
|
|
8117
|
-
|
|
8118
|
-
|
|
8119
|
-
|
|
8120
|
-
|
|
8121
|
-
|
|
8259
|
+
const tags = /* @__PURE__ */ new Set([SS_ISP_TAG, VLESS_ISP_TAG]);
|
|
8260
|
+
next.inbounds = next.inbounds.filter((inbound) => {
|
|
8261
|
+
if (!tags.has(inbound.tag)) return true;
|
|
8262
|
+
const ss = inbound.tag === SS_ISP_TAG;
|
|
8263
|
+
if (inbound.type !== (ss ? "shadowsocks" : "vless") || inbound.listen_port !== (ss ? SS_ISP_PORT : VLESS_ISP_PORT))
|
|
8264
|
+
throw new IspError("Legacy ISP inbound was customized. Migrate its tag and references manually first.");
|
|
8265
|
+
return false;
|
|
8266
|
+
});
|
|
8267
|
+
if (next.route?.rules) next.route.rules = next.route.rules.flatMap((rule) => {
|
|
8268
|
+
if (!isRecord(rule)) return [rule];
|
|
8269
|
+
const inbound = typeof rule.inbound === "string" ? [rule.inbound] : rule.inbound;
|
|
8270
|
+
if (!Array.isArray(inbound) || !inbound.some((tag) => tags.has(tag))) return [rule];
|
|
8271
|
+
const remaining = inbound.filter((tag) => !tags.has(tag));
|
|
8272
|
+
if (remaining.length) return [{ ...rule, inbound: remaining }];
|
|
8273
|
+
if (isLegacyIspRule(rule)) return [];
|
|
8274
|
+
throw new IspError("A custom route depends on legacy ISP inbounds. Migrate it manually first.");
|
|
8275
|
+
});
|
|
8276
|
+
const check = (value, key = "") => {
|
|
8277
|
+
if (typeof value === "string" && tags.has(value) && ["inbound", "detour"].includes(key))
|
|
8278
|
+
throw new IspError("A custom reference depends on legacy ISP inbounds. Migrate it manually first.");
|
|
8279
|
+
if (Array.isArray(value)) value.forEach((item) => check(item, key));
|
|
8280
|
+
else if (isRecord(value)) Object.entries(value).forEach(([name, item]) => check(item, name));
|
|
8122
8281
|
};
|
|
8123
|
-
|
|
8124
|
-
else outbounds.push(fields);
|
|
8125
|
-
const rules = (next.route ??= {}).rules ??= [];
|
|
8126
|
-
const rule = rules.find((r) => isRecord(r) && r.outbound === OUT_TAG);
|
|
8127
|
-
const inbound = [SS_ISP_TAG, VLESS_ISP_TAG];
|
|
8128
|
-
if (isRecord(rule)) {
|
|
8129
|
-
if (!isDeepStrictEqual(rule.inbound, inbound)) rule.inbound = inbound;
|
|
8130
|
-
} else rules.push({ inbound, outbound: OUT_TAG });
|
|
8282
|
+
check(next);
|
|
8131
8283
|
return next;
|
|
8132
8284
|
}
|
|
8133
|
-
function
|
|
8134
|
-
|
|
8135
|
-
const
|
|
8136
|
-
|
|
8137
|
-
|
|
8285
|
+
function addIspToConfig(config, isp, routeDefaults) {
|
|
8286
|
+
checkIspConfig(config);
|
|
8287
|
+
const next = withoutLegacyListeners(config);
|
|
8288
|
+
upsertIspOutbound(next, isp);
|
|
8289
|
+
ensureRoute(next, routeDefaults);
|
|
8290
|
+
return next;
|
|
8138
8291
|
}
|
|
8139
|
-
function
|
|
8140
|
-
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
if (!(0, import_yaml2.isSeq)(proxies)) throw new IspError(missing);
|
|
8145
|
-
let changed = false;
|
|
8146
|
-
for (const [base, name, basePort, port] of [
|
|
8147
|
-
[NODE_SS, NODE_SS_ISP, SS_PORT, SS_ISP_PORT],
|
|
8148
|
-
[NODE_VLESS, NODE_VLESS_ISP, VLESS_PORT, VLESS_ISP_PORT]
|
|
8149
|
-
]) {
|
|
8150
|
-
const proto = proxies.items.find((p) => (0, import_yaml2.isMap)(p) && p.get("name") === base);
|
|
8151
|
-
if (!(0, import_yaml2.isMap)(proto) || proto.get("port") !== basePort) throw new IspError(missing);
|
|
8152
|
-
const existing = proxies.items.find((p) => (0, import_yaml2.isMap)(p) && p.get("name") === name);
|
|
8153
|
-
if ((0, import_yaml2.isMap)(existing)) {
|
|
8154
|
-
if (existing.get("port") !== port) {
|
|
8155
|
-
existing.set("port", port);
|
|
8156
|
-
changed = true;
|
|
8157
|
-
}
|
|
8158
|
-
} else {
|
|
8159
|
-
const clone = proto.clone();
|
|
8160
|
-
(0, import_yaml2.visit)(clone, (_key, node) => {
|
|
8161
|
-
if ((0, import_yaml2.isMap)(node) || (0, import_yaml2.isSeq)(node) || (0, import_yaml2.isScalar)(node)) {
|
|
8162
|
-
delete node.comment;
|
|
8163
|
-
delete node.commentBefore;
|
|
8164
|
-
delete node.spaceBefore;
|
|
8165
|
-
}
|
|
8166
|
-
});
|
|
8167
|
-
clone.set("name", name);
|
|
8168
|
-
clone.set("port", port);
|
|
8169
|
-
proxies.add(clone);
|
|
8170
|
-
changed = true;
|
|
8171
|
-
}
|
|
8172
|
-
}
|
|
8173
|
-
const groups = doc.get("proxy-groups", true);
|
|
8174
|
-
const group = (0, import_yaml2.isSeq)(groups) ? groups.items.find((g) => (0, import_yaml2.isMap)(g) && g.get("name") === GROUP_MAIN) : void 0;
|
|
8175
|
-
const list = (0, import_yaml2.isMap)(group) ? group.get("proxies", true) : void 0;
|
|
8176
|
-
if (!(0, import_yaml2.isSeq)(list))
|
|
8177
|
-
throw new IspError(`clash.yaml has no '${GROUP_MAIN}' proxy group with a 'proxies:' list in the expected format (generated by my-singbox conf-clash). Regenerate clash.yaml, re-import it in your clients, then re-run my-singbox isp-outbound --add.`);
|
|
8178
|
-
for (const name of [NODE_SS_ISP, NODE_VLESS_ISP]) {
|
|
8179
|
-
if (!list.items.some((item) => (0, import_yaml2.isScalar)(item) && item.value === name)) {
|
|
8180
|
-
list.add(name);
|
|
8181
|
-
changed = true;
|
|
8182
|
-
}
|
|
8183
|
-
}
|
|
8184
|
-
const result2 = changed ? doc.toString({ lineWidth: 0 }) : text;
|
|
8185
|
-
if (result2.includes("REALITY_PRIVATE_KEY") || realityPrivateKey && result2.includes(realityPrivateKey))
|
|
8186
|
-
throw new IspError("Refusing to write REALITY_PRIVATE_KEY into clash.yaml.");
|
|
8187
|
-
return result2;
|
|
8292
|
+
function removeIspFromConfig(config) {
|
|
8293
|
+
checkIspConfig(config);
|
|
8294
|
+
const next = withoutLegacyListeners(config);
|
|
8295
|
+
omitIsp(next);
|
|
8296
|
+
return next;
|
|
8188
8297
|
}
|
|
8189
8298
|
function makeTmpPath(projectDir) {
|
|
8190
|
-
return
|
|
8299
|
+
return join4(projectDir, ISP_TMP_PREFIX + randomBytes3(16).toString("hex"));
|
|
8191
8300
|
}
|
|
8192
8301
|
function installFile(dest, data, projectDir) {
|
|
8193
8302
|
const tmp = makeTmpPath(projectDir);
|
|
@@ -8199,7 +8308,7 @@ function installFile(dest, data, projectDir) {
|
|
|
8199
8308
|
} finally {
|
|
8200
8309
|
closeSync2(fd);
|
|
8201
8310
|
}
|
|
8202
|
-
if (process.getuid?.() === 0 &&
|
|
8311
|
+
if (process.getuid?.() === 0 && existsSync3(dest)) {
|
|
8203
8312
|
const { uid, gid } = statSync(dest);
|
|
8204
8313
|
chownSync(tmp, uid, gid);
|
|
8205
8314
|
}
|
|
@@ -8216,59 +8325,43 @@ function copyPreserved(src, dest) {
|
|
|
8216
8325
|
utimesSync(dest, info.atimeMs / 1e3, info.mtimeMs / 1e3);
|
|
8217
8326
|
}
|
|
8218
8327
|
function backupDirs(projectDir) {
|
|
8219
|
-
const root =
|
|
8220
|
-
return
|
|
8328
|
+
const root = join4(projectDir, ISP_BACKUP_DIR);
|
|
8329
|
+
return existsSync3(root) ? readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join4(root, entry.name)).sort() : [];
|
|
8221
8330
|
}
|
|
8222
8331
|
var lastStamp = 0n;
|
|
8223
|
-
function makeBackup(projectDir) {
|
|
8332
|
+
function makeBackup(projectDir, files = MANAGED_FILES) {
|
|
8224
8333
|
const now = BigInt(Date.now()) * 1000000n;
|
|
8225
8334
|
lastStamp = now > lastStamp ? now : lastStamp + 1n;
|
|
8226
8335
|
const d = new Date(Number(lastStamp / 1000000n));
|
|
8227
8336
|
const p2 = (n) => String(n).padStart(2, "0");
|
|
8228
8337
|
const stamp = `${d.getFullYear()}${p2(d.getMonth() + 1)}${p2(d.getDate())}-${p2(d.getHours())}${p2(d.getMinutes())}${p2(d.getSeconds())}-${(lastStamp % 1000000000n).toString().padStart(9, "0")}-${process.pid}`;
|
|
8229
|
-
const root =
|
|
8338
|
+
const root = join4(projectDir, ISP_BACKUP_DIR), dir = join4(root, stamp);
|
|
8230
8339
|
mkdirSync(root, { recursive: true, mode: 448 });
|
|
8231
|
-
mkdirSync(dir, { mode: 448 });
|
|
8232
8340
|
chmodSync(root, 448);
|
|
8233
|
-
|
|
8234
|
-
for (const file of
|
|
8235
|
-
const src =
|
|
8236
|
-
if (
|
|
8341
|
+
mkdirSync(dir, { mode: 448 });
|
|
8342
|
+
for (const file of files) {
|
|
8343
|
+
const src = join4(projectDir, file);
|
|
8344
|
+
if (existsSync3(src) && statSync(src).isFile()) copyPreserved(src, join4(dir, file));
|
|
8237
8345
|
}
|
|
8238
8346
|
const all = backupDirs(projectDir);
|
|
8239
|
-
for (const old of all.slice(1, Math.max(1, all.length - 4)))
|
|
8240
|
-
rmSync2(old, { recursive: true });
|
|
8347
|
+
for (const old of all.slice(1, Math.max(1, all.length - 4))) rmSync2(old, { recursive: true });
|
|
8241
8348
|
return dir;
|
|
8242
8349
|
}
|
|
8243
|
-
function
|
|
8244
|
-
|
|
8245
|
-
const
|
|
8246
|
-
if (
|
|
8247
|
-
const text = readFileSync3(path, "utf8");
|
|
8248
|
-
if (file === ".env") return !envHasIsp(text);
|
|
8249
|
-
if (file === "clash.yaml") return !clashHasIsp(text);
|
|
8250
|
-
try {
|
|
8251
|
-
return !configHasIsp(JSON.parse(text));
|
|
8252
|
-
} catch {
|
|
8253
|
-
return false;
|
|
8254
|
-
}
|
|
8255
|
-
}));
|
|
8256
|
-
}
|
|
8257
|
-
function restoreBackup(projectDir, backupDir) {
|
|
8258
|
-
for (const file of MANAGED_FILES) {
|
|
8259
|
-
const src = join3(backupDir, file);
|
|
8260
|
-
if (existsSync2(src) && statSync(src).isFile()) copyPreserved(src, join3(projectDir, file));
|
|
8350
|
+
function restoreBackup(projectDir, backupDir, files = MANAGED_FILES) {
|
|
8351
|
+
for (const file of files) {
|
|
8352
|
+
const src = join4(backupDir, file), dest = join4(projectDir, file);
|
|
8353
|
+
if (existsSync3(src) && statSync(src).isFile()) copyPreserved(src, dest);
|
|
8261
8354
|
}
|
|
8262
8355
|
}
|
|
8263
8356
|
function sweepStaleTmp(projectDir) {
|
|
8264
8357
|
for (const entry of readdirSync(projectDir, { withFileTypes: true }))
|
|
8265
8358
|
if (entry.name.startsWith(ISP_TMP_PREFIX) && entry.isFile())
|
|
8266
|
-
rmSync2(
|
|
8359
|
+
rmSync2(join4(projectDir, entry.name));
|
|
8267
8360
|
}
|
|
8268
8361
|
function lockState(path) {
|
|
8269
8362
|
let pid;
|
|
8270
8363
|
try {
|
|
8271
|
-
pid = Number(
|
|
8364
|
+
pid = Number(readFileSync4(path, "utf8").trim());
|
|
8272
8365
|
} catch (error) {
|
|
8273
8366
|
if (error.code === "ENOENT") return "absent";
|
|
8274
8367
|
throw error;
|
|
@@ -8302,7 +8395,7 @@ function claimLock(path) {
|
|
|
8302
8395
|
}
|
|
8303
8396
|
}
|
|
8304
8397
|
async function withLock(projectDir, fn) {
|
|
8305
|
-
const path =
|
|
8398
|
+
const path = join4(projectDir, ISP_LOCK_FILE);
|
|
8306
8399
|
claimLock(path);
|
|
8307
8400
|
try {
|
|
8308
8401
|
sweepStaleTmp(projectDir);
|
|
@@ -8456,22 +8549,7 @@ var IspService = class {
|
|
|
8456
8549
|
}
|
|
8457
8550
|
return false;
|
|
8458
8551
|
}
|
|
8459
|
-
async
|
|
8460
|
-
const probe = await this.command("ss", ["-H", "-ltn"]);
|
|
8461
|
-
if (probe.missing) {
|
|
8462
|
-
this.warn(`ss is not installed. Skipping the check that ports ${SS_ISP_PORT} and ${VLESS_ISP_PORT} are free.`);
|
|
8463
|
-
return;
|
|
8464
|
-
}
|
|
8465
|
-
for (const [proto, port] of [
|
|
8466
|
-
["tcp", SS_ISP_PORT],
|
|
8467
|
-
["udp", SS_ISP_PORT],
|
|
8468
|
-
["tcp", VLESS_ISP_PORT]
|
|
8469
|
-
]) {
|
|
8470
|
-
if (await this.portListening(proto, port))
|
|
8471
|
-
throw new IspError(`Port ${port}/${proto} is already in use by another process.`);
|
|
8472
|
-
}
|
|
8473
|
-
}
|
|
8474
|
-
async verifyService(expectIsp) {
|
|
8552
|
+
async verifyService() {
|
|
8475
8553
|
let status = "missing";
|
|
8476
8554
|
for (let i = 0; i < 15; i++) {
|
|
8477
8555
|
status = await this.containerStatus();
|
|
@@ -8486,16 +8564,15 @@ var IspService = class {
|
|
|
8486
8564
|
this.warn("sing-box config check failed inside the container.");
|
|
8487
8565
|
return false;
|
|
8488
8566
|
}
|
|
8489
|
-
|
|
8490
|
-
|
|
8491
|
-
|
|
8567
|
+
const listeners = firewallPorts(readFileSync5(join5(this.io.projectDir, "config.json"), "utf8"));
|
|
8568
|
+
for (const listener of new Set(listeners)) {
|
|
8569
|
+
const [number, proto] = listener.split("/");
|
|
8570
|
+
const port = Number(number);
|
|
8571
|
+
if (!await this.waitListening(proto, port)) {
|
|
8572
|
+
this.warn(`Port ${port}/${proto} is not listening.`);
|
|
8492
8573
|
return false;
|
|
8493
8574
|
}
|
|
8494
8575
|
}
|
|
8495
|
-
if (expectIsp && !await this.waitListening("udp", SS_ISP_PORT)) {
|
|
8496
|
-
this.warn(`Port ${SS_ISP_PORT}/udp is not listening.`);
|
|
8497
|
-
return false;
|
|
8498
|
-
}
|
|
8499
8576
|
return true;
|
|
8500
8577
|
}
|
|
8501
8578
|
async socksEgressIp(isp) {
|
|
@@ -8525,140 +8602,6 @@ max-time = 15
|
|
|
8525
8602
|
return void 0;
|
|
8526
8603
|
}
|
|
8527
8604
|
};
|
|
8528
|
-
|
|
8529
|
-
// src/core/start.ts
|
|
8530
|
-
import { execFile as execFile2 } from "node:child_process";
|
|
8531
|
-
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
|
|
8532
|
-
import { connect } from "node:net";
|
|
8533
|
-
import { join as join4 } from "node:path";
|
|
8534
|
-
function parseListenPorts(configText) {
|
|
8535
|
-
const ports = /* @__PURE__ */ new Set();
|
|
8536
|
-
for (const m of configText.matchAll(/"listen_port"\s*:\s*([0-9]+)/g)) {
|
|
8537
|
-
const port = Number(m[1]);
|
|
8538
|
-
if (port < 1 || port > 65535)
|
|
8539
|
-
throw new Error(`Invalid listen_port in config.json: ${m[1]}`);
|
|
8540
|
-
ports.add(port);
|
|
8541
|
-
}
|
|
8542
|
-
if (!ports.size) throw new Error("config.json has no listen_port.");
|
|
8543
|
-
return [...ports];
|
|
8544
|
-
}
|
|
8545
|
-
function secretValues(envText) {
|
|
8546
|
-
return [...envText.matchAll(/^(?:REALITY_PRIVATE_KEY|SS_PASSWORD|VLESS_UUID|ISP_SOCKS_USER|ISP_SOCKS_PASS)=([^\r\n]*)\r?$/gm)].map((m) => envUnquote(m[1])).filter((value) => value !== null && value !== "");
|
|
8547
|
-
}
|
|
8548
|
-
function redact(text, secrets) {
|
|
8549
|
-
for (const secret of [...secrets].sort((a, b) => b.length - a.length))
|
|
8550
|
-
text = text.replaceAll(secret, "***");
|
|
8551
|
-
return text;
|
|
8552
|
-
}
|
|
8553
|
-
function composeImageTag(text) {
|
|
8554
|
-
const m = /^\s*image:\s*ghcr\.io\/sagernet\/sing-box:([^\s#]+)/m.exec(text);
|
|
8555
|
-
if (!m) throw new Error("sing-box image tag not found in compose.yaml.");
|
|
8556
|
-
return m[1];
|
|
8557
|
-
}
|
|
8558
|
-
function firewallPorts(configText) {
|
|
8559
|
-
const config = JSON.parse(configText);
|
|
8560
|
-
return config.inbounds.flatMap(
|
|
8561
|
-
(i) => i.type === "shadowsocks" ? [`${i.listen_port}/tcp`, `${i.listen_port}/udp`] : [`${i.listen_port}/tcp`]
|
|
8562
|
-
);
|
|
8563
|
-
}
|
|
8564
|
-
function realStartDeps() {
|
|
8565
|
-
return {
|
|
8566
|
-
run: (file, args) => new Promise((resolve) => {
|
|
8567
|
-
execFile2(file, args, (error, stdout, stderr) => {
|
|
8568
|
-
const code = error ? typeof error.code === "number" ? error.code : error.code === "ENOENT" ? 127 : 1 : 0;
|
|
8569
|
-
resolve({ code, stdout, stderr });
|
|
8570
|
-
});
|
|
8571
|
-
}),
|
|
8572
|
-
tcpOpen: (port) => new Promise((resolve) => {
|
|
8573
|
-
const socket = connect(port, "127.0.0.1");
|
|
8574
|
-
const finish = (open) => {
|
|
8575
|
-
socket.destroy();
|
|
8576
|
-
resolve(open);
|
|
8577
|
-
};
|
|
8578
|
-
socket.setTimeout(500, () => finish(false));
|
|
8579
|
-
socket.once("connect", () => finish(true));
|
|
8580
|
-
socket.once("error", () => finish(false));
|
|
8581
|
-
}),
|
|
8582
|
-
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
8583
|
-
};
|
|
8584
|
-
}
|
|
8585
|
-
async function startNode(opts) {
|
|
8586
|
-
const { project, deps } = opts;
|
|
8587
|
-
if (!existsSync3(join4(project, "compose.yaml")))
|
|
8588
|
-
throw new Error("compose.yaml does not exist.");
|
|
8589
|
-
if (!existsSync3(join4(project, "config.json")))
|
|
8590
|
-
throw new Error("config.json does not exist. Run my-singbox gen-config first.");
|
|
8591
|
-
const ports = parseListenPorts(readFileSync4(join4(project, "config.json"), "utf8"));
|
|
8592
|
-
if (!await opts.dockerAvailable()) throw new Error("docker is not installed.");
|
|
8593
|
-
if ((await deps.run("docker", ["compose", "version"])).code)
|
|
8594
|
-
throw new Error("docker compose is not available.");
|
|
8595
|
-
if (!await opts.dockerDaemonUp()) throw new Error("Docker daemon is not running.");
|
|
8596
|
-
const envPath = join4(project, ".env");
|
|
8597
|
-
const secrets = secretValues(existsSync3(envPath) ? readFileSync4(envPath, "utf8") : "");
|
|
8598
|
-
const compose = (...args) => deps.run("docker", ["compose", "--project-directory", project, ...args]);
|
|
8599
|
-
const forward = (output) => {
|
|
8600
|
-
opts.stdout(redact(output.stdout, secrets));
|
|
8601
|
-
opts.stderr(redact(output.stderr, secrets));
|
|
8602
|
-
};
|
|
8603
|
-
const fail = async (message) => {
|
|
8604
|
-
const logs = await compose("logs", "--tail", "50", "sing-box");
|
|
8605
|
-
opts.stderr(redact(logs.stdout, secrets));
|
|
8606
|
-
opts.stderr(redact(logs.stderr, secrets));
|
|
8607
|
-
throw new Error(redact(message, secrets));
|
|
8608
|
-
};
|
|
8609
|
-
const status = async () => {
|
|
8610
|
-
const output = await deps.run("docker", ["inspect", "-f", "{{.State.Status}}", "sing-box"]);
|
|
8611
|
-
return output.code ? "missing" : output.stdout.trim();
|
|
8612
|
-
};
|
|
8613
|
-
opts.stdout("Starting sing-box...\n");
|
|
8614
|
-
const up = await compose("up", "-d");
|
|
8615
|
-
forward(up);
|
|
8616
|
-
if (up.code) throw new Error("docker compose up failed.");
|
|
8617
|
-
let state = "missing";
|
|
8618
|
-
for (let i = 0; i < 15; i++) {
|
|
8619
|
-
state = await status();
|
|
8620
|
-
if (state === "running") break;
|
|
8621
|
-
await deps.sleep(1e3);
|
|
8622
|
-
}
|
|
8623
|
-
if (state !== "running") await fail(`sing-box did not stay running (status: ${state}).`);
|
|
8624
|
-
const check = await compose("exec", "-T", "sing-box", "sing-box", "check", "-c", "/etc/sing-box/config.json");
|
|
8625
|
-
forward(check);
|
|
8626
|
-
if (check.code) await fail("sing-box config check failed.");
|
|
8627
|
-
for (const port of ports) {
|
|
8628
|
-
let open = false;
|
|
8629
|
-
for (let i = 0; i < 10; i++) {
|
|
8630
|
-
open = await deps.tcpOpen(port);
|
|
8631
|
-
if (open) break;
|
|
8632
|
-
await deps.sleep(1e3);
|
|
8633
|
-
}
|
|
8634
|
-
if (!open) await fail(`Port ${port} is not accepting connections.`);
|
|
8635
|
-
}
|
|
8636
|
-
if (ports.includes(8388)) {
|
|
8637
|
-
let listening = false;
|
|
8638
|
-
for (let i = 0; i < 10; i++) {
|
|
8639
|
-
const udp = await deps.run("ss", ["-uln"]);
|
|
8640
|
-
if (udp.code === 127) {
|
|
8641
|
-
opts.stderr("Warning: ss is not installed. Skipping UDP listener check for port 8388.\n");
|
|
8642
|
-
listening = true;
|
|
8643
|
-
break;
|
|
8644
|
-
}
|
|
8645
|
-
if (udp.code === 0 && /:8388\s/.test(udp.stdout)) {
|
|
8646
|
-
const owners = await deps.run("ss", ["-ulnp"]);
|
|
8647
|
-
const lines = owners.stdout.split("\n").filter((l) => /:8388\s/.test(l)).join("\n");
|
|
8648
|
-
if (lines.includes("users:") && !lines.includes("sing-box"))
|
|
8649
|
-
await fail("UDP port 8388 is not owned by sing-box.");
|
|
8650
|
-
listening = true;
|
|
8651
|
-
break;
|
|
8652
|
-
}
|
|
8653
|
-
await deps.sleep(1e3);
|
|
8654
|
-
}
|
|
8655
|
-
if (!listening) await fail("UDP port 8388 is not listening.");
|
|
8656
|
-
}
|
|
8657
|
-
state = await status();
|
|
8658
|
-
if (state !== "running") await fail(`sing-box did not stay running (status: ${state}).`);
|
|
8659
|
-
opts.stdout(`Self-check passed. Ports: ${ports.join(" ")}
|
|
8660
|
-
`);
|
|
8661
|
-
}
|
|
8662
8605
|
export {
|
|
8663
8606
|
ENV_HEADER,
|
|
8664
8607
|
ENV_HEADER_NOEOL,
|
|
@@ -8691,7 +8634,6 @@ export {
|
|
|
8691
8634
|
VLESS_ISP_TAG,
|
|
8692
8635
|
VLESS_PORT,
|
|
8693
8636
|
VLESS_TAG,
|
|
8694
|
-
addIspToClash,
|
|
8695
8637
|
addIspToConfig,
|
|
8696
8638
|
buildModel,
|
|
8697
8639
|
checkArch,
|
|
@@ -8700,7 +8642,6 @@ export {
|
|
|
8700
8642
|
checkNode,
|
|
8701
8643
|
checkPlatform,
|
|
8702
8644
|
checkSocket,
|
|
8703
|
-
clashHasIsp,
|
|
8704
8645
|
composeImageTag,
|
|
8705
8646
|
configHasIsp,
|
|
8706
8647
|
defaultKeyDeps,
|
|
@@ -8714,7 +8655,6 @@ export {
|
|
|
8714
8655
|
envUnquote,
|
|
8715
8656
|
envWithIsp,
|
|
8716
8657
|
envWithoutIsp,
|
|
8717
|
-
findCleanBackup,
|
|
8718
8658
|
findPackageFile,
|
|
8719
8659
|
findTemplate,
|
|
8720
8660
|
firewallPorts,
|
|
@@ -8736,6 +8676,7 @@ export {
|
|
|
8736
8676
|
readTemplate,
|
|
8737
8677
|
realStartDeps,
|
|
8738
8678
|
redact,
|
|
8679
|
+
removeIspFromConfig,
|
|
8739
8680
|
renderClash,
|
|
8740
8681
|
renderGeneratedEnv,
|
|
8741
8682
|
renderSingbox,
|