Skip to content

fix: allow host gateway traffic for localhost/Playwright#1435

Merged
Mossaka merged 3 commits intomainfrom
fix/playwright-localhost
Mar 25, 2026
Merged

fix: allow host gateway traffic for localhost/Playwright#1435
Mossaka merged 3 commits intomainfrom
fix/playwright-localhost

Conversation

@Mossaka
Copy link
Copy Markdown
Collaborator

@Mossaka Mossaka commented Mar 25, 2026

Summary

Fixes the --allow-domains localhost feature which was completely broken — agents inside AWF containers could not reach dev servers running on the host machine, breaking Playwright testing workflows.

Three root causes fixed:

  • Host-level FW_WRAPPER iptables chain blocked gateway traffic: The DOCKER-USER → FW_WRAPPER chain had no ACCEPT rules for the Docker bridge gateway IP (172.17.0.1), so all traffic to host.docker.internal hit the default REJECT. Added HostAccessConfig parameter to setupHostIptables() with per-port ACCEPT rules for both Docker bridge and AWF network gateways.

  • localhost resolved to container loopback: The chroot /etc/hosts had 127.0.0.1 localhost, making Playwright connect to the container's own loopback. Now replaces the entry with the host gateway IP when --allow-domains localhost is used (first-match semantics).

  • AWF_ALLOW_HOST_PORTS not propagated to iptables-init: The env var was missing from the iptables-init container environment, so custom port ACCEPT rules were never created at the container level.

Additional hardening:

  • Port validation (isValidPortSpec) in TypeScript with dedup via Set
  • Port validation (is_valid_port_spec) in setup-iptables.sh (defense-in-depth)
  • Leading-zero rejection for port ranges

Test plan

  • npm test — 1180 tests pass
  • npm run lint — 0 errors
  • Manual: host.docker.internal:8080 reachable with --allow-domains localhost
  • Manual: Custom port via --allow-host-ports works
  • Manual: Non-allowed port correctly blocked (timeout)
  • Manual: Without localhost keyword, host unreachable
  • Manual: Python/Node.js (getaddrinfo) correctly resolves localhost to host gateway
  • Note: curl on Ubuntu 22.04 has hardcoded localhost resolution bypassing /etc/hosts — this is a known curl behavior, not our bug. Playwright (Node.js) uses getaddrinfo() which works correctly.

Closes #1413

🤖 Generated with Claude Code

The `--allow-domains localhost` feature was broken because:

1. **Host-level iptables blocked gateway traffic**: The FW_WRAPPER chain in
   DOCKER-USER had no ACCEPT rules for the Docker bridge gateway IP, causing
   all traffic to host.docker.internal to hit the default REJECT rule.

2. **localhost resolved to container loopback**: The chroot /etc/hosts had
   `127.0.0.1 localhost` which made tools like Playwright connect to the
   container's own loopback instead of the host machine.

3. **AWF_ALLOW_HOST_PORTS not propagated**: The env var was missing from the
   iptables-init container, so custom port rules were never created.

Changes:
- Add HostAccessConfig to setupHostIptables() with gateway ACCEPT rules for
  both Docker bridge (172.17.0.1) and AWF network (172.30.0.1) gateways
- Replace 127.0.0.1 localhost in chroot /etc/hosts with host gateway IP
  when localhostDetected (uses first-match semantics correctly)
- Propagate AWF_ALLOW_HOST_PORTS to iptables-init container environment
- Add port validation (isValidPortSpec) with dedup in host-iptables.ts
- Add port validation (is_valid_port_spec) in setup-iptables.sh
- Comprehensive test coverage for all new functionality

Closes #1413

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 25, 2026 19:34
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 25, 2026

✅ Coverage Check Passed

Overall Coverage

Metric Base PR Delta
Lines 82.69% 82.87% 📈 +0.18%
Statements 82.35% 82.52% 📈 +0.17%
Functions 81.11% 81.25% 📈 +0.14%
Branches 75.88% 76.05% 📈 +0.17%
📁 Per-file Coverage Changes (2 files)
File Lines (Before → After) Statements (Before → After)
src/docker-manager.ts 86.3% → 86.2% (-0.06%) 85.7% → 85.7% (-0.06%)
src/host-iptables.ts 91.8% → 92.4% (+0.64%) 91.8% → 92.1% (+0.28%)

Coverage comparison generated by scripts/ci/compare-coverage.ts

@github-actions
Copy link
Copy Markdown
Contributor

🤖 Smoke test results for @Mossaka

Test Result
GitHub MCP (last 2 merged PRs) #1420 fix: restore GITHUB_API_URL in agent container when api-proxy is enabled / #1419 fix: exclude GITHUB_API_URL from agent container when api-proxy is enabled
Playwright (github.com title contains "GitHub")
File write /tmp/gh-aw/agent/smoke-test-copilot-23560261673.txt
Bash file read verification

Overall: PASS

📰 BREAKING: Report filed by Smoke Copilot for issue #1435

@github-actions
Copy link
Copy Markdown
Contributor

Smoke Test Results (run 23560261692)

✅ GitHub MCP — last 2 merged PRs: fix: write apiKeyHelper to ~/.claude/settings.json for Claude Code v2.1.81+ (#1414), docs: Fix proxy env var docs and add missing CLI flags (#1350)
✅ Playwright — github.com title: "GitHub · Change is constant. GitHub keeps you ahead. · GitHub"
✅ File Write — /tmp/gh-aw/agent/smoke-test-claude-23560261692.txt created and verified
✅ Bash — file contents confirmed via cat

Overall: PASS

💥 [THE END] — Illustrated by Smoke Claude for issue #1435

@github-actions
Copy link
Copy Markdown
Contributor

Chroot Version Comparison Results

Runtime Host Version Chroot Version Match?
Python Python 3.12.13 Python 3.12.3
Node.js v24.14.0 v20.20.1
Go go1.22.12 go1.22.12

Result: Some versions differ between host and chroot. Go matches, but Python and Node.js are on different versions (chroot uses Ubuntu 22.04 system-installed runtimes while the host runner has newer versions via toolcache/nvm).

Tested by Smoke Chroot for issue #1435

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes the “--allow-domains localhost” workflow so containers can reach dev servers running on the Docker host (not the container loopback), which is needed for Playwright/local testing and similar host-service access patterns.

Changes:

  • Adds host-level iptables ACCEPT rules for host gateway traffic (including custom ports/ranges) when host access is enabled.
  • Implements chroot /etc/hosts rewriting so localhost can resolve to a host gateway IP when localhost is detected in --allow-domains.
  • Propagates AWF_ALLOW_HOST_PORTS into the iptables-init container and adds port-spec validation in both TS and shell.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/types.ts Adds localhostDetected flag to WrapperConfig to drive localhost rewriting.
src/host-iptables.ts Introduces host-access config, gateway discovery, and host-level iptables rules + port validation.
src/host-iptables.test.ts Adds unit coverage for host-access iptables behavior and port-spec validation.
src/docker-manager.ts Rewrites chroot hosts to map localhost to host gateway; propagates AWF_ALLOW_HOST_PORTS to iptables-init env.
src/cli.ts Plumbs localhostDetected from localhost keyword processing into WrapperConfig.
src/cli-workflow.ts Passes host-access config into setupHostIptables() when host access is enabled.
src/cli-workflow.test.ts Verifies host-access config wiring into workflow dependency call.
containers/agent/setup-iptables.sh Adds shell-side port spec validation before adding gateway accept/redirect rules.
Comments suppressed due to low confidence (2)

src/docker-manager.ts:851

  • This hostsContent.replace() only updates 127.0.0.1 localhost... lines and replaces the whole line with just "<gw>\tlocalhost", dropping any additional aliases that were on that line (e.g. localhost.localdomain). It also doesn’t address the common ::1 localhost ... entry, so localhost may still resolve to IPv6 loopback first and continue pointing at the container. Consider preserving the suffix (capture group) in the replacement and also removing/replacing localhost on the ::1 line when localhostDetected is enabled.
            hostsContent = hostsContent.replace(
              /^127\.0\.0\.1\s+localhost(\s+.*)?$/gm,
              `${hostGatewayIp}\tlocalhost`
            );

src/host-iptables.ts:445

  • When parsing allowHostPorts, invalid specs are only logged and skipped. Since host iptables is set up before configs are generated (and before Squid validates ports), this can leave the host firewall partially configured and lead to confusing “port still blocked” failures. Consider treating an invalid allowHostPorts entry as a hard error (throw) so the run fails early and predictably, or validate --allow-host-ports in the CLI before any iptables side effects.
      for (const entry of hostAccess.allowHostPorts.split(',')) {
        const trimmed = entry.trim();
        if (trimmed) {
          if (!isValidPortSpec(trimmed)) {
            logger.warn(`Skipping invalid port spec: ${trimmed}`);
            continue;
          }
          customPorts.push(trimmed);
        }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +30 to +34
if (String(start) !== rangeMatch[1] || String(end) !== rangeMatch[2]) return false;
return start >= 1 && start <= 65535 && end >= 1 && end <= 65535 && start <= end;
}
const port = parseInt(spec, 10);
return !isNaN(port) && String(port) === spec && port >= 1 && port <= 65535;
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isValidPortSpec() rejects leading zeros by comparing String(parseInt(...)) with the original token. That’s fine, but it creates inconsistent behavior with other layers: containers/agent/setup-iptables.sh and src/squid-config.ts currently accept leading-zero port specs, so a value like 080-090 could be allowed by Squid/NAT but silently skipped here at the host firewall layer (breaking connectivity). Recommend aligning validation across host iptables + agent iptables + Squid (either reject leading zeros everywhere and fail fast in CLI, or accept them everywhere).

This issue also appears on line 437 of the same file.

Suggested change
if (String(start) !== rangeMatch[1] || String(end) !== rangeMatch[2]) return false;
return start >= 1 && start <= 65535 && end >= 1 && end <= 65535 && start <= end;
}
const port = parseInt(spec, 10);
return !isNaN(port) && String(port) === spec && port >= 1 && port <= 65535;
return start >= 1 && start <= 65535 && end >= 1 && end <= 65535 && start <= end;
}
if (!/^\d+$/.test(spec)) return false;
const port = parseInt(spec, 10);
return port >= 1 && port <= 65535;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the catch. The leading-zero rejection is intentional for consistency — I've aligned the bash is_valid_port_spec() in setup-iptables.sh to also reject leading zeros (using [1-9][0-9]{0,4} pattern instead of [0-9]+). This makes all three layers (host iptables, container iptables, Squid config) consistent: leading zeros are rejected everywhere and the CLI fails fast.

Comment on lines +168 to +172
if echo "$spec" | grep -qE '^[0-9]+-[0-9]+$'; then
local start=$(echo "$spec" | cut -d- -f1)
local end=$(echo "$spec" | cut -d- -f2)
[ "$start" -ge 1 ] && [ "$start" -le 65535 ] && [ "$end" -ge 1 ] && [ "$end" -le 65535 ] && [ "$start" -le "$end" ]
elif echo "$spec" | grep -qE '^[0-9]+$'; then
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_valid_port_spec allows leading-zero port numbers/ranges (e.g. 080-090), while the new TypeScript isValidPortSpec() explicitly rejects them. This inconsistency can cause ports to be accepted in one layer but rejected/ignored in another. If the intent is “leading-zero rejection for port ranges”, update this shell validation to also reject leading zeros (and keep behavior consistent across host + agent + Squid).

Suggested change
if echo "$spec" | grep -qE '^[0-9]+-[0-9]+$'; then
local start=$(echo "$spec" | cut -d- -f1)
local end=$(echo "$spec" | cut -d- -f2)
[ "$start" -ge 1 ] && [ "$start" -le 65535 ] && [ "$end" -ge 1 ] && [ "$end" -le 65535 ] && [ "$start" -le "$end" ]
elif echo "$spec" | grep -qE '^[0-9]+$'; then
# Disallow leading zeros in both single ports and ranges to align with TypeScript validation.
if echo "$spec" | grep -qE '^[1-9][0-9]{0,4}-[1-9][0-9]{0,4}$'; then
local start=$(echo "$spec" | cut -d- -f1)
local end=$(echo "$spec" | cut -d- -f2)
[ "$start" -ge 1 ] && [ "$start" -le 65535 ] && [ "$end" -ge 1 ] && [ "$end" -le 65535 ] && [ "$start" -le "$end" ]
elif echo "$spec" | grep -qE '^[1-9][0-9]{0,4}$'; then

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 347c112. The bash is_valid_port_spec() now uses ^[1-9][0-9]{0,4} patterns (both for single ports and ranges), matching the TypeScript String(parseInt(x)) === x round-trip check. Leading-zero ports like 080 or 080-090 are now rejected in both layers.

Comment on lines +845 to +853
if (config.localhostDetected) {
// Replace 127.0.0.1 localhost entries with the host gateway IP
// /etc/hosts uses first-match semantics, so we must replace rather than append
hostsContent = hostsContent.replace(
/^127\.0\.0\.1\s+localhost(\s+.*)?$/gm,
`${hostGatewayIp}\tlocalhost`
);
logger.info('localhost inside container resolves to host machine (localhost keyword active)');
}
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In chroot mode, this rewrites localhost to hostGatewayIp that was computed via docker network inspect bridge. That IP may not match the host.docker.internal:host-gateway value for the AWF network (often the awf-net gateway, e.g. 172.30.0.1), which is what setup-iptables.sh uses when adding NAT RETURN/ACCEPT rules. If these differ, chrooted processes will connect to an IP that is still DNAT’d to Squid (or blocked), so localhost/host access remains broken. Consider deriving the gateway from the actual AWF network (docker network inspect awf-net ... Gateway) or otherwise using the same IP that Docker will inject for host-gateway on the awf-net network.

This issue also appears on line 848 of the same file.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good observation, but this is already handled. The host-iptables.ts FW_WRAPPER chain adds ACCEPT rules for both gateway IPs:

  1. Docker bridge gateway (172.17.0.1) — from getDockerBridgeGateway() (line 410)
  2. AWF network gateway (172.30.0.1) — hardcoded as AWF_NETWORK_GATEWAY (line 414)

Similarly, setup-iptables.sh adds NAT RETURN + FILTER ACCEPT for both the host.docker.internal IP (resolved via getent hosts, typically 172.17.0.1) AND the network gateway (172.30.0.1, resolved via route -n).

The /etc/hosts uses the Docker bridge gateway IP (172.17.0.1) for the localhost entry, which matches the host.docker.internal IP that Docker injects via extra_hosts: host-gateway. Traffic to this IP is allowed by both the container-level and host-level rules. The dual-gateway approach ensures it works regardless of which IP the tool resolves to.

@github-actions

This comment has been minimized.

Aligns is_valid_port_spec() in setup-iptables.sh with the TypeScript
isValidPortSpec() by rejecting leading-zero port specs (e.g., 080-090).
Uses [1-9][0-9]{0,4} regex pattern instead of [0-9]+ to ensure
consistent validation across all layers (host iptables, container
iptables, Squid config).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Smoke Test Results — PASS

💥 [THE END] — Illustrated by Smoke Claude for issue #1435

@github-actions
Copy link
Copy Markdown
Contributor

Smoke Test Results@Mossaka

✅ GitHub MCP: #1420 fix: restore GITHUB_API_URL in agent container when api-proxy is enabled, #1419 fix: exclude GITHUB_API_URL from agent container when api-proxy is enabled
✅ Playwright: github.com title contains "GitHub"
✅ File write: /tmp/gh-aw/agent/smoke-test-copilot-23560627501.txt created
✅ Bash verify: file read back successfully

Overall: PASS

📰 BREAKING: Report filed by Smoke Copilot for issue #1435

@github-actions
Copy link
Copy Markdown
Contributor

Chroot Version Comparison Results

Runtime Host Version Chroot Version Match?
Python Python 3.12.13 Python 3.12.3 ❌ NO
Node.js v24.14.0 v20.20.1 ❌ NO
Go go1.22.12 go1.22.12 ✅ YES

Overall: Not all tests passed — Python and Node.js versions differ between host and chroot environments.

Tested by Smoke Chroot for issue #1435

@github-actions
Copy link
Copy Markdown
Contributor

🏗️ Build Test Suite Results

Ecosystem Project Build/Install Tests Status
Bun elysia 1/1 passed ✅ PASS
Bun hono 1/1 passed ✅ PASS
C++ fmt N/A ✅ PASS
C++ json N/A ✅ PASS
Deno oak N/A 1/1 passed ✅ PASS
Deno std N/A 1/1 passed ✅ PASS
.NET hello-world N/A ✅ PASS
.NET json-parse N/A ✅ PASS
Go color 1/1 passed ✅ PASS
Go env 1/1 passed ✅ PASS
Go uuid 1/1 passed ✅ PASS
Java gson 1/1 passed ✅ PASS
Java caffeine 1/1 passed ✅ PASS
Node.js clsx passed ✅ PASS
Node.js execa passed ✅ PASS
Node.js p-limit passed ✅ PASS
Rust fd 1/1 passed ✅ PASS
Rust zoxide 1/1 passed ✅ PASS

Overall: 8/8 ecosystems passed — ✅ PASS

Generated by Build Test Suite for issue #1435 ·

@github-actions

This comment has been minimized.

@github-actions github-actions bot mentioned this pull request Mar 25, 2026
- Validate Docker bridge gateway IP as IPv4 before using in iptables
  rules or /etc/hosts injection (defense-in-depth)
- Preserve localhost.localdomain and other aliases when replacing
  127.0.0.1 localhost in /etc/hosts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Smoke Test Results

Overall: PASS

💥 [THE END] — Illustrated by Smoke Claude for issue #1435

@github-actions
Copy link
Copy Markdown
Contributor

Smoke Test Results ✅ PASS

Test Result
GitHub MCP (last 2 merged PRs) #1420 fix: restore GITHUB_API_URL in agent container when api-proxy is enabled, #1419 fix: exclude GITHUB_API_URL from agent container when api-proxy is enabled
Playwright (github.com title) ✅ "GitHub · Change is constant..."
File write /tmp/gh-aw/agent/smoke-test-copilot-23564291956.txt created
Bash verify cat confirmed file contents

Author: @Mossaka | No assignees

📰 BREAKING: Report filed by Smoke Copilot for issue #1435

@github-actions
Copy link
Copy Markdown
Contributor

Chroot Version Comparison Results

Runtime Host Version Chroot Version Match?
Python Python 3.12.13 Python 3.12.3 ❌ NO
Node.js v24.14.0 v20.20.1 ❌ NO
Go go1.22.12 go1.22.12 ✅ YES

Result: Not all tests passed. Python and Node.js versions differ between host and chroot environments.

Tested by Smoke Chroot for issue #1435

@github-actions
Copy link
Copy Markdown
Contributor

🔮 The ancient spirits stir; the smoke test omens are read.

  1. Last 2 merged PRs reviewed ✅
    Merged titles: "docs: Fix proxy env var docs and add missing CLI flags"; "fix: write apiKeyHelper to ~/.claude/settings.json for Claude Code v2.1.81+"
  2. safeinputs-gh PR query ❌ (tool unavailable in this runtime)
  3. Playwright github.com title contains "GitHub" ✅
  4. Tavily web search ❌ (Tavily MCP unavailable)
  5. File write /tmp/gh-aw/agent/smoke-test-codex-23564291978.txt
  6. Bash cat verification ✅
  7. Discussion query/comment ❌ (github-discussion-query unavailable; workflow permits one safe-output call)
  8. npm ci && npm run build
    Overall status: FAIL

🔮 The oracle has spoken through Smoke Codex

Warning

⚠️ Firewall blocked 2 domains

The following domains were blocked by the firewall during workflow execution:

  • ab.chatgpt.com
  • registry.npmjs.org

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "ab.chatgpt.com"
    - "registry.npmjs.org"

See Network Configuration for more information.

@Mossaka Mossaka merged commit 3bb5dfa into main Mar 25, 2026
58 checks passed
@Mossaka Mossaka deleted the fix/playwright-localhost branch March 25, 2026 22:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

playwright + localhost not working nicely

2 participants