Skip to content

Commit ebfdb91

Browse files
committed
fix: handle dynamic HTTP proxy settings for discovery client
Unfortunately, gRPC client library doesn't offer control on picking up HTTP proxy for a connection, and relies on sync.Once protected value which reads proxy variables from the environment just once. As environment variables can be set dynamically, there is a chance that dial might happen before they are set, so they are never respected after that. So I had to copy/adapt some part of grpc internal/transport parts to enable dynamic loading of HTTP proxy, and use new dialer in discovery client. Fixes #10136 Also, while testing this with our small "air-gapped" tool I discovered that our HTTP proxy test implementation is not complete, so this had to be fixed as well. Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
1 parent d45eaeb commit ebfdb91

5 files changed

Lines changed: 219 additions & 17 deletions

File tree

cmd/talosctl/cmd/mgmt/debug/air-gapped.go

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -194,14 +194,16 @@ func runHTTPServer(ctx context.Context, certPEM, keyPEM []byte) error {
194194
}
195195

196196
func handleTunneling(w http.ResponseWriter, r *http.Request) {
197-
dst, err := net.DialTimeout("tcp", r.Host, 10*time.Second)
197+
addr := r.URL.Host
198+
199+
dstConn, err := net.DialTimeout("tcp", addr, 10*time.Second)
198200
if err != nil {
199201
http.Error(w, err.Error(), http.StatusServiceUnavailable)
200202

201203
return
202204
}
203205

204-
w.WriteHeader(http.StatusOK)
206+
dst := dstConn.(*net.TCPConn)
205207

206208
hijacker, ok := w.(http.Hijacker)
207209
if !ok {
@@ -217,15 +219,35 @@ func handleTunneling(w http.ResponseWriter, r *http.Request) {
217219
return
218220
}
219221

220-
go transfer(dst, clientConn)
221-
go transfer(clientConn, dst)
222+
src := clientConn.(*net.TCPConn)
223+
224+
src.Write([]byte("HTTP/1.0 200 Connection established\r\n\r\n")) //nolint:errcheck
225+
226+
log.Printf("HTTP CONNECT: tunneling to %s", addr)
227+
228+
defer dst.Close() //nolint:errcheck
229+
defer src.Close() //nolint:errcheck
230+
231+
var eg errgroup.Group
232+
233+
eg.Go(func() error { return transfer(dst, src, "src -> dst: "+addr) })
234+
eg.Go(func() error { return transfer(src, dst, "dst -> src: "+addr) })
235+
236+
if err = eg.Wait(); err != nil {
237+
log.Printf("HTTP CONNECT: tunneling to %s: failed %v", addr, err)
238+
}
222239
}
223240

224-
func transfer(destination io.WriteCloser, source io.ReadCloser) {
225-
defer destination.Close() //nolint:errcheck
226-
defer source.Close() //nolint:errcheck
241+
func transfer(destination *net.TCPConn, source *net.TCPConn, label string) error {
242+
defer destination.CloseWrite() //nolint:errcheck
243+
defer source.CloseRead() //nolint:errcheck
244+
245+
n, err := io.Copy(destination, source)
246+
if err != nil {
247+
return fmt.Errorf("transfer failed %s (%d bytes copied): %w", label, n, err)
248+
}
227249

228-
io.Copy(destination, source) //nolint:errcheck
250+
return nil
229251
}
230252

231253
func handleHTTP(w http.ResponseWriter, req *http.Request) {

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,8 @@ require (
140140
github.com/safchain/ethtool v0.5.9
141141
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.30
142142
github.com/siderolabs/crypto v0.5.1
143-
github.com/siderolabs/discovery-api v0.1.5
144-
github.com/siderolabs/discovery-client v0.1.10
143+
github.com/siderolabs/discovery-api v0.1.6
144+
github.com/siderolabs/discovery-client v0.1.11
145145
github.com/siderolabs/gen v0.8.0
146146
github.com/siderolabs/go-api-signature v0.3.6
147147
github.com/siderolabs/go-blockdevice v0.4.8
@@ -191,7 +191,7 @@ require (
191191
golang.org/x/time v0.10.0
192192
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
193193
google.golang.org/grpc v1.70.0
194-
google.golang.org/protobuf v1.36.4
194+
google.golang.org/protobuf v1.36.5
195195
gopkg.in/yaml.v3 v3.0.1
196196
k8s.io/klog/v2 v2.130.1
197197
kernel.org/pub/linux/libs/security/libcap/cap v1.2.73

go.sum

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -635,10 +635,10 @@ github.com/siderolabs/coredns v1.12.50 h1:uOBWZErtM3pcncGj0XxHru53Xo3SND/qjEVkfH
635635
github.com/siderolabs/coredns v1.12.50/go.mod h1:le+OxWZF+rYtNGuYvxuOAdlrTwJAHKcuW09SPu25w2c=
636636
github.com/siderolabs/crypto v0.5.1 h1:aZEUTZBoP8rH+0TqQAlUgazriPh89MrXf4R+th+m6ps=
637637
github.com/siderolabs/crypto v0.5.1/go.mod h1:7RHC7eUKBx6RLS2lDaNXrQ83zY9iPH/aQSTxk1I4/j4=
638-
github.com/siderolabs/discovery-api v0.1.5 h1:fcHVkLkWla7C5+9IeOGEUQ4N8Yp9R7a/kcKbiay2QKw=
639-
github.com/siderolabs/discovery-api v0.1.5/go.mod h1:b9jOm9T2puYVcRqCAjWxPcHz2qBqDX8I0OZDOyOFHXg=
640-
github.com/siderolabs/discovery-client v0.1.10 h1:bTAvFLiISSzVXyYL1cIgAz8cPYd9ZfvhxwdebgtxARA=
641-
github.com/siderolabs/discovery-client v0.1.10/go.mod h1:Ew1z07eyJwqNwum84IKYH4S649KEKK5WUmRW49HlXS8=
638+
github.com/siderolabs/discovery-api v0.1.6 h1:/LhsF1ytqFEfWwV0UKfUgn90k9fk5+rhYMJ9yeUB2yc=
639+
github.com/siderolabs/discovery-api v0.1.6/go.mod h1:s5CnTyRMGid/vJNSJs8Jw9I4tnKHu/2SGqP2ytTaePQ=
640+
github.com/siderolabs/discovery-client v0.1.11 h1:Au+7QZ+CIB6g4C7ZCC4m5Ai5Uso1g/I3/E4bSUElzF8=
641+
github.com/siderolabs/discovery-client v0.1.11/go.mod h1:Iw5XUphGNNV0m2czHjbj9aLhQvfM8hYEfWCc6fdQ4ko=
642642
github.com/siderolabs/ethtool v0.3.0 h1:98kMFGnkDEikngqtDyk6R/ykMjaGpJ1df/bw9vPLKJg=
643643
github.com/siderolabs/ethtool v0.3.0/go.mod h1:3u47fCDlOQGM7IhXhUB3uPEcjdQxGmK53/45kEg1HDM=
644644
github.com/siderolabs/gen v0.8.0 h1:Pj93+hexkk5hQ7izjJ6YXnEWc8vlzOmDwFz13/VzS7o=
@@ -1084,8 +1084,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
10841084
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
10851085
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
10861086
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
1087-
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
1088-
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
1087+
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
1088+
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
10891089
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
10901090
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
10911091
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=

internal/app/machined/pkg/controllers/cluster/discovery_service.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ import (
2323
"github.com/siderolabs/gen/optional"
2424
"github.com/siderolabs/gen/xslices"
2525
"go.uber.org/zap"
26+
"google.golang.org/grpc"
2627

28+
"github.com/siderolabs/talos/pkg/grpc/dialer"
2729
"github.com/siderolabs/talos/pkg/httpdefaults"
2830
"github.com/siderolabs/talos/pkg/machinery/config/machine"
2931
"github.com/siderolabs/talos/pkg/machinery/proto"
@@ -244,6 +246,9 @@ func (ctrl *DiscoveryServiceController) Run(ctx context.Context, r controller.Ru
244246
TLSConfig: &tls.Config{
245247
RootCAs: httpdefaults.RootCAs(),
246248
},
249+
DialOptions: []grpc.DialOption{
250+
grpc.WithContextDialer(dialer.DynamicProxyDialer),
251+
},
247252
})
248253
if err != nil {
249254
return fmt.Errorf("error initializing discovery client: %w", err)

pkg/grpc/dialer/proxy.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
package dialer
6+
7+
import (
8+
"bufio"
9+
"context"
10+
"encoding/base64"
11+
"fmt"
12+
"io"
13+
"net"
14+
"net/http"
15+
"net/http/httputil"
16+
"net/url"
17+
18+
"golang.org/x/net/http/httpproxy"
19+
"google.golang.org/grpc"
20+
)
21+
22+
/*
23+
* Copyright 2023 gRPC authors.
24+
*
25+
* Licensed under the Apache License, Version 2.0 (the "License");
26+
* you may not use this file except in compliance with the License.
27+
* You may obtain a copy of the License at
28+
*
29+
* http://www.apache.org/licenses/LICENSE-2.0
30+
*
31+
* Unless required by applicable law or agreed to in writing, software
32+
* distributed under the License is distributed on an "AS IS" BASIS,
33+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
34+
* See the License for the specific language governing permissions and
35+
* limitations under the License.
36+
*
37+
*/
38+
39+
const grpcUA = "grpc-go/" + grpc.Version
40+
41+
// DynamicProxyDialer is a fork of grpc standard dialer which supports dynamic resolving of proxy settings
42+
// on each request (vs. caching it once per process).
43+
//
44+
// DynamicProxyDialer assumes that the address is using 'tcp' network.
45+
func DynamicProxyDialer(ctx context.Context, addr string) (net.Conn, error) {
46+
newAddr := addr
47+
48+
proxyURL, err := mapAddress(addr)
49+
if err != nil {
50+
return nil, err
51+
}
52+
53+
if proxyURL != nil {
54+
newAddr = proxyURL.Host
55+
}
56+
57+
conn, err := NetDialerWithTCPKeepalive().DialContext(ctx, "tcp", newAddr)
58+
if err != nil {
59+
return nil, err
60+
}
61+
62+
if proxyURL == nil {
63+
// proxy is disabled if proxyURL is nil.
64+
return conn, err
65+
}
66+
67+
return doHTTPConnectHandshake(ctx, conn, addr, proxyURL, grpcUA)
68+
}
69+
70+
const proxyAuthHeaderKey = "Proxy-Authorization"
71+
72+
func mapAddress(address string) (*url.URL, error) {
73+
req := &http.Request{
74+
URL: &url.URL{
75+
Scheme: "https",
76+
Host: address,
77+
},
78+
}
79+
80+
return httpproxy.FromEnvironment().ProxyFunc()(req.URL)
81+
}
82+
83+
// To read a response from a net.Conn, http.ReadResponse() takes a bufio.Reader.
84+
// It's possible that this reader reads more than what's need for the response and stores
85+
// those bytes in the buffer.
86+
// bufConn wraps the original net.Conn and the bufio.Reader to make sure we don't lose the
87+
// bytes in the buffer.
88+
type bufConn struct {
89+
net.Conn
90+
r io.Reader
91+
}
92+
93+
func (c *bufConn) Read(b []byte) (int, error) {
94+
return c.r.Read(b)
95+
}
96+
97+
func basicAuth(username, password string) string {
98+
auth := username + ":" + password
99+
100+
return base64.StdEncoding.EncodeToString([]byte(auth))
101+
}
102+
103+
func doHTTPConnectHandshake(ctx context.Context, conn net.Conn, backendAddr string, proxyURL *url.URL, grpcUA string) (_ net.Conn, err error) {
104+
defer func() {
105+
if err != nil {
106+
conn.Close() //nolint:errcheck
107+
}
108+
}()
109+
110+
req := &http.Request{
111+
Method: http.MethodConnect,
112+
URL: &url.URL{Host: backendAddr},
113+
Header: map[string][]string{"User-Agent": {grpcUA}},
114+
}
115+
116+
if t := proxyURL.User; t != nil {
117+
u := t.Username()
118+
p, _ := t.Password()
119+
req.Header.Add(proxyAuthHeaderKey, "Basic "+basicAuth(u, p))
120+
}
121+
122+
if err := sendHTTPRequest(ctx, req, conn); err != nil {
123+
return nil, fmt.Errorf("failed to write the HTTP request: %v", err)
124+
}
125+
126+
r := bufio.NewReader(conn)
127+
128+
resp, err := http.ReadResponse(r, req)
129+
if err != nil {
130+
return nil, fmt.Errorf("reading server HTTP response: %v", err)
131+
}
132+
133+
defer resp.Body.Close() //nolint:errcheck
134+
135+
if resp.StatusCode != http.StatusOK {
136+
dump, err := httputil.DumpResponse(resp, true)
137+
if err != nil {
138+
return nil, fmt.Errorf("failed to do connect handshake, status code: %s", resp.Status)
139+
}
140+
141+
return nil, fmt.Errorf("failed to do connect handshake, response: %q", dump)
142+
}
143+
144+
// The buffer could contain extra bytes from the target server, so we can't
145+
// discard it. However, in many cases where the server waits for the client
146+
// to send the first message (e.g. when TLS is being used), the buffer will
147+
// be empty, so we can avoid the overhead of reading through this buffer.
148+
if r.Buffered() != 0 {
149+
return &bufConn{Conn: conn, r: r}, nil
150+
}
151+
152+
return conn, nil
153+
}
154+
155+
func sendHTTPRequest(ctx context.Context, req *http.Request, conn net.Conn) error {
156+
req = req.WithContext(ctx)
157+
if err := req.Write(conn); err != nil {
158+
return fmt.Errorf("failed to write the HTTP request: %v", err)
159+
}
160+
161+
return nil
162+
}
163+
164+
// NetDialerWithTCPKeepalive returns a net.Dialer that enables TCP keepalives on
165+
// the underlying connection with OS default values for keepalive parameters.
166+
func NetDialerWithTCPKeepalive() *net.Dialer {
167+
return &net.Dialer{
168+
KeepAliveConfig: net.KeepAliveConfig{
169+
Enable: true,
170+
Idle: -1,
171+
Count: -1,
172+
Interval: -1,
173+
},
174+
}
175+
}

0 commit comments

Comments
 (0)