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