Skip to content

Commit

Permalink
Issue #1: Add TCP proxy with SNI support
Browse files Browse the repository at this point in the history
This patch adds support for configuring a listener
to provide a passthrough TCP proxy for TLS connections
and use the SNI server extension in the ClientHello
message for routing. Targets need to register their
prefix as 'host/' for now.

This implementation is functional but not production
ready since it does not yet set proper timeouts on
connections. This will be fixed before the change
is merged.
  • Loading branch information
magiconair committed Aug 21, 2016
1 parent e1fba67 commit 8a7b2e9
Show file tree
Hide file tree
Showing 13 changed files with 597 additions and 51 deletions.
2 changes: 1 addition & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ type CertSource struct {

type Listen struct {
Addr string
Scheme string
Proto string
ReadTimeout time.Duration
WriteTimeout time.Duration
CertSource CertSource
Expand Down
25 changes: 20 additions & 5 deletions config/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,13 +211,19 @@ func parseListen(cfg string, cs map[string]CertSource, readTimeout, writeTimeout

l = Listen{
Addr: opts[0],
Scheme: "http",
Proto: "http",
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
}

var csName string
for k, v := range kvParse(cfg) {
switch k {
case "proto":
l.Proto = v
if l.Proto != "http" && l.Proto != "https" && l.Proto != "tcp+sni" {
return Listen{}, fmt.Errorf("unknown protocol %q", v)
}
case "rt": // read timeout
d, err := time.ParseDuration(v)
if err != nil {
Expand All @@ -231,14 +237,23 @@ func parseListen(cfg string, cs map[string]CertSource, readTimeout, writeTimeout
}
l.WriteTimeout = d
case "cs": // cert source
csName = v
c, ok := cs[v]
if !ok {
return Listen{}, fmt.Errorf("unknown certificate source %s", v)
return Listen{}, fmt.Errorf("unknown certificate source %q", v)
}
l.CertSource = c
l.Scheme = "https"
l.Proto = "https"
}
}

if csName != "" && l.Proto != "https" {
return Listen{}, fmt.Errorf("cert source requires proto 'https'")
}
if csName == "" && l.Proto == "https" {
return Listen{}, fmt.Errorf("proto 'https' requires cert source")
}

return
}

Expand All @@ -247,13 +262,13 @@ func parseLegacyListen(cfg string, readTimeout, writeTimeout time.Duration) (l L

l = Listen{
Addr: opts[0],
Scheme: "http",
Proto: "http",
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
}

if len(opts) > 1 {
l.Scheme = "https"
l.Proto = "https"
l.CertSource.Type = "file"
l.CertSource.CertPath = opts[1]
}
Expand Down
68 changes: 58 additions & 10 deletions config/load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
func TestFromProperties(t *testing.T) {
in := `
proxy.cs = cs=name;type=path;cert=foo;clientca=bar;refresh=99s;hdr=a: b;caupgcn=furb
proxy.addr = :1234
proxy.addr = :1234;proto=tcp+sni
proxy.localip = 4.4.4.4
proxy.strategy = rr
proxy.matcher = prefix
Expand Down Expand Up @@ -55,7 +55,7 @@ ui.title = fabfab
aws.apigw.cert.cn = furb
`
out := &Config{
ListenerValue: []string{":1234"},
ListenerValue: []string{":1234;proto=tcp+sni"},
CertSourcesValue: []map[string]string{{"cs": "name", "type": "path", "cert": "foo", "clientca": "bar", "refresh": "99s", "hdr": "a: b", "caupgcn": "furb"}},
CertSources: map[string]CertSource{
"name": CertSource{
Expand Down Expand Up @@ -111,7 +111,7 @@ aws.apigw.cert.cn = furb
Listen: []Listen{
{
Addr: ":1234",
Scheme: "http",
Proto: "tcp+sni",
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
},
Expand Down Expand Up @@ -171,7 +171,7 @@ func TestParseScheme(t *testing.T) {

func TestParseListen(t *testing.T) {
cs := map[string]CertSource{
"name": CertSource{Type: "foo"},
"name": CertSource{Name: "name", Type: "foo"},
}

tests := []struct {
Expand All @@ -186,19 +186,29 @@ func TestParseListen(t *testing.T) {
},
{
":123",
Listen{Addr: ":123", Scheme: "http"},
Listen{Addr: ":123", Proto: "http"},
"",
},
{
":123;proto=http",
Listen{Addr: ":123", Proto: "http"},
"",
},
{
":123;proto=tcp+sni",
Listen{Addr: ":123", Proto: "tcp+sni"},
"",
},
{
":123;rt=5s;wt=5s",
Listen{Addr: ":123", Scheme: "http", ReadTimeout: 5 * time.Second, WriteTimeout: 5 * time.Second},
Listen{Addr: ":123", Proto: "http", ReadTimeout: 5 * time.Second, WriteTimeout: 5 * time.Second},
"",
},
{
":123;pathA;pathB;pathC",
Listen{
Addr: ":123",
Scheme: "https",
Addr: ":123",
Proto: "https",
CertSource: CertSource{
Type: "file",
CertPath: "pathA",
Expand All @@ -211,14 +221,52 @@ func TestParseListen(t *testing.T) {
{
":123;cs=name",
Listen{
Addr: ":123",
Scheme: "https",
Addr: ":123",
Proto: "https",
CertSource: CertSource{
Name: "name",
Type: "foo",
},
},
"",
},
{
":123;cs=name;proto=https",
Listen{
Addr: ":123",
Proto: "https",
CertSource: CertSource{
Name: "name",
Type: "foo",
},
},
"",
},
{
":123;proto=https",
Listen{},
"proto 'https' requires cert source",
},
{
":123;cs=name;proto=http",
Listen{},
"cert source requires proto 'https'",
},
{
":123;cs=name;proto=tcp+sni",
Listen{},
"cert source requires proto 'https'",
},
{
":123;proto=foo",
Listen{},
"unknown protocol \"foo\"",
},
{
":123;cs=foo",
Listen{},
"unknown certificate source \"foo\"",
},
}

for i, tt := range tests {
Expand Down
43 changes: 32 additions & 11 deletions demo/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,15 @@ import (

func main() {
var addr, consul, name, prefix, proto, token string
var certFile, keyFile string
flag.StringVar(&addr, "addr", "127.0.0.1:5000", "host:port of the service")
flag.StringVar(&consul, "consul", "127.0.0.1:8500", "host:port of the consul agent")
flag.StringVar(&name, "name", filepath.Base(os.Args[0]), "name of the service")
flag.StringVar(&prefix, "prefix", "", "comma-sep list of host/path prefixes to register")
flag.StringVar(&proto, "proto", "http", "protocol for endpoints: http or ws")
flag.StringVar(&token, "token", "", "consul ACL token")
flag.StringVar(&certFile, "cert", "", "path to cert file")
flag.StringVar(&keyFile, "key", "", "path to key file")
flag.Parse()

if prefix == "" {
Expand All @@ -73,19 +76,26 @@ func main() {
}
}

// register consul health check endpoint
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "OK")
})

// start http server
go func() {
log.Printf("Listening on %s serving %s", addr, prefix)
if err := http.ListenAndServe(addr, nil); err != nil {

var err error
if certFile != "" {
err = http.ListenAndServeTLS(addr, certFile, keyFile, nil)
} else {
err = http.ListenAndServe(addr, nil)
}
if err != nil {
log.Fatal(err)
}
}()

// register consul health check endpoint
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "OK")
})

// build urlprefix-host/path tag list
// e.g. urlprefix-/foo, urlprefix-/bar, ...
var tags []string
Expand All @@ -103,6 +113,21 @@ func main() {
log.Fatal(err)
}

var check *api.AgentServiceCheck
if certFile != "" {
check = &api.AgentServiceCheck{
TCP: addr,
Interval: "2s",
Timeout: "1s",
}
} else {
check = &api.AgentServiceCheck{
HTTP: "http://" + addr + "/health",
Interval: "1s",
Timeout: "1s",
}
}

// register service with health check
serviceID := name + "-" + addr
service := &api.AgentServiceRegistration{
Expand All @@ -111,11 +136,7 @@ func main() {
Port: port,
Address: host,
Tags: tags,
Check: &api.AgentServiceCheck{
HTTP: "http://" + addr + "/health",
Interval: "1s",
Timeout: "1s",
},
Check: check,
}

config := &api.Config{Address: consul, Scheme: "http", Token: token}
Expand Down
29 changes: 24 additions & 5 deletions fabio.properties
Original file line number Diff line number Diff line change
Expand Up @@ -162,22 +162,38 @@
# proxy.cs =


# proxy.addr configures the HTTP and HTTPS listeners.
# proxy.addr configures listeners.
#
# Each listener is configured with and address and a
# list of optional arguments in the form of
#
# [host]:port;opt=arg;opt[=arg];...
#
# Each listener has a protocol which is configured
# with the 'proto' option for which it routes and
# forwards traffic.
#
# The supported protocols are:
#
# * http for HTTP based protocols
# * https for HTTPS based protocols
# * tcp+sni for an SNI aware TCP proxy
#
# If no 'proto' option is specified then the protocol
# is either 'http' or 'https' depending on whether a
# certificate source is configured via the 'cs' option
# which contains the name of the certificate source.
#
# The TCP+SNI proxy analyzes the ClientHello message
# of TLS connections to extract the server name
# extension and then forwards the encrypted traffic
# to the destination without decrypting the traffic.
#
# General options:
#
# read timeout: rt=<duration>
# write timeout: wt=<duration>
#
# HTTPS listeners require a certificate source which is
# configured by setting the 'cs' option to the name of
# a certificate source.
#
# Examples:
#
# # HTTP listener on port 9999
Expand All @@ -195,6 +211,9 @@
# # HTTPS listener on port 443 with certificate source
# proxy.addr = :443;cs=some-name
#
# # TCP listener on port 443 with SNI routing
# proxy.addr = :443;proto=tcp+sni
#
# The default is
#
# proxy.addr = :9999
Expand Down
43 changes: 39 additions & 4 deletions listen.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,16 @@ func init() {
}

// startListeners runs one or more listeners for the handler
func startListeners(listen []config.Listen, wait time.Duration, h http.Handler) {
func startListeners(listen []config.Listen, wait time.Duration, h http.Handler, tcph proxy.TCPProxy) {
for _, l := range listen {
go listenAndServe(l, h)
switch l.Proto {
case "tcp+sni":
go listenAndServeTCP(l, tcph)
case "http", "https":
go listenAndServeHTTP(l, h)
default:
panic("invalid protocol: " + l.Proto)
}
}

// wait for shutdown signal
Expand All @@ -39,15 +46,43 @@ func startListeners(listen []config.Listen, wait time.Duration, h http.Handler)
log.Print("[INFO] Down")
}

func listenAndServe(l config.Listen, h http.Handler) {
func listenAndServeTCP(l config.Listen, h proxy.TCPProxy) {
log.Print("[INFO] TCP+SNI proxy listening on ", l.Addr)
ln, err := net.Listen("tcp", l.Addr)
if err != nil {
exit.Fatal("[FATAL] ", err)
}
defer ln.Close()

// close the socket on exit to terminate the accept loop
go func() {
<-quit
ln.Close()
}()

for {
conn, err := ln.Accept()
if err != nil {
select {
case <-quit:
return
default:
exit.Fatal("[FATAL] ", err)
}
}
go h.Serve(conn)
}
}

func listenAndServeHTTP(l config.Listen, h http.Handler) {
srv := &http.Server{
Handler: h,
Addr: l.Addr,
ReadTimeout: l.ReadTimeout,
WriteTimeout: l.WriteTimeout,
}

if l.Scheme == "https" {
if l.Proto == "https" {
src, err := cert.NewSource(l.CertSource)
if err != nil {
exit.Fatal("[FATAL] ", err)
Expand Down
Loading

0 comments on commit 8a7b2e9

Please sign in to comment.