From 07e76dcdaa84af758c745572da3adb78dda006c4 Mon Sep 17 00:00:00 2001 From: ooninoob Date: Tue, 22 Nov 2022 21:43:27 +0100 Subject: [PATCH] Start work on jafar --- internal/cmd/jafar/tcpproxy/tcpproxy.go | 210 +++++++++++++++++++ internal/cmd/jafar/tcpproxy/tlsproxy_test.go | 181 ++++++++++++++++ 2 files changed, 391 insertions(+) create mode 100644 internal/cmd/jafar/tcpproxy/tcpproxy.go create mode 100644 internal/cmd/jafar/tcpproxy/tlsproxy_test.go diff --git a/internal/cmd/jafar/tcpproxy/tcpproxy.go b/internal/cmd/jafar/tcpproxy/tcpproxy.go new file mode 100644 index 0000000..77f8726 --- /dev/null +++ b/internal/cmd/jafar/tcpproxy/tcpproxy.go @@ -0,0 +1,210 @@ +// Package tlsproxy contains a censoring TLS proxy. Most traffic is passed +// through using the SNI to choose the hostname to connect to. Specific offending +// SNIs are censored by returning a TLS alert to the client. +package tlsproxy + +import ( + "context" + "crypto/tls" + "errors" + "net" + "strings" + "sync" + + "github.com/apex/log" +) + +// Dialer establishes network connections +type Dialer interface { + DialContext(ctx context.Context, network, address string) (net.Conn, error) +} + +// CensoringProxy is a censoring TLS proxy +type CensoringProxy struct { + keywords []string + dial func(network, address string) (net.Conn, error) +} + +// NewCensoringProxy creates a new CensoringProxy instance using +// the specified list of keywords to censor. keywords is the list +// of keywords that trigger censorship if any of them appears in +// the SNII record of a ClientHello. dnsNetwork and dnsAddress are +// settings to configure the upstream, non censored DNS. +func NewCensoringProxy( + keywords []string, to string, uncensored Dialer, +) *CensoringProxy { + return &CensoringProxy{ + keywords: keywords, + to: to, + dial: func(network, address string) (net.Conn, error) { + return uncensored.DialContext(context.Background(), network, address) + }, + } +} + +// handshakeReader is a hack to perform the initial part of the +// TLS handshake so to know the SNI and then replay the bytes of +// this initial part of the handshake with the server. +//type handshakeReader struct { +// net.Conn +// incoming []byte +//} +// +//// Read saves the initial bytes of the handshake such that later +//// we can replay the handshake with the real TLS server. +//func (c *handshakeReader) Read(b []byte) (int, error) { +// count, err := c.Conn.Read(b) +// if err == nil { +// c.incoming = append(c.incoming, b[:count]...) +// } +// return count, err +//} +// +//// Write prevents writing on the real connection +//func (c *handshakeReader) Write(b []byte) (int, error) { +// return 0, errors.New("cannot write on this connection") +//} +type censoredReader struct { + net.Conn + outgoing []byte +} + +// forward forwards left traffic to right +func forward(wg *sync.WaitGroup, left, right net.Conn) { + data := make([]byte, 1<<18) + for { + n, err := left.Read(data) + if err != nil { + break + } + if _, err = right.Write(data[:n]); err != nil { + break + } + } + wg.Done() +} + +// reset closes the connection with a RST segment +func reset(conn net.Conn) { + if tc, ok := conn.(*net.TCPConn); ok { + tc.SetLinger(0) + } + conn.Close() +} + +// alertclose sends a TLS alert and then closes the connection +func alertclose(conn net.Conn) { + alertdata := []byte{ + 21, // alert + 3, // version[0] + 3, // version[1] + 0, // length[0] + 2, // length[1] + 2, // fatal + 80, // internal error + } + conn.Write(alertdata) + conn.Close() +} + +// getsni attempts the handshakeReader hack to obtain the SNI by reading +// the beginning of the TLS handshake. On success a nonempty SNI string +// is returned. Otherwise we cannot distinguish between the absence of a +// SNI and any other reading network error that may have occurred. +func getsni(conn *handshakeReader) string { + var ( + sni string + mutex sync.Mutex // just for safety + ) + tls.Server(conn, &tls.Config{ + GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { + mutex.Lock() + sni = info.ServerName + mutex.Unlock() + return nil, errors.New("tlsproxy: we can't really continue handshake") + }, + }).Handshake() + return sni +} + +func (p *CensoringProxy) connectingToMyself(conn net.Conn) bool { + local := conn.LocalAddr().String() + localAddr, _, localErr := net.SplitHostPort(local) + remote := conn.RemoteAddr().String() + remoteAddr, _, remoteErr := net.SplitHostPort(remote) + return localErr != nil || remoteErr != nil || localAddr == remoteAddr +} + +// handle implements the TLS SNI proxy +func (p *CensoringProxy) handle(clientconn net.Conn, to string) { + lr := &ougGoingReader{Conn: clientconn } + line := readline(lr) + //hr := &handshakeReader{Conn: clientconn} + //sni := getsni(hr) +// if sni == "" { +// log.Warn("tlsproxy: network failure or SNI not provided") +// reset(clientconn) +// return +// } + // TODO + for _, pattern := range p.keywords { + if strings.Contains(line, pattern) { + log.Warnf("tlsproxy: reject SNI by policy: %s", sni) + alertclose(clientconn) + return + } + } + serverconn, err := p.dial("tcp", to) + if err != nil { + log.WithError(err).Warn("tlsproxy: p.dial failed") + alertclose(clientconn) + return + } + if p.connectingToMyself(serverconn) { + log.Warn("tlsproxy: connecting to myself") + alertclose(clientconn) + return + } + if _, err := serverconn.Write(hr.incoming); err != nil { + log.WithError(err).Warn("tlsproxy: serverconn.Write failed") + alertclose(clientconn) + return + } + log.Debugf("tlsproxy: routing for %s", sni) + defer clientconn.Close() + defer serverconn.Close() + var wg sync.WaitGroup + wg.Add(2) + go forward(&wg, clientconn, serverconn) + go forward(&wg, serverconn, clientconn) + wg.Wait() +} + +func (p *CensoringProxy) run(listener net.Listener, outboundPort string) { + for { + conn, err := listener.Accept() + if err != nil && strings.Contains( + err.Error(), "use of closed network connection") { + return + } + if err == nil { + // It's difficult to make accept fail, so restructure + // the code such that we enter into the happy path + go p.handle(conn, outboundPort) + } + } +} + +// Start starts the censoring proxy. +func (p *CensoringProxy) Start(address string, to string) (net.Listener, error) { + _, port, err := net.SplitHostPort(address) + if err != nil { + return nil, err + } + listener, err := net.Listen("tcp", address) + if err != nil { + return nil, err + } + go p.run(listener, to) + return listener, nil +} diff --git a/internal/cmd/jafar/tcpproxy/tlsproxy_test.go b/internal/cmd/jafar/tcpproxy/tlsproxy_test.go new file mode 100644 index 0000000..86a21d8 --- /dev/null +++ b/internal/cmd/jafar/tcpproxy/tlsproxy_test.go @@ -0,0 +1,181 @@ +package tlsproxy + +import ( + "crypto/tls" + "errors" + "net" + "sync" + "testing" + + "github.com/ooni/probe-cli/v3/internal/cmd/jafar/uncensored" +) + +func TestPass(t *testing.T) { + listener := newproxy(t, "ooni.io") + checkdialtls(t, listener.Addr().String(), true, &tls.Config{ + ServerName: "example.com", + }) + killproxy(t, listener) +} + +func TestBlock(t *testing.T) { + listener := newproxy(t, "ooni.io") + checkdialtls(t, listener.Addr().String(), false, &tls.Config{ + ServerName: "api.ooni.io", + }) + killproxy(t, listener) +} + +func TestNoSNI(t *testing.T) { + listener := newproxy(t, "ooni.io") + checkdialtls(t, listener.Addr().String(), false, &tls.Config{ + ServerName: "", + }) + killproxy(t, listener) +} + +func TestInvalidDomain(t *testing.T) { + listener := newproxy(t, "ooni.io") + checkdialtls(t, listener.Addr().String(), false, &tls.Config{ + ServerName: "antani.local", + }) + killproxy(t, listener) +} + +func TestFailHandshake(t *testing.T) { + listener := newproxy(t, "ooni.io") + checkdialtls(t, listener.Addr().String(), false, &tls.Config{ + ServerName: "expired.badssl.com", + }) + killproxy(t, listener) +} + +func TestFailConnectingToSelf(t *testing.T) { + proxy := &CensoringProxy{ + dial: func(network string, address string) (net.Conn, error) { + return &mockedConnWriteError{}, nil + }, + } + listener, err := proxy.Start("127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + if listener == nil { + t.Fatal("expected non nil listener here") + } + checkdialtls(t, listener.Addr().String(), false, &tls.Config{ + ServerName: "www.google.com", + }) + killproxy(t, listener) +} + +func TestFailWriteAfterConnect(t *testing.T) { + proxy := &CensoringProxy{ + dial: func(network string, address string) (net.Conn, error) { + return &mockedConnWriteError{ + // must be different or it refuses connecting to self + localIP: net.IPv4(127, 0, 0, 1), + remoteIP: net.IPv4(127, 0, 0, 2), + }, nil + }, + } + listener, err := proxy.Start("127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + if listener == nil { + t.Fatal("expected non nil listener here") + } + checkdialtls(t, listener.Addr().String(), false, &tls.Config{ + ServerName: "www.google.com", + }) + killproxy(t, listener) +} + +func TestListenError(t *testing.T) { + proxy := NewCensoringProxy( + []string{""}, uncensored.NewClient("https://1.1.1.1/dns-query"), + ) + listener, err := proxy.Start("8.8.8.8:80") + if err == nil { + t.Fatal("expected an error here") + } + if listener != nil { + t.Fatal("expected nil listener here") + } +} + +func newproxy(t *testing.T, blocked string) net.Listener { + proxy := NewCensoringProxy( + []string{blocked}, uncensored.NewClient("https://1.1.1.1/dns-query"), + ) + listener, err := proxy.Start("127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + return listener +} + +func killproxy(t *testing.T, listener net.Listener) { + err := listener.Close() + if err != nil { + t.Fatal(err) + } +} + +func checkdialtls( + t *testing.T, proxyAddr string, expectSuccess bool, config *tls.Config, +) { + conn, err := tls.Dial("tcp", proxyAddr, config) + if err != nil && expectSuccess { + t.Fatal(err) + } + if err == nil && !expectSuccess { + t.Fatal("expected failure here") + } + if conn == nil && expectSuccess { + t.Fatal("expected actionable conn") + } + if conn != nil && !expectSuccess { + t.Fatal("expected nil conn") + } + if conn != nil { + conn.Close() + } +} + +type mockedConnWriteError struct { + net.Conn + localIP net.IP + remoteIP net.IP +} + +func (c *mockedConnWriteError) Write(b []byte) (int, error) { + return 0, errors.New("cannot write sorry") +} + +func (c *mockedConnWriteError) LocalAddr() net.Addr { + return &net.TCPAddr{ + IP: c.localIP, + } +} + +func (c *mockedConnWriteError) RemoteAddr() net.Addr { + return &net.TCPAddr{ + IP: c.remoteIP, + } +} + +func TestForwardWriteError(t *testing.T) { + var wg sync.WaitGroup + wg.Add(1) + forward(&wg, &mockedConnReadOkay{}, &mockedConnWriteError{}) +} + +type mockedConnReadOkay struct { + net.Conn +} + +func (c *mockedConnReadOkay) Read(b []byte) (int, error) { + return len(b), nil +}