Skip to content

Commit

Permalink
https+tcp+sni listener support
Browse files Browse the repository at this point in the history
Adds support for the https+tcp+sni listener.  Any routes that are marked with
proto=tcp or have a scheme tcp:// will be matched for TCP steering.  Failing
any matches there, fallthrough will be to https matching.  This resolves: fabiolb#783.

clean up error handling on 'use of closed network connection' case

fix makefile tag finding stuff

fix logic for https+tcp+sni matching so that only explicit proto=tcp matches
  • Loading branch information
nathanejohnson committed Aug 28, 2020
1 parent c034d4f commit 377ba36
Show file tree
Hide file tree
Showing 27 changed files with 1,433 additions and 61 deletions.
8 changes: 4 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
FROM golang:1.13.4-alpine AS build
FROM golang:1.14.7-alpine AS build

ARG consul_version=1.6.1
ARG consul_version=1.8.2
ADD https://releases.hashicorp.com/consul/${consul_version}/consul_${consul_version}_linux_amd64.zip /usr/local/bin
RUN cd /usr/local/bin && unzip consul_${consul_version}_linux_amd64.zip

ARG vault_version=1.2.3
ARG vault_version=1.5.0
ADD https://releases.hashicorp.com/vault/${vault_version}/vault_${vault_version}_linux_amd64.zip /usr/local/bin
RUN cd /usr/local/bin && unzip vault_${vault_version}_linux_amd64.zip

Expand All @@ -13,7 +13,7 @@ COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go test -mod=vendor -trimpath -ldflags "-s -w" ./...
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod=vendor -trimpath -ldflags "-s -w"

FROM alpine:3.10
FROM alpine:3.12
RUN apk update && apk add --no-cache ca-certificates
COPY --from=build /src/fabio /usr/bin
ADD fabio.properties /etc/fabio/fabio.properties
Expand Down
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# CUR_TAG is the last git tag plus the delta from the current commit to the tag
# e.g. v1.5.5-<nr of commits since>-g<current git sha>
CUR_TAG ?= $(shell git describe)
CUR_TAG ?= $(shell git describe --tags --first-parent)

# LAST_TAG is the last git tag
# e.g. v1.5.5
LAST_TAG ?= $(shell git describe --abbrev=0)
LAST_TAG ?= $(shell git describe --tags --first-parent --abbrev=0)

# VERSION is the last git tag without the 'v'
# e.g. 1.5.5
VERSION ?= $(shell git describe --abbrev=0 | cut -c 2-)
VERSION ?= $(shell git describe --tags --first-parent --abbrev=0 | cut -c 2-)

# GOFLAGS is the flags for the go compiler.
GOFLAGS ?= -mod=vendor -ldflags "-X main.version=$(CUR_TAG)"
Expand Down
6 changes: 3 additions & 3 deletions config/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ func parseListen(cfg map[string]string, cs map[string]CertSource, readTimeout, w
case "proto":
l.Proto = v
switch l.Proto {
case "tcp", "tcp+sni", "tcp-dynamic", "http", "https", "grpc", "grpcs":
case "tcp", "tcp+sni", "tcp-dynamic", "http", "https", "grpc", "grpcs", "https+tcp+sni":
// ok
default:
return Listen{}, fmt.Errorf("unknown protocol %q", v)
Expand Down Expand Up @@ -461,8 +461,8 @@ func parseListen(cfg map[string]string, cs map[string]CertSource, readTimeout, w
if l.Addr == "" {
return Listen{}, fmt.Errorf("need listening host:port")
}
if csName != "" && l.Proto != "https" && l.Proto != "tcp" && l.Proto != "tcp-dynamic" && l.Proto != "grpcs" {
return Listen{}, fmt.Errorf("cert source requires proto 'https', 'tcp', 'tcp-dynamic' or 'grpcs'")
if csName != "" && l.Proto != "https" && l.Proto != "tcp" && l.Proto != "tcp-dynamic" && l.Proto != "grpcs" && l.Proto != "https+tcp+sni" {
return Listen{}, fmt.Errorf("cert source requires proto 'https', 'tcp', 'tcp-dynamic', 'https+tcp+sni', or 'grpcs'")
}
if csName == "" && l.Proto == "https" {
return Listen{}, fmt.Errorf("proto 'https' requires cert source")
Expand Down
4 changes: 2 additions & 2 deletions config/load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1058,13 +1058,13 @@ func TestLoad(t *testing.T) {
desc: "-proxy.addr with cert source and proto 'http' requires proto 'https', 'tcp', or 'grpcs'",
args: []string{"-proxy.addr", ":5555;cs=name;proto=http", "-proxy.cs", "cs=name;type=path;cert=value"},
cfg: func(cfg *Config) *Config { return nil },
err: errors.New("cert source requires proto 'https', 'tcp', 'tcp-dynamic' or 'grpcs'"),
err: errors.New("cert source requires proto 'https', 'tcp', 'tcp-dynamic', 'https+tcp+sni', or 'grpcs'"),
},
{
desc: "-proxy.addr with cert source and proto 'tcp+sni' requires proto 'https', 'tcp' or 'grpcs'",
args: []string{"-proxy.addr", ":5555;cs=name;proto=tcp+sni", "-proxy.cs", "cs=name;type=path;cert=value"},
cfg: func(cfg *Config) *Config { return nil },
err: errors.New("cert source requires proto 'https', 'tcp', 'tcp-dynamic' or 'grpcs'"),
err: errors.New("cert source requires proto 'https', 'tcp', 'tcp-dynamic', 'https+tcp+sni', or 'grpcs'"),
},
{
desc: "-proxy.noroutestatus too small",
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ require (
github.com/hashicorp/serf v0.7.0 // indirect
github.com/hashicorp/vault v0.6.0
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect
github.com/inetaf/tcpproxy v0.0.0-20200125044825-b6bb9b5b8252
github.com/jonboulle/clockwork v0.1.0 // indirect
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 // indirect
github.com/kr/pretty v0.1.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ github.com/hashicorp/vault v0.6.0 h1:wc/KN2SZ76imak5yOF4Utm6p0e4BNtSKLhWlYrzX+vQ
github.com/hashicorp/vault v0.6.0/go.mod h1:KfSyffbKxoVyspOdlaGVjIuwLobi07qD1bAbosPMpP0=
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ=
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/inetaf/tcpproxy v0.0.0-20200125044825-b6bb9b5b8252 h1:jeqlfkFa5h+Ak/I33QpU4p01nFhw0G5IFm/Rsenne2Y=
github.com/inetaf/tcpproxy v0.0.0-20200125044825-b6bb9b5b8252/go.mod h1:R6mExYS3O0XXjOZye3GtXfbuGF4hWQnF45CFWoj7O6g=
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 h1:12VvqtR6Aowv3l/EQUlocDHW2Cp4G9WJVH7uyH8QFJE=
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
Expand Down
42 changes: 41 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
Expand Down Expand Up @@ -49,7 +50,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 number
var version = "1.5.12"
var version = "1.5.14"

var shuttingDown int32

Expand Down Expand Up @@ -256,10 +257,35 @@ func lookupHostFn(cfg *config.Config) func(string) *route.Target {
notFound.Inc(1)
log.Print("[WARN] No route for ", host)
}

// log.Printf("[DEBUG] in HostMatcher host: %s, match rv %t -- in tcp", host, t != nil)

return t
}
}

// Returns a matcher function compatible with tcpproxy Matcher from github.com/inetaf/tcpproxy
func lookupHostMatcher(cfg *config.Config) func(context.Context, string) bool {
pick := route.Picker[cfg.Proxy.Strategy]
return func(ctx context.Context, host string) bool {
t := route.GetTable().LookupHost(host, pick)
if t == nil {
return false
}

// Make sure this is supposed to be a tcp proxy.
// opts proto= overrides scheme if present.
var (
ok bool
proto string
)
if proto, ok = t.Opts["proto"]; !ok && t.URL != nil {
proto = t.URL.Scheme
}
return "tcp" == proto
}
}

func makeTLSConfig(l config.Listen) (*tls.Config, error) {
if l.CertSource.Name == "" {
return nil, nil
Expand Down Expand Up @@ -398,6 +424,20 @@ func startServers(cfg *config.Config) {
}
}
}()
case "https+tcp+sni":
go func() {
hp := newHTTPProxy(cfg)
tp := &tcp.SNIProxy{
DialTimeout: cfg.Proxy.DialTimeout,
Lookup: lookupHostFn(cfg),
Conn: metrics.DefaultRegistry.GetCounter("tcp_sni.conn"),
ConnFail: metrics.DefaultRegistry.GetCounter("tcp_sni.connfail"),
Noroute: metrics.DefaultRegistry.GetCounter("tcp_sni.noroute"),
}
if err := proxy.ListenAndServeHTTPSTCPSNI(l, hp, tp, tlscfg, lookupHostMatcher(cfg)); err != nil {
exit.Fatal("[FATAL] ", err)
}
}()
default:
exit.Fatal("[FATAL] Invalid protocol ", l.Proto)
}
Expand Down
4 changes: 2 additions & 2 deletions proxy/http_headers.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func addHeaders(r *http.Request, cfg config.Proxy, stripPath string) error {

// set configurable ClientIPHeader
// X-Real-Ip is set later and X-Forwarded-For is set
// by the Go HTTP reverse proxy.
// by the Go HTTP reverse Proxy.
if cfg.ClientIPHeader != "" &&
cfg.ClientIPHeader != "X-Forwarded-For" &&
cfg.ClientIPHeader != "X-Real-Ip" {
Expand All @@ -55,7 +55,7 @@ func addHeaders(r *http.Request, cfg config.Proxy, stripPath string) error {

// set the X-Forwarded-For header for websocket
// connections since they aren't handled by the
// http proxy which sets it.
// http Proxy which sets it.
ws := r.Header.Get("Upgrade") == "websocket"
if ws {
r.Header.Set("X-Forwarded-For", remoteIP)
Expand Down
6 changes: 3 additions & 3 deletions proxy/http_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ func TestProxyHost(t *testing.T) {
// 2. Test that the host header is set per target, i.e. that different
// targets can have different 'host' options.
//
// The proxy is configured to use "rr" (round-robin) distribution
// The Proxy is configured to use "rr" (round-robin) distribution
// for the requests. Therefore, requests to '/hostcustom' will be
// sent to the two different targets in alternating order.
t.Run("host is custom", func(t *testing.T) {
Expand Down Expand Up @@ -392,7 +392,7 @@ func testProxyLogOutput(t *testing.T, bodySize int, cfg config.Proxy) {
}))
defer server.Close()

// create a proxy handler with mocked time
// create a Proxy handler with mocked time
tm := time.Date(2016, 1, 1, 0, 0, 0, 12345678, time.UTC)
proxyHandler := &HTTPProxy{
Config: cfg,
Expand All @@ -410,7 +410,7 @@ func testProxyLogOutput(t *testing.T, bodySize int, cfg config.Proxy) {
Logger: l,
}

// start an http server with the proxy handler
// start an http server with the Proxy handler
// which captures some parameters from the request
var remoteAddr string
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down
12 changes: 6 additions & 6 deletions proxy/http_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,17 @@ import (
"github.com/fabiolb/fabio/uuid"
)

// HTTPProxy is a dynamic reverse proxy for HTTP and HTTPS protocols.
// HTTPProxy is a dynamic reverse Proxy for HTTP and HTTPS protocols.
type HTTPProxy struct {
// Config is the proxy configuration as provided during startup.
// Config is the Proxy configuration as provided during startup.
Config config.Proxy

// Time returns the current time as the number of seconds since the epoch.
// If Time is nil, time.Now is used.
Time func() time.Time

// Transport is the http connection pool configured with timeouts.
// The proxy will panic if this value is nil.
// The Proxy will panic if this value is nil.
Transport http.RoundTripper

// InsecureTransport is the http connection pool configured with
Expand All @@ -42,7 +42,7 @@ type HTTPProxy struct {
InsecureTransport http.RoundTripper

// Lookup returns a target host for the given request.
// The proxy will panic if this value is nil.
// The Proxy will panic if this value is nil.
Lookup func(*http.Request) *route.Target

// Requests is a timer metric which is updated for every request.
Expand Down Expand Up @@ -109,7 +109,7 @@ func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}

// build the request url since r.URL will get modified
// by the reverse proxy and contains only the RequestURI anyway
// by the reverse Proxy and contains only the RequestURI anyway
requestURL := &url.URL{
Scheme: scheme(r),
Host: r.Host,
Expand All @@ -126,7 +126,7 @@ func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}

// build the real target url that is passed to the proxy
// build the real target url that is passed to the Proxy
targetURL := &url.URL{
Scheme: t.URL.Scheme,
Host: t.URL.Host,
Expand Down
104 changes: 104 additions & 0 deletions proxy/inetaf_tcpproxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package proxy

import (
"context"
"errors"
"fmt"
"log"
"net"
"net/http"

"github.com/inetaf/tcpproxy"
)

type childProxy struct {
l net.Listener
s Server
}

type InetAfTCPProxyServer struct {
Proxy *tcpproxy.Proxy
children []*childProxy
}

// Close - implements Server - is this even called?
func (tps *InetAfTCPProxyServer) Close() error {
_ = tps.Proxy.Close()
firstErr := tps.Proxy.Wait()
errChan := make(chan error)
for _, sl := range tps.children {
go func(sl *childProxy) {
errChan <- sl.s.Close()
}(sl)
}
for range tps.children {
err := <-errChan
if errors.Is(err, http.ErrServerClosed) {
err = nil
}
if firstErr == nil {
firstErr = err
}
if err != nil {
log.Printf("[ERROR] %s", err)
}
}
return firstErr

}

// Serve - implements server. The listener is ignored, but it
// calls serve on the children
func (tps *InetAfTCPProxyServer) Serve(_ net.Listener) error {
if len(tps.children) == 0 {
return fmt.Errorf("no children defined for listener")
}
errChan := make(chan error)
for _, sl := range tps.children {
go func(sl *childProxy) {
errChan <- sl.s.Serve(sl.l)
}(sl)
}
firstErr := tps.Proxy.Wait()
for range tps.children {
err := <-errChan
if errors.Is(err, http.ErrServerClosed) {
err = nil
}
if firstErr == nil {
firstErr = err
}
if err != nil {
log.Print("[FATAL] ", err)
}
}
return firstErr
}

// ServeLater - l is really only for listeners that are
// tcpproxy.TargetListener or a derivative. Don't call after
// Serve() is called.
func (tps *InetAfTCPProxyServer) ServeLater(l net.Listener, s Server) {
tps.children = append(tps.children, &childProxy{l, s})
}

func (tps *InetAfTCPProxyServer) Shutdown(ctx context.Context) error {
_ = tps.Proxy.Close() // always returns nil error anyway
firstErr := tps.Proxy.Wait() // wait for outer listener to close before telling the childProxy
errChan := make(chan error)
for _, sl := range tps.children {
go func(sl *childProxy) {
errChan <- sl.s.Shutdown(ctx)
}(sl)
}
for range tps.children {
err := <-errChan
if firstErr == nil {
firstErr = err
}
if err != nil {
log.Print("[ERROR] ", err)
}
}
return firstErr
}
2 changes: 1 addition & 1 deletion proxy/listen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func TestGracefulShutdown(t *testing.T) {
}))
defer srv.Close()

// start proxy
// start Proxy
addr := "127.0.0.1:57777"
var wg sync.WaitGroup
wg.Add(1)
Expand Down
Loading

0 comments on commit 377ba36

Please sign in to comment.