Add sing-box support

This commit is contained in:
SukkaW
2024-08-13 01:32:55 +08:00
parent 12665a743d
commit 75f188f1c1
16 changed files with 159 additions and 17 deletions

View File

@@ -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
View 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]
};
};