mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(browser): remote tunnel matches Chromium for IPv6 bypass rules and <-loopback> (#32665)
This commit is contained in:
@@ -30,12 +30,7 @@ export function selectBrowserNetworkTunnelProxy(
|
||||
const bypassRules = fromSettings
|
||||
? normalizeProxyBypassRules(settings?.httpProxyBypassRules)
|
||||
: getProxyBypassRulesFromEnvironment(env)
|
||||
if (
|
||||
// Match browser sessions' default loopback bypass.
|
||||
isEligibleLocalCertificateHost(target.host) ||
|
||||
isLoopbackAddress(target.host) ||
|
||||
matchesProxyBypassRules(target, bypassRules, fromSettings ? 'chromium' : 'no-proxy')
|
||||
) {
|
||||
if (matchesProxyBypassRules(target, bypassRules, fromSettings ? 'chromium' : 'no-proxy')) {
|
||||
return { kind: 'direct' }
|
||||
}
|
||||
const url = new URL(proxyUrl)
|
||||
@@ -48,16 +43,20 @@ export function selectBrowserNetworkTunnelProxy(
|
||||
return { kind: 'unsupported', protocol: url.protocol }
|
||||
}
|
||||
|
||||
// BlockList also recognizes expanded IPv6 loopback addresses from the SOCKS front end.
|
||||
const LOOPBACK_ADDRESSES = new BlockList()
|
||||
LOOPBACK_ADDRESSES.addSubnet('127.0.0.0', 8, 'ipv4')
|
||||
LOOPBACK_ADDRESSES.addAddress('::1', 'ipv6')
|
||||
LOOPBACK_ADDRESSES.addSubnet('::ffff:127.0.0.0', 104, 'ipv6')
|
||||
// BlockList also recognizes expanded IPv6 addresses from the SOCKS front end.
|
||||
const IMPLICIT_BYPASS_ADDRESSES = new BlockList()
|
||||
IMPLICIT_BYPASS_ADDRESSES.addSubnet('127.0.0.0', 8, 'ipv4')
|
||||
IMPLICIT_BYPASS_ADDRESSES.addAddress('::1', 'ipv6')
|
||||
IMPLICIT_BYPASS_ADDRESSES.addSubnet('::ffff:127.0.0.0', 104, 'ipv6')
|
||||
IMPLICIT_BYPASS_ADDRESSES.addSubnet('169.254.0.0', 16, 'ipv4')
|
||||
IMPLICIT_BYPASS_ADDRESSES.addSubnet('fe80::', 10, 'ipv6')
|
||||
|
||||
function isLoopbackAddress(host: string): boolean {
|
||||
const address = normalizeHost(host)
|
||||
const family = isIP(address)
|
||||
return family !== 0 && LOOPBACK_ADDRESSES.check(address, family === 4 ? 'ipv4' : 'ipv6')
|
||||
// Chromium never proxies localhost, loopback or link-local unless `<-loopback>` subtracts them.
|
||||
function matchesImplicitBypassRules(host: string): boolean {
|
||||
const family = isIP(host)
|
||||
return family === 0
|
||||
? isEligibleLocalCertificateHost(host)
|
||||
: IMPLICIT_BYPASS_ADDRESSES.check(host, family === 4 ? 'ipv4' : 'ipv6')
|
||||
}
|
||||
|
||||
function normalizeHost(host: string): string {
|
||||
@@ -66,17 +65,22 @@ function normalizeHost(host: string): string {
|
||||
}
|
||||
|
||||
// Bare hosts match exactly in Chromium rules and include subdomains in NO_PROXY.
|
||||
// As in Chromium, the last matching rule wins (`<-loopback>` subtracts), then implicit rules apply.
|
||||
export function matchesProxyBypassRules(
|
||||
target: Pick<BrowserNetworkTunnelOpen, 'host' | 'port'>,
|
||||
rules: string,
|
||||
style: 'chromium' | 'no-proxy'
|
||||
): boolean {
|
||||
const host = normalizeHost(target.host)
|
||||
return rules
|
||||
const implicit = matchesImplicitBypassRules(host)
|
||||
const decisive = rules
|
||||
.split(';')
|
||||
.map((rule) => rule.trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
.some((rule) => matchesBypassRule(host, target.port, rule, style))
|
||||
.findLast((rule) =>
|
||||
rule === '<-loopback>' ? implicit : matchesBypassRule(host, target.port, rule, style)
|
||||
)
|
||||
return decisive === undefined ? implicit : decisive !== '<-loopback>'
|
||||
}
|
||||
|
||||
const SCHEME_PORTS: Record<string, string> = { http: '80', ws: '80', https: '443', wss: '443' }
|
||||
@@ -110,6 +114,10 @@ function matchesBypassRule(
|
||||
if (rulePort !== undefined && Number(rulePort) !== port) {
|
||||
return false
|
||||
}
|
||||
const ruleFamily = isIP(ruleHost)
|
||||
if (ruleFamily !== 0) {
|
||||
return matchesCidr(host, ruleHost, ruleFamily === 4 ? 32 : 128)
|
||||
}
|
||||
const pattern = stripTrailingDot(ruleHost.startsWith('.') ? `*${ruleHost}` : ruleHost)
|
||||
if (pattern.includes('*')) {
|
||||
// NO_PROXY also reads `.corp` and `*.corp` as the domain itself; Chromium does not.
|
||||
|
||||
@@ -191,6 +191,8 @@ describe('proxy bypass rules', () => {
|
||||
['10.0.0.0/8', '10.20.30.40', 443, true, true],
|
||||
['10.0.0.0/8', '11.0.0.1', 443, false, false],
|
||||
['[fd00::1]', 'fd00::1', 443, true, true],
|
||||
['[fd00::1]', 'fd00:0:0:0:0:0:0:1', 443, true, true],
|
||||
['fd00:0::1', '[FD00::1]', 443, true, true],
|
||||
['https://corp.test', 'corp.test', 443, true, true],
|
||||
['https://corp.test', 'corp.test', 80, false, false],
|
||||
['ftp://corp.test', 'corp.test', 21, false, false],
|
||||
@@ -199,9 +201,20 @@ describe('proxy bypass rules', () => {
|
||||
['192.168.*', '192.168.4.20', 443, true, true],
|
||||
['api.*.corp.test', 'api.eu.corp.test', 443, true, true],
|
||||
['api.*.corp.test', 'web.eu.corp.test', 443, false, false],
|
||||
['corp.test', 'corp.test.', 443, true, true]
|
||||
['corp.test', 'corp.test.', 443, true, true],
|
||||
['', '169.254.169.254', 80, true, true],
|
||||
['<-loopback>', 'localhost', 3000, false, false],
|
||||
['<-loopback>;<local>', 'localhost', 3000, true, true],
|
||||
['<local>;<-loopback>', 'localhost', 3000, false, false],
|
||||
['<local>;<-loopback>', 'intranet', 80, true, true]
|
||||
])('%s against %s:%d (NO_PROXY %s, Chromium %s)', (rule, host, port, noProxy, chromium) => {
|
||||
expect(matchesProxyBypassRules({ host, port }, rule, 'no-proxy')).toBe(noProxy)
|
||||
expect(matchesProxyBypassRules({ host, port }, rule, 'chromium')).toBe(chromium)
|
||||
})
|
||||
|
||||
it('proxies loopback destinations when the bypass list subtracts them', () => {
|
||||
const settings = { httpProxyUrl: 'http://proxy.test:8080', httpProxyBypassRules: '<-loopback>' }
|
||||
const target = { host: '127.0.0.1', port: 3000 }
|
||||
expect(selectBrowserNetworkTunnelProxy(target, settings, {})).toMatchObject({ kind: 'http' })
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user