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

Adding an integration test

make sure proxy is listening before starting test

add buffered err chan to Close() per dcarbone

add 1.14 and 1.15 to travis tests
  • Loading branch information
nathanejohnson committed Aug 31, 2020
1 parent c034d4f commit ffec71e
Show file tree
Hide file tree
Showing 23 changed files with 1,628 additions and 16 deletions.
7 changes: 7 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,15 @@ language: go

go:
- 1.13
- 1.14
- 1.15
- master

addons:
hosts:
- example.com
- example2.com

script: make travis

jobs:
Expand Down
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
39 changes: 38 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 @@ -260,6 +261,28 @@ func lookupHostFn(cfg *config.Config) func(string) *route.Target {
}
}

// 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 +421,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
11 changes: 11 additions & 0 deletions proxy/http_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,9 @@ func tlsClientConfig() *tls.Config {
if ok := rootCAs.AppendCertsFromPEM(internal.LocalhostCert); !ok {
panic("could not parse cert")
}
if ok := rootCAs.AppendCertsFromPEM(internal.LocalhostCert2); !ok {
panic("could not parse cert")
}
return &tls.Config{RootCAs: rootCAs}
}

Expand All @@ -655,6 +658,14 @@ func tlsServerConfig() *tls.Config {
return &tls.Config{Certificates: []tls.Certificate{cert}}
}

func tlsServerConfig2() *tls.Config {
cert, err := tls.X509KeyPair(internal.LocalhostCert2, internal.LocalhostKey2)
if err != nil {
panic("failed to set cert")
}
return &tls.Config{Certificates: []tls.Certificate{cert}}
}

func mustParse(rawurl string) *url.URL {
u, err := url.Parse(rawurl)
if err != nil {
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, len(tps.children))
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, len(tps.children))
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, len(tps.children))
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
}
Loading

0 comments on commit ffec71e

Please sign in to comment.