Files
harbor-net/src/server/singbox.ts
T
dokril 5da9686c27
Build and Deploy Gateway / build-and-push (push) Successful in 19s
Build and Deploy Gateway / deploy (push) Successful in 13s
Update sing-box to 1.13.18
2026-08-09 13:05:51 +03:00

135 lines
4.2 KiB
TypeScript

import fs from 'node:fs';
import { settings } from './config.js';
import { HarborError } from '../shared/errors.js';
import { normalizeRouteRules } from '../shared/routingRules.js';
import { atomicWriteFile, atomicWriteJson } from './services/stateStore.js';
const PROXY_TYPES = new Set(['vless', 'vmess', 'trojan', 'shadowsocks', 'hysteria2']);
const MIXED_INBOUND = 'mixed-in';
const TPROXY_INBOUND = 'tproxy-in';
const DIAGNOSTICS_INBOUND = 'diagnostics-vpn-in';
const SNIFF_TIMEOUT = '1s';
const SNIFFERS = ['http', 'tls', 'quic'];
interface ProxyOutbound extends Record<string, unknown> {
tag?: string;
type?: string;
packet_encoding?: string;
}
function record(value: unknown): Record<string, unknown> {
return value && typeof value === 'object' && !Array.isArray(value)
? value as Record<string, unknown>
: {};
}
function findOutbound(subscriptionConfig: unknown, selectedTag: unknown): ProxyOutbound | undefined {
const config = record(subscriptionConfig);
const outbounds = Array.isArray(config.outbounds)
? config.outbounds.map(record)
: [];
const tag = String(selectedTag || '').trim();
return outbounds.find((outbound) => (
String(outbound.tag || '').trim() === tag && PROXY_TYPES.has(String(outbound.type || ''))
));
}
export function buildGatewayConfig(subscriptionConfig: unknown, selectedTag: unknown, {
clientDirect = false,
routeRules = [],
}: { clientDirect?: boolean; routeRules?: unknown } = {}) {
const clientMode = settings.appMode === 'client';
const directClient = clientMode && clientDirect;
const vpnOutbound = structuredClone(findOutbound(subscriptionConfig, selectedTag));
if (!vpnOutbound) throw new HarborError('SERVER_NOT_FOUND');
if (!vpnOutbound.tag) vpnOutbound.tag = 'vpn-out';
if (vpnOutbound.type === 'vless' && !vpnOutbound.packet_encoding) {
vpnOutbound.packet_encoding = 'xudp';
}
const outboundTag = directClient ? 'direct' : vpnOutbound.tag;
const inbounds = [
...(!clientMode ? [{
type: 'tproxy',
tag: TPROXY_INBOUND,
listen: '::',
listen_port: settings.tproxyPort,
}] : []),
{
type: 'mixed',
tag: MIXED_INBOUND,
listen: settings.bindIp,
listen_port: settings.proxyPort,
set_system_proxy: false,
},
{
type: 'mixed',
tag: DIAGNOSTICS_INBOUND,
listen: '127.0.0.1',
listen_port: settings.diagnosticsProxyPort,
set_system_proxy: false,
},
];
const directRules = normalizeRouteRules(routeRules)
.filter((rule) => rule.enabled)
.map((rule) => ({ [rule.type]: [rule.value], outbound: 'direct' }));
const rules = clientMode
? [
{
inbound: [MIXED_INBOUND, DIAGNOSTICS_INBOUND],
action: 'sniff',
sniffer: SNIFFERS,
timeout: SNIFF_TIMEOUT,
},
{ inbound: [DIAGNOSTICS_INBOUND], outbound: vpnOutbound.tag },
...directRules,
{ inbound: [MIXED_INBOUND], outbound: outboundTag },
]
: [
{
inbound: [TPROXY_INBOUND, MIXED_INBOUND, DIAGNOSTICS_INBOUND],
action: 'sniff',
sniffer: SNIFFERS,
timeout: SNIFF_TIMEOUT,
},
{ inbound: [DIAGNOSTICS_INBOUND], outbound: outboundTag },
...directRules,
{ inbound: [TPROXY_INBOUND], outbound: outboundTag },
{ inbound: [MIXED_INBOUND], outbound: outboundTag },
];
return {
log: { level: settings.logLevel, timestamp: true },
experimental: {
cache_file: { enabled: true, path: settings.cachePath },
...(!clientMode ? {
clash_api: { external_controller: `127.0.0.1:${settings.singboxApiPort}` },
} : {}),
},
dns: { independent_cache: true },
inbounds,
outbounds: [
vpnOutbound,
{ type: 'direct', tag: 'direct' },
],
route: {
rule_set: [],
rules,
final: outboundTag,
...(clientMode ? {} : { auto_detect_interface: true }),
},
};
}
export function writeSingboxConfig(config: unknown) {
atomicWriteJson(settings.configPath, config);
}
export function restoreSingboxConfig(contents: string) {
atomicWriteFile(settings.configPath, contents);
}
export function removeSingboxConfig() {
fs.rmSync(settings.configPath, { force: true });
}