Start work on jafar

This commit is contained in:
ooni noob 2022-11-22 21:43:27 +01:00
parent faf5c0748c
commit 07e76dcdaa
2 changed files with 391 additions and 0 deletions

View File

@ -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
}

View File

@ -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
}