forked from coredns/coredns
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
plugin/grpc: New gRPC plugin (coredns#2667)
* 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
Showing
15 changed files
with
952 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -47,4 +47,5 @@ var Directives = []string{ | |
"erratic", | ||
"whoami", | ||
"on", | ||
"grpc", | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
reviewers: | ||
- inigohu | ||
- miekg | ||
approvers: | ||
- inigohu | ||
- miekg |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} | ||
}) | ||
} | ||
} |
Oops, something went wrong.