Skip to content

Commit 9fef1a1

Browse files
committed
tls: Revert back to intercept outbound all for dynamic rules until we are able to remove hostnames programmatically
1 parent 087abd4 commit 9fef1a1

2 files changed

Lines changed: 65 additions & 81 deletions

File tree

docs/egress.md

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -259,20 +259,23 @@ The request is blocked (HTTP 520).
259259

260260
## Interception strategy
261261

262-
The library avoids intercepting all outbound traffic when it is not necessary.
263-
264-
- **Catch-all interception** (`interceptAllOutboundHttp`) is only used when a
265-
catch-all `outbound` handler or a runtime `setOutboundHandler` override is
266-
configured. All outbound HTTP goes through `ContainerProxy`.
267-
- **Per-host interception** (`interceptOutboundHttp`) is used in all other
268-
cases. Only traffic to known hosts (from `outboundByHost`, `allowedHosts`,
269-
`deniedHosts`, and runtime overrides) is routed through `ContainerProxy`.
270-
Everything else follows the container's default network behaviour
271-
(`enableInternet`).
262+
The library avoids intercepting all outbound traffic when it is not necessary,
263+
but only keeps the per-host optimization for the narrow static case.
264+
265+
- **Intercept-all mode** (`interceptAllOutboundHttp`) is used whenever the
266+
container needs to evaluate all hosts, including catch-all `outbound`, a
267+
runtime `setOutboundHandler` override, any `allowedHosts` / `deniedHosts`
268+
configuration, or runtime-mutated outbound config such as
269+
`setOutboundByHost()`.
270+
- **Per-host interception** (`interceptOutboundHttp`) is only used for static
271+
`outboundByHost` rules when there is no catch-all handler and no allow/deny
272+
configuration. Only those known static hosts are routed through
273+
`ContainerProxy`; everything else follows the container's default network
274+
behaviour (`enableInternet`).
272275

273276
When `interceptHttps` is `true`:
274277

275-
- In catch-all mode, `interceptOutboundHttps('*', ...)` intercepts all HTTPS.
278+
- In intercept-all mode, `interceptOutboundHttps('*', ...)` intercepts all HTTPS.
276279
- In per-host mode, `interceptOutboundHttps(host, ...)` is called for each
277280
known host individually.
278281

src/lib/container.ts

Lines changed: 51 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -372,8 +372,8 @@ export class ContainerProxy extends WorkerEntrypoint<Cloudflare.Env, ContainerPr
372372
}
373373

374374
// In per-host mode, only specific hosts were intercepted.
375-
// No catch-all or enableInternet fallback applies — if no handler
376-
// matched above, the host was intercepted for allow/deny only.
375+
// If no handler matched above, fall back to direct internet access only
376+
// when the container already allows it.
377377
if (!interceptAll) {
378378
if (allowedHosts || enableInternet) {
379379
return fetch(request);
@@ -635,7 +635,6 @@ export class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
635635
async removeOutboundByHost(hostname: string): Promise<void> {
636636
delete this.outboundByHostOverrides[hostname];
637637
await this.refreshOutboundInterception();
638-
await this.cleanupRemovedHostInterception([hostname]);
639638
}
640639

641640
/**
@@ -653,18 +652,13 @@ export class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
653652
this.validateOutboundHandlerMethodName(methodName);
654653
}
655654

656-
const removedHosts = Object.keys(this.outboundByHostOverrides).filter(
657-
hostname => !(hostname in handlers)
658-
);
659-
660655
this.outboundByHostOverrides = Object.fromEntries(
661656
Object.entries(handlers).map(([hostname, handler]) => [
662657
hostname,
663658
typeof handler === 'string' ? { method: handler } : handler,
664659
])
665660
);
666661
await this.refreshOutboundInterception();
667-
await this.cleanupRemovedHostInterception(removedHosts);
668662
}
669663

670664
// ====================================
@@ -678,11 +672,9 @@ export class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
678672
* @param hosts - Array of hostnames to allow (e.g. `['api.stripe.com', 'example.com']`)
679673
*/
680674
async setAllowedHosts(hosts: string[]): Promise<void> {
681-
const removedHosts = (this.effectiveAllowedHosts ?? []).filter(host => !hosts.includes(host));
682675
this.allowedHostsOverride = [...hosts];
683676
this.usingInterception = true;
684677
await this.refreshOutboundInterception();
685-
await this.cleanupRemovedHostInterception(removedHosts);
686678
}
687679

688680
/**
@@ -693,11 +685,9 @@ export class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
693685
* @param hosts - Array of hostnames to deny (e.g. `['evil.com', 'blocked.org']`)
694686
*/
695687
async setDeniedHosts(hosts: string[]): Promise<void> {
696-
const removedHosts = (this.effectiveDeniedHosts ?? []).filter(host => !hosts.includes(host));
697688
this.deniedHostsOverride = [...hosts];
698689
this.usingInterception = true;
699690
await this.refreshOutboundInterception();
700-
await this.cleanupRemovedHostInterception(removedHosts);
701691
}
702692

703693
/**
@@ -736,7 +726,6 @@ export class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
736726
async removeAllowedHost(hostname: string): Promise<void> {
737727
this.allowedHostsOverride = (this.effectiveAllowedHosts ?? []).filter(h => h !== hostname);
738728
await this.refreshOutboundInterception();
739-
await this.cleanupRemovedHostInterception([hostname]);
740729
}
741730

742731
/**
@@ -747,7 +736,6 @@ export class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
747736
async removeDeniedHost(hostname: string): Promise<void> {
748737
this.deniedHostsOverride = (this.effectiveDeniedHosts ?? []).filter(h => h !== hostname);
749738
await this.refreshOutboundInterception();
750-
await this.cleanupRemovedHostInterception([hostname]);
751739
}
752740

753741
// ==========================
@@ -1353,6 +1341,10 @@ export class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
13531341
private allowedHostsOverride?: string[];
13541342
private deniedHostsOverride?: string[];
13551343

1344+
// The runtime does not expose a way to remove outbound interceptions yet, so
1345+
// once we promote an instance to intercept-all we must keep using it.
1346+
private hasInterceptAllRegistration = false;
1347+
13561348
// ==========================
13571349
// GENERAL HELPERS
13581350
// ==========================
@@ -1451,13 +1443,33 @@ export class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
14511443
return ctor.outbound !== undefined || this.outboundHandlerOverride !== undefined;
14521444
}
14531445

1446+
private hasMutableOutboundConfiguration(): boolean {
1447+
return (
1448+
Object.keys(this.outboundByHostOverrides).length > 0 ||
1449+
this.allowedHostsOverride !== undefined ||
1450+
this.deniedHostsOverride !== undefined
1451+
);
1452+
}
1453+
1454+
private shouldInterceptAllOutbound(): boolean {
1455+
return (
1456+
this.hasInterceptAllRegistration ||
1457+
this.needsCatchAllInterception() ||
1458+
this.effectiveAllowedHosts !== undefined ||
1459+
this.effectiveDeniedHosts !== undefined ||
1460+
this.hasMutableOutboundConfiguration()
1461+
);
1462+
}
1463+
1464+
private getStaticOutboundByHostKeys(): string[] {
1465+
const ctor = this.constructor as typeof Container;
1466+
return ctor.outboundByHost ? Object.keys(ctor.outboundByHost) : [];
1467+
}
1468+
14541469
/**
14551470
* Collects all hostnames that need per-host outbound interception.
1456-
* This is the union of:
1457-
* - Static `outboundByHost` keys
1458-
* - Runtime `outboundByHostOverrides` keys
1459-
* - `allowedHosts`
1460-
* - `deniedHosts`
1471+
* This path is only used for the narrow optimized case where outbound
1472+
* handling is static and host-specific.
14611473
*/
14621474
private getHostsToIntercept(): string[] {
14631475
const hosts = new Set<string>();
@@ -1473,14 +1485,6 @@ export class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
14731485
hosts.add(hostname);
14741486
}
14751487

1476-
for (const hostname of this.effectiveAllowedHosts ?? []) {
1477-
hosts.add(hostname);
1478-
}
1479-
1480-
for (const hostname of this.effectiveDeniedHosts ?? []) {
1481-
hosts.add(hostname);
1482-
}
1483-
14841488
return [...hosts];
14851489
}
14861490

@@ -1493,51 +1497,17 @@ export class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
14931497
await this.applyOutboundInterceptionPromise;
14941498
}
14951499

1496-
private async cleanupRemovedHostInterception(hosts: string[]): Promise<void> {
1497-
if (hosts.length === 0) {
1498-
return;
1499-
}
1500-
1501-
const ctx = this.ctx as unknown as {
1502-
exports?: { ContainerProxy?: (params: { props: {} }) => Fetcher };
1503-
};
1504-
if (ctx.exports?.ContainerProxy === undefined) {
1505-
return;
1506-
}
1507-
1508-
const outboundConfiguration = this.getOutboundConfiguration();
1509-
const fetcher = ctx.exports.ContainerProxy({
1510-
props: {
1511-
enableInternet: this.enableInternet,
1512-
containerId: this.ctx.id.toString(),
1513-
className: this.constructor.name,
1514-
outboundByHostOverrides: outboundConfiguration.outboundByHostOverrides,
1515-
outboundHandlerOverride: outboundConfiguration.outboundHandlerOverride,
1516-
allowedHosts: outboundConfiguration.allowedHosts,
1517-
deniedHosts: outboundConfiguration.deniedHosts,
1518-
interceptAll: true,
1519-
},
1520-
});
1521-
1522-
for (const host of new Set(hosts)) {
1523-
await this.container.interceptOutboundHttp(host, fetcher);
1524-
1525-
if (this.interceptHttps) {
1526-
await this.container.interceptOutboundHttps(host, fetcher);
1527-
}
1528-
}
1529-
}
1530-
15311500
/**
15321501
* Applies (or re-applies) outbound HTTP interception with the current
15331502
* default registries + runtime overrides passed through ContainerProxy props.
15341503
*
1535-
* Uses `interceptAllOutboundHttp` when a catch-all outbound handler is
1536-
* configured. Otherwise, sets up per-host interception for known hosts only,
1537-
* avoiding unnecessary overhead for unintercepted traffic.
1504+
* Uses per-host interception only for static host-specific outbound handlers.
1505+
* As soon as the config needs to evaluate all hosts (catch-all outbound,
1506+
* allow/deny lists, or runtime-mutated outbound config), we promote the
1507+
* container to intercept-all and keep it there until the instance restarts.
15381508
*
15391509
* When `interceptHttps` is enabled, also applies HTTPS interception:
1540-
* - Catch-all mode: `interceptOutboundHttps('*', ...)` for all HTTPS traffic
1510+
* - Intercept-all mode: `interceptOutboundHttps('*', ...)` for all HTTPS traffic
15411511
* - Per-host mode: `interceptOutboundHttps(host, ...)` for each known host
15421512
*/
15431513
private async applyOutboundInterception(): Promise<void> {
@@ -1559,7 +1529,7 @@ export class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
15591529
const outboundConfiguration = this.getOutboundConfiguration();
15601530
this.persistOutboundConfiguration(outboundConfiguration);
15611531

1562-
const needsCatchAll = this.needsCatchAllInterception();
1532+
const interceptAll = this.shouldInterceptAllOutbound();
15631533
const hosts = this.getHostsToIntercept();
15641534

15651535
const fetcher = ctx.exports.ContainerProxy({
@@ -1571,13 +1541,24 @@ export class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
15711541
outboundHandlerOverride: outboundConfiguration.outboundHandlerOverride,
15721542
allowedHosts: outboundConfiguration.allowedHosts,
15731543
deniedHosts: outboundConfiguration.deniedHosts,
1574-
interceptAll: needsCatchAll,
1544+
interceptAll,
15751545
},
15761546
});
15771547

1578-
if (needsCatchAll) {
1579-
// Catch-all: intercept all outbound HTTP traffic
1548+
if (interceptAll) {
1549+
// Intercept-all: intercept all outbound HTTP traffic
15801550
await this.container.interceptAllOutboundHttp(fetcher);
1551+
this.hasInterceptAllRegistration = true;
1552+
1553+
// If we previously installed static per-host interceptors, refresh them
1554+
// with the current fetcher so they follow the latest config too.
1555+
for (const host of this.getStaticOutboundByHostKeys()) {
1556+
await this.container.interceptOutboundHttp(host, fetcher);
1557+
1558+
if (this.interceptHttps) {
1559+
await this.container.interceptOutboundHttps(host, fetcher);
1560+
}
1561+
}
15811562

15821563
// If HTTPS interception is enabled, intercept all HTTPS traffic too
15831564
if (this.interceptHttps) {

0 commit comments

Comments
 (0)