mirror of
https://github.com/SukkaW/Surge.git
synced 2025-12-12 09:10:35 +08:00
Add sing-box support
This commit is contained in:
parent
12665a743d
commit
75f188f1c1
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,6 +10,7 @@ tmp*
|
||||
List/
|
||||
Clash/
|
||||
Internal/
|
||||
sing-box/
|
||||
Modules/sukka_local_dns_mapping.sgmodule
|
||||
Modules/sukka_url_redirect.sgmodule
|
||||
Modules/sukka_common_always_realip.sgmodule
|
||||
|
||||
@ -43,7 +43,8 @@ export const buildAppleCdn = task(require.main === module, __filename)(async (sp
|
||||
ruleset,
|
||||
'ruleset',
|
||||
path.resolve(__dirname, '../List/non_ip/apple_cdn.conf'),
|
||||
path.resolve(__dirname, '../Clash/non_ip/apple_cdn.txt')
|
||||
path.resolve(__dirname, '../Clash/non_ip/apple_cdn.txt'),
|
||||
path.resolve(__dirname, '../sing-box/non_ip/apple_cdn.json')
|
||||
),
|
||||
createRuleset(
|
||||
span,
|
||||
@ -54,6 +55,7 @@ export const buildAppleCdn = task(require.main === module, __filename)(async (sp
|
||||
'domainset',
|
||||
path.resolve(__dirname, '../List/domainset/apple_cdn.conf'),
|
||||
path.resolve(__dirname, '../Clash/domainset/apple_cdn.txt'),
|
||||
path.resolve(__dirname, '../sing-box/domainset/apple_cdn.json'),
|
||||
path.resolve(__dirname, '../Clash/clash_mrs_domain/apple_cdn.mrs')
|
||||
)
|
||||
]);
|
||||
|
||||
@ -79,6 +79,7 @@ export const buildCdnDownloadConf = task(require.main === module, __filename)(as
|
||||
'domainset',
|
||||
path.resolve(__dirname, '../List/domainset/cdn.conf'),
|
||||
path.resolve(__dirname, '../Clash/domainset/cdn.txt'),
|
||||
path.resolve(__dirname, '../sing-box/domainset/cdn.json'),
|
||||
path.resolve(__dirname, '../Clash/clash_mrs_domain/cdn.mrs')
|
||||
),
|
||||
createRuleset(
|
||||
@ -94,6 +95,7 @@ export const buildCdnDownloadConf = task(require.main === module, __filename)(as
|
||||
'domainset',
|
||||
path.resolve(__dirname, '../List/domainset/download.conf'),
|
||||
path.resolve(__dirname, '../Clash/domainset/download.txt'),
|
||||
path.resolve(__dirname, '../sing-box/domainset/download.json'),
|
||||
path.resolve(__dirname, '../Clash/clash_mrs_domain/download.mrs')
|
||||
)
|
||||
]);
|
||||
|
||||
@ -6,6 +6,7 @@ import { task } from './trace';
|
||||
|
||||
const outputSurgeDir = path.resolve(__dirname, '../List');
|
||||
const outputClashDir = path.resolve(__dirname, '../Clash');
|
||||
const outputSingboxDir = path.resolve(__dirname, '../sing-box');
|
||||
|
||||
export const buildCloudMounterRules = task(require.main === module, __filename)(async (span) => {
|
||||
// AND,((SRC-IP,192.168.1.110), (DOMAIN, example.com))
|
||||
@ -24,6 +25,7 @@ export const buildCloudMounterRules = task(require.main === module, __filename)(
|
||||
results,
|
||||
'ruleset',
|
||||
path.resolve(outputSurgeDir, 'non_ip', 'cloudmounter.conf'),
|
||||
path.resolve(outputClashDir, 'non_ip', 'cloudmounter.txt')
|
||||
path.resolve(outputClashDir, 'non_ip', 'cloudmounter.txt'),
|
||||
path.resolve(outputSingboxDir, 'non_ip', 'cloudmounter.json')
|
||||
);
|
||||
});
|
||||
|
||||
@ -18,6 +18,7 @@ const MAGIC_COMMAND_DESCRIPTION = '# $ meta_description ';
|
||||
const sourceDir = path.resolve(__dirname, '../Source');
|
||||
const outputSurgeDir = path.resolve(__dirname, '../List');
|
||||
const outputClashDir = path.resolve(__dirname, '../Clash');
|
||||
const outputSingboxDir = path.resolve(__dirname, '../sing-box');
|
||||
|
||||
const domainsetSrcFolder = 'domainset' + path.sep;
|
||||
|
||||
@ -137,7 +138,8 @@ function transformDomainset(parentSpan: Span, sourcePath: string, relativePath:
|
||||
deduped,
|
||||
'domainset',
|
||||
path.resolve(outputSurgeDir, relativePath),
|
||||
path.resolve(outputClashDir, `${clashFileBasename}.txt`)
|
||||
path.resolve(outputClashDir, `${clashFileBasename}.txt`),
|
||||
path.resolve(outputSingboxDir, `${clashFileBasename}.json`)
|
||||
);
|
||||
}
|
||||
);
|
||||
@ -164,6 +166,8 @@ async function transformRuleset(parentSpan: Span, sourcePath: string, relativePa
|
||||
description = SHARED_DESCRIPTION;
|
||||
}
|
||||
|
||||
const clashFileBasename = relativePath.slice(0, -path.extname(relativePath).length);
|
||||
|
||||
return createRuleset(
|
||||
span,
|
||||
title,
|
||||
@ -172,7 +176,8 @@ async function transformRuleset(parentSpan: Span, sourcePath: string, relativePa
|
||||
lines,
|
||||
'ruleset',
|
||||
path.resolve(outputSurgeDir, relativePath),
|
||||
path.resolve(outputClashDir, `${relativePath.slice(0, -path.extname(relativePath).length)}.txt`)
|
||||
path.resolve(outputClashDir, `${clashFileBasename}.txt`),
|
||||
path.resolve(outputSingboxDir, `${clashFileBasename}.json`)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -49,7 +49,8 @@ export const buildDomesticRuleset = task(require.main === module, __filename)(as
|
||||
res[0],
|
||||
'ruleset',
|
||||
path.resolve(__dirname, '../List/non_ip/domestic.conf'),
|
||||
path.resolve(__dirname, '../Clash/non_ip/domestic.txt')
|
||||
path.resolve(__dirname, '../Clash/non_ip/domestic.txt'),
|
||||
path.resolve(__dirname, '../sing-box/non_ip/domestic.json')
|
||||
),
|
||||
createRuleset(
|
||||
span,
|
||||
@ -63,7 +64,8 @@ export const buildDomesticRuleset = task(require.main === module, __filename)(as
|
||||
res[1],
|
||||
'ruleset',
|
||||
path.resolve(__dirname, '../List/non_ip/direct.conf'),
|
||||
path.resolve(__dirname, '../Clash/non_ip/direct.txt')
|
||||
path.resolve(__dirname, '../Clash/non_ip/direct.txt'),
|
||||
path.resolve(__dirname, '../sing-box/non_ip/direct.json')
|
||||
),
|
||||
createRuleset(
|
||||
span,
|
||||
@ -77,7 +79,8 @@ export const buildDomesticRuleset = task(require.main === module, __filename)(as
|
||||
res[2],
|
||||
'ruleset',
|
||||
path.resolve(__dirname, '../List/non_ip/lan.conf'),
|
||||
path.resolve(__dirname, '../Clash/non_ip/lan.txt')
|
||||
path.resolve(__dirname, '../Clash/non_ip/lan.txt'),
|
||||
path.resolve(__dirname, '../sing-box/non_ip/lan.json')
|
||||
),
|
||||
compareAndWriteFile(
|
||||
span,
|
||||
|
||||
@ -64,6 +64,7 @@ export const buildMicrosoftCdn = task(require.main === module, __filename)(async
|
||||
res,
|
||||
'ruleset',
|
||||
path.resolve(__dirname, '../List/non_ip/microsoft_cdn.conf'),
|
||||
path.resolve(__dirname, '../Clash/non_ip/microsoft_cdn.txt')
|
||||
path.resolve(__dirname, '../Clash/non_ip/microsoft_cdn.txt'),
|
||||
path.resolve(__dirname, '../sing-box/non_ip/microsoft_cdn.json')
|
||||
);
|
||||
});
|
||||
|
||||
@ -192,6 +192,7 @@ export const buildRejectDomainSet = task(require.main === module, __filename)(as
|
||||
'domainset',
|
||||
path.resolve(__dirname, '../List/domainset/reject.conf'),
|
||||
path.resolve(__dirname, '../Clash/domainset/reject.txt'),
|
||||
path.resolve(__dirname, '../sing-box/domainset/reject.json'),
|
||||
path.resolve(__dirname, '../Clash/clash_mrs_domain/reject.mrs')
|
||||
),
|
||||
createRuleset(
|
||||
@ -213,6 +214,7 @@ export const buildRejectDomainSet = task(require.main === module, __filename)(as
|
||||
'domainset',
|
||||
path.resolve(__dirname, '../List/domainset/reject_extra.conf'),
|
||||
path.resolve(__dirname, '../Clash/domainset/reject_extra.txt'),
|
||||
path.resolve(__dirname, '../sing-box/domainset/reject_extra.json'),
|
||||
path.resolve(__dirname, '../Clash/clash_mrs_domain/reject_extra.mrs')
|
||||
),
|
||||
compareAndWriteFile(
|
||||
|
||||
@ -101,6 +101,7 @@ export const buildRejectIPList = task(require.main === module, __filename)(async
|
||||
result,
|
||||
'ruleset',
|
||||
path.resolve(__dirname, '../List/ip/reject.conf'),
|
||||
path.resolve(__dirname, '../Clash/ip/reject.txt')
|
||||
path.resolve(__dirname, '../Clash/ip/reject.txt'),
|
||||
path.resolve(__dirname, '../sing-box/ip/reject.json')
|
||||
);
|
||||
});
|
||||
|
||||
@ -252,6 +252,7 @@ export const buildSpeedtestDomainSet = task(require.main === module, __filename)
|
||||
'domainset',
|
||||
path.resolve(__dirname, '../List/domainset/speedtest.conf'),
|
||||
path.resolve(__dirname, '../Clash/domainset/speedtest.txt'),
|
||||
path.resolve(__dirname, '../sing-box/domainset/speedtest.json'),
|
||||
path.resolve(__dirname, '../Clash/clash_mrs_domain/speedtest.mrs')
|
||||
);
|
||||
});
|
||||
|
||||
@ -23,7 +23,8 @@ export const createRulesetForStreamService = (span: Span, fileId: string, title:
|
||||
streamServices.flatMap((i) => i.rules),
|
||||
'ruleset',
|
||||
path.resolve(__dirname, `../List/non_ip/${fileId}.conf`),
|
||||
path.resolve(__dirname, `../Clash/non_ip/${fileId}.txt`)
|
||||
path.resolve(__dirname, `../Clash/non_ip/${fileId}.txt`),
|
||||
path.resolve(__dirname, `../sing-box/non_ip/${fileId}.json`)
|
||||
),
|
||||
// IP
|
||||
createRuleset(
|
||||
@ -45,7 +46,8 @@ export const createRulesetForStreamService = (span: Span, fileId: string, title:
|
||||
)),
|
||||
'ruleset',
|
||||
path.resolve(__dirname, `../List/ip/${fileId}.conf`),
|
||||
path.resolve(__dirname, `../Clash/ip/${fileId}.txt`)
|
||||
path.resolve(__dirname, `../Clash/ip/${fileId}.txt`),
|
||||
path.resolve(__dirname, `../sing-box/ip/${fileId}.json`)
|
||||
)
|
||||
]));
|
||||
};
|
||||
|
||||
@ -53,6 +53,7 @@ export const buildTelegramCIDR = task(require.main === module, __filename)(async
|
||||
results,
|
||||
'ruleset',
|
||||
path.resolve(__dirname, '../List/ip/telegram.conf'),
|
||||
path.resolve(__dirname, '../Clash/ip/telegram.txt')
|
||||
path.resolve(__dirname, '../Clash/ip/telegram.txt'),
|
||||
path.resolve(__dirname, '../sing-box/ip/telegram.json')
|
||||
);
|
||||
});
|
||||
|
||||
@ -6,6 +6,8 @@ import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { fastStringArrayJoin, writeFile } from './misc';
|
||||
import { readFileByLine } from './fetch-text-by-line';
|
||||
import stringify from 'json-stringify-pretty-compact';
|
||||
import { surgeDomainsetToSingbox, surgeRulesetToSingbox } from './singbox';
|
||||
|
||||
export async function compareAndWriteFile(span: Span, linesA: string[], filePath: string) {
|
||||
let isEqual = true;
|
||||
@ -152,14 +154,13 @@ export const createRuleset = (
|
||||
parentSpan: Span,
|
||||
title: string, description: string[] | readonly string[], date: Date, content: string[],
|
||||
type: ('ruleset' | 'domainset' | string & {}),
|
||||
surgePath: string, clashPath: string,
|
||||
clashMrsPath?: string
|
||||
surgePath: string, clashPath: string, singBoxPath: string, _clashMrsPath?: string
|
||||
) => parentSpan.traceChild(`create ruleset: ${path.basename(surgePath, path.extname(surgePath))}`).traceAsyncFn(async (childSpan) => {
|
||||
const surgeContent = withBannerArray(
|
||||
title, description, date,
|
||||
sortRuleSet(type === 'domainset'
|
||||
type === 'domainset'
|
||||
? [MARK, ...content]
|
||||
: [`DOMAIN,${MARK}`, ...content])
|
||||
: sortRuleSet([`DOMAIN,${MARK}`, ...content])
|
||||
);
|
||||
const clashContent = childSpan.traceChildSync('convert incoming ruleset to clash', () => {
|
||||
let _clashContent;
|
||||
@ -175,10 +176,25 @@ export const createRuleset = (
|
||||
}
|
||||
return withBannerArray(title, description, date, _clashContent);
|
||||
});
|
||||
const singboxContent = childSpan.traceChildSync('convert incoming ruleset to singbox', () => {
|
||||
let _singBoxContent;
|
||||
switch (type) {
|
||||
case 'domainset':
|
||||
_singBoxContent = surgeDomainsetToSingbox([MARK, ...content]);
|
||||
break;
|
||||
case 'ruleset':
|
||||
_singBoxContent = surgeRulesetToSingbox([`DOMAIN,${MARK}`, ...content]);
|
||||
break;
|
||||
default:
|
||||
throw new TypeError(`Unknown type: ${type}`);
|
||||
}
|
||||
return stringify(_singBoxContent).split('\n');
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
compareAndWriteFile(childSpan, surgeContent, surgePath),
|
||||
compareAndWriteFile(childSpan, clashContent, clashPath)
|
||||
compareAndWriteFile(childSpan, clashContent, clashPath),
|
||||
compareAndWriteFile(childSpan, singboxContent, singBoxPath)
|
||||
]);
|
||||
|
||||
// if (clashMrsPath) {
|
||||
|
||||
94
Build/lib/singbox.ts
Normal file
94
Build/lib/singbox.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import picocolors from 'picocolors';
|
||||
|
||||
const unsupported = Symbol('unsupported');
|
||||
|
||||
// https://sing-box.sagernet.org/configuration/rule-set/source-format/
|
||||
const PROCESSOR: Record<string, ((raw: string, type: string, value: string) => [key: keyof SingboxHeadlessRule, value: string]) | typeof unsupported> = {
|
||||
DOMAIN: (_1, _2, value) => ['domain', value],
|
||||
'DOMAIN-SUFFIX': (_1, _2, value) => ['domain_suffix', value],
|
||||
'DOMAIN-KEYWORD': (_1, _2, value) => ['domain_keyword', value],
|
||||
GEOIP: unsupported,
|
||||
'IP-CIDR': (_1, _2, value) => ['ip_cidr', value.endsWith(',no-resolve') ? value.slice(0, -11) : value],
|
||||
'IP-CIDR6': (_1, _2, value) => ['ip_cidr', value.endsWith(',no-resolve') ? value.slice(0, -11) : value],
|
||||
'IP-ASN': unsupported,
|
||||
'SRC-IP-CIDR': (_1, _2, value) => ['source_ip_cidr', value.endsWith(',no-resolve') ? value.slice(0, -11) : value],
|
||||
'SRC-PORT': (_1, _2, value) => ['source_port', value],
|
||||
'DST-PORT': (_1, _2, value) => ['port', value],
|
||||
'PROCESS-NAME': (_1, _2, value) => ['process_name', value],
|
||||
'PROCESS-PATH': (_1, _2, value) => ['process_path', value],
|
||||
'DEST-PORT': (_1, _2, value) => ['port', value],
|
||||
'IN-PORT': (_1, _2, value) => ['source_port', value],
|
||||
'URL-REGEX': unsupported,
|
||||
'USER-AGENT': unsupported
|
||||
};
|
||||
|
||||
interface SingboxHeadlessRule {
|
||||
domain?: string[],
|
||||
domain_suffix?: string[],
|
||||
domain_keyword?: string[],
|
||||
domain_regex?: string[],
|
||||
source_ip_cidr?: string[],
|
||||
ip_cidr?: string[],
|
||||
source_port?: string[],
|
||||
source_port_range?: string[],
|
||||
port?: string[],
|
||||
port_range?: string[],
|
||||
process_name?: string[],
|
||||
process_path?: string[]
|
||||
}
|
||||
|
||||
interface SingboxSourceFormat {
|
||||
version: 2 | number & {},
|
||||
rules: SingboxHeadlessRule[]
|
||||
}
|
||||
|
||||
export const surgeRulesetToSingbox = (rules: string[] | Set<string>): SingboxSourceFormat => {
|
||||
const rule: SingboxHeadlessRule = Array.from(rules).reduce<SingboxHeadlessRule>((acc, cur) => {
|
||||
let buf = '';
|
||||
let type = '';
|
||||
let i = 0;
|
||||
for (const len = cur.length; i < len; i++) {
|
||||
if (cur[i] === ',') {
|
||||
type = buf;
|
||||
break;
|
||||
}
|
||||
buf += cur[i];
|
||||
}
|
||||
if (type === '') {
|
||||
return acc;
|
||||
}
|
||||
const value = cur.slice(i + 1);
|
||||
if (type in PROCESSOR) {
|
||||
const proc = PROCESSOR[type];
|
||||
if (proc !== unsupported) {
|
||||
const [k, v] = proc(cur, type, value);
|
||||
acc[k] ||= [];
|
||||
acc[k].push(v);
|
||||
}
|
||||
} else {
|
||||
console.log(picocolors.yellow(`[sing-box] unknown rule type: ${type}`), cur);
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
version: 2,
|
||||
rules: [rule]
|
||||
};
|
||||
};
|
||||
|
||||
export const surgeDomainsetToSingbox = (domainset: string[]) => {
|
||||
const rule = domainset.reduce((acc, cur) => {
|
||||
if (cur[0] === '.') {
|
||||
acc.domain_suffix.push(cur.slice(1));
|
||||
} else {
|
||||
acc.domain.push(cur);
|
||||
}
|
||||
return acc;
|
||||
}, { domain: [] as string[], domain_suffix: [] as string[] } satisfies SingboxHeadlessRule);
|
||||
|
||||
return {
|
||||
version: 2,
|
||||
rules: [rule]
|
||||
};
|
||||
};
|
||||
@ -30,6 +30,7 @@
|
||||
"fast-cidr-tools": "^0.2.5",
|
||||
"fdir": "^6.2.0",
|
||||
"foxact": "^0.2.36",
|
||||
"json-stringify-pretty-compact": "^3.0.0",
|
||||
"mnemonist": "^0.39.8",
|
||||
"path-scurry": "^1.11.1",
|
||||
"picocolors": "^1.0.1",
|
||||
|
||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@ -41,6 +41,9 @@ importers:
|
||||
foxact:
|
||||
specifier: ^0.2.36
|
||||
version: 0.2.36
|
||||
json-stringify-pretty-compact:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
mnemonist:
|
||||
specifier: ^0.39.8
|
||||
version: 0.39.8
|
||||
@ -1101,6 +1104,9 @@ packages:
|
||||
json-stable-stringify-without-jsonify@1.0.1:
|
||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||
|
||||
json-stringify-pretty-compact@3.0.0:
|
||||
resolution: {integrity: sha512-Rc2suX5meI0S3bfdZuA7JMFBGkJ875ApfVyq2WHELjBiiG22My/l7/8zPpH/CfFVQHuVLd8NLR0nv6vi0BYYKA==}
|
||||
|
||||
jsonc-eslint-parser@2.4.0:
|
||||
resolution: {integrity: sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
@ -2618,6 +2624,8 @@ snapshots:
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||
|
||||
json-stringify-pretty-compact@3.0.0: {}
|
||||
|
||||
jsonc-eslint-parser@2.4.0:
|
||||
dependencies:
|
||||
acorn: 8.12.1
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user