Skip to content

Commit f48a09d

Browse files
sfranken-smartwirelessmmatczuk
authored andcommitted
SNI vhost proxy implemented
1 parent 5491fa1 commit f48a09d

File tree

10 files changed

+143
-10
lines changed

10 files changed

+143
-10
lines changed

Gopkg.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,7 @@
4444
[[constraint]]
4545
branch = "master"
4646
name = "github.com/felixge/tcpkeepalive"
47+
48+
[[constraint]]
49+
branch = "master"
50+
name = "github.com/inconshreveable/go-vhost"

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Features:
66

77
* HTTP proxy with [basic authentication](https://en.wikipedia.org/wiki/Basic_access_authentication)
88
* TCP proxy
9+
* [SNI](https://en.wikipedia.org/wiki/Server_Name_Indication) vhost proxy
910
* Client auto reconnect
1011
* Client management and eviction
1112
* Easy to use CLI
@@ -149,6 +150,10 @@ looks like this
149150
proto: tcp
150151
addr: 192.168.0.5:22
151152
remote_addr: 0.0.0.0:22
153+
tls:
154+
proto: sni
155+
addr: localhost:443
156+
host: tls.my-tunnel-host.com
152157
```
153158
154159
Configuration options:
@@ -158,10 +163,10 @@ Configuration options:
158163
* `tls_key`: path to client TLS certificate key, *default:* `client.key` *in the config file directory*
159164
* `root_ca`: path to trusted root certificate authority pool file, if empty any server certificate is accepted
160165
* `tunnels / [name]`
161-
* `proto`: tunnel protocol, `http` or `tcp`
166+
* `proto`: tunnel protocol, `http`, `tcp` or `sni`
162167
* `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`
163168
* `auth`: (`proto=http`) (optional) basic authentication credentials to enforce on tunneled requests, format `user:password`
164-
* `host`: (`proto=http`) hostname to request (requires reserved name and DNS CNAME)
169+
* `host`: (`proto=http`, `proto=sni`) hostname to request (requires reserved name and DNS CNAME)
165170
* `remote_addr`: (`proto=tcp`) bind the remote TCP address
166171
* `backoff`
167172
* `interval`: how long client would wait before redialing the server if connection was lost, exponential backoff initial interval, *default:* `500ms`

cmd/tunnel/config.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ func loadClientConfigFromFile(file string) (*ClientConfig, error) {
8888
if err := validateTCP(t); err != nil {
8989
return nil, fmt.Errorf("%s %s", name, err)
9090
}
91+
case proto.SNI:
92+
if err := validateSNI(t); err != nil {
93+
return nil, fmt.Errorf("%s %s", name, err)
94+
}
9195
default:
9296
return nil, fmt.Errorf("%s invalid protocol %q", name, t.Protocol)
9397
}
@@ -140,3 +144,27 @@ func validateTCP(t *Tunnel) error {
140144

141145
return nil
142146
}
147+
148+
func validateSNI(t *Tunnel) error {
149+
var err error
150+
if t.Host == "" {
151+
return fmt.Errorf("host: missing")
152+
}
153+
if t.Addr == "" {
154+
return fmt.Errorf("addr: missing")
155+
}
156+
if t.Addr, err = normalizeAddress(t.Addr); err != nil {
157+
return fmt.Errorf("addr: %s", err)
158+
}
159+
160+
// unexpected
161+
162+
if t.RemoteAddr != "" {
163+
return fmt.Errorf("remote_addr: unexpected")
164+
}
165+
if t.Auth != "" {
166+
return fmt.Errorf("auth: unexpected")
167+
}
168+
169+
return nil
170+
}

cmd/tunnel/options.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ config.yaml:
3838
proto: tcp
3939
addr: 192.168.0.5:22
4040
remote_addr: 0.0.0.0:22
41+
tls:
42+
proto: sni
43+
addr: localhost:443
44+
host: tls.my-tunnel-host.com
4145
4246
Author:
4347
Written by M. Matczuk (mmatczuk@gmail.com)

cmd/tunnel/tunnel.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,8 @@ func proxy(m map[string]*Tunnel, logger log.Logger) tunnel.ProxyFunc {
182182
httpURL[t.Host] = u
183183
case proto.TCP, proto.TCP4, proto.TCP6:
184184
tcpAddr[t.RemoteAddr] = t.Addr
185+
case proto.SNI:
186+
tcpAddr[t.Host] = t.Addr
185187
}
186188
}
187189

cmd/tunneld/options.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ options:
1616

1717
const usage2 string = `
1818
Example:
19-
tuneld
20-
tuneld -clients YMBKT3V-ESUTZ2Z-7MRILIJ-T35FHGO-D2DHO7D-FXMGSSR-V4LBSZX-BNDONQ4
21-
tuneld -httpAddr :8080 -httpsAddr ""
19+
tunneld
20+
tunneld -clients YMBKT3V-ESUTZ2Z-7MRILIJ-T35FHGO-D2DHO7D-FXMGSSR-V4LBSZX-BNDONQ4
21+
tunneld -httpAddr :8080 -httpsAddr ""
22+
tunneld -httpsAddr "" -sniAddr ":443" -rootCA client_root.crt -tlsCrt server.crt -tlsKey server.key
2223
2324
Author:
2425
Written by M. Matczuk (mmatczuk@gmail.com)
@@ -40,6 +41,7 @@ type options struct {
4041
httpAddr string
4142
httpsAddr string
4243
tunnelAddr string
44+
sniAddr string
4345
tlsCrt string
4446
tlsKey string
4547
rootCA string
@@ -52,6 +54,7 @@ func parseArgs() *options {
5254
httpAddr := flag.String("httpAddr", ":80", "Public address for HTTP connections, empty string to disable")
5355
httpsAddr := flag.String("httpsAddr", ":443", "Public address listening for HTTPS connections, emptry string to disable")
5456
tunnelAddr := flag.String("tunnelAddr", ":5223", "Public address listening for tunnel client")
57+
sniAddr := flag.String("sniAddr", "", "Public address listening for TLS SNI connections, empty string to disable")
5558
tlsCrt := flag.String("tlsCrt", "server.crt", "Path to a TLS certificate file")
5659
tlsKey := flag.String("tlsKey", "server.key", "Path to a TLS key file")
5760
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 {
6467
httpAddr: *httpAddr,
6568
httpsAddr: *httpsAddr,
6669
tunnelAddr: *tunnelAddr,
70+
sniAddr: *sniAddr,
6771
tlsCrt: *tlsCrt,
6872
tlsKey: *tlsKey,
6973
rootCA: *rootCA,

cmd/tunneld/tunneld.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ func main() {
4242
// setup server
4343
server, err := tunnel.NewServer(&tunnel.ServerConfig{
4444
Addr: opts.tunnelAddr,
45+
SNIAddr: opts.sniAddr,
4546
AutoSubscribe: autoSubscribe,
4647
TLSConfig: tlsconf,
4748
Logger: logger,

proto/controlmsg.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const (
3232
TCP4 = "tcp4"
3333
TCP6 = "tcp6"
3434
UNIX = "unix"
35+
SNI = "sni"
3536
)
3637

3738
// ControlMessage is sent from server to client before streaming data. It's

server.go

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818

1919
"golang.org/x/net/http2"
2020

21+
"github.com/inconshreveable/go-vhost"
2122
"github.com/mmatczuk/go-http-tunnel/id"
2223
"github.com/mmatczuk/go-http-tunnel/log"
2324
"github.com/mmatczuk/go-http-tunnel/proto"
@@ -38,6 +39,8 @@ type ServerConfig struct {
3839
Listener net.Listener
3940
// Logger is optional logger. If nil logging is disabled.
4041
Logger log.Logger
42+
// Addr is TCP address to listen for TLS SNI connections
43+
SNIAddr string
4144
}
4245

4346
// Server is responsible for proxying public connections to the client over a
@@ -50,6 +53,7 @@ type Server struct {
5053
connPool *connPool
5154
httpClient *http.Client
5255
logger log.Logger
56+
vhostMuxer *vhost.TLSMuxer
5357
}
5458

5559
// NewServer creates a new Server.
@@ -82,6 +86,54 @@ func NewServer(config *ServerConfig) (*Server, error) {
8286
},
8387
}
8488

89+
if config.SNIAddr != "" {
90+
l, err := net.Listen("tcp", config.SNIAddr)
91+
if err != nil {
92+
return nil, err
93+
}
94+
mux, err := vhost.NewTLSMuxer(l, DefaultTimeout)
95+
if err != nil {
96+
return nil, fmt.Errorf("SNI Muxer creation failed: %s", err)
97+
}
98+
s.vhostMuxer = mux
99+
go func() {
100+
for {
101+
conn, err := mux.NextError()
102+
vhostName := ""
103+
tlsConn, ok := conn.(*vhost.TLSConn)
104+
if ok {
105+
vhostName = tlsConn.Host()
106+
}
107+
108+
switch err.(type) {
109+
case vhost.BadRequest:
110+
logger.Log(
111+
"level", 0,
112+
"action", "got a bad request!",
113+
"addr", conn.RemoteAddr(),
114+
)
115+
case vhost.NotFound:
116+
117+
logger.Log(
118+
"level", 0,
119+
"action", "got a connection for an unknown vhost",
120+
"addr", vhostName,
121+
)
122+
case vhost.Closed:
123+
logger.Log(
124+
"level", 0,
125+
"action", "closed conn",
126+
"addr", vhostName,
127+
)
128+
}
129+
130+
if conn != nil {
131+
conn.Close()
132+
}
133+
}
134+
}()
135+
}
136+
85137
return s, nil
86138
}
87139

@@ -386,6 +438,25 @@ func (s *Server) addTunnels(tunnels map[string]*proto.Tunnel, identifier id.ID)
386438
"addr", l.Addr(),
387439
)
388440

441+
i.Listeners = append(i.Listeners, l)
442+
case proto.SNI:
443+
if s.vhostMuxer == nil {
444+
err = fmt.Errorf("unable to configure SNI for tunnel %s: %s", name, t.Protocol)
445+
goto rollback
446+
}
447+
var l net.Listener
448+
l, err = s.vhostMuxer.Listen(t.Host)
449+
if err != nil {
450+
goto rollback
451+
}
452+
453+
s.logger.Log(
454+
"level", 2,
455+
"action", "add SNI vhost",
456+
"identifier", identifier,
457+
"host", t.Host,
458+
)
459+
389460
i.Listeners = append(i.Listeners, l)
390461
default:
391462
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) {
430501
for {
431502
conn, err := l.Accept()
432503
if err != nil {
433-
if strings.Contains(err.Error(), "use of closed network connection") {
504+
if strings.Contains(err.Error(), "use of closed network connection") ||
505+
strings.Contains(err.Error(), "Listener closed") {
434506
s.logger.Log(
435507
"level", 2,
436508
"action", "listener closed",
@@ -452,11 +524,20 @@ func (s *Server) listen(l net.Listener, identifier id.ID) {
452524

453525
msg := &proto.ControlMessage{
454526
Action: proto.ActionProxy,
455-
ForwardedHost: l.Addr().String(),
456527
ForwardedProto: l.Addr().Network(),
457528
}
458529

459-
if err := keepAlive(conn); err != nil {
530+
tlsConn, ok := conn.(*vhost.TLSConn)
531+
if ok {
532+
msg.ForwardedHost = tlsConn.Host()
533+
err = keepAlive(tlsConn.Conn)
534+
535+
} else {
536+
msg.ForwardedHost = l.Addr().String()
537+
err = keepAlive(conn)
538+
}
539+
540+
if err != nil {
460541
s.logger.Log(
461542
"level", 1,
462543
"msg", "TCP keepalive for tunneled connection failed",
@@ -603,7 +684,10 @@ func (s *Server) proxyConn(identifier id.ID, conn net.Conn, msg *proto.ControlMe
603684
"src", identifier,
604685
))
605686

606-
<-done
687+
select {
688+
case <-done:
689+
case <-time.After(DefaultTimeout):
690+
}
607691

608692
s.logger.Log(
609693
"level", 2,

tcpproxy.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func NewMultiTCPProxy(localAddrMap map[string]string, logger log.Logger) *TCPPro
5757
// Proxy is a ProxyFunc.
5858
func (p *TCPProxy) Proxy(w io.Writer, r io.ReadCloser, msg *proto.ControlMessage) {
5959
switch msg.ForwardedProto {
60-
case proto.TCP, proto.TCP4, proto.TCP6, proto.UNIX:
60+
case proto.TCP, proto.TCP4, proto.TCP6, proto.UNIX, proto.SNI:
6161
// ok
6262
default:
6363
p.logger.Log(

0 commit comments

Comments
 (0)