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
@@ -279,14 +279,18 @@ func (f *proxyFlow) createAndStartProxyServer(
|
|||||||
func (f *proxyFlow) setupEnvForProxy(proxyAddr, caCertPath string) []string {
|
func (f *proxyFlow) setupEnvForProxy(proxyAddr, caCertPath string) []string {
|
||||||
proxyURL := fmt.Sprintf("http://%s", proxyAddr)
|
proxyURL := fmt.Sprintf("http://%s", proxyAddr)
|
||||||
|
|
||||||
|
noProxyList := "localhost,127.0.0.1,[::1]"
|
||||||
|
|
||||||
env := os.Environ()
|
env := os.Environ()
|
||||||
env = append(env,
|
env = append(env,
|
||||||
"NODE_USE_ENV_PROXY=1",
|
"NODE_USE_ENV_PROXY=1",
|
||||||
fmt.Sprintf("HTTP_PROXY=%s", proxyURL),
|
fmt.Sprintf("HTTP_PROXY=%s", proxyURL),
|
||||||
fmt.Sprintf("HTTPS_PROXY=%s", proxyURL),
|
fmt.Sprintf("HTTPS_PROXY=%s", proxyURL),
|
||||||
|
fmt.Sprintf("NO_PROXY=%s", noProxyList),
|
||||||
fmt.Sprintf("NODE_EXTRA_CA_CERTS=%s", caCertPath),
|
fmt.Sprintf("NODE_EXTRA_CA_CERTS=%s", caCertPath),
|
||||||
fmt.Sprintf("http_proxy=%s", proxyURL),
|
fmt.Sprintf("http_proxy=%s", proxyURL),
|
||||||
fmt.Sprintf("https_proxy=%s", proxyURL),
|
fmt.Sprintf("https_proxy=%s", proxyURL),
|
||||||
|
fmt.Sprintf("no_proxy=%s", noProxyList),
|
||||||
fmt.Sprintf("SSL_CERT_FILE=%s", caCertPath),
|
fmt.Sprintf("SSL_CERT_FILE=%s", caCertPath),
|
||||||
fmt.Sprintf("REQUESTS_CA_BUNDLE=%s", caCertPath),
|
fmt.Sprintf("REQUESTS_CA_BUNDLE=%s", caCertPath),
|
||||||
fmt.Sprintf("PIP_CERT=%s", caCertPath),
|
fmt.Sprintf("PIP_CERT=%s", caCertPath),
|
||||||
|
|||||||
+13
-2
@@ -134,6 +134,15 @@ func NewProxyServer(config *ProxyConfig) (ProxyServer, error) {
|
|||||||
return ps, nil
|
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 {
|
func newUpstreamTransport(config *ProxyConfig) *http.Transport {
|
||||||
dialer := &net.Dialer{
|
dialer := &net.Dialer{
|
||||||
Timeout: config.ConnectTimeout,
|
Timeout: config.ConnectTimeout,
|
||||||
@@ -141,7 +150,9 @@ func newUpstreamTransport(config *ProxyConfig) *http.Transport {
|
|||||||
|
|
||||||
// Proxy honours the environment (HTTP_PROXY, HTTPS_PROXY, NO_PROXY) so
|
// Proxy honours the environment (HTTP_PROXY, HTTPS_PROXY, NO_PROXY) so
|
||||||
// that PMG works in enterprise environments that require a corporate
|
// 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
|
// ForceAttemptHTTP2 is required because Go's http.Transport silently
|
||||||
// disables HTTP/2 when a custom TLSClientConfig or DialContext is set.
|
// 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
|
// MaxIdleConnsPerHost is raised from the default of 2 to improve
|
||||||
// connection reuse.
|
// connection reuse.
|
||||||
return &http.Transport{
|
return &http.Transport{
|
||||||
Proxy: http.ProxyFromEnvironment,
|
Proxy: proxyWithLoopbackBypass,
|
||||||
DialContext: dialer.DialContext,
|
DialContext: dialer.DialContext,
|
||||||
ForceAttemptHTTP2: true,
|
ForceAttemptHTTP2: true,
|
||||||
MaxConnsPerHost: 100,
|
MaxConnsPerHost: 100,
|
||||||
|
|||||||
@@ -126,6 +126,35 @@ func TestNormalizeRequestURLNilSafety(t *testing.T) {
|
|||||||
normalizeRequestURL(&http.Request{URL: &url.URL{}})
|
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) {
|
func TestNewProxyServerRejectsUntrustedUpstreamCertByDefault(t *testing.T) {
|
||||||
target := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
target := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|||||||
Reference in New Issue
Block a user