Skip to content

Commit 537e383

Browse files
Mossakaclaude
andcommitted
fix(api-proxy): use IP address for API base URLs to avoid DNS issues
In chroot mode, Docker container hostname resolution can fail because the DNS resolver may not properly reach Docker's embedded DNS. Use the api-proxy IP address directly (e.g., http://172.30.0.30:10001) instead of the hostname (http://api-proxy:10001) to eliminate DNS resolution as a failure point. Also add test coverage for the host-iptables api-proxy ACCEPT rule. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1c3e5f4 commit 537e383

7 files changed

Lines changed: 81 additions & 29 deletions

File tree

containers/api-proxy/package-lock.json

Lines changed: 47 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

containers/api-proxy/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
},
99
"dependencies": {
1010
"express": "^4.18.2",
11-
"http-proxy-middleware": "^2.0.6"
11+
"http-proxy-middleware": "^2.0.6",
12+
"https-proxy-agent": "^7.0.0"
1213
},
1314
"engines": {
1415
"node": ">=18.0.0"

containers/api-proxy/server.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
const express = require('express');
1414
const { createProxyMiddleware } = require('http-proxy-middleware');
15+
const { HttpsProxyAgent } = require('https-proxy-agent');
1516

1617
// Read API keys from environment (set by docker-compose)
1718
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
@@ -21,6 +22,16 @@ const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
2122
const HTTP_PROXY = process.env.HTTP_PROXY;
2223
const HTTPS_PROXY = process.env.HTTPS_PROXY;
2324

25+
// Create proxy agent to route outbound HTTPS through Squid
26+
// http-proxy-middleware doesn't use HTTP_PROXY env vars natively,
27+
// so we create an explicit agent that tunnels through Squid
28+
const proxyAgent = HTTPS_PROXY ? new HttpsProxyAgent(HTTPS_PROXY) : undefined;
29+
if (proxyAgent) {
30+
console.log('[API Proxy] Using Squid proxy agent for outbound HTTPS connections');
31+
} else {
32+
console.log('[API Proxy] WARNING: No HTTPS_PROXY configured, connections will be direct');
33+
}
34+
2435
console.log('[API Proxy] Starting AWF API proxy sidecar...');
2536
console.log(`[API Proxy] HTTP_PROXY: ${HTTP_PROXY}`);
2637
console.log(`[API Proxy] HTTPS_PROXY: ${HTTPS_PROXY}`);
@@ -54,6 +65,7 @@ if (OPENAI_API_KEY) {
5465
target: 'https://api.openai.com',
5566
changeOrigin: true,
5667
secure: true,
68+
agent: proxyAgent,
5769
onProxyReq: (proxyReq, req, res) => {
5870
// Inject Authorization header
5971
proxyReq.setHeader('Authorization', `Bearer ${OPENAI_API_KEY}`);
@@ -89,6 +101,7 @@ if (ANTHROPIC_API_KEY) {
89101
target: 'https://api.anthropic.com',
90102
changeOrigin: true,
91103
secure: true,
104+
agent: proxyAgent,
92105
onProxyReq: (proxyReq, req, res) => {
93106
// Inject Anthropic authentication headers
94107
proxyReq.setHeader('x-api-key', ANTHROPIC_API_KEY);

src/cli-workflow.ts

Lines changed: 3 additions & 4 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, dnsServers: string[], apiProxyIp?: string) => Promise<void>;
5+
setupHostIptables: (squidIp: string, port: number, dnsServers: string[]) => Promise<void>;
66
writeConfigs: (config: WrapperConfig) => Promise<void>;
77
startContainers: (workDir: string, allowedDomains: string[], proxyLogsDir?: string, skipPull?: boolean) => Promise<void>;
88
runAgentCommand: (
@@ -43,9 +43,8 @@ export async function runMainWorkflow(
4343
logger.info('Setting up host-level firewall network and iptables rules...');
4444
const networkConfig = await dependencies.ensureFirewallNetwork();
4545
const dnsServers = config.dnsServers || ['8.8.8.8', '8.8.4.4'];
46-
// Pass api-proxy IP so host iptables allows its outbound traffic to external APIs
47-
const apiProxyIp = config.enableApiProxy ? networkConfig.proxyIp : undefined;
48-
await dependencies.setupHostIptables(networkConfig.squidIp, 3128, dnsServers, apiProxyIp);
46+
// API proxy routes through Squid via https-proxy-agent, no firewall exemption needed
47+
await dependencies.setupHostIptables(networkConfig.squidIp, 3128, dnsServers);
4948
onHostIptablesSetup?.();
5049

5150
// Step 1: Write configuration files

src/docker-manager.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1584,7 +1584,7 @@ describe('docker-manager', () => {
15841584
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
15851585
const agent = result.services.agent;
15861586
const env = agent.environment as Record<string, string>;
1587-
expect(env.OPENAI_BASE_URL).toBe('http://api-proxy:10000');
1587+
expect(env.OPENAI_BASE_URL).toBe('http://172.30.0.30:10000');
15881588
});
15891589

15901590
it('should configure HTTP_PROXY and HTTPS_PROXY in api-proxy to route through Squid', () => {
@@ -1601,16 +1601,16 @@ describe('docker-manager', () => {
16011601
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
16021602
const agent = result.services.agent;
16031603
const env = agent.environment as Record<string, string>;
1604-
expect(env.ANTHROPIC_BASE_URL).toBe('http://api-proxy:10001');
1604+
expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001');
16051605
});
16061606

16071607
it('should set both BASE_URL variables when both keys are provided', () => {
16081608
const configWithProxy = { ...mockConfig, enableApiProxy: true, openaiApiKey: 'sk-test-openai-key', anthropicApiKey: 'sk-ant-test-key' };
16091609
const result = generateDockerCompose(configWithProxy, mockNetworkConfigWithProxy);
16101610
const agent = result.services.agent;
16111611
const env = agent.environment as Record<string, string>;
1612-
expect(env.OPENAI_BASE_URL).toBe('http://api-proxy:10000');
1613-
expect(env.ANTHROPIC_BASE_URL).toBe('http://api-proxy:10001');
1612+
expect(env.OPENAI_BASE_URL).toBe('http://172.30.0.30:10000');
1613+
expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001');
16141614
});
16151615

16161616
it('should not set OPENAI_BASE_URL in agent when only Anthropic key is provided', () => {
@@ -1619,7 +1619,7 @@ describe('docker-manager', () => {
16191619
const agent = result.services.agent;
16201620
const env = agent.environment as Record<string, string>;
16211621
expect(env.OPENAI_BASE_URL).toBeUndefined();
1622-
expect(env.ANTHROPIC_BASE_URL).toBe('http://api-proxy:10001');
1622+
expect(env.ANTHROPIC_BASE_URL).toBe('http://172.30.0.30:10001');
16231623
});
16241624

16251625
it('should not set ANTHROPIC_BASE_URL in agent when only OpenAI key is provided', () => {
@@ -1628,7 +1628,7 @@ describe('docker-manager', () => {
16281628
const agent = result.services.agent;
16291629
const env = agent.environment as Record<string, string>;
16301630
expect(env.ANTHROPIC_BASE_URL).toBeUndefined();
1631-
expect(env.OPENAI_BASE_URL).toBe('http://api-proxy:10000');
1631+
expect(env.OPENAI_BASE_URL).toBe('http://172.30.0.30:10000');
16321632
});
16331633
});
16341634
});

src/docker-manager.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -960,13 +960,14 @@ export function generateDockerCompose(
960960
environment.AWF_API_PROXY_IP = networkConfig.proxyIp;
961961

962962
// Set environment variables in agent to use the proxy
963+
// Use IP address instead of hostname to avoid DNS resolution issues in chroot mode
963964
if (config.openaiApiKey) {
964-
environment.OPENAI_BASE_URL = `http://api-proxy:10000`;
965-
logger.debug('OpenAI API will be proxied through sidecar at http://api-proxy:10000');
965+
environment.OPENAI_BASE_URL = `http://${networkConfig.proxyIp}:10000`;
966+
logger.debug(`OpenAI API will be proxied through sidecar at http://${networkConfig.proxyIp}:10000`);
966967
}
967968
if (config.anthropicApiKey) {
968-
environment.ANTHROPIC_BASE_URL = `http://api-proxy:10001`;
969-
logger.debug('Anthropic API will be proxied through sidecar at http://api-proxy:10001');
969+
environment.ANTHROPIC_BASE_URL = `http://${networkConfig.proxyIp}:10001`;
970+
logger.debug(`Anthropic API will be proxied through sidecar at http://${networkConfig.proxyIp}:10001`);
970971
}
971972

972973
logger.info('API proxy sidecar enabled - API keys will be held securely in sidecar container');

src/host-iptables.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ async function setupIpv6Chain(bridgeName: string): Promise<void> {
160160
* @param squidPort - Port number of the Squid proxy
161161
* @param dnsServers - Array of trusted DNS server IP addresses (DNS traffic is ONLY allowed to these servers)
162162
*/
163-
export async function setupHostIptables(squidIp: string, squidPort: number, dnsServers: string[], apiProxyIp?: string): Promise<void> {
163+
export async function setupHostIptables(squidIp: string, squidPort: number, dnsServers: string[]): Promise<void> {
164164
logger.info('Setting up host-level iptables rules...');
165165

166166
// Get the bridge interface name
@@ -247,18 +247,10 @@ export async function setupHostIptables(squidIp: string, squidPort: number, dnsS
247247
'-j', 'ACCEPT',
248248
]);
249249

250-
// 1b. Allow all traffic FROM the API proxy sidecar (it needs to reach external APIs)
251-
// The api-proxy holds API keys and forwards requests to api.openai.com/api.anthropic.com.
252-
// http-proxy-middleware connects directly to targets (doesn't use HTTP_PROXY env vars),
253-
// so the api-proxy needs unrestricted outbound access like Squid.
254-
if (apiProxyIp) {
255-
logger.debug(`Allowing outbound traffic from API proxy sidecar (${apiProxyIp})...`);
256-
await execa('iptables', [
257-
'-t', 'filter', '-A', CHAIN_NAME,
258-
'-s', apiProxyIp,
259-
'-j', 'ACCEPT',
260-
]);
261-
}
250+
// Note: API proxy sidecar does NOT get a firewall exemption.
251+
// It routes through Squid via https-proxy-agent, ensuring domain whitelisting
252+
// is enforced by Squid ACLs. The api-proxy only needs to reach Squid (allowed
253+
// by the Squid proxy rule below) for its outbound HTTPS connections.
262254

263255
// 2. Allow established and related connections (return traffic)
264256
await execa('iptables', [

0 commit comments

Comments
 (0)