Skip to content

Commit d9c1ce6

Browse files
Mossakaclaude
andcommitted
fix: restore DNS forwarding rules in DOCKER-USER chain
Docker's embedded DNS (127.0.0.11) forwards queries to upstream servers through the container's network interface, which traverses the Docker bridge and DOCKER-USER chain. The previous commit incorrectly assumed Docker DNS bypasses container iptables entirely, but the DNS proxy runs within the container's network namespace. Without DNS ACCEPT rules in DOCKER-USER, forwarded queries are blocked, causing SERVFAIL. Add UDP/TCP port 53 ACCEPT rules for configured upstream DNS servers in the AWF_EGRESS chain, while keeping the simplified model where containers can only use Docker embedded DNS (no direct external DNS). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2f8546d commit d9c1ce6

3 files changed

Lines changed: 46 additions & 15 deletions

File tree

src/cli-workflow.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { WrapperConfig } from './types';
22

33
export interface WorkflowDependencies {
44
ensureFirewallNetwork: () => Promise<{ squidIp: string; agentIp: string; proxyIp: string; subnet: string }>;
5-
setupHostIptables: (squidIp: string, port: number, apiProxyIp?: string) => Promise<void>;
5+
setupHostIptables: (squidIp: string, port: number, apiProxyIp?: string, dnsServers?: string[]) => Promise<void>;
66
writeConfigs: (config: WrapperConfig) => Promise<void>;
77
startContainers: (workDir: string, allowedDomains: string[], proxyLogsDir?: string, skipPull?: boolean) => Promise<void>;
88
runAgentCommand: (
@@ -46,7 +46,7 @@ export async function runMainWorkflow(
4646
// When API proxy is enabled, allow agent→sidecar traffic at the host level.
4747
// The sidecar itself routes through Squid, so domain whitelisting is still enforced.
4848
const apiProxyIp = config.enableApiProxy ? networkConfig.proxyIp : undefined;
49-
await dependencies.setupHostIptables(networkConfig.squidIp, 3128, apiProxyIp);
49+
await dependencies.setupHostIptables(networkConfig.squidIp, 3128, apiProxyIp, config.dnsServers);
5050
onHostIptablesSetup?.();
5151

5252
// Step 1: Write configuration files

src/host-iptables.test.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,12 @@ describe('host-iptables', () => {
143143
'-j', 'ACCEPT',
144144
]);
145145

146-
// Verify no DNS-specific rules (simplified model uses Docker embedded DNS)
147-
expect(mockedExeca).not.toHaveBeenCalledWith('iptables', expect.arrayContaining(['--dport', '53']));
146+
// Verify DNS forwarding rules for default upstream servers
147+
expect(mockedExeca).toHaveBeenCalledWith('iptables', [
148+
'-t', 'filter', '-A', 'FW_WRAPPER',
149+
'-p', 'udp', '-d', '8.8.8.8', '--dport', '53',
150+
'-j', 'ACCEPT',
151+
]);
148152

149153
// Verify traffic to Squid rule
150154
expect(mockedExeca).toHaveBeenCalledWith('iptables', [
@@ -441,7 +445,7 @@ describe('host-iptables', () => {
441445
]);
442446
});
443447

444-
it('should not create IPv6 chain (no DNS-specific rules in simplified model)', async () => {
448+
it('should not create IPv6 chain but should add DNS forwarding rules', async () => {
445449
mockedExeca
446450
// Mock getNetworkBridgeName
447451
.mockResolvedValueOnce({
@@ -468,9 +472,19 @@ describe('host-iptables', () => {
468472

469473
await setupHostIptables('172.30.0.10', 3128);
470474

471-
// Verify no DNS-specific rules and no IPv6 chain
475+
// Verify no IPv6 chain
472476
expect(mockedExeca).not.toHaveBeenCalledWith('ip6tables', ['-t', 'filter', '-N', 'FW_WRAPPER_V6']);
473-
expect(mockedExeca).not.toHaveBeenCalledWith('iptables', expect.arrayContaining(['--dport', '53']));
477+
// DNS forwarding rules should exist for default upstream servers (8.8.8.8, 8.8.4.4)
478+
expect(mockedExeca).toHaveBeenCalledWith('iptables', [
479+
'-t', 'filter', '-A', 'FW_WRAPPER',
480+
'-p', 'udp', '-d', '8.8.8.8', '--dport', '53',
481+
'-j', 'ACCEPT',
482+
]);
483+
expect(mockedExeca).toHaveBeenCalledWith('iptables', [
484+
'-t', 'filter', '-A', 'FW_WRAPPER',
485+
'-p', 'udp', '-d', '8.8.4.4', '--dport', '53',
486+
'-j', 'ACCEPT',
487+
]);
474488
});
475489

476490
it('should disable IPv6 via sysctl when ip6tables unavailable', async () => {

src/host-iptables.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -143,15 +143,18 @@ export async function ensureFirewallNetwork(): Promise<{
143143
* Sets up host-level iptables rules using DOCKER-USER chain
144144
* This ensures ALL containers on the firewall network are subject to egress filtering.
145145
*
146-
* Simplified security model: only localhost and Squid proxy are allowed.
147-
* DNS resolution is handled by Docker's embedded DNS (127.0.0.11) which forwards
148-
* to upstream servers at the Docker daemon level, bypassing container iptables.
146+
* Simplified security model: only localhost, Squid proxy, and DNS forwarding are allowed.
147+
* Containers use Docker's embedded DNS (127.0.0.11) as their only nameserver.
148+
* Docker's DNS proxy forwards queries to upstream servers configured via docker-compose dns: field.
149+
* These forwarded queries traverse the Docker bridge and must be allowed in DOCKER-USER.
149150
* Squid resolves DNS internally for all HTTP/HTTPS traffic.
150151
*
151152
* @param squidIp - IP address of the Squid proxy
152153
* @param squidPort - Port number of the Squid proxy
154+
* @param apiProxyIp - Optional IP address of the API proxy sidecar
155+
* @param dnsServers - Upstream DNS servers that Docker embedded DNS forwards to
153156
*/
154-
export async function setupHostIptables(squidIp: string, squidPort: number, apiProxyIp?: string): Promise<void> {
157+
export async function setupHostIptables(squidIp: string, squidPort: number, apiProxyIp?: string, dnsServers?: string[]): Promise<void> {
155158
logger.info('Setting up host-level iptables rules...');
156159

157160
// Get the bridge interface name
@@ -269,10 +272,24 @@ export async function setupHostIptables(squidIp: string, squidPort: number, apiP
269272
await disableIpv6ViaSysctl();
270273
}
271274

272-
// Note: No DNS-specific rules needed. Docker's embedded DNS (127.0.0.11) handles
273-
// all name resolution. It forwards to upstream servers at the Docker daemon level,
274-
// which bypasses container iptables entirely. Squid resolves DNS internally for
275-
// all HTTP/HTTPS traffic.
275+
// 4b. Allow DNS forwarding to upstream servers
276+
// Docker's embedded DNS (127.0.0.11) proxies queries to upstream servers configured
277+
// via docker-compose dns: field. These forwarded queries traverse the Docker bridge
278+
// and need to be allowed here. Only the configured upstream servers are permitted.
279+
const upstreamDns = dnsServers && dnsServers.length > 0 ? dnsServers : ['8.8.8.8', '8.8.4.4'];
280+
logger.debug(`Allowing DNS forwarding to upstream servers: ${upstreamDns.join(', ')}`);
281+
for (const dnsServer of upstreamDns) {
282+
await execa('iptables', [
283+
'-t', 'filter', '-A', CHAIN_NAME,
284+
'-p', 'udp', '-d', dnsServer, '--dport', '53',
285+
'-j', 'ACCEPT',
286+
]);
287+
await execa('iptables', [
288+
'-t', 'filter', '-A', CHAIN_NAME,
289+
'-p', 'tcp', '-d', dnsServer, '--dport', '53',
290+
'-j', 'ACCEPT',
291+
]);
292+
}
276293

277294
// 5. Allow traffic to Squid proxy
278295
await execa('iptables', [

0 commit comments

Comments
 (0)