diff --git a/containers/agent/setup-iptables.sh b/containers/agent/setup-iptables.sh index d349d68f..a9b6a735 100644 --- a/containers/agent/setup-iptables.sh +++ b/containers/agent/setup-iptables.sh @@ -32,8 +32,11 @@ fi # Get Squid proxy configuration from environment SQUID_HOST="${SQUID_PROXY_HOST:-squid-proxy}" SQUID_PORT="${SQUID_PROXY_PORT:-3128}" +# Intercept port for NAT-redirected transparent proxy traffic +# Squid's "intercept" mode handles relative URLs by extracting destination from Host header +SQUID_INTERCEPT_PORT="${SQUID_INTERCEPT_PORT:-3129}" -echo "[iptables] Squid proxy: ${SQUID_HOST}:${SQUID_PORT}" +echo "[iptables] Squid proxy: ${SQUID_HOST}:${SQUID_PORT} (intercept: ${SQUID_INTERCEPT_PORT})" # Resolve Squid hostname to IP # Use awk's NR to get first line to avoid host binary dependency in chroot mode @@ -154,15 +157,18 @@ for port in "${DANGEROUS_PORTS[@]}"; do done echo "[iptables] NAT blacklist applied for ${#DANGEROUS_PORTS[@]} dangerous ports" -# Redirect standard HTTP/HTTPS ports to Squid +# Redirect standard HTTP/HTTPS ports to Squid's INTERCEPT port # This provides defense-in-depth: iptables enforces port policy, Squid enforces domain policy -echo "[iptables] Redirect HTTP (80) and HTTPS (443) to Squid..." -iptables -t nat -A OUTPUT -p tcp --dport 80 -j DNAT --to-destination "${SQUID_IP}:${SQUID_PORT}" -iptables -t nat -A OUTPUT -p tcp --dport 443 -j DNAT --to-destination "${SQUID_IP}:${SQUID_PORT}" +# We use the intercept port (not the regular port) because NAT-redirected traffic is "transparent" - +# clients send relative URLs (GET /path) which require Squid's intercept mode to handle properly. +# The regular port (3128) is for explicit proxy usage via HTTP_PROXY/HTTPS_PROXY env vars. +echo "[iptables] Redirect HTTP (80) and HTTPS (443) to Squid intercept port..." +iptables -t nat -A OUTPUT -p tcp --dport 80 -j DNAT --to-destination "${SQUID_IP}:${SQUID_INTERCEPT_PORT}" +iptables -t nat -A OUTPUT -p tcp --dport 443 -j DNAT --to-destination "${SQUID_IP}:${SQUID_INTERCEPT_PORT}" # If user specified additional ports via --allow-host-ports, redirect those too if [ -n "$AWF_ALLOW_HOST_PORTS" ]; then - echo "[iptables] Redirect user-specified ports to Squid..." + echo "[iptables] Redirect user-specified ports to Squid intercept port..." # Parse comma-separated port list IFS=',' read -ra PORTS <<< "$AWF_ALLOW_HOST_PORTS" @@ -173,13 +179,13 @@ if [ -n "$AWF_ALLOW_HOST_PORTS" ]; then if [[ $port_spec == *"-"* ]]; then # Port range (e.g., "3000-3010") - echo "[iptables] Redirect port range $port_spec to Squid..." + echo "[iptables] Redirect port range $port_spec to Squid intercept port..." # For port ranges, use --dport with range syntax (without multiport) - iptables -t nat -A OUTPUT -p tcp --dport "$port_spec" -j DNAT --to-destination "${SQUID_IP}:${SQUID_PORT}" + iptables -t nat -A OUTPUT -p tcp --dport "$port_spec" -j DNAT --to-destination "${SQUID_IP}:${SQUID_INTERCEPT_PORT}" else # Single port (e.g., "3000") - echo "[iptables] Redirect port $port_spec to Squid..." - iptables -t nat -A OUTPUT -p tcp --dport "$port_spec" -j DNAT --to-destination "${SQUID_IP}:${SQUID_PORT}" + echo "[iptables] Redirect port $port_spec to Squid intercept port..." + iptables -t nat -A OUTPUT -p tcp --dport "$port_spec" -j DNAT --to-destination "${SQUID_IP}:${SQUID_INTERCEPT_PORT}" fi done else diff --git a/src/docker-manager.ts b/src/docker-manager.ts index ef1602c9..423e08cd 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -267,7 +267,7 @@ export function generateDockerCompose( }, volumes: squidVolumes, healthcheck: { - test: ['CMD', 'nc', '-z', 'localhost', '3128'], + test: ['CMD-SHELL', 'nc -z localhost 3128 && nc -z localhost 3129'], interval: '5s', timeout: '3s', retries: 5, @@ -713,6 +713,7 @@ export async function writeConfigs(config: WrapperConfig): Promise { domains: config.allowedDomains, blockedDomains: config.blockedDomains, port: SQUID_PORT, + interceptPort: SQUID_INTERCEPT_PORT, sslBump: config.sslBump, caFiles: sslConfig?.caFiles, sslDbPath: sslConfig ? '/var/spool/squid_ssl_db' : undefined, diff --git a/src/squid-config.test.ts b/src/squid-config.test.ts index 620034b7..b4931784 100644 --- a/src/squid-config.test.ts +++ b/src/squid-config.test.ts @@ -1120,6 +1120,27 @@ describe('generateSquidConfig', () => { expect(result).toContain('http_port 3128'); expect(result).not.toContain('https_port'); }); + + it('should add intercept port when specified', () => { + const config: SquidConfig = { + domains: ['github.com'], + port: 3128, + interceptPort: 3129, + }; + const result = generateSquidConfig(config); + expect(result).toContain('http_port 3128'); + expect(result).toContain('http_port 3129 intercept'); + }); + + it('should not add intercept port when not specified', () => { + const config: SquidConfig = { + domains: ['github.com'], + port: 3128, + }; + const result = generateSquidConfig(config); + expect(result).toContain('http_port 3128'); + expect(result).not.toContain('intercept'); + }); }); }); diff --git a/src/squid-config.ts b/src/squid-config.ts index 5e1478d6..de1faada 100644 --- a/src/squid-config.ts +++ b/src/squid-config.ts @@ -155,6 +155,11 @@ http_port 3128 ssl-bump \\ dynamic_cert_mem_cache_size=16MB \\ options=NO_SSLv3,NO_TLSv1,NO_TLSv1_1 +# Intercept port for NAT-redirected transparent proxy traffic +# Traffic is DNAT'd here by iptables. Squid uses "intercept" mode to handle +# relative URLs (GET /path) by extracting the destination from the Host header. +http_port 3129 intercept + # SSL certificate database for dynamic certificate generation # Using 16MB for certificate cache (sufficient for typical AI agent sessions) sslcrtd_program /usr/lib/squid/security_file_certgen -s ${sslDbPath} -M 16MB @@ -205,7 +210,7 @@ ${urlAclSection}${urlAccessRules}`; * // Blocked: internal.example.com -> acl blocked_domains dstdomain .internal.example.com */ export function generateSquidConfig(config: SquidConfig): string { - const { domains, blockedDomains, port, sslBump, caFiles, sslDbPath, urlPatterns, enableHostAccess, allowHostPorts } = config; + const { domains, blockedDomains, port, interceptPort, sslBump, caFiles, sslDbPath, urlPatterns, enableHostAccess, allowHostPorts } = config; // Parse domains into plain domains and wildcard patterns // Note: parseDomainList extracts and preserves protocol info from prefixes (http://, https://) @@ -408,10 +413,16 @@ export function generateSquidConfig(config: SquidConfig): string { // Generate SSL Bump section if enabled let sslBumpSection = ''; - // Port configuration: Use normal proxy mode (not intercept mode) - // With targeted port redirection in iptables, traffic is explicitly redirected - // to Squid on specific ports (80, 443, + user-specified), maintaining defense-in-depth + // Port configuration: + // - Regular port (3128): For explicit proxy usage via HTTP_PROXY/HTTPS_PROXY env vars + // Clients aware of the proxy send absolute URLs (GET http://example.com/path) + // - Intercept port (3129): For NAT-redirected transparent proxy traffic + // Traffic is DNAT'd here by iptables. Squid uses "intercept" mode to handle + // relative URLs (GET /path) by extracting the destination from the Host header. let portConfig = `http_port ${port}`; + if (interceptPort) { + portConfig += `\nhttp_port ${interceptPort} intercept`; + } // For SSL Bump, we need to check hasPlainDomains and hasPatterns for the 'both' protocol domains // since those are the ones that go into allowed_domains / allowed_domains_regex ACLs diff --git a/src/types.ts b/src/types.ts index a1e1f726..611921fb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -474,6 +474,21 @@ export interface SquidConfig { * @example "3000-3010,8000-8090" */ allowHostPorts?: string; + + /** + * Port number for transparently intercepted traffic (NAT/DNAT redirected) + * + * When traffic is redirected via iptables NAT (DNAT) to Squid, it arrives + * as "transparent" traffic - the client doesn't know it's talking to a proxy. + * This requires Squid's "intercept" mode which handles relative URLs and + * reconstructs the original destination from the Host header. + * + * This port should be used for NAT-redirected traffic, while the regular + * `port` is used for explicit proxy connections (HTTP_PROXY env var). + * + * @default 3129 + */ + interceptPort?: number; } /**