Rebuild vpn proxy around gateway mode

This commit is contained in:
2026-05-08 16:04:38 +03:00
parent a3816cbedc
commit ef752d66bc
66 changed files with 1884 additions and 14734 deletions

175
src/server/singbox.js Normal file
View 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');
}