From f48a09d2ab15c6f4fcbeec67864d39a584f22b30 Mon Sep 17 00:00:00 2001 From: Sven Franken Date: Wed, 23 Oct 2019 16:12:33 +0200 Subject: [PATCH] SNI vhost proxy implemented --- Gopkg.toml | 4 ++ README.md | 9 ++++- cmd/tunnel/config.go | 28 +++++++++++++ cmd/tunnel/options.go | 4 ++ cmd/tunnel/tunnel.go | 2 + cmd/tunneld/options.go | 10 +++-- cmd/tunneld/tunneld.go | 1 + proto/controlmsg.go | 1 + server.go | 92 ++++++++++++++++++++++++++++++++++++++++-- tcpproxy.go | 2 +- 10 files changed, 143 insertions(+), 10 deletions(-) diff --git a/Gopkg.toml b/Gopkg.toml index ed87414..0c0b3d1 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -44,3 +44,7 @@ [[constraint]] branch = "master" name = "github.com/felixge/tcpkeepalive" + +[[constraint]] + branch = "master" + name = "github.com/inconshreveable/go-vhost" diff --git a/README.md b/README.md index 5d00bcc..6b3d042 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ Features: * HTTP proxy with [basic authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) * TCP proxy +* [SNI](https://en.wikipedia.org/wiki/Server_Name_Indication) vhost proxy * Client auto reconnect * Client management and eviction * Easy to use CLI @@ -149,6 +150,10 @@ looks like this proto: tcp addr: 192.168.0.5:22 remote_addr: 0.0.0.0:22 + tls: + proto: sni + addr: localhost:443 + host: tls.my-tunnel-host.com ``` Configuration options: @@ -158,10 +163,10 @@ Configuration options: * `tls_key`: path to client TLS certificate key, *default:* `client.key` *in the config file directory* * `root_ca`: path to trusted root certificate authority pool file, if empty any server certificate is accepted * `tunnels / [name]` - * `proto`: tunnel protocol, `http` or `tcp` + * `proto`: tunnel protocol, `http`, `tcp` or `sni` * `addr`: forward traffic to this local port number or network address, for `proto=http` this can be full URL i.e. `https://machine/sub/path/?plus=params`, supports URL schemes `http` and `https` * `auth`: (`proto=http`) (optional) basic authentication credentials to enforce on tunneled requests, format `user:password` - * `host`: (`proto=http`) hostname to request (requires reserved name and DNS CNAME) + * `host`: (`proto=http`, `proto=sni`) hostname to request (requires reserved name and DNS CNAME) * `remote_addr`: (`proto=tcp`) bind the remote TCP address * `backoff` * `interval`: how long client would wait before redialing the server if connection was lost, exponential backoff initial interval, *default:* `500ms` diff --git a/cmd/tunnel/config.go b/cmd/tunnel/config.go index da20ffd..896cd7f 100644 --- a/cmd/tunnel/config.go +++ b/cmd/tunnel/config.go @@ -88,6 +88,10 @@ func loadClientConfigFromFile(file string) (*ClientConfig, error) { if err := validateTCP(t); err != nil { return nil, fmt.Errorf("%s %s", name, err) } + case proto.SNI: + if err := validateSNI(t); err != nil { + return nil, fmt.Errorf("%s %s", name, err) + } default: return nil, fmt.Errorf("%s invalid protocol %q", name, t.Protocol) } @@ -140,3 +144,27 @@ func validateTCP(t *Tunnel) error { return nil } + +func validateSNI(t *Tunnel) error { + var err error + if t.Host == "" { + return fmt.Errorf("host: missing") + } + if t.Addr == "" { + return fmt.Errorf("addr: missing") + } + if t.Addr, err = normalizeAddress(t.Addr); err != nil { + return fmt.Errorf("addr: %s", err) + } + + // unexpected + + if t.RemoteAddr != "" { + return fmt.Errorf("remote_addr: unexpected") + } + if t.Auth != "" { + return fmt.Errorf("auth: unexpected") + } + + return nil +} diff --git a/cmd/tunnel/options.go b/cmd/tunnel/options.go index ef3176f..5b705ca 100644 --- a/cmd/tunnel/options.go +++ b/cmd/tunnel/options.go @@ -38,6 +38,10 @@ config.yaml: proto: tcp addr: 192.168.0.5:22 remote_addr: 0.0.0.0:22 + tls: + proto: sni + addr: localhost:443 + host: tls.my-tunnel-host.com Author: Written by M. Matczuk (mmatczuk@gmail.com) diff --git a/cmd/tunnel/tunnel.go b/cmd/tunnel/tunnel.go index 9bce9ea..73776a6 100644 --- a/cmd/tunnel/tunnel.go +++ b/cmd/tunnel/tunnel.go @@ -182,6 +182,8 @@ func proxy(m map[string]*Tunnel, logger log.Logger) tunnel.ProxyFunc { httpURL[t.Host] = u case proto.TCP, proto.TCP4, proto.TCP6: tcpAddr[t.RemoteAddr] = t.Addr + case proto.SNI: + tcpAddr[t.Host] = t.Addr } } diff --git a/cmd/tunneld/options.go b/cmd/tunneld/options.go index 06994dc..08c09e0 100644 --- a/cmd/tunneld/options.go +++ b/cmd/tunneld/options.go @@ -16,9 +16,10 @@ options: const usage2 string = ` Example: - tuneld - tuneld -clients YMBKT3V-ESUTZ2Z-7MRILIJ-T35FHGO-D2DHO7D-FXMGSSR-V4LBSZX-BNDONQ4 - tuneld -httpAddr :8080 -httpsAddr "" + tunneld + tunneld -clients YMBKT3V-ESUTZ2Z-7MRILIJ-T35FHGO-D2DHO7D-FXMGSSR-V4LBSZX-BNDONQ4 + tunneld -httpAddr :8080 -httpsAddr "" + tunneld -httpsAddr "" -sniAddr ":443" -rootCA client_root.crt -tlsCrt server.crt -tlsKey server.key Author: Written by M. Matczuk (mmatczuk@gmail.com) @@ -40,6 +41,7 @@ type options struct { httpAddr string httpsAddr string tunnelAddr string + sniAddr string tlsCrt string tlsKey string rootCA string @@ -52,6 +54,7 @@ func parseArgs() *options { httpAddr := flag.String("httpAddr", ":80", "Public address for HTTP connections, empty string to disable") httpsAddr := flag.String("httpsAddr", ":443", "Public address listening for HTTPS connections, emptry string to disable") tunnelAddr := flag.String("tunnelAddr", ":5223", "Public address listening for tunnel client") + sniAddr := flag.String("sniAddr", "", "Public address listening for TLS SNI connections, empty string to disable") tlsCrt := flag.String("tlsCrt", "server.crt", "Path to a TLS certificate file") tlsKey := flag.String("tlsKey", "server.key", "Path to a TLS key file") rootCA := flag.String("rootCA", "", "Path to the trusted certificate chian used for client certificate authentication, if empty any client certificate is accepted") @@ -64,6 +67,7 @@ func parseArgs() *options { httpAddr: *httpAddr, httpsAddr: *httpsAddr, tunnelAddr: *tunnelAddr, + sniAddr: *sniAddr, tlsCrt: *tlsCrt, tlsKey: *tlsKey, rootCA: *rootCA, diff --git a/cmd/tunneld/tunneld.go b/cmd/tunneld/tunneld.go index 1c541ca..2418113 100644 --- a/cmd/tunneld/tunneld.go +++ b/cmd/tunneld/tunneld.go @@ -42,6 +42,7 @@ func main() { // setup server server, err := tunnel.NewServer(&tunnel.ServerConfig{ Addr: opts.tunnelAddr, + SNIAddr: opts.sniAddr, AutoSubscribe: autoSubscribe, TLSConfig: tlsconf, Logger: logger, diff --git a/proto/controlmsg.go b/proto/controlmsg.go index 2dddc93..5430712 100644 --- a/proto/controlmsg.go +++ b/proto/controlmsg.go @@ -32,6 +32,7 @@ const ( TCP4 = "tcp4" TCP6 = "tcp6" UNIX = "unix" + SNI = "sni" ) // ControlMessage is sent from server to client before streaming data. It's diff --git a/server.go b/server.go index b3f7bd6..e999b2a 100644 --- a/server.go +++ b/server.go @@ -18,6 +18,7 @@ import ( "golang.org/x/net/http2" + "github.com/inconshreveable/go-vhost" "github.com/mmatczuk/go-http-tunnel/id" "github.com/mmatczuk/go-http-tunnel/log" "github.com/mmatczuk/go-http-tunnel/proto" @@ -38,6 +39,8 @@ type ServerConfig struct { Listener net.Listener // Logger is optional logger. If nil logging is disabled. Logger log.Logger + // Addr is TCP address to listen for TLS SNI connections + SNIAddr string } // Server is responsible for proxying public connections to the client over a @@ -50,6 +53,7 @@ type Server struct { connPool *connPool httpClient *http.Client logger log.Logger + vhostMuxer *vhost.TLSMuxer } // NewServer creates a new Server. @@ -82,6 +86,54 @@ func NewServer(config *ServerConfig) (*Server, error) { }, } + if config.SNIAddr != "" { + l, err := net.Listen("tcp", config.SNIAddr) + if err != nil { + return nil, err + } + mux, err := vhost.NewTLSMuxer(l, DefaultTimeout) + if err != nil { + return nil, fmt.Errorf("SNI Muxer creation failed: %s", err) + } + s.vhostMuxer = mux + go func() { + for { + conn, err := mux.NextError() + vhostName := "" + tlsConn, ok := conn.(*vhost.TLSConn) + if ok { + vhostName = tlsConn.Host() + } + + switch err.(type) { + case vhost.BadRequest: + logger.Log( + "level", 0, + "action", "got a bad request!", + "addr", conn.RemoteAddr(), + ) + case vhost.NotFound: + + logger.Log( + "level", 0, + "action", "got a connection for an unknown vhost", + "addr", vhostName, + ) + case vhost.Closed: + logger.Log( + "level", 0, + "action", "closed conn", + "addr", vhostName, + ) + } + + if conn != nil { + conn.Close() + } + } + }() + } + return s, nil } @@ -386,6 +438,25 @@ func (s *Server) addTunnels(tunnels map[string]*proto.Tunnel, identifier id.ID) "addr", l.Addr(), ) + i.Listeners = append(i.Listeners, l) + case proto.SNI: + if s.vhostMuxer == nil { + err = fmt.Errorf("unable to configure SNI for tunnel %s: %s", name, t.Protocol) + goto rollback + } + var l net.Listener + l, err = s.vhostMuxer.Listen(t.Host) + if err != nil { + goto rollback + } + + s.logger.Log( + "level", 2, + "action", "add SNI vhost", + "identifier", identifier, + "host", t.Host, + ) + i.Listeners = append(i.Listeners, l) default: err = fmt.Errorf("unsupported protocol for tunnel %s: %s", name, t.Protocol) @@ -430,7 +501,8 @@ func (s *Server) listen(l net.Listener, identifier id.ID) { for { conn, err := l.Accept() if err != nil { - if strings.Contains(err.Error(), "use of closed network connection") { + if strings.Contains(err.Error(), "use of closed network connection") || + strings.Contains(err.Error(), "Listener closed") { s.logger.Log( "level", 2, "action", "listener closed", @@ -452,11 +524,20 @@ func (s *Server) listen(l net.Listener, identifier id.ID) { msg := &proto.ControlMessage{ Action: proto.ActionProxy, - ForwardedHost: l.Addr().String(), ForwardedProto: l.Addr().Network(), } - if err := keepAlive(conn); err != nil { + tlsConn, ok := conn.(*vhost.TLSConn) + if ok { + msg.ForwardedHost = tlsConn.Host() + err = keepAlive(tlsConn.Conn) + + } else { + msg.ForwardedHost = l.Addr().String() + err = keepAlive(conn) + } + + if err != nil { s.logger.Log( "level", 1, "msg", "TCP keepalive for tunneled connection failed", @@ -603,7 +684,10 @@ func (s *Server) proxyConn(identifier id.ID, conn net.Conn, msg *proto.ControlMe "src", identifier, )) - <-done + select { + case <-done: + case <-time.After(DefaultTimeout): + } s.logger.Log( "level", 2, diff --git a/tcpproxy.go b/tcpproxy.go index 613de9b..0440dfd 100644 --- a/tcpproxy.go +++ b/tcpproxy.go @@ -57,7 +57,7 @@ func NewMultiTCPProxy(localAddrMap map[string]string, logger log.Logger) *TCPPro // Proxy is a ProxyFunc. func (p *TCPProxy) Proxy(w io.Writer, r io.ReadCloser, msg *proto.ControlMessage) { switch msg.ForwardedProto { - case proto.TCP, proto.TCP4, proto.TCP6, proto.UNIX: + case proto.TCP, proto.TCP4, proto.TCP6, proto.UNIX, proto.SNI: // ok default: p.logger.Log(