Skip to content

Commit

Permalink
Issue #179: TCP Proxy support
Browse files Browse the repository at this point in the history
Changes for 1.4rc1

* Add test for TLS TCP proxy
* Move TLSConfig creation out of the listener code and into main
  • Loading branch information
magiconair committed Mar 25, 2017
1 parent 08261e4 commit bfea015
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 56 deletions.
27 changes: 23 additions & 4 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"log"
Expand All @@ -16,6 +17,7 @@ import (
"time"

"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"
Expand All @@ -37,7 +39,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

Expand Down Expand Up @@ -143,16 +145,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)
}
Expand Down
44 changes: 1 addition & 43 deletions proxy/listen.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion proxy/listen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}()
Expand Down
11 changes: 6 additions & 5 deletions proxy/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package proxy

import (
"context"
"crypto/tls"
"net"
"net/http"
"sync"
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down
89 changes: 86 additions & 3 deletions proxy/tcp_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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()
Expand All @@ -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)
}
}()
Expand All @@ -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()
Expand All @@ -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)
}
}()
Expand All @@ -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()

Expand Down

0 comments on commit bfea015

Please sign in to comment.