From 878dae99c7ab337b21b230ebff807711583c8dd5 Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Thu, 23 Mar 2017 10:34:32 +0100 Subject: [PATCH] Issue #179: TCP Proxy support Changes for 1.4rc1 * Add test for TLS TCP proxy * Move TLSConfig creation out of the listener code and into main --- main.go | 27 +++++++++-- proxy/listen.go | 44 +---------------- proxy/listen_test.go | 3 +- proxy/serve.go | 11 +++-- proxy/tcp_integration_test.go | 89 +++++++++++++++++++++++++++++++++-- 5 files changed, 118 insertions(+), 56 deletions(-) diff --git a/main.go b/main.go index 4ce28644a..a21416bec 100755 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "crypto/tls" "encoding/json" "fmt" "log" @@ -17,6 +18,7 @@ import ( "io" "github.com/Graylog2/go-gelf/gelf" "github.com/eBay/fabio/admin" + "github.com/eBay/fabio/cert" "github.com/eBay/fabio/config" "github.com/eBay/fabio/exit" "github.com/eBay/fabio/metrics" @@ -38,7 +40,7 @@ import ( // It is also set by the linker when fabio // is built via the Makefile or the build/docker.sh // script to ensure the correct version nubmer -var version = "1.4beta2" +var version = "1.4rc1" var shuttingDown int32 @@ -145,16 +147,33 @@ func startAdmin(cfg *config.Config) { func startServers(cfg *config.Config) { for _, l := range cfg.Listen { + var tlscfg *tls.Config + if l.CertSource.Name != "" { + src, err := cert.NewSource(l.CertSource) + if err != nil { + exit.Fatal("[FATAL] Failed to create cert source %s. %s", l.CertSource.Name, err) + } + tlscfg, err = cert.TLSConfig(src, l.StrictMatch) + if err != nil { + exit.Fatal("[FATAL] Failed to create TLS config for cert source %s. %s", l.CertSource.Name, err) + } + } + + log.Printf("[INFO] %s proxy listening on %s", strings.ToUpper(l.Proto), l.Addr) + if tlscfg != nil && tlscfg.ClientAuth == tls.RequireAndVerifyClientCert { + log.Printf("[INFO] Client certificate authentication enabled on %s", l.Addr) + } + switch l.Proto { case "http", "https": h := newHTTPProxy(cfg) - go proxy.ListenAndServeHTTP(l, h) + go proxy.ListenAndServeHTTP(l, h, tlscfg) case "tcp": h := &tcp.Proxy{cfg.Proxy.DialTimeout, lookupHostFn(cfg)} - go proxy.ListenAndServeTCP(l, h) + go proxy.ListenAndServeTCP(l, h, tlscfg) case "tcp+sni": h := &tcp.SNIProxy{cfg.Proxy.DialTimeout, lookupHostFn(cfg)} - go proxy.ListenAndServeTCP(l, h) + go proxy.ListenAndServeTCP(l, h, tlscfg) default: exit.Fatal("[FATAL] Invalid protocol ", l.Proto) } diff --git a/proxy/listen.go b/proxy/listen.go index 0696ffbc8..41ad9ddbb 100644 --- a/proxy/listen.go +++ b/proxy/listen.go @@ -7,51 +7,9 @@ import ( "time" proxyproto "github.com/armon/go-proxyproto" - "github.com/eBay/fabio/cert" - "github.com/eBay/fabio/config" ) -//func listenAndServeHTTP(l config.Listen, h http.Handler) { -// ln, err := ListenTCP(l.Addr, l.CertSource, l.StrictMatch) -// if err != nil { -// exit.Fatal("[FATAL] ", err) -// } -// -// srv := &http.Server{ -// Handler: h, -// Addr: l.Addr, -// ReadTimeout: l.ReadTimeout, -// WriteTimeout: l.WriteTimeout, -// TLSConfig: ln.(*tcpListener).cfg, -// } -// -// if srv.TLSConfig != nil { -// log.Printf("[INFO] HTTPS proxy listening on %s", l.Addr) -// if srv.TLSConfig.ClientAuth == tls.RequireAndVerifyClientCert { -// log.Printf("[INFO] Client certificate authentication enabled on %s", l.Addr) -// } -// } else { -// log.Printf("[INFO] HTTP proxy listening on %s", l.Addr) -// } -// -// if err := srv.Serve(ln); err != nil { -// exit.Fatal("[FATAL] ", err) -// } -//} - -func ListenTCP(laddr string, cs config.CertSource, strictMatch bool) (net.Listener, error) { - var cfg *tls.Config - if cs.Name != "" { - src, err := cert.NewSource(cs) - if err != nil { - return nil, fmt.Errorf("listen: Fail to create cert source. %s", err) - } - cfg, err = cert.TLSConfig(src, strictMatch) - if err != nil { - return nil, fmt.Errorf("listen: Fail to create TLS config. %s", err) - } - } - +func ListenTCP(laddr string, cfg *tls.Config) (net.Listener, error) { addr, err := net.ResolveTCPAddr("tcp", laddr) if err != nil { return nil, fmt.Errorf("listen: Fail to resolve tcp addr. %s", laddr) diff --git a/proxy/listen_test.go b/proxy/listen_test.go index 0127bbecc..5cf61d29e 100755 --- a/proxy/listen_test.go +++ b/proxy/listen_test.go @@ -35,7 +35,8 @@ func TestGracefulShutdown(t *testing.T) { return tbl.Lookup(r, "", route.Picker["rr"], route.Matcher["prefix"]) }, } - if err := ListenAndServeHTTP(config.Listen{Addr: addr}, h); err != nil { + l := config.Listen{Addr: addr} + if err := ListenAndServeHTTP(l, h, nil); err != nil { t.Log("ListenAndServeHTTP: ", err) } }() diff --git a/proxy/serve.go b/proxy/serve.go index 0d94fb9d6..5f3d15f06 100644 --- a/proxy/serve.go +++ b/proxy/serve.go @@ -2,6 +2,7 @@ package proxy import ( "context" + "crypto/tls" "net" "net/http" "sync" @@ -53,8 +54,8 @@ func Shutdown(timeout time.Duration) { wg.Wait() } -func ListenAndServeHTTP(l config.Listen, h http.Handler) error { - ln, err := ListenTCP(l.Addr, l.CertSource, l.StrictMatch) +func ListenAndServeHTTP(l config.Listen, h http.Handler, cfg *tls.Config) error { + ln, err := ListenTCP(l.Addr, cfg) if err != nil { return err } @@ -63,13 +64,13 @@ func ListenAndServeHTTP(l config.Listen, h http.Handler) error { Handler: h, ReadTimeout: l.ReadTimeout, WriteTimeout: l.WriteTimeout, - TLSConfig: ln.(*tcpListener).tlsConfig, + TLSConfig: cfg, } return serve(ln, srv) } -func ListenAndServeTCP(l config.Listen, h tcp.Handler) error { - ln, err := ListenTCP(l.Addr, l.CertSource, l.StrictMatch) +func ListenAndServeTCP(l config.Listen, h tcp.Handler, cfg *tls.Config) error { + ln, err := ListenTCP(l.Addr, cfg) if err != nil { return err } diff --git a/proxy/tcp_integration_test.go b/proxy/tcp_integration_test.go index 88970f1ff..7e11440c3 100644 --- a/proxy/tcp_integration_test.go +++ b/proxy/tcp_integration_test.go @@ -5,9 +5,14 @@ import ( "bytes" "crypto/tls" "crypto/x509" + "io/ioutil" "net" + "os" + "path/filepath" "testing" + "time" + "github.com/eBay/fabio/cert" "github.com/eBay/fabio/config" "github.com/eBay/fabio/proxy/internal" "github.com/eBay/fabio/proxy/tcp" @@ -26,6 +31,8 @@ var echoHandler tcp.HandlerFunc = func(c net.Conn) error { return err } +// TestTCPProxy tests proxying an unencrypted TCP connection +// to a TCP upstream server. func TestTCPProxy(t *testing.T) { srv := tcptest.NewServer(echoHandler) defer srv.Close() @@ -41,7 +48,7 @@ func TestTCPProxy(t *testing.T) { }, } l := config.Listen{Addr: proxyAddr} - if err := ListenAndServeTCP(l, h); err != nil { + if err := ListenAndServeTCP(l, h, nil); err != nil { t.Log("ListenAndServeTCP: ", err) } }() @@ -57,6 +64,82 @@ func TestTCPProxy(t *testing.T) { testRoundtrip(t, out) } +// TestTCPProxyWithTLS tests proxying an encrypted TCP connection +// to an unencrypted upstream TCP server. The proxy terminates the +// TLS connection. +func TestTCPProxyWithTLS(t *testing.T) { + srv := tcptest.NewServer(echoHandler) + defer srv.Close() + + // setup cert source + dir, err := ioutil.TempDir("", "fabio") + if err != nil { + t.Fatal("ioutil.TempDir", err) + } + defer os.RemoveAll(dir) + + mustWrite := func(name string, data []byte) { + path := filepath.Join(dir, name) + if err := ioutil.WriteFile(path, data, 0644); err != nil { + t.Fatalf("ioutil.WriteFile: %s", err) + } + } + mustWrite("example.com-key.pem", internal.LocalhostKey) + mustWrite("example.com-cert.pem", internal.LocalhostCert) + + // start tcp proxy + proxyAddr := "127.0.0.1:57779" + go func() { + cs := config.CertSource{Name: "cs", Type: "path", CertPath: dir} + src, err := cert.NewSource(cs) + if err != nil { + t.Fatal("cert.NewSource: ", err) + } + cfg, err := cert.TLSConfig(src, false) + if err != nil { + t.Fatal("cert.TLSConfig: ", err) + } + + h := &tcp.Proxy{ + Lookup: func(string) string { return srv.Addr }, + } + + l := config.Listen{Addr: proxyAddr} + if err := ListenAndServeTCP(l, h, cfg); err != nil { + // closing the listener returns this error from the accept loop + // which we can ignore. + if err.Error() != "accept tcp 127.0.0.1:57779: use of closed network connection" { + t.Log("ListenAndServeTCP: ", err) + } + } + }() + defer Close() + + // give cert store some time to pick up certs + time.Sleep(250 * time.Millisecond) + + rootCAs := x509.NewCertPool() + if ok := rootCAs.AppendCertsFromPEM(internal.LocalhostCert); !ok { + t.Fatal("could not parse cert") + } + cfg := &tls.Config{ + RootCAs: rootCAs, + ServerName: "example.com", + } + + // connect to proxy + out, err := tls.Dial("tcp", proxyAddr, cfg) + if err != nil { + t.Fatalf("tls.Dial: %#v", err) + } + defer out.Close() + + testRoundtrip(t, out) +} + +// TestTCPSNIProxy tests proxying an encrypted TCP connection +// to an upstream TCP service without decrypting the traffic. +// The upstream server terminates the TLS connection. func TestTCPSNIProxy(t *testing.T) { srv := tcptest.NewTLSServer(echoHandler) defer srv.Close() @@ -68,7 +151,7 @@ func TestTCPSNIProxy(t *testing.T) { Lookup: func(string) string { return srv.Addr }, } l := config.Listen{Addr: proxyAddr} - if err := ListenAndServeTCP(l, h); err != nil { + if err := ListenAndServeTCP(l, h, nil); err != nil { t.Log("ListenAndServeTCP: ", err) } }() @@ -86,7 +169,7 @@ func TestTCPSNIProxy(t *testing.T) { // connect to proxy out, err := tls.Dial("tcp", proxyAddr, cfg) if err != nil { - t.Fatalf("net.Dial: %#v", err) + t.Fatalf("tls.Dial: %#v", err) } defer out.Close()