Begin implementing TLS passthrough

Basically working, but still needs:

* UI for selecting TLS passthrough
* Client Let's Encrypt integration for automatically getting certs.
* More testing. The changes were pretty invasive.
This commit is contained in:
Anders Pitman 2020-11-26 22:37:51 -07:00
parent 05aeec0e9b
commit 14a666481a
3 changed files with 175 additions and 9 deletions

View File

@ -10,9 +10,11 @@ import (
"io"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"strings"
"sync"
"time"
)
@ -29,9 +31,10 @@ type SmtpConfig struct {
}
type BoringProxy struct {
db *Database
tunMan *TunnelManager
httpClient *http.Client
db *Database
tunMan *TunnelManager
httpClient *http.Client
httpListener *PassthroughListener
}
func Listen() {
@ -98,16 +101,15 @@ func Listen() {
},
}
p := &BoringProxy{db, tunMan, httpClient}
httpListener := NewPassthroughListener()
p := &BoringProxy{db, tunMan, httpClient, httpListener}
tlsConfig := &tls.Config{
GetCertificate: certConfig.GetCertificate,
NextProtos: []string{"h2", "acme-tls/1"},
}
tlsListener, err := tls.Listen("tcp", ":443", tlsConfig)
if err != nil {
log.Fatal(err)
}
tlsListener := tls.NewListener(httpListener, tlsConfig)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
timestamp := time.Now().Format(time.RFC3339)
@ -131,7 +133,69 @@ func Listen() {
}
}()
http.Serve(tlsListener, nil)
go http.Serve(tlsListener, nil)
listener, err := net.Listen("tcp", ":443")
if err != nil {
log.Fatal(err)
}
for {
conn, err := listener.Accept()
if err != nil {
log.Print(err)
continue
}
go p.handleConnection(conn)
}
}
func (p *BoringProxy) handleConnection(clientConn net.Conn) {
clientHello, clientReader, err := peekClientHello(clientConn)
if err != nil {
log.Println("peekClientHello error", err)
return
}
passConn := NewProxyConn(clientConn, clientReader)
tunnel, exists := p.db.GetTunnel(clientHello.ServerName)
if exists && tunnel.TlsPassthrough {
p.passthroughRequest(passConn, tunnel)
} else {
p.httpListener.PassConn(passConn)
}
}
func (p *BoringProxy) passthroughRequest(conn net.Conn, tunnel Tunnel) {
upstreamAddr := fmt.Sprintf("localhost:%d", tunnel.TunnelPort)
upstreamConn, err := net.Dial("tcp", upstreamAddr)
if err != nil {
log.Print(err)
return
}
defer upstreamConn.Close()
var wg sync.WaitGroup
wg.Add(2)
go func() {
io.Copy(conn, upstreamConn)
conn.(*ProxyConn).CloseWrite()
wg.Done()
}()
go func() {
io.Copy(upstreamConn, conn)
upstreamConn.(*net.TCPConn).CloseWrite()
wg.Done()
}()
wg.Wait()
}
func (p *BoringProxy) proxyRequest(w http.ResponseWriter, r *http.Request) {

View File

@ -50,6 +50,7 @@ type Tunnel struct {
AuthUsername string `json:"auth_username"`
AuthPassword string `json:"auth_password"`
CssId string `json:"css_id"`
TlsPassthrough bool `json:"tls_passthrough"`
}
func NewDatabase() (*Database, error) {

101
sni.go Normal file
View File

@ -0,0 +1,101 @@
// NOTE: The code in this file was mostly copied from this very helpful
// article:
// https://www.agwa.name/blog/post/writing_an_sni_proxy_in_go
package main
import (
"bytes"
"crypto/tls"
"io"
"net"
"time"
)
type readOnlyConn struct {
reader io.Reader
}
func (conn readOnlyConn) Read(p []byte) (int, error) { return conn.reader.Read(p) }
func (conn readOnlyConn) Write(p []byte) (int, error) { return 0, io.ErrClosedPipe }
func (conn readOnlyConn) Close() error { return nil }
func (conn readOnlyConn) LocalAddr() net.Addr { return nil }
func (conn readOnlyConn) RemoteAddr() net.Addr { return nil }
func (conn readOnlyConn) SetDeadline(t time.Time) error { return nil }
func (conn readOnlyConn) SetReadDeadline(t time.Time) error { return nil }
func (conn readOnlyConn) SetWriteDeadline(t time.Time) error { return nil }
func peekClientHello(reader io.Reader) (*tls.ClientHelloInfo, io.Reader, error) {
peekedBytes := new(bytes.Buffer)
hello, err := readClientHello(io.TeeReader(reader, peekedBytes))
if err != nil {
return nil, nil, err
}
return hello, io.MultiReader(peekedBytes, reader), nil
}
func readClientHello(reader io.Reader) (*tls.ClientHelloInfo, error) {
var hello *tls.ClientHelloInfo
err := tls.Server(readOnlyConn{reader: reader}, &tls.Config{
GetConfigForClient: func(argHello *tls.ClientHelloInfo) (*tls.Config, error) {
hello = new(tls.ClientHelloInfo)
*hello = *argHello
return nil, nil
},
}).Handshake()
if hello == nil {
return nil, err
}
return hello, nil
}
type PassthroughListener struct {
ch chan net.Conn
}
func NewPassthroughListener() *PassthroughListener {
return &PassthroughListener{
ch: make(chan net.Conn),
}
}
func (f *PassthroughListener) Accept() (net.Conn, error) {
return <-f.ch, nil
}
func (f *PassthroughListener) Close() error {
return nil
}
func (f *PassthroughListener) Addr() net.Addr {
return nil
}
func (f *PassthroughListener) PassConn(conn net.Conn) {
f.ch <- conn
}
// This type creates a new net.Conn that's the same as an old one, except a new
// reader is provided. So it proxies every method except Read. I'm sure there's
// a cleaner way to do this...
type ProxyConn struct {
conn net.Conn
reader io.Reader
}
func NewProxyConn(conn net.Conn, reader io.Reader) *ProxyConn {
return &ProxyConn{
conn,
reader,
}
}
func (c ProxyConn) CloseWrite() error { return c.conn.(*net.TCPConn).CloseWrite() }
func (c ProxyConn) Read(p []byte) (int, error) { return c.reader.Read(p) }
func (c ProxyConn) Write(p []byte) (int, error) { return c.conn.Write(p) }
// TODO: is this safe? Will it actually close properly?
func (c ProxyConn) Close() error { return c.conn.Close() }
func (c ProxyConn) LocalAddr() net.Addr { return c.conn.LocalAddr() }
func (c ProxyConn) RemoteAddr() net.Addr { return c.conn.RemoteAddr() }
func (c ProxyConn) SetDeadline(t time.Time) error { return c.conn.SetDeadline(t) }
func (c ProxyConn) SetReadDeadline(t time.Time) error { return c.conn.SetReadDeadline(t) }
func (c ProxyConn) SetWriteDeadline(t time.Time) error { return c.conn.SetWriteDeadline(t) }