skydns/000755 000765 000024 00000000000 13063661551 012530 5ustar00tpotstaff000000 000000 skydns/.gitignore000644 000765 000024 00000000014 12733124226 014507 0ustar00tpotstaff000000 000000 skydns tags skydns/.travis.yml000644 000765 000024 00000000701 13063661551 014637 0ustar00tpotstaff000000 000000 language: go go: - 1.5 - 1.6 before_script: - curl -L https://github.com/coreos/etcd/releases/download/v2.3.1/etcd-v2.3.1-linux-amd64.tar.gz -o etcd-v2.3.1-linux-amd64.tar.gz - tar xzvf etcd-v2.3.1-linux-amd64.tar.gz - ./etcd-v2.3.1-linux-amd64/etcd & - go get - go get github.com/coreos/go-etcd/etcd - go get github.com/rcrowley/go-metrics/stathat - go get golang.org/x/net/context script: - go test -tags etcd -bench=. ./... skydns/AUTHORS000644 000765 000024 00000000072 12733124226 013573 0ustar00tpotstaff000000 000000 Erik St. Martin Brian Ketelsen Miek Gieben Michael Crosby skydns/backends/000755 000765 000024 00000000000 12733124226 014276 5ustar00tpotstaff000000 000000 skydns/cache/000755 000765 000024 00000000000 12733124226 013567 5ustar00tpotstaff000000 000000 skydns/CONTRIBUTORS000644 000765 000024 00000000346 12733124226 014407 0ustar00tpotstaff000000 000000 The following people have contributed to this project: Erik St. Martin Brian Ketelsen Miek Gieben Sean Carey Michael Crosby skydns/Dockerfile000644 000765 000024 00000000272 13063661551 014523 0ustar00tpotstaff000000 000000 FROM alpine:latest MAINTAINER Miek Gieben (@miekg) RUN apk --update add bind-tools && rm -rf /var/cache/apk/* ADD skydns skydns EXPOSE 53 53/udp ENTRYPOINT ["/skydns"] skydns/LICENSE000644 000765 000024 00000002075 12733124226 013535 0ustar00tpotstaff000000 000000 The MIT License (MIT) Copyright (c) 2013 The SkyDNS Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. skydns/main.go000644 000765 000024 00000016364 13063661551 014015 0ustar00tpotstaff000000 000000 // Copyright (c) 2014 The SkyDNS Authors. All rights reserved. // Use of this source code is governed by The MIT License (MIT) that can be // found in the LICENSE file. package main import ( "crypto/tls" "crypto/x509" "encoding/json" "flag" "fmt" "io/ioutil" "log" "math/rand" "net" "net/http" "os" "strconv" "strings" "time" backendetcd "github.com/skynetservices/skydns/backends/etcd" "github.com/skynetservices/skydns/metrics" "github.com/skynetservices/skydns/msg" "github.com/skynetservices/skydns/server" etcd "github.com/coreos/etcd/client" "github.com/miekg/dns" "golang.org/x/net/context" ) var ( tlskey = "" tlspem = "" cacert = "" config = &server.Config{ReadTimeout: 0, Domain: "", DnsAddr: "", DNSSEC: ""} nameserver = "" machine = "" stub = false ctx = context.Background() ) func env(key, def string) string { if x := os.Getenv(key); x != "" { return x } return def } func boolEnv(key string, def bool) bool { if x := os.Getenv(key); x != "" { if v, err := strconv.ParseBool(x); err == nil { return v } } return def } func init() { flag.StringVar(&config.Domain, "domain", env("SKYDNS_DOMAIN", "skydns.local."), "domain to anchor requests to (SKYDNS_DOMAIN)") flag.StringVar(&config.DnsAddr, "addr", env("SKYDNS_ADDR", "127.0.0.1:53"), "ip:port to bind to (SKYDNS_ADDR)") flag.StringVar(&nameserver, "nameservers", env("SKYDNS_NAMESERVERS", ""), "nameserver address(es) to forward (non-local) queries to e.g. 8.8.8.8:53,8.8.4.4:53") flag.BoolVar(&config.NoRec, "no-rec", false, "do not provide a recursive service") flag.StringVar(&machine, "machines", env("ETCD_MACHINES", "http://127.0.0.1:2379"), "machine address(es) running etcd") flag.StringVar(&config.DNSSEC, "dnssec", "", "basename of DNSSEC key file e.q. Kskydns.local.+005+38250") flag.StringVar(&config.Local, "local", "", "optional unique value for this skydns instance") flag.StringVar(&tlskey, "tls-key", env("ETCD_TLSKEY", ""), "TLS Private Key path") flag.StringVar(&tlspem, "tls-pem", env("ETCD_TLSPEM", ""), "X509 Certificate") flag.StringVar(&cacert, "ca-cert", env("ETCD_CACERT", ""), "CA Certificate") flag.DurationVar(&config.ReadTimeout, "rtimeout", 2*time.Second, "read timeout") flag.BoolVar(&config.RoundRobin, "round-robin", true, "round robin A/AAAA replies") flag.BoolVar(&config.NSRotate, "ns-rotate", true, "round robin selection of nameservers from among those listed") flag.BoolVar(&stub, "stubzones", false, "support stub zones") flag.BoolVar(&config.Verbose, "verbose", false, "log queries") flag.BoolVar(&config.Systemd, "systemd", boolEnv("SKYDNS_SYSTEMD", false), "bind to socket(s) activated by systemd (ignore -addr)") // Version flag.BoolVar(&config.Version, "version", false, "Print the version and exit.") // TTl // Minttl flag.StringVar(&config.Hostmaster, "hostmaster", "hostmaster@skydns.local.", "hostmaster email address to use") flag.IntVar(&config.SCache, "scache", server.SCacheCapacity, "capacity of the signature cache") flag.IntVar(&config.RCache, "rcache", 0, "capacity of the response cache") // default to 0 for now flag.IntVar(&config.RCacheTtl, "rcache-ttl", server.RCacheTtl, "TTL of the response cache") flag.StringVar(&msg.PathPrefix, "path-prefix", env("SKYDNS_PATH_PREFIX", "skydns"), "backend(etcd) path prefix, default: skydns") } func main() { flag.Parse() if config.Version { fmt.Printf("skydns server version: %s\n", server.Version) os.Exit(0) } machines := strings.Split(machine, ",") client, err := newEtcdClient(machines, tlspem, tlskey, cacert) if err != nil { panic(err) } if nameserver != "" { for _, hostPort := range strings.Split(nameserver, ",") { if err := validateHostPort(hostPort); err != nil { log.Fatalf("skydns: nameserver is invalid: %s", err) } config.Nameservers = append(config.Nameservers, hostPort) } } if err := validateHostPort(config.DnsAddr); err != nil { log.Fatalf("skydns: addr is invalid: %s", err) } if err := loadConfig(client, config); err != nil { log.Fatalf("skydns: %s", err) } if err := server.SetDefaults(config); err != nil { log.Fatalf("skydns: defaults could not be set from /etc/resolv.conf: %v", err) } if config.Local != "" { config.Local = dns.Fqdn(config.Local) } backend := backendetcd.NewBackend(client, ctx, &backendetcd.Config{ Ttl: config.Ttl, Priority: config.Priority, }) s := server.New(backend, config) if stub { s.UpdateStubZones() go func() { duration := 1 * time.Second var watcher etcd.Watcher watcher = client.Watcher(msg.Path(config.Domain)+"/dns/stub/", &etcd.WatcherOptions{AfterIndex: 0, Recursive: true}) for { _, err := watcher.Next(ctx) if err != nil { // log.Printf("skydns: stubzone update failed, sleeping %s + ~3s", duration) time.Sleep(duration + (time.Duration(rand.Float32() * 3e9))) // Add some random. duration *= 2 if duration > 32*time.Second { duration = 32 * time.Second } } else { s.UpdateStubZones() log.Printf("skydns: stubzone update") duration = 1 * time.Second // reset } } }() } if err := metrics.Metrics(); err != nil { log.Fatalf("skydns: %s", err) } else { log.Printf("skydns: metrics enabled on :%s%s", metrics.Port, metrics.Path) } if err := s.Run(); err != nil { log.Fatalf("skydns: %s", err) } } func loadConfig(client etcd.KeysAPI, config *server.Config) error { // Override what isn't set yet from the command line. configPath := "/" + msg.PathPrefix + "/config" resp, err := client.Get(ctx, configPath, nil) if err != nil { log.Printf("skydns: falling back to default configuration, could not read from etcd: %s", err) return nil } if err := json.Unmarshal([]byte(resp.Node.Value), config); err != nil { return fmt.Errorf("failed to unmarshal config: %s", err.Error()) } return nil } func validateHostPort(hostPort string) error { host, port, err := net.SplitHostPort(hostPort) if err != nil { return err } if ip := net.ParseIP(host); ip == nil { return fmt.Errorf("bad IP address: %s", host) } if p, _ := strconv.Atoi(port); p < 1 || p > 65535 { return fmt.Errorf("bad port number %s", port) } return nil } func newEtcdClient(machines []string, tlsCert, tlsKey, tlsCACert string) (etcd.KeysAPI, error) { etcdCfg := etcd.Config{ Endpoints: machines, Transport: newHTTPSTransport(tlsCert, tlsKey, tlsCACert), } cli, err := etcd.New(etcdCfg) if err != nil { return nil, err } return etcd.NewKeysAPI(cli), nil } func newHTTPSTransport(tlsCertFile, tlsKeyFile, tlsCACertFile string) etcd.CancelableTransport { var cc *tls.Config = nil if tlsCertFile != "" && tlsKeyFile != "" { var rpool *x509.CertPool if tlsCACertFile != "" { if pemBytes, err := ioutil.ReadFile(tlsCACertFile); err == nil { rpool = x509.NewCertPool() rpool.AppendCertsFromPEM(pemBytes) } } if tlsCert, err := tls.LoadX509KeyPair(tlsCertFile, tlsKeyFile); err == nil { cc = &tls.Config{ RootCAs: rpool, Certificates: []tls.Certificate{tlsCert}, InsecureSkipVerify: true, } } } tr := &http.Transport{ Proxy: http.ProxyFromEnvironment, Dial: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }).Dial, TLSHandshakeTimeout: 10 * time.Second, TLSClientConfig: cc, } return tr } skydns/metrics/000755 000765 000024 00000000000 13063661551 014176 5ustar00tpotstaff000000 000000 skydns/msg/000755 000765 000024 00000000000 12733124226 013312 5ustar00tpotstaff000000 000000 skydns/README.md000644 000765 000024 00000074071 13063661551 014020 0ustar00tpotstaff000000 000000 # SkyDNS [![Build Status](https://travis-ci.org/skynetservices/skydns.png?branch=master)](https://travis-ci.org/skynetservices/skydns) *Version 2.5.3a* SkyDNS is a distributed service for announcement and discovery of services built on top of [etcd](https://github.com/coreos/etcd). It utilizes DNS queries to discover available services. This is done by leveraging SRV records in DNS, with special meaning given to subdomains, priorities and weights. This is the original [announcement blog post](http://blog.gopheracademy.com/skydns) for version 1. Since then, SkyDNS has seen some changes, most notably the ability to use etcd as a backend. [Here you can find the SkyDNS2 announcement](http://miek.nl/posts/2014/Jun/08/announcing%20SkyDNS%20version%202/). # Changes since version 1 SkyDNS2: * Does away with Raft and uses etcd (which uses raft). * Makes it possible to query arbitrary domain names. * Is a thin layer above etcd, that translates etcd keys and values to the DNS. * Does DNSSEC with NSEC3 instead of NSEC. Note that bugs in SkyDNS1 will still be fixed, but the main development effort will be focussed on version 2. [Version 1 of SkyDNS can be found here](https://github.com/skynetservices/skydns1). ## Setup / Install Download/compile and run etcd. See the documentation for etcd at . Then get and compile SkyDNS: go get github.com/skynetservices/skydns cd $GOPATH/src/github.com/skynetservices/skydns go build -v SkyDNS' configuration is stored *in* etcd: but there are also flags and environment variables you can set. To start SkyDNS, set the etcd machines with the environment variable ETCD_MACHINES: export ETCD_MACHINES='http://192.168.0.1:4001,http://192.168.0.2:4001' ./skydns If `ETCD_MACHINES` is not set, SkyDNS will default to using `http://127.0.0.1:4001` to connect to etcd. Or you can use the flag `-machines`. Auto-discovering new machines added to the network can be enabled by enabling the flag `-discover`. Optionally (but recommended) give it a nameserver: curl -XPUT http://127.0.0.1:4001/v2/keys/skydns/local/skydns/dns/ns/ns1 \ -d value='{"host":"192.168.0.1"}' Also see the section "NS Records". ## Configuration SkyDNS' configuration is stored in etcd as a JSON object under the key `/skydns/config`. The following parameters may be set: * `dns_addr`: IP:port on which SkyDNS should listen, defaults to `127.0.0.1:53`. * `domain`: domain for which SkyDNS is authoritative, defaults to `skydns.local.`. * `dnssec`: enable DNSSEC * `hostmaster`: hostmaster email address to use. * `local`: optional unique value for this skydns instance, default is none. This is returned when queried for `local.dns.skydns.local`. * `round_robin`: enable round-robin sorting for A and AAAA responses, defaults to true. Note that packets containing more than one CNAME are exempt from this (see issue #128 on Github). * `nameservers`: forward DNS requests to these (recursive) nameservers (array of IP:port combination), when not authoritative for a domain. This defaults to the servers listed in `/etc/resolv.conf`. Also see `no_rec`. * `no_rec`: never (ever) provide a recursive service (i.e. forward to the servers provided in -nameservers). * `read_timeout`: network read timeout, for DNS and talking with etcd. * `ttl`: default TTL in seconds to use on replies when none is set in etcd, defaults to 3600. * `min_ttl`: minimum TTL in seconds to use on NXDOMAIN, defaults to 30. * `scache`: the capacity of the DNSSEC signature cache, defaults to 10000 signatures if not set. * `rcache`: the capacity of the response cache, defaults to 0 messages if not set. * `rcache_ttl`: the TTL of the response cache, defaults to 60 if not set. * `systemd`: bind to socket(s) activated by systemd (ignores -addr). * `path-prefix`: backend(etcd) path prefix, defaults to skydns (i.e. if it is set to `mydns`, the SkyDNS's configuration object should be stored under the key `/mydns/config`). To set the configuration, use something like: curl -XPUT http://127.0.0.1:4001/v2/keys/skydns/config \ -d value='{"dns_addr":"127.0.0.1:5354","ttl":3600, "nameservers": ["8.8.8.8:53","8.8.4.4:53"]}' SkyDNS needs to be restarted for configuration changes to take effect. This might change, so that SkyDNS can re-read the config from etcd after a HUP signal. You can also use the command line options, however the settings in etcd take precedence. ### Commandline flags * `-addr`: used to specify the address to listen on (note: this will be changed into `-dns_addr` to match the json. * `-local`: used to specify a unique service for this SkyDNS instance. This should point to a (unique) domain into etcd, when SkyDNS receives a query for the name `local.dns.skydns.local` it will fetch this service and return it. For instance: `-local e2016c14-fbba-11e3-ae08-10604b7efbe2.dockerhosts.skydns.local` and then curl -XPUT http://127.0.0.1:4001/v2/keys/skydns/local/skydns/dockerhosts/e2016c14-fbba-11e3-ae08-10604b7efbe2 \ -d value='{"host":"10.1.1.16"}' To register the local IP address. Now when SkyDNS receives a query for local.dns.skydns.local it will fetch the above key and returns that one service. In other words skydns will substitute `e2016c14-fbba-11e3-ae08-10604b7efbe2.dockerhosts.skydns.local` for `local.dns.skydns.local`. This follows the same rules as the other services, so it can also be an external names, which will be resolved. Also see the section Host Local Values. ### Environment Variables SkyDNS uses these environment variables: * `ETCD_MACHINES` - list of etcd machines, "http://localhost:4001,http://etcd.example.com:4001". Overwrite with `-machines` string flag. * `ETCD_TLSKEY` - path of TLS client certificate - private key. Overwrite with `-tls-key` string flag. * `ETCD_TLSPEM` - path of TLS client certificate - public key. Overwrite with `-tls-pem` string flag. * `ETCD_CACERT` - path of TLS certificate authority public key. Overwrite with `-ca-cert` string flag. * `SKYDNS_ADDR` - specify address to bind to. Overwrite with `-addr` string flag. * `SKYDNS_DOMAIN` - set a default domain if not specified by etcd config. Overwrite with `-domain` string flag. * `SKYDNS_NAMESERVERS` - set a list of nameservers to forward DNS requests to when not authoritative for a domain, "8.8.8.8:53,8.8.4.4:53". Overwrite with `-nameservers` string flag. * `SKYDNS_PATH_PREFIX` - backend(etcd) path prefix, defaults to skydns (i.e. if it is set to `mydns`, the SkyDNS's configuration object should be stored under the key `/mydns/config`). Overwrite with `-path-prefix` string flag. * `SKYDNS_SYSTEMD`: set to `true` to bind to socket(s) activated by systemd (ignores SKYDNS_ADDR). Overwrite with `-systemd` bool flag. For [Prometheus](http://prometheus.io/) the following environment variables are available: * `PROMETHEUS_PORT`: port where the HTTP server for prometheus will run. * `PROMETHEUS_PATH`: path for the metrics, defaults to `/metrics`. * `PROMETHEUS_NAMESPACE`: namespace used in the metrics, no default. * `PROMETHEUS_SUBSYSTEM`: subsystem used in the metric, defaults to `skydns`. If `PROMETHEUS_PORT` is set to an integer larger than 0, Prometheus support will be enabled. Current counters are: * `dns_request_count_total`, total count of request made against SkyDNS. * `dns_request_duration_seconds`, duration of the request handling in seconds. * `dns_response_size_bytes`, size of the repsonses in bytes. * `dns_error_count_total`, total count of responses containing errors. * `dns_cachemiss_count_total`, total count of cache misses. ### SSL Usage and Authentication with Client Certificates In order to connect to an SSL-secured etcd, you will at least need to set ETCD_CACERT to be the public key of the Certificate Authority which signed the server certificate. If the SSL-secured etcd expects client certificates to authorize connections, you also need to set ETCD_TLSKEY to the *private* key of the client, and ETCD_TLSPEM to the *public* key of the client. ## Service Announcements Announce your service by submitting JSON over HTTP to etcd with information about your service. This information will then be available for queries via DNS. We use the directory `/skydns` to anchor all names. When providing information you will need to fill out (some of) the following values. * Path - The path of the key in etcd, e.g. if the domain you want to register is "rails.production.east.skydns.local", you need to reverse it and replace the dots with slashes. So the name here becomes: `local/skydns/east/production/rails`. Then prefix the `/skydns/` string too, so the final path becomes `/v2/keys/skydns/local/skydns/east/production/rails` * Host - The name of your service, e.g., `service5.mydomain.com` or an IP address (either v4 or v6); * Port - the port where the service can be reached; * Priority - the priority of the service, the lower the value, the more preferred; * Weight - a weight factor that will be used for services with the same Priority; * Text - text you want to add (this returned when doing a TXT query); * TTL - the time-to-live of the service, overriding the default TTL. If the etcd key also has a TTL, the minimum of this value and the etcd TTL is used. * TargetStrip - when synthesising a name for an IP only SRV record, take the path name and strip `TargetStrip` labels from the ride hand side. * Group - limit recursion and only return services that share the Group's value. Path is the only mandatory field. The lookups into Etcd will be done with a *lower* cased path name. Adding the service can thus be done with: curl -XPUT http://127.0.0.1:4001/v2/keys/skydns/local/skydns/east/production/rails \ -d value='{"host":"service5.example.com","priority":20}' Or with [`etcdctl`](https://github.com/coreos/etcdctl): etcdctl set /skydns/local/skydns/east/production/rails \ '{"host":"service5.example.com","priority":20}' When doing a SRV query for these keys an SRV record is returned with the priority and a certain weight. The weight of a service is calculated as follows. We treat weight as a percentage, so if there are 3 services, the weight is set to 33 for each: | Service | Weight | SRV.Weight | | --------| ------- | ---------- | | a | 100 | 33 | | b | 100 | 33 | | c | 100 | 33 | If we add other weights to the equation some services will get a different Weight: | Service | Weight | SRV.Weight | | --------| ------- | ---------- | | a | 120 | 34 | | b | 100 | 28 | | c | 130 | 37 | Note, all calculations are rounded down, so the sum total might be lower than 100. When querying the DNS for services you can use wildcards or query for subdomains. See the section named "Wildcards" below for more information. ## Service Discovery via the DNS You can find services by querying SkyDNS via any DNS client or utility. It uses a known domain syntax with subdomains to find matching services. For the purpose of this document, let's suppose we have added the following services to etcd: * 1.rails.production.east.skydns.local, mapping to service1.example.com * 2.rails.production.west.skydns.local, mapping to service2.example.com * 4.rails.staging.east.skydns.local, mapping to 10.0.1.125 * 6.rails.staging.east.skydns.local, mapping to 2003::8:1 These names can be added with: curl -XPUT http://127.0.0.1:4001/v2/keys/skydns/local/skydns/east/production/rails/1 \ -d value='{"host":"service1.example.com","port":8080}' curl -XPUT http://127.0.0.1:4001/v2/keys/skydns/local/skydns/west/production/rails/2 \ -d value='{"host":"service2.example.com","port":8080}' curl -XPUT http://127.0.0.1:4001/v2/keys/skydns/local/skydns/east/staging/rails/4 \ -d value='{"host":"10.0.1.125","port":8080}' curl -XPUT http://127.0.0.1:4001/v2/keys/skydns/local/skydns/east/staging/rails/6 \ -d value='{"host":"2003::8:1","port":8080}' Testing one of the names with `dig`: % dig @localhost SRV 1.rails.production.east.skydns.local ;; ANSWER SECTION: 1.rails.production.east.skydns.local. 3600 IN SRV 10 0 8080 service1.example.com. ### Wildcards Of course using the full names isn't *that* useful, so SkyDNS lets you query for subdomains, and returns responses based upon the amount of services matched by the subdomain or from the wildcard query. If we are interested in all the servers in the `east` region, we simply omit the rightmost labels from our query: % dig @localhost SRV east.skydns.local ;; ANSWER SECTION: east.skydns.local. 3600 IN SRV 10 20 8080 service1.example.com. east.skydns.local. 3600 IN SRV 10 20 8080 4.rails.staging.east.skydns.local. east.skydns.local. 3600 IN SRV 10 20 8080 6.rails.staging.east.skydns.local. ;; ADDITIONAL SECTION: 4.rails.staging.east.skydns.local. 3600 IN A 10.0.1.125 6.rails.staging.east.skydns.local. 3600 IN AAAA 2003::8:1 Here all three entries of the `east` are returned. There is one other feature at play here. The second and third names, `{4,6}.rails.staging.east.skydns.local`, only had an IP record configured. Here SkyDNS used the etcd path (also see `TargetStrip`) to construct a target name and then puts the actual IP address in the additional section. Directly querying for the A records of `4.rails.staging.east.skydns.local.` of course also works: % dig @localhost -p 5354 +noall +answer A 4.rails.staging.east.skydns.local. 4.rails.staging.east.skydns.local. 3600 IN A 10.0.1.125 Another way to leads to the same result it to query for `*.east.skydns.local`, you even put the wildcard (the `*` or `any`) in the middle of a name `staging.*.skydns.local` or `staging.any.skydns.local` is a valid query, which returns all name in staging, regardless of the region. Multiple wildcards per name are also permitted. Note that `any` is synonymous for a `*`, as shown above. ### Examples Now we can try some of our example DNS lookups: #### SRV Records Get all Services in staging.east: % dig @localhost staging.east.skydns.local. SRV ;; ANSWER SECTION: staging.east.skydns.local. 3600 IN SRV 10 50 8080 4.rails.staging.east.skydns.local. staging.east.skydns.local. 3600 IN SRV 10 50 8080 6.rails.staging.east.skydns.local. ;; ADDITIONAL SECTION: 4.rails.staging.east.skydns.local. 3600 IN A 10.0.1.125 6.rails.staging.east.skydns.local. 3600 IN AAAA 2003::8:1 If you ask for a service who's Host value is an IP address you would (in theory) get back a SRV record such as: % dig @localhost 4.rails.staging.east.skydns.local SRV ;; ANSWER SECTION: 4.rails.staging.east.skydns.local 3600 IN SRV 10 100 8080 10.0.1.125 Where the target of the SRV is an IP address. This is not how SRV records work. SkyDNS will in this case synthesize a domain name and add the actual IP address to the additional section of the response: % dig @localhost 4.rails.staging.east.skydns.local SRV ;; ANSWER SECTION: 4.rails.staging.east.skydns.local 3600 IN SRV 10 100 4.rails.staging.east.skydns.local. ;; ADDITIONAL SECTION: 4.rails.staging.east.skydns.local. 3600 IN A 10.0.1.125 Which conveys the same information and is legal in the DNS. To have some control on how the target names look you can register a service with `TargetStrip` set to a non-zero value. Setting TargetStrip to "2" strips 2 labels from the generated target name: ;; ANSWER SECTION: 4.rails.staging.east.skydns.local 3600 IN SRV 10 100 staging.east.skydns.local. ;; ADDITIONAL SECTION: staging.east.skydns.local. 3600 IN A 10.0.1.125 Which removed the `4.rails` from the target name. #### A/AAAA Records To return A records, simply run a normal DNS query for a service matching the above patterns. Now do a normal DNS query: % dig @localhost staging.east.skydns.local. A ;; ANSWER SECTION: staging.east.skydns.local. 3600 IN A 10.0.1.125 Now you have a list of all known IP Addresses registered running in staging in the east area. Because we're returning A records and not SRV records, there are no ports listed, so this is only useful when you're querying for services running on ports known to you in advance. #### MX Records If a service is added with `"mail": true` it is *also* an MX record, the Priority doubles as the MX's Preference. #### CNAME Records If for an A or AAAA query the IP address can not be parsed, SkyDNS will try to see if there is a chain of names that will lead to an IP address. The chain can not be longer than 8. So for instance if the following services have been registered: curl -XPUT http://127.0.0.1:4001/v2/keys/skydns/local/skydns/east/production/rails/1 \ -d value='{"host":"service1.skydns.local","port":8080}' and curl -XPUT http://127.0.0.1:4001/v2/keys/skydns/local/skydns/service1 \ -d value='{"host":"10.0.2.15","port":8080}' We have created the following CNAME chain: `1.rails.production.east.skydns.local` -> `service1.skydns.local` -> `10.0.2.15`. If you then query for an A or AAAA for 1.rails.production.east.skydns.local SkyDNS returns: 1.rails.production.east.skydns.local. 3600 IN CNAME service1.skydns.local. service1.skydns.local. 3600 IN A 10.0.2.15 ##### External Names If the CNAME chains leads to a name that falls outside of the domain (i.e. does not end with `skydns.local.`), a.k.a. an external name, SkyDNS will attempt to resolve that name using the supplied nameservers. If this succeeds the reply is concatenated to the current one and send to the client. So if we register this service: curl -XPUT http://127.0.0.1:4001/v2/keys/skydns/local/skydns/east/production/rails/1 \ -d value='{"host":"www.miek.nl","port":8080}' Doing an A/AAAA query for this will lead to the following response: 1.rails.production.east.skydns.local. 3600 IN CNAME www.miek.nl. www.miek.nl. 3600 IN CNAME a.miek.nl. a.miek.nl. 3600 IN A 176.58.119.54 The first CNAME is generated from within SkyDNS, the other CNANE is returned from the remote name server. #### TXT Records SkyDNS also allows you to query for TXT records. Just register a json with the 'text' field set. #### NS Records For DNS to work properly SkyDNS needs to tell its parents its nameservers. This information is stored inside etcd, under the key `local/skydns/dns/ns`. There multiple services maybe stored. Note these services MUST use IP address, using names will not work. For instance: curl -XPUT http://127.0.0.1:4001/v2/keys/skydns/local/skydns/dns/ns/ns1 \ -d value='{"host":"172.16.0.1"}' Registers `ns1.ns.dns.skydns.local` as a nameserver with IP address 172.16.0.1: % dig @localhost NS skydns.local ;; ANSWER SECTION: skydns.local. 3600 IN NS ns1.ns.dns.skydns.local. ;; ADDITIONAL SECTION: n1.ns.dns.skydns.local. 3600 IN A 172.16.0.1 Having the nameserver(s) in etcd make sense because usually it is hard for SkyDNS to figure this out by itself, especially when running behind NAT or running on 127.0.0.1:53 and being forwarded packets IPv6 packets, etc. etc. #### PTR Records: Reverse Addresses When registering a service with an IP address only, you might also want to register the reverse (adding a hostname the address points to). In the DNS these records are called PTR records. So looking back at some of the services in the section "Service Discovery via the DNS", we register these IP only ones: 4.rails.staging.east.skydns.local. 10.0.1.125 To add the reverse of this address you need to add the DNS name that will be used when doing a reverse lookup. With `dig -x ` you can easiliy find what the reverse name should be: % dig -x 10.0.1.125 +noall +question ;125.1.0.10.in-addr.arpa. IN PTR So the name must be `125.1.0.10.in-addr.arpa` which should point to `4.rails.staging.east.skydns.local`. These can be added with the following command. Note that the IP address is reversed *again* and is actually back in its original form. curl -XPUT http://127.0.0.1:4001/v2/keys/skydns/arpa/in-addr/10/0/1/125 \ -d value='{"host":"4.rails.staging.east.skydns.local"}' If SkyDNS receives a PTR query it will check these paths and will return the contents. Note that these replies are sent with the AA (Authoritative Answer) bit *off*. If nothing is found locally the query is forwarded to the local recursor (if so configured), otherwise SERVFAIL is returned. This also works for IPv6 addresses, except that the reverse path is quite long. #### DNS Forwarding By specifying nameservers in SkyDNS's config, for instance `8.8.8.8:53,8.8.4.4:53`, you create a DNS forwarding proxy. In this case it round-robins between the two nameserver IPs mentioned. Requests for which SkyDNS isn't authoritative will be forwarded and proxied back to the client. This means that you can set SkyDNS as the primary DNS server in `/etc/resolv.conf` and use it for both service discovery and normal DNS operations. #### DNSSEC SkyDNS supports signing DNS answers, also known as DNSSEC. To use it, you need to create a DNSSEC keypair and use that in SkyDNS. For instance, if the domain for SkyDNS is `skydns.local`: % dnssec-keygen skydns.local Generating key pair............++++++ ...................................++++++ Kskydns.local.+005+49860 This creates two files with the basename `Kskydns.local.+005.49860`, one with the extension `.key` (this holds the public key) and one with the extension `.private` which holds the private key. The basename of these files should be given to SkyDNS's DNSSEC configuration option like so (together with some other options): curl -XPUT http://127.0.0.1:4001/v2/keys/skydns/config -d \ value='{"dns_addr":"127.0.0.1:5354","dnssec":"Kskydns.local.+005+55656"}' If you then query with `dig +dnssec` you will get signatures, keys and NSEC3 records returned. Authenticated denial of existence is implemented using NSEC3 white lies, see [RFC7129](http://tools.ietf.org/html/rfc7129), Appendix B. #### Host Local Values SkyDNS supports storing values which are specific for that *instance* of SkyDNS. This can be useful when you have SkyDNS running on multiple hosts, but want to store values that are specific for a single host. For example the public IP-address of the host or the IP-address on the tenant network. To do that you need to specify a unique value for that host with `-local`. A good unique value for that would be an UUID which you can generate with `uuidgen` for instance. That unique value is used as a path in etcd to store the values separately from the normal values. It is still stored in the etcd backend so a restart of SkyDNS with the same unique value will give it access to the old data. In the example here, we don't use an UUID, we use `public.addresses`: % skydns -local public.addresses.skydns.local & % curl -XPUT http://127.0.0.1:4001/v2/keys/skydns/local/skydns/local/addresses/public \ -d value='{"host":"192.0.2.1"}' % dig @127.0.0.1 local.dns.skydns.local. A ;; ANSWER SECTION: local.dns.skydns.local. 3600 IN A 192.0.2.1 The name `local.dns.skydns.local.` is fixed, i.e. you can retrieve the Host Local Value by querying for `local.dns.`. #### Groups Groups can be used to group set of services together. The main use of this is to limit recursion, i.e. don't give back *all* records, but only a subset. Say that I have configuration like this: /skydns/local/domain/ /skydns/local/domain/a - {"host": "127.0.0.1", "group": "g1"} /skydns/local/domain/b - {"host": "127.0.0.2", "group": "g1"} /skydns/local/domain/subdom/ /skydns/local/domain/subdom/c - {"host": "127.0.0.3", "group": "g2"} /skydns/local/domain/subdom/d - {"host": "127.0.0.4", "group": "g2"} And you want `domain.local` to return (127.0.0.1 and 127.0.0.2) and `subdom.domain.local` to return (127.0.0.3 and 127.0.0.4). For this the two domains, need to be in different groups. What those groups are does not matter, as long as `a` and `b` belong to the same group which is *different* from the group `c` and `d` belong to. If a service is found *without* a group it is *always included*. ## Implementing a custom DNS backend The SkyDNS `server` package may be used as a library, which allows a custom record retrieval implementation (referred to as a `Backend`) to be provided. The default Etcd implementation resides under `backends/etcd/etcd.go`. To provide your own backend implementation, you must implement the `server.Backend` interface. If you want to preserve the ability to answer arbitrary queries from etcd, but use your custom implementation for certain subsets of the namespace, the `server.FirstBackend` helper type will allow you to chain multiple `Backends` in order. The first backend that answers a `Records` or `ReverseRecord` call with a record and with no error will be served. ## Stub Zones Stub Zones are pointers that point to *another set* of servers which should provide an answer for the current query. This is similar to the (recursive) forwarding SkyDNS does, but different in that you need to specify a domain name and a set of authoritative servers. Also this can be dynamically controlled by writing values into Etcd. Note, that when enabled SkyDNS will *first* consult the stub configuration, potentially bypassing any configured local records. The stub zone configuration lives under `stub.dns.skydns.local.`. The following example shows on how to set this up. Suppose we want to create a stub zone for `skydns.com` and point to the nameservers reachable by following address *and* (optional) ports: * 172.16.1.1, port 54 * 10.10.244.1, port 53 (53 is the default that will be used if there isn't one specified) We should then register 2 services under the name `skydns.com.stub.dns.skydns.local` % curl -XPUT http://127.0.0.1:4001/v2/keys/skydns/local/skydns/dns/stub/com/skydns/ns1 \ -d value='{"host":"172.16.1.1", "port":54}' % curl -XPUT http://127.0.0.1:4001/v2/keys/skydns/local/skydns/dns/stub/com/skydns/ns2 \ -d value='{"host":"10.10.244.1"}' So the *leaves* should have the nameserver information. xxx..stub.dns.skydns.local | | v | nameservers | v stub domain name When SkyDNS receives a query for `skydns.com` it will *not* forward it to the recursors, but instead will query 172.16.1.1 on port 54 and if that fails will query 10.10.244.1 (on 53) to get an answer. That answer will then be given back to the original client. When forwarding to a stub, SkyDNS adds a EDNS0 meta data RR to the packet telling the remote server (if its a SkyDNS instance) that this is a stub request. SkyDNS will not (stub)forward packets with this EDNS0 meta data, instead the request will be dropped and logged. Remember this will only work when SkyDNS is started with `-stubzones`. ## How Do I Create an Address Pool and Round Robin Between Them You have 3 machines with 3 different IP addresses and you want to have 1 name pointing to all 3 possible addresses. The name we want to use is: `db.skydns.local` and the 3 addresses are 127.0.0.{1,2,3}. For this to work we create the hosts named `x{1,2,3}.db.skydns.local` in etcd: curl -XPUT http://127.0.0.1:4001/v2/keys/skydns/local/skydns/db/x1 -d \ value='{"host":"127.0.0.1"}' curl -XPUT http://127.0.0.1:4001/v2/keys/skydns/local/skydns/db/x2 -d \ value='{"host": "127.0.0.2"'} curl -XPUT http://127.0.0.1:4001/v2/keys/skydns/local/skydns/db/x3 -d \ value='{"host": "127.0.0.3"'} Now the name `db.skydns.local` is the "load balanced" name for the database, SkyDNS will round-robin by default in this case unless `-round-robin=false` is enabled. ## How I Do Create Multiple SRV Records For the Same Name You want this response from SkyDNS, which says there are 2 open ports on bar.skydns.local and this name has IP addres 192.168.0.1: ;; ANSWER SECTION: bar.skydns.local. 3600 IN SRV 10 50 80 bar.skydns.local. bar.skydns.local. 3600 IN SRV 10 50 443 bar.skydns.local. ;; ADDITIONAL SECTION: bar.skydns.local. 3600 IN A 192.168.0.1 So you register a "dummy" host named `x1`: curl -XPUT http://127.0.0.1:4001/v2/keys/skydns/local/skydns/bar/x1 -d \ value='{"host":"192.168.0.1","port":80}' curl -XPUT http://127.0.0.1:4001/v2/keys/skydns/local/skydns/bar/x2 -d \ value='{"host": "bar.skydns.local","port":443}' And try it out: ;; ANSWER SECTION: bar.skydns.local. 3600 IN SRV 10 50 80 x1.bar.skydns.local. bar.skydns.local. 3600 IN SRV 10 50 443 bar.skydns.local. ;; ADDITIONAL SECTION: x1.bar.skydns.local. 3600 IN A 192.168.0.1 Which has `x1` in the name, which is not the name you wanted to see there, and worse does not match the name in the other SRV record. To makes this work you'll need `TargetStrip` which allows you to tell SkyDNS to strip labels from the name it makes up: curl -XPUT http://127.0.0.1:4001/v2/keys/skydns/local/skydns/bar/x1 -d \ value='{"host":"192.168.0.1","port":80,"targetstrip":1}' % dig @127.0.0.1 bar.skydns.local. SRV ;; ANSWER SECTION: bar.skydns.local. 3600 IN SRV 10 50 80 bar.skydns.local. bar.skydns.local. 3600 IN SRV 10 50 443 bar.skydns.local. ;; ADDITIONAL SECTION: bar.skydns.local. 3600 IN A 192.168.0.1 ## How do you limit recursion? By default SkyDNS will returns *all* records under a name. Suppose you want we have `bar.skydns.local`... TODO. # Docker Official Docker images are at the [Docker Hub](https://registry.hub.docker.com/u/skynetservices/skydns/): * master -> skynetservices/skydns:latest * latest tag -> skynetservices/skydns:latest-tagged The supplied `Dockerfile` can be used to build an image as well. Note that the image is based of Alpine Linux which used musl libc instead of glibc, so when building SkyDNS you must make sure if does not need glibc when run: Build SkyDNS with: % GOOS=linux go build -a -tags netgo -installsuffix netgo And then build the docker image: % docker build -t $USER/skydns . If you run it, SkyDNS needs to access Etcd (or whatever backend), which usually runs on the host server (i.e. when using CoreOS), to make that work, just run: docker run --net host skydns/server/000755 000765 000024 00000000000 13063661551 014036 5ustar00tpotstaff000000 000000 skydns/singleflight/000755 000765 000024 00000000000 13063661551 015207 5ustar00tpotstaff000000 000000 skydns/singleflight/singleflight.go000644 000765 000024 00000003222 13063661551 020214 0ustar00tpotstaff000000 000000 /* Copyright 2012 Google Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package singleflight provides a duplicate function call suppression // mechanism. package singleflight import "sync" // call is an in-flight or completed Do call type call struct { wg sync.WaitGroup val interface{} err error } // Group represents a class of work and forms a namespace in which // units of work can be executed with duplicate suppression. type Group struct { mu sync.Mutex // protects m m map[string]*call // lazily initialized } // Do executes and returns the results of the given function, making // sure that only one execution is in-flight for a given key at a // time. If a duplicate comes in, the duplicate caller waits for the // original to complete and receives the same results. func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) { g.mu.Lock() if g.m == nil { g.m = make(map[string]*call) } if c, ok := g.m[key]; ok { g.mu.Unlock() c.wg.Wait() return c.val, c.err } c := new(call) c.wg.Add(1) g.m[key] = c g.mu.Unlock() c.val, c.err = fn() c.wg.Done() g.mu.Lock() delete(g.m, key) g.mu.Unlock() return c.val, c.err } skydns/server/backend.go000644 000765 000024 00000002445 13063661551 015761 0ustar00tpotstaff000000 000000 // Copyright (c) 2014 The SkyDNS Authors. All rights reserved. // Use of this source code is governed by The MIT License (MIT) that can be // found in the LICENSE file. package server import "github.com/skynetservices/skydns/msg" type Backend interface { Records(name string, exact bool) ([]msg.Service, error) ReverseRecord(name string) (*msg.Service, error) } // FirstBackend exposes the Backend interface over multiple Backends, returning // the first Backend that answers the provided record request. If no Backend answers // a record request, the last error seen will be returned. type FirstBackend []Backend // FirstBackend implements Backend var _ Backend = FirstBackend{} func (g FirstBackend) Records(name string, exact bool) (records []msg.Service, err error) { var lastError error for _, backend := range g { if records, err = backend.Records(name, exact); err == nil && len(records) > 0 { return records, nil } if err != nil { lastError = err } } return nil, lastError } func (g FirstBackend) ReverseRecord(name string) (record *msg.Service, err error) { var lastError error for _, backend := range g { if record, err = backend.ReverseRecord(name); err == nil && record != nil { return record, nil } if err != nil { lastError = err } } return nil, lastError } skydns/server/cache_test.go000644 000765 000024 00000003447 12733124226 016473 0ustar00tpotstaff000000 000000 // Copyright (c) 2014 The SkyDNS Authors. All rights reserved. // Use of this source code is governed by The MIT License (MIT) that can be // found in the LICENSE file. package server import ( "testing" "github.com/miekg/dns" "github.com/skynetservices/skydns/cache" ) func TestFit(t *testing.T) { m := new(dns.Msg) m.SetQuestion("miek.nl", dns.TypeA) rr, _ := dns.NewRR("www.miek.nl. IN SRV 10 10 8080 blaat.miek.nl.") for i := 0; i < 101; i++ { m.Answer = append(m.Answer, rr) } // Uncompresses length is now 4424. Try trimming this to 1927 Fit(m, 1927, true) if m.Len() > 1927 { t.Fatalf("failed to fix message, expected < %d, got %d", 1927, m.Len()) } } func TestCacheTruncated(t *testing.T) { s := newTestServer(t, true) m := &dns.Msg{} m.SetQuestion("skydns.test.", dns.TypeSRV) m.Truncated = true s.rcache.InsertMessage(cache.Key(m.Question[0], false, false), m) // Now asking for this should result in a non-truncated answer. resp, _ := dns.Exchange(m, "127.0.0.1:"+StrPort) if resp.Truncated { t.Fatal("truncated bit should be false") } } // Store a large message in the cache, then query with a smaller bufsize and check // we get back a smaller message. // TODO(miek). /* func testCacheStoreLarge(t *testing.T) { s := newTestServer(t, true) defer s.Stop() c := new(dns.Client) m := new(dns.Msg) for i := 0; i < 2000; i++ { is := strconv.Itoa(i) m := &msg.Service{ Host: "2001::" + is, Key: "machine" + is + ".machines.skydns.test.", } addService(t, s, m.Key, 0, m) defer delService(t, s, m.Key) } m.SetQuestion("machines.skydns.test.", dns.TypeSRV) resp, _, err := c.Exchange(m, "127.0.0.1:"+StrPort) if err != nil { t.Fatal(err) } t.Logf("%s", resp) if resp.Rcode != dns.RcodeSuccess { t.Fatalf("expecting server failure, got %d", resp.Rcode) } } */ skydns/server/config.go000644 000765 000024 00000011432 13063661551 015633 0ustar00tpotstaff000000 000000 // Copyright (c) 2014 The SkyDNS Authors. All rights reserved. // Use of this source code is governed by The MIT License (MIT) that can be // found in the LICENSE file. package server import ( "crypto" "fmt" "net" "os" "strings" "time" "github.com/miekg/dns" ) const ( SCacheCapacity = 10000 RCacheCapacity = 100000 RCacheTtl = 60 ) // Config provides options to the SkyDNS resolver. type Config struct { // The ip:port SkyDNS should be listening on for incoming DNS requests. DnsAddr string `json:"dns_addr,omitempty"` // bind to port(s) activated by systemd. If set to true, this overrides DnsAddr. Systemd bool `json:"systemd,omitempty"` // The domain SkyDNS is authoritative for, defaults to skydns.local. Domain string `json:"domain,omitempty"` // Domain pointing to a key where service info is stored when being queried // for local.dns.skydns.local. Local string `json:"local,omitempty"` // The hostmaster responsible for this domain, defaults to hostmaster.. Hostmaster string `json:"hostmaster,omitempty"` DNSSEC string `json:"dnssec,omitempty"` // Round robin A/AAAA replies. Default is true. RoundRobin bool `json:"round_robin,omitempty"` // Round robin selection of nameservers from among those listed, rather than have all forwarded requests try the first listed server first every time. NSRotate bool `json:"ns_rotate,omitempty"` // List of ip:port, separated by commas of recursive nameservers to forward queries to. Nameservers []string `json:"nameservers,omitempty"` // Never provide a recursive service. NoRec bool `json:"no_rec,omitempty"` ReadTimeout time.Duration `json:"read_timeout,omitempty"` // Default priority on SRV records when none is given. Defaults to 10. Priority uint16 `json:"priority"` // Default TTL, in seconds, when none is given in etcd. Defaults to 3600. Ttl uint32 `json:"ttl,omitempty"` // Minimum TTL, in seconds, for NXDOMAIN responses. Defaults to 300. MinTtl uint32 `json:"min_ttl,omitempty"` // SCache, capacity of the signature cache in signatures stored. SCache int `json:"scache,omitempty"` // RCache, capacity of response cache in resource records stored. RCache int `json:"rcache,omitempty"` // RCacheTtl, how long to cache in seconds. RCacheTtl int `json:"rcache_ttl,omitempty"` // How many labels a name should have before we allow forwarding. Default to 2. Ndots int `json:"ndot,omitempty"` // DNSSEC key material PubKey *dns.DNSKEY `json:"-"` KeyTag uint16 `json:"-"` PrivKey crypto.Signer `json:"-"` Verbose bool `json:"-"` Version bool // some predefined string "constants" localDomain string // "local.dns." + config.Domain dnsDomain string // "ns.dns". + config.Domain // Stub zones support. Pointer to a map that we refresh when we see // an update. Map contains domainname -> nameserver:port stub *map[string][]string } func SetDefaults(config *Config) error { if config.ReadTimeout == 0 { config.ReadTimeout = 2 * time.Second } if config.DnsAddr == "" { config.DnsAddr = "127.0.0.1:53" } if config.Domain == "" { config.Domain = "skydns.local." } if config.Hostmaster == "" { config.Hostmaster = appendDomain("hostmaster", config.Domain) } // People probably don't know that SOA's email addresses cannot // contain @-signs, replace them with dots config.Hostmaster = dns.Fqdn(strings.Replace(config.Hostmaster, "@", ".", -1)) if config.MinTtl == 0 { config.MinTtl = 60 } if config.Ttl == 0 { config.Ttl = 3600 } if config.Priority == 0 { config.Priority = 10 } if config.RCache < 0 { config.RCache = 0 } if config.SCache < 0 { config.SCache = 0 } if config.RCacheTtl == 0 { config.RCacheTtl = RCacheTtl } if config.Ndots <= 0 { config.Ndots = 2 } if len(config.Nameservers) == 0 { c, err := dns.ClientConfigFromFile("/etc/resolv.conf") if !os.IsNotExist(err) { if err != nil { return err } for _, s := range c.Servers { config.Nameservers = append(config.Nameservers, net.JoinHostPort(s, c.Port)) } } } config.Domain = dns.Fqdn(strings.ToLower(config.Domain)) if config.DNSSEC != "" { // For some reason the + are replaces by spaces in etcd. Re-replace them keyfile := strings.Replace(config.DNSSEC, " ", "+", -1) k, p, err := ParseKeyFile(keyfile) if err != nil { return err } if k.Header().Name != dns.Fqdn(config.Domain) { return fmt.Errorf("ownername of DNSKEY must match SkyDNS domain") } k.Header().Ttl = config.Ttl config.PubKey = k config.KeyTag = k.KeyTag() config.PrivKey = p } config.localDomain = appendDomain("local.dns", config.Domain) config.dnsDomain = appendDomain("ns.dns", config.Domain) stubmap := make(map[string][]string) config.stub = &stubmap return nil } func appendDomain(s1, s2 string) string { if len(s2) > 0 && s2[0] == '.' { return s1 + s2 } return s1 + "." + s2 } skydns/server/dnssec.go000644 000765 000024 00000011126 13063661551 015645 0ustar00tpotstaff000000 000000 // Copyright (c) 2013 Erik St. Martin, Brian Ketelsen. All rights reserved. // Use of this source code is governed by The MIT License (MIT) that can be // found in the LICENSE file. package server import ( "crypto" "crypto/ecdsa" "crypto/rsa" "os" "time" "github.com/skynetservices/skydns/cache" "github.com/skynetservices/skydns/metrics" "github.com/skynetservices/skydns/singleflight" "github.com/miekg/dns" ) var ( inflight = &singleflight.Group{} ) // ParseKeyFile read a DNSSEC keyfile as generated by dnssec-keygen or other // utilities. It add ".key" for the public key and ".private" for the private key. func ParseKeyFile(file string) (*dns.DNSKEY, crypto.Signer, error) { f, e := os.Open(file + ".key") if e != nil { return nil, nil, e } k, e := dns.ReadRR(f, file+".key") if e != nil { return nil, nil, e } f, e = os.Open(file + ".private") if e != nil { return nil, nil, e } p, e := k.(*dns.DNSKEY).ReadPrivateKey(f, file+".private") if e != nil { return nil, nil, e } if v, ok := p.(*rsa.PrivateKey); ok { return k.(*dns.DNSKEY), v, nil } if v, ok := p.(*ecdsa.PrivateKey); ok { return k.(*dns.DNSKEY), v, nil } return k.(*dns.DNSKEY), nil, nil } // Sign signs a message m, it takes care of negative or nodata responses as // well by synthesising NSEC3 records. It will also cache the signatures, using // a hash of the signed data as a key. // We also fake the origin TTL in the signature, because we don't want to // throw away signatures when services decide to have longer TTL. So we just // set the origTTL to 60. // TODO(miek): revisit origTTL func (s *server) Sign(m *dns.Msg, bufsize uint16) { now := time.Now().UTC() incep := uint32(now.Add(-3 * time.Hour).Unix()) // 2+1 hours, be sure to catch daylight saving time and such expir := uint32(now.Add(7 * 24 * time.Hour).Unix()) // sign for a week for _, r := range rrSets(m.Answer) { if r[0].Header().Rrtype == dns.TypeRRSIG { continue } if !dns.IsSubDomain(s.config.Domain, r[0].Header().Name) { continue } if sig, err := s.signSet(r, now, incep, expir); err == nil { m.Answer = append(m.Answer, sig) } } for _, r := range rrSets(m.Ns) { if r[0].Header().Rrtype == dns.TypeRRSIG { continue } if !dns.IsSubDomain(s.config.Domain, r[0].Header().Name) { continue } if sig, err := s.signSet(r, now, incep, expir); err == nil { m.Ns = append(m.Ns, sig) } } for _, r := range rrSets(m.Extra) { if r[0].Header().Rrtype == dns.TypeRRSIG || r[0].Header().Rrtype == dns.TypeOPT { continue } if !dns.IsSubDomain(s.config.Domain, r[0].Header().Name) { continue } if sig, err := s.signSet(r, now, incep, expir); err == nil { m.Extra = append(m.Extra, sig) } } o := new(dns.OPT) o.Hdr.Name = "." o.Hdr.Rrtype = dns.TypeOPT o.SetDo() o.SetUDPSize(4096) // TODO(miek): echo client m.Extra = append(m.Extra, o) return } func (s *server) signSet(r []dns.RR, now time.Time, incep, expir uint32) (*dns.RRSIG, error) { key := cache.KeyRRset(r) if m, exp, hit := s.scache.Search(key); hit { // There can only be one sig in this cache. // Is it still valid 24 hours from now? if now.Add(+24*time.Hour).Sub(exp) < -24*time.Hour { return m.Answer[0].(*dns.RRSIG), nil } s.scache.Remove(key) } if s.config.Verbose { logf("scache miss for %s type %d", r[0].Header().Name, r[0].Header().Rrtype) } metrics.ReportCacheMiss("signature") sig, err := inflight.Do(key, func() (interface{}, error) { sig1 := s.NewRRSIG(incep, expir) sig1.Header().Ttl = r[0].Header().Ttl if r[0].Header().Rrtype == dns.TypeTXT { sig1.OrigTtl = 0 } e := sig1.Sign(s.config.PrivKey, r) if e != nil { logf("failed to sign: %s", e.Error()) } return sig1, e }) if err != nil { return nil, err } s.scache.InsertSignature(key, sig.(*dns.RRSIG)) return dns.Copy(sig.(*dns.RRSIG)).(*dns.RRSIG), nil } func (s *server) NewRRSIG(incep, expir uint32) *dns.RRSIG { sig := new(dns.RRSIG) sig.Hdr.Rrtype = dns.TypeRRSIG sig.Hdr.Ttl = s.config.Ttl sig.OrigTtl = s.config.Ttl sig.Algorithm = s.config.PubKey.Algorithm sig.KeyTag = s.config.KeyTag sig.Inception = incep sig.Expiration = expir sig.SignerName = s.config.PubKey.Hdr.Name return sig } type rrset struct { qname string qtype uint16 } func rrSets(rrs []dns.RR) map[rrset][]dns.RR { m := make(map[rrset][]dns.RR) for _, r := range rrs { if s, ok := m[rrset{r.Header().Name, r.Header().Rrtype}]; ok { s = append(s, r) m[rrset{r.Header().Name, r.Header().Rrtype}] = s } else { s := make([]dns.RR, 1, 3) s[0] = r m[rrset{r.Header().Name, r.Header().Rrtype}] = s } } if len(m) > 0 { return m } return nil } skydns/server/doc.go000644 000765 000024 00000000611 12733124226 015124 0ustar00tpotstaff000000 000000 // Copyright (c) 2014 The SkyDNS Authors. All rights reserved. // Use of this source code is governed by The MIT License (MIT) that can be // found in the LICENSE file. // Package server provides a DNS server implementation that handles DNS // queries. To answer a query, the server asks the provided Backend for // DNS records, which are then converted to the proper answers. package server skydns/server/exchange.go000644 000765 000024 00000001750 13063661551 016152 0ustar00tpotstaff000000 000000 // Copyright (c) 2014 The SkyDNS Authors. All rights reserved. // Use of this source code is governed by The MIT License (MIT) that can be // found in the LICENSE file. package server import "github.com/miekg/dns" // exchangeMsg returns a new dns message based on name, type, bufsize and dnssec. func newExchangeMsg(name string, typ, bufsize uint16, dnssec bool) *dns.Msg { m := new(dns.Msg) m.SetQuestion(name, typ) m.SetEdns0(bufsize, dnssec) return m } // exchangeWithRetry sends message m to server, but retries on ServerFailure. func exchangeWithRetry(c *dns.Client, m *dns.Msg, server string) (*dns.Msg, error) { r, _, err := c.Exchange(m, server) if err == nil && r.Rcode == dns.RcodeServerFailure { // redo the query r, _, err = c.Exchange(m, server) } return r, err } func (s *server) randomNameserverID(id uint16) int { nsid := 0 if s.config.NSRotate { // Use request Id for "random" nameserver selection. nsid = int(id) % len(s.config.Nameservers) } return nsid } skydns/server/forwarding.go000644 000765 000024 00000007201 13063661551 016527 0ustar00tpotstaff000000 000000 // Copyright (c) 2014 The SkyDNS Authors. All rights reserved. // Use of this source code is governed by The MIT License (MIT) that can be // found in the LICENSE file. package server import ( "fmt" "github.com/miekg/dns" ) // ServeDNSForward forwards a request to a nameservers and returns the response. func (s *server) ServeDNSForward(w dns.ResponseWriter, req *dns.Msg) *dns.Msg { if s.config.NoRec { m := s.ServerFailure(req) w.WriteMsg(m) return m } if len(s.config.Nameservers) == 0 || dns.CountLabel(req.Question[0].Name) < s.config.Ndots { if s.config.Verbose { if len(s.config.Nameservers) == 0 { logf("can not forward, no nameservers defined") } else { logf("can not forward, name too short (less than %d labels): `%s'", s.config.Ndots, req.Question[0].Name) } } m := s.ServerFailure(req) m.RecursionAvailable = true // this is still true w.WriteMsg(m) return m } var ( r *dns.Msg err error ) nsid := s.randomNameserverID(req.Id) try := 0 Redo: if isTCP(w) { r, err = exchangeWithRetry(s.dnsTCPclient, req, s.config.Nameservers[nsid]) } else { r, err = exchangeWithRetry(s.dnsUDPclient, req, s.config.Nameservers[nsid]) } if err == nil { r.Compress = true r.Id = req.Id w.WriteMsg(r) return r } // Seen an error, this can only mean, "server not reached", try again // but only if we have not exausted our nameservers. if try < len(s.config.Nameservers) { try++ nsid = (nsid + 1) % len(s.config.Nameservers) goto Redo } logf("failure to forward request %q", err) m := s.ServerFailure(req) return m } // ServeDNSReverse is the handler for DNS requests for the reverse zone. If nothing is found // locally the request is forwarded to the forwarder for resolution. func (s *server) ServeDNSReverse(w dns.ResponseWriter, req *dns.Msg) *dns.Msg { m := new(dns.Msg) m.SetReply(req) m.Compress = true m.Authoritative = false // Set to false, because I don't know what to do wrt DNSSEC. m.RecursionAvailable = true var err error if m.Answer, err = s.PTRRecords(req.Question[0]); err == nil { // TODO(miek): Reverse DNSSEC. We should sign this, but requires a key....and more // Probably not worth the hassle? if err := w.WriteMsg(m); err != nil { logf("failure to return reply %q", err) } return m } // Always forward if not found locally. return s.ServeDNSForward(w, req) } // Lookup looks up name,type using the recursive nameserver defines // in the server's config. If none defined it returns an error. func (s *server) Lookup(n string, t, bufsize uint16, dnssec bool) (*dns.Msg, error) { if len(s.config.Nameservers) == 0 { return nil, fmt.Errorf("no nameservers configured can not lookup name") } if dns.CountLabel(n) < s.config.Ndots { return nil, fmt.Errorf("name has fewer than %d labels", s.config.Ndots) } m := newExchangeMsg(n, t, bufsize, dnssec) nsid := s.randomNameserverID(m.Id) try := 0 Redo: r, err := exchangeWithRetry(s.dnsUDPclient, m, s.config.Nameservers[nsid]) if err == nil { if r.Rcode != dns.RcodeSuccess { return nil, fmt.Errorf("rcode %d is not equal to success", r.Rcode) } // Reset TTLs to rcache TTL to make some of the other code // and the tests not care about TTLs for _, rr := range r.Answer { rr.Header().Ttl = uint32(s.config.RCacheTtl) } for _, rr := range r.Extra { rr.Header().Ttl = uint32(s.config.RCacheTtl) } return r, nil } // Seen an error, this can only mean, "server not reached", try again // but only if we have not exausted our nameservers. if try < len(s.config.Nameservers) { try++ nsid = (nsid + 1) % len(s.config.Nameservers) goto Redo } return nil, fmt.Errorf("failure to lookup name") } skydns/server/log.go000644 000765 000024 00000000737 12733124226 015151 0ustar00tpotstaff000000 000000 // Copyright (c) 2014 The SkyDNS Authors. All rights reserved. // Use of this source code is governed by The MIT License (MIT) that can be // found in the LICENSE file. package server import "log" // printf calls log.Printf with the parameters given. func logf(format string, a ...interface{}) { log.Printf("skydns: "+format, a...) } // fatalf calls log.Fatalf with the parameters given. func fatalf(format string, a ...interface{}) { log.Fatalf("skydns: "+format, a...) } skydns/server/metrics_test.go000644 000765 000024 00000002667 13063661551 017105 0ustar00tpotstaff000000 000000 // Copyright (c) 2014 The SkyDNS Authors. All rights reserved. // Use of this source code is governed by The MIT License (MIT) that can be // found in the LICENSE file. package server import ( "bytes" "io/ioutil" "net/http" "strconv" "testing" "github.com/skynetservices/skydns/metrics" "github.com/miekg/dns" ) func query(n string, t uint16) { m := new(dns.Msg) m.SetQuestion(n, t) dns.Exchange(m, "127.0.0.1:"+StrPort) } func scrape(t *testing.T, key string) int { resp, err := http.Get("http://localhost:12300/metrics") if err != nil { t.Logf("could not get metrics") return -1 } body, err := ioutil.ReadAll(resp.Body) if err != nil { return -1 } // Find value for key. n := bytes.Index(body, []byte(key)) if n == -1 { return -1 } i := n for i < len(body) { if body[i] == '\n' { break } if body[i] == ' ' { n = i + 1 } i++ } value, err := strconv.Atoi(string(body[n:i])) if err != nil { t.Fatal("failed to get value") } return value } func TestMetrics(t *testing.T) { s := newTestServer(t, false) defer s.Stop() metrics.Port = "12300" metrics.Subsystem = "test" metrics.Namespace = "test" metrics.Metrics() query("miek.nl.", dns.TypeMX) v0 := scrape(t, "test_test_dns_request_count_total{system=\"recursive\"}") query("miek.nl.", dns.TypeMX) v1 := scrape(t, "test_test_dns_request_count_total{system=\"recursive\"}") if v1 != v0+1 { t.Fatalf("expecting %d, got %d", v0+1, v1) } } skydns/server/msg.go000644 000765 000024 00000002467 12733124226 015160 0ustar00tpotstaff000000 000000 // Copyright (c) 2014 The SkyDNS Authors. All rights reserved. // Use of this source code is governed by The MIT License (MIT) that can be // found in the LICENSE file. package server import "github.com/miekg/dns" // Fit will make m fit the size. If a message is larger than size then entire // additional section is dropped. If it is still to large and the transport // is udp we return a truncated message. // If the transport is tcp we are going to drop RR from the answer section // until it fits. When this is case the returned bool is true. func Fit(m *dns.Msg, size int, tcp bool) (*dns.Msg, bool) { if m.Len() > size { // Check for OPT Records at the end and keep those. TODO(miek) m.Extra = nil } if m.Len() < size { return m, false } // With TCP setting TC does not mean anything. if !tcp { m.Truncated = true // fall through here, so we at least return a message that can // fit the udp buffer. } // Additional section is gone, binary search until we have length that fits. min, max := 0, len(m.Answer) original := make([]dns.RR, len(m.Answer)) copy(original, m.Answer) for { if min == max { break } mid := (min + max) / 2 m.Answer = original[:mid] if m.Len() < size { min++ continue } max = mid } if max > 1 { max-- } m.Answer = m.Answer[:max] return m, true } skydns/server/nsec3.go000644 000765 000024 00000010202 12733124226 015367 0ustar00tpotstaff000000 000000 // Copyright (c) 2013 Erik St. Martin, Brian Ketelsen. All rights reserved. // Use of this source code is governed by The MIT License (MIT) that can be // found in the LICENSE file. package server import ( "encoding/base32" "strings" "github.com/miekg/dns" ) // Do DNSSEC NXDOMAIN with NSEC3 whitelies: rfc 7129, appendix B. // The closest encloser will be qname - the left most label and the // next closer will be the full qname which we then will deny. // Idem for source of synthesis. func (s *server) Denial(m *dns.Msg) { if m.Rcode == dns.RcodeNameError { // ce is qname minus the left label idx := dns.Split(m.Question[0].Name) ce := m.Question[0].Name[idx[1]:] nsec3ce, nsec3wildcard := newNSEC3CEandWildcard(s.config.Domain, ce, s.config.MinTtl) // Add ce and wildcard m.Ns = append(m.Ns, nsec3ce) m.Ns = append(m.Ns, nsec3wildcard) // Deny Qname nsec3 m.Ns = append(m.Ns, s.newNSEC3NameError(m.Question[0].Name)) } if m.Rcode == dns.RcodeSuccess && len(m.Ns) == 1 { // NODATA if _, ok := m.Ns[0].(*dns.SOA); ok { m.Ns = append(m.Ns, s.newNSEC3NoData(m.Question[0].Name)) } } } func packBase32(s string) []byte { b32len := base32.HexEncoding.DecodedLen(len(s)) buf := make([]byte, b32len) n, _ := base32.HexEncoding.Decode(buf, []byte(s)) buf = buf[:n] return buf } func unpackBase32(b []byte) string { b32 := make([]byte, base32.HexEncoding.EncodedLen(len(b))) base32.HexEncoding.Encode(b32, b) return string(b32) } // newNSEC3NameError returns the NSEC3 record needed to denial qname. func (s *server) newNSEC3NameError(qname string) *dns.NSEC3 { n := new(dns.NSEC3) n.Hdr.Class = dns.ClassINET n.Hdr.Rrtype = dns.TypeNSEC3 n.Hdr.Ttl = s.config.MinTtl n.Hash = dns.SHA1 n.Flags = 0 n.Salt = "" n.TypeBitMap = []uint16{} covername := dns.HashName(qname, dns.SHA1, 0, "") buf := packBase32(covername) byteArith(buf, false) // one before n.Hdr.Name = appendDomain(strings.ToLower(unpackBase32(buf)), s.config.Domain) byteArith(buf, true) // one next byteArith(buf, true) // and another one n.NextDomain = unpackBase32(buf) return n } // newNSEC3NoData returns the NSEC3 record needed to denial the types func (s *server) newNSEC3NoData(qname string) *dns.NSEC3 { n := new(dns.NSEC3) n.Hdr.Class = dns.ClassINET n.Hdr.Rrtype = dns.TypeNSEC3 n.Hdr.Ttl = s.config.MinTtl n.Hash = dns.SHA1 n.Flags = 0 n.Salt = "" n.TypeBitMap = []uint16{dns.TypeA, dns.TypeAAAA, dns.TypeSRV, dns.TypeRRSIG} n.Hdr.Name = dns.HashName(qname, dns.SHA1, 0, "") buf := packBase32(n.Hdr.Name) byteArith(buf, true) // one next n.NextDomain = unpackBase32(buf) n.Hdr.Name += appendDomain("", s.config.Domain) return n } // newNSEC3CEandWildcard returns the NSEC3 for the closest encloser // and the NSEC3 that denies that wildcard at that level. func newNSEC3CEandWildcard(apex, ce string, ttl uint32) (*dns.NSEC3, *dns.NSEC3) { n1 := new(dns.NSEC3) n1.Hdr.Class = dns.ClassINET n1.Hdr.Rrtype = dns.TypeNSEC3 n1.Hdr.Ttl = ttl n1.Hash = dns.SHA1 n1.Flags = 0 n1.Iterations = 0 n1.Salt = "" // for the apex we need another bitmap n1.TypeBitMap = []uint16{dns.TypeA, dns.TypeAAAA, dns.TypeSRV, dns.TypeRRSIG} prev := dns.HashName(ce, dns.SHA1, n1.Iterations, n1.Salt) n1.Hdr.Name = strings.ToLower(prev) + "." + apex buf := packBase32(prev) byteArith(buf, true) // one next n1.NextDomain = unpackBase32(buf) n2 := new(dns.NSEC3) n2.Hdr.Class = dns.ClassINET n2.Hdr.Rrtype = dns.TypeNSEC3 n2.Hdr.Ttl = ttl n2.Hash = dns.SHA1 n2.Flags = 0 n2.Iterations = 0 n2.Salt = "" prev = dns.HashName("*."+ce, dns.SHA1, n2.Iterations, n2.Salt) buf = packBase32(prev) byteArith(buf, false) // one before n2.Hdr.Name = appendDomain(strings.ToLower(unpackBase32(buf)), apex) byteArith(buf, true) // one next byteArith(buf, true) // and another one n2.NextDomain = unpackBase32(buf) return n1, n2 } // byteArith adds either 1 or -1 to b, there is no check for under- or overflow. func byteArith(b []byte, x bool) { if x { for i := len(b) - 1; i >= 0; i-- { if b[i] == 255 { b[i] = 0 continue } b[i]++ return } } for i := len(b) - 1; i >= 0; i-- { if b[i] == 0 { b[i] = 255 continue } b[i]-- return } } skydns/server/server.go000644 000765 000024 00000056415 13063661551 015706 0ustar00tpotstaff000000 000000 // Copyright (c) 2014 The SkyDNS Authors. All rights reserved. // Use of this source code is governed by The MIT License (MIT) that can be // found in the LICENSE file. package server import ( "fmt" "math" "net" "strconv" "strings" "sync" "time" "github.com/skynetservices/skydns/cache" "github.com/skynetservices/skydns/metrics" "github.com/skynetservices/skydns/msg" etcd "github.com/coreos/etcd/client" "github.com/coreos/go-systemd/activation" "github.com/miekg/dns" ) const Version = "2.5.3a" type server struct { backend Backend config *Config group *sync.WaitGroup dnsUDPclient *dns.Client // used for forwarding queries dnsTCPclient *dns.Client // used for forwarding queries scache *cache.Cache rcache *cache.Cache } // New returns a new SkyDNS server. func New(backend Backend, config *Config) *server { return &server{ backend: backend, config: config, group: new(sync.WaitGroup), scache: cache.New(config.SCache, 0), rcache: cache.New(config.RCache, config.RCacheTtl), dnsUDPclient: &dns.Client{Net: "udp", ReadTimeout: config.ReadTimeout, WriteTimeout: config.ReadTimeout, SingleInflight: true}, dnsTCPclient: &dns.Client{Net: "tcp", ReadTimeout: config.ReadTimeout, WriteTimeout: config.ReadTimeout, SingleInflight: true}, } } // Run is a blocking operation that starts the server listening on the DNS ports. func (s *server) Run() error { mux := dns.NewServeMux() mux.Handle(".", s) dnsReadyMsg := func(addr, net string) { if s.config.DNSSEC == "" { logf("ready for queries on %s for %s://%s [rcache %d]", s.config.Domain, net, addr, s.config.RCache) } else { logf("ready for queries on %s for %s://%s [rcache %d], signing with %s [scache %d]", s.config.Domain, net, addr, s.config.RCache, s.config.DNSSEC, s.config.SCache) } } if s.config.Systemd { packetConns, err := activation.PacketConns(false) if err != nil { return err } listeners, err := activation.Listeners(true) if err != nil { return err } if len(packetConns) == 0 && len(listeners) == 0 { return fmt.Errorf("no UDP or TCP sockets supplied by systemd") } for _, p := range packetConns { if u, ok := p.(*net.UDPConn); ok { s.group.Add(1) go func() { defer s.group.Done() if err := dns.ActivateAndServe(nil, u, mux); err != nil { fatalf("%s", err) } }() dnsReadyMsg(u.LocalAddr().String(), "udp") } } for _, l := range listeners { if t, ok := l.(*net.TCPListener); ok { s.group.Add(1) go func() { defer s.group.Done() if err := dns.ActivateAndServe(t, nil, mux); err != nil { fatalf("%s", err) } }() dnsReadyMsg(t.Addr().String(), "tcp") } } } else { s.group.Add(1) go func() { defer s.group.Done() if err := dns.ListenAndServe(s.config.DnsAddr, "tcp", mux); err != nil { fatalf("%s", err) } }() dnsReadyMsg(s.config.DnsAddr, "tcp") s.group.Add(1) go func() { defer s.group.Done() if err := dns.ListenAndServe(s.config.DnsAddr, "udp", mux); err != nil { fatalf("%s", err) } }() dnsReadyMsg(s.config.DnsAddr, "udp") } s.group.Wait() return nil } // Stop stops a server. func (s *server) Stop() { // TODO(miek) //s.group.Add(-2) } // ServeDNS is the handler for DNS requests, responsible for parsing DNS request, possibly forwarding // it to a real dns server and returning a response. func (s *server) ServeDNS(w dns.ResponseWriter, req *dns.Msg) { m := new(dns.Msg) m.SetReply(req) m.Authoritative = true m.RecursionAvailable = true m.Compress = true bufsize := uint16(512) dnssec := false tcp := false start := time.Now() q := req.Question[0] name := strings.ToLower(q.Name) if q.Qtype == dns.TypeANY { m.Authoritative = false m.Rcode = dns.RcodeRefused m.RecursionAvailable = false m.RecursionDesired = false m.Compress = false w.WriteMsg(m) metrics.ReportRequestCount(m, metrics.Auth) metrics.ReportDuration(m, start, metrics.Auth) metrics.ReportErrorCount(m, metrics.Auth) return } if o := req.IsEdns0(); o != nil { bufsize = o.UDPSize() dnssec = o.Do() } if bufsize < 512 { bufsize = 512 } // with TCP we can send 64K if tcp = isTCP(w); tcp { bufsize = dns.MaxMsgSize - 1 } if s.config.Verbose { logf("received DNS Request for %q from %q with type %d", q.Name, w.RemoteAddr(), q.Qtype) } // Check cache first. m1 := s.rcache.Hit(q, dnssec, tcp, m.Id) if m1 != nil { metrics.ReportRequestCount(req, metrics.Cache) if send := s.overflowOrTruncated(w, m1, int(bufsize), metrics.Cache); send { return } // Still round-robin even with hits from the cache. // Only shuffle A and AAAA records with each other. if q.Qtype == dns.TypeA || q.Qtype == dns.TypeAAAA { s.RoundRobin(m1.Answer) } if err := w.WriteMsg(m1); err != nil { logf("failure to return reply %q", err) } metrics.ReportDuration(m1, start, metrics.Cache) metrics.ReportErrorCount(m1, metrics.Cache) return } for zone, ns := range *s.config.stub { if strings.HasSuffix(name, "." + zone) || name == zone { metrics.ReportRequestCount(req, metrics.Stub) resp := s.ServeDNSStubForward(w, req, ns) if resp != nil { s.rcache.InsertMessage(cache.Key(q, dnssec, tcp), resp) } metrics.ReportDuration(resp, start, metrics.Stub) metrics.ReportErrorCount(resp, metrics.Stub) return } } // If the qname is local.ds.skydns.local. and s.config.Local != "", substitute that name. if s.config.Local != "" && name == s.config.localDomain { name = s.config.Local } if q.Qtype == dns.TypePTR && strings.HasSuffix(name, ".in-addr.arpa.") || strings.HasSuffix(name, ".ip6.arpa.") { metrics.ReportRequestCount(req, metrics.Reverse) resp := s.ServeDNSReverse(w, req) if resp != nil { s.rcache.InsertMessage(cache.Key(q, dnssec, tcp), resp) } metrics.ReportDuration(resp, start, metrics.Reverse) metrics.ReportErrorCount(resp, metrics.Reverse) return } if q.Qclass != dns.ClassCHAOS && !strings.HasSuffix(name, "." +s.config.Domain) && name != s.config.Domain { metrics.ReportRequestCount(req, metrics.Rec) resp := s.ServeDNSForward(w, req) if resp != nil { s.rcache.InsertMessage(cache.Key(q, dnssec, tcp), resp) } metrics.ReportDuration(resp, start, metrics.Rec) metrics.ReportErrorCount(resp, metrics.Rec) return } metrics.ReportCacheMiss(metrics.Response) defer func() { metrics.ReportDuration(m, start, metrics.Auth) metrics.ReportErrorCount(m, metrics.Auth) if m.Rcode == dns.RcodeServerFailure { if err := w.WriteMsg(m); err != nil { logf("failure to return reply %q", err) } return } // Set TTL to the minimum of the RRset and dedup the message, i.e. remove identical RRs. m = s.dedup(m) minttl := s.config.Ttl if len(m.Answer) > 1 { for _, r := range m.Answer { if r.Header().Ttl < minttl { minttl = r.Header().Ttl } } for _, r := range m.Answer { r.Header().Ttl = minttl } } if dnssec { if s.config.PubKey != nil { m.AuthenticatedData = true s.Denial(m) s.Sign(m, bufsize) } } if send := s.overflowOrTruncated(w, m, int(bufsize), metrics.Auth); send { return } s.rcache.InsertMessage(cache.Key(q, dnssec, tcp), m) if err := w.WriteMsg(m); err != nil { logf("failure to return reply %q", err) } }() if name == s.config.Domain { if q.Qtype == dns.TypeSOA { m.Answer = []dns.RR{s.NewSOA()} return } if q.Qtype == dns.TypeDNSKEY { if s.config.PubKey != nil { m.Answer = []dns.RR{s.config.PubKey} return } } } if q.Qclass == dns.ClassCHAOS { if q.Qtype == dns.TypeTXT { switch name { case "authors.bind.": fallthrough case s.config.Domain: hdr := dns.RR_Header{Name: q.Name, Rrtype: dns.TypeTXT, Class: dns.ClassCHAOS, Ttl: 0} authors := []string{"Erik St. Martin", "Brian Ketelsen", "Miek Gieben", "Michael Crosby"} for _, a := range authors { m.Answer = append(m.Answer, &dns.TXT{Hdr: hdr, Txt: []string{a}}) } for j := 0; j < len(authors)*(int(dns.Id())%4+1); j++ { q := int(dns.Id()) % len(authors) p := int(dns.Id()) % len(authors) if q == p { p = (p + 1) % len(authors) } m.Answer[q], m.Answer[p] = m.Answer[p], m.Answer[q] } return case "version.bind.": fallthrough case "version.server.": hdr := dns.RR_Header{Name: q.Name, Rrtype: dns.TypeTXT, Class: dns.ClassCHAOS, Ttl: 0} m.Answer = []dns.RR{&dns.TXT{Hdr: hdr, Txt: []string{Version}}} return case "hostname.bind.": fallthrough case "id.server.": // TODO(miek): machine name to return hdr := dns.RR_Header{Name: q.Name, Rrtype: dns.TypeTXT, Class: dns.ClassCHAOS, Ttl: 0} m.Answer = []dns.RR{&dns.TXT{Hdr: hdr, Txt: []string{"localhost"}}} return } } // still here, fail m.SetReply(req) m.SetRcode(req, dns.RcodeServerFailure) return } switch q.Qtype { case dns.TypeNS: if name != s.config.Domain { break } // Lookup s.config.DnsDomain 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 } } func (s *server) AddressRecords(q dns.Question, name string, previousRecords []dns.RR, bufsize uint16, dnssec, both bool) (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) { logf("CNAME loop detected: %q -> %q", q.Name, q.Name) // 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), bufsize, dnssec, both) 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 from %q: %s", target, 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())) } } s.RoundRobin(records) return records, nil } // NSRecords returns NS records from etcd. func (s *server) NSRecords(q dns.Question, name string) (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(q dns.Question, name string, bufsize uint16, dnssec bool) (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, bufsize uint16, dnssec bool) (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, name string) (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, name string) (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 (s *server) PTRRecords(q dns.Question) (records []dns.RR, err error) { name := strings.ToLower(q.Name) serv, err := s.backend.ReverseRecord(name) if err != nil { return nil, err } records = append(records, serv.NewPTR(q.Name, serv.Ttl)) return records, nil } // SOA returns a SOA record for this SkyDNS instance. func (s *server) NewSOA() dns.RR { return &dns.SOA{Hdr: dns.RR_Header{Name: s.config.Domain, Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: s.config.Ttl}, Ns: appendDomain("ns.dns", s.config.Domain), Mbox: s.config.Hostmaster, Serial: uint32(time.Now().Truncate(time.Hour).Unix()), Refresh: 28800, Retry: 7200, Expire: 604800, Minttl: s.config.MinTtl, } } func (s *server) 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 } 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 } func (s *server) ServerFailure(req *dns.Msg) *dns.Msg { m := new(dns.Msg) m.SetRcode(req, dns.RcodeServerFailure) return m } func (s *server) RoundRobin(rrs []dns.RR) { if !s.config.RoundRobin { return } // If we have more than 1 CNAME don't touch the packet, because some stub resolver (=glibc) // can't deal with the returned packet if the CNAMEs need to be accesses in the reverse order. cname := 0 for _, r := range rrs { if r.Header().Rrtype == dns.TypeCNAME { cname++ if cname > 1 { return } } } switch l := len(rrs); l { case 2: if dns.Id()%2 == 0 { rrs[0], rrs[1] = rrs[1], rrs[0] } default: for j := 0; j < l*(int(dns.Id())%4+1); j++ { q := int(dns.Id()) % l p := int(dns.Id()) % l if q == p { p = (p + 1) % l } rrs[q], rrs[p] = rrs[p], rrs[q] } } } // dedup will de-duplicate a message on a per section basis. // Multiple identical (same name, class, type and rdata) RRs will be coalesced into one. func (s *server) dedup(m *dns.Msg) *dns.Msg { // Answer section ma := make(map[string]dns.RR) for _, a := range m.Answer { // Or use Pack()... Think this function also could be placed in go dns. s1 := a.Header().Name s1 += strconv.Itoa(int(a.Header().Class)) s1 += strconv.Itoa(int(a.Header().Rrtype)) // there can only be one CNAME for an ownername if a.Header().Rrtype == dns.TypeCNAME { if _, ok := ma[s1]; ok { // already exist, randomly overwrite if roundrobin is true // Note: even with roundrobin *off* this depends on the // order we get the names. if s.config.RoundRobin && dns.Id()%2 == 0 { ma[s1] = a continue } } ma[s1] = a continue } for i := 1; i <= dns.NumField(a); i++ { s1 += dns.Field(a, i) } ma[s1] = a } // Only is our map is smaller than the #RR in the answer section we should reset the RRs // in the section it self if len(ma) < len(m.Answer) { i := 0 for _, v := range ma { m.Answer[i] = v i++ } m.Answer = m.Answer[:len(ma)] } // Additional section me := make(map[string]dns.RR) for _, e := range m.Extra { s1 := e.Header().Name s1 += strconv.Itoa(int(e.Header().Class)) s1 += strconv.Itoa(int(e.Header().Rrtype)) // there can only be one CNAME for an ownername if e.Header().Rrtype == dns.TypeCNAME { if _, ok := me[s1]; ok { // already exist, randomly overwrite if roundrobin is true if s.config.RoundRobin && dns.Id()%2 == 0 { me[s1] = e continue } } me[s1] = e continue } for i := 1; i <= dns.NumField(e); i++ { s1 += dns.Field(e, i) } me[s1] = e } if len(me) < len(m.Extra) { i := 0 for _, v := range me { m.Extra[i] = v i++ } m.Extra = m.Extra[:len(me)] } 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 } // isTCP returns true if the client is connecting over TCP. func isTCP(w dns.ResponseWriter) bool { _, ok := w.RemoteAddr().(*net.TCPAddr) return ok } // 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 } skydns/server/server_test.go000644 000765 000024 00000132220 13063661551 016732 0ustar00tpotstaff000000 000000 // Copyright (c) 2014 The SkyDNS Authors. All rights reserved. // Use of this source code is governed by The MIT License (MIT) that can be // found in the LICENSE file. package server // etcd needs to be running on http://127.0.0.1:4001 import ( "crypto/rsa" "encoding/json" "sort" "strconv" "strings" "sync" "testing" "time" backendetcd "github.com/skynetservices/skydns/backends/etcd" "github.com/skynetservices/skydns/cache" "github.com/skynetservices/skydns/msg" etcd "github.com/coreos/etcd/client" "github.com/miekg/dns" "golang.org/x/net/context" ) // Keep global port counter that increments with 10 for each // new call to newTestServer. The dns server is started on port 'Port'. var ( Port = 9400 StrPort = "9400" // string equivalent of Port ctx = context.Background() ) func addService(t *testing.T, s *server, k string, ttl time.Duration, m *msg.Service) { b, err := json.Marshal(m) if err != nil { t.Fatal(err) } path, _ := msg.PathWithWildcard(k) _, err = s.backend.(*backendetcd.Backend).Client().Set(ctx, path, string(b), &etcd.SetOptions{TTL: ttl}) if err != nil { // TODO(miek): allow for existing keys... t.Fatal(err) } } func delService(t *testing.T, s *server, k string) { path, _ := msg.PathWithWildcard(k) _, err := s.backend.(*backendetcd.Backend).Client().Delete(ctx, path, &etcd.DeleteOptions{Recursive: false}) if err != nil { t.Fatal(err) } } func newTestServer(t *testing.T, c bool) *server { Port += 10 StrPort = strconv.Itoa(Port) s := new(server) client, _ := etcd.New(etcd.Config{ Endpoints: []string{"http://127.0.0.1:4001/"}, Transport: etcd.DefaultTransport, }) kapi := etcd.NewKeysAPI(client) // TODO(miek): why don't I use NewServer?? s.group = new(sync.WaitGroup) s.scache = cache.New(100, 0) s.rcache = cache.New(100, 0) if c { s.rcache = cache.New(100, 60) // 100 items, 60s ttl } s.config = new(Config) s.config.Domain = "skydns.test." s.config.DnsAddr = "127.0.0.1:" + StrPort s.config.Nameservers = []string{"8.8.4.4:53"} SetDefaults(s.config) s.config.Local = "104.server1.development.region1.skydns.test." s.config.Priority = 10 s.config.RCacheTtl = RCacheTtl s.config.Ttl = 3600 s.config.Ndots = 2 s.dnsUDPclient = &dns.Client{Net: "udp", ReadTimeout: 2 * s.config.ReadTimeout, WriteTimeout: 2 * s.config.ReadTimeout, SingleInflight: true} s.dnsTCPclient = &dns.Client{Net: "tcp", ReadTimeout: 2 * s.config.ReadTimeout, WriteTimeout: 2 * s.config.ReadTimeout, SingleInflight: true} s.backend = backendetcd.NewBackend(kapi, ctx, &backendetcd.Config{ Ttl: s.config.Ttl, Priority: s.config.Priority, }) go s.Run() time.Sleep(500 * time.Millisecond) // Yeah, yeah, should do a proper fix return s } func newTestServerDNSSEC(t *testing.T, cache bool) *server { var err error s := newTestServer(t, cache) s.config.PubKey = newDNSKEY("skydns.test. IN DNSKEY 256 3 5 AwEAAaXfO+DOBMJsQ5H4TfiabwSpqE4cGL0Qlvh5hrQumrjr9eNSdIOjIHJJKCe56qBU5mH+iBlXP29SVf6UiiMjIrAPDVhClLeWFe0PC+XlWseAyRgiLHdQ8r95+AfkhO5aZgnCwYf9FGGSaT0+CRYN+PyDbXBTLK5FN+j5b6bb7z+d") s.config.KeyTag = s.config.PubKey.KeyTag() privKey, err := s.config.PubKey.ReadPrivateKey(strings.NewReader(`Private-key-format: v1.3 Algorithm: 5 (RSASHA1) Modulus: pd874M4EwmxDkfhN+JpvBKmoThwYvRCW+HmGtC6auOv141J0g6MgckkoJ7nqoFTmYf6IGVc/b1JV/pSKIyMisA8NWEKUt5YV7Q8L5eVax4DJGCIsd1Dyv3n4B+SE7lpmCcLBh/0UYZJpPT4JFg34/INtcFMsrkU36PlvptvvP50= PublicExponent: AQAB PrivateExponent: C6e08GXphbPPx6j36ZkIZf552gs1XcuVoB4B7hU8P/Qske2QTFOhCwbC8I+qwdtVWNtmuskbpvnVGw9a6X8lh7Z09RIgzO/pI1qau7kyZcuObDOjPw42exmjqISFPIlS1wKA8tw+yVzvZ19vwRk1q6Rne+C1romaUOTkpA6UXsE= Prime1: 2mgJ0yr+9vz85abrWBWnB8Gfa1jOw/ccEg8ZToM9GLWI34Qoa0D8Dxm8VJjr1tixXY5zHoWEqRXciTtY3omQDQ== Prime2: wmxLpp9rTzU4OREEVwF43b/TxSUBlUq6W83n2XP8YrCm1nS480w4HCUuXfON1ncGYHUuq+v4rF+6UVI3PZT50Q== Exponent1: wkdTngUcIiau67YMmSFBoFOq9Lldy9HvpVzK/R0e5vDsnS8ZKTb4QJJ7BaG2ADpno7pISvkoJaRttaEWD3a8rQ== Exponent2: YrC8OglEXIGkV3tm2494vf9ozPL6+cBkFsPPg9dXbvVCyyuW0pGHDeplvfUqs4nZp87z8PsoUL+LAUqdldnwcQ== Coefficient: mMFr4+rDY5V24HZU3Oa5NEb55iQ56ZNa182GnNhWqX7UqWjcUUGjnkCy40BqeFAQ7lp52xKHvP5Zon56mwuQRw== `), "stdin") s.config.PrivKey = privKey.(*rsa.PrivateKey) if err != nil { t.Fatal(err) } return s } func TestDNSForward(t *testing.T) { s := newTestServer(t, false) defer s.Stop() c := new(dns.Client) m := new(dns.Msg) m.SetQuestion("www.example.com.", dns.TypeA) resp, _, err := c.Exchange(m, "127.0.0.1:"+StrPort) if err != nil { // try twice resp, _, err = c.Exchange(m, "127.0.0.1:"+StrPort) if err != nil { t.Fatal(err) } } if len(resp.Answer) == 0 || resp.Rcode != dns.RcodeSuccess { t.Fatal("answer expected to have A records or rcode not equal to RcodeSuccess") } // TCP c.Net = "tcp" resp, _, err = c.Exchange(m, "127.0.0.1:"+StrPort) if err != nil { t.Fatal(err) } if len(resp.Answer) == 0 || resp.Rcode != dns.RcodeSuccess { t.Fatal("answer expected to have A records or rcode not equal to RcodeSuccess") } // disable recursion and check s.config.NoRec = true m.SetQuestion("www.example.com.", dns.TypeA) resp, _, err = c.Exchange(m, "127.0.0.1:"+StrPort) if err != nil { t.Fatal(err) } if resp.Rcode != dns.RcodeServerFailure { t.Fatal("answer expected to have rcode equal to RcodeFailure") } } func TestDNSStubForward(t *testing.T) { s := newTestServer(t, false) defer s.Stop() c := new(dns.Client) m := new(dns.Msg) stubEx := &msg.Service{ // IP address of a.iana-servers.net. Host: "199.43.132.53", Key: "a.example.com.stub.dns.skydns.test.", } stubBroken := &msg.Service{ Host: "127.0.0.1", Port: 5454, Key: "b.example.org.stub.dns.skydns.test.", } stubLoop := &msg.Service{ Host: "127.0.0.1", Port: Port, Key: "b.example.net.stub.dns.skydns.test.", } addService(t, s, stubEx.Key, 0, stubEx) defer delService(t, s, stubEx.Key) addService(t, s, stubBroken.Key, 0, stubBroken) defer delService(t, s, stubBroken.Key) addService(t, s, stubLoop.Key, 0, stubLoop) defer delService(t, s, stubLoop.Key) s.UpdateStubZones() m.SetQuestion("www.example.com.", dns.TypeA) resp, _, err := c.Exchange(m, "127.0.0.1:"+StrPort) if err != nil { // try twice resp, _, err = c.Exchange(m, "127.0.0.1:"+StrPort) if err != nil { t.Fatal(err) } } if len(resp.Answer) == 0 || resp.Rcode != dns.RcodeSuccess { t.Fatal("answer expected to have A records or rcode not equal to RcodeSuccess") } // The main diff. here is that we expect the AA bit to be set, because we directly // queried the authoritative servers. if resp.Authoritative != true { t.Fatal("answer expected to have AA bit set") } // This should fail. m.SetQuestion("www.example.org.", dns.TypeA) resp, _, err = c.Exchange(m, "127.0.0.1:"+StrPort) if len(resp.Answer) != 0 || resp.Rcode != dns.RcodeServerFailure { t.Fatal("answer expected to fail for example.org") } // This should really fail with a timeout. m.SetQuestion("www.example.net.", dns.TypeA) resp, _, err = c.Exchange(m, "127.0.0.1:"+StrPort) if err == nil { t.Fatal("answer expected to fail for example.net") } else { t.Logf("succesfully failing %s", err) } // Packet with EDNS0 m.SetEdns0(4096, true) resp, _, err = c.Exchange(m, "127.0.0.1:"+StrPort) if err == nil { t.Fatal("answer expected to fail for example.net") } else { t.Logf("succesfully failing %s", err) } // Now start another SkyDNS instance on a different port, // add a stubservice for it and check if the forwarding is // actually working. oldStrPort := StrPort s1 := newTestServer(t, false) defer s1.Stop() s1.config.Domain = "skydns.com." // Add forwarding IP for internal.skydns.com. Use Port to point to server s. stubForward := &msg.Service{ Host: "127.0.0.1", Port: Port, Key: "b.internal.skydns.com.stub.dns.skydns.test.", } addService(t, s, stubForward.Key, 0, stubForward) defer delService(t, s, stubForward.Key) s.UpdateStubZones() // Add an answer for this in our "new" server. stubReply := &msg.Service{ Host: "127.1.1.1", Key: "www.internal.skydns.com.", } addService(t, s1, stubReply.Key, 0, stubReply) defer delService(t, s1, stubReply.Key) m = new(dns.Msg) m.SetQuestion("www.internal.skydns.com.", dns.TypeA) resp, _, err = c.Exchange(m, "127.0.0.1:"+oldStrPort) if err != nil { t.Fatalf("failed to forward %s", err) } if resp.Answer[0].(*dns.A).A.String() != "127.1.1.1" { t.Fatalf("failed to get correct reply") } // Adding an in baliwick internal domain forward. s2 := newTestServer(t, false) defer s2.Stop() s2.config.Domain = "internal.skydns.net." // Add forwarding IP for internal.skydns.net. Use Port to point to server s. stubForward1 := &msg.Service{ Host: "127.0.0.1", Port: Port, Key: "b.internal.skydns.net.stub.dns.skydns.test.", } addService(t, s, stubForward1.Key, 0, stubForward1) defer delService(t, s, stubForward1.Key) s.UpdateStubZones() // Add an answer for this in our "new" server. stubReply1 := &msg.Service{ Host: "127.10.10.10", Key: "www.internal.skydns.net.", } addService(t, s2, stubReply1.Key, 0, stubReply1) defer delService(t, s2, stubReply1.Key) m = new(dns.Msg) m.SetQuestion("www.internal.skydns.net.", dns.TypeA) resp, _, err = c.Exchange(m, "127.0.0.1:"+oldStrPort) if err != nil { t.Fatalf("failed to forward %s", err) } if resp.Answer[0].(*dns.A).A.String() != "127.10.10.10" { t.Fatalf("failed to get correct reply") } } func TestDNSTtlRR(t *testing.T) { s := newTestServerDNSSEC(t, false) defer s.Stop() serv := &msg.Service{Host: "10.0.0.2", Key: "ttl.skydns.test.", Ttl: 360} addService(t, s, serv.Key, time.Duration(serv.Ttl)*time.Second, serv) defer delService(t, s, serv.Key) c := new(dns.Client) tc := dnsTestCases[9] // TTL Test t.Logf("%v\n", tc) m := new(dns.Msg) m.SetQuestion(tc.Qname, tc.Qtype) if tc.dnssec == true { m.SetEdns0(4096, true) } resp, _, err := c.Exchange(m, "127.0.0.1:"+StrPort) if err != nil { t.Errorf("failing: %s: %s\n", m.String(), err.Error()) } t.Logf("%s\n", resp) for i, a := range resp.Answer { if a.Header().Ttl != 360 { t.Errorf("Answer %d should have a Header TTL of %d, but has %d", i, 360, a.Header().Ttl) } } } type rrSet []dns.RR func (p rrSet) Len() int { return len(p) } func (p rrSet) Swap(i, j int) { p[i], p[j] = p[j], p[i] } func (p rrSet) Less(i, j int) bool { return p[i].String() < p[j].String() } func TestDNS(t *testing.T) { s := newTestServerDNSSEC(t, false) defer s.Stop() for _, serv := range services { addService(t, s, serv.Key, 0, serv) defer delService(t, s, serv.Key) } c := new(dns.Client) for _, tc := range dnsTestCases { m := new(dns.Msg) m.SetQuestion(tc.Qname, tc.Qtype) if tc.dnssec { m.SetEdns0(4096, true) } if tc.chaos { m.Question[0].Qclass = dns.ClassCHAOS } resp, _, err := c.Exchange(m, "127.0.0.1:"+StrPort) if err != nil { // try twice, be more resilent against remote lookups // timing out. resp, _, err = c.Exchange(m, "127.0.0.1:"+StrPort) if err != nil { t.Fatalf("failing: %s: %s\n", m.String(), err.Error()) } } sort.Sort(rrSet(resp.Answer)) sort.Sort(rrSet(resp.Ns)) sort.Sort(rrSet(resp.Extra)) fatal := false defer func() { if fatal { t.Logf("question: %s\n", m.Question[0].String()) t.Logf("%s\n", resp) } }() if resp.Rcode != tc.Rcode { fatal = true t.Fatalf("rcode is %q, expected %q", dns.RcodeToString[resp.Rcode], dns.RcodeToString[tc.Rcode]) } if len(resp.Answer) != len(tc.Answer) { fatal = true t.Fatalf("answer for %q contained %d results, %d expected", tc.Qname, len(resp.Answer), len(tc.Answer)) } for i, a := range resp.Answer { if a.Header().Name != tc.Answer[i].Header().Name { fatal = true t.Fatalf("answer %d should have a Header Name of %q, but has %q", i, tc.Answer[i].Header().Name, a.Header().Name) } if a.Header().Ttl != tc.Answer[i].Header().Ttl { fatal = true t.Fatalf("Answer %d should have a Header TTL of %d, but has %d", i, tc.Answer[i].Header().Ttl, a.Header().Ttl) } if a.Header().Rrtype != tc.Answer[i].Header().Rrtype { fatal = true t.Fatalf("answer %d should have a header response type of %d, but has %d", i, tc.Answer[i].Header().Rrtype, a.Header().Rrtype) } switch x := a.(type) { case *dns.SRV: if x.Priority != tc.Answer[i].(*dns.SRV).Priority { fatal = true t.Fatalf("answer %d should have a Priority of %d, but has %d", i, tc.Answer[i].(*dns.SRV).Priority, x.Priority) } if x.Weight != tc.Answer[i].(*dns.SRV).Weight { fatal = true t.Fatalf("answer %d should have a Weight of %d, but has %d", i, tc.Answer[i].(*dns.SRV).Weight, x.Weight) } if x.Port != tc.Answer[i].(*dns.SRV).Port { fatal = true t.Fatalf("answer %d should have a Port of %d, but has %d", i, tc.Answer[i].(*dns.SRV).Port, x.Port) } if x.Target != tc.Answer[i].(*dns.SRV).Target { fatal = true t.Fatalf("answer %d should have a Target of %q, but has %q", i, tc.Answer[i].(*dns.SRV).Target, x.Target) } case *dns.A: if x.A.String() != tc.Answer[i].(*dns.A).A.String() { fatal = true t.Fatalf("answer %d should have a Address of %q, but has %q", i, tc.Answer[i].(*dns.A).A.String(), x.A.String()) } case *dns.AAAA: if x.AAAA.String() != tc.Answer[i].(*dns.AAAA).AAAA.String() { fatal = true t.Fatalf("answer %d should have a Address of %q, but has %q", i, tc.Answer[i].(*dns.AAAA).AAAA.String(), x.AAAA.String()) } case *dns.TXT: for j, txt := range x.Txt { if txt != tc.Answer[i].(*dns.TXT).Txt[j] { fatal = true t.Fatalf("answer %d should have a Txt of %q, but has %q", i, tc.Answer[i].(*dns.TXT).Txt[j], txt) } } case *dns.DNSKEY: tt := tc.Answer[i].(*dns.DNSKEY) if x.Flags != tt.Flags { fatal = true t.Fatalf("DNSKEY flags should be %q, but is %q", x.Flags, tt.Flags) } if x.Protocol != tt.Protocol { fatal = true t.Fatalf("DNSKEY protocol should be %q, but is %q", x.Protocol, tt.Protocol) } if x.Algorithm != tt.Algorithm { fatal = true t.Fatalf("DNSKEY algorithm should be %q, but is %q", x.Algorithm, tt.Algorithm) } case *dns.RRSIG: tt := tc.Answer[i].(*dns.RRSIG) if x.TypeCovered != tt.TypeCovered { fatal = true t.Fatalf("RRSIG type-covered should be %d, but is %d", x.TypeCovered, tt.TypeCovered) } if x.Algorithm != tt.Algorithm { fatal = true t.Fatalf("RRSIG algorithm should be %d, but is %d", x.Algorithm, tt.Algorithm) } if x.Labels != tt.Labels { fatal = true t.Fatalf("RRSIG label should be %d, but is %d", x.Labels, tt.Labels) } if x.OrigTtl != tt.OrigTtl { fatal = true t.Fatalf("RRSIG orig-ttl should be %d, but is %d", x.OrigTtl, tt.OrigTtl) } if x.KeyTag != tt.KeyTag { fatal = true t.Fatalf("RRSIG key-tag should be %d, but is %d", x.KeyTag, tt.KeyTag) } if x.SignerName != tt.SignerName { fatal = true t.Fatalf("RRSIG signer-name should be %q, but is %q", x.SignerName, tt.SignerName) } case *dns.SOA: tt := tc.Answer[i].(*dns.SOA) if x.Ns != tt.Ns { fatal = true t.Fatalf("SOA nameserver should be %q, but is %q", x.Ns, tt.Ns) } case *dns.PTR: tt := tc.Answer[i].(*dns.PTR) if x.Ptr != tt.Ptr { fatal = true t.Fatalf("PTR ptr should be %q, but is %q", x.Ptr, tt.Ptr) } case *dns.CNAME: tt := tc.Answer[i].(*dns.CNAME) if x.Target != tt.Target { fatal = true t.Fatalf("CNAME target should be %q, but is %q", x.Target, tt.Target) } case *dns.MX: tt := tc.Answer[i].(*dns.MX) if x.Mx != tt.Mx { t.Fatalf("MX Mx should be %q, but is %q", x.Mx, tt.Mx) } if x.Preference != tt.Preference { t.Fatalf("MX Preference should be %q, but is %q", x.Preference, tt.Preference) } } } if len(resp.Ns) != len(tc.Ns) { fatal = true t.Fatalf("authority for %q contained %d results, %d expected", tc.Qname, len(resp.Ns), len(tc.Ns)) } for i, n := range resp.Ns { switch x := n.(type) { case *dns.SOA: tt := tc.Ns[i].(*dns.SOA) if x.Ns != tt.Ns { fatal = true t.Fatalf("SOA nameserver should be %q, but is %q", x.Ns, tt.Ns) } case *dns.NS: tt := tc.Ns[i].(*dns.NS) if x.Ns != tt.Ns { fatal = true t.Fatalf("NS nameserver should be %q, but is %q", x.Ns, tt.Ns) } case *dns.NSEC3: tt := tc.Ns[i].(*dns.NSEC3) if x.NextDomain != tt.NextDomain { fatal = true t.Fatalf("NSEC3 nextdomain should be %q, but is %q", x.NextDomain, tt.NextDomain) } if x.Hdr.Name != tt.Hdr.Name { fatal = true t.Fatalf("NSEC3 ownername should be %q, but is %q", x.Hdr.Name, tt.Hdr.Name) } for j, y := range x.TypeBitMap { if y != tt.TypeBitMap[j] { fatal = true t.Fatalf("NSEC3 bitmap should have %q, but is %q", dns.TypeToString[y], dns.TypeToString[tt.TypeBitMap[j]]) } } } } if len(resp.Extra) != len(tc.Extra) { fatal = true t.Fatalf("additional for %q contained %d results, %d expected", tc.Qname, len(resp.Extra), len(tc.Extra)) } for i, e := range resp.Extra { switch x := e.(type) { case *dns.A: if x.A.String() != tc.Extra[i].(*dns.A).A.String() { fatal = true t.Fatalf("extra %d should have a address of %q, but has %q", i, tc.Extra[i].(*dns.A).A.String(), x.A.String()) } case *dns.AAAA: if x.AAAA.String() != tc.Extra[i].(*dns.AAAA).AAAA.String() { fatal = true t.Fatalf("extra %d should have a address of %q, but has %q", i, tc.Extra[i].(*dns.AAAA).AAAA.String(), x.AAAA.String()) } case *dns.CNAME: tt := tc.Extra[i].(*dns.CNAME) if x.Target != tt.Target { // Super super gross hack. if x.Target == "a.ipaddr.skydns.test." && tt.Target == "b.ipaddr.skydns.test." { // These records are randomly choosen, either one is OK. continue } fatal = true t.Fatalf("CNAME target should be %q, but is %q", x.Target, tt.Target) } } } } } type dnsTestCase struct { Qname string Qtype uint16 dnssec bool chaos bool Rcode int Answer []dns.RR Ns []dns.RR Extra []dns.RR } // Note the key is encoded as DNS name, while in "reality" it is a etcd path. var services = []*msg.Service{ {Host: "server1", Port: 8080, Key: "100.server1.development.region1.skydns.test."}, {Host: "server2", Port: 80, Key: "101.server2.production.region1.skydns.test."}, {Host: "server4", Port: 80, Priority: 333, Key: "102.server4.development.region6.skydns.test."}, {Host: "server3", Key: "103.server4.development.region2.skydns.test."}, {Host: "172.16.1.1", Key: "a.ipaddr.skydns.test."}, {Host: "172.16.1.2", Key: "b.ipaddr.skydns.test."}, {Host: "ipaddr.skydns.test", Key: "1.backend.in.skydns.test."}, {Host: "10.0.0.1", Key: "104.server1.development.region1.skydns.test."}, {Host: "2001::8:8:8:8", Key: "105.server3.production.region2.skydns.test."}, {Host: "104.server1.development.region1.skydns.test", Key: "1.cname.skydns.test."}, {Host: "100.server1.development.region1.skydns.test", Key: "2.cname.skydns.test."}, {Host: "www.miek.nl", Key: "external1.cname.skydns.test."}, {Host: "www.miek.nl", Key: "ext1.cname2.skydns.test."}, {Host: "www.miek.nl", Key: "ext2.cname2.skydns.test."}, {Host: "wwwwwww.miek.nl", Key: "external2.cname.skydns.test."}, {Host: "4.cname.skydns.test", Key: "3.cname.skydns.test."}, {Host: "3.cname.skydns.test", Key: "4.cname.skydns.test."}, {Host: "10.0.0.2", Key: "ttl.skydns.test.", Ttl: 360}, {Host: "reverse.example.com", Key: "1.0.0.10.in-addr.arpa."}, // 10.0.0.1 {Host: "server1", Weight: 130, Key: "100.server1.region5.skydns.test."}, {Host: "server2", Weight: 80, Key: "101.server2.region5.skydns.test."}, {Host: "server3", Weight: 150, Key: "103.server3.region5.skydns.test."}, {Host: "server4", Priority: 30, Key: "104.server4.region5.skydns.test."}, {Host: "172.16.1.1", Key: "a.ipaddr2.skydns.test."}, {Host: "2001::8:8:8:8", Key: "b.ipaddr2.skydns.test."}, {Host: "ipaddr2.skydns.test", Key: "both.v4v6.test.skydns.test."}, // A name: bar.skydns.test with 2 ports open and points to one ip: 192.168.0.1 {Host: "192.168.0.1", Port: 80, Key: "x.bar.skydns.test.", TargetStrip: 1}, {Host: "bar.skydns.local", Port: 443, Key: "y.bar.skydns.test.", TargetStrip: 0}, // nameserver {Host: "10.0.0.2", Key: "a.ns.dns.skydns.test."}, {Host: "10.0.0.3", Key: "b.ns.dns.skydns.test."}, // txt {Text: "abc", Key: "a1.txt.skydns.test."}, {Text: "abc abc", Key: "a2.txt.skydns.test."}, // duplicate ip address {Host: "10.11.11.10", Key: "http.multiport.http.skydns.test.", Port: 80}, {Host: "10.11.11.10", Key: "https.multiport.http.skydns.test.", Port: 443}, // uppercase name {Host: "127.0.0.1", Key: "upper.skydns.test.", Port: 443}, // mx {Host: "mx.skydns.test", Priority: 50, Mail: true, Key: "a.mail.skydns.test."}, {Host: "mx.miek.nl", Priority: 50, Mail: true, Key: "b.mail.skydns.test."}, {Host: "a.ipaddr.skydns.test", Priority: 30, Mail: true, Key: "a.mx.skydns.test."}, // double CNAME, see issue #168 {Host: "mx2.skydns.test", Priority: 50, Mail: true, Key: "a.mail2.skydns.test."}, {Host: "a.ipaddr.skydns.test", Mail: true, Key: "a.mx2.skydns.test."}, // Sometimes we *do* get back a.ipaddr.skydns.test, making this test flaky. {Host: "b.ipaddr.skydns.test", Mail: true, Key: "b.mx2.skydns.test."}, // groups {Host: "127.0.0.1", Key: "a.dom.skydns.test.", Group: "g1"}, {Host: "127.0.0.2", Key: "b.sub.dom.skydns.test.", Group: "g1"}, {Host: "127.0.0.1", Key: "a.dom2.skydns.test.", Group: "g1"}, {Host: "127.0.0.2", Key: "b.sub.dom2.skydns.test.", Group: ""}, {Host: "127.0.0.1", Key: "a.dom1.skydns.test.", Group: "g1"}, {Host: "127.0.0.2", Key: "b.sub.dom1.skydns.test.", Group: "g2"}, } var dnsTestCases = []dnsTestCase{ // Full Name Test { Qname: "100.server1.development.region1.skydns.test.", Qtype: dns.TypeSRV, Answer: []dns.RR{newSRV("100.server1.development.region1.skydns.test. 3600 SRV 10 100 8080 server1.")}, }, // SOA Record Test { Qname: "skydns.test.", Qtype: dns.TypeSOA, Answer: []dns.RR{newSOA("skydns.test. 3600 SOA ns.dns.skydns.test. hostmaster.skydns.test. 0 0 0 0 0")}, }, // NS Record Test { Qname: "skydns.test.", Qtype: dns.TypeNS, Answer: []dns.RR{ newNS("skydns.test. 3600 NS a.ns.dns.skydns.test."), newNS("skydns.test. 3600 NS b.ns.dns.skydns.test."), }, Extra: []dns.RR{ newA("a.ns.dns.skydns.test. 3600 A 10.0.0.2"), newA("b.ns.dns.skydns.test. 3600 A 10.0.0.3"), }, }, // A Record For NS Record Test { Qname: "ns.dns.skydns.test.", Qtype: dns.TypeA, Answer: []dns.RR{ newA("ns.dns.skydns.test. 3600 A 10.0.0.2"), newA("ns.dns.skydns.test. 3600 A 10.0.0.3"), }, }, // A Record Test { Qname: "104.server1.development.region1.skydns.test.", Qtype: dns.TypeA, Answer: []dns.RR{newA("104.server1.development.region1.skydns.test. 3600 A 10.0.0.1")}, }, // Multiple A Record Test { Qname: "ipaddr.skydns.test.", Qtype: dns.TypeA, Answer: []dns.RR{ newA("ipaddr.skydns.test. 3600 A 172.16.1.1"), newA("ipaddr.skydns.test. 3600 A 172.16.1.2"), }, }, // A Record Test with SRV { Qname: "104.server1.development.region1.skydns.test.", Qtype: dns.TypeSRV, Answer: []dns.RR{newSRV("104.server1.development.region1.skydns.test. 3600 SRV 10 100 0 104.server1.development.region1.skydns.test.")}, Extra: []dns.RR{newA("104.server1.development.region1.skydns.test. 3600 A 10.0.0.1")}, }, // AAAAA Record Test { Qname: "105.server3.production.region2.skydns.test.", Qtype: dns.TypeAAAA, Answer: []dns.RR{newAAAA("105.server3.production.region2.skydns.test. 3600 AAAA 2001::8:8:8:8")}, }, // Multi SRV with the same target, should be dedupped. { Qname: "*.cname2.skydns.test.", Qtype: dns.TypeSRV, Answer: []dns.RR{ newSRV("*.cname2.skydns.test. 3600 IN SRV 10 100 0 www.miek.nl."), }, Extra: []dns.RR{ newA("a.miek.nl. 3600 IN A 139.162.196.78"), newAAAA("a.miek.nl. 3600 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"), newCNAME("www.miek.nl. 3600 IN CNAME a.miek.nl."), }, }, // TTL Test { // This test is referenced by number from DNSTtlRRset Qname: "ttl.skydns.test.", Qtype: dns.TypeA, Answer: []dns.RR{newA("ttl.skydns.test. 360 A 10.0.0.2")}, }, // CNAME Test { Qname: "1.cname.skydns.test.", Qtype: dns.TypeA, Answer: []dns.RR{ newCNAME("1.cname.skydns.test. 3600 CNAME 104.server1.development.region1.skydns.test."), newA("104.server1.development.region1.skydns.test. 3600 A 10.0.0.1"), }, }, // Direct CNAME Test { Qname: "1.cname.skydns.test.", Qtype: dns.TypeCNAME, Answer: []dns.RR{ newCNAME("1.cname.skydns.test. 3600 CNAME 104.server1.development.region1.skydns.test."), }, }, // CNAME (unresolvable internal name) { Qname: "2.cname.skydns.test.", Qtype: dns.TypeA, Answer: []dns.RR{}, Ns: []dns.RR{newSOA("skydns.test. 60 SOA ns.dns.skydns.test. hostmaster.skydns.test. 1407441600 28800 7200 604800 60")}, }, // CNAME loop detection { Qname: "3.cname.skydns.test.", Qtype: dns.TypeA, Answer: []dns.RR{}, Ns: []dns.RR{newSOA("skydns.test. 60 SOA ns.dns.skydns.test. hostmaster.skydns.test. 1407441600 28800 7200 604800 60")}, }, // CNAME (resolvable external name) { Qname: "external1.cname.skydns.test.", Qtype: dns.TypeA, Answer: []dns.RR{ newA("a.miek.nl. 60 IN A 139.162.196.78"), newCNAME("external1.cname.skydns.test. 60 IN CNAME www.miek.nl."), newCNAME("www.miek.nl. 60 IN CNAME a.miek.nl."), }, }, // CNAME (unresolvable external name) { Qname: "external2.cname.skydns.test.", Qtype: dns.TypeA, Answer: []dns.RR{}, Ns: []dns.RR{newSOA("skydns.test. 60 SOA ns.dns.skydns.test. hostmaster.skydns.test. 1407441600 28800 7200 604800 60")}, }, // Priority Test { Qname: "region6.skydns.test.", Qtype: dns.TypeSRV, Answer: []dns.RR{newSRV("region6.skydns.test. 3600 SRV 333 100 80 server4.")}, }, // Subdomain Test { Qname: "region1.skydns.test.", Qtype: dns.TypeSRV, Answer: []dns.RR{ newSRV("region1.skydns.test. 3600 SRV 10 33 0 104.server1.development.region1.skydns.test."), newSRV("region1.skydns.test. 3600 SRV 10 33 80 server2"), newSRV("region1.skydns.test. 3600 SRV 10 33 8080 server1.")}, Extra: []dns.RR{newA("104.server1.development.region1.skydns.test. 3600 A 10.0.0.1")}, }, // Subdomain Weight Test { Qname: "region5.skydns.test.", Qtype: dns.TypeSRV, Answer: []dns.RR{ newSRV("region5.skydns.test. 3600 SRV 10 22 0 server2."), newSRV("region5.skydns.test. 3600 SRV 10 36 0 server1."), newSRV("region5.skydns.test. 3600 SRV 10 41 0 server3."), newSRV("region5.skydns.test. 3600 SRV 30 100 0 server4.")}, }, // Wildcard Test { Qname: "*.region1.skydns.test.", Qtype: dns.TypeSRV, Answer: []dns.RR{ newSRV("*.region1.skydns.test. 3600 SRV 10 33 0 104.server1.development.region1.skydns.test."), newSRV("*.region1.skydns.test. 3600 SRV 10 33 80 server2"), newSRV("*.region1.skydns.test. 3600 SRV 10 33 8080 server1.")}, Extra: []dns.RR{newA("104.server1.development.region1.skydns.test. 3600 A 10.0.0.1")}, }, // Wildcard Test { Qname: "production.*.skydns.test.", Qtype: dns.TypeSRV, Answer: []dns.RR{ newSRV("production.*.skydns.test. 3600 IN SRV 10 50 0 105.server3.production.region2.skydns.test."), newSRV("production.*.skydns.test. 3600 IN SRV 10 50 80 server2.")}, Extra: []dns.RR{newAAAA("105.server3.production.region2.skydns.test. 3600 IN AAAA 2001::8:8:8:8")}, }, // Wildcard Test { Qname: "production.any.skydns.test.", Qtype: dns.TypeSRV, Answer: []dns.RR{ newSRV("production.any.skydns.test. 3600 IN SRV 10 50 0 105.server3.production.region2.skydns.test."), newSRV("production.any.skydns.test. 3600 IN SRV 10 50 80 server2.")}, Extra: []dns.RR{newAAAA("105.server3.production.region2.skydns.test. 3600 IN AAAA 2001::8:8:8:8")}, }, // NXDOMAIN Test { Qname: "doesnotexist.skydns.test.", Qtype: dns.TypeA, Rcode: dns.RcodeNameError, Ns: []dns.RR{ newSOA("skydns.test. 3600 SOA ns.dns.skydns.test. hostmaster.skydns.test. 0 0 0 0 0"), }, }, // NODATA Test { Qname: "104.server1.development.region1.skydns.test.", Qtype: dns.TypeTXT, Ns: []dns.RR{newSOA("skydns.test. 3600 SOA ns.dns.skydns.test. hostmaster.skydns.test. 0 0 0 0 0")}, }, // NODATA Test 2 { Qname: "100.server1.development.region1.skydns.test.", Qtype: dns.TypeA, Rcode: dns.RcodeSuccess, Ns: []dns.RR{newSOA("skydns.test. 3600 SOA ns.dns.skydns.test. hostmaster.skydns.test. 0 0 0 0 0")}, }, // CNAME Test that targets multiple A records (hits a directory in etcd) { Qname: "1.backend.in.skydns.test.", Qtype: dns.TypeA, Answer: []dns.RR{ newCNAME("1.backend.in.skydns.test. IN CNAME ipaddr.skydns.test."), newA("ipaddr.skydns.test. IN A 172.16.1.1"), newA("ipaddr.skydns.test. IN A 172.16.1.2"), }, }, // Query a etcd directory key { Qname: "backend.in.skydns.test.", Qtype: dns.TypeA, Answer: []dns.RR{ newCNAME("backend.in.skydns.test. IN CNAME ipaddr.skydns.test."), newA("ipaddr.skydns.test. IN A 172.16.1.1"), newA("ipaddr.skydns.test. IN A 172.16.1.2"), }, }, // Txt { Qname: "a1.txt.skydns.test.", Qtype: dns.TypeTXT, Answer: []dns.RR{ newTXT("a1.txt.skydns.test. IN TXT \"abc\""), }, }, { Qname: "a2.txt.skydns.test.", Qtype: dns.TypeTXT, Answer: []dns.RR{ newTXT("a2.txt.skydns.test. IN TXT \"abc abc\""), }, }, { Qname: "txt.skydns.test.", Qtype: dns.TypeTXT, Answer: []dns.RR{ newTXT("txt.skydns.test. IN TXT \"abc abc\""), newTXT("txt.skydns.test. IN TXT \"abc\""), }, }, // DNSSEC // DNSKEY Test { dnssec: true, Qname: "skydns.test.", Qtype: dns.TypeDNSKEY, Answer: []dns.RR{ newDNSKEY("skydns.test. 3600 DNSKEY 256 3 5 deadbeaf"), newRRSIG("skydns.test. 3600 RRSIG DNSKEY 5 2 3600 0 0 51945 skydns.test. deadbeaf"), }, Extra: []dns.RR{new(dns.OPT)}, }, // Signed Response Test { dnssec: true, Qname: "104.server1.development.region1.skydns.test.", Qtype: dns.TypeSRV, Answer: []dns.RR{ newRRSIG("104.server1.development.region1.skydns.test. 3600 RRSIG SRV 5 6 3600 0 0 51945 skydns.test. deadbeaf"), newSRV("104.server1.development.region1.skydns.test. 3600 SRV 10 100 0 104.server1.development.region1.skydns.test.")}, Extra: []dns.RR{ newRRSIG("104.server1.developmen.region1.skydns.test. 3600 RRSIG A 5 6 3600 0 0 51945 skydns.test. deadbeaf"), newA("104.server1.development.region1.skydns.test. 3600 A 10.0.0.1"), new(dns.OPT), }, }, // Signed Response Test, ask twice to check cache { dnssec: true, Qname: "104.server1.development.region1.skydns.test.", Qtype: dns.TypeSRV, Answer: []dns.RR{ newRRSIG("104.server1.development.region1.skydns.test. 3600 RRSIG SRV 5 6 3600 0 0 51945 skydns.test. deadbeaf"), newSRV("104.server1.development.region1.skydns.test. 3600 SRV 10 100 0 104.server1.development.region1.skydns.test.")}, Extra: []dns.RR{ newRRSIG("104.server1.developmen.region1.skydns.test. 3600 RRSIG A 5 6 3600 0 0 51945 skydns.test. deadbeaf"), newA("104.server1.development.region1.skydns.test. 3600 A 10.0.0.1"), new(dns.OPT), }, }, // NXDOMAIN Test { dnssec: true, Qname: "doesnotexist.skydns.test.", Qtype: dns.TypeA, Rcode: dns.RcodeNameError, Ns: []dns.RR{ newNSEC3("44ohaq2njb0idnvolt9ggthvsk1e1uv8.skydns.test. 60 NSEC3 1 0 0 - 44OHAQ2NJB0IDNVOLT9GGTHVSK1E1UVA"), newRRSIG("44ohaq2njb0idnvolt9ggthvsk1e1uv8.skydns.test. 60 RRSIG NSEC3 5 3 3600 20140814205559 20140807175559 51945 skydns.test. deadbeef"), newNSEC3("ah4v7g5qoiri26armrb3bldqi1sng6a2.skydns.test. 60 NSEC3 1 0 0 - AH4V7G5QOIRI26ARMRB3BLDQI1SNG6A3 A AAAA SRV RRSIG"), newRRSIG("ah4v7g5qoiri26armrb3bldqi1sng6a2.skydns.test. 60 RRSIG NSEC3 5 3 3600 20140814205559 20140807175559 51945 skydns.test. deadbeef"), newNSEC3("lksd858f4cldl7emdord75k5jeks49p8.skydns.test. 60 NSEC3 1 0 0 - LKSD858F4CLDL7EMDORD75K5JEKS49PA"), newRRSIG("lksd858f4cldl7emdord75k5jeks49p8.skydns.test. 60 RRSIG NSEC3 5 3 3600 20140814205559 20140807175559 51945 skydns.test. deadbeef"), newRRSIG("skydns.test. 60 RRSIG SOA 5 2 3600 20140814205559 20140807175559 51945 skydns.test. deadbeaf"), newSOA("skydns.test. 3600 SOA ns.dns.skydns.test. hostmaster.skydns.test. 0 0 0 0 0"), }, Extra: []dns.RR{new(dns.OPT)}, }, // NXDOMAIN Test, cache test { dnssec: true, Qname: "doesnotexist.skydns.test.", Qtype: dns.TypeA, Rcode: dns.RcodeNameError, Ns: []dns.RR{ newNSEC3("44ohaq2njb0idnvolt9ggthvsk1e1uv8.skydns.test. 60 NSEC3 1 0 0 - 44OHAQ2NJB0IDNVOLT9GGTHVSK1E1UVA"), newRRSIG("44ohaq2njb0idnvolt9ggthvsk1e1uv8.skydns.test. 60 RRSIG NSEC3 5 3 3600 20140814205559 20140807175559 51945 skydns.test. deadbeef"), newNSEC3("ah4v7g5qoiri26armrb3bldqi1sng6a2.skydns.test. 60 NSEC3 1 0 0 - AH4V7G5QOIRI26ARMRB3BLDQI1SNG6A3 A AAAA SRV RRSIG"), newRRSIG("ah4v7g5qoiri26armrb3bldqi1sng6a2.skydns.test. 60 RRSIG NSEC3 5 3 3600 20140814205559 20140807175559 51945 skydns.test. deadbeef"), newNSEC3("lksd858f4cldl7emdord75k5jeks49p8.skydns.test. 60 NSEC3 1 0 0 - LKSD858F4CLDL7EMDORD75K5JEKS49PA"), newRRSIG("lksd858f4cldl7emdord75k5jeks49p8.skydns.test. 60 RRSIG NSEC3 5 3 3600 20140814205559 20140807175559 51945 skydns.test. deadbeef"), newRRSIG("skydns.test. 60 RRSIG SOA 5 2 3600 20140814205559 20140807175559 51945 skydns.test. deadbeaf"), newSOA("skydns.test. 3600 SOA ns.dns.skydns.test. hostmaster.skydns.test. 0 0 0 0 0"), }, Extra: []dns.RR{new(dns.OPT)}, }, // NODATA Test { dnssec: true, Qname: "104.server1.development.region1.skydns.test.", Qtype: dns.TypeTXT, Rcode: dns.RcodeSuccess, Ns: []dns.RR{ newNSEC3("E76CLEL5E7TQHRTFLTBVH0645NEKFJV9.skydns.test. 60 NSEC3 1 0 0 - E76CLEL5E7TQHRTFLTBVH0645NEKFJVA A AAAA SRV RRSIG"), newRRSIG("E76CLEL5E7TQHRTFLTBVH0645NEKFJV9.skydns.test. 60 RRSIG NSEC3 5 3 3600 20140814211641 20140807181641 51945 skydns.test. deadbeef"), newRRSIG("skydns.test. 60 RRSIG SOA 5 2 3600 20140814211641 20140807181641 51945 skydns.test. deadbeef"), newSOA("skydns.test. 60 SOA ns.dns.skydns.test. hostmaster.skydns.test. 1407445200 28800 7200 604800 60"), }, Extra: []dns.RR{new(dns.OPT)}, }, // Reverse v4 local answer { Qname: "1.0.0.10.in-addr.arpa.", Qtype: dns.TypePTR, Answer: []dns.RR{newPTR("1.0.0.10.in-addr.arpa. 3600 PTR reverse.example.com.")}, }, // Reverse v6 local answer // Reverse forwarding answer, TODO(miek) does not work // { // Qname: "1.0.16.172.in-addr.arpa.", Qtype: dns.TypePTR, // Rcode: dns.RcodeNameError, // Ns: []dns.RR{newSOA("16.172.in-addr.arpa. 10800 SOA localhost. nobody.invalid. 0 0 0 0 0")}, // }, // Reverse no answer // Local data query { Qname: "local.dns.skydns.test.", Qtype: dns.TypeA, Answer: []dns.RR{newA("local.dns.skydns.test. 3600 A 10.0.0.1")}, }, // Author test { Qname: "skydns.test.", Qtype: dns.TypeTXT, chaos: true, Answer: []dns.RR{ newTXT("skydns.test. 0 TXT \"Brian Ketelsen\""), newTXT("skydns.test. 0 TXT \"Erik St. Martin\""), newTXT("skydns.test. 0 TXT \"Michael Crosby\""), newTXT("skydns.test. 0 TXT \"Miek Gieben\""), }, }, // Author test 2 { Qname: "authors.bind.", Qtype: dns.TypeTXT, chaos: true, Answer: []dns.RR{ newTXT("authors.bind. 0 TXT \"Brian Ketelsen\""), newTXT("authors.bind. 0 TXT \"Erik St. Martin\""), newTXT("authors.bind. 0 TXT \"Michael Crosby\""), newTXT("authors.bind. 0 TXT \"Miek Gieben\""), }, }, // Author test, caps test { Qname: "AUTHOrs.BIND.", Qtype: dns.TypeTXT, chaos: true, Answer: []dns.RR{ newTXT("AUTHOrs.BIND. 0 TXT \"Brian Ketelsen\""), newTXT("AUTHOrs.BIND. 0 TXT \"Erik St. Martin\""), newTXT("AUTHOrs.BIND. 0 TXT \"Michael Crosby\""), newTXT("AUTHOrs.BIND. 0 TXT \"Miek Gieben\""), }, }, // Author test 3, no answer. { Qname: "local.dns.skydns.test.", Qtype: dns.TypeA, Rcode: dns.RcodeServerFailure, chaos: true, }, // HINFO Test, should be nodata for the apex { Qname: "skydns.test.", Qtype: dns.TypeHINFO, Ns: []dns.RR{newSOA("skydns.test. 3600 SOA ns.dns.skydns.test. hostmaster.skydns.test. 0 0 0 0 0")}, }, // One IP, two ports open, ask for the IP only. { Qname: "bar.skydns.test.", Qtype: dns.TypeA, Answer: []dns.RR{ newA("bar.skydns.test. 3600 A 192.168.0.1"), }, }, // Then ask for the SRV records. { Qname: "bar.skydns.test.", Qtype: dns.TypeSRV, Answer: []dns.RR{ newSRV("bar.skydns.test. 3600 SRV 10 50 443 bar.skydns.local."), // Issue 144 says x.bar.skydns.test should be bar.skydns.test newSRV("bar.skydns.test. 3600 SRV 10 50 80 bar.skydns.test."), }, Extra: []dns.RR{ newA("bar.skydns.test. 3600 A 192.168.0.1"), }, }, // Duplicate IP address test { Qname: "multiport.http.skydns.test.", Qtype: dns.TypeA, Answer: []dns.RR{newA("multiport.http.skydns.test. IN A 10.11.11.10")}, }, // Casing test { Qname: "uppeR.skydns.test.", Qtype: dns.TypeA, Answer: []dns.RR{newA("uppeR.skydns.test. IN A 127.0.0.1")}, }, { Qname: "upper.skydns.test.", Qtype: dns.TypeA, Answer: []dns.RR{newA("upper.skydns.test. IN A 127.0.0.1")}, }, // SRV record with name that is internally resolvable. { Qname: "1.cname.skydns.test.", Qtype: dns.TypeSRV, Answer: []dns.RR{newSRV("1.cname.skydns.test. IN SRV 10 100 0 104.server1.development.region1.skydns.test.")}, Extra: []dns.RR{newA("104.server1.development.region1.skydns.test. IN A 10.0.0.1")}, }, // SRV record with name that is internally resolvable. Get v4 and v6 records. { Qname: "both.v4v6.test.skydns.test.", Qtype: dns.TypeSRV, Answer: []dns.RR{newSRV("both.v4v6.test.skydns.test. IN SRV 10 100 0 ipaddr2.skydns.test.")}, Extra: []dns.RR{ newA("ipaddr2.skydns.test. IN A 172.16.1.1"), newAAAA("ipaddr2.skydns.test. IN AAAA 2001::8:8:8:8"), }, }, // MX Tests { // NODATA as this is not an Mail: true record. Qname: "100.server1.development.region1.skydns.test.", Qtype: dns.TypeMX, Ns: []dns.RR{ newSOA("skydns.test. 3600 SOA ns.dns.skydns.test. hostmaster.skydns.test. 0 0 0 0 0"), }, }, { Qname: "b.mail.skydns.test.", Qtype: dns.TypeMX, Answer: []dns.RR{newMX("b.mail.skydns.test. IN MX 50 mx.miek.nl.")}, }, { // See issue #168 Qname: "a.mail.skydns.test.", Qtype: dns.TypeMX, Answer: []dns.RR{newMX("a.mail.skydns.test. IN MX 50 mx.skydns.test.")}, Extra: []dns.RR{ newA("a.ipaddr.skydns.test. IN A 172.16.1.1"), newCNAME("mx.skydns.tests. IN CNAME a.ipaddr.skydns.test."), }, }, { Qname: "mx.skydns.test.", Qtype: dns.TypeMX, Answer: []dns.RR{ newMX("mx.skydns.test. IN MX 30 a.ipaddr.skydns.test."), }, Extra: []dns.RR{ newA("a.ipaddr.skydns.test. A 172.16.1.1"), }, }, // Double CNAMEs in the additional { Qname: "a.mail2.skydns.test.", Qtype: dns.TypeMX, Answer: []dns.RR{ newMX("a.mail2.skydns.test. IN MX 50 mx2.skydns.test."), }, Extra: []dns.RR{ newA("a.ipaddr.skydns.test. A 172.16.1.1"), newA("b.ipaddr.skydns.test. A 172.16.1.2"), // only one CNAME can be here, if we round-robin we randomly choose // without it, pick the first newCNAME("mx2.skydns.test. CNAME b.ipaddr.skydns.test."), }, }, // Groups { // hits the group 'g1' and only includes those records Qname: "dom.skydns.test.", Qtype: dns.TypeA, Answer: []dns.RR{ newA("dom.skydns.test. IN A 127.0.0.1"), newA("dom.skydns.test. IN A 127.0.0.2"), }, }, { // One has group, the other has not... Include the non-group always. Qname: "dom2.skydns.test.", Qtype: dns.TypeA, Answer: []dns.RR{ newA("dom2.skydns.test. IN A 127.0.0.1"), newA("dom2.skydns.test. IN A 127.0.0.2"), }, }, { // The groups differ. Qname: "dom1.skydns.test.", Qtype: dns.TypeA, Answer: []dns.RR{ newA("dom1.skydns.test. IN A 127.0.0.1"), }, }, } func newA(rr string) *dns.A { r, _ := dns.NewRR(rr); return r.(*dns.A) } func newAAAA(rr string) *dns.AAAA { r, _ := dns.NewRR(rr); return r.(*dns.AAAA) } func newCNAME(rr string) *dns.CNAME { r, _ := dns.NewRR(rr); return r.(*dns.CNAME) } func newSRV(rr string) *dns.SRV { r, _ := dns.NewRR(rr); return r.(*dns.SRV) } func newSOA(rr string) *dns.SOA { r, _ := dns.NewRR(rr); return r.(*dns.SOA) } func newNS(rr string) *dns.NS { r, _ := dns.NewRR(rr); return r.(*dns.NS) } func newDNSKEY(rr string) *dns.DNSKEY { r, _ := dns.NewRR(rr); return r.(*dns.DNSKEY) } func newRRSIG(rr string) *dns.RRSIG { r, _ := dns.NewRR(rr); return r.(*dns.RRSIG) } func newNSEC3(rr string) *dns.NSEC3 { r, _ := dns.NewRR(rr); return r.(*dns.NSEC3) } func newPTR(rr string) *dns.PTR { r, _ := dns.NewRR(rr); return r.(*dns.PTR) } func newTXT(rr string) *dns.TXT { r, _ := dns.NewRR(rr); return r.(*dns.TXT) } func newMX(rr string) *dns.MX { r, _ := dns.NewRR(rr); return r.(*dns.MX) } func TestDedup(t *testing.T) { m := new(dns.Msg) m.Answer = []dns.RR{ newA("svc.ns.kubernetes.local. IN A 3.3.3.3"), newA("svc.ns.kubernetes.local. IN A 2.2.2.2"), newA("svc.ns.kubernetes.local. IN A 3.3.3.3"), newA("svc.ns.kubernetes.local. IN A 2.2.2.2"), newA("svc.ns.kubernetes.local. IN A 1.1.1.1"), newA("svc.ns.kubernetes.local. IN A 1.1.1.1"), } s := &server{} m = s.dedup(m) sort.Sort(rrSet(m.Answer)) if len(m.Answer) != 3 { t.Fatalf("failing dedup: should have collapsed it to 3 records") } if dns.Field(m.Answer[0], 1) != "1.1.1.1" || dns.Field(m.Answer[1], 1) != "2.2.2.2" || dns.Field(m.Answer[2], 1) != "3.3.3.3" { t.Fatalf("failing dedup: %s", m) } } func TestTargetStripAdditional(t *testing.T) { s := newTestServer(t, false) defer s.Stop() c := new(dns.Client) m := new(dns.Msg) pre := "bliep." expected := "blaat.skydns.test." serv := &msg.Service{ Host: "199.43.132.53", Key: pre + expected, TargetStrip: 1, Text: "Text", } addService(t, s, serv.Key, 0, serv) defer delService(t, s, serv.Key) m.SetQuestion(pre+expected, dns.TypeSRV) resp, _, err := c.Exchange(m, "127.0.0.1:"+StrPort) if err != nil { t.Fatal(err) } t.Logf("%s", resp.String()) if resp.Extra[0].Header().Name != expected { t.Fatalf("expected %s, got %s for SRV with v4", expected, resp.Extra[0].Header().Name) } serv = &msg.Service{ Host: "2001::1", Key: pre + expected, TargetStrip: 1, Text: "Text", } delService(t, s, serv.Key) // previous defer still stands addService(t, s, serv.Key, 0, serv) m.SetQuestion(pre+expected, dns.TypeSRV) resp, _, err = c.Exchange(m, "127.0.0.1:"+StrPort) if err != nil { t.Fatal(err) } t.Logf("%s", resp.String()) if resp.Extra[0].Header().Name != expected { t.Fatalf("expected %s, got %s for SRV with v6", expected, resp.Extra[0].Header().Name) } } func TestMsgOverflow(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode.") } s := newTestServer(t, false) defer s.Stop() c := new(dns.Client) m := new(dns.Msg) for i := 0; i < 2000; i++ { is := strconv.Itoa(i) m := &msg.Service{ Host: "2001::" + is, Key: "machine" + is + ".machines.skydns.test.", } addService(t, s, m.Key, 0, m) defer delService(t, s, m.Key) } m.SetQuestion("machines.skydns.test.", dns.TypeSRV) resp, _, err := c.Exchange(m, "127.0.0.1:"+StrPort) if err != nil { // Unpack can fail, and it should (i.e. msg too large) t.Logf("%s", err) return } t.Logf("%s", resp) if resp.Rcode != dns.RcodeSuccess { t.Fatalf("expecting server failure, got %d", resp.Rcode) } } func BenchmarkDNSSingleCache(b *testing.B) { b.StopTimer() t := new(testing.T) s := newTestServerDNSSEC(t, true) defer s.Stop() serv := services[0] addService(t, s, serv.Key, 0, serv) defer delService(t, s, serv.Key) c := new(dns.Client) tc := dnsTestCases[0] m := new(dns.Msg) m.SetQuestion(tc.Qname, tc.Qtype) b.StartTimer() for i := 0; i < b.N; i++ { c.Exchange(m, "127.0.0.1:"+StrPort) } } func BenchmarkDNSWildcardCache(b *testing.B) { b.StopTimer() t := new(testing.T) s := newTestServerDNSSEC(t, true) defer s.Stop() for _, serv := range services { m := &msg.Service{Host: serv.Host, Port: serv.Port} addService(t, s, serv.Key, 0, m) defer delService(t, s, serv.Key) } c := new(dns.Client) tc := dnsTestCases[8] // Wildcard Test m := new(dns.Msg) m.SetQuestion(tc.Qname, tc.Qtype) b.StartTimer() for i := 0; i < b.N; i++ { c.Exchange(m, "127.0.0.1:"+StrPort) } } func BenchmarkDNSSECSingleCache(b *testing.B) { b.StopTimer() t := new(testing.T) s := newTestServerDNSSEC(t, true) defer s.Stop() serv := services[0] addService(t, s, serv.Key, 0, serv) defer delService(t, s, serv.Key) c := new(dns.Client) tc := dnsTestCases[0] m := new(dns.Msg) m.SetQuestion(tc.Qname, tc.Qtype) m.SetEdns0(4096, true) b.StartTimer() for i := 0; i < b.N; i++ { c.Exchange(m, "127.0.0.1:"+StrPort) } } func BenchmarkDNSSingleNoCache(b *testing.B) { b.StopTimer() t := new(testing.T) s := newTestServerDNSSEC(t, false) defer s.Stop() serv := services[0] addService(t, s, serv.Key, 0, serv) defer delService(t, s, serv.Key) c := new(dns.Client) tc := dnsTestCases[0] m := new(dns.Msg) m.SetQuestion(tc.Qname, tc.Qtype) b.StartTimer() for i := 0; i < b.N; i++ { c.Exchange(m, "127.0.0.1:"+StrPort) } } func BenchmarkDNSWildcardNoCache(b *testing.B) { b.StopTimer() t := new(testing.T) s := newTestServerDNSSEC(t, false) defer s.Stop() for _, serv := range services { m := &msg.Service{Host: serv.Host, Port: serv.Port} addService(t, s, serv.Key, 0, m) defer delService(t, s, serv.Key) } c := new(dns.Client) tc := dnsTestCases[8] // Wildcard Test m := new(dns.Msg) m.SetQuestion(tc.Qname, tc.Qtype) b.StartTimer() for i := 0; i < b.N; i++ { c.Exchange(m, "127.0.0.1:"+StrPort) } } func BenchmarkDNSSECSingleNoCache(b *testing.B) { b.StopTimer() t := new(testing.T) s := newTestServerDNSSEC(t, false) defer s.Stop() serv := services[0] addService(t, s, serv.Key, 0, serv) defer delService(t, s, serv.Key) c := new(dns.Client) tc := dnsTestCases[0] m := new(dns.Msg) m.SetQuestion(tc.Qname, tc.Qtype) m.SetEdns0(4096, true) b.StartTimer() for i := 0; i < b.N; i++ { c.Exchange(m, "127.0.0.1:"+StrPort) } } skydns/server/stub.go000644 000765 000024 00000006537 13063661551 015355 0ustar00tpotstaff000000 000000 // Copyright (c) 2014 The SkyDNS Authors. All rights reserved. // Use of this source code is governed by The MIT License (MIT) that can be // found in the LICENSE file. package server import ( "net" "strconv" "strings" "github.com/miekg/dns" "github.com/skynetservices/skydns/msg" ) const ednsStubCode = dns.EDNS0LOCALSTART + 10 // ednsStub is the EDNS0 record we add to stub queries. Queries which have this record are // not forwarded again. var ednsStub = func() *dns.OPT { o := new(dns.OPT) o.Hdr.Name = "." o.Hdr.Rrtype = dns.TypeOPT e := new(dns.EDNS0_LOCAL) e.Code = ednsStubCode e.Data = []byte{1} o.Option = append(o.Option, e) return o }() // Look in .../dns/stub//xx for msg.Services. Loop through them // extract and add them as forwarders (ip:port-combos) for // the stub zones. Only numeric (i.e. IP address) hosts are used. func (s *server) UpdateStubZones() { stubmap := make(map[string][]string) services, err := s.backend.Records("stub.dns."+s.config.Domain, false) if err != nil { logf("stub zone update failed: %s", err) return } for _, serv := range services { if serv.Port == 0 { serv.Port = 53 } ip := net.ParseIP(serv.Host) if ip == nil { logf("stub zone non-address %s seen for: %s", serv.Key, serv.Host) continue } domain := msg.Domain(serv.Key) // Chop of left most label, because that is used as the nameserver place holder // and drop the right most labels that belong to localDomain. labels := dns.SplitDomainName(domain) domain = dns.Fqdn(strings.Join(labels[1:len(labels)-dns.CountLabel(s.config.localDomain)], ".")) // If the remaining name equals s.config.LocalDomain we ignore it. if domain == s.config.localDomain { logf("not adding stub zone for my own domain") continue } stubmap[domain] = append(stubmap[domain], net.JoinHostPort(serv.Host, strconv.Itoa(serv.Port))) } s.config.stub = &stubmap } // ServeDNSStubForward forwards a request to a nameservers and returns the response. func (s *server) ServeDNSStubForward(w dns.ResponseWriter, req *dns.Msg, ns []string) *dns.Msg { // Check EDNS0 Stub option, if set drop the packet. option := req.IsEdns0() if option != nil { for _, o := range option.Option { if o.Option() == ednsStubCode && len(o.(*dns.EDNS0_LOCAL).Data) == 1 && o.(*dns.EDNS0_LOCAL).Data[0] == 1 { // Maybe log source IP here? logf("not fowarding stub request to another stub") return nil } } } // Add a custom EDNS0 option to the packet, so we can detect loops // when 2 stubs are forwarding to each other. if option != nil { option.Option = append(option.Option, &dns.EDNS0_LOCAL{ednsStubCode, []byte{1}}) } else { req.Extra = append(req.Extra, ednsStub) } var ( r *dns.Msg err error ) // Use request Id for "random" nameserver selection. nsid := int(req.Id) % len(ns) try := 0 Redo: if isTCP(w) { r, err = exchangeWithRetry(s.dnsTCPclient, req, ns[nsid]) } else { r, err = exchangeWithRetry(s.dnsUDPclient, req, ns[nsid]) } if err == nil { r.Compress = true r.Id = req.Id w.WriteMsg(r) return r } // Seen an error, this can only mean, "server not reached", try again // but only if we have not exausted our nameservers. if try < len(ns) { try++ nsid = (nsid + 1) % len(ns) goto Redo } logf("failure to forward stub request %q", err) m := s.ServerFailure(req) w.WriteMsg(m) return m } skydns/msg/service.go000644 000765 000024 00000016011 12733124226 015300 0ustar00tpotstaff000000 000000 // Copyright (c) 2014 The SkyDNS Authors. All rights reserved. // Use of this source code is governed by The MIT License (MIT) that can be // found in the LICENSE file. package msg import ( "net" "path" "strings" "github.com/miekg/dns" ) // PathPrefix is the prefix used to store SkyDNS data in the backend. // It defaults to `skydns`. // You can change it by set `path-prefix` configuration or SKYDNS_PATH_PREFIX env. variable. // Then: // The SkyDNS's configuration object should be stored under the key "/mydns/config"; // The etcd path of domain `service.staging.skydns.local.` will be "/mydns/local/skydns/staging/service". var PathPrefix string = "skydns" // This *is* the rdata from a SRV record, but with a twist. // Host (Target in SRV) must be a domain name, but if it looks like an IP // address (4/6), we will treat it like an IP address. type Service struct { Host string `json:"host,omitempty"` Port int `json:"port,omitempty"` Priority int `json:"priority,omitempty"` Weight int `json:"weight,omitempty"` Text string `json:"text,omitempty"` Mail bool `json:"mail,omitempty"` // Be an MX record. Priority becomes Preference. Ttl uint32 `json:"ttl,omitempty"` // When a SRV record with a "Host: IP-address" is added, we synthesize // a srv.Target domain name. Normally we convert the full Key where // the record lives to a DNS name and use this as the srv.Target. When // TargetStrip > 0 we strip the left most TargetStrip labels from the // DNS name. TargetStrip int `json:"targetstrip,omitempty"` // Group is used to group (or *not* to group) different services // together. Services with an identical Group are returned in the same // answer. Group string `json:"group,omitempty"` // Etcd key where we found this service and ignored from json un-/marshalling Key string `json:"-"` } // NewSRV returns a new SRV record based on the Service. func (s *Service) NewSRV(name string, weight uint16) *dns.SRV { host := targetStrip(dns.Fqdn(s.Host), s.TargetStrip) return &dns.SRV{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeSRV, Class: dns.ClassINET, Ttl: s.Ttl}, Priority: uint16(s.Priority), Weight: weight, Port: uint16(s.Port), Target: host} } // NewMX returns a new MX record based on the Service. func (s *Service) NewMX(name string) *dns.MX { host := targetStrip(dns.Fqdn(s.Host), s.TargetStrip) return &dns.MX{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeMX, Class: dns.ClassINET, Ttl: s.Ttl}, Preference: uint16(s.Priority), Mx: host} } // NewA returns a new A record based on the Service. func (s *Service) NewA(name string, ip net.IP) *dns.A { return &dns.A{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: s.Ttl}, A: ip} } // NewAAAA returns a new AAAA record based on the Service. func (s *Service) NewAAAA(name string, ip net.IP) *dns.AAAA { return &dns.AAAA{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: s.Ttl}, AAAA: ip} } // NewCNAME returns a new CNAME record based on the Service. func (s *Service) NewCNAME(name string, target string) *dns.CNAME { return &dns.CNAME{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: s.Ttl}, Target: target} } // NewNS returns a new NS record based on the Service. func (s *Service) NewNS(name string, target string) *dns.NS { return &dns.NS{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: s.Ttl}, Ns: target} } // NewTXT returns a new TXT record based on the Service. func (s *Service) NewTXT(name string) *dns.TXT { return &dns.TXT{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: s.Ttl}, Txt: split255(s.Text)} } // NewPTR returns a new PTR record based on the Service. func (s *Service) NewPTR(name string, ttl uint32) *dns.PTR { return &dns.PTR{Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypePTR, Class: dns.ClassINET, Ttl: ttl}, Ptr: dns.Fqdn(s.Host)} } // As Path, but if a name contains wildcards (* or any), the name will be // chopped of before the (first) wildcard, and we do a highler evel search and // later find the matching names. So service.*.skydns.local, will look for all // services under skydns.local and will later check for names that match // service.*.skydns.local. If a wildcard is found the returned bool is true. func PathWithWildcard(s string) (string, bool) { l := dns.SplitDomainName(s) for i, j := 0, len(l)-1; i < j; i, j = i+1, j-1 { l[i], l[j] = l[j], l[i] } for i, k := range l { if k == "*" || k == "any" { return path.Join(append([]string{"/" + PathPrefix + "/"}, l[:i]...)...), true } } return path.Join(append([]string{"/" + PathPrefix + "/"}, l...)...), false } // Path converts a domainname to an etcd path. If s looks like service.staging.skydns.local., // the resulting key will be /skydns/local/skydns/staging/service . func Path(s string) string { l := dns.SplitDomainName(s) for i, j := 0, len(l)-1; i < j; i, j = i+1, j-1 { l[i], l[j] = l[j], l[i] } return path.Join(append([]string{"/" + PathPrefix + "/"}, l...)...) } // Domain is the opposite of Path. func Domain(s string) string { l := strings.Split(s, "/") // start with 1, to strip /skydns for i, j := 1, len(l)-1; i < j; i, j = i+1, j-1 { l[i], l[j] = l[j], l[i] } return dns.Fqdn(strings.Join(l[1:len(l)-1], ".")) } // Group checks the services in sx, it looks for a Group attribute on the shortest // keys. If there are multiple shortest keys *and* the group attribute disagrees (and // is not empty), we don't consider it a group. // If a group is found, only services with *that* group (or no group) will be returned. func Group(sx []Service) []Service { if len(sx) == 0 { return sx } // Shortest key with group attribute sets the group for this set. group := sx[0].Group slashes := strings.Count(sx[0].Key, "/") length := make([]int, len(sx)) for i, s := range sx { x := strings.Count(s.Key, "/") length[i] = x if x < slashes { if s.Group == "" { break } slashes = x group = s.Group } } if group == "" { return sx } ret := []Service{} // with slice-tricks in sx we can prolly save this allocation (TODO) for i, s := range sx { if s.Group == "" { ret = append(ret, s) continue } // Disagreement on the same level if length[i] == slashes && s.Group != group { return sx } if s.Group == group { ret = append(ret, s) } } return ret } // Split255 splits a string into 255 byte chunks. func split255(s string) []string { if len(s) < 255 { return []string{s} } sx := []string{} p, i := 0, 255 for { if i <= len(s) { sx = append(sx, s[p:i]) } else { sx = append(sx, s[p:]) break } p, i = p+255, i+255 } return sx } // targetStrip strips "targetstrip" labels from the left side of the fully qualified name. func targetStrip(name string, targetStrip int) string { if targetStrip == 0 { return name } offset, end := 0, false for i := 0; i < targetStrip; i++ { offset, end = dns.NextLabel(name, offset) } if end { // We overshot the name, use the orignal one. offset = 0 } name = name[offset:] return name } skydns/msg/service_test.go000644 000765 000024 00000007571 12733124226 016352 0ustar00tpotstaff000000 000000 // Copyright (c) 2015 The SkyDNS Authors. All rights reserved. // Use of this source code is governed by The MIT License (MIT) that can be // found in the LICENSE file. package msg import "testing" func TestPath(t *testing.T) { PathPrefix = "mydns" result := Path("service.staging.skydns.local.") if result != "/mydns/local/skydns/staging/service" { t.Logf("Failure to get domain's path with prefix: mydns") t.Fail() } PathPrefix = "skydns" result = Path("service.staging.skydns.local.") if result != "/skydns/local/skydns/staging/service" { t.Logf("Failure to get domain's path with default prefix: skydns") t.Fail() } } func TestSplit255(t *testing.T) { xs := split255("abc") if len(xs) != 1 && xs[0] != "abc" { t.Logf("Failure to split abc") t.Fail() } s := "" for i := 0; i < 255; i++ { s += "a" } xs = split255(s) if len(xs) != 1 && xs[0] != s { t.Logf("failure to split 255 char long string") t.Logf("%s %v\n", s, xs) t.Fail() } s += "b" xs = split255(s) if len(xs) != 2 || xs[1] != "b" { t.Logf("failure to split 256 char long string: %d", len(xs)) t.Logf("%s %v\n", s, xs) t.Fail() } for i := 0; i < 255; i++ { s += "a" } xs = split255(s) if len(xs) != 3 || xs[2] != "a" { t.Logf("failure to split 510 char long string: %d", len(xs)) t.Logf("%s %v\n", s, xs) t.Fail() } } func TestGroup(t *testing.T) { // Key are in the wrong order, but for this test it does not matter. sx := Group( []Service{ {Host: "127.0.0.1", Group: "g1", Key: "b/sub/dom1/skydns/test"}, {Host: "127.0.0.2", Group: "g2", Key: "a/dom1/skydns/test"}, }, ) // Expecting to return the shortest key with a Group attribute. if len(sx) != 1 { t.Fatalf("failure to group zeroth set: %v", sx) } if sx[0].Key != "a/dom1/skydns/test" { t.Fatalf("failure to group zeroth set: %v, wrong Key", sx) } // Groups disagree, so we will not do anything. sx = Group( []Service{ {Host: "server1", Group: "g1", Key: "region1/skydns/test"}, {Host: "server2", Group: "g2", Key: "region1/skydns/test"}, }, ) if len(sx) != 2 { t.Fatalf("failure to group first set: %v", sx) } // Group is g1, include only the top-level one. sx = Group( []Service{ {Host: "server1", Group: "g1", Key: "a/dom/region1/skydns/test"}, {Host: "server2", Group: "g2", Key: "a/subdom/dom/region1/skydns/test"}, }, ) if len(sx) != 1 { t.Fatalf("failure to group second set: %v", sx) } // Groupless services must be included. sx = Group( []Service{ {Host: "server1", Group: "g1", Key: "a/dom/region1/skydns/test"}, {Host: "server2", Group: "g2", Key: "a/subdom/dom/region1/skydns/test"}, {Host: "server2", Group: "", Key: "b/subdom/dom/region1/skydns/test"}, }, ) if len(sx) != 2 { t.Fatalf("failure to group third set: %v", sx) } // Empty group on the highest level: include that one also. sx = Group( []Service{ {Host: "server1", Group: "g1", Key: "a/dom/region1/skydns/test"}, {Host: "server1", Group: "", Key: "b/dom/region1/skydns/test"}, {Host: "server2", Group: "g2", Key: "a/subdom/dom/region1/skydns/test"}, }, ) if len(sx) != 2 { t.Fatalf("failure to group fourth set: %v", sx) } // Empty group on the highest level: include that one also, and the rest. sx = Group( []Service{ {Host: "server1", Group: "g5", Key: "a/dom/region1/skydns/test"}, {Host: "server1", Group: "", Key: "b/dom/region1/skydns/test"}, {Host: "server2", Group: "g5", Key: "a/subdom/dom/region1/skydns/test"}, }, ) if len(sx) != 3 { t.Fatalf("failure to group fith set: %v", sx) } // One group. sx = Group( []Service{ {Host: "server1", Group: "g6", Key: "a/dom/region1/skydns/test"}, }, ) if len(sx) != 1 { t.Fatalf("failure to group sixth set: %v", sx) } // No group, once service sx = Group( []Service{ {Host: "server1", Key: "a/dom/region1/skydns/test"}, }, ) if len(sx) != 1 { t.Fatalf("failure to group seventh set: %v", sx) } } skydns/metrics/metrics.go000644 000765 000024 00000011337 13063661551 016200 0ustar00tpotstaff000000 000000 // Copyright (c) 2014 The SkyDNS Authors. All rights reserved. // Use of this source code is governed by The MIT License (MIT) that can be // found in the LICENSE file. package metrics import ( "fmt" "net/http" "os" "strconv" "time" "github.com/miekg/dns" "github.com/prometheus/client_golang/prometheus" ) var ( Port = os.Getenv("PROMETHEUS_PORT") Path = envOrDefault("PROMETHEUS_PATH", "/metrics") Namespace = envOrDefault("PROMETHEUS_NAMESPACE", "skydns") Subsystem = envOrDefault("PROMETHEUS_SUBSYSTEM", "skydns") requestCount *prometheus.CounterVec requestDuration *prometheus.HistogramVec responseSize *prometheus.HistogramVec errorCount *prometheus.CounterVec cacheMiss *prometheus.CounterVec ) type ( System string Cause string CacheType string ) var ( Auth System = "auth" Cache System = "cache" Rec System = "recursive" Reverse System = "reverse" Stub System = "stub" Nxdomain Cause = "nxdomain" Nodata Cause = "nodata" Truncated Cause = "truncated" Refused Cause = "refused" Overflow Cause = "overflow" Fail Cause = "servfail" Response CacheType = "response" Signature CacheType = "signature" ) func defineMetrics() { requestCount = prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: Namespace, Subsystem: Subsystem, Name: "dns_request_count_total", Help: "Counter of DNS requests made.", }, []string{"system"}) requestDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{ Namespace: Namespace, Subsystem: Subsystem, Name: "dns_request_duration_seconds", Help: "Histogram of the time (in seconds) each request took to resolve.", Buckets: append([]float64{0.001, 0.003}, prometheus.DefBuckets...), }, []string{"system"}) responseSize = prometheus.NewHistogramVec(prometheus.HistogramOpts{ Namespace: Namespace, Subsystem: Subsystem, Name: "dns_response_size_bytes", Help: "Size of the returns response in bytes.", Buckets: []float64{0, 512, 1024, 1500, 2048, 4096, 8192, 12288, 16384, 20480, 24576, 28672, 32768, 36864, 40960, 45056, 49152, 53248, 57344, 61440, 65536, }, }, []string{"system"}) errorCount = prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: Namespace, Subsystem: Subsystem, Name: "dns_error_count_total", Help: "Counter of DNS requests resulting in an error.", }, []string{"system", "cause"}) cacheMiss = prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: Namespace, Subsystem: Subsystem, Name: "dns_cachemiss_count_total", Help: "Counter of DNS requests that result in a cache miss.", }, []string{"cache"}) } // Metrics registers the DNS metrics to Prometheus, and starts the internal metrics // server if the environment variable PROMETHEUS_PORT is set. func Metrics() error { // We do this in a function instead of using var + init(), because we want to // able to set Namespace and/or Subsystem. if Port == "" { return nil } _, err := strconv.Atoi(Port) if err != nil { fmt.Errorf("bad port for prometheus: %s", Port) } defineMetrics() prometheus.MustRegister(requestCount) prometheus.MustRegister(requestDuration) prometheus.MustRegister(responseSize) prometheus.MustRegister(errorCount) prometheus.MustRegister(cacheMiss) http.Handle(Path, prometheus.Handler()) go func() { fmt.Errorf("%s", http.ListenAndServe(":"+Port, nil)) }() return nil } func ReportDuration(resp *dns.Msg, start time.Time, sys System) { if requestDuration == nil || responseSize == nil { return } rlen := float64(0) if resp != nil { rlen = float64(resp.Len()) } requestDuration.WithLabelValues(string(sys)).Observe(float64(time.Since(start)) / float64(time.Second)) responseSize.WithLabelValues(string(sys)).Observe(rlen) } func ReportRequestCount(req *dns.Msg, sys System) { if requestCount == nil { return } requestCount.WithLabelValues(string(sys)).Inc() } func ReportErrorCount(resp *dns.Msg, sys System) { if resp == nil || errorCount == nil { return } if resp.Truncated { errorCount.WithLabelValues(string(sys), string(Truncated)).Inc() return } if resp.Len() > dns.MaxMsgSize { errorCount.WithLabelValues(string(sys), string(Overflow)).Inc() return } switch resp.Rcode { case dns.RcodeServerFailure: errorCount.WithLabelValues(string(sys), string(Fail)).Inc() case dns.RcodeRefused: errorCount.WithLabelValues(string(sys), string(Refused)).Inc() case dns.RcodeNameError: errorCount.WithLabelValues(string(sys), string(Nxdomain)).Inc() // nodata ?? } } func ReportCacheMiss(ca CacheType) { if cacheMiss == nil { return } cacheMiss.WithLabelValues(string(ca)).Inc() } func envOrDefault(env, def string) string { e := os.Getenv(env) if e != "" { return e } return def } skydns/cache/cache.go000644 000765 000024 00000010164 12733124226 015163 0ustar00tpotstaff000000 000000 // Copyright (c) 2014 The SkyDNS Authors. All rights reserved. // Use of this source code is governed by The MIT License (MIT) that can be // found in the LICENSE file. package cache // Cache that holds RRs and for DNSSEC an RRSIG. // TODO(miek): there is a lot of copying going on to copy myself out of data // races. This should be optimized. import ( "crypto/sha1" "sync" "time" "github.com/miekg/dns" ) // Elem hold an answer and additional section that returned from the cache. // The signature is put in answer, extra is empty there. This wastes some memory. type elem struct { expiration time.Time // time added + TTL, after this the elem is invalid msg *dns.Msg } // Cache is a cache that holds on the a number of RRs or DNS messages. The cache // eviction is randomized. type Cache struct { sync.RWMutex capacity int m map[string]*elem ttl time.Duration } // New returns a new cache with the capacity and the ttl specified. func New(capacity, ttl int) *Cache { c := new(Cache) c.m = make(map[string]*elem) c.capacity = capacity c.ttl = time.Duration(ttl) * time.Second return c } func (c *Cache) Capacity() int { return c.capacity } func (c *Cache) Remove(s string) { c.Lock() delete(c.m, s) c.Unlock() } // EvictRandom removes a random member a the cache. // Must be called under a write lock. func (c *Cache) EvictRandom() { clen := len(c.m) if clen < c.capacity { return } i := c.capacity - clen for k, _ := range c.m { delete(c.m, k) i-- if i == 0 { break } } } // InsertMessage inserts a message in the Cache. We will cache it for ttl seconds, which // should be a small (60...300) integer. func (c *Cache) InsertMessage(s string, msg *dns.Msg) { if c.capacity <= 0 { return } c.Lock() if _, ok := c.m[s]; !ok { c.m[s] = &elem{time.Now().UTC().Add(c.ttl), msg.Copy()} } c.EvictRandom() c.Unlock() } // InsertSignature inserts a signature, the expiration time is used as the cache ttl. func (c *Cache) InsertSignature(s string, sig *dns.RRSIG) { if c.capacity <= 0 { return } c.Lock() if _, ok := c.m[s]; !ok { m := ((int64(sig.Expiration) - time.Now().Unix()) / (1 << 31)) - 1 if m < 0 { m = 0 } t := time.Unix(int64(sig.Expiration)-(m*(1<<31)), 0).UTC() c.m[s] = &elem{t, &dns.Msg{Answer: []dns.RR{dns.Copy(sig)}}} } c.EvictRandom() c.Unlock() } // Search returns a dns.Msg, the expiration time and a boolean indicating if we found something // in the cache. func (c *Cache) Search(s string) (*dns.Msg, time.Time, bool) { if c.capacity <= 0 { return nil, time.Time{}, false } c.RLock() if e, ok := c.m[s]; ok { e1 := e.msg.Copy() c.RUnlock() return e1, e.expiration, true } c.RUnlock() return nil, time.Time{}, false } // Key creates a hash key from a question section. It creates a different key // for requests with DNSSEC. func Key(q dns.Question, dnssec, tcp bool) string { h := sha1.New() i := append([]byte(q.Name), packUint16(q.Qtype)...) if dnssec { i = append(i, byte(255)) } if tcp { i = append(i, byte(254)) } return string(h.Sum(i)) } // Key uses the name, type and rdata, which is serialized and then hashed as the key for the lookup. func KeyRRset(rrs []dns.RR) string { h := sha1.New() i := []byte(rrs[0].Header().Name) i = append(i, packUint16(rrs[0].Header().Rrtype)...) for _, r := range rrs { switch t := r.(type) { // we only do a few type, serialize these manually case *dns.SOA: // We only fiddle with the serial so store that. i = append(i, packUint32(t.Serial)...) case *dns.SRV: i = append(i, packUint16(t.Priority)...) i = append(i, packUint16(t.Weight)...) i = append(i, packUint16(t.Weight)...) i = append(i, []byte(t.Target)...) case *dns.A: i = append(i, []byte(t.A)...) case *dns.AAAA: i = append(i, []byte(t.AAAA)...) case *dns.NSEC3: i = append(i, []byte(t.NextDomain)...) // Bitmap does not differentiate in SkyDNS. case *dns.DNSKEY: case *dns.NS: case *dns.TXT: } } return string(h.Sum(i)) } func packUint16(i uint16) []byte { return []byte{byte(i >> 8), byte(i)} } func packUint32(i uint32) []byte { return []byte{byte(i >> 24), byte(i >> 16), byte(i >> 8), byte(i)} } skydns/cache/cache_test.go000644 000765 000024 00000004653 12733124226 016230 0ustar00tpotstaff000000 000000 // Copyright (c) 2014 The SkyDNS Authors. All rights reserved. // Use of this source code is governed by The MIT License (MIT) that can be // found in the LICENSE file. package cache import ( "testing" "time" "github.com/miekg/dns" ) const testTTL = 2 type testcase struct { m *dns.Msg dnssec, tcp bool } func newMsg(zone string, typ uint16) *dns.Msg { m := &dns.Msg{} m.SetQuestion(zone, typ) return m } func TestInsertMessage(t *testing.T) { c := New(10, testTTL) testcases := []testcase{ {newMsg("miek.nl.", dns.TypeMX), false, false}, {newMsg("miek2.nl.", dns.TypeNS), false, false}, {newMsg("miek3.nl.", dns.TypeMX), true, false}, } for _, tc := range testcases { c.InsertMessage(Key(tc.m.Question[0], tc.dnssec, tc.tcp), tc.m) m1 := c.Hit(tc.m.Question[0], tc.dnssec, tc.tcp, tc.m.Id) if m1.Question[0].Qtype != tc.m.Question[0].Qtype { t.Fatalf("bad Qtype, expected %d, got %d:", tc.m.Question[0].Qtype, m1.Question[0].Qtype) } if m1.Question[0].Name != tc.m.Question[0].Name { t.Fatalf("bad Qtype, expected %s, got %s:", tc.m.Question[0].Name, m1.Question[0].Name) } m1 = c.Hit(tc.m.Question[0], !tc.dnssec, tc.tcp, tc.m.Id) if m1 != nil { t.Fatalf("bad cache hit, expected , got %s:", m1) } m1 = c.Hit(tc.m.Question[0], !tc.dnssec, !tc.tcp, tc.m.Id) if m1 != nil { t.Fatalf("bad cache hit, expected , got %s:", m1) } m1 = c.Hit(tc.m.Question[0], tc.dnssec, !tc.tcp, tc.m.Id) if m1 != nil { t.Fatalf("bad cache hit, expected , got %s:", m1) } } } func TestExpireMessage(t *testing.T) { c := New(10, testTTL-1) tc := testcase{newMsg("miek.nl.", dns.TypeMX), false, false} c.InsertMessage(Key(tc.m.Question[0], tc.dnssec, tc.tcp), tc.m) m1 := c.Hit(tc.m.Question[0], tc.dnssec, tc.tcp, tc.m.Id) if m1.Question[0].Qtype != tc.m.Question[0].Qtype { t.Fatalf("bad Qtype, expected %d, got %d:", tc.m.Question[0].Qtype, m1.Question[0].Qtype) } if m1.Question[0].Name != tc.m.Question[0].Name { t.Fatalf("bad Qtype, expected %s, got %s:", tc.m.Question[0].Name, m1.Question[0].Name) } time.Sleep(testTTL) m1 = c.Hit(tc.m.Question[0], tc.dnssec, tc.tcp, tc.m.Id) if m1.Question[0].Qtype != tc.m.Question[0].Qtype { t.Fatalf("bad Qtype, expected %d, got %d:", tc.m.Question[0].Qtype, m1.Question[0].Qtype) } if m1.Question[0].Name != tc.m.Question[0].Name { t.Fatalf("bad Qtype, expected %s, got %s:", tc.m.Question[0].Name, m1.Question[0].Name) } } skydns/cache/hit.go000644 000765 000024 00000001424 12733124226 014703 0ustar00tpotstaff000000 000000 // Copyright (c) 2014 The SkyDNS Authors. All rights reserved. // Use of this source code is governed by The MIT License (MIT) that can be // found in the LICENSE file. package cache import ( "time" "github.com/miekg/dns" ) // Hit returns a dns message from the cache. If the message's TTL is expired nil // is returned and the message is removed from the cache. func (c *Cache) Hit(question dns.Question, dnssec, tcp bool, msgid uint16) *dns.Msg { key := Key(question, dnssec, tcp) m1, exp, hit := c.Search(key) if hit { // Cache hit! \o/ if time.Since(exp) < 0 { m1.Id = msgid m1.Compress = true // Even if something ended up with the TC bit *in* the cache, set it to off m1.Truncated = false return m1 } // Expired! /o\ c.Remove(key) } return nil } skydns/backends/etcd/000755 000765 000024 00000000000 13063661551 015221 5ustar00tpotstaff000000 000000 skydns/backends/etcd/etcd.go000644 000765 000024 00000011156 13063661551 016473 0ustar00tpotstaff000000 000000 // Copyright (c) 2014 The SkyDNS Authors. All rights reserved. // Use of this source code is governed by The MIT License (MIT) that can be // found in the LICENSE file. // Package etcd provides the default SkyDNS server Backend implementation, // which looks up records stored under the `/skydns` key in etcd when queried. package etcd import ( "encoding/json" "fmt" "strings" "github.com/skynetservices/skydns/msg" "github.com/skynetservices/skydns/singleflight" etcd "github.com/coreos/etcd/client" "golang.org/x/net/context" ) // Config represents configuration for the Etcd backend - these values // should be taken directly from server.Config type Config struct { Ttl uint32 Priority uint16 } type Backend struct { client etcd.KeysAPI ctx context.Context config *Config inflight *singleflight.Group } // NewBackend returns a new Backend for SkyDNS, backed by etcd. func NewBackend(client etcd.KeysAPI, ctx context.Context, config *Config) *Backend { return &Backend{ client: client, ctx: ctx, config: config, inflight: &singleflight.Group{}, } } 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([]*etcd.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([]*etcd.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) (*etcd.Response, error) { resp, err := g.inflight.Do(path, func() (interface{}, error) { r, e := g.client.Get(g.ctx, path, &etcd.GetOptions{Sort: false, Recursive: recursive}) if e != nil { return nil, e } return r, e }) if err != nil { return nil, err } return resp.(*etcd.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 []*etcd.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 = int(g.config.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 *etcd.Node, serv *msg.Service) uint32 { etcdTtl := uint32(node.TTL) if etcdTtl == 0 && serv.Ttl == 0 { return g.config.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() etcd.KeysAPI { return g.client }