Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GRPC proxy support #575

Merged
merged 13 commits into from
Dec 7, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
9 changes: 6 additions & 3 deletions config/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,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", "http", "https":
case "tcp", "tcp+sni", "http", "https", "grpc", "grpcs":
// ok
default:
return Listen{}, fmt.Errorf("unknown protocol %q", v)
Expand Down Expand Up @@ -391,12 +391,15 @@ 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" {
return Listen{}, fmt.Errorf("cert source requires proto 'https' or 'tcp'")
if csName != "" && l.Proto != "https" && l.Proto != "tcp" && l.Proto != "grpcs" {
return Listen{}, fmt.Errorf("cert source requires proto 'https', 'tcp' or 'grpcs'")
}
if csName == "" && l.Proto == "https" {
return Listen{}, fmt.Errorf("proto 'https' requires cert source")
}
if csName == "" && l.Proto == "grpcs" {
return Listen{}, fmt.Errorf("proto 'grpcs' requires cert source")
}
if cs[csName].Type == "vault-pki" && !l.StrictMatch {
// Without StrictMatch the first issued certificate is used for all
// subsequent requests, even if the common name doesn't match.
Expand Down
21 changes: 17 additions & 4 deletions config/load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,13 @@ func TestLoad(t *testing.T) {
return cfg
},
},
{
args: []string{"-proxy.addr", ":5555;proto=grpc"},
cfg: func(cfg *Config) *Config {
cfg.Listen = []Listen{{Addr: ":5555", Proto: "grpc"}}
return cfg
},
},
{
desc: "-proxy.addr with tls configs",
args: []string{"-proxy.addr", `:5555;rt=1s;wt=2s;tlsmin=0x0300;tlsmax=0x305;tlsciphers="0x123,0x456"`},
Expand Down Expand Up @@ -941,16 +948,22 @@ func TestLoad(t *testing.T) {
err: errors.New("proto 'https' requires cert source"),
},
{
desc: "-proxy.addr with cert source and proto 'http' requires proto 'https' or 'tcp'",
desc: "-proxy.addr with proto 'grpcs' requires cert source",
args: []string{"-proxy.addr", ":5555;proto=grpcs"},
cfg: func(cfg *Config) *Config { return nil },
err: errors.New("proto 'grpcs' requires cert source"),
},
{
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' or 'tcp'"),
err: errors.New("cert source requires proto 'https', 'tcp' or 'grpcs'"),
},
{
desc: "-proxy.addr with cert source and proto 'tcp+sni' requires proto 'https' or 'tcp'",
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' or 'tcp'"),
err: errors.New("cert source requires proto 'https', 'tcp' or 'grpcs'"),
},
{
desc: "-proxy.noroutestatus too small",
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ require (
github.com/mitchellh/go-homedir v0.0.0-20160606030122-1111e456ffea // indirect
github.com/mitchellh/go-testing-interface v1.0.0 // indirect
github.com/mitchellh/mapstructure v0.0.0-20160212031839-d2dd02622084 // indirect
github.com/mwitkow/grpc-proxy v0.0.0-20181017164139-0f1106ef9c76
github.com/ncw/swift v1.0.42 // indirect
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492 // indirect
github.com/opentracing/opentracing-go v1.0.2
Expand Down Expand Up @@ -96,7 +97,7 @@ require (
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8 // indirect
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect
google.golang.org/appengine v1.3.0 // indirect
google.golang.org/grpc v1.16.0 // indirect
google.golang.org/grpc v1.16.0
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/vmihailenco/msgpack.v2 v2.9.1 // indirect
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ github.com/mitchellh/mapstructure v0.0.0-20160212031839-d2dd02622084 h1:Z2EJ6SUY
github.com/mitchellh/mapstructure v0.0.0-20160212031839-d2dd02622084/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mwitkow/grpc-proxy v0.0.0-20181017164139-0f1106ef9c76 h1:LxaK681Aane3f53b4qVTXXq83lL9fWwBTaaZOf9VizA=
github.com/mwitkow/grpc-proxy v0.0.0-20181017164139-0f1106ef9c76/go.mod h1:x5OoJHDHqxHS801UIuhqGl6QdSAEJvtausosHSdazIo=
github.com/ncw/swift v1.0.42 h1:ztvRb6hs52IHOcaYt73f9lXYLIeIuWgdooRDhdyllGI=
github.com/ncw/swift v1.0.42/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492 h1:lM6RxxfUMrYL/f8bWEUqdXrANWtrL7Nndbm9iFN0DlU=
Expand Down
13 changes: 13 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"crypto/tls"
"encoding/json"
"fmt"
"google.golang.org/grpc"
"io"
"log"
"net"
Expand Down Expand Up @@ -32,6 +33,7 @@ import (
"github.com/fabiolb/fabio/registry/static"
"github.com/fabiolb/fabio/route"
"github.com/fabiolb/fabio/trace"
grpc_proxy "github.com/mwitkow/grpc-proxy/proxy"
"github.com/pkg/profile"
dmp "github.com/sergi/go-diff/diffmatchpatch"
)
Expand Down Expand Up @@ -139,6 +141,10 @@ func main() {
log.Print("[INFO] Down")
}

func newGrpcProxy(cfg *config.Config, tlscfg *tls.Config) grpc.StreamHandler {
return grpc_proxy.TransparentHandler(proxy.GetGRPCDirector(cfg, tlscfg))
}

func newHTTPProxy(cfg *config.Config) http.Handler {
var w io.Writer
switch cfg.Log.AccessTarget {
Expand Down Expand Up @@ -273,6 +279,13 @@ func startServers(cfg *config.Config) {
exit.Fatal("[FATAL] ", err)
}
}()
case "grpc", "grpcs":
go func() {
h := newGrpcProxy(cfg, tlscfg)
if err := proxy.ListenAndServeGRPC(l, h, tlscfg); err != nil {
exit.Fatal("[FATAL] ", err)
}
}()
case "tcp":
go func() {
h := &tcp.Proxy{
Expand Down
89 changes: 89 additions & 0 deletions proxy/grpc_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package proxy

import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"net/url"

"github.com/fabiolb/fabio/config"
"github.com/fabiolb/fabio/route"
grpc_proxy "github.com/mwitkow/grpc-proxy/proxy"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
)

type GRPCServer struct {
server *grpc.Server
}

func (s *GRPCServer) Close() error {
s.server.Stop()
return nil
}

func (s *GRPCServer) Shutdown(ctx context.Context) error {
s.server.GracefulStop()
return nil
}

func (s *GRPCServer) Serve(lis net.Listener) error {
return s.server.Serve(lis)
}

func GetGRPCDirector(cfg *config.Config, tlscfg *tls.Config) func(ctx context.Context, fullMethodName string) (context.Context, *grpc.ClientConn, error) {

pick := route.Picker[cfg.Proxy.Strategy]
match := route.Matcher[cfg.Proxy.Matcher]

return func(ctx context.Context, fullMethodName string) (context.Context, *grpc.ClientConn, error) {
md, ok := metadata.FromIncomingContext(ctx)

if !ok {
return ctx, nil, fmt.Errorf("error extracting metadata from request")
}

reqUrl, err := url.ParseRequestURI(fullMethodName)

if err != nil {
return ctx, nil, fmt.Errorf("error parsing request url")
}

headers := http.Header{}

for k, v := range md {
for _, h := range v {
headers.Add(k, h)
}
}

req := &http.Request{
Host: "",
URL: reqUrl,
Header: headers,
}

target := route.GetTable().Lookup(req, req.Header.Get("trace"), pick, match, cfg.GlobMatchingDisabled)

if target == nil {
return nil, nil, fmt.Errorf("no route found")
}

opts := []grpc.DialOption{
grpc.WithDefaultCallOptions(grpc.CallCustomCodec(grpc_proxy.Codec())),
}

if target.URL.Scheme == "grpcs" && tlscfg != nil {
opts = append(opts, grpc.WithTransportCredentials(
credentials.NewTLS(&tls.Config{ClientCAs: tlscfg.ClientCAs, InsecureSkipVerify: target.TLSSkipVerify})))
}

newCtx := context.Background()
conn, err := grpc.DialContext(newCtx, target.URL.Host, opts...)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to return conn even if there was an error?


return newCtx, conn, err
}
}
18 changes: 18 additions & 0 deletions proxy/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package proxy
import (
"context"
"crypto/tls"
"google.golang.org/grpc"
"net"
"net/http"
"sync"
"time"

"github.com/fabiolb/fabio/config"
"github.com/fabiolb/fabio/proxy/tcp"
grpc_proxy "github.com/mwitkow/grpc-proxy/proxy"
)

type Server interface {
Expand Down Expand Up @@ -69,6 +71,22 @@ func ListenAndServeHTTP(l config.Listen, h http.Handler, cfg *tls.Config) error
return serve(ln, srv)
}

func ListenAndServeGRPC(l config.Listen, h grpc.StreamHandler, cfg *tls.Config) error {
ln, err := ListenTCP(l.Addr, cfg)
if err != nil {
return err
}

srv := &GRPCServer{
server: grpc.NewServer(
grpc.CustomCodec(grpc_proxy.Codec()),
grpc.UnknownServiceHandler(h),
),
}

return serve(ln, srv)
}

func ListenAndServeTCP(l config.Listen, h tcp.Handler, cfg *tls.Config) error {
ln, err := ListenTCP(l.Addr, cfg)
if err != nil {
Expand Down
6 changes: 6 additions & 0 deletions registry/consul/routecmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ func (r routecmd) build() []string {
case o == "proto=https":
dst = "https://" + addr

case o == "proto=grpcs":
dst = "grpcs://" + addr

case o == "proto=grpc":
dst = "grpc://" + addr

case strings.HasPrefix(o, "weight="):
weight = o[len("weight="):]

Expand Down
3 changes: 3 additions & 0 deletions vendor/github.com/golang/protobuf/AUTHORS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions vendor/github.com/golang/protobuf/CONTRIBUTORS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 28 additions & 0 deletions vendor/github.com/golang/protobuf/LICENSE

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading