mirror of
https://github.com/mmatczuk/go-http-tunnel.git
synced 2026-05-15 06:06:03 -06:00
SNI vhost proxy implemented
This commit is contained in:
parent
5491fa1a22
commit
f48a09d2ab
10 changed files with 143 additions and 10 deletions
|
|
@ -44,3 +44,7 @@
|
|||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/felixge/tcpkeepalive"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/inconshreveable/go-vhost"
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
92
server.go
92
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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue