mirror of
				https://github.com/coredns/coredns.git
				synced 2025-10-30 17:53:21 -04:00 
			
		
		
		
	more etcd stuff
This commit is contained in:
		| @@ -8,24 +8,24 @@ import ( | |||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	etcdc "github.com/coreos/etcd/client" |  | ||||||
| 	"github.com/miekg/coredns/middleware" | 	"github.com/miekg/coredns/middleware" | ||||||
| 	"github.com/miekg/coredns/middleware/file" | 	"github.com/miekg/coredns/middleware/etcd" | ||||||
|  |  | ||||||
|  | 	etcdc "github.com/coreos/etcd/client" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const defaultAddress = "http://127.0.0.1:2379" | const defaultEndpoint = "http://127.0.0.1:2379" | ||||||
|  |  | ||||||
| // Etcd sets up the etcd middleware. | // Etcd sets up the etcd middleware. | ||||||
| func Etcd(c *Controller) (middleware.Middleware, error) { | func Etcd(c *Controller) (middleware.Middleware, error) { | ||||||
| 	keysapi, err := etcdParse(c) | 	client, err := etcdParse(c) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return func(next middleware.Handler) middleware.Handler { | 	return func(next middleware.Handler) middleware.Handler { | ||||||
| 		return file.File{Next: next, Zones: zones} | 		return etcd.NewEtcd(client, next, c.ServerBlockHosts) | ||||||
| 	}, nil | 	}, nil | ||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func etcdParse(c *Controller) (etcdc.KeysAPI, error) { | func etcdParse(c *Controller) (etcdc.KeysAPI, error) { | ||||||
| @@ -33,43 +33,30 @@ func etcdParse(c *Controller) (etcdc.KeysAPI, error) { | |||||||
| 		if c.Val() == "etcd" { | 		if c.Val() == "etcd" { | ||||||
| 			// etcd [address...] | 			// etcd [address...] | ||||||
| 			if !c.NextArg() { | 			if !c.NextArg() { | ||||||
|  | 				// TODO(certs) and friends, this is client side | ||||||
| 				return file.Zones{}, c.ArgErr() | 				client, err := newEtcdClient([]string{defaultEndpoint}, "", "", "") | ||||||
|  | 				return client, err | ||||||
| 			} | 			} | ||||||
| 			args1 := c.RemainingArgs() | 			client, err := newEtcdClient(c.RemainingArgs(), "", "", "") | ||||||
| 			fileName := c.Val() | 			return client, err | ||||||
|  |  | ||||||
| 			origin := c.ServerBlockHosts[c.ServerBlockHostIndex] |  | ||||||
| 			if c.NextArg() { |  | ||||||
| 				c.Next() |  | ||||||
| 				origin = c.Val() |  | ||||||
| 			} |  | ||||||
| 			// normalize this origin |  | ||||||
| 			origin = middleware.Host(origin).StandardHost() |  | ||||||
|  |  | ||||||
| 			zone, err := parseZone(origin, fileName) |  | ||||||
| 			if err == nil { |  | ||||||
| 				z[origin] = zone |  | ||||||
| 			} |  | ||||||
| 			names = append(names, origin) |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return file.Zones{Z: z, Names: names}, nil | 	return nil, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func newEtcdClient(machines []string, tlsCert, tlsKey, tlsCACert string) (etcd.KeysAPI, error) { | func newEtcdClient(endpoints []string, tlsCert, tlsKey, tlsCACert string) (etcdc.KeysAPI, error) { | ||||||
| 	etcdCfg := etcd.Config{ | 	etcdCfg := etcdc.Config{ | ||||||
| 		Endpoints: machines, | 		Endpoints: endpoints, | ||||||
| 		Transport: newHTTPSTransport(tlsCert, tlsKey, tlsCACert), | 		Transport: newHTTPSTransport(tlsCert, tlsKey, tlsCACert), | ||||||
| 	} | 	} | ||||||
| 	cli, err := etcd.New(etcdCfg) | 	cli, err := etcdc.New(etcdCfg) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	return etcd.NewKeysAPI(cli), nil | 	return etcdc.NewKeysAPI(cli), nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func newHTTPSTransport(tlsCertFile, tlsKeyFile, tlsCACertFile string) etcd.CancelableTransport { | func newHTTPSTransport(tlsCertFile, tlsKeyFile, tlsCACertFile string) etcdc.CancelableTransport { | ||||||
| 	var cc *tls.Config = nil | 	var cc *tls.Config = nil | ||||||
|  |  | ||||||
| 	if tlsCertFile != "" && tlsKeyFile != "" { | 	if tlsCertFile != "" && tlsKeyFile != "" { | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ import ( | |||||||
|  |  | ||||||
| 	"github.com/miekg/coredns/middleware" | 	"github.com/miekg/coredns/middleware" | ||||||
| 	"github.com/miekg/coredns/middleware/file" | 	"github.com/miekg/coredns/middleware/file" | ||||||
|  |  | ||||||
| 	"github.com/miekg/dns" | 	"github.com/miekg/dns" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,164 +0,0 @@ | |||||||
| // Package etcd provides the etcd server Backend implementation, |  | ||||||
| package etcd |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"encoding/json" |  | ||||||
| 	"fmt" |  | ||||||
| 	"strings" |  | ||||||
|  |  | ||||||
| 	"github.com/miekg/coredns/middleware/etcd/msg" |  | ||||||
|  |  | ||||||
| 	etcdc "github.com/coreos/etcd/client" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| const ( |  | ||||||
| 	priority = 10   // default priority when nothing is set |  | ||||||
| 	ttl      = 3600 // default ttl when nothing is set |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func (g *Backend) Records(name string, exact bool) ([]msg.Service, error) { |  | ||||||
| 	path, star := msg.PathWithWildcard(name) |  | ||||||
| 	r, err := g.get(path, true) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	segments := strings.Split(msg.Path(name), "/") |  | ||||||
| 	switch { |  | ||||||
| 	case exact && r.Node.Dir: |  | ||||||
| 		return nil, nil |  | ||||||
| 	case r.Node.Dir: |  | ||||||
| 		return g.loopNodes(r.Node.Nodes, segments, star, nil) |  | ||||||
| 	default: |  | ||||||
| 		return g.loopNodes([]*etcdc.Node{r.Node}, segments, false, nil) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (g *Backend) ReverseRecord(name string) (*msg.Service, error) { |  | ||||||
| 	path, star := msg.PathWithWildcard(name) |  | ||||||
| 	if star { |  | ||||||
| 		return nil, fmt.Errorf("reverse can not contain wildcards") |  | ||||||
| 	} |  | ||||||
| 	r, err := g.get(path, true) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	if r.Node.Dir { |  | ||||||
| 		return nil, fmt.Errorf("reverse must not be a directory") |  | ||||||
| 	} |  | ||||||
| 	segments := strings.Split(msg.Path(name), "/") |  | ||||||
| 	records, err := g.loopNodes([]*etcdc.Node{r.Node}, segments, false, nil) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	if len(records) != 1 { |  | ||||||
| 		return nil, fmt.Errorf("must be only one service record") |  | ||||||
| 	} |  | ||||||
| 	return &records[0], nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // get is a wrapper for client.Get that uses SingleInflight to suppress multiple |  | ||||||
| // outstanding queries. |  | ||||||
| func (g *Backend) get(path string, recursive bool) (*etcdc.Response, error) { |  | ||||||
| 	resp, err := g.inflight.Do(path, func() (interface{}, error) { |  | ||||||
| 		r, e := g.client.Get(g.ctx, path, &etcdc.GetOptions{Sort: false, Recursive: recursive}) |  | ||||||
| 		if e != nil { |  | ||||||
| 			return nil, e |  | ||||||
| 		} |  | ||||||
| 		return r, e |  | ||||||
| 	}) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	return resp.(*etcdc.Response), err |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type bareService struct { |  | ||||||
| 	Host     string |  | ||||||
| 	Port     int |  | ||||||
| 	Priority int |  | ||||||
| 	Weight   int |  | ||||||
| 	Text     string |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // skydns/local/skydns/east/staging/web |  | ||||||
| // skydns/local/skydns/west/production/web |  | ||||||
| // |  | ||||||
| // skydns/local/skydns/*/*/web |  | ||||||
| // skydns/local/skydns/*/web |  | ||||||
|  |  | ||||||
| // loopNodes recursively loops through the nodes and returns all the values. The nodes' keyname |  | ||||||
| // will be match against any wildcards when star is true. |  | ||||||
| func (g *Backend) loopNodes(ns []*etcdc.Node, nameParts []string, star bool, bx map[bareService]bool) (sx []msg.Service, err error) { |  | ||||||
| 	if bx == nil { |  | ||||||
| 		bx = make(map[bareService]bool) |  | ||||||
| 	} |  | ||||||
| Nodes: |  | ||||||
| 	for _, n := range ns { |  | ||||||
| 		if n.Dir { |  | ||||||
| 			nodes, err := g.loopNodes(n.Nodes, nameParts, star, bx) |  | ||||||
| 			if err != nil { |  | ||||||
| 				return nil, err |  | ||||||
| 			} |  | ||||||
| 			sx = append(sx, nodes...) |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
| 		if star { |  | ||||||
| 			keyParts := strings.Split(n.Key, "/") |  | ||||||
| 			for i, n := range nameParts { |  | ||||||
| 				if i > len(keyParts)-1 { |  | ||||||
| 					// name is longer than key |  | ||||||
| 					continue Nodes |  | ||||||
| 				} |  | ||||||
| 				if n == "*" || n == "any" { |  | ||||||
| 					continue |  | ||||||
| 				} |  | ||||||
| 				if keyParts[i] != n { |  | ||||||
| 					continue Nodes |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 		serv := new(msg.Service) |  | ||||||
| 		if err := json.Unmarshal([]byte(n.Value), serv); err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
| 		b := bareService{serv.Host, serv.Port, serv.Priority, serv.Weight, serv.Text} |  | ||||||
| 		if _, ok := bx[b]; ok { |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
| 		bx[b] = true |  | ||||||
|  |  | ||||||
| 		serv.Key = n.Key |  | ||||||
| 		serv.Ttl = g.calculateTtl(n, serv) |  | ||||||
| 		if serv.Priority == 0 { |  | ||||||
| 			serv.Priority = priority |  | ||||||
| 		} |  | ||||||
| 		sx = append(sx, *serv) |  | ||||||
| 	} |  | ||||||
| 	return sx, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // calculateTtl returns the smaller of the etcd TTL and the service's |  | ||||||
| // TTL. If neither of these are set (have a zero value), the server |  | ||||||
| // default is used. |  | ||||||
| func (g *Backend) calculateTtl(node *etcdc.Node, serv *msg.Service) uint32 { |  | ||||||
| 	etcdTtl := uint32(node.TTL) |  | ||||||
|  |  | ||||||
| 	if etcdTtl == 0 && serv.Ttl == 0 { |  | ||||||
| 		return ttl |  | ||||||
| 	} |  | ||||||
| 	if etcdTtl == 0 { |  | ||||||
| 		return serv.Ttl |  | ||||||
| 	} |  | ||||||
| 	if serv.Ttl == 0 { |  | ||||||
| 		return etcdTtl |  | ||||||
| 	} |  | ||||||
| 	if etcdTtl < serv.Ttl { |  | ||||||
| 		return etcdTtl |  | ||||||
| 	} |  | ||||||
| 	return serv.Ttl |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Client exposes the underlying Etcd client (used in tests). |  | ||||||
| func (g *Backend) Client() etcdc.KeysAPI { |  | ||||||
| 	return g.client |  | ||||||
| } |  | ||||||
| @@ -2,9 +2,12 @@ | |||||||
| package etcd | package etcd | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/miekg/coredns/middleware" | 	"github.com/miekg/coredns/middleware" | ||||||
| 	"github.com/miekg/dns" | 	"github.com/miekg/coredns/middleware/etcd/msg" | ||||||
| 	"github.com/skynetservices/skydns/singleflight" | 	"github.com/miekg/coredns/middleware/etcd/singleflight" | ||||||
|  |  | ||||||
| 	etcdc "github.com/coreos/etcd/client" | 	etcdc "github.com/coreos/etcd/client" | ||||||
| 	"golang.org/x/net/context" | 	"golang.org/x/net/context" | ||||||
| @@ -13,22 +16,135 @@ import ( | |||||||
| type ( | type ( | ||||||
| 	Etcd struct { | 	Etcd struct { | ||||||
| 		Next     middleware.Handler | 		Next     middleware.Handler | ||||||
|  | 		Zones    []string | ||||||
| 		client   etcd.KeysAPI | 		client   etcdc.KeysAPI | ||||||
| 		ctx      context.Context | 		ctx      context.Context | ||||||
| 		inflight *singleflight.Group | 		inflight *singleflight.Group | ||||||
| 	} | 	} | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func NewEtcd(client etcdc.KeysAPI, next middleware.Handler) Etcd { | func NewEtcd(client etcdc.KeysAPI, next middleware.Handler, zones []string) Etcd { | ||||||
| 	return Etcd{ | 	return Etcd{ | ||||||
| 		Next:     next, | 		Next:     next, | ||||||
|  | 		Zones:    zones, | ||||||
| 		client:   client, | 		client:   client, | ||||||
| 		ctx:      context.Background(), | 		ctx:      context.Background(), | ||||||
| 		inflight: &singleflight.Group{}, | 		inflight: &singleflight.Group{}, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (e Etcd) ServerDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { | func (g Etcd) Records(name string, exact bool) ([]msg.Service, error) { | ||||||
| 	return 0, nil | 	path, star := msg.PathWithWildcard(name) | ||||||
|  | 	r, err := g.Get(path, true) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	segments := strings.Split(msg.Path(name), "/") | ||||||
|  | 	switch { | ||||||
|  | 	case exact && r.Node.Dir: | ||||||
|  | 		return nil, nil | ||||||
|  | 	case r.Node.Dir: | ||||||
|  | 		return g.loopNodes(r.Node.Nodes, segments, star, nil) | ||||||
|  | 	default: | ||||||
|  | 		return g.loopNodes([]*etcdc.Node{r.Node}, segments, false, nil) | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Get is a wrapper for client.Get that uses SingleInflight to suppress multiple outstanding queries. | ||||||
|  | func (g Etcd) Get(path string, recursive bool) (*etcdc.Response, error) { | ||||||
|  | 	resp, err := g.inflight.Do(path, func() (interface{}, error) { | ||||||
|  | 		r, e := g.client.Get(g.ctx, path, &etcdc.GetOptions{Sort: false, Recursive: recursive}) | ||||||
|  | 		if e != nil { | ||||||
|  | 			return nil, e | ||||||
|  | 		} | ||||||
|  | 		return r, e | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return resp.(*etcdc.Response), err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // skydns/local/skydns/east/staging/web | ||||||
|  | // skydns/local/skydns/west/production/web | ||||||
|  | // | ||||||
|  | // skydns/local/skydns/*/*/web | ||||||
|  | // skydns/local/skydns/*/web | ||||||
|  |  | ||||||
|  | // loopNodes recursively loops through the nodes and returns all the values. The nodes' keyname | ||||||
|  | // will be match against any wildcards when star is true. | ||||||
|  | func (g Etcd) loopNodes(ns []*etcdc.Node, nameParts []string, star bool, bx map[msg.Service]bool) (sx []msg.Service, err error) { | ||||||
|  | 	if bx == nil { | ||||||
|  | 		bx = make(map[msg.Service]bool) | ||||||
|  | 	} | ||||||
|  | Nodes: | ||||||
|  | 	for _, n := range ns { | ||||||
|  | 		if n.Dir { | ||||||
|  | 			nodes, err := g.loopNodes(n.Nodes, nameParts, star, bx) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, err | ||||||
|  | 			} | ||||||
|  | 			sx = append(sx, nodes...) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if star { | ||||||
|  | 			keyParts := strings.Split(n.Key, "/") | ||||||
|  | 			for i, n := range nameParts { | ||||||
|  | 				if i > len(keyParts)-1 { | ||||||
|  | 					// name is longer than key | ||||||
|  | 					continue Nodes | ||||||
|  | 				} | ||||||
|  | 				if n == "*" || n == "any" { | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 				if keyParts[i] != n { | ||||||
|  | 					continue Nodes | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		serv := new(msg.Service) | ||||||
|  | 		if err := json.Unmarshal([]byte(n.Value), serv); err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		b := msg.Service{Host: serv.Host, Port: serv.Port, Priority: serv.Priority, Weight: serv.Weight, Text: serv.Text} | ||||||
|  | 		if _, ok := bx[b]; ok { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		bx[b] = true | ||||||
|  |  | ||||||
|  | 		serv.Key = n.Key | ||||||
|  | 		serv.Ttl = g.Ttl(n, serv) | ||||||
|  | 		if serv.Priority == 0 { | ||||||
|  | 			serv.Priority = priority | ||||||
|  | 		} | ||||||
|  | 		sx = append(sx, *serv) | ||||||
|  | 	} | ||||||
|  | 	return sx, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Ttl returns the smaller of the etcd TTL and the service's | ||||||
|  | // TTL. If neither of these are set (have a zero value), a default is used. | ||||||
|  | func (g Etcd) Ttl(node *etcdc.Node, serv *msg.Service) uint32 { | ||||||
|  | 	etcdTtl := uint32(node.TTL) | ||||||
|  |  | ||||||
|  | 	if etcdTtl == 0 && serv.Ttl == 0 { | ||||||
|  | 		return ttl | ||||||
|  | 	} | ||||||
|  | 	if etcdTtl == 0 { | ||||||
|  | 		return serv.Ttl | ||||||
|  | 	} | ||||||
|  | 	if serv.Ttl == 0 { | ||||||
|  | 		return etcdTtl | ||||||
|  | 	} | ||||||
|  | 	if etcdTtl < serv.Ttl { | ||||||
|  | 		return etcdTtl | ||||||
|  | 	} | ||||||
|  | 	return serv.Ttl | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	priority   = 10  // default priority when nothing is set | ||||||
|  | 	ttl        = 300 // default ttl when nothing is set | ||||||
|  | 	minTtl     = 60 | ||||||
|  | 	hostmaster = "hostmaster" | ||||||
|  | ) | ||||||
|   | |||||||
| @@ -2,7 +2,10 @@ | |||||||
|  |  | ||||||
| `etcd` enabled reading zone data from an etcd instance. The data in etcd has to be encoded as | `etcd` enabled reading zone data from an etcd instance. The data in etcd has to be encoded as | ||||||
| a [message](https://github.com/skynetservices/skydns/blob/2fcff74cdc9f9a7dd64189a447ef27ac354b725f/msg/service.go#L26) | a [message](https://github.com/skynetservices/skydns/blob/2fcff74cdc9f9a7dd64189a447ef27ac354b725f/msg/service.go#L26) | ||||||
| like SkyDNS. | like [SkyDNS](https//github.com/skynetservices/skydns). | ||||||
|  |  | ||||||
|  | If you need replies to SOA and NS queries you should add a little zone after etcd directive that has | ||||||
|  | these resource records. | ||||||
|  |  | ||||||
| ## Syntax | ## Syntax | ||||||
|  |  | ||||||
| @@ -14,16 +17,16 @@ etcd [endpoint...] | |||||||
|  |  | ||||||
| The will default to `/skydns` as the path and the local etcd proxy (http://127.0.0.1:2379). | The will default to `/skydns` as the path and the local etcd proxy (http://127.0.0.1:2379). | ||||||
|  |  | ||||||
|  | If you want to `round robin` A and AAAA responses look at the `round_robin` middleware. | ||||||
|  |  | ||||||
| ~~~ | ~~~ | ||||||
| etcd { | etcd { | ||||||
|     round_robin |  | ||||||
|     path /skydns |     path /skydns | ||||||
|     endpoint address... |     endpoint address... | ||||||
|     stubzones |     stubzones | ||||||
| } | } | ||||||
| ~~~ | ~~~ | ||||||
|  |  | ||||||
| * `round_robin` |  | ||||||
| * `path` /skydns | * `path` /skydns | ||||||
| * `endpoint` address... | * `endpoint` address... | ||||||
| * `stubzones` | * `stubzones` | ||||||
|   | |||||||
| @@ -0,0 +1,105 @@ | |||||||
|  | package etcd | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"github.com/miekg/coredns/middleware" | ||||||
|  | 	"github.com/miekg/dns" | ||||||
|  | 	"golang.org/x/net/context" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func (e Etcd) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { | ||||||
|  | 	println("ETCD MIDDLEWARE HIT") | ||||||
|  |  | ||||||
|  | 	state := middleware.State{W: w, Req: r} | ||||||
|  |  | ||||||
|  | 	m := state.AnswerMessage() | ||||||
|  | 	m.Authoritative = true | ||||||
|  | 	m.RecursionAvailable = true | ||||||
|  | 	m.Compress = true | ||||||
|  |  | ||||||
|  | 	return 0, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // only needs state and current zone name we are auth for. | ||||||
|  | /* | ||||||
|  | func (s *server) ServeDNS(w dns.ResponseWriter, req *dns.Msg) { | ||||||
|  |  | ||||||
|  | 	q := req.Question[0] | ||||||
|  | 	name := strings.ToLower(q.Name) | ||||||
|  |  | ||||||
|  | 	switch q.Qtype { | ||||||
|  | 	case dns.TypeNS: | ||||||
|  | 		records, extra, err := s.NSRecords(q, s.config.dnsDomain) | ||||||
|  | 		if isEtcdNameError(err, s) { | ||||||
|  | 			m = s.NameError(req) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		m.Answer = append(m.Answer, records...) | ||||||
|  | 		m.Extra = append(m.Extra, extra...) | ||||||
|  | 	case dns.TypeA, dns.TypeAAAA: | ||||||
|  | 		records, err := s.AddressRecords(q, name, nil, bufsize, dnssec, false) | ||||||
|  | 		if isEtcdNameError(err, s) { | ||||||
|  | 			m = s.NameError(req) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		m.Answer = append(m.Answer, records...) | ||||||
|  | 	case dns.TypeTXT: | ||||||
|  | 		records, err := s.TXTRecords(q, name) | ||||||
|  | 		if isEtcdNameError(err, s) { | ||||||
|  | 			m = s.NameError(req) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		m.Answer = append(m.Answer, records...) | ||||||
|  | 	case dns.TypeCNAME: | ||||||
|  | 		records, err := s.CNAMERecords(q, name) | ||||||
|  | 		if isEtcdNameError(err, s) { | ||||||
|  | 			m = s.NameError(req) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		m.Answer = append(m.Answer, records...) | ||||||
|  | 	case dns.TypeMX: | ||||||
|  | 		records, extra, err := s.MXRecords(q, name, bufsize, dnssec) | ||||||
|  | 		if isEtcdNameError(err, s) { | ||||||
|  | 			m = s.NameError(req) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		m.Answer = append(m.Answer, records...) | ||||||
|  | 		m.Extra = append(m.Extra, extra...) | ||||||
|  | 	default: | ||||||
|  | 		fallthrough // also catch other types, so that they return NODATA | ||||||
|  | 	case dns.TypeSRV: | ||||||
|  | 		records, extra, err := s.SRVRecords(q, name, bufsize, dnssec) | ||||||
|  | 		if err != nil { | ||||||
|  | 			if isEtcdNameError(err, s) { | ||||||
|  | 				m = s.NameError(req) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			logf("got error from backend: %s", err) | ||||||
|  | 			if q.Qtype == dns.TypeSRV { // Otherwise NODATA | ||||||
|  | 				m = s.ServerFailure(req) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		// if we are here again, check the types, because an answer may only | ||||||
|  | 		// be given for SRV. All other types should return NODATA, the | ||||||
|  | 		// NXDOMAIN part is handled in the above code. TODO(miek): yes this | ||||||
|  | 		// can be done in a more elegant manor. | ||||||
|  | 		if q.Qtype == dns.TypeSRV { | ||||||
|  | 			m.Answer = append(m.Answer, records...) | ||||||
|  | 			m.Extra = append(m.Extra, extra...) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(m.Answer) == 0 { // NODATA response | ||||||
|  | 		m.Ns = []dns.RR{s.NewSOA()} | ||||||
|  | 		m.Ns[0].Header().Ttl = s.config.MinTtl | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // etcNameError checks if the error is ErrorCodeKeyNotFound from etcd. | ||||||
|  | func isEtcdNameError(err error, s *server) bool { | ||||||
|  | 	if e, ok := err.(etcd.Error); ok && e.Code == etcd.ErrorCodeKeyNotFound { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  | */ | ||||||
|   | |||||||
							
								
								
									
										327
									
								
								middleware/etcd/lookup.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										327
									
								
								middleware/etcd/lookup.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,327 @@ | |||||||
|  | package etcd | ||||||
|  |  | ||||||
|  | /* | ||||||
|  | func (s *server) AddressRecords(q dns.Question, name string, previousRecords []dns.RR, state middleware.State) (records []dns.RR, err error) { | ||||||
|  | 	services, err := s.backend.Records(name, false) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	services = msg.Group(services) | ||||||
|  |  | ||||||
|  | 	for _, serv := range services { | ||||||
|  | 		ip := net.ParseIP(serv.Host) | ||||||
|  | 		switch { | ||||||
|  | 		case ip == nil: | ||||||
|  | 			// Try to resolve as CNAME if it's not an IP, but only if we don't create loops. | ||||||
|  | 			if q.Name == dns.Fqdn(serv.Host) { | ||||||
|  | 				// x CNAME x is a direct loop, don't add those | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			newRecord := serv.NewCNAME(q.Name, dns.Fqdn(serv.Host)) | ||||||
|  | 			if len(previousRecords) > 7 { | ||||||
|  | 				logf("CNAME lookup limit of 8 exceeded for %s", newRecord) | ||||||
|  | 				// don't add it, and just continue | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			if s.isDuplicateCNAME(newRecord, previousRecords) { | ||||||
|  | 				logf("CNAME loop detected for record %s", newRecord) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			nextRecords, err := s.AddressRecords(dns.Question{Name: dns.Fqdn(serv.Host), Qtype: q.Qtype, Qclass: q.Qclass}, | ||||||
|  | 				strings.ToLower(dns.Fqdn(serv.Host)), append(previousRecords, newRecord), state) | ||||||
|  | 			if err == nil { | ||||||
|  | 				// Only have we found something we should add the CNAME and the IP addresses. | ||||||
|  | 				if len(nextRecords) > 0 { | ||||||
|  | 					records = append(records, newRecord) | ||||||
|  | 					records = append(records, nextRecords...) | ||||||
|  | 				} | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			// This means we can not complete the CNAME, try to look else where. | ||||||
|  | 			target := newRecord.Target | ||||||
|  | 			if dns.IsSubDomain(s.config.Domain, target) { | ||||||
|  | 				// We should already have found it | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			m1, e1 := s.Lookup(target, q.Qtype, bufsize, dnssec) | ||||||
|  | 			if e1 != nil { | ||||||
|  | 				logf("incomplete CNAME chain: %s", e1) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			// Len(m1.Answer) > 0 here is well? | ||||||
|  | 			records = append(records, newRecord) | ||||||
|  | 			records = append(records, m1.Answer...) | ||||||
|  | 			continue | ||||||
|  | 		case ip.To4() != nil && (q.Qtype == dns.TypeA || both): | ||||||
|  | 			records = append(records, serv.NewA(q.Name, ip.To4())) | ||||||
|  | 		case ip.To4() == nil && (q.Qtype == dns.TypeAAAA || both): | ||||||
|  | 			records = append(records, serv.NewAAAA(q.Name, ip.To16())) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return records, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NSRecords returns NS records from etcd. | ||||||
|  | func (s *server) NSRecords(q dns.Question, state middleware.State) (records []dns.RR, extra []dns.RR, err error) { | ||||||
|  | 	services, err := s.backend.Records(name, false) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	services = msg.Group(services) | ||||||
|  |  | ||||||
|  | 	for _, serv := range services { | ||||||
|  | 		ip := net.ParseIP(serv.Host) | ||||||
|  | 		switch { | ||||||
|  | 		case ip == nil: | ||||||
|  | 			return nil, nil, fmt.Errorf("NS record must be an IP address") | ||||||
|  | 		case ip.To4() != nil: | ||||||
|  | 			serv.Host = msg.Domain(serv.Key) | ||||||
|  | 			records = append(records, serv.NewNS(q.Name, serv.Host)) | ||||||
|  | 			extra = append(extra, serv.NewA(serv.Host, ip.To4())) | ||||||
|  | 		case ip.To4() == nil: | ||||||
|  | 			serv.Host = msg.Domain(serv.Key) | ||||||
|  | 			records = append(records, serv.NewNS(q.Name, serv.Host)) | ||||||
|  | 			extra = append(extra, serv.NewAAAA(serv.Host, ip.To16())) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return records, extra, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SRVRecords returns SRV records from etcd. | ||||||
|  | // If the Target is not a name but an IP address, a name is created. | ||||||
|  | func (s *server) SRVRecords(s middleware.State) (records []dns.RR, extra []dns.RR, err error) { | ||||||
|  | 	services, err := s.backend.Records(name, false) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	services = msg.Group(services) | ||||||
|  |  | ||||||
|  | 	// Looping twice to get the right weight vs priority | ||||||
|  | 	w := make(map[int]int) | ||||||
|  | 	for _, serv := range services { | ||||||
|  | 		weight := 100 | ||||||
|  | 		if serv.Weight != 0 { | ||||||
|  | 			weight = serv.Weight | ||||||
|  | 		} | ||||||
|  | 		if _, ok := w[serv.Priority]; !ok { | ||||||
|  | 			w[serv.Priority] = weight | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		w[serv.Priority] += weight | ||||||
|  | 	} | ||||||
|  | 	lookup := make(map[string]bool) | ||||||
|  | 	for _, serv := range services { | ||||||
|  | 		w1 := 100.0 / float64(w[serv.Priority]) | ||||||
|  | 		if serv.Weight == 0 { | ||||||
|  | 			w1 *= 100 | ||||||
|  | 		} else { | ||||||
|  | 			w1 *= float64(serv.Weight) | ||||||
|  | 		} | ||||||
|  | 		weight := uint16(math.Floor(w1)) | ||||||
|  | 		ip := net.ParseIP(serv.Host) | ||||||
|  | 		switch { | ||||||
|  | 		case ip == nil: | ||||||
|  | 			srv := serv.NewSRV(q.Name, weight) | ||||||
|  | 			records = append(records, srv) | ||||||
|  |  | ||||||
|  | 			if _, ok := lookup[srv.Target]; ok { | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			lookup[srv.Target] = true | ||||||
|  |  | ||||||
|  | 			if !dns.IsSubDomain(s.config.Domain, srv.Target) { | ||||||
|  | 				m1, e1 := s.Lookup(srv.Target, dns.TypeA, bufsize, dnssec) | ||||||
|  | 				if e1 == nil { | ||||||
|  | 					extra = append(extra, m1.Answer...) | ||||||
|  | 				} | ||||||
|  | 				m1, e1 = s.Lookup(srv.Target, dns.TypeAAAA, bufsize, dnssec) | ||||||
|  | 				if e1 == nil { | ||||||
|  | 					// If we have seen CNAME's we *assume* that they are already added. | ||||||
|  | 					for _, a := range m1.Answer { | ||||||
|  | 						if _, ok := a.(*dns.CNAME); !ok { | ||||||
|  | 							extra = append(extra, a) | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 			// Internal name, we should have some info on them, either v4 or v6 | ||||||
|  | 			// Clients expect a complete answer, because we are a recursor in their | ||||||
|  | 			// view. | ||||||
|  | 			addr, e1 := s.AddressRecords(dns.Question{srv.Target, dns.ClassINET, dns.TypeA}, | ||||||
|  | 				srv.Target, nil, bufsize, dnssec, true) | ||||||
|  | 			if e1 == nil { | ||||||
|  | 				extra = append(extra, addr...) | ||||||
|  | 			} | ||||||
|  | 		case ip.To4() != nil: | ||||||
|  | 			serv.Host = msg.Domain(serv.Key) | ||||||
|  | 			srv := serv.NewSRV(q.Name, weight) | ||||||
|  |  | ||||||
|  | 			records = append(records, srv) | ||||||
|  | 			extra = append(extra, serv.NewA(srv.Target, ip.To4())) | ||||||
|  | 		case ip.To4() == nil: | ||||||
|  | 			serv.Host = msg.Domain(serv.Key) | ||||||
|  | 			srv := serv.NewSRV(q.Name, weight) | ||||||
|  |  | ||||||
|  | 			records = append(records, srv) | ||||||
|  | 			extra = append(extra, serv.NewAAAA(srv.Target, ip.To16())) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return records, extra, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // MXRecords returns MX records from etcd. | ||||||
|  | // If the Target is not a name but an IP address, a name is created. | ||||||
|  | func (s *server) MXRecords(q dns.Question, name string, s middleware.State) (records []dns.RR, extra []dns.RR, err error) { | ||||||
|  | 	services, err := s.backend.Records(name, false) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	lookup := make(map[string]bool) | ||||||
|  | 	for _, serv := range services { | ||||||
|  | 		if !serv.Mail { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		ip := net.ParseIP(serv.Host) | ||||||
|  | 		switch { | ||||||
|  | 		case ip == nil: | ||||||
|  | 			mx := serv.NewMX(q.Name) | ||||||
|  | 			records = append(records, mx) | ||||||
|  | 			if _, ok := lookup[mx.Mx]; ok { | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			lookup[mx.Mx] = true | ||||||
|  |  | ||||||
|  | 			if !dns.IsSubDomain(s.config.Domain, mx.Mx) { | ||||||
|  | 				m1, e1 := s.Lookup(mx.Mx, dns.TypeA, bufsize, dnssec) | ||||||
|  | 				if e1 == nil { | ||||||
|  | 					extra = append(extra, m1.Answer...) | ||||||
|  | 				} | ||||||
|  | 				m1, e1 = s.Lookup(mx.Mx, dns.TypeAAAA, bufsize, dnssec) | ||||||
|  | 				if e1 == nil { | ||||||
|  | 					// If we have seen CNAME's we *assume* that they are already added. | ||||||
|  | 					for _, a := range m1.Answer { | ||||||
|  | 						if _, ok := a.(*dns.CNAME); !ok { | ||||||
|  | 							extra = append(extra, a) | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 			// Internal name | ||||||
|  | 			addr, e1 := s.AddressRecords(dns.Question{mx.Mx, dns.ClassINET, dns.TypeA}, | ||||||
|  | 				mx.Mx, nil, bufsize, dnssec, true) | ||||||
|  | 			if e1 == nil { | ||||||
|  | 				extra = append(extra, addr...) | ||||||
|  | 			} | ||||||
|  | 		case ip.To4() != nil: | ||||||
|  | 			serv.Host = msg.Domain(serv.Key) | ||||||
|  | 			records = append(records, serv.NewMX(q.Name)) | ||||||
|  | 			extra = append(extra, serv.NewA(serv.Host, ip.To4())) | ||||||
|  | 		case ip.To4() == nil: | ||||||
|  | 			serv.Host = msg.Domain(serv.Key) | ||||||
|  | 			records = append(records, serv.NewMX(q.Name)) | ||||||
|  | 			extra = append(extra, serv.NewAAAA(serv.Host, ip.To16())) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return records, extra, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (s *server) CNAMERecords(q dns.Question, state middleware.State) (records []dns.RR, err error) { | ||||||
|  | 	services, err := s.backend.Records(name, true) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	services = msg.Group(services) | ||||||
|  |  | ||||||
|  | 	if len(services) > 0 { | ||||||
|  | 		serv := services[0] | ||||||
|  | 		if ip := net.ParseIP(serv.Host); ip == nil { | ||||||
|  | 			records = append(records, serv.NewCNAME(q.Name, dns.Fqdn(serv.Host))) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return records, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (s *server) TXTRecords(q dns.Question, state middleware.State) (records []dns.RR, err error) { | ||||||
|  | 	services, err := s.backend.Records(name, false) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	services = msg.Group(services) | ||||||
|  |  | ||||||
|  | 	for _, serv := range services { | ||||||
|  | 		if serv.Text == "" { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		records = append(records, serv.NewTXT(q.Name)) | ||||||
|  | 	} | ||||||
|  | 	return records, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func isDuplicateCNAME(r *dns.CNAME, records []dns.RR) bool { | ||||||
|  | 	for _, rec := range records { | ||||||
|  | 		if v, ok := rec.(*dns.CNAME); ok { | ||||||
|  | 			if v.Target == r.Target { | ||||||
|  | 				return true | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Move to state.go somehow? | ||||||
|  | func (s *server) NameError(req *dns.Msg) *dns.Msg { | ||||||
|  | 	m := new(dns.Msg) | ||||||
|  | 	m.SetRcode(req, dns.RcodeNameError) | ||||||
|  | 	m.Ns = []dns.RR{s.NewSOA()} | ||||||
|  | 	m.Ns[0].Header().Ttl = s.config.MinTtl | ||||||
|  | 	return m | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // overflowOrTruncated writes back an error to the client if the message does not fit. | ||||||
|  | // It updates prometheus metrics. If something has been written to the client, true | ||||||
|  | // will be returned. | ||||||
|  | func (s *server) overflowOrTruncated(w dns.ResponseWriter, m *dns.Msg, bufsize int, sy metrics.System) bool { | ||||||
|  | 	switch isTCP(w) { | ||||||
|  | 	case true: | ||||||
|  | 		if _, overflow := Fit(m, dns.MaxMsgSize, true); overflow { | ||||||
|  | 			metrics.ReportErrorCount(m, sy) | ||||||
|  | 			msgFail := s.ServerFailure(m) | ||||||
|  | 			w.WriteMsg(msgFail) | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 	case false: | ||||||
|  | 		// Overflow with udp always results in TC. | ||||||
|  | 		Fit(m, bufsize, false) | ||||||
|  | 		metrics.ReportErrorCount(m, sy) | ||||||
|  | 		if m.Truncated { | ||||||
|  | 			w.WriteMsg(m) | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // etcNameError return a NameError to the client if the error | ||||||
|  | // returned from etcd has ErrorCode == 100. | ||||||
|  | func isEtcdNameError(err error, s *server) bool { | ||||||
|  | 	if e, ok := err.(etcd.Error); ok && e.Code == etcd.ErrorCodeKeyNotFound { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 	if err != nil { | ||||||
|  | 		logf("error from backend: %s", err) | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  | */ | ||||||
| @@ -8,3 +8,7 @@ func Exchange(c *dns.Client, m *dns.Msg, server string) (*dns.Msg, error) { | |||||||
| 	r, _, err := c.Exchange(m, server) | 	r, _, err := c.Exchange(m, server) | ||||||
| 	return r, err | 	return r, err | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Lookup functions, ala | ||||||
|  | // LookupHost | ||||||
|  | // LookupCNAME | ||||||
|   | |||||||
| @@ -83,6 +83,30 @@ func (s State) Family() int { | |||||||
| 	return 2 | 	return 2 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Do returns if the request has the DO (DNSSEC OK) bit set. | ||||||
|  | func (s State) Do() bool { | ||||||
|  | 	if o := s.Req.IsEdns0(); o != nil { | ||||||
|  | 		return o.Do() | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // UDPSize returns if UDP buffer size advertised in the requests OPT record. | ||||||
|  | // Or when the request was over TCP, we return the maximum allowed size of 64K. | ||||||
|  | func (s State) Size() int { | ||||||
|  | 	if s.Proto() == "tcp" { | ||||||
|  | 		return dns.MaxMsgSize | ||||||
|  | 	} | ||||||
|  | 	if o := s.Req.IsEdns0(); o != nil { | ||||||
|  | 		s := o.UDPSize() | ||||||
|  | 		if s < dns.MinMsgSize { | ||||||
|  | 			s = dns.MinMsgSize | ||||||
|  | 		} | ||||||
|  | 		return int(s) | ||||||
|  | 	} | ||||||
|  | 	return dns.MinMsgSize | ||||||
|  | } | ||||||
|  |  | ||||||
| // Type returns the type of the question as a string. | // Type returns the type of the question as a string. | ||||||
| func (s State) Type() string { | func (s State) Type() string { | ||||||
| 	return dns.Type(s.Req.Question[0].Qtype).String() | 	return dns.Type(s.Req.Question[0].Qtype).String() | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user