feat(proxy): add destination overrides and fix PAC helpers

Add a hot-reloaded override table (overrides.json) that forces specific
hosts/IPs to a chosen profile regardless of the active profile, applied
across all bridge ports. Matching supports exact host, wildcard, domain
suffix, single IP, and CIDR. Managed via `proxy-bridge override
list|add|remove|clear` and surfaced in status/config.

Also fix two PAC sandbox helpers: localHostOrDomainIs (unqualified-host
case) and isInNet (was a no-op stub, now does real IPv4 subnet matching).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
tobias
2026-05-28 21:58:11 +02:00
parent 7f73746427
commit 401b3e1781
2 changed files with 190 additions and 19 deletions
+70
View File
@@ -12,6 +12,7 @@ const LISTEN_PORT = Number(process.env.PROXY_BRIDGE_LISTEN_PORT || 8888);
const USER_FILE = process.env.PROXY_BRIDGE_USER_FILE || '/opt/proxy-bridge/user.json';
const CONFIG_FILE = process.env.PROXY_BRIDGE_CONFIG_FILE || '/opt/proxy-bridge/config.json';
const PROFILE_FILE = '/opt/proxy-bridge/profile.json';
const OVERRIDES_FILE = process.env.PROXY_BRIDGE_OVERRIDES_FILE || '/opt/proxy-bridge/overrides.json';
const LOCAL_PROXY_URL = `http://${LISTEN_HOST}:${LISTEN_PORT}`;
const VSCODE_CERT_FLAG = ''; // e.g. '--ignore-certificate-errors'
@@ -81,6 +82,60 @@ function setProfile(profile) {
console.log(`Profile set to: ${profile}`);
}
function readOverrides() {
try {
const data = readJsonFile(OVERRIDES_FILE);
if (Array.isArray(data)) return data;
if (Array.isArray(data.rules)) return data.rules;
} catch {}
return [];
}
function writeOverrides(rules) {
writeJsonFile(OVERRIDES_FILE, { rules });
}
function overrideCmd(args) {
const action = args[0] || 'list';
switch (action) {
case 'list': {
const rules = readOverrides();
if (!rules.length) { console.log('No overrides configured.'); return; }
console.log('Overrides (first match wins):');
rules.forEach((r, i) => console.log(` [${i}] ${r.match} -> ${r.profile}`));
return;
}
case 'add': {
const [match, profile] = [args[1], args[2]];
if (!match || !profile) { console.error('Usage: proxy-bridge override add <host|ip|cidr|glob> <profile>'); process.exit(1); }
const valid = getAvailableProfiles();
if (!valid.includes(profile)) { console.error(`Invalid profile: ${profile}. Valid options: ${valid.join(', ')}`); process.exit(1); }
const rules = readOverrides().filter(r => r.match !== match);
rules.push({ match, profile });
writeOverrides(rules);
console.log(`Override set: ${match} -> ${profile}`);
return;
}
case 'remove': {
const match = args[1];
if (!match) { console.error('Usage: proxy-bridge override remove <match>'); process.exit(1); }
const before = readOverrides();
const after = before.filter(r => r.match !== match);
if (after.length === before.length) { console.log(`No override found for: ${match}`); return; }
writeOverrides(after);
console.log(`Removed override: ${match}`);
return;
}
case 'clear':
writeOverrides([]);
console.log('All overrides cleared.');
return;
default:
console.error('Usage: proxy-bridge override [list|add <match> <profile>|remove <match>|clear]');
process.exit(1);
}
}
function systemctl(args, options = {}) {
return run('systemctl', ['--user', ...args], options);
}
@@ -133,6 +188,7 @@ async function status() {
console.log(`pid: ${pid && pid !== '0' ? pid : 'none'}`);
if (isUpstreamEnabled()) console.log(`account: ${readAccount()}`);
console.log(`upstream: ${getUpstreamConfig()}`);
console.log(`overrides: ${readOverrides().length}`);
console.log(`\nPort Mappings:`);
console.log(` ${LISTEN_PORT.toString().padEnd(5)} -> dynamic (currently: ${current})${listening ? '' : ' [NOT REACHABLE]'}`);
all.forEach((name, i) => {
@@ -170,6 +226,10 @@ function config() {
all.forEach((name, i) => {
console.log(` ${(LISTEN_PORT + 1 + i).toString().padEnd(5)} -> ${name}`);
});
const overrides = readOverrides();
console.log(`\nOverrides (first match wins, apply to all ports):`);
if (!overrides.length) console.log(' (none)');
else overrides.forEach((r, i) => console.log(` [${i}] ${r.match} -> ${r.profile}`));
}
function codeSettingsPath() {
@@ -293,6 +353,7 @@ Commands:
status Show service, listener, upstream, profile, and account
profile [name] Show or set the active routing profile
toggle Cycle through profiles (internet -> direct -> off -> [pacs...])
override [...] Manage destination overrides (see below)
start Start the local proxy bridge
stop Stop the local proxy bridge
restart Restart the local proxy bridge
@@ -304,6 +365,14 @@ Commands:
vscode Manage VS Code proxy settings
help Show this help
Overrides (force a destination to a profile, regardless of active profile):
proxy-bridge override list
proxy-bridge override add <match> <profile>
proxy-bridge override remove <match>
proxy-bridge override clear
<match> may be an exact host, a wildcard (*.example.com), a domain
suffix (.example.com), an IP (192.168.1.1), or a CIDR (10.0.0.0/8).
VS Code:
proxy-bridge vscode status
proxy-bridge vscode setup
@@ -326,6 +395,7 @@ async function main() {
const next = profiles[(profiles.indexOf(current) + 1) % profiles.length];
setProfile(next);
break;
case 'override': overrideCmd(process.argv.slice(3)); break;
case 'start':
case 'stop':
case 'restart':