Skip to content

feat: add upstream corporate proxy support for self-hosted runners#1976

Merged
lpcox merged 2 commits intomainfrom
feat/upstream-proxy-support
Apr 14, 2026
Merged

feat: add upstream corporate proxy support for self-hosted runners#1976
lpcox merged 2 commits intomainfrom
feat/upstream-proxy-support

Conversation

@lpcox
Copy link
Copy Markdown
Collaborator

@lpcox lpcox commented Apr 14, 2026

Summary

Adds support for self-hosted runners behind a corporate proxy. AWF now auto-detects host proxy environment variables (https_proxy/http_proxy/no_proxy) and configures Squid to chain through the corporate proxy via cache_peer.

Changes

New: src/upstream-proxy.ts

  • parseProxyUrl() — validates proxy URLs, rejects credentials/loopback/HTTPS/injection chars
  • parseNoProxy() — parses no_proxy into domain suffixes, skipping IPs/CIDRs/wildcards
  • detectUpstreamProxy() — auto-detects from host env, fails if http_proxy ≠ https_proxy
  • PROXY_ENV_VARS — constant listing all proxy env var names for exclusion

Modified: src/squid-config.ts

  • generateUpstreamProxySection() — generates cache_peer, always_direct (bypass), never_direct directives

Modified: src/cli.ts

  • --upstream-proxy <url> flag for explicit override
  • Auto-detection from host env when flag is not provided
  • Help text grouping under 'Network & Security'

Modified: src/docker-manager.ts

  • PROXY_ENV_VARS added to EXCLUDED_ENV_VARS — prevents host proxy vars from leaking into containers via --env-all
  • upstreamProxy passed through to generateSquidConfig()

Modified: src/types.ts

  • UpstreamProxyConfig interface (host, port, optional noProxy)
  • Added upstreamProxy? field to WrapperConfig and SquidConfig

Tests (35 new)

  • src/upstream-proxy.test.ts — 30 tests covering parsing, validation, detection
  • src/squid-config.test.ts — 4 tests for cache_peer generation
  • src/docker-manager.test.ts — 1 test for proxy env var exclusion

Documentation

  • docs/environment.md — new 'Upstream (Corporate) Proxy Support' section

Security considerations

  • Credentials in proxy URLs are rejected (squid.conf is written to audit artifacts)
  • Loopback addresses rejected (Squid is in a container)
  • Injection characters rejected (whitespace, #, ;, quotes, backslash)
  • Host proxy env vars always excluded from container passthrough

Traffic flow (with upstream proxy)

Agent → Squid → Corporate Proxy → Internet
         ↑ cache_peer + never_direct

Domains in no_proxy bypass the corporate proxy via always_direct.

Closes #1975

Add --upstream-proxy flag and auto-detection from host https_proxy/
http_proxy/no_proxy environment variables. When configured, Squid
chains outbound traffic through the corporate proxy via cache_peer.

Key changes:
- New upstream-proxy.ts with parseProxyUrl(), parseNoProxy(),
  detectUpstreamProxy(), and PROXY_ENV_VARS constant
- UpstreamProxyConfig interface in types.ts
- generateUpstreamProxySection() in squid-config.ts for cache_peer,
  always_direct (no_proxy bypass), and never_direct directives
- CLI auto-detection with --upstream-proxy explicit override
- Host proxy env vars excluded from --env-all passthrough
- Security: reject credentials, loopback, HTTPS scheme, injection chars
- 35 new tests across upstream-proxy, squid-config, docker-manager
- Documentation in docs/environment.md

Closes #1975

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 14, 2026 20:32
@lpcox lpcox requested a review from Mossaka as a code owner April 14, 2026 20:33
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 14, 2026

Documentation Preview

Documentation build failed for this PR. View logs.

Built from commit 5f310a2

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 14, 2026

✅ Coverage Check Passed

Overall Coverage

Metric Base PR Delta
Lines 85.24% 85.27% 📈 +0.03%
Statements 85.11% 85.16% 📈 +0.05%
Functions 87.57% 87.85% 📈 +0.28%
Branches 77.58% 77.87% 📈 +0.29%
📁 Per-file Coverage Changes (3 files)
File Lines (Before → After) Statements (Before → After)
src/cli.ts 60.6% → 59.5% (-1.07%) 61.0% → 60.0% (-1.05%)
src/squid-config.ts 96.7% → 96.9% (+0.17%) 96.8% → 97.0% (+0.16%)
src/docker-manager.ts 86.3% → 86.6% (+0.33%) 85.8% → 86.2% (+0.32%)
✨ New Files (1 files)
  • src/upstream-proxy.ts: 94.5% lines

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

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

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

Adds first-class support for self-hosted runners that require outbound traffic to go through a corporate proxy, by auto-detecting host proxy settings and configuring Squid to chain via cache_peer (with no_proxy-based bypass).

Changes:

  • Introduces upstream proxy parsing/validation + host env auto-detection (src/upstream-proxy.ts) and wires it into CLI/config generation.
  • Extends Squid config generation to emit cache_peer + always_direct/never_direct directives when an upstream proxy is configured.
  • Prevents host proxy env vars from leaking into containers (even with --env-all), and adds tests + documentation for the new behavior.
Show a summary per file
File Description
src/upstream-proxy.ts Implements proxy URL parsing, no_proxy parsing, and host env auto-detection; defines PROXY_ENV_VARS.
src/upstream-proxy.test.ts Adds unit tests for proxy parsing and detection behavior.
src/types.ts Adds UpstreamProxyConfig and threads it through WrapperConfig/SquidConfig.
src/squid-config.ts Generates Squid upstream chaining directives (cache_peer, bypass ACLs, directness rules).
src/squid-config.test.ts Adds tests asserting correct upstream proxy directive generation.
src/docker-manager.ts Ensures proxy env vars are excluded from env passthrough and passes upstream proxy config into Squid config generation.
src/docker-manager.test.ts Adds a regression test ensuring host proxy vars don’t leak with --env-all.
src/cli.ts Adds --upstream-proxy flag and auto-detection path into wrapper config creation.
docs/environment.md Documents upstream proxy chaining behavior, limitations, and env var exclusion.

Copilot's findings

Tip

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

Comments suppressed due to low confidence (2)

src/upstream-proxy.ts:181

  • detectUpstreamProxy() compares the raw http_proxy/https_proxy strings for equality. Equivalent proxies can differ textually (e.g., trailing /, missing scheme, different casing) and this will incorrectly throw and force --upstream-proxy. Consider normalizing both values first (e.g., parse both with parseProxyUrl() and compare host/port), or compare via new URL() fields after adding a default scheme/port.
  // If both are set and differ, we can't determine which to use
  if (httpsProxy && httpProxy && httpsProxy !== httpProxy) {
    throw new Error(
      'Host has different http_proxy and https_proxy values. ' +
      'AWF cannot determine which upstream proxy to use. ' +
      `Use --upstream-proxy to specify explicitly.\n` +
      `  http_proxy:  ${httpProxy}\n` +
      `  https_proxy: ${httpsProxy}`
    );

src/upstream-proxy.ts:139

  • In parseNoProxy(), the IPv6 filter (entry.includes(':')) runs before the “entry with port” check. This means common values like host:8080 (or http://host:8080) will be classified as “IPv6” and won’t hit the intended port-specific warning/message. Reorder the checks (port first) and/or use net.isIP() on a bracket-stripped host to detect true IPv6 literals.
    // Skip IPv6 addresses
    if (entry.includes(':') || entry.startsWith('[')) {
      logger.warn(`Ignoring no_proxy IPv6 entry "${entry}" — only domain suffixes are supported for upstream proxy bypass`);
      continue;
    }

    // Skip entries with ports (e.g., "host:8080")
    if (/:\d+$/.test(entry)) {
      logger.warn(`Ignoring no_proxy entry with port "${entry}" — port-based bypass is not supported for upstream proxy`);
      continue;
    }
  • Files reviewed: 9/9 changed files
  • Comments generated: 3

Comment on lines +80 to +88
// Reject loopback addresses — Squid runs in a container and localhost != host localhost
const loopbackPatterns = ['localhost', '127.0.0.1', '::1', '0.0.0.0'];
if (loopbackPatterns.includes(host.toLowerCase())) {
throw new Error(
`Upstream proxy "${host}" is a loopback address. Squid runs in a Docker container ` +
'where localhost refers to the container, not the host. ' +
'Use the host machine\'s network IP or configure --enable-host-access with host.docker.internal.'
);
}
Comment on lines +31 to +35
// Domain suffixes: .corp.com matches *.corp.com
// Exact domains: internal.corp.com matches only that host
const squidDomain = domain.startsWith('.') ? domain : `.${domain}`;
lines.push(`acl upstream_bypass dstdomain ${squidDomain}`);
// Also add exact match for non-wildcard domains
Comment on lines +249 to +258
## Upstream (Corporate) Proxy Support

When running on self-hosted runners behind a corporate proxy, AWF can chain Squid
through the upstream proxy using the `cache_peer` directive.

### Auto-detection

If the host has `https_proxy`/`HTTPS_PROXY` or `http_proxy`/`HTTP_PROXY` set, AWF
automatically configures Squid to route outbound traffic through that proxy.
`no_proxy`/`NO_PROXY` domain suffixes are honored as bypass rules (`always_direct`).
@github-actions github-actions bot mentioned this pull request Apr 14, 2026
@github-actions

This comment has been minimized.

- Robust loopback detection: check full 127.0.0.0/8 range and IPv6
  variants via isLoopback() helper instead of exact-match list
- Fix misleading comments in squid-config.ts: non-dot no_proxy entries
  are treated as suffix matches (domain + subdomains), not exact-only
- Update docs/environment.md: clarify that host proxy vars are excluded
  from container passthrough but are read for upstream proxy detection

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

Smoke test results (run 24422261479)

Overall: PASS

💥 [THE END] — Illustrated by Smoke Claude

@github-actions
Copy link
Copy Markdown
Contributor

🤖 Smoke Test Results

Test Status
GitHub MCP (latest merged PR: "optimize(secret-digger-claude): default threat detection to Haiku, drop version-reporting import")
GitHub.com connectivity (HTTP 200)
File write/read (smoke-test-copilot-24422261475.txt)

Overall: PASS

PR by @lpcox, no assignees.

📰 BREAKING: Report filed by Smoke Copilot

@github-actions
Copy link
Copy Markdown
Contributor

Smoke test summary
Merged: optimize(secret-digger-claude): default threat detection to Haiku, drop version-reporting import
Merged: secret-digger-claude: switch to Haiku, lower max-turns to 4
Queried: feat: add upstream corporate proxy support for self-hosted runners / chore(deps): bump the all-github-actions group across 1 directory with 20 updates
GitHub MCP ✅ | safeinputs-gh CLI ❌
Playwright ✅ | Tavily search ❌
File write ✅ | Bash cat ✅
Discussion interaction ❌
Build (npm ci && npm run build) ✅
Overall: FAIL

🔮 The oracle has spoken through Smoke Codex

@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.1 v20.20.2
Go go1.22.12 go1.22.12

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

Tested by Smoke Chroot

@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 all passed ✅ PASS
Node.js execa all passed ✅ PASS
Node.js p-limit all passed ✅ PASS
Rust fd 1/1 passed ✅ PASS
Rust zoxide 1/1 passed ✅ PASS

Overall: 8/8 ecosystems passed — ✅ PASS

Note: Java Maven required a custom local repo path (-Dmaven.repo.local) because ~/.m2/ was created by root during runner setup, preventing the runner user from writing to it. All tests still passed once this was worked around.

Generated by Build Test Suite for issue #1976 · ● 600K ·

@github-actions
Copy link
Copy Markdown
Contributor

Smoke Test: GitHub Actions Services Connectivity

Check Result
Redis PING (host.docker.internal:6379) PONG
PostgreSQL pg_isready (host.docker.internal:5432) ✅ accepting connections
PostgreSQL SELECT 1 (db: smoketest, user: postgres) ✅ returned 1

All checks passed. Note: redis-cli was not installable in this environment (no apt access), so Redis was verified via bash TCP socket — server returned +PONG.

🔌 Service connectivity validated by Smoke Services

@lpcox lpcox merged commit 501f0b9 into main Apr 14, 2026
56 of 57 checks passed
@lpcox lpcox deleted the feat/upstream-proxy-support branch April 14, 2026 21:06
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.

Squid should route through upstream corporate proxy on self-hosted runners

2 participants