Skip to content

Commit 87e42d9

Browse files
committed
utils,cluster/spec: honor NO_PROXY in HTTP clients
NewHTTPClient and makeTransport set the proxy via http.ProxyURL, which ignores NO_PROXY and forces internal PD/TiKV requests through HTTP_PROXY, breaking `tiup cluster reload` behind a proxy. Resolve it via x/net/http/httpproxy so NO_PROXY is honored; the TIUP_INNER_HTTP_PROXY override is kept. Add unit tests. Signed-off-by: Jaehui-Lee <tkdoqr1234@gmail.com>
1 parent 9bdae04 commit 87e42d9

5 files changed

Lines changed: 176 additions & 19 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ require (
5656
go.uber.org/zap v1.24.0
5757
golang.org/x/crypto v0.48.0
5858
golang.org/x/mod v0.33.0
59+
golang.org/x/net v0.50.0
5960
golang.org/x/sync v0.19.0
6061
golang.org/x/sys v0.41.0
6162
golang.org/x/term v0.40.0
@@ -145,7 +146,6 @@ require (
145146
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
146147
golang.org/x/exp/typeparams v0.0.0-20230321023759-10a507213a29 // indirect
147148
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect
148-
golang.org/x/net v0.50.0 // indirect
149149
golang.org/x/telemetry v0.0.0-20260213145524-e0ab670178e1 // indirect
150150
golang.org/x/tools v0.42.0 // indirect
151151
golang.org/x/tools/go/expect v0.1.1-deprecated // indirect

pkg/cluster/spec/tikv.go

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
dto "github.com/prometheus/client_model/go"
3838
"github.com/prometheus/prom2json"
3939
"go.uber.org/zap"
40+
"golang.org/x/net/http/httpproxy"
4041
)
4142

4243
const (
@@ -514,15 +515,18 @@ func makeTransport(tlsCfg *tls.Config) *http.Transport {
514515
transport.TLSClientConfig = tlsCfg.Clone()
515516
}
516517

517-
// prefer to use the inner http proxy
518-
httpProxy := os.Getenv("TIUP_INNER_HTTP_PROXY")
519-
if len(httpProxy) == 0 {
520-
httpProxy = os.Getenv("HTTP_PROXY")
518+
// Resolve the proxy from the environment so that both HTTP(S)_PROXY and
519+
// NO_PROXY are honored. TIUP_INNER_HTTP_PROXY, if set, overrides the proxy
520+
// URL but NO_PROXY is still respected, so internal endpoints (e.g. the TiKV
521+
// status port) are scraped directly instead of through the proxy.
522+
proxyCfg := httpproxy.FromEnvironment()
523+
if inner := os.Getenv("TIUP_INNER_HTTP_PROXY"); len(inner) > 0 {
524+
proxyCfg.HTTPProxy = inner
525+
proxyCfg.HTTPSProxy = inner
521526
}
522-
if len(httpProxy) > 0 {
523-
if proxyURL, err := url.Parse(httpProxy); err == nil {
524-
transport.Proxy = http.ProxyURL(proxyURL)
525-
}
527+
proxyFunc := proxyCfg.ProxyFunc()
528+
transport.Proxy = func(req *http.Request) (*url.URL, error) {
529+
return proxyFunc(req.URL)
526530
}
527531
return transport
528532
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright 2024 PingCAP, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package spec
15+
16+
import (
17+
"net/http"
18+
"testing"
19+
)
20+
21+
// transportProxyFor returns the proxy URL string that makeTransport would use
22+
// for rawurl ("" means a direct connection / no proxy).
23+
func transportProxyFor(t *testing.T, rawurl string) string {
24+
t.Helper()
25+
tr := makeTransport(nil)
26+
if tr.Proxy == nil {
27+
t.Fatal("transport proxy func is not set")
28+
}
29+
req, err := http.NewRequest(http.MethodGet, rawurl, nil)
30+
if err != nil {
31+
t.Fatal(err)
32+
}
33+
u, err := tr.Proxy(req)
34+
if err != nil {
35+
t.Fatal(err)
36+
}
37+
if u == nil {
38+
return ""
39+
}
40+
return u.String()
41+
}
42+
43+
// TestMakeTransportHonorsNoProxy verifies that a host listed in NO_PROXY (e.g.
44+
// an internal TiKV status endpoint scraped for the leader count) is reached
45+
// directly even when HTTP(S)_PROXY is set, while other hosts use the proxy.
46+
func TestMakeTransportHonorsNoProxy(t *testing.T) {
47+
t.Setenv("TIUP_INNER_HTTP_PROXY", "")
48+
t.Setenv("HTTP_PROXY", "http://proxy.example.com:3128")
49+
t.Setenv("HTTPS_PROXY", "http://proxy.example.com:3128")
50+
t.Setenv("NO_PROXY", "10.0.0.0/8,.internal.example.com")
51+
52+
if got := transportProxyFor(t, "http://tikv-1.internal.example.com:20180/metrics"); got != "" {
53+
t.Errorf("host in NO_PROXY must bypass the proxy, got %q", got)
54+
}
55+
if got := transportProxyFor(t, "https://tiup-mirrors.pingcap.com/timestamp.json"); got != "http://proxy.example.com:3128" {
56+
t.Errorf("external host must use the proxy, got %q", got)
57+
}
58+
}
59+
60+
// TestMakeTransportInnerProxyRespectsNoProxy verifies the TIUP_INNER_HTTP_PROXY
61+
// override still applies to external hosts but also respects NO_PROXY.
62+
func TestMakeTransportInnerProxyRespectsNoProxy(t *testing.T) {
63+
t.Setenv("HTTP_PROXY", "")
64+
t.Setenv("HTTPS_PROXY", "")
65+
t.Setenv("NO_PROXY", ".internal.example.com")
66+
t.Setenv("TIUP_INNER_HTTP_PROXY", "http://inner.example.com:3128")
67+
68+
if got := transportProxyFor(t, "https://tiup-mirrors.pingcap.com/timestamp.json"); got != "http://inner.example.com:3128" {
69+
t.Errorf("external host must use the inner proxy, got %q", got)
70+
}
71+
if got := transportProxyFor(t, "http://tikv-1.internal.example.com:20180/metrics"); got != "" {
72+
t.Errorf("host in NO_PROXY must bypass the inner proxy, got %q", got)
73+
}
74+
}

pkg/utils/http_client.go

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import (
2424
"os"
2525
"path/filepath"
2626
"time"
27+
28+
"golang.org/x/net/http/httpproxy"
2729
)
2830

2931
// HTTPClient is a wrap of http.Client
@@ -37,19 +39,22 @@ func NewHTTPClient(timeout time.Duration, tlsConfig *tls.Config) *HTTPClient {
3739
if timeout < time.Second {
3840
timeout = 10 * time.Second // default timeout is 10s
3941
}
42+
// Resolve the proxy from the environment so that both HTTP(S)_PROXY and
43+
// NO_PROXY are honored. TIUP_INNER_HTTP_PROXY, if set, overrides the proxy
44+
// URL but NO_PROXY is still respected, so internal endpoints (e.g. PD)
45+
// listed in NO_PROXY are reached directly instead of through the proxy.
46+
proxyCfg := httpproxy.FromEnvironment()
47+
if inner := os.Getenv("TIUP_INNER_HTTP_PROXY"); len(inner) > 0 {
48+
proxyCfg.HTTPProxy = inner
49+
proxyCfg.HTTPSProxy = inner
50+
}
51+
proxyFunc := proxyCfg.ProxyFunc()
4052
tr := &http.Transport{
4153
TLSClientConfig: tlsConfig,
4254
Dial: (&net.Dialer{Timeout: 3 * time.Second}).Dial,
43-
}
44-
// prefer to use the inner http proxy
45-
httpProxy := os.Getenv("TIUP_INNER_HTTP_PROXY")
46-
if len(httpProxy) == 0 {
47-
httpProxy = os.Getenv("HTTP_PROXY")
48-
}
49-
if len(httpProxy) > 0 {
50-
if proxyURL, err := url.Parse(httpProxy); err == nil {
51-
tr.Proxy = http.ProxyURL(proxyURL)
52-
}
55+
Proxy: func(req *http.Request) (*url.URL, error) {
56+
return proxyFunc(req.URL)
57+
},
5358
}
5459
return &HTTPClient{
5560
client: &http.Client{
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright 2024 PingCAP, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package utils
15+
16+
import (
17+
"net/http"
18+
"testing"
19+
)
20+
21+
// proxyForURL returns the proxy URL string that the client's transport would
22+
// use for rawurl ("" means a direct connection / no proxy).
23+
func proxyForURL(t *testing.T, rawurl string) string {
24+
t.Helper()
25+
c := NewHTTPClient(0, nil)
26+
tr, ok := c.client.Transport.(*http.Transport)
27+
if !ok || tr.Proxy == nil {
28+
t.Fatalf("transport proxy func is not set")
29+
}
30+
req, err := http.NewRequest(http.MethodGet, rawurl, nil)
31+
if err != nil {
32+
t.Fatal(err)
33+
}
34+
u, err := tr.Proxy(req)
35+
if err != nil {
36+
t.Fatal(err)
37+
}
38+
if u == nil {
39+
return ""
40+
}
41+
return u.String()
42+
}
43+
44+
// TestNewHTTPClientHonorsNoProxy verifies that a host listed in NO_PROXY is
45+
// reached directly even when HTTP(S)_PROXY is set, while other hosts use it.
46+
func TestNewHTTPClientHonorsNoProxy(t *testing.T) {
47+
t.Setenv("TIUP_INNER_HTTP_PROXY", "")
48+
t.Setenv("HTTP_PROXY", "http://proxy.example.com:3128")
49+
t.Setenv("HTTPS_PROXY", "http://proxy.example.com:3128")
50+
t.Setenv("NO_PROXY", "10.0.0.0/8,.internal.example.com")
51+
52+
if got := proxyForURL(t, "http://pd-1.internal.example.com:2379/pd/api/v1/config"); got != "" {
53+
t.Errorf("host in NO_PROXY must bypass the proxy, got %q", got)
54+
}
55+
if got := proxyForURL(t, "https://tiup-mirrors.pingcap.com/timestamp.json"); got != "http://proxy.example.com:3128" {
56+
t.Errorf("external host must use the proxy, got %q", got)
57+
}
58+
}
59+
60+
// TestNewHTTPClientInnerProxyRespectsNoProxy verifies the TIUP_INNER_HTTP_PROXY
61+
// override still applies to external hosts but also respects NO_PROXY.
62+
func TestNewHTTPClientInnerProxyRespectsNoProxy(t *testing.T) {
63+
t.Setenv("HTTP_PROXY", "")
64+
t.Setenv("HTTPS_PROXY", "")
65+
t.Setenv("NO_PROXY", ".internal.example.com")
66+
t.Setenv("TIUP_INNER_HTTP_PROXY", "http://inner.example.com:3128")
67+
68+
if got := proxyForURL(t, "https://tiup-mirrors.pingcap.com/timestamp.json"); got != "http://inner.example.com:3128" {
69+
t.Errorf("external host must use the inner proxy, got %q", got)
70+
}
71+
if got := proxyForURL(t, "http://pd-1.internal.example.com:2379/x"); got != "" {
72+
t.Errorf("host in NO_PROXY must bypass the inner proxy, got %q", got)
73+
}
74+
}

0 commit comments

Comments
 (0)