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(testingx): import and adapt jafar TLS SNI proxy #1238

Merged
merged 2 commits into from
Sep 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
9 changes: 5 additions & 4 deletions internal/logx/handler_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package logx
package logx_test

import (
"fmt"
Expand All @@ -8,12 +8,13 @@ import (

"github.com/apex/log"
"github.com/google/go-cmp/cmp"
"github.com/ooni/probe-cli/v3/internal/logx"
"github.com/ooni/probe-cli/v3/internal/mocks"
"github.com/ooni/probe-cli/v3/internal/testingx"
)

func TestNewHandlerWithDefaultSettings(t *testing.T) {
lh := NewHandlerWithDefaultSettings()
lh := logx.NewHandlerWithDefaultSettings()
if lh.Emoji {
t.Fatal("expected false")
}
Expand All @@ -27,8 +28,8 @@ func TestNewHandlerWithDefaultSettings(t *testing.T) {
}

// creates a new handler with deterministic time to help with testing
func newHandlerForTesting() *Handler {
lh := NewHandlerWithDefaultSettings()
func newHandlerForTesting() *logx.Handler {
lh := logx.NewHandlerWithDefaultSettings()
dtime := testingx.NewTimeDeterministic(time.Now())
lh.Now = dtime.Now
lh.StartTime = dtime.Now()
Expand Down
15 changes: 8 additions & 7 deletions internal/logx/prefix_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package logx
package logx_test

import (
"testing"

"github.com/ooni/probe-cli/v3/internal/logx"
"github.com/ooni/probe-cli/v3/internal/mocks"
)

Expand All @@ -16,7 +17,7 @@ func TestPrefixLogger(t *testing.T) {
}
},
}
logger := &PrefixLogger{
logger := &logx.PrefixLogger{
Prefix: "<0>",
Logger: base,
}
Expand All @@ -32,7 +33,7 @@ func TestPrefixLogger(t *testing.T) {
}
},
}
logger := &PrefixLogger{
logger := &logx.PrefixLogger{
Prefix: "<0>",
Logger: base,
}
Expand All @@ -48,7 +49,7 @@ func TestPrefixLogger(t *testing.T) {
}
},
}
logger := &PrefixLogger{
logger := &logx.PrefixLogger{
Prefix: "<0>",
Logger: base,
}
Expand All @@ -64,7 +65,7 @@ func TestPrefixLogger(t *testing.T) {
}
},
}
logger := &PrefixLogger{
logger := &logx.PrefixLogger{
Prefix: "<0>",
Logger: base,
}
Expand All @@ -80,7 +81,7 @@ func TestPrefixLogger(t *testing.T) {
}
},
}
logger := &PrefixLogger{
logger := &logx.PrefixLogger{
Prefix: "<0>",
Logger: base,
}
Expand All @@ -96,7 +97,7 @@ func TestPrefixLogger(t *testing.T) {
}
},
}
logger := &PrefixLogger{
logger := &logx.PrefixLogger{
Prefix: "<0>",
Logger: base,
}
Expand Down
132 changes: 132 additions & 0 deletions internal/testingx/tlssniproxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package testingx

import (
"context"
"errors"
"fmt"
"io"
"net"
"sync"

"github.com/ooni/netem"
"github.com/ooni/probe-cli/v3/internal/logx"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/runtimex"
)

// TLSSNIProxyNetx is how [TLSSNIProxy] views [*netxlite.Netx].
type TLSSNIProxyNetx interface {
NewDialerWithResolver(dl model.DebugLogger, r model.Resolver, w ...model.DialerWrapper) model.Dialer
NewStdlibResolver(logger model.DebugLogger, wrappers ...model.DNSTransportWrapper) model.Resolver
}

// TLSSNIProxy is a proxy using the SNI to figure out where to connect to.
type TLSSNIProxy struct {
// closeOnce provides "once" semantics for Close.
closeOnce sync.Once

// listener is the TCP listener we're using.
listener net.Listener

// logger is the logger we should use.
logger model.Logger

// netx is the underlying network.
netx TLSSNIProxyNetx

// wg is the wait group for the background listener
wg *sync.WaitGroup
}

// TODO(bassosimone): MustNewTLSSNIProxyEx prototype would be simpler if
// netxlite.Netx was also able to create listening TCP connections

// MustNewTLSSNIProxyEx creates a new [*TLSSNIProxy].
func MustNewTLSSNIProxyEx(
logger model.Logger, netx TLSSNIProxyNetx, tcpAddr *net.TCPAddr, tcpListener TCPListener) *TLSSNIProxy {
listener := runtimex.Try1(tcpListener.ListenTCP("tcp", tcpAddr))
proxy := &TLSSNIProxy{
closeOnce: sync.Once{},
listener: listener,
logger: &logx.PrefixLogger{
Prefix: fmt.Sprintf("%-16s", "TLSPROXY"),
Logger: logger,
},
netx: netx,
wg: &sync.WaitGroup{},
}
proxy.wg.Add(1)
go proxy.mainloop()
return proxy
}

// Close implements io.Closer
func (tp *TLSSNIProxy) Close() (err error) {
tp.closeOnce.Do(func() {
err = tp.listener.Close()
tp.wg.Wait()
})
return
}

// Endpoint returns the listening endpoint or nil after Close has been called.
func (tp *TLSSNIProxy) Endpoint() string {
return tp.listener.Addr().String()
}

func (tp *TLSSNIProxy) mainloop() {
// make sure panics don't crash the process
defer runtimex.CatchLogAndIgnorePanic(tp.logger, "TLSSNIProxy.mainloop")

defer tp.wg.Done()
for {
conn, err := tp.listener.Accept()
if errors.Is(err, net.ErrClosed) {
return
}

// use panics to reduce the testing surface, which is ~okay given
// that this code is meant to support testing
runtimex.PanicOnError(err, "tp.listener.Accept() failed")

// we're creating a goroutine per connection, which is ~okay because
// this code is designed for helping with testing
go tp.handle(conn)
}
}

func (tp *TLSSNIProxy) handle(clientConn net.Conn) {
// make sure panics don't crash the process
defer runtimex.CatchLogAndIgnorePanic(tp.logger, "TLSSNIProxy.handle")

// make sure we close the client connection
defer clientConn.Close()

// read initial records
buffer := make([]byte, 1<<17)
count := runtimex.Try1(clientConn.Read(buffer))
rawRecords := buffer[:count]

// inspecty the raw records to find the SNI
sni := runtimex.Try1(netem.ExtractTLSServerName(rawRecords))

// connect to the remote host
tcpDialer := tp.netx.NewDialerWithResolver(tp.logger, tp.netx.NewStdlibResolver(tp.logger))
serverConn := runtimex.Try1(tcpDialer.DialContext(context.Background(), "tcp", net.JoinHostPort(sni, "443")))
defer serverConn.Close()

// forward the initial records to the server
_ = runtimex.Try1(serverConn.Write(rawRecords))

// route traffic between the conns
wg := &sync.WaitGroup{}
wg.Add(2)
go tp.forward(wg, clientConn, serverConn)
go tp.forward(wg, serverConn, clientConn)
wg.Wait()
}

func (tp *TLSSNIProxy) forward(wg *sync.WaitGroup, left, right net.Conn) {
defer wg.Done()
io.Copy(right, left)
}
121 changes: 121 additions & 0 deletions internal/testingx/tlssniproxy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package testingx_test

import (
"context"
"crypto/tls"
"io"
"net"
"net/http"
"testing"

"github.com/apex/log"
"github.com/ooni/netem"
"github.com/ooni/probe-cli/v3/internal/netxlite"
"github.com/ooni/probe-cli/v3/internal/runtimex"
"github.com/ooni/probe-cli/v3/internal/testingx"
)

func TestTLSSNIProxy(t *testing.T) {
// testcase is a test case run by this function
type testcase struct {
name string
construct func() (*testingx.TLSSNIProxy, *netxlite.Netx, []io.Closer)
short bool
}

testcases := []testcase{{
name: "when using the real network",
construct: func() (*testingx.TLSSNIProxy, *netxlite.Netx, []io.Closer) {
var closers []io.Closer

netxProxy := &netxlite.Netx{
Underlying: nil, // use the network
}
tcpAddr := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1)}
tcpListener := &testingx.TCPListenerStdlib{}

proxy := testingx.MustNewTLSSNIProxyEx(log.Log, netxProxy, tcpAddr, tcpListener)
closers = append(closers, proxy)

netxClient := &netxlite.Netx{
Underlying: nil, // use the network
}

return proxy, netxClient, closers
},
short: false,
}, {
name: "when using netem",
construct: func() (*testingx.TLSSNIProxy, *netxlite.Netx, []io.Closer) {
var closers []io.Closer

topology := runtimex.Try1(netem.NewStarTopology(log.Log))
closers = append(closers, topology)

wwwStack := runtimex.Try1(topology.AddHost("142.251.209.14", "142.251.209.14", &netem.LinkConfig{}))
proxyStack := runtimex.Try1(topology.AddHost("10.0.0.1", "142.251.209.14", &netem.LinkConfig{}))
clientStack := runtimex.Try1(topology.AddHost("10.0.0.2", "142.251.209.14", &netem.LinkConfig{}))

dnsConfig := netem.NewDNSConfig()
dnsConfig.AddRecord("www.google.com", "", "142.251.209.14")
dnsServer := runtimex.Try1(netem.NewDNSServer(log.Log, wwwStack, "142.251.209.14", dnsConfig))
closers = append(closers, dnsServer)

wwwServer := testingx.MustNewHTTPServerTLSEx(
&net.TCPAddr{IP: net.IPv4(142, 251, 209, 14), Port: 443},
wwwStack,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Bonsoir, Elliot!"))
}),
wwwStack,
)
closers = append(closers, wwwServer)

proxy := testingx.MustNewTLSSNIProxyEx(
log.Log,
&netxlite.Netx{Underlying: &netxlite.NetemUnderlyingNetworkAdapter{UNet: proxyStack}},
&net.TCPAddr{IP: net.IPv4(10, 0, 0, 1), Port: 443},
proxyStack,
)
closers = append(closers, proxy)

clientNet := &netxlite.Netx{Underlying: &netxlite.NetemUnderlyingNetworkAdapter{UNet: clientStack}}
return proxy, clientNet, closers
},
short: true,
}}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
if !tc.short && testing.Short() {
t.Skip("skip test in short mode")
}

proxy, clientNet, closers := tc.construct()
defer func() {
for _, closer := range closers {
closer.Close()
}
}()

//log.SetLevel(log.DebugLevel)

tlsConfig := &tls.Config{
ServerName: "www.google.com",
}
tcpDialer := clientNet.NewDialerWithResolver(log.Log, clientNet.NewStdlibResolver(log.Log))
tlsHandshaker := clientNet.NewTLSHandshakerStdlib(log.Log)
tlsDialer := netxlite.NewTLSDialerWithConfig(tcpDialer, tlsHandshaker, tlsConfig)

conn, err := tlsDialer.DialTLSContext(context.Background(), "tcp", proxy.Endpoint())
if err != nil {
t.Fatal(err)
}
defer conn.Close()

tconn := conn.(netxlite.TLSConn)
connstate := tconn.ConnectionState()
t.Logf("%+v", connstate)
})
}
}
13 changes: 7 additions & 6 deletions internal/testingx/tlsx.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"sync"
"time"

"github.com/apex/log"
"github.com/ooni/netem"
"github.com/ooni/probe-cli/v3/internal/runtimex"
)
Expand Down Expand Up @@ -142,16 +143,16 @@ func (p *TLSServer) Close() (err error) {
}

func (p *TLSServer) mainloop(ctx context.Context) {
defer runtimex.CatchLogAndIgnorePanic(log.Log, "TLSServer.mainloop")
defer p.wg.Done()

for {
conn, err := p.listener.Accept()
if errors.Is(err, net.ErrClosed) {
return
}
if err != nil {
continue
}

// because this is a testing server and because golang returns net.ErrClosed while
// gvisor returns "invalid argument", here we're using panic to handle the error
// such that we can quickly exit and we don't need to test these implementation details
runtimex.PanicOnError(err, "p.listener.Accept")

// create a goroutine for connection, which is overkill in general
// but reasonable for a server designed for testing
Expand Down
Loading