Start work on jafar
This commit is contained in:
parent
faf5c0748c
commit
07e76dcdaa
210
internal/cmd/jafar/tcpproxy/tcpproxy.go
Normal file
210
internal/cmd/jafar/tcpproxy/tcpproxy.go
Normal 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
|
||||
}
|
181
internal/cmd/jafar/tcpproxy/tlsproxy_test.go
Normal file
181
internal/cmd/jafar/tcpproxy/tlsproxy_test.go
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user