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