ooni-probe-cli/internal/cmd/jafar/tlsproxy/tlsproxy.go

199 lines
5.2 KiB
Go
Raw Permalink Normal View History

// 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, uncensored Dialer,
) *CensoringProxy {
return &CensoringProxy{
keywords: keywords,
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")
}
// 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) {
hr := &handshakeReader{Conn: clientconn}
sni := getsni(hr)
if sni == "" {
log.Warn("tlsproxy: network failure or SNI not provided")
reset(clientconn)
return
}
for _, pattern := range p.keywords {
if strings.Contains(sni, pattern) {
log.Warnf("tlsproxy: reject SNI by policy: %s", sni)
alertclose(clientconn)
return
}
}
serverconn, err := p.dial("tcp", net.JoinHostPort(sni, "443"))
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) {
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)
}
}
}
// Start starts the censoring proxy.
func (p *CensoringProxy) Start(address string) (net.Listener, error) {
listener, err := net.Listen("tcp", address)
if err != nil {
return nil, err
}
go p.run(listener)
return listener, nil
}