返回 DeepSeek-Reasonix
dialer.go
根目录 / internal / netclient / dialer.go
1 package netclient
2
3 import (
4 "bufio"
5 "context"
6 "crypto/tls"
7 "fmt"
8 "net"
9 "net/http"
10 "net/url"
11 "strings"
12 "time"
13
14 "golang.org/x/net/proxy"
15 )
16
17 // StreamDialer opens a raw TCP stream under the same proxy policy netclient
18 // applies to HTTP. It is the dial seam for non-HTTP protocols (SSH) so a
19 // user's configured proxy is honored consistently.
20 type StreamDialer interface {
21 DialContext(ctx context.Context, network, addr string) (net.Conn, error)
22 }
23
24 // DialerFunc adapts a function to StreamDialer.
25 type DialerFunc func(ctx context.Context, network, addr string) (net.Conn, error)
26
27 // DialContext implements StreamDialer.
28 func (f DialerFunc) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
29 return f(ctx, network, addr)
30 }
31
32 // NewStreamDialer builds a StreamDialer for spec. Off/env/auto with no
33 // applicable proxy dial directly; socks5/socks5h dial through the SOCKS proxy;
34 // http/https dial through an HTTP CONNECT tunnel. DirectHosts and NoProxy are
35 // honored via the shared proxy resolution.
36 func NewStreamDialer(spec ProxySpec) (StreamDialer, error) {
37 pf, err := proxyFunc(spec)
38 if err != nil {
39 return nil, err
40 }
41 base := &net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}
42 if pf == nil {
43 return DialerFunc(base.DialContext), nil
44 }
45 return DialerFunc(func(ctx context.Context, network, addr string) (net.Conn, error) {
46 // proxyFunc keys off a request URL; synthesize one for the target so
47 // scheme-agnostic TCP dials reuse the exact NoProxy/DirectHosts logic.
48 host, _, splitErr := net.SplitHostPort(addr)
49 if splitErr != nil {
50 host = addr
51 }
52 probe := &http.Request{URL: &url.URL{Scheme: "https", Host: host}}
53 pu, perr := pf(probe)
54 if perr != nil {
55 return nil, perr
56 }
57 if pu == nil {
58 return base.DialContext(ctx, network, addr)
59 }
60 switch strings.ToLower(pu.Scheme) {
61 case "socks5", "socks5h":
62 return dialSOCKS5(ctx, pu, base, network, addr)
63 case "http", "https":
64 return dialHTTPConnect(ctx, pu, base, addr)
65 default:
66 return nil, fmt.Errorf("netclient: unsupported proxy scheme %q for stream dial", pu.Scheme)
67 }
68 }), nil
69 }
70
71 func dialSOCKS5(ctx context.Context, pu *url.URL, fwd *net.Dialer, network, addr string) (net.Conn, error) {
72 var auth *proxy.Auth
73 if pu.User != nil {
74 pw, _ := pu.User.Password()
75 auth = &proxy.Auth{User: pu.User.Username(), Password: pw}
76 }
77 d, err := proxy.SOCKS5("tcp", pu.Host, auth, fwd)
78 if err != nil {
79 return nil, err
80 }
81 if cd, ok := d.(proxy.ContextDialer); ok {
82 return cd.DialContext(ctx, network, addr)
83 }
84 return d.Dial(network, addr)
85 }
86
87 // dialHTTPConnect opens a CONNECT tunnel through an http/https proxy. For an
88 // https proxy the connection to the proxy itself must be TLS: the CONNECT
89 // request (including Proxy-Authorization credentials) is sent inside that TLS
90 // session, not in cleartext. The SSH client then speaks its own protocol over
91 // the established stream, so only the tunnel handshake lives here.
92 func dialHTTPConnect(ctx context.Context, pu *url.URL, base *net.Dialer, target string) (net.Conn, error) {
93 conn, err := base.DialContext(ctx, "tcp", pu.Host)
94 if err != nil {
95 return nil, err
96 }
97 if strings.EqualFold(pu.Scheme, "https") {
98 host := pu.Hostname()
99 tconn := tls.Client(conn, &tls.Config{ServerName: host})
100 hsCtx := ctx
101 if _, ok := ctx.Deadline(); !ok {
102 var cancel context.CancelFunc
103 hsCtx, cancel = context.WithTimeout(ctx, 30*time.Second)
104 defer cancel()
105 }
106 if herr := tconn.HandshakeContext(hsCtx); herr != nil {
107 _ = conn.Close()
108 return nil, fmt.Errorf("netclient: TLS handshake to https proxy %s: %w", pu.Host, herr)
109 }
110 conn = tconn
111 }
112 if deadline, ok := ctx.Deadline(); ok {
113 _ = conn.SetDeadline(deadline)
114 }
115 req := &http.Request{
116 Method: http.MethodConnect,
117 URL: &url.URL{Opaque: target},
118 Host: target,
119 Header: make(http.Header),
120 }
121 if pu.User != nil {
122 pw, _ := pu.User.Password()
123 req.SetBasicAuth(pu.User.Username(), pw)
124 req.Header.Set("Proxy-Authorization", req.Header.Get("Authorization"))
125 req.Header.Del("Authorization")
126 }
127 if err := req.Write(conn); err != nil {
128 conn.Close()
129 return nil, err
130 }
131 br := bufio.NewReader(conn)
132 resp, err := http.ReadResponse(br, req)
133 if err != nil {
134 conn.Close()
135 return nil, err
136 }
137 defer resp.Body.Close()
138 if resp.StatusCode != http.StatusOK {
139 conn.Close()
140 return nil, fmt.Errorf("netclient: proxy CONNECT to %s failed: %s", target, resp.Status)
141 }
142 // Clear the handshake deadline; the caller manages timeouts thereafter.
143 _ = conn.SetDeadline(time.Time{})
144 if br.Buffered() > 0 {
145 // A well-behaved proxy sends nothing before the tunnel opens; if it
146 // did, wrap so buffered bytes aren't lost.
147 return &bufferedConn{Conn: conn, r: br}, nil
148 }
149 return conn, nil
150 }
151
152 type bufferedConn struct {
153 net.Conn
154 r *bufio.Reader
155 }
156
157 func (b *bufferedConn) Read(p []byte) (int, error) { return b.r.Read(p) }
158
158 lines GO