From 2547cb2b34d7fb817cbc29f371ec12dc6084ab92 Mon Sep 17 00:00:00 2001 From: Anders Pitman Date: Thu, 24 Feb 2022 11:38:57 -0700 Subject: [PATCH 1/4] Add tuntls command When used in conjunction with raw Client TLS termination, allows wrapping plain TCP in TLS with SNI routing. Supports both stdin/stdout (useful for things like ssh ProxyCommand) and listening on a local port and forwarding all connections to that port. --- cmd/boringproxy/main.go | 73 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/cmd/boringproxy/main.go b/cmd/boringproxy/main.go index 4e28b61..5331dc1 100644 --- a/cmd/boringproxy/main.go +++ b/cmd/boringproxy/main.go @@ -2,9 +2,13 @@ package main import ( "context" + "crypto/tls" "flag" "fmt" + "io" + "net" "os" + "sync" "github.com/boringproxy/boringproxy" ) @@ -15,6 +19,7 @@ Commands: version Prints version information. server Start a new server. client Connect to a server. + tuntls Tunnel a raw TLS connection. Use "%[1]s command -h" for a list of flags for the command. ` @@ -25,7 +30,6 @@ func fail(msg string) { fmt.Fprintln(os.Stderr, msg) os.Exit(1) } - func main() { if len(os.Args) < 2 { fmt.Fprintln(os.Stderr, os.Args[0]+": Need a command") @@ -40,6 +44,44 @@ func main() { fmt.Println(Version) case "help", "-h", "--help", "-help": fmt.Printf(usage, os.Args[0]) + case "tuntls": + // This command is a direct port of https://github.com/anderspitman/tuntls + flagSet := flag.NewFlagSet(os.Args[0], flag.ExitOnError) + server := flagSet.String("server", "", "boringproxy server") + port := flagSet.Int("port", 0, "Local port to bind to") + err := flagSet.Parse(os.Args[2:]) + if err != nil { + fmt.Fprintf(os.Stderr, "%s: parsing flags: %s\n", os.Args[0], err) + os.Exit(1) + } + + if *server == "" { + fmt.Fprintf(os.Stderr, "server argument is required\n") + os.Exit(1) + } + + if *port == 0 { + // one-time tunnel over stdin/stdout + doTlsTunnel(*server, os.Stdin, os.Stdout) + } else { + // listen on a port and create tunnels for each connection + fmt.Fprintf(os.Stderr, "Listening on port %d\n", *port) + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + + for { + conn, err := listener.Accept() + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + + go doTlsTunnel(*server, conn, conn) + } + } case "server": boringproxy.Listen() case "client": @@ -97,3 +139,32 @@ func main() { fail(os.Args[0] + ": Invalid command " + command) } } + +func doTlsTunnel(server string, in io.Reader, out io.Writer) { + fmt.Fprintf(os.Stderr, "tuntls connecting to server: %s\n", server) + + conn, err := tls.Dial("tcp", fmt.Sprintf("%s:443", server), &tls.Config{ + //RootCAs: roots, + }) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to connect: "+err.Error()) + os.Exit(1) + } + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + io.Copy(conn, in) + wg.Done() + }() + + go func() { + io.Copy(out, conn) + wg.Done() + }() + + wg.Wait() + + conn.Close() +} From 197e202d694b30d7901aa1aed9bf6ab0d8786a7d Mon Sep 17 00:00:00 2001 From: Anders Pitman Date: Thu, 24 Feb 2022 14:12:09 -0700 Subject: [PATCH 2/4] 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 { From dcb06497ad76635bddd52aa9cd135e1efb7afb2a Mon Sep 17 00:00:00 2001 From: Anders Pitman Date: Thu, 24 Feb 2022 14:33:13 -0700 Subject: [PATCH 3/4] Implement overriding SSH server per tunnel This lets you use a proxy for connecting to the SSH server, which is useful on networks that block SSH/port 22. For example you can use the boringproxy tuntls command to create a proxy that will tunnel the client's SSH connections over TLS to the server. It's all very meta and forces at least double encryption, but it could be useful. --- api.go | 18 ++++++++++++++++++ client.go | 1 + templates/edit_tunnel.tmpl | 9 +++++++++ tunnel_manager.go | 2 -- 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/api.go b/api.go index 8899370..c622527 100644 --- a/api.go +++ b/api.go @@ -378,6 +378,22 @@ func (a *Api) CreateTunnel(tokenData TokenData, params url.Values) (*Tunnel, err return nil, errors.New("Invalid tls-termination parameter") } + sshServerAddr := a.db.GetAdminDomain() + sshServerAddrParam := params.Get("ssh-server-addr") + if sshServerAddrParam != "" { + sshServerAddr = sshServerAddrParam + } + + sshServerPort := a.config.SshServerPort + sshServerPortParam := params.Get("ssh-server-port") + if sshServerPortParam != "" { + var err error + sshServerPort, err = strconv.Atoi(sshServerPortParam) + if err != nil { + return nil, errors.New("Invalid ssh-server-port parameter") + } + } + request := Tunnel{ Domain: domain, Owner: owner, @@ -389,6 +405,8 @@ func (a *Api) CreateTunnel(tokenData TokenData, params url.Values) (*Tunnel, err AuthUsername: username, AuthPassword: password, TlsTermination: tlsTerm, + ServerAddress: sshServerAddr, + ServerPort: sshServerPort, } tunnel, err := a.tunMan.RequestCreateTunnel(request) diff --git a/client.go b/client.go index 9cf2d45..dcdccb9 100644 --- a/client.go +++ b/client.go @@ -276,6 +276,7 @@ func (c *Client) BoreTunnel(ctx context.Context, tunnel Tunnel) error { } sshHost := fmt.Sprintf("%s:%d", tunnel.ServerAddress, tunnel.ServerPort) + fmt.Println(sshHost) client, err := ssh.Dial("tcp", sshHost, config) if err != nil { return errors.New(fmt.Sprintf("Failed to dial: ", err)) diff --git a/templates/edit_tunnel.tmpl b/templates/edit_tunnel.tmpl index 81a4adc..94366f2 100644 --- a/templates/edit_tunnel.tmpl +++ b/templates/edit_tunnel.tmpl @@ -59,6 +59,15 @@ +
+ + +
+
+ + +
+ diff --git a/tunnel_manager.go b/tunnel_manager.go index 7f994b1..0092a79 100644 --- a/tunnel_manager.go +++ b/tunnel_manager.go @@ -98,8 +98,6 @@ func (m *TunnelManager) RequestCreateTunnel(tunReq Tunnel) (Tunnel, error) { return Tunnel{}, err } - tunReq.ServerAddress = m.db.GetAdminDomain() - tunReq.ServerPort = m.config.SshServerPort tunReq.ServerPublicKey = "" tunReq.Username = m.user.Username tunReq.TunnelPrivateKey = privKey From da7396dc31624322d1f3567acc05fdf493e88ed7 Mon Sep 17 00:00:00 2001 From: Anders Pitman Date: Thu, 24 Feb 2022 14:51:28 -0700 Subject: [PATCH 4/4] Remove print --- client.go | 1 - 1 file changed, 1 deletion(-) diff --git a/client.go b/client.go index dcdccb9..9cf2d45 100644 --- a/client.go +++ b/client.go @@ -276,7 +276,6 @@ func (c *Client) BoreTunnel(ctx context.Context, tunnel Tunnel) error { } sshHost := fmt.Sprintf("%s:%d", tunnel.ServerAddress, tunnel.ServerPort) - fmt.Println(sshHost) client, err := ssh.Dial("tcp", sshHost, config) if err != nil { return errors.New(fmt.Sprintf("Failed to dial: ", err))