176 lines
4.7 KiB
JavaScript
176 lines
4.7 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { settings } from './config.js';
|
|
|
|
const PROXY_TYPES = new Set(['vless', 'vmess', 'trojan', 'shadowsocks', 'hysteria2']);
|
|
const CUSTOM_OUTBOUNDS = new Set(['direct', 'vpn', 'block']);
|
|
|
|
function clone(value) {
|
|
return JSON.parse(JSON.stringify(value));
|
|
}
|
|
|
|
function findOutbound(subscriptionConfig, selectedTag) {
|
|
const outbounds = Array.isArray(subscriptionConfig?.outbounds) ? subscriptionConfig.outbounds : [];
|
|
return outbounds.find((outbound) => outbound.tag === selectedTag && PROXY_TYPES.has(outbound.type));
|
|
}
|
|
|
|
function ruleSets() {
|
|
if (!settings.routingRuDirect) return [];
|
|
|
|
return [
|
|
{
|
|
type: 'remote',
|
|
tag: 'geoip-ru',
|
|
format: 'binary',
|
|
url: 'https://cdn.jsdelivr.net/gh/SagerNet/sing-geoip@rule-set/geoip-ru.srs',
|
|
download_detour: 'direct',
|
|
},
|
|
{
|
|
type: 'remote',
|
|
tag: 'geosite-category-ru',
|
|
format: 'binary',
|
|
url: 'https://cdn.jsdelivr.net/gh/SagerNet/sing-geosite@rule-set/geosite-category-ru.srs',
|
|
download_detour: 'direct',
|
|
},
|
|
];
|
|
}
|
|
|
|
function uniqueClean(values) {
|
|
return Array.from(
|
|
new Set(
|
|
(Array.isArray(values) ? values : [])
|
|
.map((value) => String(value || '').trim())
|
|
.filter(Boolean),
|
|
),
|
|
);
|
|
}
|
|
|
|
function parsePorts(values) {
|
|
return uniqueClean(values)
|
|
.map((value) => Number.parseInt(value, 10))
|
|
.filter((value) => Number.isInteger(value) && value > 0 && value <= 65535);
|
|
}
|
|
|
|
function toSingboxRule(customRule, vpnTag) {
|
|
if (!customRule?.enabled) return null;
|
|
if (!CUSTOM_OUTBOUNDS.has(customRule.outbound)) return null;
|
|
|
|
const rule = {};
|
|
const domains = uniqueClean(customRule.domains);
|
|
const domainSuffixes = uniqueClean(customRule.domainSuffixes);
|
|
const domainKeywords = uniqueClean(customRule.domainKeywords);
|
|
const ipCidrs = uniqueClean(customRule.ipCidrs);
|
|
const ports = parsePorts(customRule.ports);
|
|
const networks = uniqueClean(customRule.networks).filter((network) => ['tcp', 'udp'].includes(network));
|
|
|
|
if (domains.length) rule.domain = domains;
|
|
if (domainSuffixes.length) rule.domain_suffix = domainSuffixes;
|
|
if (domainKeywords.length) rule.domain_keyword = domainKeywords;
|
|
if (ipCidrs.length) rule.ip_cidr = ipCidrs;
|
|
if (ports.length) rule.port = ports;
|
|
if (networks.length) rule.network = networks;
|
|
|
|
if (
|
|
!rule.domain &&
|
|
!rule.domain_suffix &&
|
|
!rule.domain_keyword &&
|
|
!rule.ip_cidr &&
|
|
!rule.port &&
|
|
!rule.network
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
rule.outbound = customRule.outbound === 'vpn' ? vpnTag : customRule.outbound;
|
|
return rule;
|
|
}
|
|
|
|
function customRouteRules(customRules, vpnTag) {
|
|
return (Array.isArray(customRules) ? customRules : [])
|
|
.map((rule) => toSingboxRule(rule, vpnTag))
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function routeRules(customRules, vpnTag) {
|
|
const rules = [
|
|
{
|
|
ip_is_private: true,
|
|
outbound: 'direct',
|
|
},
|
|
];
|
|
|
|
rules.push(...customRouteRules(customRules, vpnTag));
|
|
|
|
if (settings.routingRuDirect) {
|
|
rules.push({
|
|
rule_set: ['geoip-ru', 'geosite-category-ru'],
|
|
outbound: 'direct',
|
|
});
|
|
}
|
|
|
|
return rules;
|
|
}
|
|
|
|
export function buildGatewayConfig(subscriptionConfig, selectedTag) {
|
|
const selectedOutbound = findOutbound(subscriptionConfig, selectedTag);
|
|
if (!selectedOutbound) {
|
|
throw new Error(`Selected outbound not found: ${selectedTag}`);
|
|
}
|
|
|
|
const vpnOutbound = clone(selectedOutbound);
|
|
if (!vpnOutbound.tag) vpnOutbound.tag = 'vpn-out';
|
|
if (vpnOutbound.type === 'vless' && !vpnOutbound.packet_encoding) {
|
|
vpnOutbound.packet_encoding = 'xudp';
|
|
}
|
|
|
|
return {
|
|
log: {
|
|
level: settings.logLevel,
|
|
timestamp: true,
|
|
},
|
|
experimental: {
|
|
cache_file: {
|
|
enabled: true,
|
|
path: settings.cachePath,
|
|
},
|
|
},
|
|
dns: {
|
|
independent_cache: true,
|
|
},
|
|
inbounds: [
|
|
{
|
|
type: 'tproxy',
|
|
tag: 'tproxy-in',
|
|
listen: '::',
|
|
listen_port: settings.tproxyPort,
|
|
sniff: true,
|
|
sniff_override_destination: true,
|
|
},
|
|
{
|
|
type: 'mixed',
|
|
tag: 'mixed-in',
|
|
listen: settings.bindIp,
|
|
listen_port: settings.proxyPort,
|
|
sniff: true,
|
|
set_system_proxy: false,
|
|
},
|
|
],
|
|
outbounds: [
|
|
vpnOutbound,
|
|
{ type: 'direct', tag: 'direct' },
|
|
{ type: 'block', tag: 'block' },
|
|
],
|
|
route: {
|
|
rule_set: ruleSets(),
|
|
rules: routeRules(subscriptionConfig.customRules, vpnOutbound.tag),
|
|
final: vpnOutbound.tag,
|
|
auto_detect_interface: true,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function writeSingboxConfig(config) {
|
|
fs.mkdirSync(path.dirname(settings.configPath), { recursive: true });
|
|
fs.writeFileSync(settings.configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
}
|