Skip to content

Commit 6293187

Browse files
authored
Merge pull request #115 from SenseUnit/h2
HTTP/2 upstream proxies
2 parents 3874dcb + b77bb09 commit 6293187

File tree

5 files changed

+254
-57
lines changed

5 files changed

+254
-57
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,17 @@ Supported proxy schemes are:
340340
* `curves` - colon-separated list of enabled TLS key exchange curves.
341341
* `min-tls-version` - minimum TLS version.
342342
* `max-tls-version` - maximum TLS version.
343+
* `h2` - HTTP/2 proxy over TLS connection. Examples: `h2://user:[email protected]`, `h2://example.org?cert=cert.pem&key=key.pem`. This method also supports additional parameters passed in query string:
344+
* `cafile` - file with CA certificates in PEM format used to verify TLS peer.
345+
* `sni` - override value of ServerName Indication extension.
346+
* `peername` - expect specified name in peer certificate. Empty string relaxes any name constraints.
347+
* `cert` - file with user certificate for mutual TLS authentication. Should be used in conjunction with `key`.
348+
* `key` - file with private key matching user certificate specified with `cert` option.
349+
* `ciphers` - colon-separated list of enabled TLS ciphersuites.
350+
* `curves` - colon-separated list of enabled TLS key exchange curves.
351+
* `min-tls-version` - minimum TLS version.
352+
* `max-tls-version` - maximum TLS version.
353+
* `h2c` - HTTP/2 proxy over plaintext connection with the CONNECT method support. Examples: `h2c://example.org:8080`.
343354
* `socks5`, `socks5h` - SOCKS5 proxy with hostname resolving via remote proxy. Example: `socks5://127.0.0.1:9050`.
344355
* `set-src-hints` - not an actual proxy, but a signal to use different source IP address hints for this connection. It's useful to route traffic across multiple network interfaces, including VPN connections. URL has to have one query parameter `hints` with a comma-separated list of IP addresses. See `-ip-hints` command line option for more details. Example: `set-src-hints://?hints=10.2.0.2`
345356
* `cached` - pseudo-dialer which caches construction of another dialer specified by URL passed in `url` parameter of query string. Useful for dialers which are constructed dynamically from JS router script and which load certificate files. Example: `cache://?url=https%3A%2F%2Fexample.org%3Fcert%3Dcert.pem%26key%3Dkey.pem&ttl=5m`. Query string parameters are:

dialer/dialer.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
func init() {
1313
xproxy.RegisterDialerType("http", HTTPProxyDialerFromURL)
1414
xproxy.RegisterDialerType("https", HTTPProxyDialerFromURL)
15+
xproxy.RegisterDialerType("h2", H2ProxyDialerFromURL)
16+
xproxy.RegisterDialerType("h2c", H2ProxyDialerFromURL)
1517
xproxy.RegisterDialerType("set-src-hints", NewHintsSettingDialerFromURL)
1618
xproxy.RegisterDialerType("cached", GetCachedDialer)
1719
}

dialer/h2.go

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package dialer
2+
3+
import (
4+
"context"
5+
"crypto/tls"
6+
"errors"
7+
"fmt"
8+
"io"
9+
"net"
10+
"net/http"
11+
"net/url"
12+
"slices"
13+
"strings"
14+
"time"
15+
16+
"github.com/SenseUnit/dumbproxy/tlsutil"
17+
"golang.org/x/net/http2"
18+
xproxy "golang.org/x/net/proxy"
19+
)
20+
21+
type H2ProxyDialer struct {
22+
address string
23+
tlsConfig *tls.Config
24+
userinfo *url.Userinfo
25+
next Dialer
26+
t *http2.Transport
27+
}
28+
29+
func H2ProxyDialerFromURL(u *url.URL, next xproxy.Dialer) (xproxy.Dialer, error) {
30+
host := u.Hostname()
31+
port := u.Port()
32+
33+
var (
34+
tlsConfig *tls.Config
35+
err error
36+
h2c bool
37+
)
38+
switch strings.ToLower(u.Scheme) {
39+
case "h2c":
40+
if port == "" {
41+
port = "80"
42+
}
43+
h2c = true
44+
case "h2":
45+
if port == "" {
46+
port = "443"
47+
}
48+
tlsConfig, err = tlsutil.TLSConfigFromURL(u)
49+
if !slices.Contains(tlsConfig.NextProtos, "h2") {
50+
tlsConfig.NextProtos = append([]string{"h2"}, tlsConfig.NextProtos...)
51+
}
52+
if err != nil {
53+
return nil, fmt.Errorf("TLS configuration failed: %w", err)
54+
}
55+
default:
56+
return nil, errors.New("unsupported proxy type")
57+
}
58+
59+
address := net.JoinHostPort(host, port)
60+
t := &http2.Transport{
61+
AllowHTTP: h2c,
62+
TLSClientConfig: tlsConfig,
63+
}
64+
nextDialer := MaybeWrapWithContextDialer(next)
65+
if h2c {
66+
t.DialTLSContext = func(ctx context.Context, network, _ string, _ *tls.Config) (net.Conn, error) {
67+
return nextDialer.DialContext(ctx, network, address)
68+
}
69+
} else {
70+
t.DialTLSContext = func(ctx context.Context, network, _ string, _ *tls.Config) (net.Conn, error) {
71+
conn, err := nextDialer.DialContext(ctx, network, address)
72+
if err != nil {
73+
return nil, err
74+
}
75+
conn = tls.Client(conn, tlsConfig)
76+
return conn, nil
77+
}
78+
}
79+
80+
return &H2ProxyDialer{
81+
address: address,
82+
tlsConfig: tlsConfig,
83+
userinfo: u.User,
84+
next: MaybeWrapWithContextDialer(next),
85+
t: t,
86+
}, nil
87+
}
88+
89+
func (d *H2ProxyDialer) Dial(network, address string) (net.Conn, error) {
90+
return d.DialContext(context.Background(), network, address)
91+
}
92+
93+
func (d *H2ProxyDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
94+
h2c := d.tlsConfig == nil
95+
scheme := "https"
96+
if h2c {
97+
scheme = "http"
98+
}
99+
pr, pw := io.Pipe()
100+
connCtx, connCl := context.WithCancel(ctx)
101+
req := (&http.Request{
102+
Method: "CONNECT",
103+
URL: &url.URL{
104+
Scheme: scheme,
105+
Host: address,
106+
},
107+
Header: http.Header{
108+
"User-Agent": []string{"dumbproxy"},
109+
},
110+
Body: pr,
111+
Host: address,
112+
}).WithContext(connCtx)
113+
if d.userinfo != nil {
114+
req.Header.Set("Proxy-Authorization", basicAuthHeader(d.userinfo))
115+
}
116+
resp, err := d.t.RoundTrip(req)
117+
if err != nil {
118+
return nil, err
119+
}
120+
if resp.StatusCode != http.StatusOK {
121+
resp.Body.Close()
122+
pw.Close()
123+
return nil, errors.New(resp.Status)
124+
}
125+
return &h2Conn{
126+
r: resp.Body,
127+
w: pw,
128+
cl: connCl,
129+
}, nil
130+
}
131+
132+
type h2Conn struct {
133+
r io.ReadCloser
134+
w io.WriteCloser
135+
cl func()
136+
}
137+
138+
func (c *h2Conn) Read(b []byte) (n int, err error) {
139+
return c.r.Read(b)
140+
}
141+
142+
func (c *h2Conn) Write(b []byte) (n int, err error) {
143+
return c.w.Write(b)
144+
}
145+
146+
func (c *h2Conn) Close() (err error) {
147+
defer c.cl()
148+
return errors.Join(c.w.Close(), c.r.Close())
149+
}
150+
151+
func (c *h2Conn) LocalAddr() net.Addr {
152+
return &net.TCPAddr{IP: net.IPv4zero, Port: 0}
153+
}
154+
155+
func (c *h2Conn) RemoteAddr() net.Addr {
156+
return &net.TCPAddr{IP: net.IPv4zero, Port: 0}
157+
}
158+
159+
func (c *h2Conn) SetDeadline(t time.Time) error {
160+
return &net.OpError{Op: "set", Net: "h2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")}
161+
}
162+
163+
func (c *h2Conn) SetReadDeadline(t time.Time) error {
164+
return &net.OpError{Op: "set", Net: "h2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")}
165+
}
166+
167+
func (c *h2Conn) SetWriteDeadline(t time.Time) error {
168+
return &net.OpError{Op: "set", Net: "h2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")}
169+
}
170+
171+
func (c *h2Conn) CloseWrite() error {
172+
return c.w.Close()
173+
}

dialer/upstream.go

Lines changed: 4 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,9 @@ func NewHTTPProxyDialer(address string, tlsConfig *tls.Config, userinfo *url.Use
3939
func HTTPProxyDialerFromURL(u *url.URL, next xproxy.Dialer) (xproxy.Dialer, error) {
4040
host := u.Hostname()
4141
port := u.Port()
42-
params, err := url.ParseQuery(u.RawQuery)
43-
if err != nil {
44-
return nil, fmt.Errorf("unable to parse query string of proxy specification URL %q: %w", u.String(), err)
45-
}
4642

4743
var tlsConfig *tls.Config
44+
var err error
4845
switch strings.ToLower(u.Scheme) {
4946
case "http":
5047
if port == "" {
@@ -54,59 +51,9 @@ func HTTPProxyDialerFromURL(u *url.URL, next xproxy.Dialer) (xproxy.Dialer, erro
5451
if port == "" {
5552
port = "443"
5653
}
57-
tlsConfig = &tls.Config{
58-
ServerName: host,
59-
}
60-
if params.Has("cafile") {
61-
roots, err := tlsutil.LoadCAfile(params.Get("cafile"))
62-
if err != nil {
63-
return nil, err
64-
}
65-
tlsConfig.RootCAs = roots
66-
}
67-
if params.Has("sni") {
68-
tlsConfig.ServerName = params.Get("sni")
69-
tlsConfig.InsecureSkipVerify = true
70-
tlsConfig.VerifyConnection = tlsutil.ExpectPeerName(host, tlsConfig.RootCAs)
71-
}
72-
if params.Has("peername") {
73-
tlsConfig.InsecureSkipVerify = true
74-
tlsConfig.VerifyConnection = tlsutil.ExpectPeerName(params.Get("peername"), tlsConfig.RootCAs)
75-
}
76-
if params.Has("cert") {
77-
cert, err := tls.LoadX509KeyPair(params.Get("cert"), params.Get("key"))
78-
if err != nil {
79-
return nil, err
80-
}
81-
tlsConfig.Certificates = []tls.Certificate{cert}
82-
}
83-
if params.Has("ciphers") {
84-
cipherList, err := tlsutil.ParseCipherList(params.Get("ciphers"))
85-
if err != nil {
86-
return nil, err
87-
}
88-
tlsConfig.CipherSuites = cipherList
89-
}
90-
if params.Has("curves") {
91-
curveList, err := tlsutil.ParseCurveList(params.Get("curves"))
92-
if err != nil {
93-
return nil, err
94-
}
95-
tlsConfig.CurvePreferences = curveList
96-
}
97-
if params.Has("min-tls-version") {
98-
ver, err := tlsutil.ParseVersion(params.Get("min-tls-version"))
99-
if err != nil {
100-
return nil, err
101-
}
102-
tlsConfig.MinVersion = ver
103-
}
104-
if params.Has("max-tls-version") {
105-
ver, err := tlsutil.ParseVersion(params.Get("max-tls-version"))
106-
if err != nil {
107-
return nil, err
108-
}
109-
tlsConfig.MaxVersion = ver
54+
tlsConfig, err = tlsutil.TLSConfigFromURL(u)
55+
if err != nil {
56+
return nil, fmt.Errorf("TLS configuration failed: %w", err)
11057
}
11158
default:
11259
return nil, errors.New("unsupported proxy type")

tlsutil/util.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"crypto/tls"
55
"crypto/x509"
66
"fmt"
7+
"net/url"
78
"os"
89
"strings"
910
)
@@ -158,3 +159,66 @@ func FormatVersion(v uint16) string {
158159
return fmt.Sprintf("%#04x", v)
159160
}
160161
}
162+
163+
func TLSConfigFromURL(u *url.URL) (*tls.Config, error) {
164+
host := u.Hostname()
165+
params, err := url.ParseQuery(u.RawQuery)
166+
if err != nil {
167+
return nil, fmt.Errorf("unable to parse query string of proxy specification URL %q: %w", u.String(), err)
168+
}
169+
tlsConfig := &tls.Config{
170+
ServerName: host,
171+
}
172+
if params.Has("cafile") {
173+
roots, err := LoadCAfile(params.Get("cafile"))
174+
if err != nil {
175+
return nil, err
176+
}
177+
tlsConfig.RootCAs = roots
178+
}
179+
if params.Has("sni") {
180+
tlsConfig.ServerName = params.Get("sni")
181+
tlsConfig.InsecureSkipVerify = true
182+
tlsConfig.VerifyConnection = ExpectPeerName(host, tlsConfig.RootCAs)
183+
}
184+
if params.Has("peername") {
185+
tlsConfig.InsecureSkipVerify = true
186+
tlsConfig.VerifyConnection = ExpectPeerName(params.Get("peername"), tlsConfig.RootCAs)
187+
}
188+
if params.Has("cert") {
189+
cert, err := tls.LoadX509KeyPair(params.Get("cert"), params.Get("key"))
190+
if err != nil {
191+
return nil, err
192+
}
193+
tlsConfig.Certificates = []tls.Certificate{cert}
194+
}
195+
if params.Has("ciphers") {
196+
cipherList, err := ParseCipherList(params.Get("ciphers"))
197+
if err != nil {
198+
return nil, err
199+
}
200+
tlsConfig.CipherSuites = cipherList
201+
}
202+
if params.Has("curves") {
203+
curveList, err := ParseCurveList(params.Get("curves"))
204+
if err != nil {
205+
return nil, err
206+
}
207+
tlsConfig.CurvePreferences = curveList
208+
}
209+
if params.Has("min-tls-version") {
210+
ver, err := ParseVersion(params.Get("min-tls-version"))
211+
if err != nil {
212+
return nil, err
213+
}
214+
tlsConfig.MinVersion = ver
215+
}
216+
if params.Has("max-tls-version") {
217+
ver, err := ParseVersion(params.Get("max-tls-version"))
218+
if err != nil {
219+
return nil, err
220+
}
221+
tlsConfig.MaxVersion = ver
222+
}
223+
return tlsConfig, nil
224+
}

0 commit comments

Comments
 (0)