Skip to content
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(enginenetx): support HTTP and HTTPS proxies #1282

Merged
merged 2 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions internal/enginenetx/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,13 @@ func NewHTTPTransport(
resolver model.Resolver,
) *HTTPTransport {
dialer := netxlite.NewDialerWithResolver(logger, resolver)
dialer = netxlite.MaybeWrapWithProxyDialer(dialer, proxyURL)
handshaker := netxlite.NewTLSHandshakerStdlib(logger)
tlsDialer := netxlite.NewTLSDialer(dialer, handshaker)
// TODO(https://github.com/ooni/probe/issues/2534): here we're using the QUIRKY netxlite.NewHTTPTransport
// function, but we can probably avoid using it, given that this code is
// not using tracing and does not care about those quirks.
txp := netxlite.NewHTTPTransport(logger, dialer, tlsDialer)
txp := netxlite.NewHTTPTransportWithOptions(
logger, dialer, tlsDialer,
netxlite.HTTPTransportOptionDisableCompression(false),
netxlite.HTTPTransportOptionProxyURL(proxyURL), // nil implies "no proxy"
)
txp = bytecounter.WrapHTTPTransport(txp, counter)
return &HTTPTransport{txp}
}
265 changes: 247 additions & 18 deletions internal/enginenetx/http_test.go
Original file line number Diff line number Diff line change
@@ -1,34 +1,263 @@
package enginenetx
package enginenetx_test

import (
"context"
"net"
"net/http"
"net/url"
"testing"
"time"

"github.com/apex/log"
"github.com/ooni/probe-cli/v3/internal/bytecounter"
"github.com/ooni/probe-cli/v3/internal/enginenetx"
"github.com/ooni/probe-cli/v3/internal/measurexlite"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/netemx"
"github.com/ooni/probe-cli/v3/internal/netxlite"
"github.com/ooni/probe-cli/v3/internal/testingsocks5"
"github.com/ooni/probe-cli/v3/internal/testingx"
)

func TestHTTPTransport(t *testing.T) {
func TestHTTPTransportWAI(t *testing.T) {
t.Run("is WAI when not using any proxy", func(t *testing.T) {
env := netemx.MustNewScenario(netemx.InternetScenario)
defer env.Close()

// TODO(bassosimone): we should replace this integration test with netemx
// as soon as we can sever the hard link between netxlite and this pkg
t.Run("is working as intended", func(t *testing.T) {
txp := NewHTTPTransport(
bytecounter.New(), model.DiscardLogger, nil, netxlite.NewStdlibResolver(model.DiscardLogger))
client := txp.NewHTTPClient()
resp, err := client.Get("https://www.google.com/robots.txt")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
t.Fatal("unexpected status code")
}
env.Do(func() {
txp := enginenetx.NewHTTPTransport(
bytecounter.New(),
model.DiscardLogger,
nil,
netxlite.NewStdlibResolver(model.DiscardLogger),
)
client := txp.NewHTTPClient()
resp, err := client.Get("https://www.example.com/")
if err != nil {
t.Fatal(err)
}
t.Logf("%+v", resp)
defer resp.Body.Close()
if resp.StatusCode != 200 {
t.Fatal("unexpected status code")
}
})
})

t.Run("is WAI when using a SOCKS5 proxy", func(t *testing.T) {
// create internet measurement scenario
env := netemx.MustNewScenario(netemx.InternetScenario)
defer env.Close()

// create a proxy using the client's TCP/IP stack
proxy := testingsocks5.MustNewServer(
log.Log,
&netxlite.Netx{Underlying: &netxlite.NetemUnderlyingNetworkAdapter{UNet: env.ClientStack}},
&net.TCPAddr{
IP: net.ParseIP(env.ClientStack.IPAddress()),
Port: 9050,
},
)
defer proxy.Close()

env.Do(func() {
txp := enginenetx.NewHTTPTransport(
bytecounter.New(),
model.DiscardLogger,
&url.URL{
Scheme: "socks5",
Host: net.JoinHostPort(env.ClientStack.IPAddress(), "9050"),
Path: "/",
},
netxlite.NewStdlibResolver(model.DiscardLogger),
)
client := txp.NewHTTPClient()

// To make sure we're connecting to the expected endpoint, we're going to use
// measurexlite and tracing to observe the destination endpoints
trace := measurexlite.NewTrace(0, time.Now())
ctx := netxlite.ContextWithTrace(context.Background(), trace)

// create request using the above context
//
// Implementation note: we cannot use HTTPS with netem here as explained
// by the https://github.com/ooni/probe/issues/2536 issue.
req, err := http.NewRequestWithContext(ctx, "GET", "http://www.example.com/", nil)
if err != nil {
t.Fatal(err)
}

resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
t.Logf("%+v", resp)
defer resp.Body.Close()
if resp.StatusCode != 200 {
t.Fatal("unexpected status code")
}

// make sure that we only connected to the SOCKS5 proxy
tcpConnects := trace.TCPConnects()
if len(tcpConnects) <= 0 {
t.Fatal("expected at least one TCP connect")
}
for idx, entry := range tcpConnects {
t.Logf("%d: %+v", idx, entry)
if entry.IP != env.ClientStack.IPAddress() {
t.Fatal("unexpected IP address")
}
if entry.Port != 9050 {
t.Fatal("unexpected port")
}
}
})
})

t.Run("is WAI when using an HTTP proxy", func(t *testing.T) {
// create internet measurement scenario
env := netemx.MustNewScenario(netemx.InternetScenario)
defer env.Close()

// create a proxy using the client's TCP/IP stack
proxy := testingx.MustNewHTTPServerEx(
&net.TCPAddr{IP: net.ParseIP(env.ClientStack.IPAddress()), Port: 8080},
env.ClientStack,
testingx.NewHTTPProxyHandler(log.Log, &netxlite.Netx{
Underlying: &netxlite.NetemUnderlyingNetworkAdapter{UNet: env.ClientStack}}),
)
defer proxy.Close()

env.Do(func() {
txp := enginenetx.NewHTTPTransport(
bytecounter.New(),
model.DiscardLogger,
&url.URL{
Scheme: "http",
Host: net.JoinHostPort(env.ClientStack.IPAddress(), "8080"),
Path: "/",
},
netxlite.NewStdlibResolver(model.DiscardLogger),
)
client := txp.NewHTTPClient()

// To make sure we're connecting to the expected endpoint, we're going to use
// measurexlite and tracing to observe the destination endpoints
trace := measurexlite.NewTrace(0, time.Now())
ctx := netxlite.ContextWithTrace(context.Background(), trace)

// create request using the above context
//
// Implementation note: we cannot use HTTPS with netem here as explained
// by the https://github.com/ooni/probe/issues/2536 issue.
req, err := http.NewRequestWithContext(ctx, "GET", "http://www.example.com/", nil)
if err != nil {
t.Fatal(err)
}

resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
t.Logf("%+v", resp)
defer resp.Body.Close()
if resp.StatusCode != 200 {
t.Fatal("unexpected status code")
}

// make sure that we only connected to the SOCKS5 proxy
bassosimone marked this conversation as resolved.
Show resolved Hide resolved
tcpConnects := trace.TCPConnects()
if len(tcpConnects) <= 0 {
t.Fatal("expected at least one TCP connect")
}
for idx, entry := range tcpConnects {
t.Logf("%d: %+v", idx, entry)
if entry.IP != env.ClientStack.IPAddress() {
t.Fatal("unexpected IP address")
}
if entry.Port != 8080 {
t.Fatal("unexpected port")
}
}
})
})

t.Run("is WAI when using an HTTPS proxy", func(t *testing.T) {
// create internet measurement scenario
env := netemx.MustNewScenario(netemx.InternetScenario)
defer env.Close()

// create a proxy using the client's TCP/IP stack
proxy := testingx.MustNewHTTPServerTLSEx(
&net.TCPAddr{IP: net.ParseIP(env.ClientStack.IPAddress()), Port: 4443},
env.ClientStack,
testingx.NewHTTPProxyHandler(log.Log, &netxlite.Netx{
Underlying: &netxlite.NetemUnderlyingNetworkAdapter{UNet: env.ClientStack}}),
env.ClientStack,
)
defer proxy.Close()

env.Do(func() {
txp := enginenetx.NewHTTPTransport(
bytecounter.New(),
model.DiscardLogger,
&url.URL{
Scheme: "https",
Host: net.JoinHostPort(env.ClientStack.IPAddress(), "4443"),
Path: "/",
},
netxlite.NewStdlibResolver(model.DiscardLogger),
)
client := txp.NewHTTPClient()

// To make sure we're connecting to the expected endpoint, we're going to use
// measurexlite and tracing to observe the destination endpoints
trace := measurexlite.NewTrace(0, time.Now())
ctx := netxlite.ContextWithTrace(context.Background(), trace)

// create request using the above context
//
// Implementation note: we cannot use HTTPS with netem here as explained
// by the https://github.com/ooni/probe/issues/2536 issue.
req, err := http.NewRequestWithContext(ctx, "GET", "http://www.example.com/", nil)
if err != nil {
t.Fatal(err)
}

resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
t.Logf("%+v", resp)
defer resp.Body.Close()
if resp.StatusCode != 200 {
t.Fatal("unexpected status code")
}

// make sure that we only connected to the SOCKS5 proxy
bassosimone marked this conversation as resolved.
Show resolved Hide resolved
tcpConnects := trace.TCPConnects()
if len(tcpConnects) <= 0 {
t.Fatal("expected at least one TCP connect")
}
for idx, entry := range tcpConnects {
t.Logf("%d: %+v", idx, entry)
if entry.IP != env.ClientStack.IPAddress() {
t.Fatal("unexpected IP address")
}
if entry.Port != 4443 {
t.Fatal("unexpected port")
}
}
})
})

t.Run("NewHTTPClient returns a client with a cookie jar", func(t *testing.T) {
txp := NewHTTPTransport(
bytecounter.New(), model.DiscardLogger, nil, netxlite.NewStdlibResolver(model.DiscardLogger))
txp := enginenetx.NewHTTPTransport(
bytecounter.New(),
model.DiscardLogger,
nil,
netxlite.NewStdlibResolver(model.DiscardLogger),
)
client := txp.NewHTTPClient()
if client.Jar == nil {
t.Fatal("expected non-nil cookie jar")
Expand Down
10 changes: 5 additions & 5 deletions internal/netemx/qaenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,8 @@ type QAEnv struct {
// clientNICWrapper is the OPTIONAL wrapper for the client NIC.
clientNICWrapper netem.LinkNICWrapper

// clientStack is the client stack to use.
clientStack *netem.UNetStack
// ClientStack is the client stack to use.
ClientStack *netem.UNetStack

// closables contains all entities where we have to take care of closing.
closables []io.Closer
Expand Down Expand Up @@ -197,7 +197,7 @@ func MustNewQAEnv(options ...QAEnvOption) *QAEnv {
env := &QAEnv{
baseLogger: config.logger,
clientNICWrapper: config.clientNICWrapper,
clientStack: nil,
ClientStack: nil,
closables: []io.Closer{},
emulateAndroidGetaddrinfo: &atomic.Bool{},
ispResolverConfig: netem.NewDNSConfig(),
Expand All @@ -208,7 +208,7 @@ func MustNewQAEnv(options ...QAEnvOption) *QAEnv {
}

// create all the required internals
env.clientStack = env.mustNewClientStack(config)
env.ClientStack = env.mustNewClientStack(config)
env.closables = append(env.closables, env.mustNewNetStacks(config)...)

return env
Expand Down Expand Up @@ -306,7 +306,7 @@ func (env *QAEnv) EmulateAndroidGetaddrinfo(value bool) {
// Do executes the given function such that [netxlite] code uses the
// underlying clientStack rather than ordinary networking code.
func (env *QAEnv) Do(function func()) {
var stack netem.UnderlyingNetwork = env.clientStack
var stack netem.UnderlyingNetwork = env.ClientStack
if env.emulateAndroidGetaddrinfo.Load() {
stack = &androidStack{stack}
}
Expand Down
2 changes: 2 additions & 0 deletions internal/netxlite/maybeproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ type proxyDialer struct {

// MaybeWrapWithProxyDialer returns the original dialer if the proxyURL is nil
// and otherwise returns a wrapped dialer that implements proxying.
//
// Deprecated: do not use this function in new code.
func MaybeWrapWithProxyDialer(dialer model.Dialer, proxyURL *url.URL) model.Dialer {
if proxyURL == nil {
return dialer
Expand Down