Skip to content

Forward contained connections through https:// and SOCKS5 upstream proxies - #154

Open
fstubner wants to merge 2 commits into
mainfrom
feat/https-and-socks5-upstream-proxy
Open

fstubner wants to merge 2 commits into
mainfrom
feat/https-and-socks5-upstream-proxy

Conversation

@fstubner

@fstubner fstubner commented Oct 6, 2026

Copy link
Copy Markdown
Owner

What changes

Only an http:// value in HTTPS_PROXY / HTTP_PROXY was used. An https://, socks5:// or socks5h:// value was ignored with a warning, and allowed hosts were dialled directly. A machine whose only route out is such a proxy reached nothing from a contained install.

  • https://. TLS to the proxy, verified against the system roots with the proxy's host as server name (TLS 1.2 minimum). Then the same CONNECT and Proxy-Authorization as http://. A proxy whose certificate fails verification never receives the CONNECT or the credential. Default port 443.
  • socks5h://. Minimal RFC 1928 CONNECT with the destination name. Default port 1080.
  • socks5://. Same client, sending the addresses nvx resolved and vetted (each in turn, as dialVetted does). An empty answer is re-resolved through the same link-local check via vettedOrResolve, split out of dialVetted.
  • RFC 1929 login from the URL's userinfo for both SOCKS schemes. The greeting offers no-auth only when the URL has no user, and no-auth plus user/pass when it has one. A refused login is not repeated for the next address.
  • Other schemes (socks4://, unknown) keep the warning and the direct dial.
  • The allowlist decision is untouched and still runs before any upstream dial. No new dependency, go.sum stays empty.
  • Errors name the proxy's host:port only. No log or audit line carries the user or password.

Tests

internal/nvx/egress_upstream_tls_socks_test.go, with in-process fake upstreams:

  • TestAllowedConnectGoesThroughAnHTTPSUpstreamProxy. httptest TLS proxy with its CA handed in through upstreamProxy.rootCAs. Checks the CONNECT target and Proxy-Authorization, the echoed tunnel bytes, and that a refused host gets 403 with no second connection to the proxy.
  • TestHTTPSUpstreamProxyWithABadCertificateIsRefused. Untrusted CA, and a trusted certificate that does not name the host. Both give 502, the proxy was dialled, and no CONNECT reached it.
  • TestAllowedConnectGoesThroughASOCKS5hUpstreamProxy. Exact greeting and domain-type request bytes, the tunnel, refusal before dial.
  • TestAllowedConnectGoesThroughASOCKS5UpstreamProxyWithLogin. Exact greeting, RFC 1929 login and IPv4 request bytes for the vetted address, the tunnel, refusal before dial.
  • TestSOCKS5UpstreamLoginIsTriedOnce. Two resolved addresses with a wrong password give one login attempt and a 502.
  • TestUpstreamProxySchemes. Accepted schemes and default ports.

All six fail against main's egress_upstream.go and egress_resolve.go. Disabling the allowlist check in handleHTTPConn fails the three refusal assertions. Removing the login-retry guard fails TestSOCKS5UpstreamLoginIsTriedOnce.

Local, Windows: gofmt -l internal cmd empty, go vet ./... and with GOOS=linux / GOOS=darwin clean, go test ./internal/nvx -count=1 ok.

site/ is not touched here. limitations.md and policy.md still describe the http-only behaviour.

…oxies

Only an http:// HTTPS_PROXY or HTTP_PROXY was used. An https://, socks5://
or socks5h:// value was ignored with a warning and allowed hosts were dialled
directly, so a machine whose only route out is such a proxy reached nothing.

An https:// proxy is now reached over TLS, verified against the system roots
with the proxy's host as server name, and then gets the same CONNECT request
and Proxy-Authorization header as an http:// one. socks5:// and socks5h://
use a small RFC 1928 client with RFC 1929 login from the URL's userinfo.
socks5h:// sends the name. socks5:// sends the addresses nvx resolved and
vetted, trying each in turn as dialVetted does, and a refused login is not
repeated for the next address. The allowlist decision is unchanged and still
happens before any upstream dial. Other schemes keep the warning and direct
dial. No new dependency.

vettedOrResolve is split out of dialVetted so the socks5:// path re-resolves
an empty answer through the same link-local check.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant