Split gateway control and dataplane into separate services
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import fs from 'node:fs';
|
||||
import http from 'node:http';
|
||||
import path from 'node:path';
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { isDeepStrictEqual } from 'node:util';
|
||||
import { createDataplaneClient } from './dataplaneClient.js';
|
||||
import { settings } from './config.js';
|
||||
import {
|
||||
applyGatewayPreference,
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
readHostNetworkState,
|
||||
sameGatewayRoute,
|
||||
} from './gatewayPresence.js';
|
||||
import { setGatewayInterception } from './gatewayRouting.js';
|
||||
import { createSingboxRuntime } from './singboxRuntime.js';
|
||||
import { tcpPing } from './ping.js';
|
||||
import { buildSharedProxyInfo } from './sharedProxy.js';
|
||||
import {
|
||||
@@ -29,8 +29,14 @@ const GATEWAY_DISCOVERY_INTERVAL_MS = 5_000;
|
||||
|
||||
fs.mkdirSync(settings.dataDir, { recursive: true });
|
||||
|
||||
let singboxProcess = null;
|
||||
let singboxStartedAt = null;
|
||||
const remoteDataplane = settings.appMode === 'gateway' && Boolean(process.env.DATAPLANE_SOCKET);
|
||||
const singboxRuntime = remoteDataplane
|
||||
? createDataplaneClient(settings.dataplaneSocket)
|
||||
: createSingboxRuntime({
|
||||
configPath: settings.configPath,
|
||||
gateway: settings.appMode === 'gateway',
|
||||
tproxyChain: settings.tproxyChain,
|
||||
});
|
||||
let subscriptionRefreshPromise = null;
|
||||
let subscriptionRefreshTimer = null;
|
||||
let gatewayDiscoveryPromise = null;
|
||||
@@ -110,79 +116,11 @@ function buildActiveConfig(subscriptionConfig, selectedTag) {
|
||||
});
|
||||
}
|
||||
|
||||
function checkSingboxConfig() {
|
||||
const result = spawnSync('sing-box', ['check', '-c', settings.configPath], {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error((result.stderr || result.stdout || 'sing-box check failed').trim());
|
||||
}
|
||||
}
|
||||
const stopSingbox = () => singboxRuntime.stop();
|
||||
const startSingbox = () => singboxRuntime.apply();
|
||||
|
||||
function stopSingbox() {
|
||||
return new Promise((resolve) => {
|
||||
if (settings.appMode === 'gateway') {
|
||||
setGatewayInterception(false, settings.tproxyChain);
|
||||
}
|
||||
if (!singboxProcess) {
|
||||
singboxStartedAt = null;
|
||||
return resolve();
|
||||
}
|
||||
|
||||
const current = singboxProcess;
|
||||
singboxProcess = null;
|
||||
singboxStartedAt = null;
|
||||
const timeout = setTimeout(() => {
|
||||
current.kill('SIGKILL');
|
||||
resolve();
|
||||
}, 4000);
|
||||
|
||||
current.once('exit', () => {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
});
|
||||
current.kill('SIGTERM');
|
||||
});
|
||||
}
|
||||
|
||||
async function startSingbox() {
|
||||
if (!fs.existsSync(settings.configPath)) {
|
||||
if (settings.appMode === 'gateway') {
|
||||
setGatewayInterception(false, settings.tproxyChain);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
checkSingboxConfig();
|
||||
await stopSingbox();
|
||||
|
||||
const child = spawn('sing-box', ['run', '-c', settings.configPath], {
|
||||
stdio: ['ignore', 'inherit', 'inherit'],
|
||||
});
|
||||
singboxProcess = child;
|
||||
singboxStartedAt = new Date().toISOString();
|
||||
try {
|
||||
if (settings.appMode === 'gateway') {
|
||||
setGatewayInterception(true, settings.tproxyChain);
|
||||
}
|
||||
} catch (error) {
|
||||
child.kill('SIGTERM');
|
||||
singboxProcess = null;
|
||||
singboxStartedAt = null;
|
||||
throw error;
|
||||
}
|
||||
child.once('exit', () => {
|
||||
if (singboxProcess === child) {
|
||||
singboxProcess = null;
|
||||
singboxStartedAt = null;
|
||||
if (settings.appMode === 'gateway') {
|
||||
setGatewayInterception(false, settings.tproxyChain);
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function publicState() {
|
||||
async function publicState() {
|
||||
await singboxRuntime.refresh();
|
||||
const state = readJson(settings.statePath, {});
|
||||
const gatewayAutoEnabled = state.gatewayAutoEnabled !== false;
|
||||
return {
|
||||
@@ -190,8 +128,8 @@ function publicState() {
|
||||
port: settings.port,
|
||||
proxyPort: settings.proxyPort,
|
||||
configExists: fs.existsSync(settings.configPath),
|
||||
singboxRunning: Boolean(singboxProcess),
|
||||
singboxStartedAt,
|
||||
singboxRunning: singboxRuntime.running,
|
||||
singboxStartedAt: singboxRuntime.startedAt,
|
||||
subscriptionHost: subscriptionHost(state.subscriptionUrl),
|
||||
hasSubscription: Boolean(state.subscriptionUrl),
|
||||
selectedTag: state.selectedTag || '',
|
||||
@@ -230,7 +168,7 @@ async function applyGatewayAutoState(nextState, { reconfigure = true } = {}) {
|
||||
const previousConfig = fs.existsSync(settings.configPath)
|
||||
? fs.readFileSync(settings.configPath, 'utf8')
|
||||
: null;
|
||||
const wasRunning = Boolean(singboxProcess);
|
||||
const wasRunning = singboxRuntime.running;
|
||||
try {
|
||||
const configured = writeCurrentConfig();
|
||||
if (reconfigure && configured && wasRunning) await startSingbox();
|
||||
@@ -388,9 +326,9 @@ function refreshSavedSubscription() {
|
||||
writeJson(settings.subscriptionCachePath, { url: subscriptionUrl, ...parsed });
|
||||
|
||||
try {
|
||||
if (singboxProcess && activeConfigChanged) await applySelectedServer(selectedTag);
|
||||
if (singboxRuntime.running && activeConfigChanged) await applySelectedServer(selectedTag);
|
||||
else if (selectedTag) {
|
||||
if (!singboxProcess) writeSingboxConfig(buildActiveConfig(parsed.config, selectedTag));
|
||||
if (!singboxRuntime.running) writeSingboxConfig(buildActiveConfig(parsed.config, selectedTag));
|
||||
} else {
|
||||
removeSingboxConfig();
|
||||
}
|
||||
@@ -428,14 +366,14 @@ function refreshSavedSubscription() {
|
||||
|
||||
async function handleApi(req, res) {
|
||||
if (req.method === 'GET' && req.url === '/api/state') {
|
||||
return sendJson(res, 200, publicState());
|
||||
return sendJson(res, 200, await publicState());
|
||||
}
|
||||
|
||||
if (req.method === 'GET' && req.url === '/api/shared-proxy') {
|
||||
return sendJson(res, 200, buildSharedProxyInfo({
|
||||
appMode: settings.appMode,
|
||||
proxyPort: settings.proxyPort,
|
||||
running: Boolean(singboxProcess),
|
||||
running: (await singboxRuntime.refresh()).running,
|
||||
hostHeader: req.headers.host,
|
||||
sharedProxyHost: settings.sharedProxyHost,
|
||||
}));
|
||||
@@ -508,7 +446,7 @@ async function handleApi(req, res) {
|
||||
});
|
||||
await applyGatewayAutoState(applyGatewayPreference(gatewayAutoState, enabled));
|
||||
});
|
||||
return sendJson(res, 200, { success: true, gatewayAuto: publicState().gatewayAuto });
|
||||
return sendJson(res, 200, { success: true, gatewayAuto: (await publicState()).gatewayAuto });
|
||||
}
|
||||
|
||||
if (req.method === 'DELETE' && req.url === '/api/subscription') {
|
||||
@@ -542,7 +480,7 @@ async function handleApi(req, res) {
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
await startSingbox();
|
||||
await singboxRuntime.restart();
|
||||
});
|
||||
return sendJson(res, 200, { success: true, singboxRunning: true });
|
||||
}
|
||||
@@ -591,7 +529,7 @@ const server = http.createServer(async (req, res) => {
|
||||
async function shutdown() {
|
||||
clearInterval(subscriptionRefreshTimer);
|
||||
clearInterval(gatewayDiscoveryTimer);
|
||||
await serializeControl(() => stopSingbox());
|
||||
await serializeControl(() => singboxRuntime.shutdown());
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user