From 197e202d694b30d7901aa1aed9bf6ab0d8786a7d Mon Sep 17 00:00:00 2001 From: Anders Pitman Date: Thu, 24 Feb 2022 14:12:09 -0700 Subject: [PATCH] Implement raw server TLS tunnels Also cleaned up things a bit by moving the ProxyTcp logic into a separate file and sharing it between the client and server. --- api.go | 2 +- boringproxy.go | 11 +++- client.go | 98 +++------------------------------ templates/edit_tunnel.tmpl | 1 + tls_proxy.go | 108 +++++++++++++++++++++++++++++++++++++ tunnel_manager.go | 4 +- 6 files changed, 127 insertions(+), 97 deletions(-) create mode 100644 tls_proxy.go diff --git a/api.go b/api.go index d733fc0..8899370 100644 --- a/api.go +++ b/api.go @@ -374,7 +374,7 @@ func (a *Api) CreateTunnel(tokenData TokenData, params url.Values) (*Tunnel, err } tlsTerm := params.Get("tls-termination") - if tlsTerm != "server" && tlsTerm != "client" && tlsTerm != "passthrough" && tlsTerm != "client-tls" { + if tlsTerm != "server" && tlsTerm != "client" && tlsTerm != "passthrough" && tlsTerm != "client-tls" && tlsTerm != "server-tls" { return nil, errors.New("Invalid tls-termination parameter") } diff --git a/boringproxy.go b/boringproxy.go index 7bcf11c..7b77269 100644 --- a/boringproxy.go +++ b/boringproxy.go @@ -340,11 +340,11 @@ func Listen() { continue } - go p.handleConnection(conn) + go p.handleConnection(conn, certConfig) } } -func (p *Server) handleConnection(clientConn net.Conn) { +func (p *Server) handleConnection(clientConn net.Conn, certConfig *certmagic.Config) { clientHello, clientReader, err := peekClientHello(clientConn) if err != nil { @@ -358,6 +358,13 @@ func (p *Server) handleConnection(clientConn net.Conn) { if exists && (tunnel.TlsTermination == "client" || tunnel.TlsTermination == "passthrough") || tunnel.TlsTermination == "client-tls" { p.passthroughRequest(passConn, tunnel) + } else if exists && tunnel.TlsTermination == "server-tls" { + useTls := true + err := ProxyTcp(passConn, "127.0.0.1", tunnel.TunnelPort, useTls, certConfig) + if err != nil { + log.Println(err.Error()) + return + } } else { p.httpListener.PassConn(passConn) } diff --git a/client.go b/client.go index 2976cf2..9cf2d45 100644 --- a/client.go +++ b/client.go @@ -6,12 +6,10 @@ import ( "encoding/json" "errors" "fmt" - "io" "io/ioutil" "log" "net" "net/http" - "strings" "sync" "time" @@ -321,18 +319,6 @@ func (c *Client) BoreTunnel(ctx context.Context, tunnel Tunnel) error { } else { - if tunnel.TlsTermination == "client-tls" { - tlsConfig := &tls.Config{ - GetCertificate: c.certConfig.GetCertificate, - } - - tlsConfig.NextProtos = append([]string{"http/1.1", "h2", "acme-tls/1"}, tlsConfig.NextProtos...) - - tlsListener := tls.NewListener(listener, tlsConfig) - - listener = tlsListener - } - go func() { for { conn, err := listener.Accept() @@ -346,17 +332,14 @@ func (c *Client) BoreTunnel(ctx context.Context, tunnel Tunnel) error { //continue } - // If ALPN type is acme-tls/1, certmagic will do its thing under the hood, and the - // connection should not be used. - if tlsConn, ok := conn.(*tls.Conn); ok { - tlsConn.Handshake() - if tlsConn.ConnectionState().NegotiatedProtocol == "acme-tls/1" { - tlsConn.Close() - continue - } + var useTls bool + if tunnel.TlsTermination == "client-tls" { + useTls = true + } else { + useTls = false } - go c.handleConnection(conn, tunnel.ClientAddress, tunnel.ClientPort) + go ProxyTcp(conn, tunnel.ClientAddress, tunnel.ClientPort, useTls, c.certConfig) } }() } @@ -376,75 +359,6 @@ func (c *Client) BoreTunnel(ctx context.Context, tunnel Tunnel) error { return nil } -func (c *Client) handleConnection(conn net.Conn, upstreamAddr string, port int) { - - defer conn.Close() - - useTls := false - addr := upstreamAddr - - if strings.HasPrefix(upstreamAddr, "https://") { - addr = upstreamAddr[len("https://"):] - useTls = true - } - - var upstreamConn net.Conn - var err error - - if useTls { - tlsConfig := &tls.Config{ - InsecureSkipVerify: true, - } - upstreamConn, err = tls.Dial("tcp", fmt.Sprintf("%s:%d", addr, port), tlsConfig) - } else { - upstreamConn, err = net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) - } - - if err != nil { - log.Print(err) - return - } - - defer upstreamConn.Close() - - var wg sync.WaitGroup - wg.Add(2) - - // Copy request to upstream - go func() { - _, err := io.Copy(upstreamConn, conn) - if err != nil { - log.Println(err.Error()) - } - - if c, ok := upstreamConn.(*net.TCPConn); ok { - c.CloseWrite() - } else if c, ok := upstreamConn.(*tls.Conn); ok { - c.CloseWrite() - } - - wg.Done() - }() - - // Copy response to downstream - go func() { - _, err := io.Copy(conn, upstreamConn) - //conn.(*net.TCPConn).CloseWrite() - if err != nil { - log.Println(err.Error()) - } - // TODO: I added this to fix a bug where the copy to - // upstreamConn was never closing, even though the copy to - // conn was. It seems related to persistent connections going - // idle and upstream closing the connection. I'm a bit worried - // this might not be thread safe. - conn.Close() - wg.Done() - }() - - wg.Wait() -} - func printJson(data interface{}) { d, _ := json.MarshalIndent(data, "", " ") fmt.Println(string(d)) diff --git a/templates/edit_tunnel.tmpl b/templates/edit_tunnel.tmpl index 30c3684..81a4adc 100644 --- a/templates/edit_tunnel.tmpl +++ b/templates/edit_tunnel.tmpl @@ -39,6 +39,7 @@ + diff --git a/tls_proxy.go b/tls_proxy.go new file mode 100644 index 0000000..dbc6476 --- /dev/null +++ b/tls_proxy.go @@ -0,0 +1,108 @@ +package boringproxy + +import ( + //"errors" + "crypto/tls" + "fmt" + "io" + "log" + "net" + "strings" + "sync" + + "github.com/caddyserver/certmagic" +) + +func ProxyTcp(conn net.Conn, addr string, port int, useTls bool, certConfig *certmagic.Config) error { + + if useTls { + tlsConfig := &tls.Config{ + GetCertificate: certConfig.GetCertificate, + } + + tlsConfig.NextProtos = append([]string{"http/1.1", "h2", "acme-tls/1"}, tlsConfig.NextProtos...) + + tlsConn := tls.Server(conn, tlsConfig) + + tlsConn.Handshake() + if tlsConn.ConnectionState().NegotiatedProtocol == "acme-tls/1" { + tlsConn.Close() + return nil + } + + go handleConnection(tlsConn, addr, port) + } else { + go handleConnection(conn, addr, port) + } + + return nil +} + +func handleConnection(conn net.Conn, upstreamAddr string, port int) { + + defer conn.Close() + + useTls := false + addr := upstreamAddr + + if strings.HasPrefix(upstreamAddr, "https://") { + addr = upstreamAddr[len("https://"):] + useTls = true + } + + var upstreamConn net.Conn + var err error + + if useTls { + tlsConfig := &tls.Config{ + InsecureSkipVerify: true, + } + upstreamConn, err = tls.Dial("tcp", fmt.Sprintf("%s:%d", addr, port), tlsConfig) + } else { + upstreamConn, err = net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port)) + } + + if err != nil { + log.Print(err) + return + } + + defer upstreamConn.Close() + + var wg sync.WaitGroup + wg.Add(2) + + // Copy request to upstream + go func() { + _, err := io.Copy(upstreamConn, conn) + if err != nil { + log.Println(err.Error()) + } + + if c, ok := upstreamConn.(*net.TCPConn); ok { + c.CloseWrite() + } else if c, ok := upstreamConn.(*tls.Conn); ok { + c.CloseWrite() + } + + wg.Done() + }() + + // Copy response to downstream + go func() { + _, err := io.Copy(conn, upstreamConn) + //conn.(*net.TCPConn).CloseWrite() + if err != nil { + log.Println(err.Error()) + } + // TODO: I added this to fix a bug where the copy to + // upstreamConn was never closing, even though the copy to + // conn was. It seems related to persistent connections going + // idle and upstream closing the connection. I'm a bit worried + // this might not be thread safe. + conn.Close() + wg.Done() + }() + + wg.Wait() +} diff --git a/tunnel_manager.go b/tunnel_manager.go index 11efec7..7f994b1 100644 --- a/tunnel_manager.go +++ b/tunnel_manager.go @@ -35,7 +35,7 @@ func NewTunnelManager(config *Config, db *Database, certConfig *certmagic.Config if config.autoCerts { for domainName, tun := range db.GetTunnels() { - if tun.TlsTermination == "server" { + if tun.TlsTermination == "server" || tun.TlsTermination == "server-tls" { err = certConfig.ManageSync(context.Background(), []string{domainName}) if err != nil { log.Println("CertMagic error at startup") @@ -63,7 +63,7 @@ func (m *TunnelManager) RequestCreateTunnel(tunReq Tunnel) (Tunnel, error) { return Tunnel{}, errors.New("Owner required") } - if tunReq.TlsTermination == "server" { + if tunReq.TlsTermination == "server" || tunReq.TlsTermination == "server-tls" { if m.config.autoCerts { err := m.certConfig.ManageSync(context.Background(), []string{tunReq.Domain}) if err != nil {