199 lines
5.2 KiB
Go
199 lines
5.2 KiB
Go
// 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
|
|
}
|