-
Notifications
You must be signed in to change notification settings - Fork 54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(netxlite): add HTTP/HTTPS proxy dialer #1162
base: master
Are you sure you want to change the base?
Changes from 4 commits
7812cb7
d1817c9
f1f003a
263b486
65eb0fd
6c45c7f
b6a9d00
fa35c9d
ad0e1b0
09bf5d8
bdc7c59
2e87095
fe221a5
367b67d
cdc717c
a17d298
0abb2ea
236338b
3c92f1b
5586866
4072763
bff5db7
6ce0847
c5107f7
145812e
b394f7d
1a179ab
2492145
d9f1366
7043e47
be76630
069705b
a032484
a99423b
546ccd6
cfd7168
5fa7bd7
f4eb5af
088b086
f6b3843
06820c9
9c713e8
74a025f
18b383e
519cce1
217bbec
b725a06
546d6f4
8637821
6a78f5e
1df748b
65d1a18
ecd945a
267780c
da20e69
99b4a6a
64154e0
2c515b5
6614095
bdc25c6
d6cbf84
e3e60d9
6daa1e6
7d6b30b
d9652e4
aed7d71
99123e6
21b8ef6
4acfedc
576d130
be7b768
a674c6a
1fb2502
937f7e2
99b6d88
a2d7a2f
406a11e
89c7385
d425770
aeecd5d
d2a9ed9
7404aed
7b6aa6d
5bccd71
a3a0eb7
d437da0
7293b50
6e9a9ac
5d4435d
c395c75
f352783
f5af9a7
ba7e913
39bf550
27d2b9a
e95e8d8
6069871
3fec1d6
3f145a4
736e5e6
60f5ca1
01a8f4b
af3ee42
e5306e9
566d410
23e8fb5
91e20fd
6485c8b
2d709bc
1452293
d1fa91c
6497f87
8bc336d
7154467
8da6341
b429542
50c9a4e
15f6ffd
c55096b
e6ed7a2
1239f54
55c7891
84e1ad6
99d2169
6228893
e01e0c5
9920cff
dd12ea5
7e10550
c214d5e
0f556e5
de44810
412145f
de99ecf
2fc7125
23a6844
e782a31
e85c352
75873f0
b755ecd
e509128
98525dc
62e47ec
b710bb9
6792f2c
9924da4
98d066b
784495d
5ded144
7e5d98f
885905e
207db60
ea297fc
47b1fca
c1d24c8
6766140
70794a6
a952d71
540259a
cfb3a35
0dec1c7
aabfbce
440b76f
6ec6701
7e71e8c
2bddee9
bf7a9df
b3c293d
c81f9f3
e10302d
05547c4
02a32fb
a4554dc
829876d
c986674
5f45449
b5d5264
fb08981
d6239ef
f5cd0c0
b9a76a6
dcb5d5a
99cf201
0aa3919
2dc5c00
140d18f
18b694c
4c81abe
0cb3a45
dbe10d3
f013db5
da656b3
f359fef
940d6ae
a30eb55
3ace615
6973451
87aafd5
f944cf5
669b27c
495c237
d578149
0cf610a
63d31d2
51f2a0a
37367b5
ec19103
e9414f9
8daf64f
7b6cf3e
ae80474
ef127a7
e30e816
c1ea612
e435d33
76ff37b
617aea6
71802a5
fa9d3c2
acc4a47
5d41159
54ce48a
eeab472
21db18c
b964cd6
a2ea223
b9ec11a
013513c
1bec4e0
fed3ea1
21bcf7e
ab5a1cb
2063d53
67d120c
c980fc3
3bf045e
68b2925
56e034f
abe9779
30d5a17
d0b85d0
afb8a36
e3abb00
f9484b5
6e0e734
edbb31c
970f89f
80a87a6
c6b0a2c
b933f34
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,222 @@ | ||
package netxlite | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"github.com/ooni/probe-cli/v3/internal/model" | ||
"golang.org/x/net/proxy" | ||
"net" | ||
"net/http" | ||
"strconv" | ||
"strings" | ||
"time" | ||
) | ||
|
||
// A HttpDialer holds HTTP-specific options | ||
// Specifically for HTTP proxy, we build an HTTP tunnel | ||
type HttpDialer struct { | ||
Murphy-OrangeMud marked this conversation as resolved.
Show resolved
Hide resolved
|
||
proxy.Dialer | ||
proxyNetwork string | ||
proxyAddress string | ||
timeout time.Duration | ||
ProxyDial func(context.Context, string, string) (net.Conn, error) | ||
} | ||
|
||
func (d *HttpDialer) Dial(network, address string) (net.Conn, error) { | ||
Murphy-OrangeMud marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if err := d.validateTarget(network, address); err != nil { | ||
proxy, dst, _ := d.pathAddrs(address) | ||
return nil, &net.OpError{Op: http.MethodConnect, Net: network, Source: proxy, Addr: dst, Err: err} | ||
} | ||
var err error | ||
var c net.Conn | ||
|
||
if d.ProxyDial != nil { | ||
c, err = d.ProxyDial(context.Background(), d.proxyNetwork, d.proxyAddress) | ||
} else { | ||
nd := &net.Dialer{Timeout: d.timeout} | ||
c, err = nd.DialContext(context.Background(), d.proxyNetwork, d.proxyAddress) | ||
} | ||
|
||
if err != nil { | ||
proxy, dst, _ := d.pathAddrs(address) | ||
return nil, &net.OpError{Op: http.MethodConnect, Net: network, Source: proxy, Addr: dst, Err: err} | ||
} | ||
if err := d.DialWithConn(context.Background(), c, network, address); err != nil { | ||
c.Close() | ||
return nil, err | ||
} | ||
return c, nil | ||
} | ||
|
||
func (d *HttpDialer) validateTarget(network, address string) error { | ||
Murphy-OrangeMud marked this conversation as resolved.
Show resolved
Hide resolved
|
||
switch network { | ||
case "tcp", "tcp6", "tcp4": | ||
default: | ||
return errors.New("network not implemented") | ||
} | ||
return nil | ||
} | ||
|
||
type Addr struct { | ||
Murphy-OrangeMud marked this conversation as resolved.
Show resolved
Hide resolved
|
||
network string | ||
Name string // fully-qualified domain name | ||
IP net.IP | ||
Port int | ||
} | ||
|
||
func (a *Addr) Network() string { return a.network } | ||
|
||
func (a *Addr) String() string { | ||
if a == nil { | ||
return "<nil>" | ||
} | ||
port := strconv.Itoa(a.Port) | ||
if a.IP == nil { | ||
return net.JoinHostPort(a.Name, port) | ||
} | ||
return net.JoinHostPort(a.IP.String(), port) | ||
} | ||
|
||
func splitHostPort(address string) (string, int, error) { | ||
host, port, err := net.SplitHostPort(address) | ||
if err != nil { | ||
return "", 0, err | ||
} | ||
portnum, err := strconv.Atoi(port) | ||
if err != nil { | ||
return "", 0, err | ||
} | ||
if 1 > portnum || portnum > 0xffff { | ||
return "", 0, errors.New("port number out of range " + port) | ||
} | ||
return host, portnum, nil | ||
} | ||
|
||
func (d *HttpDialer) pathAddrs(address string) (proxy, dst net.Addr, err error) { | ||
for i, s := range []string{d.proxyAddress, address} { | ||
host, port, err := splitHostPort(s) | ||
if err != nil { | ||
return nil, nil, err | ||
} | ||
a := &Addr{Port: port} | ||
a.IP = net.ParseIP(host) | ||
if a.IP == nil { | ||
a.Name = host | ||
} | ||
if i == 0 { | ||
proxy = a | ||
} else { | ||
dst = a | ||
} | ||
} | ||
return | ||
} | ||
|
||
func (d *HttpDialer) DialWithConn(ctx context.Context, c net.Conn, network, address string) error { | ||
Murphy-OrangeMud marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if err := d.validateTarget(network, address); err != nil { | ||
proxy, dst, _ := d.pathAddrs(address) | ||
return &net.OpError{Op: http.MethodConnect, Net: network, Source: proxy, Addr: dst, Err: err} | ||
} | ||
if ctx == nil { | ||
proxy, dst, _ := d.pathAddrs(address) | ||
return &net.OpError{Op: http.MethodConnect, Net: network, Source: proxy, Addr: dst, Err: errors.New("nil context")} | ||
} | ||
|
||
connectReq := fmt.Sprintf("%v %v HTTP/1.1\r\n"+ | ||
"Host: %v\r\n"+ | ||
"Proxy-Connection: keep-alive\r\n"+ | ||
"User-Agent: %v\r\n\r\n", http.MethodConnect, address, address, model.HTTPHeaderUserAgent) | ||
|
||
b := []byte(connectReq) | ||
|
||
n, err := c.Write(b) | ||
if err != nil { | ||
proxy, dst, _ := d.pathAddrs(address) | ||
return &net.OpError{Op: http.MethodConnect, Net: network, Source: proxy, Addr: dst, Err: err} | ||
} | ||
if n != len(b) { | ||
proxy, dst, _ := d.pathAddrs(address) | ||
return &net.OpError{Op: http.MethodConnect, Net: network, Source: proxy, Addr: dst, Err: errors.New("not write enough bytes")} | ||
} | ||
|
||
c.Read(b) | ||
if err != nil { | ||
proxy, dst, _ := d.pathAddrs(address) | ||
return &net.OpError{Op: http.MethodConnect, Net: network, Source: proxy, Addr: dst, Err: err} | ||
} | ||
|
||
str := string(b) | ||
if strings.Split(str, " ")[1] != "200" { | ||
proxy, dst, _ := d.pathAddrs(address) | ||
return &net.OpError{Op: http.MethodConnect, Net: network, Source: proxy, Addr: dst, Err: errors.New("cannot establish connection")} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (d *HttpDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { | ||
Murphy-OrangeMud marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if err := d.validateTarget(network, address); err != nil { | ||
Murphy-OrangeMud marked this conversation as resolved.
Show resolved
Hide resolved
|
||
proxy, dst, _ := d.pathAddrs(address) | ||
return nil, &net.OpError{Op: http.MethodConnect, Net: network, Source: proxy, Addr: dst, Err: err} | ||
Murphy-OrangeMud marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
if ctx == nil { | ||
proxy, dst, _ := d.pathAddrs(address) | ||
return nil, &net.OpError{Op: http.MethodConnect, Net: network, Source: proxy, Addr: dst, Err: errors.New("nil context")} | ||
} | ||
Murphy-OrangeMud marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// proxy dial | ||
var err error | ||
var c net.Conn | ||
Murphy-OrangeMud marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if d.ProxyDial != nil { | ||
c, err = d.ProxyDial(ctx, d.proxyNetwork, d.proxyAddress) | ||
} else { | ||
nd := &net.Dialer{Timeout: d.timeout} | ||
c, err = nd.DialContext(ctx, d.proxyNetwork, d.proxyAddress) | ||
} | ||
if err != nil { | ||
proxy, dst, _ := d.pathAddrs(address) | ||
return nil, &net.OpError{Op: http.MethodConnect, Net: network, Source: proxy, Addr: dst, Err: err} | ||
} | ||
|
||
connectReq := fmt.Sprintf("%v %v HTTP/1.1\r\n"+ | ||
"Host: %v\r\n"+ | ||
"Proxy-Connection: keep-alive\r\n"+ | ||
"User-Agent: %v\r\n\r\n", http.MethodConnect, address, address, model.HTTPHeaderUserAgent) | ||
|
||
b := []byte(connectReq) | ||
Murphy-OrangeMud marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
n, err := c.Write(b) | ||
Murphy-OrangeMud marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if err != nil { | ||
proxy, dst, _ := d.pathAddrs(address) | ||
return nil, &net.OpError{Op: http.MethodConnect, Net: network, Source: proxy, Addr: dst, Err: err} | ||
} | ||
if n != len(b) { | ||
Murphy-OrangeMud marked this conversation as resolved.
Show resolved
Hide resolved
|
||
proxy, dst, _ := d.pathAddrs(address) | ||
return nil, &net.OpError{Op: http.MethodConnect, Net: network, Source: proxy, Addr: dst, Err: errors.New("not write enough bytes")} | ||
} | ||
|
||
c.Read(b) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems wrong since you're not checking for an error or the amount of bytes read here. |
||
if err != nil { | ||
proxy, dst, _ := d.pathAddrs(address) | ||
return nil, &net.OpError{Op: http.MethodConnect, Net: network, Source: proxy, Addr: dst, Err: err} | ||
} | ||
|
||
str := string(b) | ||
if strings.Split(str, " ")[1] != "200" { | ||
proxy, dst, _ := d.pathAddrs(address) | ||
return nil, &net.OpError{Op: http.MethodConnect, Net: network, Source: proxy, Addr: dst, Err: errors.New("cannot establish connection")} | ||
} | ||
Murphy-OrangeMud marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
return c, nil | ||
|
||
} | ||
|
||
func NewHTTPDialer(network, address string) *HttpDialer { | ||
return &HttpDialer{ | ||
proxyNetwork: network, | ||
proxyAddress: address, | ||
timeout: dialerDefaultTimeout, | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
package netxlite | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"net" | ||
"net/http" | ||
"testing" | ||
) | ||
|
||
func TestHTTPProxyDialer(t *testing.T) { | ||
// REMINDER: This test need a http proxy running locally | ||
dialer := NewHTTPDialer("tcp", "localhost:7890") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We recently added support for proxies in the |
||
t.Run("DialContextSuccess", func(t *testing.T) { | ||
conn, err := dialer.DialContext(context.Background(), "tcp", "google.com:443") | ||
if err != nil { | ||
t.Fatal(fmt.Sprintf("unexpected error: %v", err)) | ||
} | ||
if conn == nil { | ||
t.Fatal("unexpected nil connection") | ||
} | ||
}) | ||
|
||
t.Run("DialSuccess", func(t *testing.T) { | ||
conn, err := dialer.Dial("tcp", "google.com:443") | ||
if err != nil { | ||
t.Fatal(fmt.Sprintf("unexpected error: %v", err)) | ||
} | ||
if conn == nil { | ||
t.Fatal("unexpected nil connection") | ||
} | ||
}) | ||
|
||
t.Run("DialContextInvalidNetwork", func(t *testing.T) { | ||
expected := errors.New("network not implemented") | ||
conn, err := dialer.DialContext(context.Background(), "udp", "google.com:443") | ||
if conn != nil { | ||
t.Fatal("unexpected connection") | ||
} | ||
if err.(*net.OpError).Err.Error() != expected.Error() { | ||
t.Fatal(fmt.Sprintf("unexpected error: %v", err)) | ||
} | ||
}) | ||
|
||
t.Run("DialInvalidNetwork", func(t *testing.T) { | ||
expected := errors.New("network not implemented") | ||
conn, err := dialer.Dial("udp", "google.com:443") | ||
if conn != nil { | ||
t.Fatal("unexpected connection") | ||
} | ||
if err.(*net.OpError).Err.Error() != expected.Error() { | ||
t.Fatal(fmt.Sprintf("unexpected error: %v", err)) | ||
} | ||
}) | ||
} | ||
|
||
func TestHTTPProxyDialerFailure(t *testing.T) { | ||
dialer := NewHTTPDialer("tcp", "localhost:8888") | ||
go func() { | ||
listener, err := net.Listen("tcp", "localhost:8888") | ||
defer listener.Close() | ||
if err != nil { | ||
t.Error(fmt.Sprintf("error: listen failed%v", err)) | ||
return | ||
} | ||
err = http.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
if r.Method == http.MethodConnect { | ||
w.WriteHeader(500) | ||
return | ||
} else { | ||
t.Error("error: unexpected request") | ||
return | ||
} | ||
})) | ||
}() | ||
t.Run("DialContextFailure", func(t *testing.T) { | ||
expected := errors.New("cannot establish connection") | ||
|
||
conn, err := dialer.DialContext(context.Background(), "tcp", "google.com:443") | ||
if conn != nil { | ||
t.Fatal("unexpected connection") | ||
} | ||
if err.(*net.OpError).Err.Error() != expected.Error() { | ||
t.Fatal("unexpected error") | ||
} | ||
}) | ||
|
||
t.Run("DialFailure", func(t *testing.T) { | ||
expected := errors.New("cannot establish connection") | ||
|
||
conn, err := dialer.Dial("tcp", "google.com:443") | ||
if conn != nil { | ||
t.Fatal("unexpected connection") | ||
} | ||
if err.(*net.OpError).Err.Error() != expected.Error() { | ||
t.Fatal(fmt.Sprintf("unexpected error: %v", err)) | ||
} | ||
}) | ||
|
||
return | ||
Murphy-OrangeMud marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -45,12 +45,19 @@ var ErrProxyUnsupportedScheme = errors.New("proxy: unsupported scheme") | |
// DialContext implements Dialer.DialContext. | ||
func (d *proxyDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { | ||
url := d.ProxyURL | ||
if url.Scheme != "socks5" { | ||
if url.Scheme == "socks5" { | ||
// the code at proxy/socks5.go never fails; see https://git.io/JfJ4g | ||
child, _ := proxy.SOCKS5(network, url.Host, nil, &proxyDialerWrapper{d.Dialer}) | ||
return d.dial(ctx, child, network, address) | ||
} else if url.Scheme == "http" { | ||
child := NewHTTPDialer(network, url.Host) | ||
child.ProxyDial = func(ctx context.Context, network string, address string) (net.Conn, error) { | ||
return d.Dialer.(proxy.ContextDialer).DialContext(ctx, network, address) | ||
} | ||
return d.dial(ctx, child, network, address) | ||
} else { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A few comments here:
|
||
return nil, ErrProxyUnsupportedScheme | ||
} | ||
// the code at proxy/socks5.go never fails; see https://git.io/JfJ4g | ||
child, _ := proxy.SOCKS5(network, url.Host, nil, &proxyDialerWrapper{d.Dialer}) | ||
return d.dial(ctx, child, network, address) | ||
} | ||
|
||
func (d *proxyDialer) dial( | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code is very fragile and assumes that we're creating a specific chain of wrappers. I'd rather try to rewrite the pull request such that we avoid using this code entirely. My understanding is that you don't want to wrap the connection more than once. Wrapping multiple times should be fine, but probably it's also possible to avoid wrapping by using a
model.UnderlyingNetwork
, which provides a dialer that, in the common case, is the standard library dialer.