core: add more transports (#574)

* core: add listening for other protocols

Allow CoreDNS to listen for TLS request coming over port 853. This can
be enabled with `tls://` in the config file.

Implement listening for grps:// as well.

a Corefile like:

~~~
. tls://.:1853 {
    whoami
    tls
}
~~~

Means we listen on 1853 for tls requests, the `tls` config item allows
configuration for TLS parameters. We *might* be tempted to use Caddy's
Let's Encrypt implementation here.

* Refactor coredns/grpc into CoreDNS

This makes gRPC a first class citizen in CoreDNS. Add defines as being
just another server.

* some cleanups

* unexport the servers

* Move protobuf dir

* Hook up TLS properly

* Fix test

* listen for TLS as well. README updates

* disable test, fix package

* fix test

* Fix tests

* Fix remaining test

* Some tests

* Make the test work

* Add grpc test from #580

* fix crash

* Fix tests

* Close conn

* README cleanups

* README

* link RFC
This commit is contained in:
Miek Gieben
2017-03-13 20:24:37 +00:00
committed by GitHub
parent 4985d698e2
commit bfaf9e0aec
24 changed files with 570 additions and 50 deletions

View File

@@ -9,12 +9,26 @@ import (
)
type zoneAddr struct {
Zone string
Port string
Zone string
Port string
Transport string // dns, tls or grpc
}
// String return z.Zone + ":" + z.Port as a string.
func (z zoneAddr) String() string { return z.Zone + ":" + z.Port }
// String return the string represenation of z.
func (z zoneAddr) String() string { return z.Transport + "://" + z.Zone + ":" + z.Port }
// Transport returns the protocol of the string s
func Transport(s string) string {
switch {
case strings.HasPrefix(s, TransportTLS+"://"):
return TransportTLS
case strings.HasPrefix(s, TransportDNS+"://"):
return TransportDNS
case strings.HasPrefix(s, TransportGRPC+"://"):
return TransportGRPC
}
return TransportDNS
}
// normalizeZone parses an zone string into a structured format with separate
// host, and port portions, as well as the original input string.
@@ -23,14 +37,28 @@ func (z zoneAddr) String() string { return z.Zone + ":" + z.Port }
func normalizeZone(str string) (zoneAddr, error) {
var err error
// separate host and port
// Default to DNS if there isn't a transport protocol prefix.
trans := TransportDNS
switch {
case strings.HasPrefix(str, TransportTLS+"://"):
trans = TransportTLS
str = str[len(TransportTLS+"://"):]
case strings.HasPrefix(str, TransportDNS+"://"):
trans = TransportDNS
str = str[len(TransportDNS+"://"):]
case strings.HasPrefix(str, TransportGRPC+"://"):
trans = TransportGRPC
str = str[len(TransportGRPC+"://"):]
}
host, port, err := net.SplitHostPort(str)
if err != nil {
host, port, err = net.SplitHostPort(str + ":")
// no error check here; return err at end of function
}
if len(host) > 255 {
if len(host) > 255 { // TODO(miek): this should take escaping into account.
return zoneAddr{}, fmt.Errorf("specified zone is too long: %d > 255", len(host))
}
_, d := dns.IsDomainName(host)
@@ -39,8 +67,23 @@ func normalizeZone(str string) (zoneAddr, error) {
}
if port == "" {
port = Port
if trans == TransportDNS {
port = Port
}
if trans == TransportTLS {
port = TLSPort
}
if trans == TransportGRPC {
port = GRPCPort
}
}
return zoneAddr{Zone: strings.ToLower(dns.Fqdn(host)), Port: port}, err
return zoneAddr{Zone: strings.ToLower(dns.Fqdn(host)), Port: port, Transport: trans}, err
}
// Supported transports.
const (
TransportDNS = "dns"
TransportTLS = "tls"
TransportGRPC = "grpc"
)

View File

@@ -8,10 +8,10 @@ func TestNormalizeZone(t *testing.T) {
expected string
shouldErr bool
}{
{".", ".:53", false},
{".:54", ".:54", false},
{"..", ":", true},
{"..", ":", true},
{".", "dns://.:53", false},
{".:54", "dns://.:54", false},
{"..", "://:", true},
{"..", "://:", true},
} {
addr, err := normalizeZone(test.input)
actual := addr.String()

View File

@@ -1,6 +1,8 @@
package dnsserver
import (
"crypto/tls"
"github.com/coredns/coredns/middleware"
"github.com/mholt/caddy"
@@ -21,8 +23,12 @@ type Config struct {
// First consumer is the file middleware to looks for zone files in this place.
Root string
// Server is the server that handles this config
Server *Server
// The transport we implement, normally just "dns" over TCP/UDP, but could be
// DNS-over-TLS or DNS-over-gRPC.
Transport string
// TLSConfig when listening for encrypted connections (gRPC, DNS-over-TLS).
TLSConfig *tls.Config
// Middleware stack.
Middleware []middleware.Middleware
@@ -50,7 +56,6 @@ func GetConfig(c *caddy.Controller) *Config {
// Note that this is order dependent and the order is defined in directives.go, i.e. if your middleware
// comes before the middleware you are checking; it will not be there (yet).
func GetMiddleware(c *caddy.Controller, name string) middleware.Handler {
// TODO(miek): calling the handler h(nil) should be a noop...
conf := GetConfig(c)
for _, h := range conf.Middleware {
x := h(nil)

View File

@@ -61,15 +61,16 @@ func (h *dnsContext) InspectServerBlocks(sourceFile string, serverBlocks []caddy
return nil, err
}
s.Keys[i] = za.String()
if v, ok := dups[za.Zone]; ok {
if v, ok := dups[za.String()]; ok {
return nil, fmt.Errorf("cannot serve %s - zone already defined for %v", za, v)
}
dups[za.Zone] = za.String()
dups[za.String()] = za.String()
// Save the config to our master list, and key it for lookups
cfg := &Config{
Zone: za.Zone,
Port: za.Port,
Zone: za.Zone,
Port: za.Port,
Transport: za.Transport,
}
h.saveConfig(za.String(), cfg)
}
@@ -88,11 +89,31 @@ func (h *dnsContext) MakeServers() ([]caddy.Server, error) {
// then we create a server for each group
var servers []caddy.Server
for addr, group := range groups {
s, err := NewServer(addr, group)
if err != nil {
return nil, err
// switch on addr
switch Transport(addr) {
case TransportDNS:
s, err := NewServer(addr, group)
if err != nil {
return nil, err
}
servers = append(servers, s)
case TransportTLS:
s, err := NewServerTLS(addr, group)
if err != nil {
return nil, err
}
servers = append(servers, s)
case TransportGRPC:
s, err := NewServergRPC(addr, group)
if err != nil {
return nil, err
}
servers = append(servers, s)
}
servers = append(servers, s)
}
return servers, nil
@@ -112,14 +133,11 @@ func groupConfigsByListenAddr(configs []*Config) (map[string][]*Config, error) {
groups := make(map[string][]*Config)
for _, conf := range configs {
if conf.Port == "" {
conf.Port = Port
}
addr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort(conf.ListenHost, conf.Port))
if err != nil {
return nil, err
}
addrstr := addr.String()
addrstr := conf.Transport + "://" + addr.String()
groups[addrstr] = append(groups[addrstr], conf)
}
@@ -129,6 +147,10 @@ func groupConfigsByListenAddr(configs []*Config) (map[string][]*Config, error) {
const (
// DefaultPort is the default port.
DefaultPort = "53"
// TLSPort is the default port for DNS-over-TLS.
TLSPort = "853"
// GRPCPort is the default port for DNS-over-gRPC.
GRPCPort = "443"
)
// These "soft defaults" are configurable by

View File

@@ -0,0 +1,165 @@
package dnsserver
import (
"crypto/tls"
"errors"
"fmt"
"net"
"github.com/miekg/dns"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/peer"
"github.com/coredns/coredns/pb"
)
// servergRPC represents an instance of a DNS-over-gRPC server.
type servergRPC struct {
*Server
grpcServer *grpc.Server
listenAddr net.Addr
}
// NewGRPCServer returns a new CoreDNS GRPC server and compiles all middleware in to it.
func NewServergRPC(addr string, group []*Config) (*servergRPC, error) {
s, err := NewServer(addr, group)
if err != nil {
return nil, err
}
gs := &servergRPC{Server: s}
gs.grpcServer = grpc.NewServer()
// trace foo... TODO(miek)
pb.RegisterDnsServiceServer(gs.grpcServer, gs)
return gs, nil
}
// Serve implements caddy.TCPServer interface.
func (s *servergRPC) Serve(l net.Listener) error {
s.m.Lock()
s.listenAddr = l.Addr()
s.m.Unlock()
return s.grpcServer.Serve(l)
}
// ServePacket implements caddy.UDPServer interface.
func (s *servergRPC) ServePacket(p net.PacketConn) error { return nil }
// Listen implements caddy.TCPServer interface.
func (s *servergRPC) Listen() (net.Listener, error) {
// The *tls* middleware must make sure that multiple conflicting
// TLS configuration return an error: it can only be specified once.
tlsConfig := new(tls.Config)
for _, conf := range s.zones {
// Should we error if some configs *don't* have TLS?
tlsConfig = conf.TLSConfig
}
var (
l net.Listener
err error
)
if tlsConfig == nil {
l, err = net.Listen("tcp", s.Addr[len(TransportGRPC+"://"):])
} else {
l, err = tls.Listen("tcp", s.Addr[len(TransportGRPC+"://"):], tlsConfig)
}
if err != nil {
return nil, err
}
return l, nil
}
// ListenPacket implements caddy.UDPServer interface.
func (s *servergRPC) ListenPacket() (net.PacketConn, error) { return nil, nil }
// OnStartupComplete lists the sites served by this server
// and any relevant information, assuming Quiet is false.
func (s *servergRPC) OnStartupComplete() {
if Quiet {
return
}
for zone, config := range s.zones {
fmt.Println(TransportGRPC + "://" + zone + ":" + config.Port)
}
}
func (s *servergRPC) Stop() (err error) {
s.m.Lock()
defer s.m.Unlock()
if s.grpcServer != nil {
s.grpcServer.GracefulStop()
}
return
}
// Query is the main entry-point into the gRPC server. From here we call ServeDNS like
// any normal server. We use a custom responseWriter to pick up the bytes we need to write
// back to the client as a protobuf.
func (s *servergRPC) Query(ctx context.Context, in *pb.DnsPacket) (*pb.DnsPacket, error) {
msg := new(dns.Msg)
err := msg.Unpack(in.Msg)
if err != nil {
return nil, err
}
p, ok := peer.FromContext(ctx)
if !ok {
return nil, errors.New("no peer in gRPC context")
}
a, ok := p.Addr.(*net.TCPAddr)
if !ok {
return nil, fmt.Errorf("no TCP peer in gRPC context: %v", p.Addr)
}
r := &net.IPAddr{IP: a.IP}
w := &gRPCresponse{localAddr: s.listenAddr, remoteAddr: r, Msg: msg}
s.ServeDNS(ctx, w, msg)
packed, err := w.Msg.Pack()
if err != nil {
return nil, err
}
return &pb.DnsPacket{Msg: packed}, nil
}
func (g *servergRPC) Shutdown() error {
if g.grpcServer != nil {
g.grpcServer.Stop()
}
return nil
}
type gRPCresponse struct {
localAddr net.Addr
remoteAddr net.Addr
Msg *dns.Msg
}
// Write is the hack that makes this work. It does not actually write the message
// but returns the bytes we need to to write in r. We can then pick this up in Query
// and write a proper protobuf back to the client.
func (r *gRPCresponse) Write(b []byte) (int, error) {
r.Msg = new(dns.Msg)
return len(b), r.Msg.Unpack(b)
}
// These methods implement the dns.ResponseWriter interface from Go DNS.
func (r *gRPCresponse) Close() error { return nil }
func (r *gRPCresponse) TsigStatus() error { return nil }
func (r *gRPCresponse) TsigTimersOnly(b bool) { return }
func (r *gRPCresponse) Hijack() { return }
func (r *gRPCresponse) LocalAddr() net.Addr { return r.localAddr }
func (r *gRPCresponse) RemoteAddr() net.Addr { return r.remoteAddr }
func (r *gRPCresponse) WriteMsg(m *dns.Msg) error { r.Msg = m; return nil }

View File

@@ -0,0 +1,85 @@
package dnsserver
import (
"context"
"crypto/tls"
"fmt"
"net"
"github.com/miekg/dns"
)
// serverTLS represents an instance of a TLS-over-DNS-server.
type serverTLS struct {
*Server
}
// NewTLSServer returns a new CoreDNS TLS server and compiles all middleware in to it.
func NewServerTLS(addr string, group []*Config) (*serverTLS, error) {
s, err := NewServer(addr, group)
if err != nil {
return nil, err
}
return &serverTLS{Server: s}, nil
}
// Serve implements caddy.TCPServer interface.
func (s *serverTLS) Serve(l net.Listener) error {
s.m.Lock()
// Only fill out the TCP server for this one.
s.server[tcp] = &dns.Server{Listener: l, Net: "tcp-tls", Handler: dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
ctx := context.Background()
s.ServeDNS(ctx, w, r)
})}
s.m.Unlock()
return s.server[tcp].ActivateAndServe()
}
// ServePacket implements caddy.UDPServer interface.
func (s *serverTLS) ServePacket(p net.PacketConn) error { return nil }
// Listen implements caddy.TCPServer interface.
func (s *serverTLS) Listen() (net.Listener, error) {
// The *tls* middleware must make sure that multiple conflicting
// TLS configuration return an error: it can only be specified once.
tlsConfig := new(tls.Config)
for _, conf := range s.zones {
// Should we error if some configs *don't* have TLS?
tlsConfig = conf.TLSConfig
}
var (
l net.Listener
err error
)
if tlsConfig == nil {
l, err = net.Listen("tcp", s.Addr[len(TransportTLS+"://"):])
} else {
l, err = tls.Listen("tcp", s.Addr[len(TransportTLS+"://"):], tlsConfig)
}
if err != nil {
return nil, err
}
return l, nil
}
// ListenPacket implements caddy.UDPServer interface.
func (s *serverTLS) ListenPacket() (net.PacketConn, error) { return nil, nil }
// OnStartupComplete lists the sites served by this server
// and any relevant information, assuming Quiet is false.
func (s *serverTLS) OnStartupComplete() {
if Quiet {
return
}
for zone, config := range s.zones {
fmt.Println(TransportTLS + "://" + zone + ":" + config.Port)
}
}

View File

@@ -61,7 +61,6 @@ func NewServer(addr string, group []*Config) (*Server, error) {
stack = site.Middleware[i](stack)
}
site.middlewareChain = stack
site.Server = s
}
return s, nil
@@ -95,7 +94,7 @@ func (s *Server) ServePacket(p net.PacketConn) error {
// Listen implements caddy.TCPServer interface.
func (s *Server) Listen() (net.Listener, error) {
l, err := net.Listen("tcp", s.Addr)
l, err := net.Listen("tcp", s.Addr[len(TransportDNS+"://"):])
if err != nil {
return nil, err
}
@@ -104,7 +103,7 @@ func (s *Server) Listen() (net.Listener, error) {
// ListenPacket implements caddy.UDPServer interface.
func (s *Server) ListenPacket() (net.PacketConn, error) {
p, err := net.ListenPacket("udp", s.Addr)
p, err := net.ListenPacket("udp", s.Addr[len(TransportDNS+"://"):])
if err != nil {
return nil, err
}

View File

@@ -11,6 +11,7 @@ package dnsserver
// care what middleware above them are doing.
var directives = []string{
"tls",
"root",
"bind",
"trace",