Skip to content

Commit

Permalink
plugin/grpc: New gRPC plugin (coredns#2667)
Browse files Browse the repository at this point in the history
* plugin/grpc: New gRPC plugin

* some changes after the first review:

- remove healthcheck. gRPC already has this implicitly implemented
- some naming and stetic changes
- fix some comments
- other minor fixes

* plugin/grpc: New gRPC plugin

* some changes after the first review:

- remove healthcheck. gRPC already has this implicitly implemented
- some naming and stetic changes
- fix some comments
- other minor fixes

* add OWNERS file and change plugin order

* remove Rcode checker
  • Loading branch information
inigohu authored and miekg committed Mar 14, 2019
1 parent 0d8e1cf commit 7b6cb76
Show file tree
Hide file tree
Showing 15 changed files with 952 additions and 0 deletions.
1 change: 1 addition & 0 deletions core/dnsserver/zdirectives.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,5 @@ var Directives = []string{
"erratic",
"whoami",
"on",
"grpc",
}
1 change: 1 addition & 0 deletions core/plugin/zplugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
_ "github.com/coredns/coredns/plugin/federation"
_ "github.com/coredns/coredns/plugin/file"
_ "github.com/coredns/coredns/plugin/forward"
_ "github.com/coredns/coredns/plugin/grpc"
_ "github.com/coredns/coredns/plugin/health"
_ "github.com/coredns/coredns/plugin/hosts"
_ "github.com/coredns/coredns/plugin/k8s_external"
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ github.com/opentracing/opentracing-go v1.0.2 h1:3jA2P6O1F9UOrWVpwrIo17pu01KWvNWg
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/openzipkin/zipkin-go-opentracing v0.3.4 h1:x/pBv/5VJNWkcHF1G9xqhug8Iw7X1y1zOMzDmyuvP2g=
github.com/openzipkin/zipkin-go-opentracing v0.3.4/go.mod h1:js2AbwmHW0YD9DwIw2JhQWmbfFi/UnWyYwdVhqbCDOE=
github.com/petar/GoLLRB v0.0.0-20130427215148-53be0d36a84c/go.mod h1:HUpKUBZnpzkdx0kD/+Yfuft+uD3zHGtXF/XJB14TUr4=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
Expand Down
1 change: 1 addition & 0 deletions plugin.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ etcd:etcd
loop:loop
forward:forward
proxy:deprecated
grpc:grpc
erratic:erratic
whoami:whoami
on:github.com/mholt/caddy/onevent
6 changes: 6 additions & 0 deletions plugin/grpc/OWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
reviewers:
- inigohu
- miekg
approvers:
- inigohu
- miekg
135 changes: 135 additions & 0 deletions plugin/grpc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# grpc

## Name

*grpc* - facilitates proxying DNS messages to upstream resolvers via gRPC protocol.

## Description

The *grpc* plugin supports gRPC and TLS.

This plugin can only be used once per Server Block.

## Syntax

In its most basic form:

~~~
grpc FROM TO...
~~~

* **FROM** is the base domain to match for the request to be proxied.
* **TO...** are the destination endpoints to proxy to. The number of upstreams is
limited to 15.

Multiple upstreams are randomized (see `policy`) on first use. When a proxy returns an error
the next upstream in the list is tried.

Extra knobs are available with an expanded syntax:

~~~
grpc FROM TO... {
except IGNORED_NAMES...
tls CERT KEY CA
tls_servername NAME
policy random|round_robin|sequential
}
~~~

* **FROM** and **TO...** as above.
* **IGNORED_NAMES** in `except` is a space-separated list of domains to exclude from proxying.
Requests that match none of these names will be passed through.
* `tls` **CERT** **KEY** **CA** define the TLS properties for TLS connection. From 0 to 3 arguments can be
provided with the meaning as described below

* `tls` - no client authentication is used, and the system CAs are used to verify the server certificate
* `tls` **CA** - no client authentication is used, and the file CA is used to verify the server certificate
* `tls` **CERT** **KEY** - client authentication is used with the specified cert/key pair.
The server certificate is verified with the system CAs
* `tls` **CERT** **KEY** **CA** - client authentication is used with the specified cert/key pair.
The server certificate is verified using the specified CA file

* `tls_servername` **NAME** allows you to set a server name in the TLS configuration; for instance 9.9.9.9
needs this to be set to `dns.quad9.net`. Multiple upstreams are still allowed in this scenario,
but they have to use the same `tls_servername`. E.g. mixing 9.9.9.9 (QuadDNS) with 1.1.1.1
(Cloudflare) will not work.
* `policy` specifies the policy to use for selecting upstream servers. The default is `random`.

Also note the TLS config is "global" for the whole grpc proxy if you need a different
`tls-name` for different upstreams you're out of luck.

## Metrics

If monitoring is enabled (via the *prometheus* directive) then the following metric are exported:

* `coredns_grpc_request_duration_seconds{to}` - duration per upstream interaction.
* `coredns_grpc_request_count_total{to}` - query count per upstream.
* `coredns_grpc_response_rcode_total{to, rcode}` - count of RCODEs per upstream.
and we are randomly (this always uses the `random` policy) spraying to an upstream.

## Examples

Proxy all requests within `example.org.` to a nameserver running on a different port:

~~~ corefile
example.org {
grpc . 127.0.0.1:9005
}
~~~

Load balance all requests between three resolvers, one of which has a IPv6 address.

~~~ corefile
. {
grpc . 10.0.0.10:53 10.0.0.11:1053 [2003::1]:53
}
~~~

Forward everything except requests to `example.org`

~~~ corefile
. {
grpc . 10.0.0.10:1234 {
except example.org
}
}
~~~

Proxy everything except `example.org` using the host's `resolv.conf`'s nameservers:

~~~ corefile
. {
grpc . /etc/resolv.conf {
except example.org
}
}
~~~

Proxy all requests to 9.9.9.9 using the TLS protocol, and cache every answer for up to 30
seconds. Note the `tls_servername` is mandatory if you want a working setup, as 9.9.9.9 can't be
used in the TLS negotiation.

~~~ corefile
. {
grpc . 9.9.9.9 {
tls_servername dns.quad9.net
}
cache 30
}
~~~

Or with multiple upstreams from the same provider

~~~ corefile
. {
grpc . 1.1.1.1 1.0.0.1 {
tls_servername cloudflare-dns.com
}
cache 30
}
~~~

## Bugs

The TLS config is global for the whole grpc proxy if you need a different `tls_servername` for
different upstreams you're out of luck.
130 changes: 130 additions & 0 deletions plugin/grpc/grpc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package grpc

import (
"context"
"crypto/tls"
"time"

"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/debug"
"github.com/coredns/coredns/request"

"github.com/miekg/dns"
ot "github.com/opentracing/opentracing-go"
)

// GRPC represents a plugin instance that can proxy requests to another (DNS) server via gRPC protocol.
// It has a list of proxies each representing one upstream proxy.
type GRPC struct {
proxies []*Proxy
p Policy

from string
ignored []string

tlsConfig *tls.Config
tlsServerName string

Next plugin.Handler
}

// ServeDNS implements the plugin.Handler interface.
func (g *GRPC) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := request.Request{W: w, Req: r}
if !g.match(state) {
return plugin.NextOrFailure(g.Name(), g.Next, ctx, w, r)
}

var (
span, child ot.Span
ret *dns.Msg
err error
i int
)
span = ot.SpanFromContext(ctx)
list := g.list()
deadline := time.Now().Add(defaultTimeout)

for time.Now().Before(deadline) {
if i >= len(list) {
// reached the end of list without any answer
if ret != nil {
// write empty response and finish
w.WriteMsg(ret)
}
break
}

proxy := list[i]
i++

if span != nil {
child = span.Tracer().StartSpan("query", ot.ChildOf(span.Context()))
ctx = ot.ContextWithSpan(ctx, child)
}

ret, err = proxy.query(ctx, r)
if err != nil {
// Continue with the next proxy
continue
}

if child != nil {
child.Finish()
}

// Check if the reply is correct; if not return FormErr.
if !state.Match(ret) {
debug.Hexdumpf(ret, "Wrong reply for id: %d, %s %d", ret.Id, state.QName(), state.QType())

formerr := state.ErrorMessage(dns.RcodeFormatError)
w.WriteMsg(formerr)
return 0, nil
}

w.WriteMsg(ret)
return 0, nil
}

return 0, nil
}

// NewGRPC returns a new GRPC.
func newGRPC() *GRPC {
g := &GRPC{
p: new(random),
}
return g
}

// Name implements the Handler interface.
func (g *GRPC) Name() string { return "grpc" }

// Len returns the number of configured proxies.
func (g *GRPC) len() int { return len(g.proxies) }

func (g *GRPC) match(state request.Request) bool {
if !plugin.Name(g.from).Matches(state.Name()) || !g.isAllowedDomain(state.Name()) {
return false
}

return true
}

func (g *GRPC) isAllowedDomain(name string) bool {
if dns.Name(name) == dns.Name(g.from) {
return true
}

for _, ignore := range g.ignored {
if plugin.Name(ignore).Matches(name) {
return false
}
}
return true
}

// List returns a set of proxies to be used for this client depending on the policy in p.
func (g *GRPC) list() []*Proxy { return g.p.List(g.proxies) }

const defaultTimeout = 5 * time.Second
75 changes: 75 additions & 0 deletions plugin/grpc/grpc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package grpc

import (
"context"
"errors"
"testing"

"github.com/coredns/coredns/pb"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/test"

"github.com/miekg/dns"
)

func TestGRPC(t *testing.T) {
m := &dns.Msg{}
msg, err := m.Pack()
if err != nil {
t.Fatalf("Error packing response: %s", err.Error())
}
dnsPacket := &pb.DnsPacket{Msg: msg}
tests := map[string]struct {
proxies []*Proxy
wantErr bool
}{
"single_proxy_ok": {
proxies: []*Proxy{
{client: &testServiceClient{dnsPacket: dnsPacket, err: nil}},
},
wantErr: false,
},
"multiple_proxies_ok": {
proxies: []*Proxy{
{client: &testServiceClient{dnsPacket: dnsPacket, err: nil}},
{client: &testServiceClient{dnsPacket: dnsPacket, err: nil}},
{client: &testServiceClient{dnsPacket: dnsPacket, err: nil}},
},
wantErr: false,
},
"single_proxy_ko": {
proxies: []*Proxy{
{client: &testServiceClient{dnsPacket: nil, err: errors.New("")}},
},
wantErr: true,
},
"multiple_proxies_one_ko": {
proxies: []*Proxy{
{client: &testServiceClient{dnsPacket: dnsPacket, err: nil}},
{client: &testServiceClient{dnsPacket: nil, err: errors.New("")}},
{client: &testServiceClient{dnsPacket: dnsPacket, err: nil}},
},
wantErr: false,
},
"multiple_proxies_ko": {
proxies: []*Proxy{
{client: &testServiceClient{dnsPacket: nil, err: errors.New("")}},
{client: &testServiceClient{dnsPacket: nil, err: errors.New("")}},
{client: &testServiceClient{dnsPacket: nil, err: errors.New("")}},
},
wantErr: true,
},
}

for name, tt := range tests {
t.Run(name, func(t *testing.T) {
g := newGRPC()
g.from = "."
g.proxies = tt.proxies
rec := dnstest.NewRecorder(&test.ResponseWriter{})
if _, err := g.ServeDNS(context.TODO(), rec, m); err != nil && !tt.wantErr {
t.Fatal("Expected to receive reply, but didn't")
}
})
}
}
Loading

0 comments on commit 7b6cb76

Please sign in to comment.