mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
fix: Bypass proxy for loopback addresses to prevent localhost connection failures (#194)
When users have HTTP_PROXY/HTTPS_PROXY set in their shell (e.g. corporate proxy), PMG's upstream transport routes all traffic through that external proxy — including requests to localhost/127.0.0.1. The external proxy cannot reach the user's loopback, causing EFAULT/ConnectionRefused errors. This adds loopback bypass in two places: - Proxy upstream transport skips external proxy for localhost/127.0.0.1/::1 - Child process env gets NO_PROXY so proxy-aware libs (axios) skip PMG's proxy for loopback addresses Fixes #193 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
5fdc3a03ae
commit
635e40cc1f
+13
-2
@@ -134,6 +134,15 @@ func NewProxyServer(config *ProxyConfig) (ProxyServer, error) {
|
||||
return ps, nil
|
||||
}
|
||||
|
||||
func proxyWithLoopbackBypass(req *http.Request) (*url.URL, error) {
|
||||
host := req.URL.Hostname()
|
||||
if host == "localhost" || host == "127.0.0.1" || host == "::1" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return http.ProxyFromEnvironment(req)
|
||||
}
|
||||
|
||||
func newUpstreamTransport(config *ProxyConfig) *http.Transport {
|
||||
dialer := &net.Dialer{
|
||||
Timeout: config.ConnectTimeout,
|
||||
@@ -141,7 +150,9 @@ func newUpstreamTransport(config *ProxyConfig) *http.Transport {
|
||||
|
||||
// Proxy honours the environment (HTTP_PROXY, HTTPS_PROXY, NO_PROXY) so
|
||||
// that PMG works in enterprise environments that require a corporate
|
||||
// upstream proxy to reach the internet.
|
||||
// upstream proxy to reach the internet. Loopback addresses are always
|
||||
// bypassed to avoid routing localhost traffic through an external proxy,
|
||||
// which would fail because the proxy can't reach the user's localhost.
|
||||
//
|
||||
// ForceAttemptHTTP2 is required because Go's http.Transport silently
|
||||
// disables HTTP/2 when a custom TLSClientConfig or DialContext is set.
|
||||
@@ -157,7 +168,7 @@ func newUpstreamTransport(config *ProxyConfig) *http.Transport {
|
||||
// MaxIdleConnsPerHost is raised from the default of 2 to improve
|
||||
// connection reuse.
|
||||
return &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
Proxy: proxyWithLoopbackBypass,
|
||||
DialContext: dialer.DialContext,
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxConnsPerHost: 100,
|
||||
|
||||
@@ -126,6 +126,35 @@ func TestNormalizeRequestURLNilSafety(t *testing.T) {
|
||||
normalizeRequestURL(&http.Request{URL: &url.URL{}})
|
||||
}
|
||||
|
||||
func TestProxyWithLoopbackBypass(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
shouldBypass bool
|
||||
}{
|
||||
{"localhost bypassed", "http://localhost:9876/", true},
|
||||
{"127.0.0.1 bypassed", "http://127.0.0.1:9876/", true},
|
||||
{"ipv6 loopback bypassed", "http://[::1]:9876/", true},
|
||||
{"registry not bypassed", "https://registry.npmjs.org/lodash", false},
|
||||
{"external host not bypassed", "http://example.com/", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
parsed, err := url.Parse(tt.url)
|
||||
require.NoError(t, err)
|
||||
|
||||
req := &http.Request{URL: parsed}
|
||||
proxyURL, err := proxyWithLoopbackBypass(req)
|
||||
assert.NoError(t, err)
|
||||
|
||||
if tt.shouldBypass {
|
||||
assert.Nil(t, proxyURL, "loopback address should bypass proxy")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewProxyServerRejectsUntrustedUpstreamCertByDefault(t *testing.T) {
|
||||
target := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
Reference in New Issue
Block a user