Add local routing rules to Harbor
This commit is contained in:
@@ -30,6 +30,7 @@ import {
|
||||
withStateV0Compatibility,
|
||||
} from '../shared/contracts/state.js';
|
||||
import { HarborError, normalizeHarborError } from '../shared/errors.js';
|
||||
import { normalizeRouteRules } from '../shared/routingRules.js';
|
||||
import { createJsonStore, createStateStore } from './services/stateStore.js';
|
||||
import { buildVersionInfo } from './version.js';
|
||||
|
||||
@@ -192,9 +193,14 @@ function subscriptionHost(url) {
|
||||
}
|
||||
}
|
||||
|
||||
function buildActiveConfig(subscriptionConfig, selectedTag) {
|
||||
function buildActiveConfig(
|
||||
subscriptionConfig,
|
||||
selectedTag,
|
||||
routeRules = stateStore.read().routeRules,
|
||||
) {
|
||||
return buildGatewayConfig(subscriptionConfig, selectedTag, {
|
||||
clientDirect: settings.appMode === 'client' && gatewayAutoState.mode === 'gateway-direct',
|
||||
routeRules,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -383,6 +389,38 @@ async function applySelectedServer(selectedTag, { persist = true } = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
async function applyRouteRules(routeRules) {
|
||||
const state = normalizeStoredState(stateStore.read());
|
||||
const cached = readSubscriptionCache();
|
||||
if (!state.selectedTag || !cached?.config) {
|
||||
updateStoredState((current) => ({ ...current, routeRules }));
|
||||
return;
|
||||
}
|
||||
|
||||
const previousConfig = fs.existsSync(settings.configPath)
|
||||
? fs.readFileSync(settings.configPath, 'utf8')
|
||||
: null;
|
||||
const wasRunning = singboxRuntime.running;
|
||||
try {
|
||||
writeSingboxConfig(buildActiveConfig(cached.config, state.selectedTag, routeRules));
|
||||
if (wasRunning) await startSingbox();
|
||||
updateStoredState((current) => ({ ...current, routeRules }));
|
||||
} catch (error) {
|
||||
if (previousConfig === null) removeSingboxConfig();
|
||||
else restoreSingboxConfig(previousConfig);
|
||||
if (wasRunning) {
|
||||
try {
|
||||
await startSingbox();
|
||||
} catch (rollbackError) {
|
||||
throw new HarborError('PROCESS_START_FAILED', {
|
||||
cause: new AggregateError([error, rollbackError], 'Route rules rollback failed'),
|
||||
});
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function refreshSavedSubscription() {
|
||||
if (subscriptionRefreshPromise) return subscriptionRefreshPromise;
|
||||
|
||||
@@ -515,6 +553,7 @@ async function handleApi(req, res) {
|
||||
removeSingboxConfig();
|
||||
subscriptionCacheStore.write({ url: normalizedUrl, ...result });
|
||||
updateStoredState((state) => ({
|
||||
routeRules: state.routeRules,
|
||||
subscriptionUrl: normalizedUrl,
|
||||
gatewayAutoEnabled: state.gatewayAutoEnabled !== false,
|
||||
servers: result.servers,
|
||||
@@ -564,12 +603,32 @@ async function handleApi(req, res) {
|
||||
return sendJson(res, 200, { success: true, gatewayAuto: state.gatewayAuto, state });
|
||||
}
|
||||
|
||||
if (req.method === 'PUT' && req.url === '/api/route-rules') {
|
||||
const { rules, expectedRevision } = await readBody(req);
|
||||
let routeRules;
|
||||
try {
|
||||
routeRules = normalizeRouteRules(rules, { strict: true });
|
||||
} catch (cause) {
|
||||
throw new HarborError('REQUEST_INVALID', { cause });
|
||||
}
|
||||
if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0) {
|
||||
throw new HarborError('REQUEST_INVALID');
|
||||
}
|
||||
await serializeControl(async () => {
|
||||
const current = normalizeStoredState(stateStore.read());
|
||||
if (current.revision !== expectedRevision) throw new HarborError('STATE_CONFLICT');
|
||||
if (isDeepStrictEqual(current.routeRules, routeRules)) return;
|
||||
await withOperation('route-rules', () => applyRouteRules(routeRules));
|
||||
});
|
||||
return sendState(res);
|
||||
}
|
||||
|
||||
if (req.method === 'DELETE' && req.url === '/api/subscription') {
|
||||
await withOperation('subscription-forget', () => serializeControl(async () => {
|
||||
await stopSingbox();
|
||||
removeSingboxConfig();
|
||||
subscriptionCacheStore.remove();
|
||||
updateStoredState(() => ({}));
|
||||
updateStoredState((state) => ({ routeRules: state.routeRules }));
|
||||
gatewayAutoState = createGatewayAutoState();
|
||||
}));
|
||||
return sendState(res);
|
||||
|
||||
@@ -3,7 +3,7 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { normalizeStoredState } from '../../shared/contracts/state.js';
|
||||
|
||||
export const STATE_SCHEMA_VERSION = 1;
|
||||
export const STATE_SCHEMA_VERSION = 2;
|
||||
|
||||
const clone = (value) => structuredClone(value);
|
||||
const stamp = (value) => value.toISOString().replace(/[:.]/g, '-');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from 'node:fs';
|
||||
import { settings } from './config.js';
|
||||
import { HarborError } from '../shared/errors.js';
|
||||
import { BUILT_IN_DIRECT_RULES, normalizeRouteRules } from '../shared/routingRules.js';
|
||||
import { atomicWriteFile, atomicWriteJson } from './services/stateStore.js';
|
||||
|
||||
const PROXY_TYPES = new Set(['vless', 'vmess', 'trojan', 'shadowsocks', 'hysteria2']);
|
||||
@@ -17,7 +18,10 @@ function findOutbound(subscriptionConfig, selectedTag) {
|
||||
));
|
||||
}
|
||||
|
||||
export function buildGatewayConfig(subscriptionConfig, selectedTag, { clientDirect = false } = {}) {
|
||||
export function buildGatewayConfig(subscriptionConfig, selectedTag, {
|
||||
clientDirect = false,
|
||||
routeRules = [],
|
||||
} = {}) {
|
||||
const clientMode = settings.appMode === 'client';
|
||||
const directClient = clientMode && clientDirect;
|
||||
const vpnOutbound = directClient
|
||||
@@ -48,7 +52,10 @@ export function buildGatewayConfig(subscriptionConfig, selectedTag, { clientDire
|
||||
set_system_proxy: false,
|
||||
},
|
||||
];
|
||||
const directRules = [{ domain_suffix: ['ru'], outbound: 'direct' }];
|
||||
const directRules = [...BUILT_IN_DIRECT_RULES, ...normalizeRouteRules(routeRules)].map((rule) => ({
|
||||
[rule.type]: [rule.value],
|
||||
outbound: 'direct',
|
||||
}));
|
||||
const rules = clientMode
|
||||
? [...directRules, { inbound: [MIXED_INBOUND], outbound: outboundTag }]
|
||||
: [
|
||||
|
||||
Reference in New Issue
Block a user