Rebuild vpn proxy around gateway mode
This commit is contained in:
175
src/server/singbox.js
Normal file
175
src/server/singbox.js
Normal file
@@ -0,0 +1,175 @@
|
||||
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');
|
||||
}
|
||||
Reference in New Issue
Block a user