SNI vhost proxy implemented

This commit is contained in:
Sven Franken 2019-10-23 16:12:33 +02:00 committed by Michal Matczuk
parent 5491fa1a22
commit f48a09d2ab
10 changed files with 143 additions and 10 deletions

View file

@ -44,3 +44,7 @@
[[constraint]]
branch = "master"
name = "github.com/felixge/tcpkeepalive"
[[constraint]]
branch = "master"
name = "github.com/inconshreveable/go-vhost"

View file

@ -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`

View file

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

View file

@ -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)

View file

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

View file

@ -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,

View file

@ -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,

View file

@ -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

View file

@ -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,

View file

@ -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(