diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..68d70d1816fdf215dd9ca6d84895227130814095 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,97 @@ +variables: + DOCKER_DRIVER: overlay +services: + - labreg.arvan.me/docker:dind +before_script: + - ls + - pwd + - export GOPATH=/root/go + - export PATH=$PATH:/opt/go/bin + - mkdir -p $GOPATH/src/arvancloud/ + - ln -sf `pwd` $GOPATH/src/arvancloud/redins + +stages: + - test-lab + - test-production + - lab-deploy + - deploy-production + +test-lab: + stage: test-lab + image: golang:latest + services: + - name: redis:latest + alias: redis + script: + - echo test + - cd $GOPATH/src/arvancloud/redins + - go get + - go build + - go test ./... + artifacts: + paths: + - ./redins + only: + - dev + +test-production: + stage: test-production + image: golang:latest + services: + - name: redis:latest + alias: redis + script: + - echo test + - cd $GOPATH/src/arvancloud/redins + - go get + - go build + - go test ./... + artifacts: + paths: + - ./redins + only: + - master + only: + - tags + +image-build: + stage: lab-deploy + image: docker:latest + dependencies: + - test-lab + script: + - echo deploy-lab + - docker login labreg.arvan.me -u $REPO_USR -p $REPO_PWD + - docker build . -t labreg.arvan.me/arvan_redins:$CI_PIPELINE_ID -t labreg.arvan.me/arvan_redins:latest + - docker push labreg.arvan.me/arvan_redins:$CI_PIPELINE_ID + - docker push labreg.arvan.me/arvan_redins:latest + only: + - dev + +salt-apply: + image: labreg.arvan.me/ssh-client + stage: lab-deploy + script: + - mkdir -p ~/.ssh + - chmod 700 ~/.ssh + - echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config + - echo "$DEPLOY_SSH_KEY" > ~/.ssh/id_rsa + - chmod 600 ~/.ssh/id_rsa + - ssh -p 65422 gitlab-deploy@148.251.239.12 sudo salt-call state.apply docker/redins + only: + - dev + +deploy-production: + stage: deploy-production + image: docker:latest + dependencies: + - test-production + script: + - echo deploy-production + - docker login reg.arvan.me -u $PROD_REPO_USR -p $PROD_REPO_PWD + - docker build . -t reg.arvan.me/arvan_redins:${CI_COMMIT_TAG}-production + - docker push reg.arvan.me/arvan_redins:${CI_COMMIT_TAG}-production + only: + - master + only: + - tags diff --git a/Dockerfile b/Dockerfile index 349041fe442064c0ec7ab7be95de09305b318e4b..71f6edca65a12720fa83c08bcd8fd00887347d4f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ FROM alpine:latest RUN apk update && apk add libc6-compat ADD redins /usr/bin -ADD config.json /CORE/redins/etc/ +ADD template-config.json /CORE/redins/etc/config.json #RUN mkdir -p /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2 diff --git a/README.md b/README.md index 6594fbe95a714d477c0cfbfd94a33a3d195731b6..365efe39d251a5b8816d2f3709922989c6e3f15a 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,6 @@ - [CAA](#caa) - [PTR](#ptr) - [TLSA](#tlsa) - - [SOA](#soa) - [example](#zone-example) @@ -42,36 +41,47 @@ dns listening server configuration ~~~json -"server": { - "ip": "127.0.0.1", - "port": 1053, - "protocol": "udp" +{ + "server": { + "ip": "127.0.0.1", + "port": 1053, + "protocol": "udp", + "count": 1 + } } ~~~ -* ip : ip address to bind, default: 127.0.0.1 -* port : port number to bind, default: 1053 -* protocol : protocol; can be tcp or udp, default: udp +* `ip` : ip address to bind, default: 127.0.0.1 +* `port` : port number to bind, default: 1053 +* `protocol` : protocol; can be tcp or udp, default: udp +* `count` : number of listeners per address, default: 1 ### handler dns query handler configuration ~~~json +{ "handler": { "max_ttl": 300, "cache_timeout": 60, "zone_reload": 600, "log_source_location": false, - "upstream_fallback": false, "redis": { - "ip": "127.0.0.1", - "port": 6379, + "address": "127.0.0.1:6379", + "net": "tcp", "password": "", "db": 0, "prefix": "test_", "suffix": "_test", - "connect_timeout": 0, - "read_timeout": 0 + "connection": { + "max_idle_connections": 10, + "max_active_connections": 10, + "connect_timeout": 500, + "read_timeout": 500, + "idle_keep_alive": 30, + "max_keep_alive": 0, + "wait_for_connection": true + } }, "log": { "enable": true, @@ -86,14 +96,12 @@ dns query handler configuration "update_interval": 600, "check_interval": 600, "redis": { - "ip": "127.0.0.1", - "port": 6379, + "address": "127.0.0.1:6379", + "net": "tcp", "db": 0, "password": "", "prefix": "healthcheck_", - "suffix": "_healthcheck", - "connect_timeout": 0, - "read_timeout": 0 + "suffix": "_healthcheck" }, "log": { "enable": true, @@ -114,21 +122,23 @@ dns query handler configuration "protocol": "udp", "timeout": 400 }] + } } ~~~ -* max_ttl : max ttl in seconds, default: 3600 -* cache_timeout : time in seconds before cached responses expire -* zone_reload : time in seconds before zone data is reloaded from redis -* log_source_location : enable logging source location of every request -* upstream_fallback : enable using upstream for querying non-authoritative requests -* redis : redis configuration to use for handler -* log : log configuration to use for handler +* `max_ttl` : max ttl in seconds, default: 3600 +* `cache_timeout` : time in seconds before cached responses expire +* `zone_reload` : time in seconds before zone data is reloaded from redis +* `log_source_location` : enable logging source location of every request +* `upstream_fallback` : enable using upstream for querying non-authoritative requests +* `redis` : redis configuration to use for handler +* `log` : log configuration to use for handler ### healthcheck healthcheck configuration ~~~json +{ "healthcheck": { "enable": true, "max_requests": 10, @@ -136,14 +146,12 @@ healthcheck configuration "update_interval": 600, "check_interval": 600, "redis": { - "ip": "127.0.0.1", - "port": 6379, + "address": "127.0.0.1:6379", + "net": "tcp", "db": 0, "password": "", "prefix": "healthcheck_", - "suffix": "_healthcheck", - "connect_timeout": 0, - "read_timeout": 0 + "suffix": "_healthcheck" }, "log": { "enable": true, @@ -153,57 +161,64 @@ healthcheck configuration "path": "/tmp/healthcheck.log" } } +} ~~~ -* enable : enable/disable healthcheck, default: disable -* max_requests : maximum number of simultanous healthcheck requests, deafult: 10 -* max_pending_requests : maximum number of requests to queue, default: 100 -* update_interval : time between checking for updated data from redis in seconds, default: 300 -* check_interval : time between two healthcheck requests in seconds, default: 600 -* redis : redis configuration to use for healthcheck stats -* log : log configuration to use for healthcheck logs +* `enable` : enable/disable healthcheck, default: disable +* `max_requests` : maximum number of simultanous healthcheck requests, deafult: 10 +* `max_pending_requests` : maximum number of requests to queue, default: 100 +* `update_interval` : time between checking for updated data from redis in seconds, default: 300 +* `check_interval` : time between two healthcheck requests in seconds, default: 600 +* `redis` : redis configuration to use for healthcheck stats +* `log` : log configuration to use for healthcheck logs ### geoip geoip configuration ~~~json +{ "geoip": { "enable": true, "country_db": "geoCity.mmdb", "asn_db": "geoIsp.mmdb" } +} ~~~ -* enable : enable/disable geoip calculations, default: disable -* country_db : maxminddb file for country codes to use, default: geoCity.mmdb -* asn_db : maxminddb file for autonomous system numbers to use, default: geoIsp.mmdb +* `enable` : enable/disable geoip calculations, default: disable +* `country_db` : maxminddb file for country codes to use, default: geoCity.mmdb +* `asn_db` : maxminddb file for autonomous system numbers to use, default: geoIsp.mmdb ### upstream ~~~json -"upstream": [{ +{ + "upstream": [{ "ip": "1.1.1.1", "port": 53, "protocol": "udp", "timeout": 400 -}], + }] +} ~~~ -* ip : upstream ip address, default: 1.1.1.1 -* port : upstream port number, deafult: 53 -* protocol : upstream protocol, default : udp -* timeout : request timeout in milliseconds, default: 400 +* `ip` : upstream ip address, default: 1.1.1.1 +* `port` : upstream port number, deafult: 53 +* `protocol` : upstream protocol, default : udp +* `timeout` : request timeout in milliseconds, default: 400 ### error_log log configuration for error, debug, ... messages ~~~json -"log": { - "enable": true, - "level": "info", - "target": "file", - "format": "json", - "path": "/tmp/redins.log" +{ + "log": { + "enable": true, + "level": "info", + "target": "file", + "format": "json", + "path": "/tmp/redins.log" + } } ~~~ @@ -211,64 +226,92 @@ log configuration for error, debug, ... messages redis configurations ~~~json -"redis": { - "ip": "127.0.0.1", - "port": 6379, - "db": 0, - "password": "", - "prefix": "test_", - "suffix": "_test", - "connect_timeout": 0, - "read_timeout": 0 -}, -~~~ - -* ip : redis server ip, default: 127.0.0.1 -* port : redis server port, deafult: 6379 -* db : redis database, default: 0 -* password : redis password, deafult: "" -* prefix : limit redis keys to those prefixed with this string -* suffix : limit redis keys to those suffixed with this string -* connect_timeout : time to wait for connecting to redis server in milliseconds, deafult: 0 -* read_timeout : time to wait for redis query results in milliseconds, default: 0 +{ + "redis": { + "address": "127.0.0.1:6379", + "net": "tcp", + "db": 0, + "password": "", + "prefix": "test_", + "suffix": "_test", + "connection": { + "max_idle_connections": 10, + "max_active_connections": 10, + "connect_timeout": 500, + "read_timeout": 500, + "idle_keep_alive": 30, + "max_keep_alive": 0, + "wait_for_connection": false + }, + "connect_timeout": 0, + "read_timeout": 0 + } +} +~~~ + +* `address` : redis address: "ip:port" for "tcp" and "/path/to/unix/socket.sock" for "unix", default: "127.0.0.1:6379" +* `net`: connection protocol: "tcp" or "unix", default: "tcp" +* `db`: redis database to use, default: 0 +* `password`: redis AUTH string, default is empty +* `prefix`, `suffix`: strings to prepend/append to all redis queries, default is empty +* `max_idle_connections`: maximum number of idle connections that pool keeps, default: 10 +* `max_active_connections`: maximum number of active connections, default: 10 +* `connect_timeout`: time to wait for connecting to redis server in milliseconds, 0 for no timeout; default: 500 +* `read_timeout`: time to wait for redis query results in milliseconds, 0 for no timeout; default: 500 +* `idle_keep_alive`: time to keep idle connections in seconds, 0 for unlimited; default: 30 +* `max_keep_alive`: maximum time to keep a connection in seconds, 0 for unlimited; default: 0 +* `wait_for_connection`: whether or not wait for a connection to be available if connection pool is full, default: false ### log log configuration ~~~json -"log": { - "enable": true, - "level": "info", - "target": "file", - "format": "json", - "time_format": "2006-01-02T15:04:05.999999-07:00", - "path": "/tmp/redins.log", - "sentry": { - "enable": false, - "dsn": "" - }, - "syslog": { - "enable": false, - "protocol": "udp", - "address": "localhost:514" - }, - "kafka": { - "enable": false, - "brokers": ["127.0.0.1:9092"], - "topic": "redins" +{ + "log": { + "enable": true, + "level": "info", + "target": "file", + "format": "json", + "time_format": "2006-01-02T15:04:05.999999-07:00", + "path": "/tmp/redins.log", + "sentry": { + "enable": false, + "dsn": "" + }, + "syslog": { + "enable": false, + "protocol": "udp", + "address": "localhost:514" + }, + "kafka": { + "enable": false, + "brokers": ["127.0.0.1:9092"], + "topic": "redins", + "format": "capnp_request", + "compression": "none", + "timeout": 3000, + "buffer_size": 1000 + } } } ~~~ -* enable : enable/disable this log resource, default: disable -* level : log level, can be debug, info, warning, error, default: info -* target : log target, can be stdout, stderr, file, default: stdout -* format : log format, can be text, json, default: text -* time_format : timestamp format using example-based layout, reference time is Mon Jan 2 15:04:05 MST 2006 -* path : log output file path -* sentry : sentry hook configurations -* syslog : syslog hook configurations -* kafka : kafka hook configurations +* `enable` : enable/disable this log resource, default: disable +* `level` : log level, can be debug, info, warning, error, default: info +* `target` : log target, can be stdout, stderr, file, udp default: stdout +* `format` : log format, can be text, json, default: text. an extra log format ("capnp_request") is also available for request logs +* `time_format` : timestamp format using example-based layout, reference time is Mon Jan 2 15:04:05 MST 2006 +* `path` : log output file path, net address if target is udp +* `sentry` : sentry hook configurations +* `syslog` : syslog hook configurations +* `kafka` : kafka hook configurations + * `enable`: enable/disable kafka hook, default: disable + * `brokers`: list of brokers in "ip:port" format, default : "127.0.0.1:9092" + * `topic`: name of kafka topic, default : "redins" + * `format`: message format, default: "json" + * `compression`: compression format : "snappy", "gzip", "lz4", "zstd", "none", default: "none" + * `timeout`: kafka operation timeout (dial, read, write) in milliseconds, default : 3000 + * `buffer_size`: kafka producer buffer size, default : 1000 ### rate limit rate limit connfiguration @@ -285,11 +328,11 @@ rate limit connfiguration } ~~~ -* enable : enable/disable rate limit -* rate : maximum allowed request per minute -* burst : number of burst requests -* blacklist : list of ips to refuse all request -* whitelist : list of ips to bypass rate limit +* `enable` : enable/disable rate limit +* `rate` : maximum allowed request per minute +* `burst` : number of burst requests +* `blacklist` : list of ips to refuse all request +* `whitelist` : list of ips to bypass rate limit ### example sample config: @@ -299,22 +342,29 @@ sample config: "server": { "ip": "127.0.0.1", "port": 1053, - "protocol": "udp" + "protocol": "udp", + "count": 1 }, "handler": { "max_ttl": 300, "cache_timeout": 60, "zone_reload": 600, "log_source_location": false, - "upstream_fallback": false, "redis": { "ip": "127.0.0.1", "port": 6379, "password": "", "prefix": "test_", "suffix": "_test", - "connect_timeout": 0, - "read_timeout": 0 + "connection": { + "max_idle_connections": 10, + "max_active_connections": 10, + "connect_timeout": 500, + "read_timeout": 500, + "idle_keep_alive": 30, + "max_keep_alive": 60, + "wait_for_connection": false + } }, "log": { "enable": true, @@ -345,8 +395,15 @@ sample config: "password": "", "prefix": "healthcheck_", "suffix": "_healthcheck", - "connect_timeout": 0, - "read_timeout": 0 + "connection": { + "max_idle_connections": 10, + "max_active_connections": 10, + "connect_timeout": 500, + "read_timeout": 500, + "idle_keep_alive": 30, + "max_keep_alive": 60, + "wait_for_connection": false + } }, "log": { "enable": true, @@ -491,18 +548,18 @@ redis-cli>HGETALL example.com. ~~~ `filter` : filtering mode: -* count : return single or multiple results. values : "multi", "single" -* order : order of result. values : "none" - saved order, "weighted" - weighted shuffle, "rr" - uniform shuffle -* geo_filter : geo filter. values : "country" - same country, "location" - nearest destination, "asn" - same isp, "asn+country" same isp then same country, "none" +* `count` : return single or multiple results. values : "multi", "single" +* `order` : order of result. values : "none" - saved order, "weighted" - weighted shuffle, "rr" - uniform shuffle +* `geo_filter` : geo filter. values : "country" - same country, "location" - nearest destination, "asn" - same isp, "asn+country" same isp then same country, "none" `health_check` : health check configuration -* enable : enable/disable healthcheck for this host:ip -* uri : uri to use in healthcheck request -* port : port to use in healthcheck request -* protocol : protocol to use in healthcheck request, can be http or https -* up_count : number of successful healthcheck requests to consider an ip valid -* down_count : number of unsuccessful healthcheck requests to consider an ip invalid -* timeout time : to wait for a healthcheck response +* `enable` : enable/disable healthcheck for this host:ip +* `uri` : uri to use in healthcheck request +* `port` : port to use in healthcheck request +* `protocol` : protocol to use in healthcheck request, can be http or https +* `up_count` : number of successful healthcheck requests to consider an ip valid +* `down_count` : number of unsuccessful healthcheck requests to consider an ip invalid +* `timeout time` : to wait for a healthcheck response #### ANAME @@ -656,9 +713,9 @@ redis-cli>HGETALL example.com. } ~~~ -`cname_flattening`: enable/disable cname flattening, default: false -`dnssec`: enable/disable dnssec, default: false -`domain_id`: unique domain id for logging, optional +* `cname_flattening`: enable/disable cname flattening, default: false +* `dnssec`: enable/disable dnssec, default: false +* `domain_id`: unique domain id for logging, optional ### zone example diff --git a/VERSION b/VERSION index ee90284c27f187a315f1267b063fa81b5b84f613..80e78df6830f8bf4da6777e2ab28252afc8c7507 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.4 +1.3.5 diff --git a/config.json b/config.json deleted file mode 100644 index 76796fb9eff23638e48103e8e6e5b8b866ba9404..0000000000000000000000000000000000000000 --- a/config.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "server": [ - { - "ip": "127.0.0.1", - "port": 1053, - "protocol": "udp" - }, - { - "ip": "127.0.0.1", - "port": 10853, - "protocol": "tcp", - "tls": { - "enable": true, - "cert_path": "", - "key_path": "", - "ca_path": "" - } - } - ], - "handler": { - "max_ttl": 300, - "cache_timeout": 10, - "zone_reload": 600, - "log_source_location": false, - "upstream_fallback": false, - "redis": { - "ip": "redis", - "port": 6379, - "password": "", - "prefix": "test_", - "suffix": "_test", - "connect_timeout": 0, - "read_timeout": 0, - "active_connections": 10 - }, - "log": { - "enable": true, - "target": "file", - "level": "info", - "path": "/tmp/redins.log", - "time_format": "2006-01-02 15:04:05", - "kafka": { - "enable": true, - "topic": "redins", - "brokers": ["127.0.0.1:9092"] - } - }, - "upstream": [{ - "ip": "1.1.1.1", - "port": 53, - "protocol": "udp", - "timeout": 400 - },{ - "ip": "8.8.8.8", - "port": 53, - "protocol": "udp", - "timeout": 400 - }], - "geoip": { - "enable": true, - "country_db": "geoCity.mmdb", - "asn_db": "geoIsp.mmdb" - }, - "healthcheck": { - "enable": false, - "max_requests": 5, - "max_pending_requests": 100, - "update_interval": 600, - "check_interval": 10, - "redis": { - "ip": "redis", - "port": 6379, - "password": "", - "prefix": "healthcheck_", - "suffix": "_healthcheck", - "connect_timeout": 0, - "read_timeout": 0, - "active_connections": 10 - }, - "log": { - "enable": true, - "target": "file", - "level": "info", - "path": "/tmp/healthcheck.log", - "time_format": "2006-01-02 15:04:05" - } - } - }, - "error_log": { - "enable": true, - "target": "stdout", - "level": "debug", - "path": "/tmp/error.log", - "format": "text", - "time_format": "2006-01-02 15:04:05" - }, - "ratelimit": { - "enable": true, - "rate": 60, - "burst": 10, - "blacklist":[], - "whitelist": [] - } -} diff --git a/handler/bench_test.go b/handler/bench_test.go new file mode 100644 index 0000000000000000000000000000000000000000..e54700878b45f38fee865e2816cdba110587b1c4 --- /dev/null +++ b/handler/bench_test.go @@ -0,0 +1,134 @@ +package handler + +import ( + "arvancloud/redins/test" + "github.com/hawell/logger" + "github.com/miekg/dns" + "log" + "os" + "testing" +) + +var benchZone = "bench.zon." + +var benchEntries = [][]string{ + { + "www", + `{ + "a":{"ttl":300, "records":[{"ip":"1.2.3.4"}]}, + "aaaa":{"ttl":300, "records":[{"ip":"::1"}]}, + }`, + }, + { + "www2", + `{ + "cname":{"ttl":300, "host":"www.bench.zon."}, + }`, + }, +} + +var benchTestHandler *DnsRequestHandler + +func TestMain(m *testing.M) { + logger.Default = logger.NewLogger(&logger.LogConfig{}, nil) + + benchTestHandler = NewHandler(&defaultConfig) + err := benchTestHandler.Redis.Del("*") + log.Println(err) + err = benchTestHandler.Redis.SAdd("redins:zones", benchZone) + log.Println(err) + err = benchTestHandler.Redis.Set("redins:zones:"+benchZone+":config", "{\"cname_flattening\": false}") + log.Println(err) + for _, cmd := range benchEntries { + err := benchTestHandler.Redis.HSet("redins:zones:"+benchZone, cmd[0], cmd[1]) + if err != nil { + log.Printf("[ERROR] cannot connect to redis: %s", err) + return + } + } + + benchTestHandler.LoadZones() + os.Exit(m.Run()) +} + +var response dns.Msg + +func BenchmarkA(b *testing.B) { + tc := test.Case{ + Qname: "www.bench.zon.", Qtype: dns.TypeA, + } + var resp *dns.Msg + for n := 0; n < b.N; n++ { + r := tc.Msg() + w := test.NewRecorder(&test.ResponseWriter{}) + state := NewRequestContext(w, r) + benchTestHandler.HandleRequest(state) + + resp = w.Msg + } + response = *resp +} + +func BenchmarkAAAA(b *testing.B) { + tc := test.Case{ + Qname: "www.bench.zon.", Qtype: dns.TypeAAAA, + } + var resp *dns.Msg + for n := 0; n < b.N; n++ { + r := tc.Msg() + w := test.NewRecorder(&test.ResponseWriter{}) + state := NewRequestContext(w, r) + benchTestHandler.HandleRequest(state) + + resp = w.Msg + } + response = *resp +} + +func BenchmarkCNAME(b *testing.B) { + tc := test.Case{ + Qname: "www2.bench.zon.", Qtype: dns.TypeA, + } + var resp *dns.Msg + for n := 0; n < b.N; n++ { + r := tc.Msg() + w := test.NewRecorder(&test.ResponseWriter{}) + state := NewRequestContext(w, r) + benchTestHandler.HandleRequest(state) + + resp = w.Msg + } + response = *resp +} + +func BenchmarkNXDomain(b *testing.B) { + tc := test.Case{ + Qname: "www3.bench.zon.", Qtype: dns.TypeA, + } + var resp *dns.Msg + for n := 0; n < b.N; n++ { + r := tc.Msg() + w := test.NewRecorder(&test.ResponseWriter{}) + state := NewRequestContext(w, r) + benchTestHandler.HandleRequest(state) + + resp = w.Msg + } + response = *resp +} + +func BenchmarkNotAuth(b *testing.B) { + tc := test.Case{ + Qname: "www.bench2.zon.", Qtype: dns.TypeA, + } + var resp *dns.Msg + for n := 0; n < b.N; n++ { + r := tc.Msg() + w := test.NewRecorder(&test.ResponseWriter{}) + state := NewRequestContext(w, r) + benchTestHandler.HandleRequest(state) + + resp = w.Msg + } + response = *resp +} diff --git a/handler/dns_types.go b/handler/dns_types.go index 2326cee424ecd6de606142dc161a97b1dad8bea5..3799032e5f6987dab81d46853e0255a6ec1f84c3 100644 --- a/handler/dns_types.go +++ b/handler/dns_types.go @@ -2,7 +2,7 @@ package handler import ( "crypto" - "encoding/json" + "github.com/json-iterator/go" "github.com/miekg/dns" "github.com/pkg/errors" "net" @@ -35,22 +35,6 @@ type ZoneKey struct { KeyExpiration uint32 } -type ZoneConfig struct { - DomainId string `json:"domain_id,omitempty"` - SOA *SOA_RRSet `json:"soa,omitempty"` - DnsSec bool `json:"dnssec,omitempty"` - CnameFlattening bool `json:"cname_flattening,omitempty"` -} - -type Zone struct { - Name string - Config ZoneConfig - Locations map[string]struct{} - ZSK *ZoneKey - KSK *ZoneKey - DnsKeySig dns.RR -} - type IP_RRSet struct { FilterConfig IpFilterConfig `json:"filter,omitempty"` HealthCheckConfig IpHealthCheckConfig `json:"health_check,omitempty"` @@ -74,7 +58,7 @@ type _IP_RR struct { func (iprr *IP_RR) UnmarshalJSON(data []byte) error { var _ip_rr _IP_RR - if err := json.Unmarshal(data, &_ip_rr); err != nil { + if err := jsoniter.Unmarshal(data, &_ip_rr); err != nil { return err } diff --git a/handler/dnssec.go b/handler/dnssec.go index 275d5886e82d220969a1cc90b9127414521453b4..eba7dae0c8c053cce8e4c2615bf94610edb2a1ff 100644 --- a/handler/dnssec.go +++ b/handler/dnssec.go @@ -43,7 +43,7 @@ func splitSets(rrs []dns.RR) map[rrset][]dns.RR { return nil } -func Sign(rrs []dns.RR, qname string, record *Record) []dns.RR { +func Sign(rrs []dns.RR, qname string, z *Zone) []dns.RR { var res []dns.RR sets := splitSets(rrs) for _, set := range sets { @@ -52,9 +52,9 @@ func Sign(rrs []dns.RR, qname string, record *Record) []dns.RR { case dns.TypeRRSIG, dns.TypeOPT: continue case dns.TypeDNSKEY: - res = append(res, record.Zone.DnsKeySig) + res = append(res, z.DnsKeySig) default: - if rrsig, err := sign(set, qname, record.Zone.ZSK, set[0].Header().Ttl); err == nil { + if rrsig, err := sign(set, qname, z.ZSK, set[0].Header().Ttl); err == nil { res = append(res, rrsig) } } @@ -64,7 +64,7 @@ func Sign(rrs []dns.RR, qname string, record *Record) []dns.RR { func sign(rrs []dns.RR, name string, key *ZoneKey, ttl uint32) (*dns.RRSIG, error) { rrsig := &dns.RRSIG{ - Hdr: dns.RR_Header{name, dns.TypeRRSIG, dns.ClassINET, ttl, 0}, + Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeRRSIG, Class: dns.ClassINET, Ttl: ttl}, Inception: key.KeyInception, Expiration: key.KeyExpiration, KeyTag: key.DnsKey.KeyTag(), @@ -93,7 +93,7 @@ func sign(rrs []dns.RR, name string, key *ZoneKey, ttl uint32) (*dns.RRSIG, erro func NSec(name string, zone *Zone) dns.RR { nsec := &dns.NSEC{ - Hdr: dns.RR_Header{name, dns.TypeNSEC, dns.ClassINET, zone.Config.SOA.MinTtl, 0}, + Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeNSEC, Class: dns.ClassINET, Ttl: zone.Config.SOA.MinTtl}, NextDomain: "\\000." + name, TypeBitMap: NSecTypes, } diff --git a/handler/dnssec_test.go b/handler/dnssec_test.go index f879af0f7ed796cef35d727cbe87ee0ea3bf8a7d..cdbf97e3570385c109756795858b0bc5d555bb7e 100644 --- a/handler/dnssec_test.go +++ b/handler/dnssec_test.go @@ -3,7 +3,6 @@ package handler import ( "arvancloud/redins/test" "fmt" - "github.com/coredns/coredns/request" "github.com/hawell/logger" "github.com/hawell/uperdis" "github.com/miekg/dns" @@ -12,22 +11,24 @@ import ( "testing" ) -var dnssecZone = string("dnssec_test.com.") +var dnssecZone = "dnssec_test.com." var dnssecConfig = `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.dnssec_test.com.","ns":"ns1.dnssec_test.com.","refresh":44,"retry":55,"expire":66},"dnssec": true}` var dnssecEntries = [][]string{ {"@", - `{"ns":{"ttl":300,"records":[{"host":"a.dnssec_test.com."}]}}`, + `{"ns":{"ttl":300,"records":[{"host":"ns1.dnssec_test.com."},{"host":"ns2.dnssec_test.com."}]}}`, }, {"x", `{ "a":{"ttl":300, "records":[{"ip":"1.2.3.4", "country":"ES"},{"ip":"5.6.7.8", "country":""}]}, "aaaa":{"ttl":300, "records":[{"ip":"::1"}]}, "txt":{"ttl":300, "records":[{"text":"foo"},{"text":"bar"}]}, - "ns":{"ttl":300, "records":[{"host":"ns1.dnssec_test.com."},{"host":"ns2.dnssec_test.com."}]}, "mx":{"ttl":300, "records":[{"host":"mx1.dnssec_test.com.", "preference":10},{"host":"mx2.dnssec_test.com.", "preference":10}]}, "srv":{"ttl":300, "records":[{"target":"sip.dnssec_test.com.","port":555,"priority":10,"weight":100}]} - }`, + }`, + }, + {"y", + `{"ns":{"ttl":300, "records":[{"host":"ns1.dnssec_test.com."},{"host":"ns2.dnssec_test.com."}]}}`, }, {"*", `{"txt":{"ttl":300,"records":[{"text":"wildcard text"}]}}`, @@ -61,8 +62,7 @@ var dnssecEntries = [][]string{ }, } -var zskPriv = string( - `Private-key-format: v1.3 +var zskPriv = `Private-key-format: v1.3 Algorithm: 5 (RSASHA1) Modulus: oqwXm/EF8q6p5Rrj66Bbft+0Vk7Kj6TuvZp4nNl0htiT/8/92kIcri5gbxnV2v+p6jXYQI1Vx/vqP5cB0kPzjUQuJFVpm14fxOp89D6N0fPXR7xJ+SHs5nigHBIJdaP5 PublicExponent: AQAB @@ -75,17 +75,16 @@ Coefficient: GhzOVUQcUJkvbYc9/+9MZngzDCeoetXDR6IILqG0/Rmt7FHWwSD7nOSoUUE5GslF Created: 20180717134704 Publish: 20180717134704 Activate: 20180717134704 -`) +` -var zskPub = string("dnssec_test.com. IN DNSKEY 256 3 5 AwEAAaKsF5vxBfKuqeUa4+ugW37ftFZOyo+k7r2aeJzZdIbYk//P/dpC HK4uYG8Z1dr/qeo12ECNVcf76j+XAdJD841ELiRVaZteH8TqfPQ+jdHz 10e8Sfkh7OZ4oBwSCXWj+Q==") +var zskPub = "dnssec_test.com. IN DNSKEY 256 3 5 AwEAAaKsF5vxBfKuqeUa4+ugW37ftFZOyo+k7r2aeJzZdIbYk//P/dpC HK4uYG8Z1dr/qeo12ECNVcf76j+XAdJD841ELiRVaZteH8TqfPQ+jdHz 10e8Sfkh7OZ4oBwSCXWj+Q==" var dnskeyQuery = test.Case{ Do: true, Qname: "dnssec_test.com", Qtype: dns.TypeDNSKEY, } -var kskPriv = string( - `Private-key-format: v1.3 +var kskPriv = `Private-key-format: v1.3 Algorithm: 5 (RSASHA1) Modulus: 5WuOIP3GHID5Qmed6L+2ehBCkusTAXNv9uUfpzzTJHsA+bBesZSFsRNzMAV2drM7fApcL5IgNqrhb5twxu1/+cZj2Ld3PALbkENzn/erTl4A4uQdSWdkj8KnaLiJQPaT PublicExponent: AQAB @@ -98,9 +97,9 @@ Coefficient: QCGY0yr+kkmOZfUoL9YCCgau/xjyEPRZgiGTfIy0PtGGMDKfUswJ+1KWI9Jue3E5 Created: 20190518113600 Publish: 20190518113600 Activate: 20190518113600 -`) +` -var kskPub = string("dnssec_test.com. IN DNSKEY 257 3 5 AwEAAeVrjiD9xhyA+UJnnei/tnoQQpLrEwFzb/blH6c80yR7APmwXrGU hbETczAFdnazO3wKXC+SIDaq4W+bcMbtf/nGY9i3dzwC25BDc5/3q05e AOLkHUlnZI/Cp2i4iUD2kw==") +var kskPub = "dnssec_test.com. IN DNSKEY 257 3 5 AwEAAeVrjiD9xhyA+UJnnei/tnoQQpLrEwFzb/blH6c80yR7APmwXrGU hbETczAFdnazO3wKXC+SIDaq4W+bcMbtf/nGY9i3dzwC25BDc5/3q05e AOLkHUlnZI/Cp2i4iUD2kw==" var dnssecTestCases = []test.Case{ { @@ -110,6 +109,14 @@ var dnssecTestCases = []test.Case{ test.DNSKEY("dnssec_test.com. 3600 IN DNSKEY 257 3 5 AwEAAeVrjiD9xhyA+UJnnei/tnoQQpLrEwFzb/blH6c80yR7APmwXrGUhbETczAFdnazO3wKXC+SIDaq4W+bcMbtf/nGY9i3dzwC25BDc5/3q05eAOLkHUlnZI/Cp2i4iUD2kw=="), test.RRSIG("dnssec_test.com. 3600 IN RRSIG DNSKEY 5 2 3600 20190527081109 20190519051109 37456 dnssec_test.com. oVwtVEf9eOkcuSJlsH0OSBUvLOxgKM1pIAe7v717oRyCoyC+FIG5uGsdrZWhgklh/fpEmRdJQ+nHXKWT/son8zvxAoskuIIp49wwgvcS400IoHiyjIY0BHNTFPvsPdy0"), }, + Do: true, + }, + { + Qname: "x.dnssec_test.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("x.dnssec_test.com. 300 IN A 1.2.3.4"), + test.A("x.dnssec_test.com. 300 IN A 5.6.7.8"), + }, }, { Qname: "x.dnssec_test.com.", Qtype: dns.TypeA, @@ -149,11 +156,11 @@ var dnssecTestCases = []test.Case{ }, // NS Test { - Qname: "x.dnssec_test.com.", Qtype: dns.TypeNS, + Qname: "dnssec_test.com.", Qtype: dns.TypeNS, Answer: []dns.RR{ - test.NS("x.dnssec_test.com. 300 IN NS ns1.dnssec_test.com."), - test.NS("x.dnssec_test.com. 300 IN NS ns2.dnssec_test.com."), - test.RRSIG("x.dnssec_test.com. 300 IN RRSIG NS 5 3 300 20180726104727 20180718074727 22548 dnssec_test.com. NTYiqJBR8hFjYQcHeuUUWH2zIEqpF5xfFeHBb24icTbd5kg7VU9QHkzc/odnAFu80SfDJVnxX9OTV7re8Epp06CBT7m8VpUUv6+qnn6ma2qukWa8wyvFPg/PXJLA8cpG"), + test.NS("dnssec_test.com. 300 IN NS ns1.dnssec_test.com."), + test.NS("dnssec_test.com. 300 IN NS ns2.dnssec_test.com."), + test.RRSIG("dnssec_test.com. 300 IN RRSIG NS 5 2 300 20191122140218 20191114110218 22548 dnssec_test.com. DK9giOXPadNyDfFtsPjEd9JpWGIeCyIOgDDwzvgsYc/k/Q5blgtWBNxJ Fk0aPqj6M15RFTig2nA3uEJpEJx7OAj5zzSSTqyPozT/qrmPMWdxJcuK yaJ+CH+Ws9wJsM3S"), }, Do: true, Extra: []dns.RR{ @@ -261,19 +268,26 @@ var dnssecTestCases = []test.Case{ }, } -var dnssecTestConfig = HandlerConfig{ +var dnssecTestConfig = DnsRequestHandlerConfig{ MaxTtl: 300, CacheTimeout: 60, ZoneReload: 600, Redis: uperdis.RedisConfig{ - Ip: "redis", - Port: 6379, - DB: 0, - Password: "", - Prefix: "test_", - Suffix: "_test", - ConnectTimeout: 0, - ReadTimeout: 0, + Address: "redis:6379", + Net: "tcp", + DB: 0, + Password: "", + Prefix: "test_", + Suffix: "_test", + Connection: uperdis.RedisConnectionConfig{ + MaxIdleConnections: 10, + MaxActiveConnections: 10, + ConnectTimeout: 500, + ReadTimeout: 500, + IdleKeepAlive: 30, + MaxKeepAlive: 0, + WaitForConnection: true, + }, }, Log: logger.LogConfig{ Enable: false, @@ -293,11 +307,13 @@ var dnssecTestConfig = HandlerConfig{ } func TestDNSSEC(t *testing.T) { - logger.Default = logger.NewLogger(&logger.LogConfig{}) + logger.Default = logger.NewLogger(&logger.LogConfig{}, nil) h := NewHandler(&dnssecTestConfig) - h.Redis.Del(dnssecZone) + if err := h.Redis.Del(dnssecZone); err != nil { + fmt.Println(err) + } for _, cmd := range dnssecEntries { err := h.Redis.HSet("redins:zones:"+dnssecZone, cmd[0], cmd[1]) if err != nil { @@ -305,12 +321,24 @@ func TestDNSSEC(t *testing.T) { t.Fail() } } - h.Redis.Set("redins:zones:"+dnssecZone+":config", dnssecConfig) - h.Redis.Set("redins:zones:"+dnssecZone+":zsk:pub", zskPub) - h.Redis.Set("redins:zones:"+dnssecZone+":zsk:priv", zskPriv) - h.Redis.Set("redins:zones:"+dnssecZone+":ksk:pub", kskPub) - h.Redis.Set("redins:zones:"+dnssecZone+":ksk:priv", kskPriv) - h.Redis.SAdd("redins:zones", dnssecZone) + if err := h.Redis.Set("redins:zones:"+dnssecZone+":config", dnssecConfig); err != nil { + fmt.Println(err) + } + if err := h.Redis.Set("redins:zones:"+dnssecZone+":zsk:pub", zskPub); err != nil { + fmt.Println(err) + } + if err := h.Redis.Set("redins:zones:"+dnssecZone+":zsk:priv", zskPriv); err != nil { + fmt.Println(err) + } + if err := h.Redis.Set("redins:zones:"+dnssecZone+":ksk:pub", kskPub); err != nil { + fmt.Println(err) + } + if err := h.Redis.Set("redins:zones:"+dnssecZone+":ksk:priv", kskPriv); err != nil { + fmt.Println(err) + } + if err := h.Redis.SAdd("redins:zones", dnssecZone); err != nil { + fmt.Println(err) + } h.LoadZones() var zsk dns.RR @@ -318,10 +346,10 @@ func TestDNSSEC(t *testing.T) { { r := dnskeyQuery.Msg() w := test.NewRecorder(&test.ResponseWriter{}) - state := request.Request{W: w, Req: r} - h.HandleRequest(&state) + state := NewRequestContext(w, r) + h.HandleRequest(state) resp := w.Msg - fmt.Println(resp.Answer) + // fmt.Println(resp.Answer) for _, answer := range resp.Answer { if key, ok := answer.(*dns.DNSKEY); ok { if key.Flags == 256 { @@ -336,15 +364,19 @@ func TestDNSSEC(t *testing.T) { // fmt.Println("ksk is ", ksk.String()) for i, tc0 := range dnssecTestCases { + // fmt.Println(i) tc := test.Case{ Qname: dnssecTestCases[i].Qname, Qtype: dnssecTestCases[i].Qtype, Answer: make([]dns.RR, len(dnssecTestCases[i].Answer)), Ns: make([]dns.RR, len(dnssecTestCases[i].Ns)), - Do: true, + Do: dnssecTestCases[i].Do, Extra: []dns.RR{ test.OPT(4096, true), }, } + if !tc.Do { + tc.Extra = []dns.RR{} + } copy(tc.Answer, dnssecTestCases[i].Answer) copy(tc.Ns, dnssecTestCases[i].Ns) sort.Sort(test.RRSet(tc.Answer)) @@ -352,8 +384,8 @@ func TestDNSSEC(t *testing.T) { r := tc.Msg() w := test.NewRecorder(&test.ResponseWriter{}) - state := request.Request{W: w, Req: r} - h.HandleRequest(&state) + state := NewRequestContext(w, r) + h.HandleRequest(state) resp := w.Msg for _, rrs := range [][]dns.RR{tc0.Answer, tc0.Ns, resp.Answer, resp.Ns} { s := 0 @@ -363,7 +395,7 @@ func TestDNSSEC(t *testing.T) { break } if rrsig, ok := rrs[e].(*dns.RRSIG); ok { - //fmt.Printf("s = %d, e = %d\n", s, e) + // fmt.Printf("s = %d, e = %d\n", s, e) if tc.Qtype == dns.TypeDNSKEY { if rrsig.Verify(ksk.(*dns.DNSKEY), rrs[s:e]) != nil { fmt.Println("fail") @@ -389,4 +421,5 @@ func TestDNSSEC(t *testing.T) { } //fmt.Println("xxxx") } + } diff --git a/handler/geoip.go b/handler/geoip.go index 78ed37a3ce7ac6975a976ce3a69fd0f4afdf9a7f..364f447460964ac9a20186fd72cbf74c3bbe5963 100644 --- a/handler/geoip.go +++ b/handler/geoip.go @@ -15,9 +15,9 @@ type GeoIp struct { } type GeoIpConfig struct { - Enable bool `json:"enable,omitempty"` - CountryDB string `json:"country_db,omitempty"` - ASNDB string `json:"asn_db,omitempty"` + Enable bool `json:"enable"` + CountryDB string `json:"country_db"` + ASNDB string `json:"asn_db"` } func NewGeoIp(config *GeoIpConfig) *GeoIp { @@ -39,7 +39,7 @@ func NewGeoIp(config *GeoIpConfig) *GeoIp { return g } -func (g *GeoIp) GetSameCountry(sourceIp net.IP, ips []IP_RR, logData map[string]interface{}) []IP_RR { +func (g *GeoIp) GetSameCountry(sourceIp net.IP, ips []IP_RR) []IP_RR { if !g.Enable || g.CountryDB == nil { return ips } @@ -48,7 +48,6 @@ func (g *GeoIp) GetSameCountry(sourceIp net.IP, ips []IP_RR, logData map[string] logger.Default.Error("getSameCountry failed") return ips } - logData["source_country"] = sourceCountry var result []IP_RR if sourceCountry != "" { @@ -84,7 +83,7 @@ func (g *GeoIp) GetSameCountry(sourceIp net.IP, ips []IP_RR, logData map[string] return ips } -func (g *GeoIp) GetSameASN(sourceIp net.IP, ips []IP_RR, logData map[string]interface{}) []IP_RR { +func (g *GeoIp) GetSameASN(sourceIp net.IP, ips []IP_RR) []IP_RR { if !g.Enable || g.ASNDB == nil { return ips } @@ -93,7 +92,6 @@ func (g *GeoIp) GetSameASN(sourceIp net.IP, ips []IP_RR, logData map[string]inte logger.Default.Error("getSameASN failed") return ips } - logData["source_asn"] = sourceASN var result []IP_RR if sourceASN != 0 { @@ -129,7 +127,7 @@ func (g *GeoIp) GetSameASN(sourceIp net.IP, ips []IP_RR, logData map[string]inte return ips } -func (g *GeoIp) GetMinimumDistance(sourceIp net.IP, ips []IP_RR, logData map[string]interface{}) []IP_RR { +func (g *GeoIp) GetMinimumDistance(sourceIp net.IP, ips []IP_RR) []IP_RR { if !g.Enable || g.CountryDB == nil { return ips } @@ -193,12 +191,11 @@ func (g *GeoIp) GetGeoLocation(ip net.IP) (latitude float64, longitude float64, } `maxminddb:"country"` } logger.Default.Debugf("ip : %s", ip) - err = g.CountryDB.Lookup(ip, &record) - if err != nil { + if err := g.CountryDB.Lookup(ip, &record); err != nil { logger.Default.Errorf("lookup failed : %s", err) return 0, 0, "", err } - g.CountryDB.Decode(record.Location.LongitudeOffset, &longitude) + _ = g.CountryDB.Decode(record.Location.LongitudeOffset, &longitude) logger.Default.Debug("lat = ", record.Location.Latitude, " lang = ", longitude, " country = ", record.Country.ISOCode) return record.Location.Latitude, longitude, record.Country.ISOCode, nil } diff --git a/handler/geoip_test.go b/handler/geoip_test.go index 891362e5496de28db4f347858538f3831a5ba35d..04abb5ee4486ac6c32117b945b3dfa453ec8b9ec 100644 --- a/handler/geoip_test.go +++ b/handler/geoip_test.go @@ -56,7 +56,7 @@ func TestGeoIpAutomatic(t *testing.T) { Enable: true, CountryDB: "../geoCity.mmdb", } - logger.Default = logger.NewLogger(&logger.LogConfig{}) + logger.Default = logger.NewLogger(&logger.LogConfig{}, nil) g := NewGeoIp(&cfg) @@ -73,7 +73,7 @@ func TestGeoIpAutomatic(t *testing.T) { dest.Data = append(dest.Data, r) } dest.Ttl = 100 - ips := g.GetMinimumDistance(net.ParseIP(sip[i][0]), dest.Data, map[string]interface{}{}) + ips := g.GetMinimumDistance(net.ParseIP(sip[i][0]), dest.Data) log.Println("[DEBUG]", sip[i][0], " ", ips[0].Ip.String(), " ", len(ips)) if sip[i][2] != ips[0].Ip.String() { t.Fail() @@ -93,7 +93,7 @@ func TestGetSameCountry(t *testing.T) { Enable: true, CountryDB: "../geoCity.mmdb", } - logger.Default = logger.NewLogger(&logger.LogConfig{}) + logger.Default = logger.NewLogger(&logger.LogConfig{}, nil) g := NewGeoIp(&cfg) @@ -104,7 +104,7 @@ func TestGetSameCountry(t *testing.T) { {Ip: net.ParseIP("2.3.4.5"), Country: []string{"FR"}}, {Ip: net.ParseIP("3.4.5.6"), Country: []string{""}}, } - ips := g.GetSameCountry(net.ParseIP(sip[i][0]), dest.Data, map[string]interface{}{}) + ips := g.GetSameCountry(net.ParseIP(sip[i][0]), dest.Data) if len(ips) != 1 { t.Fail() } @@ -147,7 +147,7 @@ func TestGetSameASN(t *testing.T) { g := NewGeoIp(&cfg) for i := range sip { - ips := g.GetSameASN(net.ParseIP(sip[i]), dip.Data, map[string]interface{}{}) + ips := g.GetSameASN(net.ParseIP(sip[i]), dip.Data) if len(ips) != 1 { t.Fail() } diff --git a/handler/handler.go b/handler/handler.go index c6371303f5af580a18042a5af51b082f8cb15f0e..8689bf04064be10f6a4ddf38a36325a8266afbc3 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -1,317 +1,348 @@ package handler import ( - "encoding/json" + "arvancloud/redins/handler/logformat" + "errors" + "fmt" + "github.com/json-iterator/go" + "github.com/karlseguin/ccache" + "github.com/sirupsen/logrus" + "golang.org/x/sync/singleflight" "math/rand" "net" "strings" "sync" "time" - "github.com/coredns/coredns/request" "github.com/hashicorp/go-immutable-radix" "github.com/hawell/logger" "github.com/hawell/uperdis" "github.com/miekg/dns" - "github.com/patrickmn/go-cache" ) type DnsRequestHandler struct { - Config *HandlerConfig + Config *DnsRequestHandlerConfig Zones *iradix.Tree LastZoneUpdate time.Time Redis *uperdis.Redis Logger *logger.EventLogger - RecordCache *cache.Cache - ZoneCache *cache.Cache + RecordCache *ccache.Cache + RecordInflight *singleflight.Group + ZoneCache *ccache.Cache + ZoneInflight *singleflight.Group geoip *GeoIp healthcheck *Healthcheck upstream *Upstream quit chan struct{} quitWG sync.WaitGroup - numRoutines int + logQueue chan map[string]interface{} } -type HandlerConfig struct { - Upstream []UpstreamConfig `json:"upstream,omitempty"` - GeoIp GeoIpConfig `json:"geoip,omitempty"` - HealthCheck HealthcheckConfig `json:"healthcheck,omitempty"` - MaxTtl int `json:"max_ttl,omitempty"` - CacheTimeout int `json:"cache_timeout,omitempty"` - ZoneReload int `json:"zone_reload,omitempty"` - LogSourceLocation bool `json:"log_source_location,omitempty"` - UpstreamFallback bool `json:"upstream_fallback,omitempty"` - Redis uperdis.RedisConfig `json:"redis,omitempty"` - Log logger.LogConfig `json:"log,omitempty"` +type DnsRequestHandlerConfig struct { + Upstream []UpstreamConfig `json:"upstream"` + GeoIp GeoIpConfig `json:"geoip"` + HealthCheck HealthcheckConfig `json:"healthcheck"` + MaxTtl int `json:"max_ttl"` + CacheTimeout int `json:"cache_timeout"` + ZoneReload int `json:"zone_reload"` + LogSourceLocation bool `json:"log_source_location"` + Redis uperdis.RedisConfig `json:"redis"` + Log logger.LogConfig `json:"log"` } -func NewHandler(config *HandlerConfig) *DnsRequestHandler { +const ( + RecordCacheSize = 1000000 + ZoneCacheSize = 10000 + CacheItemsToPrune = 100 +) + +func NewHandler(config *DnsRequestHandlerConfig) *DnsRequestHandler { h := &DnsRequestHandler{ Config: config, } + getFormatter := func(name string) logrus.Formatter { + switch name { + case "capnp_request": + return &logformat.CapnpRequestLogFormatter{} + case "json": + return &logrus.JSONFormatter{TimestampFormat: h.Config.Log.TimeFormat} + case "text": + return &logrus.TextFormatter{TimestampFormat: h.Config.Log.TimeFormat} + default: + return &logrus.TextFormatter{TimestampFormat: h.Config.Log.TimeFormat} + } + } + + h.logQueue = make(chan map[string]interface{}, 1000) + go func() { + h.quitWG.Add(1) + for { + select { + case <-h.quit: + h.quitWG.Done() + return + case data := <-h.logQueue: + h.Logger.Log(data, "dns request") + } + } + }() h.Redis = uperdis.NewRedis(&config.Redis) - h.Logger = logger.NewLogger(&config.Log) + h.Logger = logger.NewLogger(&config.Log, getFormatter) h.geoip = NewGeoIp(&config.GeoIp) h.healthcheck = NewHealthcheck(&config.HealthCheck, h.Redis) h.upstream = NewUpstream(config.Upstream) h.Zones = iradix.New() - h.quit = make(chan struct{}, 1) + h.quit = make(chan struct{}) h.LoadZones() - h.RecordCache = cache.New(time.Second*time.Duration(h.Config.CacheTimeout), time.Duration(h.Config.CacheTimeout)*time.Second*10) - h.ZoneCache = cache.New(time.Second*time.Duration(h.Config.CacheTimeout), time.Duration(h.Config.CacheTimeout)*time.Second*10) + h.RecordCache = ccache.New(ccache.Configure().MaxSize(RecordCacheSize).ItemsToPrune(CacheItemsToPrune)) + h.RecordInflight = new(singleflight.Group) + h.ZoneCache = ccache.New(ccache.Configure().MaxSize(ZoneCacheSize).ItemsToPrune(CacheItemsToPrune)) + h.ZoneInflight = new(singleflight.Group) go h.healthcheck.Start() - if h.Redis.SubscribeEvent("redins:zones", func(channel string, event string) { - logger.Default.Debug("loading zones") - h.LoadZones() - }) != nil { - logger.Default.Warning("event notification is not available, adding/removing zones will not be instant") - go func() { - h.numRoutines++ - for { - select { - case <-h.quit: - // fmt.Println("updateZone : quit") - h.quitWG.Done() - return - case <-time.After(time.Duration(h.Config.ZoneReload) * time.Second): - logger.Default.Debugf("%v", h.Zones) + go func() { + logger.Default.Debug("zone updater") + h.quitWG.Add(1) + quit := make(chan *sync.WaitGroup, 1) + modified := false + go h.Redis.SubscribeEvent("redins:zones", func() { + modified = true + }, func(channel string, data string) { + modified = true + }, func(err error) { + logger.Default.Error(err) + }, quit) + + reloadTicker := time.NewTicker(time.Duration(h.Config.ZoneReload) * time.Second) + forceReloadTicker := time.NewTicker(time.Duration(h.Config.ZoneReload) * time.Second * 10) + for { + select { + case <-h.quit: + reloadTicker.Stop() + forceReloadTicker.Stop() + logger.Default.Debug("zone updater stopped") + quit <- &h.quitWG + return + case <-reloadTicker.C: + if modified { logger.Default.Debug("loading zones") h.LoadZones() + modified = false } + case <- forceReloadTicker.C: + modified = true } - }() - } + } + }() return h } func (h *DnsRequestHandler) ShutDown() { - // fmt.Println("handler : stopping") + logger.Default.Debug("handler : stopping") h.healthcheck.ShutDown() - h.quitWG.Add(h.numRoutines) + close(h.logQueue) close(h.quit) h.quitWG.Wait() - // fmt.Println("handler : stopped") + logger.Default.Debug("handler : stopped") } -func (h *DnsRequestHandler) HandleRequest(state *request.Request) { - qname := state.Name() - qtype := state.QType() +func (h *DnsRequestHandler) Response(context *RequestContext, res int) { + h.LogRequest(context, res) + context.Response(res) +} - logger.Default.Debugf("name : %s", state.Name()) - logger.Default.Debugf("type : %s", state.Type()) +func (h *DnsRequestHandler) HandleRequest(context *RequestContext) { + logger.Default.Debugf("[%d] start handle request - name : %s, type : %s", context.Req.Id, context.RawName(), context.Type()) + if h.Config.LogSourceLocation { + sourceIP := context.SourceIp + _, _, sourceCountry, _ := h.geoip.GetGeoLocation(sourceIP) + context.LogData["source_country"] = sourceCountry + sourceASN, _ := h.geoip.GetASN(sourceIP) + context.LogData["source_asn"] = sourceASN + } - requestStartTime := time.Now() + zoneName := h.FindZone(context.RawName()) + if zoneName == "" { + h.Response(context, dns.RcodeNotAuth) + return + } + logger.Default.Debugf("[%d] zone name : %s", context.Req.Id, zoneName) - logData := map[string]interface{}{ - "source_ip": state.IP(), - "record": state.Name(), - "type": state.Type(), + zone := h.LoadZone(zoneName) + if zone == nil { + h.Response(context, dns.RcodeServerFailure) + return } - logData["client_subnet"] = GetSourceSubnet(state) + context.LogData["domain_uuid"] = zone.Config.DomainId - if h.Config.LogSourceLocation { - sourceIP := GetSourceIp(state) - _, _, sourceCountry, _ := h.geoip.GetGeoLocation(sourceIP) - logData["source_country"] = sourceCountry - sourceASN, _ := h.geoip.GetASN(sourceIP) - logData["source_asn"] = sourceASN - } - - auth := true - - var record *Record - var localRes int - var res int - var answers []dns.RR - var authority []dns.RR - record, localRes = h.FetchRecord(qname, logData) - originalRecord := record - if record != nil { - logData["domain_uuid"] = record.Zone.Config.DomainId - if qtype != dns.TypeCNAME { - count := 0 - for { - if count >= 10 { - answers = []dns.RR{} - localRes = dns.RcodeServerFailure - break - } - if localRes != dns.RcodeSuccess { - break - } - if record.CNAME == nil { - break - } - if !record.Zone.Config.CnameFlattening { - answers = append(answers, h.CNAME(qname, record)...) - if h.Matches(record.CNAME.Host) != originalRecord.Zone.Name { - break - } - qname = record.CNAME.Host - } - record, localRes = h.FetchRecord(record.CNAME.Host, logData) - count++ - } + loopCount := 0 + currentQName := context.RawName() + currentRecord := &Record{} + res := dns.RcodeSuccess +loop: + for { + if loopCount > 10 { + logger.Default.Errorf("CNAME loop in request %s->%s", context.RawName(), context.Type()) + context.Answer = []dns.RR{} + res = dns.RcodeServerFailure + break loop } - } + loopCount++ - res = localRes - if localRes == dns.RcodeSuccess { - switch qtype { - case dns.TypeA: - if len(record.A.Data) == 0 { - if record.ANAME != nil { - anameAnswer, anameRes := h.FetchRecord(record.ANAME.Location, logData) - if anameRes == dns.RcodeSuccess { - ips := h.Filter(state, &anameAnswer.A, logData) - answers = append(answers, h.A(qname, anameAnswer, ips)...) - } else { - upstreamAnswers, upstreamRes := h.upstream.Query(record.ANAME.Location, dns.TypeA) - if upstreamRes == dns.RcodeSuccess { - var anameRecord []dns.RR - for _, r := range upstreamAnswers { - if r.Header().Name == record.ANAME.Location && r.Header().Rrtype == dns.TypeA { - a := r.(*dns.A) - anameRecord = append(anameRecord, &dns.A{A: a.A, Hdr: dns.RR_Header{Rrtype: dns.TypeA, Name: qname, Ttl: a.Hdr.Ttl, Class: dns.ClassINET, Rdlength: 0}}) - } - } - answers = append(answers, anameRecord...) - } - res = upstreamRes - } + if h.FindZone(currentQName) != zoneName { + logger.Default.Debugf("[%d] out of zone - qname : %s, zone : %s", context.Req.Id, currentQName, zoneName) + res = dns.RcodeSuccess + break loop + } + + location, match := zone.FindLocation(currentQName) + switch match { + case NoMatch: + logger.Default.Debugf("[%d] no location matched for %s in %s", context.Req.Id, currentQName, zoneName) + context.Authority = []dns.RR{zone.Config.SOA.Data} + res = dns.RcodeNameError + break loop + + case WildCardMatch: + fallthrough + + case ExactMatch: + logger.Default.Debugf("[%d] loading location %s", context.Req.Id, location) + currentRecord = h.LoadLocation(location, zone) + if currentRecord == nil { + res = dns.RcodeServerFailure + break loop + } + if currentRecord.CNAME != nil && context.QType() != dns.TypeCNAME { + logger.Default.Debugf("[%d] cname chain %s -> %s", context.Req.Id, currentQName, currentRecord.CNAME.Host) + if !zone.Config.CnameFlattening { + context.Answer = append(context.Answer, h.CNAME(currentQName, currentRecord)...) + } else if h.FindZone(currentRecord.CNAME.Host) != zoneName { + context.Answer = append(context.Answer, h.CNAME(context.RawName(), currentRecord)...) + break loop } - } else { - ips := h.Filter(state, &record.A, logData) - answers = append(answers, h.A(qname, record, ips)...) + currentQName = dns.Fqdn(currentRecord.CNAME.Host) + continue } - case dns.TypeAAAA: - if len(record.AAAA.Data) == 0 { - if record.ANAME != nil { - anameAnswer, anameRes := h.FetchRecord(record.ANAME.Location, logData) - if anameRes == dns.RcodeSuccess { - ips := h.Filter(state, &anameAnswer.AAAA, logData) - answers = append(answers, h.AAAA(qname, anameAnswer, ips)...) - } else { - upstreamAnswers, upstreamRes := h.upstream.Query(record.ANAME.Location, dns.TypeAAAA) - if upstreamRes == dns.RcodeSuccess { - var anameRecord []dns.RR - for _, r := range upstreamAnswers { - if r.Header().Name == record.ANAME.Location && r.Header().Rrtype == dns.TypeAAAA { - a := r.(*dns.AAAA) - anameRecord = append(anameRecord, &dns.AAAA{AAAA: a.AAAA, Hdr: dns.RR_Header{Rrtype: dns.TypeAAAA, Name: qname, Ttl: a.Hdr.Ttl, Class: dns.ClassINET, Rdlength: 0}}) - } - } - answers = append(answers, anameRecord...) + if len(currentRecord.NS.Data) > 0 && currentQName != zone.Name { + logger.Default.Debugf("[%d] delegation", context.Req.Id) + context.Authority = append(context.Authority, h.NS(currentQName, currentRecord)...) + for _, ns := range currentRecord.NS.Data { + glueLocation, match := zone.FindLocation(ns.Host) + if match != NoMatch { + glueRecord := h.LoadLocation(glueLocation, zone) + // XXX : should we return with RcodeServerFailure? + if glueRecord != nil { + context.Additional = append(context.Additional, h.A(ns.Host, glueRecord, glueRecord.A.Data)...) + context.Additional = append(context.Additional, h.AAAA(ns.Host, glueRecord, glueRecord.AAAA.Data)...) } - res = upstreamRes } } - } else { - ips := h.Filter(state, &record.AAAA, logData) - answers = append(answers, h.AAAA(qname, record, ips)...) - } - case dns.TypeCNAME: - answers = append(answers, h.CNAME(qname, record)...) - case dns.TypeTXT: - answers = append(answers, h.TXT(qname, record)...) - case dns.TypeNS: - answers = append(answers, h.NS(qname, record)...) - case dns.TypeMX: - answers = append(answers, h.MX(qname, record)...) - case dns.TypeSRV: - answers = append(answers, h.SRV(qname, record)...) - case dns.TypeCAA: - caaRecord := h.FindCAA(record) - if caaRecord != nil { - answers = append(answers, h.CAA(qname, caaRecord)...) + break loop } - case dns.TypePTR: - answers = append(answers, h.PTR(qname, record)...) - case dns.TypeTLSA: - answers = append(answers, h.TLSA(qname, record)...) - case dns.TypeSOA: - answers = append(answers, record.Zone.Config.SOA.Data) - case dns.TypeDNSKEY: - if record.Zone.Config.DnsSec { - answers = []dns.RR{record.Zone.ZSK.DnsKey, record.Zone.KSK.DnsKey} - } - default: - answers = []dns.RR{} - authority = []dns.RR{} - res = dns.RcodeNotImplemented - } - if len(answers) == 0 { - if originalRecord.CNAME != nil { - answers = append(answers, h.CNAME(qname, record)...) - } else { - authority = append(authority, originalRecord.Zone.Config.SOA.Data) + + logger.Default.Debugf("[%d] final location : %s", context.Req.Id, currentQName) + if zone.Config.CnameFlattening { + currentQName = context.RawName() } - } - } else if localRes == dns.RcodeNameError { - answers = []dns.RR{} - authority = append(authority, originalRecord.Zone.Config.SOA.Data) - } else if localRes == dns.RcodeNotAuth { - if h.Config.UpstreamFallback { - upstreamAnswers, upstreamRes := h.upstream.Query(dns.Fqdn(qname), qtype) - if upstreamRes == dns.RcodeSuccess { - answers = append(answers, upstreamAnswers...) - auth = false + var answer []dns.RR + switch context.QType() { + case dns.TypeA: + var ips []IP_RR + var ttl uint32 + if len(currentRecord.A.Data) == 0 && currentRecord.ANAME != nil { + ips, res, ttl = h.FindANAME(context, currentRecord.ANAME.Location, dns.TypeA) + currentRecord.A.Ttl = ttl + } else { + ips = h.Filter(currentRecord.Name, context.SourceIp, ¤tRecord.A) + } + answer = h.A(currentQName, currentRecord, ips) + case dns.TypeAAAA: + var ips []IP_RR + var ttl uint32 + if len(currentRecord.AAAA.Data) == 0 && currentRecord.ANAME != nil { + ips, res, ttl = h.FindANAME(context, currentRecord.ANAME.Location, dns.TypeAAAA) + currentRecord.AAAA.Ttl = ttl + } else { + ips = h.Filter(currentRecord.Name, context.SourceIp, ¤tRecord.AAAA) + } + answer = h.AAAA(currentQName, currentRecord, ips) + case dns.TypeCNAME: + answer = h.CNAME(currentQName, currentRecord) + case dns.TypeTXT: + answer = h.TXT(currentQName, currentRecord) + case dns.TypeNS: + answer = h.NS(currentQName, currentRecord) + case dns.TypeMX: + answer = h.MX(currentQName, currentRecord) + case dns.TypeSRV: + answer = h.SRV(currentQName, currentRecord) + case dns.TypeCAA: + // TODO: handle FindCAA error response + caaRecord := h.FindCAA(currentRecord) + if caaRecord != nil { + answer = h.CAA(currentQName, caaRecord) + } + case dns.TypePTR: + answer = h.PTR(currentQName, currentRecord) + case dns.TypeTLSA: + answer = h.TLSA(currentQName, currentRecord) + case dns.TypeSOA: + answer = []dns.RR{zone.Config.SOA.Data} + case dns.TypeDNSKEY: + if zone.Config.DnsSec { + answer = []dns.RR{zone.ZSK.DnsKey, zone.KSK.DnsKey} + } + default: + context.Answer = []dns.RR{} + context.Authority = []dns.RR{zone.Config.SOA.Data} + res = dns.RcodeNotImplemented + break loop } - res = upstreamRes - } else if originalRecord != nil && originalRecord.CNAME != nil { - if len(answers) == 0 { - answers = append(answers, h.CNAME(qname, originalRecord)...) + context.Answer = append(context.Answer, answer...) + if len(answer) == 0 && res == dns.RcodeSuccess { + context.Authority = []dns.RR{zone.Config.SOA.Data} } - res = dns.RcodeSuccess + break loop } } - if auth && state.Do() && originalRecord != nil && originalRecord.Zone.Config.DnsSec { + if context.Do() && context.Auth && zone.Config.DnsSec { switch res { case dns.RcodeSuccess: - if len(answers) == 0 { - authority = append(authority, NSec(qname, originalRecord.Zone)) + if len(context.Answer) == 0 { + context.Authority = append(context.Authority, NSec(context.RawName(), zone)) } case dns.RcodeNameError: - authority = append(authority, NSec(qname, originalRecord.Zone)) + context.Authority = append(context.Authority, NSec(context.RawName(), zone)) res = dns.RcodeSuccess } - answers = Sign(answers, qname, originalRecord) - authority = Sign(authority, qname, originalRecord) + context.Answer = Sign(context.Answer, context.RawName(), zone) + context.Authority = Sign(context.Authority, context.RawName(), zone) + context.Additional = Sign(context.Additional, context.RawName(), zone) } - - h.LogRequest(logData, requestStartTime, res) - m := new(dns.Msg) - m.SetReply(state.Req) - m.Authoritative, m.RecursionAvailable, m.Compress = auth, h.Config.UpstreamFallback, true - m.SetRcode(state.Req, res) - m.Answer = append(m.Answer, answers...) - m.Ns = append(m.Ns, authority...) - - state.SizeAndDo(m) - m = state.Scrub(m) - state.W.WriteMsg(m) + h.Response(context, res) + logger.Default.Debugf("[%d] end handle request - name : %s, type : %s", context.Req.Id, context.RawName(), context.Type()) } -func (h *DnsRequestHandler) Filter(request *request.Request, rrset *IP_RRSet, logData map[string]interface{}) []IP_RR { - ips := h.healthcheck.FilterHealthcheck(request.Name(), rrset) +func (h *DnsRequestHandler) Filter(name string, sourceIp net.IP, rrset *IP_RRSet) []IP_RR { + ips := h.healthcheck.FilterHealthcheck(name, rrset) switch rrset.FilterConfig.GeoFilter { case "asn": - ips = h.geoip.GetSameASN(GetSourceIp(request), ips, logData) + ips = h.geoip.GetSameASN(sourceIp, ips) case "country": - ips = h.geoip.GetSameCountry(GetSourceIp(request), ips, logData) + ips = h.geoip.GetSameCountry(sourceIp, ips) case "asn+country": - ips = h.geoip.GetSameASN(GetSourceIp(request), ips, logData) - ips = h.geoip.GetSameCountry(GetSourceIp(request), ips, logData) + ips = h.geoip.GetSameASN(sourceIp, ips) + ips = h.geoip.GetSameCountry(sourceIp, ips) case "location": - ips = h.geoip.GetMinimumDistance(GetSourceIp(request), ips, logData) + ips = h.geoip.GetMinimumDistance(sourceIp, ips) default: } if len(ips) <= 1 { @@ -329,8 +360,6 @@ func (h *DnsRequestHandler) Filter(request *request.Request, rrset *IP_RRSet, lo default: index = 0 } - logData["destination_ip"] = ips[index].Ip.String() - logData["destination_country"] = ips[index].Country return []IP_RR{ips[index]} case "multi": @@ -349,37 +378,15 @@ func (h *DnsRequestHandler) Filter(request *request.Request, rrset *IP_RRSet, lo } } -func (h *DnsRequestHandler) LogRequest(data map[string]interface{}, startTime time.Time, responseCode int) { - data["process_time"] = time.Since(startTime).Nanoseconds() / 1000000 - data["response_code"] = responseCode - data["log_type"] = "request" - h.Logger.Log(data, "dns request") -} - -func GetSourceIp(request *request.Request) net.IP { - opt := request.Req.IsEdns0() - if opt != nil && len(opt.Option) != 0 { - for _, o := range opt.Option { - switch v := o.(type) { - case *dns.EDNS0_SUBNET: - return v.Address - } - } - } - return net.ParseIP(request.IP()) -} - -func GetSourceSubnet(request *request.Request) string { - opt := request.Req.IsEdns0() - if opt != nil && len(opt.Option) != 0 { - for _, o := range opt.Option { - switch o.(type) { - case *dns.EDNS0_SUBNET: - return o.String() - } - } +func (h *DnsRequestHandler) LogRequest(state *RequestContext, responseCode int) { + state.LogData["process_time"] = time.Since(state.StartTime).Nanoseconds() / 1000000 + state.LogData["response_code"] = responseCode + state.LogData["log_type"] = "request" + select { + case h.logQueue <- state.LogData: + default: + logger.Default.Warning("log queue is full") } - return "" } func reverseZone(zone string) string { @@ -396,6 +403,7 @@ func (h *DnsRequestHandler) LoadZones() { zones, err := h.Redis.SMembers("redins:zones") if err != nil { logger.Default.Error("cannot load zones : ", err) + return } newZones := iradix.New() for _, zone := range zones { @@ -404,22 +412,6 @@ func (h *DnsRequestHandler) LoadZones() { h.Zones = newZones } -func (h *DnsRequestHandler) FetchRecord(qname string, logData map[string]interface{}) (*Record, int) { - cachedRecord, found := h.RecordCache.Get(qname) - if found { - logger.Default.Debug("cached") - logData["cache"] = "HIT" - return cachedRecord.(*Record), dns.RcodeSuccess - } else { - logData["cache"] = "MISS" - record, res := h.GetRecord(qname) - if res == dns.RcodeSuccess { - h.RecordCache.Set(qname, record, time.Duration(h.Config.CacheTimeout)*time.Second) - } - return record, res - } -} - func (h *DnsRequestHandler) A(name string, record *Record, ips []IP_RR) (answers []dns.RR) { for _, ip := range ips { if ip.Ip == nil { @@ -482,7 +474,7 @@ func (h *DnsRequestHandler) NS(name string, record *Record) (answers []dns.RR) { r := new(dns.NS) r.Hdr = dns.RR_Header{Name: name, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: h.getTtl(record.NS.Ttl)} - r.Ns = ns.Host + r.Ns = dns.Fqdn(ns.Host) answers = append(answers, r) } return @@ -496,7 +488,7 @@ func (h *DnsRequestHandler) MX(name string, record *Record) (answers []dns.RR) { r := new(dns.MX) r.Hdr = dns.RR_Header{Name: name, Rrtype: dns.TypeMX, Class: dns.ClassINET, Ttl: h.getTtl(record.MX.Ttl)} - r.Mx = mx.Host + r.Mx = dns.Fqdn(mx.Host) r.Preference = mx.Preference answers = append(answers, r) } @@ -511,7 +503,7 @@ func (h *DnsRequestHandler) SRV(name string, record *Record) (answers []dns.RR) r := new(dns.SRV) r.Hdr = dns.RR_Header{Name: name, Rrtype: dns.TypeSRV, Class: dns.ClassINET, Ttl: h.getTtl(record.SRV.Ttl)} - r.Target = srv.Target + r.Target = dns.Fqdn(srv.Target) r.Weight = srv.Weight r.Port = srv.Port r.Priority = srv.Priority @@ -573,75 +565,6 @@ func (h *DnsRequestHandler) getTtl(ttl uint32) uint32 { return ttl } -func (h *DnsRequestHandler) findLocation(query string, z *Zone) string { - var ( - ok bool - closestEncloser string - sourceOfSynthesis string - ) - - // request for zone records - if query == z.Name { - return query - } - - query = strings.TrimSuffix(query, "."+z.Name) - - if _, ok = z.Locations[query]; ok { - return query - } - - closestEncloser, sourceOfSynthesis, ok = splitQuery(query) - for ok { - ceExists := keyMatches(closestEncloser, z) || keyExists(closestEncloser, z) - ssExists := keyExists(sourceOfSynthesis, z) - if ceExists { - if ssExists { - return sourceOfSynthesis - } else { - return "" - } - } else { - closestEncloser, sourceOfSynthesis, ok = splitQuery(closestEncloser) - } - } - return "" -} - -func keyExists(key string, z *Zone) bool { - _, ok := z.Locations[key] - return ok -} - -func keyMatches(key string, z *Zone) bool { - for value := range z.Locations { - if strings.HasSuffix(value, key) { - return true - } - } - return false -} - -func splitQuery(query string) (string, string, bool) { - if query == "" { - return "", "", false - } - var ( - splits []string - closestEncloser string - sourceOfSynthesis string - ) - splits = strings.SplitAfterN(query, ".", 2) - if len(splits) == 2 { - closestEncloser = splits[1] - sourceOfSynthesis = "*." + closestEncloser - } else { - closestEncloser = "" - sourceOfSynthesis = "*" - } - return closestEncloser, sourceOfSynthesis, true -} - func split255(s string) []string { if len(s) < 255 { return []string{s} @@ -662,7 +585,7 @@ func split255(s string) []string { return sx } -func (h *DnsRequestHandler) Matches(qname string) string { +func (h *DnsRequestHandler) FindZone(qname string) string { rname := reverseZone(qname) if _, zname, ok := h.Zones.Root().LongestPrefix([]byte(rname)); ok { return zname.(string) @@ -670,36 +593,6 @@ func (h *DnsRequestHandler) Matches(qname string) string { return "" } -func (h *DnsRequestHandler) GetRecord(qname string) (record *Record, rcode int) { - logger.Default.Debug("GetRecord") - - zone := h.Matches(qname) - logger.Default.Debugf("zone : %s", zone) - if zone == "" { - logger.Default.Debugf("no matching zone found for %s", qname) - return nil, dns.RcodeNotAuth - } - - z := h.LoadZone(zone) - if z == nil { - logger.Default.Errorf("empty zone : %s", zone) - return nil, dns.RcodeServerFailure - } - - location := h.findLocation(qname, z) - if len(location) == 0 { // empty, no results - return &Record{Name: qname, Zone: z}, dns.RcodeNameError - } - logger.Default.Debugf("location : %s", location) - - record = h.LoadLocation(location, z) - if record == nil { - return nil, dns.RcodeServerFailure - } - - return record, dns.RcodeSuccess -} - func (h *DnsRequestHandler) loadKey(pub string, priv string) *ZoneKey { pubStr, _ := h.Redis.Get(pub) if pubStr == "" { @@ -732,131 +625,134 @@ func (h *DnsRequestHandler) loadKey(pub string, priv string) *ZoneKey { } func (h *DnsRequestHandler) LoadZone(zone string) *Zone { - cachedZone, found := h.ZoneCache.Get(zone) - if found { - return cachedZone.(*Zone) + cachedZone := h.ZoneCache.Get(zone) + if cachedZone != nil && !cachedZone.Expired() { + return cachedZone.Value().(*Zone) } - z := new(Zone) - z.Name = zone - vals, err := h.Redis.GetHKeys("redins:zones:" + zone) - if err != nil { - logger.Default.Errorf("cannot load zone %s locations : %s", zone, err) - } - z.Locations = make(map[string]struct{}) - for _, val := range vals { - z.Locations[val] = struct{}{} - } - - z.Config = ZoneConfig{ - DnsSec: false, - CnameFlattening: false, - SOA: &SOA_RRSet{ - Ns: "ns1." + z.Name, - MinTtl: 300, - Refresh: 86400, - Retry: 7200, - Expire: 3600, - MBox: "hostmaster." + z.Name, - Serial: uint32(time.Now().Unix()), - Ttl: 300, - }, - } - val, err := h.Redis.Get("redins:zones:" + zone + ":config") - if err != nil { - logger.Default.Errorf("cannot load zone %s config : %s", zone, err) - } - if len(val) > 0 { - err := json.Unmarshal([]byte(val), &z.Config) + answer, _, _ := h.ZoneInflight.Do(zone, func() (interface{}, error) { + locations, err := h.Redis.GetHKeys("redins:zones:" + zone) if err != nil { - logger.Default.Errorf("cannot parse zone config : %s", err) - } - } - z.Config.SOA.Ns = dns.Fqdn(z.Config.SOA.Ns) - z.Config.SOA.Data = &dns.SOA{ - Hdr: dns.RR_Header{Name: z.Name, Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: z.Config.SOA.Ttl, Rdlength: 0}, - Ns: z.Config.SOA.Ns, - Mbox: z.Config.SOA.MBox, - Refresh: z.Config.SOA.Refresh, - Retry: z.Config.SOA.Retry, - Expire: z.Config.SOA.Expire, - Minttl: z.Config.SOA.MinTtl, - Serial: z.Config.SOA.Serial, - } - - z = func() *Zone { - if z.Config.DnsSec { - z.ZSK = h.loadKey("redins:zones:" + z.Name + ":zsk:pub", "redins:zones:" + z.Name + ":zsk:priv") - if z.ZSK == nil { - z.Config.DnsSec = false - return z - } - z.KSK = h.loadKey("redins:zones:" + z.Name + ":ksk:pub", "redins:zones:" + z.Name + ":ksk:priv") - if z.KSK == nil { - z.Config.DnsSec = false - return z - } + logger.Default.Errorf("cannot load zone %s locations : %s", zone, err) + return nil, err + } + config, err := h.Redis.Get("redins:zones:" + zone + ":config") + if err != nil { + logger.Default.Errorf("cannot load zone %s config : %s", zone, err) + } - z.ZSK.DnsKey.Flags = 256 - z.KSK.DnsKey.Flags = 257 - if z.ZSK.DnsKey.Hdr.Ttl != z.KSK.DnsKey.Hdr.Ttl { - z.ZSK.DnsKey.Hdr.Ttl = z.KSK.DnsKey.Hdr.Ttl - } + z := NewZone(zone, locations, config) + h.LoadZoneKeys(z) - if rrsig, err := sign([]dns.RR{z.ZSK.DnsKey, z.KSK.DnsKey}, z.Name, z.KSK, z.KSK.DnsKey.Hdr.Ttl); err == nil { - z.DnsKeySig = rrsig - } else { - logger.Default.Errorf("cannot create RRSIG for DNSKEY : %s", err) - z.Config.DnsSec = false - return z - } + h.ZoneCache.Set(zone, z, time.Duration(h.Config.CacheTimeout)*time.Second) + return z, nil + }) + if answer != nil { + return answer.(*Zone) + } else if cachedZone != nil { + return cachedZone.Value().(*Zone) + } + return nil +} + +func (h *DnsRequestHandler) LoadZoneKeys(z *Zone) { + if z.Config.DnsSec { + z.ZSK = h.loadKey("redins:zones:"+z.Name+":zsk:pub", "redins:zones:"+z.Name+":zsk:priv") + if z.ZSK == nil { + z.Config.DnsSec = false + return + } + z.KSK = h.loadKey("redins:zones:"+z.Name+":ksk:pub", "redins:zones:"+z.Name+":ksk:priv") + if z.KSK == nil { + z.Config.DnsSec = false + return + } + + z.ZSK.DnsKey.Flags = 256 + z.KSK.DnsKey.Flags = 257 + if z.ZSK.DnsKey.Hdr.Ttl != z.KSK.DnsKey.Hdr.Ttl { + z.ZSK.DnsKey.Hdr.Ttl = z.KSK.DnsKey.Hdr.Ttl } - return z - }() - h.ZoneCache.Set(zone, z, time.Duration(h.Config.CacheTimeout)*time.Second) - return z + if rrsig, err := sign([]dns.RR{z.ZSK.DnsKey, z.KSK.DnsKey}, z.Name, z.KSK, z.KSK.DnsKey.Hdr.Ttl); err == nil { + z.DnsKeySig = rrsig + } else { + logger.Default.Errorf("cannot create RRSIG for DNSKEY : %s", err) + z.Config.DnsSec = false + return + } + } } func (h *DnsRequestHandler) LoadLocation(location string, z *Zone) *Record { - var label, name string - if location == z.Name { - name = z.Name - label = "@" - } else { - name = location + "." + z.Name - label = location - } - r := new(Record) - r.A = IP_RRSet{ - FilterConfig: IpFilterConfig{ - Count: "multi", - Order: "none", - GeoFilter: "none", - }, - HealthCheckConfig: IpHealthCheckConfig{ - Enable: false, - }, - } - r.AAAA = r.A - r.Zone = z - r.Name = name - - val, _ := h.Redis.HGet("redins:zones:"+z.Name, label) - if val == "" && name == z.Name { - return r - } - err := json.Unmarshal([]byte(val), r) - if err != nil { - logger.Default.Errorf("cannot parse json : zone -> %s, location -> %s, \"%s\" -> %s", z.Name, location, val, err) - return nil + key := location + "." + z.Name + cachedRecord := h.RecordCache.Get(key) + if cachedRecord != nil && !cachedRecord.Expired() { + logger.Default.Debug("cached") + return cachedRecord.Value().(*Record) } - return r + answer, _, _ := h.RecordInflight.Do(key, func() (interface{}, error) { + var label, name string + if location == z.Name { + name = z.Name + label = "@" + } else { + name = location + "." + z.Name + label = location + } + r := new(Record) + r.A = IP_RRSet{ + FilterConfig: IpFilterConfig{ + Count: "multi", + Order: "none", + GeoFilter: "none", + }, + HealthCheckConfig: IpHealthCheckConfig{ + Enable: false, + }, + } + r.AAAA = r.A + r.Zone = z + r.Name = name + + if _, ok := z.Locations[label]; !ok { + // implicit root location + if label == "@" { + h.RecordCache.Set(key, r, time.Duration(h.Config.CacheTimeout)*time.Second) + return r, nil + } + err := errors.New(fmt.Sprintf("location %s not exists in %s", label, z.Name)) + logger.Default.Error(err) + return nil, err + } + + val, err := h.Redis.HGet("redins:zones:"+z.Name, label) + if err != nil { + logger.Default.Error(err, " : ", label, " ", z.Name) + return nil, err + } + if val != "" { + err := jsoniter.Unmarshal([]byte(val), r) + if err != nil { + logger.Default.Errorf("cannot parse json : zone -> %s, location -> %s, \"%s\" -> %s", z.Name, location, val, err) + return nil, err + } + } + h.RecordCache.Set(key, r, time.Duration(h.Config.CacheTimeout)*time.Second) + return r, nil + }) + + if answer != nil { + return answer.(*Record) + } else if cachedRecord != nil { + return cachedRecord.Value().(*Record) + } + return nil } func (h *DnsRequestHandler) SetLocation(location string, z *Zone, val *Record) { - jsonValue, err := json.Marshal(val) + jsonValue, err := jsoniter.Marshal(val) if err != nil { logger.Default.Errorf("cannot encode to json : %s", err) return @@ -867,7 +763,9 @@ func (h *DnsRequestHandler) SetLocation(location string, z *Zone, val *Record) { } else { label = location } - h.Redis.HSet(z.Name, label, string(jsonValue)) + if err = h.Redis.HSet(z.Name, label, string(jsonValue)); err != nil { + logger.Default.Error("redis error : ", err) + } } func ChooseIp(ips []IP_RR, weighted bool) int { @@ -905,15 +803,111 @@ func ChooseIp(ips []IP_RR, weighted bool) int { func (h *DnsRequestHandler) FindCAA(record *Record) *Record { zone := record.Zone currentRecord := record - for currentRecord != nil && strings.HasSuffix(currentRecord.Name, zone.Name) { + currentLocation := strings.TrimSuffix(currentRecord.Name, "."+zone.Name) + for { + logger.Default.Debug("location : ", currentLocation) if len(currentRecord.CAA.Data) != 0 { return currentRecord } - splits := strings.SplitAfterN(currentRecord.Name, ".", 2) + splits := strings.SplitAfterN(currentLocation, ".", 2) if len(splits) != 2 { + break + } + var match int + currentLocation, match = zone.FindLocation(splits[1]) + if match == NoMatch { + return nil + } + currentRecord = h.LoadLocation(currentLocation, zone) + if currentRecord == nil { return nil } - currentRecord, _ = h.FetchRecord(splits[1], map[string]interface{}{}) + } + currentRecord = h.LoadLocation(zone.Name, zone) + if currentRecord == nil { + return nil + } + if len(currentRecord.CAA.Data) != 0 { + return currentRecord } return nil } + +func (h *DnsRequestHandler) FindANAME(context *RequestContext, aname string, qtype uint16) ([]IP_RR, int, uint32) { + logger.Default.Debug("finding aname") + currentQName := aname + currentRecord := &Record{} + loopCount := 0 + for { + if loopCount > 10 { + logger.Default.Errorf("ANAME loop in request %s->%s", context.RawName(), context.Type()) + return []IP_RR{}, dns.RcodeServerFailure, 0 + } + loopCount++ + + zoneName := h.FindZone(currentQName) + logger.Default.Debug("zone : ", zoneName, " qname : ", currentQName, " record : ", currentRecord.Name) + if zoneName == "" { + logger.Default.Debug("non-authoritative zone, using upstream") + upstreamAnswers, upstreamRes := h.upstream.Query(currentQName, qtype) + if upstreamRes == dns.RcodeSuccess { + var ips []IP_RR + var upstreamTtl uint32 + if len(upstreamAnswers) > 0 { + upstreamTtl = upstreamAnswers[0].Header().Ttl + } + for _, r := range upstreamAnswers { + if qtype == dns.TypeA { + if a, ok := r.(*dns.A); ok { + ips = append(ips, IP_RR{Ip: a.A}) + } + } else { + if aaaa, ok := r.(*dns.AAAA); ok { + ips = append(ips, IP_RR{Ip: aaaa.AAAA}) + } + } + } + return ips, upstreamRes, upstreamTtl + } else { + return []IP_RR{}, dns.RcodeServerFailure, 0 + } + } + + zone := h.LoadZone(zoneName) + if zone == nil { + logger.Default.Debugf("error loading zone : %s", zoneName) + return []IP_RR{}, dns.RcodeServerFailure, 0 + } + location, _ := zone.FindLocation(currentQName) + if location == "" { + logger.Default.Debugf("location not found for %s", currentQName) + return []IP_RR{}, dns.RcodeServerFailure, 0 + } + + currentRecord = h.LoadLocation(location, zone) + if currentRecord == nil { + return []IP_RR{}, dns.RcodeServerFailure, 0 + } + if currentRecord.CNAME != nil { + logger.Default.Debug("cname") + currentQName = currentRecord.CNAME.Host + continue + } + + if qtype == dns.TypeA && len(currentRecord.A.Data) > 0 { + logger.Default.Debug("found a") + return h.Filter(currentRecord.Name, context.SourceIp, ¤tRecord.A), dns.RcodeSuccess, currentRecord.A.Ttl + } else if qtype == dns.TypeAAAA && len(currentRecord.AAAA.Data) > 0 { + logger.Default.Debug("found aaaa") + return h.Filter(currentRecord.Name, context.SourceIp, ¤tRecord.AAAA), dns.RcodeSuccess, currentRecord.AAAA.Ttl + } + + if currentRecord.ANAME != nil { + logger.Default.Debug("aname") + currentQName = currentRecord.ANAME.Location + continue + } + + return []IP_RR{}, dns.RcodeSuccess, 0 + } +} diff --git a/handler/handler_test.go b/handler/handler_test.go index e5390103fd023995437524555ee485e245323907..8d36f64ac74e8dca42042b579ab86f65746a2851 100644 --- a/handler/handler_test.go +++ b/handler/handler_test.go @@ -1,1898 +1,2524 @@ package handler import ( - "log" - "net" - "testing" - "arvancloud/redins/test" + "errors" "fmt" - "github.com/coredns/coredns/request" "github.com/hawell/logger" "github.com/hawell/uperdis" "github.com/miekg/dns" + "net" + "strings" + "testing" "time" ) -var lookupZones = []string{ - "example.com.", "example.net.", "example.aaa.", "example.bbb.", "example.ccc.", "example.ddd.", "example.caa.", "0.0.127.in-addr.arpa.", "20.127.10.in-addr.arpa.", +type TestCase struct { + Name string + Description string + Enabled bool + Config DnsRequestHandlerConfig + Initialize func(testCase *TestCase) (*DnsRequestHandler, error) + ApplyAndVerify func(testCase *TestCase, handler *DnsRequestHandler, t *testing.T) + Zones []string + ZoneConfigs []string + Entries [][][]string + TestCases []test.Case } -var lookupConfig = []string{ - `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.example.com.","ns":"ns1.example.com.","refresh":44,"retry":55,"expire":66}}`, - `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.example.net.","ns":"ns1.example.net.","refresh":44,"retry":55,"expire":66}}`, - `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.example.aaa.","ns":"ns1.example.aaa.","refresh":44,"retry":55,"expire":66}}`, - `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.example.bbb.","ns":"ns1.example.bbb.","refresh":44,"retry":55,"expire":66}}`, - `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.example.ccc.","ns":"ns1.example.ccc.","refresh":44,"retry":55,"expire":66}}`, - `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.example.ddd.","ns":"ns1.example.ddd.","refresh":44,"retry":55,"expire":66},"cname_flattening":true}`, - `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.example.caa.","ns":"ns1.example.caa.","refresh":44,"retry":55,"expire":66}}`, - "", - "", +func defaultInitialize(testCase *TestCase) (*DnsRequestHandler, error) { + logger.Default = logger.NewLogger(&logger.LogConfig{}, nil) + + h := NewHandler(&testCase.Config) + if err := h.Redis.Del("*"); err != nil { + return nil, err + } + for i, zone := range testCase.Zones { + if err := h.Redis.SAdd("redins:zones", zone); err != nil { + return nil, err + } + for _, cmd := range testCase.Entries[i] { + err := h.Redis.HSet("redins:zones:"+zone, cmd[0], cmd[1]) + if err != nil { + return nil, errors.New(fmt.Sprintf("[ERROR] cannot connect to redis: %s", err)) + } + } + if err := h.Redis.Set("redins:zones:"+zone+":config", testCase.ZoneConfigs[i]); err != nil { + return nil, err + } + } + h.LoadZones() + return h, nil } -var lookupEntries = [][][]string{ - { - {"x", - `{ - "a":{"ttl":300, "records":[{"ip":"1.2.3.4", "country":"ES"},{"ip":"5.6.7.8", "country":""}]}, - "aaaa":{"ttl":300, "records":[{"ip":"::1"}]}, - "txt":{"ttl":300, "records":[{"text":"foo"},{"text":"bar"}]}, - "ns":{"ttl":300, "records":[{"host":"ns1.example.com."},{"host":"ns2.example.com."}]}, - "mx":{"ttl":300, "records":[{"host":"mx1.example.com.", "preference":10},{"host":"mx2.example.com.", "preference":10}]}, - "srv":{"ttl":300, "records":[{"target":"sip.example.com.","port":555,"priority":10,"weight":100}]} - }`, - }, - {"y", - `{"cname":{"ttl":300, "host":"x.example.com."}}`, - }, - {"ns1", - `{"a":{"ttl":300, "records":[{"ip":"2.2.2.2"}]}}`, - }, - {"ns2", - `{"a":{"ttl":300, "records":[{"ip":"3.3.3.3"}]}}`, - }, - {"_sip._tcp", - `{"srv":{"ttl":300, "records":[{"target":"sip.example.com.","port":555,"priority":10,"weight":100}]}}`, - }, - {"_443._tcp.www", - `{"tlsa":{"ttl":300, "records":[{"usage":0, "selector":0, "matching_type":1, "certificate":"d2abde240d7cd3ee6b4b28c54df034b97983a1d16e8a410e4561cb106618e971"}]}}`, - }, - {"_990._tcp", - `{ - "tlsa":{"ttl":300, "records":[ - {"usage":1, "selector":1, "matching_type":1, "certificate":"1CFC98A706BCF3683015"}, - {"usage":1, "selector":1, "matching_type":1, "certificate":"62D5414CD1CC657E3D30"} - ]}}`, - }, - {"sip", - `{"a":{"ttl":300, "records":[{"ip":"7.7.7.7"}]}, - "aaaa":{"ttl":300, "records":[{"ip":"::1"}]}}`, - }, - {"t.u.v.w", - `{"a":{"ttl":300, "records":[{"ip":"9.9.9.9"}]}}`, - }, - }, - { - {"@", - `{"ns":{"ttl":300, "records":[{"host":"ns1.example.net."},{"host":"ns2.example.net."}]}}`, - }, - {"sub.*", - `{"txt":{"ttl":300, "records":[{"text":"this is not a wildcard"}]}}`, - }, - {"host1", - `{"a":{"ttl":300, "records":[{"ip":"5.5.5.5"}]}}`, - }, - {"subdel", - `{"ns":{"ttl":300, "records":[{"host":"ns1.subdel.example.net."},{"host":"ns2.subdel.example.net."}]}}`, - }, - {"*", - `{"txt":{"ttl":300, "records":[{"text":"this is a wildcard"}]}, - "mx":{"ttl":300, "records":[{"host":"host1.example.net.","preference": 10}]}}`, - }, - {"_ssh._tcp.host1", - `{"srv":{"ttl":300, "records":[{"target":"tcp.example.com.","port":123,"priority":10,"weight":100}]}}`, - }, - {"_ssh._tcp.host2", - `{"srv":{"ttl":300, "records":[{"target":"tcp.example.com.","port":123,"priority":10,"weight":100}]}}`, - }, - }, - { - {"x", - `{"a":{"ttl":300, "records":[{"ip":"1.2.3.4"}]}, - "aaaa":{"ttl":300, "records":[{"ip":"::1"}]}, - "txt":{"ttl":300, "records":[{"text":"foo"},{"text":"bar"}]}, - "ns":{"ttl":300, "records":[{"host":"ns1.example.aaa."},{"ttl":300, "host":"ns2.example.aaa."}]}, - "mx":{"ttl":300, "records":[{"host":"mx1.example.aaa.", "preference":10},{"host":"mx2.example.aaa.", "preference":10}]}, - "srv":{"ttl":300, "records":[{"target":"sip.example.aaa.","port":555,"priority":10,"weight":100}]}}`, - }, - {"y", - `{"cname":{"ttl":300, "host":"x.example.aaa."}}`, - }, - {"z", - `{"cname":{"ttl":300, "host":"y.example.aaa."}}`, - }, - }, - { - {"x", - `{"a":{"ttl":300, "records":[{"ip":"1.2.3.4"}]}}`, - }, - {"y", - `{"cname":{"ttl":300, "host":"x.example.bbb."}}`, - }, - {"z", - `{}`, - }, - }, - { - {"x", - `{"txt":{"ttl":300, "records":[{"text":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}]}}`, - }, - }, - { - {"a", - `{"a":{"ttl":300, "records":[{"ip":"1.2.3.4"}]}, - "aaaa":{"ttl":300, "records":[{"ip":"::1"}]}, - "txt":{"ttl":300, "records":[{"text":"foo"},{"text":"bar"}]}, - "ns":{"ttl":300, "records":[{"host":"ns1.example.ddd."},{"ttl":300, "host":"ns2.example.ddd."}]}, - "mx":{"ttl":300, "records":[{"host":"mx1.example.ddd.", "preference":10},{"host":"mx2.example.ddd.", "preference":10}]}, - "srv":{"ttl":300, "records":[{"target":"sip.example.ddd.","port":555,"priority":10,"weight":100}]}}`, - }, - {"b", - `{"cname":{"ttl":300, "host":"a.example.ddd."}}`, - }, - {"c", - `{"cname":{"ttl":300, "host":"b.example.ddd."}}`, - }, - {"d", - `{"cname":{"ttl":300, "host":"c.example.ddd."}}`, - }, - {"e", - `{"cname":{"ttl":300, "host":"d.example.ddd."}}`, + +func defaultApplyAndVerify(testCase *TestCase, handler *DnsRequestHandler, t *testing.T) { + for i, tc := range testCase.TestCases { + + r := tc.Msg() + w := test.NewRecorder(&test.ResponseWriter{}) + state := NewRequestContext(w, r) + handler.HandleRequest(state) + + resp := w.Msg + + if err := test.SortAndCheck(resp, tc); err != nil { + fmt.Println(i, err, tc.Qname, tc.Answer, resp.Answer) + t.Fail() + } + } +} + +var defaultConfig = DnsRequestHandlerConfig{ + MaxTtl: 300, + CacheTimeout: 60, + ZoneReload: 600, + Redis: uperdis.RedisConfig{ + Address: "redis:6379", + Net: "tcp", + DB: 0, + Password: "", + Prefix: "test_", + Suffix: "_test", + Connection: uperdis.RedisConnectionConfig{ + MaxIdleConnections: 10, + MaxActiveConnections: 10, + ConnectTimeout: 500, + ReadTimeout: 500, + IdleKeepAlive: 30, + MaxKeepAlive: 0, + WaitForConnection: true, }, }, - { - {"@", - `{"caa":{"ttl":300, "records":[{"tag":"issue", "value":"godaddy.com;", "flag":0}]}}`, - }, - {"a.b.c.d", - `{"cname":{"ttl":300, "host":"b.c.d.example.caa."}}`, - }, - {"b.c.d", - `{"cname":{"ttl":300, "host":"c.d.example.caa."}}`, - }, - {"c.d", - `{"cname":{"ttl":300, "host":"d.example.caa."}}`, - }, - {"d", - `{"cname":{"ttl":300, "host":"example.caa."}}`, - }, - {"x.y.z", - `{"a":{"ttl":300, "records":[{"ip":"1.2.3.4"}]}}`, - }, - {"y.z", - `{"a":{"ttl":300, "records":[{"ip":"1.2.3.4"}]}}`, - }, - {"z", - `{"a":{"ttl":300, "records":[{"ip":"1.2.3.4"}]}}`, - }, + Log: logger.LogConfig{ + Enable: false, }, - { - {"1", - `{"ptr":{"ttl":300, "domain":"localhost"}}`, + Upstream: []UpstreamConfig{ + { + Ip: "1.1.1.1", + Port: 53, + Protocol: "udp", + Timeout: 1000, }, }, - { - {"54", - `{"ptr":{"ttl":300, "domain":"example.fff"}}`, - }, + GeoIp: GeoIpConfig{ + Enable: true, + CountryDB: "../geoCity.mmdb", + ASNDB: "../geoIsp.mmdb", }, } -var lookupTestCases = [][]test.Case{ - // basic tests +var testCases = []*TestCase{ { - // NOAUTH Test - { - Qname: "dsdsd.sdf.dfd.", Qtype: dns.TypeA, - Rcode: dns.RcodeNotAuth, - }, - // A Test - { - Qname: "x.example.com.", Qtype: dns.TypeA, - Answer: []dns.RR{ - test.A("x.example.com. 300 IN A 1.2.3.4"), - test.A("x.example.com. 300 IN A 5.6.7.8"), + Name: "Basic Usage", + Description: "Test Basic functionality", + Enabled: true, + Config: defaultConfig, + Initialize: defaultInitialize, + ApplyAndVerify: defaultApplyAndVerify, + Zones: []string{"example.com."}, + ZoneConfigs: []string{`{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.example.com.","ns":"ns1.example.com.","refresh":44,"retry":55,"expire":66}}`}, + Entries: [][][]string{ + { + {"@", + `{"ns":{"ttl":300, "records":[{"host":"ns1.example.com."},{"host":"ns2.example.com."}]}}`, + }, + {"x", + `{ + "a":{"ttl":300, "records":[{"ip":"1.2.3.4", "country":"ES"},{"ip":"5.6.7.8", "country":""}]}, + "aaaa":{"ttl":300, "records":[{"ip":"::1"}]}, + "txt":{"ttl":300, "records":[{"text":"foo"},{"text":"bar"}]}, + "mx":{"ttl":300, "records":[{"host":"mx1.example.com.", "preference":10},{"host":"mx2.example.com.", "preference":10}]}, + "srv":{"ttl":300, "records":[{"target":"sip.example.com.","port":555,"priority":10,"weight":100}]} + }`, + }, + {"y", + `{"cname":{"ttl":300, "host":"x.example.com."}}`, + }, + {"ns1", + `{"a":{"ttl":300, "records":[{"ip":"2.2.2.2"}]}}`, + }, + {"ns2", + `{"a":{"ttl":300, "records":[{"ip":"3.3.3.3"}]}}`, + }, + {"_sip._tcp", + `{"srv":{"ttl":300, "records":[{"target":"sip.example.com.","port":555,"priority":10,"weight":100}]}}`, + }, + {"_443._tcp.www", + `{"tlsa":{"ttl":300, "records":[{"usage":0, "selector":0, "matching_type":1, "certificate":"d2abde240d7cd3ee6b4b28c54df034b97983a1d16e8a410e4561cb106618e971"}]}}`, + }, + {"_990._tcp", + `{ + "tlsa":{"ttl":300, "records":[ + {"usage":1, "selector":1, "matching_type":1, "certificate":"1CFC98A706BCF3683015"}, + {"usage":1, "selector":1, "matching_type":1, "certificate":"62D5414CD1CC657E3D30"} + ]} + }`, + }, + {"sip", + `{ + "a":{"ttl":300, "records":[{"ip":"7.7.7.7"}]}, + "aaaa":{"ttl":300, "records":[{"ip":"::1"}]} + }`, + }, + {"t.u.v.w", + `{"a":{"ttl":300, "records":[{"ip":"9.9.9.9"}]}}`, + }, + {"cnametonx", + `{"cname":{"ttl":300, "host":"notexists.example.com."}}`, + }, }, }, - // AAAA Test - { - Qname: "x.example.com.", Qtype: dns.TypeAAAA, - Answer: []dns.RR{ - test.AAAA("x.example.com. 300 IN AAAA ::1"), + TestCases: []test.Case{ + // NOAUTH Test + { + Qname: "dsdsd.sdf.dfd.", Qtype: dns.TypeA, + Rcode: dns.RcodeNotAuth, }, - }, - // TXT Test - { - Qname: "x.example.com.", Qtype: dns.TypeTXT, - Answer: []dns.RR{ - test.TXT("x.example.com. 300 IN TXT bar"), - test.TXT("x.example.com. 300 IN TXT foo"), + // A Test + { + Qname: "x.example.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("x.example.com. 300 IN A 1.2.3.4"), + test.A("x.example.com. 300 IN A 5.6.7.8"), + }, }, - }, - // CNAME Test - { - Qname: "y.example.com.", Qtype: dns.TypeCNAME, - Answer: []dns.RR{ - test.CNAME("y.example.com. 300 IN CNAME x.example.com."), + // AAAA Test + { + Qname: "x.example.com.", Qtype: dns.TypeAAAA, + Answer: []dns.RR{ + test.AAAA("x.example.com. 300 IN AAAA ::1"), + }, }, - }, - // NS Test - { - Qname: "x.example.com.", Qtype: dns.TypeNS, - Answer: []dns.RR{ - test.NS("x.example.com. 300 IN NS ns1.example.com."), - test.NS("x.example.com. 300 IN NS ns2.example.com."), + // TXT Test + { + Qname: "x.example.com.", Qtype: dns.TypeTXT, + Answer: []dns.RR{ + test.TXT("x.example.com. 300 IN TXT bar"), + test.TXT("x.example.com. 300 IN TXT foo"), + }, }, - }, - // MX Test - { - Qname: "x.example.com.", Qtype: dns.TypeMX, - Answer: []dns.RR{ - test.MX("x.example.com. 300 IN MX 10 mx1.example.com."), - test.MX("x.example.com. 300 IN MX 10 mx2.example.com."), + // CNAME Test + { + Qname: "y.example.com.", Qtype: dns.TypeCNAME, + Answer: []dns.RR{ + test.CNAME("y.example.com. 300 IN CNAME x.example.com."), + }, }, - }, - // SRV Test - { - Qname: "_sip._tcp.example.com.", Qtype: dns.TypeSRV, - Answer: []dns.RR{ - test.SRV("_sip._tcp.example.com. 300 IN SRV 10 100 555 sip.example.com."), + // NS Test + { + Qname: "example.com.", Qtype: dns.TypeNS, + Answer: []dns.RR{ + test.NS("example.com. 300 IN NS ns1.example.com."), + test.NS("example.com. 300 IN NS ns2.example.com."), + }, }, - }, - // TLSA Test - { - Qname: "_443._tcp.www.example.com.", Qtype: dns.TypeTLSA, - Answer: []dns.RR{ - test.TLSA("_443._tcp.www.example.com. 300 IN TLSA 0 0 1 d2abde240d7cd3ee6b4b28c54df034b97983a1d16e8a410e4561cb106618e971"), + // MX Test + { + Qname: "x.example.com.", Qtype: dns.TypeMX, + Answer: []dns.RR{ + test.MX("x.example.com. 300 IN MX 10 mx1.example.com."), + test.MX("x.example.com. 300 IN MX 10 mx2.example.com."), + }, }, - }, - { - Qname: "_990._tcp.example.com.", Qtype: dns.TypeTLSA, - Answer: []dns.RR{ - test.TLSA("_990._tcp.example.com. 300 IN TLSA 1 1 1 1CFC98A706BCF3683015"), - test.TLSA("_990._tcp.example.com. 300 IN TLSA 1 1 1 62D5414CD1CC657E3D30"), + // SRV Test + { + Qname: "_sip._tcp.example.com.", Qtype: dns.TypeSRV, + Answer: []dns.RR{ + test.SRV("_sip._tcp.example.com. 300 IN SRV 10 100 555 sip.example.com."), + }, }, - }, - // NXDOMAIN Test - { - Qname: "notexists.example.com.", Qtype: dns.TypeA, - Rcode: dns.RcodeNameError, - Ns: []dns.RR{ - test.SOA("example.com. 300 IN SOA ns1.example.com. hostmaster.example.com. 1460498836 44 55 66 100"), + // TLSA Test + { + Qname: "_443._tcp.www.example.com.", Qtype: dns.TypeTLSA, + Answer: []dns.RR{ + test.TLSA("_443._tcp.www.example.com. 300 IN TLSA 0 0 1 d2abde240d7cd3ee6b4b28c54df034b97983a1d16e8a410e4561cb106618e971"), + }, }, - }, - // SOA Test - { - Qname: "example.com.", Qtype: dns.TypeSOA, - Answer: []dns.RR{ - test.SOA("example.com. 300 IN SOA ns1.example.com. hostmaster.example.com. 1460498836 44 55 66 100"), + { + Qname: "_990._tcp.example.com.", Qtype: dns.TypeTLSA, + Answer: []dns.RR{ + test.TLSA("_990._tcp.example.com. 300 IN TLSA 1 1 1 1CFC98A706BCF3683015"), + test.TLSA("_990._tcp.example.com. 300 IN TLSA 1 1 1 62D5414CD1CC657E3D30"), + }, }, - }, - // not implemented - { - Qname: "example.com.", Qtype: dns.TypeUNSPEC, - Rcode: dns.RcodeNotImplemented, - Ns: []dns.RR{ - test.SOA("example.com. 300 IN SOA ns1.example.com. hostmaster.example.com. 1460498836 44 55 66 100"), - }, - }, - // Empty non-terminal Test - // FIXME: should return NOERROR instead of NXDOMAIN - /* - { - Qname:"v.w.example.com.", Qtype: dns.TypeA, - }, - */ - }, - // Wildcard Tests - { - { - Qname: "host3.example.net.", Qtype: dns.TypeMX, - Answer: []dns.RR{ - test.MX("host3.example.net. 300 IN MX 10 host1.example.net."), + // NXDOMAIN Test + { + Qname: "notexists.example.com.", Qtype: dns.TypeA, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.SOA("example.com. 300 IN SOA ns1.example.com. hostmaster.example.com. 1460498836 44 55 66 100"), + }, }, - }, - { - Qname: "host3.example.net.", Qtype: dns.TypeA, - Ns: []dns.RR{ - test.SOA("example.net. 300 IN SOA ns1.example.net. hostmaster.example.net. 1460498836 44 55 66 100"), + // NXDOMAIN through CNAME Test + { + Qname: "cnametonx.example.com.", Qtype: dns.TypeA, + Rcode: dns.RcodeNameError, + Answer: []dns.RR{ + test.CNAME("cnametonx.example.com. 300 IN CNAME notexists.example.com."), + }, + Ns: []dns.RR{ + test.SOA("example.com. 300 IN SOA ns1.example.com. hostmaster.example.com. 1460498836 44 55 66 100"), + }, }, - }, - { - Qname: "foo.bar.example.net.", Qtype: dns.TypeTXT, - Answer: []dns.RR{ - test.TXT("foo.bar.example.net. 300 IN TXT \"this is a wildcard\""), + // SOA Test + { + Qname: "example.com.", Qtype: dns.TypeSOA, + Answer: []dns.RR{ + test.SOA("example.com. 300 IN SOA ns1.example.com. hostmaster.example.com. 1460498836 44 55 66 100"), + }, }, - }, - { - Qname: "host1.example.net.", Qtype: dns.TypeMX, - Ns: []dns.RR{ - test.SOA("example.net. 300 IN SOA ns1.example.net. hostmaster.example.net. 1460498836 44 55 66 100"), + // not implemented + { + Qname: "example.com.", Qtype: dns.TypeUNSPEC, + Rcode: dns.RcodeNotImplemented, + Ns: []dns.RR{ + test.SOA("example.com. 300 IN SOA ns1.example.com. hostmaster.example.com. 1460498836 44 55 66 100"), + }, }, + // Empty non-terminal Test + // FIXME: should return NOERROR instead of NXDOMAIN + /* + { + Qname:"v.w.example.com.", Qtype: dns.TypeA, + }, + */ }, - { - Qname: "sub.*.example.net.", Qtype: dns.TypeMX, - Ns: []dns.RR{ - test.SOA("example.net. 300 IN SOA ns1.example.net. hostmaster.example.net. 1460498836 44 55 66 100"), + }, + { + Name: "WildCard", + Description: "tests related to handling of different wildcard scenarios", + Enabled: true, + Config: defaultConfig, + Initialize: defaultInitialize, + ApplyAndVerify: defaultApplyAndVerify, + Zones: []string{"example.net."}, + ZoneConfigs: []string{`{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.example.net.","ns":"ns1.example.net.","refresh":44,"retry":55,"expire":66}}`}, + Entries: [][][]string{ + { + {"@", + `{"ns":{"ttl":300, "records":[{"host":"ns1.example.net."},{"host":"ns2.example.net."}]}}`, + }, + {"sub.*", + `{"txt":{"ttl":300, "records":[{"text":"this is not a wildcard"}]}}`, + }, + {"host1", + `{"a":{"ttl":300, "records":[{"ip":"5.5.5.5"}]}}`, + }, + {"subdel", + `{"ns":{"ttl":300, "records":[{"host":"ns1.subdel.example.net."},{"host":"ns2.subdel.example.net."}]}}`, + }, + {"*", + `{ + "txt":{"ttl":300, "records":[{"text":"this is a wildcard"}]}, + "mx":{"ttl":300, "records":[{"host":"host1.example.net.","preference": 10}]} + }`, + }, + {"_ssh._tcp.host1", + `{"srv":{"ttl":300, "records":[{"target":"tcp.example.com.","port":123,"priority":10,"weight":100}]}}`, + }, + {"_ssh._tcp.host2", + `{"srv":{"ttl":300, "records":[{"target":"tcp.example.com.","port":123,"priority":10,"weight":100}]}}`, + }, }, }, - { - Qname: "host.subdel.example.net.", Qtype: dns.TypeA, - Rcode: dns.RcodeNameError, - Ns: []dns.RR{ - test.SOA("example.net. 300 IN SOA ns1.example.net. hostmaster.example.net. 1460498836 44 55 66 100"), + TestCases: []test.Case{ + { + Qname: "host3.example.net.", Qtype: dns.TypeMX, + Answer: []dns.RR{ + test.MX("host3.example.net. 300 IN MX 10 host1.example.net."), + }, }, - }, - { - Qname: "ghost.*.example.net.", Qtype: dns.TypeMX, - Rcode: dns.RcodeNameError, - Ns: []dns.RR{ - test.SOA("example.net. 300 IN SOA ns1.example.net. hostmaster.example.net. 1460498836 44 55 66 100"), + { + Qname: "host3.example.net.", Qtype: dns.TypeA, + Ns: []dns.RR{ + test.SOA("example.net. 300 IN SOA ns1.example.net. hostmaster.example.net. 1460498836 44 55 66 100"), + }, }, - }, - { - Qname: "f.h.g.f.t.r.e.example.net.", Qtype: dns.TypeTXT, - Answer: []dns.RR{ - test.TXT("f.h.g.f.t.r.e.example.net. 300 IN TXT \"this is a wildcard\""), + { + Qname: "foo.bar.example.net.", Qtype: dns.TypeTXT, + Answer: []dns.RR{ + test.TXT("foo.bar.example.net. 300 IN TXT \"this is a wildcard\""), + }, }, - }, - }, - // CNAME tests - { - { - Qname: "y.example.aaa.", Qtype: dns.TypeCNAME, - Answer: []dns.RR{ - test.CNAME("y.example.aaa. 300 IN CNAME x.example.aaa."), + { + Qname: "host1.example.net.", Qtype: dns.TypeMX, + Ns: []dns.RR{ + test.SOA("example.net. 300 IN SOA ns1.example.net. hostmaster.example.net. 1460498836 44 55 66 100"), + }, }, - }, - { - Qname: "z.example.aaa.", Qtype: dns.TypeCNAME, - Answer: []dns.RR{ - test.CNAME("z.example.aaa. 300 IN CNAME y.example.aaa."), + { + Qname: "sub.*.example.net.", Qtype: dns.TypeMX, + Ns: []dns.RR{ + test.SOA("example.net. 300 IN SOA ns1.example.net. hostmaster.example.net. 1460498836 44 55 66 100"), + }, }, - }, - { - Qname: "z.example.aaa.", Qtype: dns.TypeA, - Answer: []dns.RR{ - test.A("x.example.aaa. 300 IN A 1.2.3.4"), - test.CNAME("y.example.aaa. 300 IN CNAME x.example.aaa."), - test.CNAME("z.example.aaa. 300 IN CNAME y.example.aaa."), + { + Qname: "host.subdel.example.net.", Qtype: dns.TypeA, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.SOA("example.net. 300 IN SOA ns1.example.net. hostmaster.example.net. 1460498836 44 55 66 100"), + }, + }, + { + Qname: "ghost.*.example.net.", Qtype: dns.TypeMX, + Rcode: dns.RcodeNameError, + Ns: []dns.RR{ + test.SOA("example.net. 300 IN SOA ns1.example.net. hostmaster.example.net. 1460498836 44 55 66 100"), + }, + }, + { + Qname: "f.h.g.f.t.r.e.example.net.", Qtype: dns.TypeTXT, + Answer: []dns.RR{ + test.TXT("f.h.g.f.t.r.e.example.net. 300 IN TXT \"this is a wildcard\""), + }, }, }, }, - // empty values tests { - // empty A test - { - Qname: "z.example.bbb.", Qtype: dns.TypeA, - Ns: []dns.RR{ - test.SOA("example.bbb. 300 IN SOA ns1.example.bbb. hostmaster.example.bbb. 1460498836 44 55 66 100"), + Name: "CNAME", + Description: "normal cname functionality", + Enabled: true, + Config: defaultConfig, + Initialize: defaultInitialize, + ApplyAndVerify: defaultApplyAndVerify, + Zones: []string{"example.aaa."}, + ZoneConfigs: []string{`{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.example.aaa.","ns":"ns1.example.aaa.","refresh":44,"retry":55,"expire":66}}`}, + Entries: [][][]string{ + { + {"@", + `{"ns":{"ttl":300, "records":[{"host":"ns1.example.aaa."},{"ttl":300, "host":"ns2.example.aaa."}]},}`, + }, + {"x", + `{ + "a":{"ttl":300, "records":[{"ip":"1.2.3.4"}]}, + "aaaa":{"ttl":300, "records":[{"ip":"::1"}]}, + "txt":{"ttl":300, "records":[{"text":"foo"},{"text":"bar"}]}, + "mx":{"ttl":300, "records":[{"host":"mx1.example.aaa.", "preference":10},{"host":"mx2.example.aaa.", "preference":10}]}, + "srv":{"ttl":300, "records":[{"target":"sip.example.aaa.","port":555,"priority":10,"weight":100}]} + }`, + }, + {"y", + `{"cname":{"ttl":300, "host":"x.example.aaa."}}`, + }, + {"z", + `{"cname":{"ttl":300, "host":"y.example.aaa."}}`, + }, + {"w", + `{ + "a":{"ttl":300, "records":[{"ip":"1.1.1.1"}]}, + "aaaa":{"ttl":300, "records":[{"ip":"::2"}]}, + "txt":{"ttl":300, "records":[{"text":"www"},{"text":"qqq"}]}, + "mx":{"ttl":300, "records":[{"host":"mx3.example.aaa.", "preference":10},{"host":"mx4.example.aaa.", "preference":10}]}, + "srv":{"ttl":300, "records":[{"target":"sip2.example.aaa.","port":555,"priority":10,"weight":100}]}, + "ns":{"ttl":300, "records":[{"host":"ns1.example.aaa."},{"ttl":300, "host":"ns2.example.aaa."}]}, + "cname":{"ttl":300, "host":"x.example.aaa."} + }`, + }, }, }, - // empty AAAA test - { - Qname: "z.example.bbb.", Qtype: dns.TypeAAAA, - Ns: []dns.RR{ - test.SOA("example.bbb. 300 IN SOA ns1.example.bbb. hostmaster.example.bbb. 1460498836 44 55 66 100"), + TestCases: []test.Case{ + { + Qname: "y.example.aaa.", Qtype: dns.TypeCNAME, + Answer: []dns.RR{ + test.CNAME("y.example.aaa. 300 IN CNAME x.example.aaa."), + }, }, - }, - // empty TXT test - { - Qname: "z.example.bbb.", Qtype: dns.TypeTXT, - Ns: []dns.RR{ - test.SOA("example.bbb. 300 IN SOA ns1.example.bbb. hostmaster.example.bbb. 1460498836 44 55 66 100"), + { + Qname: "z.example.aaa.", Qtype: dns.TypeCNAME, + Answer: []dns.RR{ + test.CNAME("z.example.aaa. 300 IN CNAME y.example.aaa."), + }, }, - }, - // empty NS test - { - Qname: "z.example.bbb.", Qtype: dns.TypeNS, - Ns: []dns.RR{ - test.SOA("example.bbb. 300 IN SOA ns1.example.bbb. hostmaster.example.bbb. 1460498836 44 55 66 100"), + { + Qname: "z.example.aaa.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("x.example.aaa. 300 IN A 1.2.3.4"), + test.CNAME("y.example.aaa. 300 IN CNAME x.example.aaa."), + test.CNAME("z.example.aaa. 300 IN CNAME y.example.aaa."), + }, }, - }, - // empty MX test - { - Qname: "z.example.bbb.", Qtype: dns.TypeMX, - Ns: []dns.RR{ - test.SOA("example.bbb. 300 IN SOA ns1.example.bbb. hostmaster.example.bbb. 1460498836 44 55 66 100"), + { + Qname: "z.example.aaa.", Qtype: dns.TypeAAAA, + Answer: []dns.RR{ + test.AAAA("x.example.aaa. 300 IN AAAA ::1"), + test.CNAME("y.example.aaa. 300 IN CNAME x.example.aaa."), + test.CNAME("z.example.aaa. 300 IN CNAME y.example.aaa."), + }, }, - }, - // empty SRV test - { - Qname: "z.example.bbb.", Qtype: dns.TypeSRV, - Ns: []dns.RR{ - test.SOA("example.bbb. 300 IN SOA ns1.example.bbb. hostmaster.example.bbb. 1460498836 44 55 66 100"), + { + Qname: "z.example.aaa.", Qtype: dns.TypeTXT, + Answer: []dns.RR{ + test.TXT("x.example.aaa. 300 IN TXT bar"), + test.TXT("x.example.aaa. 300 IN TXT foo"), + test.CNAME("y.example.aaa. 300 IN CNAME x.example.aaa."), + test.CNAME("z.example.aaa. 300 IN CNAME y.example.aaa."), + }, }, - }, - // empty CNAME test - { - Qname: "x.example.bbb.", Qtype: dns.TypeCNAME, - Ns: []dns.RR{ - test.SOA("example.bbb. 300 IN SOA ns1.example.bbb. hostmaster.example.bbb. 1460498836 44 55 66 100"), + { + Qname: "z.example.aaa.", Qtype: dns.TypeNS, + Answer: []dns.RR{ + test.CNAME("y.example.aaa. 300 IN CNAME x.example.aaa."), + test.CNAME("z.example.aaa. 300 IN CNAME y.example.aaa."), + }, + Ns: []dns.RR{ + test.SOA("example.aaa. 300 IN SOA ns1.example.aaa. hostmaster.example.aaa. 1460498836 44 55 66 100"), + }, }, - }, - // empty A test with cname - { - Qname: "y.example.bbb.", Qtype: dns.TypeA, - Answer: []dns.RR{ - test.A("x.example.bbb. 300 IN A 1.2.3.4"), - test.CNAME("y.example.bbb. 300 IN CNAME x.example.bbb."), + { + Qname: "z.example.aaa.", Qtype: dns.TypeMX, + Answer: []dns.RR{ + test.MX("x.example.aaa. 300 IN MX 10 mx1.example.aaa."), + test.MX("x.example.aaa. 300 IN MX 10 mx2.example.aaa."), + test.CNAME("y.example.aaa. 300 IN CNAME x.example.aaa."), + test.CNAME("z.example.aaa. 300 IN CNAME y.example.aaa."), + }, }, - }, - // empty AAAA test with cname - { - Qname: "y.example.bbb.", Qtype: dns.TypeAAAA, - Answer: []dns.RR{ - test.CNAME("y.example.bbb. 300 IN CNAME x.example.bbb."), + { + Qname: "z.example.aaa.", Qtype: dns.TypeSRV, + Answer: []dns.RR{ + test.SRV("x.example.aaa. 300 IN SRV 10 100 555 sip.example.aaa."), + test.CNAME("y.example.aaa. 300 IN CNAME x.example.aaa."), + test.CNAME("z.example.aaa. 300 IN CNAME y.example.aaa."), + }, }, - }, - // empty TXT test with cname - { - Qname: "y.example.bbb.", Qtype: dns.TypeTXT, - Answer: []dns.RR{ - test.CNAME("y.example.bbb. 300 IN CNAME x.example.bbb."), + { + Qname: "w.example.aaa.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.CNAME("w.example.aaa. 300 IN CNAME x.example.aaa."), + test.A("x.example.aaa. 300 IN A 1.2.3.4"), + }, }, - }, - // empty NS test with cname - { - Qname: "y.example.bbb.", Qtype: dns.TypeNS, - Answer: []dns.RR{ - test.CNAME("y.example.bbb. 300 IN CNAME x.example.bbb."), + { + Qname: "w.example.aaa.", Qtype: dns.TypeAAAA, + Answer: []dns.RR{ + test.CNAME("w.example.aaa. 300 IN CNAME x.example.aaa."), + test.AAAA("x.example.aaa. 300 IN AAAA ::1"), + }, }, - }, - // empty MX test with cname - { - Qname: "y.example.bbb.", Qtype: dns.TypeMX, - Answer: []dns.RR{ - test.CNAME("y.example.bbb. 300 IN CNAME x.example.bbb."), + { + Qname: "w.example.aaa.", Qtype: dns.TypeTXT, + Answer: []dns.RR{ + test.CNAME("w.example.aaa. 300 IN CNAME x.example.aaa."), + test.TXT("x.example.aaa. 300 IN TXT bar"), + test.TXT("x.example.aaa. 300 IN TXT foo"), + }, }, - }, - // empty SRV test with cname - { - Qname: "y.example.bbb.", Qtype: dns.TypeSRV, - Answer: []dns.RR{ - test.CNAME("y.example.bbb. 300 IN CNAME x.example.bbb."), + { + Qname: "w.example.aaa.", Qtype: dns.TypeNS, + Answer: []dns.RR{ + test.CNAME("w.example.aaa. 300 IN CNAME x.example.aaa."), + }, + Ns: []dns.RR{ + test.SOA("example.aaa. 300 IN SOA ns1.example.aaa. hostmaster.example.aaa. 1460498836 44 55 66 100"), + }, + }, + { + Qname: "w.example.aaa.", Qtype: dns.TypeMX, + Answer: []dns.RR{ + test.CNAME("w.example.aaa. 300 IN CNAME x.example.aaa."), + test.MX("x.example.aaa. 300 IN MX 10 mx1.example.aaa."), + test.MX("x.example.aaa. 300 IN MX 10 mx2.example.aaa."), + }, + }, + { + Qname: "w.example.aaa.", Qtype: dns.TypeSRV, + Answer: []dns.RR{ + test.CNAME("w.example.aaa. 300 IN CNAME x.example.aaa."), + test.SRV("x.example.aaa. 300 IN SRV 10 100 555 sip.example.aaa."), + }, }, }, }, - // long text { - { - Qname: "x.example.ccc.", Qtype: dns.TypeTXT, - Answer: []dns.RR{ - test.TXT("x.example.ccc. 300 IN TXT \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\""), + Name: "empty values", + Description: "test handler behaviour with empty records", + Enabled: true, + Config: defaultConfig, + Initialize: defaultInitialize, + ApplyAndVerify: defaultApplyAndVerify, + Zones: []string{"example.bbb."}, + ZoneConfigs: []string{`{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.example.bbb.","ns":"ns1.example.bbb.","refresh":44,"retry":55,"expire":66}}`}, + Entries: [][][]string{ + { + {"x", + `{}`, + }, + {"y", + `{"cname":{"ttl":300, "host":"x.example.bbb."}}`, + }, + {"z", + `{}`, + }, + }, + }, + TestCases: []test.Case{ + // empty A test + { + Qname: "z.example.bbb.", Qtype: dns.TypeA, + Ns: []dns.RR{ + test.SOA("example.bbb. 300 IN SOA ns1.example.bbb. hostmaster.example.bbb. 1460498836 44 55 66 100"), + }, + }, + // empty AAAA test + { + Qname: "z.example.bbb.", Qtype: dns.TypeAAAA, + Ns: []dns.RR{ + test.SOA("example.bbb. 300 IN SOA ns1.example.bbb. hostmaster.example.bbb. 1460498836 44 55 66 100"), + }, + }, + // empty TXT test + { + Qname: "z.example.bbb.", Qtype: dns.TypeTXT, + Ns: []dns.RR{ + test.SOA("example.bbb. 300 IN SOA ns1.example.bbb. hostmaster.example.bbb. 1460498836 44 55 66 100"), + }, + }, + // empty NS test + { + Qname: "z.example.bbb.", Qtype: dns.TypeNS, + Ns: []dns.RR{ + test.SOA("example.bbb. 300 IN SOA ns1.example.bbb. hostmaster.example.bbb. 1460498836 44 55 66 100"), + }, + }, + // empty MX test + { + Qname: "z.example.bbb.", Qtype: dns.TypeMX, + Ns: []dns.RR{ + test.SOA("example.bbb. 300 IN SOA ns1.example.bbb. hostmaster.example.bbb. 1460498836 44 55 66 100"), + }, + }, + // empty SRV test + { + Qname: "z.example.bbb.", Qtype: dns.TypeSRV, + Ns: []dns.RR{ + test.SOA("example.bbb. 300 IN SOA ns1.example.bbb. hostmaster.example.bbb. 1460498836 44 55 66 100"), + }, + }, + // empty CNAME test + { + Qname: "x.example.bbb.", Qtype: dns.TypeCNAME, + Ns: []dns.RR{ + test.SOA("example.bbb. 300 IN SOA ns1.example.bbb. hostmaster.example.bbb. 1460498836 44 55 66 100"), + }, + }, + // empty A test with cname + { + Qname: "y.example.bbb.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.CNAME("y.example.bbb. 300 IN CNAME x.example.bbb."), + }, + Ns: []dns.RR{ + test.SOA("example.bbb. 300 IN SOA ns1.example.bbb. hostmaster.example.bbb. 1460498836 44 55 66 100"), + }, + }, + // empty AAAA test with cname + { + Qname: "y.example.bbb.", Qtype: dns.TypeAAAA, + Answer: []dns.RR{ + test.CNAME("y.example.bbb. 300 IN CNAME x.example.bbb."), + }, + Ns: []dns.RR{ + test.SOA("example.bbb. 300 IN SOA ns1.example.bbb. hostmaster.example.bbb. 1460498836 44 55 66 100"), + }, + }, + // empty TXT test with cname + { + Qname: "y.example.bbb.", Qtype: dns.TypeTXT, + Answer: []dns.RR{ + test.CNAME("y.example.bbb. 300 IN CNAME x.example.bbb."), + }, + Ns: []dns.RR{ + test.SOA("example.bbb. 300 IN SOA ns1.example.bbb. hostmaster.example.bbb. 1460498836 44 55 66 100"), + }, + }, + // empty NS test with cname + { + Qname: "y.example.bbb.", Qtype: dns.TypeNS, + Answer: []dns.RR{ + test.CNAME("y.example.bbb. 300 IN CNAME x.example.bbb."), + }, + Ns: []dns.RR{ + test.SOA("example.bbb. 300 IN SOA ns1.example.bbb. hostmaster.example.bbb. 1460498836 44 55 66 100"), + }, + }, + // empty MX test with cname + { + Qname: "y.example.bbb.", Qtype: dns.TypeMX, + Answer: []dns.RR{ + test.CNAME("y.example.bbb. 300 IN CNAME x.example.bbb."), + }, + Ns: []dns.RR{ + test.SOA("example.bbb. 300 IN SOA ns1.example.bbb. hostmaster.example.bbb. 1460498836 44 55 66 100"), + }, + }, + // empty SRV test with cname + { + Qname: "y.example.bbb.", Qtype: dns.TypeSRV, + Answer: []dns.RR{ + test.CNAME("y.example.bbb. 300 IN CNAME x.example.bbb."), + }, + Ns: []dns.RR{ + test.SOA("example.bbb. 300 IN SOA ns1.example.bbb. hostmaster.example.bbb. 1460498836 44 55 66 100"), + }, }, }, }, - // CNAME flattening { - { - Qname: "e.example.ddd.", Qtype: dns.TypeA, - Answer: []dns.RR{ - test.A("e.example.ddd. 300 IN A 1.2.3.4"), + Name: "long text", + Description: "text field longer than 255 bytes", + Enabled: true, + Config: defaultConfig, + Initialize: defaultInitialize, + ApplyAndVerify: defaultApplyAndVerify, + Zones: []string{"example.ccc."}, + ZoneConfigs: []string{`{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.example.ccc.","ns":"ns1.example.ccc.","refresh":44,"retry":55,"expire":66}}`}, + Entries: [][][]string{ + { + {"x", + `{"txt":{"ttl":300, "records":[{"text":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}]}}`, + }, }, }, - { - Qname: "e.example.ddd.", Qtype: dns.TypeAAAA, - Answer: []dns.RR{ - test.AAAA("e.example.ddd. 300 IN AAAA ::1"), + TestCases: []test.Case{ + { + Qname: "x.example.ccc.", Qtype: dns.TypeTXT, + Answer: []dns.RR{ + test.TXT("x.example.ccc. 300 IN TXT \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\""), + }, }, }, - { - Qname: "e.example.ddd.", Qtype: dns.TypeTXT, - Answer: []dns.RR{ - test.TXT("e.example.ddd. 300 IN TXT \"bar\""), - test.TXT("e.example.ddd. 300 IN TXT \"foo\""), + }, + { + Name: "cname flattening", + Description: "eliminate intermediate cname records when cname flatenning is enabled", + Enabled: true, + Config: defaultConfig, + Initialize: defaultInitialize, + ApplyAndVerify: defaultApplyAndVerify, + Zones: []string{"example.ddd."}, + ZoneConfigs: []string{`{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.example.ddd.","ns":"ns1.example.ddd.","refresh":44,"retry":55,"expire":66},"cname_flattening":true}`}, + Entries: [][][]string{ + { + {"@", + `{"ns":{"ttl":300, "records":[{"host":"ns1.example.ddd."},{"ttl":300, "host":"ns2.example.ddd."}]}}`, + }, + {"a", + `{ + "a":{"ttl":300, "records":[{"ip":"1.2.3.4"}]}, + "aaaa":{"ttl":300, "records":[{"ip":"::1"}]}, + "txt":{"ttl":300, "records":[{"text":"foo"},{"text":"bar"}]}, + "mx":{"ttl":300, "records":[{"host":"mx1.example.ddd.", "preference":10},{"host":"mx2.example.ddd.", "preference":10}]}, + "srv":{"ttl":300, "records":[{"target":"sip.example.ddd.","port":555,"priority":10,"weight":100}]} + }`, + }, + {"b", + `{"cname":{"ttl":300, "host":"a.example.ddd."}}`, + }, + {"c", + `{"cname":{"ttl":300, "host":"b.example.ddd."}}`, + }, + {"d", + `{"cname":{"ttl":300, "host":"c.example.ddd."}}`, + }, + {"e", + `{"cname":{"ttl":300, "host":"d.example.ddd."}}`, + }, }, }, - { - Qname: "e.example.ddd.", Qtype: dns.TypeNS, - Answer: []dns.RR{ - test.NS("e.example.ddd. 300 IN NS ns1.example.ddd."), - test.NS("e.example.ddd. 300 IN NS ns2.example.ddd."), + TestCases: []test.Case{ + { + Qname: "e.example.ddd.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("e.example.ddd. 300 IN A 1.2.3.4"), + }, }, - }, - // MX Test - { - Qname: "e.example.ddd.", Qtype: dns.TypeMX, - Answer: []dns.RR{ - test.MX("e.example.ddd. 300 IN MX 10 mx1.example.ddd."), - test.MX("e.example.ddd. 300 IN MX 10 mx2.example.ddd."), + { + Qname: "e.example.ddd.", Qtype: dns.TypeAAAA, + Answer: []dns.RR{ + test.AAAA("e.example.ddd. 300 IN AAAA ::1"), + }, }, - }, - // SRV Test - { - Qname: "e.example.ddd.", Qtype: dns.TypeSRV, - Answer: []dns.RR{ - test.SRV("e.example.ddd. 300 IN SRV 10 100 555 sip.example.ddd."), + { + Qname: "e.example.ddd.", Qtype: dns.TypeTXT, + Answer: []dns.RR{ + test.TXT("e.example.ddd. 300 IN TXT \"bar\""), + test.TXT("e.example.ddd. 300 IN TXT \"foo\""), + }, }, - }, - { - Qname: "e.example.ddd.", Qtype: dns.TypeCNAME, - Answer: []dns.RR{ - test.CNAME("e.example.ddd. 300 IN CNAME d.example.ddd."), + // MX Test + { + Qname: "e.example.ddd.", Qtype: dns.TypeMX, + Answer: []dns.RR{ + test.MX("e.example.ddd. 300 IN MX 10 mx1.example.ddd."), + test.MX("e.example.ddd. 300 IN MX 10 mx2.example.ddd."), + }, + }, + // SRV Test + { + Qname: "e.example.ddd.", Qtype: dns.TypeSRV, + Answer: []dns.RR{ + test.SRV("e.example.ddd. 300 IN SRV 10 100 555 sip.example.ddd."), + }, + }, + { + Qname: "e.example.ddd.", Qtype: dns.TypeCNAME, + Answer: []dns.RR{ + test.CNAME("e.example.ddd. 300 IN CNAME d.example.ddd."), + }, }, }, }, - // CAA Test { - { - Qname: "example.caa.", Qtype: dns.TypeCAA, - Answer: []dns.RR{ - test.CAA("example.caa. 300 IN CAA 0 issue \"godaddy.com;\""), + Name: "caa test", + Description: "basic caa functionality", + Enabled: true, + Config: defaultConfig, + Initialize: defaultInitialize, + ApplyAndVerify: defaultApplyAndVerify, + Zones: []string{"example.caa.", "nocaa.caa."}, + ZoneConfigs: []string{ + `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.example.caa.","ns":"ns1.example.caa.","refresh":44,"retry":55,"expire":66}}`, + `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.nocaa.caa.","ns":"ns1.nocaa.caa.","refresh":44,"retry":55,"expire":66}}`, + }, + Entries: [][][]string{ + { + {"@", + `{"caa":{"ttl":300, "records":[{"tag":"issue", "value":"godaddy.com;", "flag":0}]}}`, + }, + {"a.b.c.d", + `{"cname":{"ttl":300, "host":"b.c.d.example.caa."}}`, + }, + {"b.c.d", + `{"cname":{"ttl":300, "host":"c.d.example.caa."}}`, + }, + {"c.d", + `{"cname":{"ttl":300, "host":"d.example.caa."}}`, + }, + {"d", + `{"cname":{"ttl":300, "host":"example.caa."}}`, + }, + {"x.y.z", + `{"a":{"ttl":300, "records":[{"ip":"1.2.3.4"}]}}`, + }, + {"y.z", + `{"a":{"ttl":300, "records":[{"ip":"1.2.3.4"}]}}`, + }, + {"z", + `{"a":{"ttl":300, "records":[{"ip":"1.2.3.4"}]}}`, + }, + {"h", + `{"caa":{"ttl":300, "records":[{"tag":"issue", "value":"godaddy2.com;", "flag":0}]}}`, + }, + {"g.h", + `{"a":{"ttl":300, "records":[{"ip":"1.2.3.4"}]}}`, + }, + {"j.g.h", + `{"a":{"ttl":300, "records":[{"ip":"1.2.3.4"}]}}`, + }, }, - }, - { - Qname: "a.b.c.d.example.caa.", Qtype: dns.TypeCAA, - Answer: []dns.RR{ - test.CNAME("a.b.c.d.example.caa. 300 IN CNAME b.c.d.example.caa."), - test.CNAME("b.c.d.example.caa. 300 IN CNAME c.d.example.caa."), - test.CNAME("c.d.example.caa. 300 IN CNAME d.example.caa."), - test.CNAME("d.example.caa. 300 IN CNAME example.caa."), - test.CAA("example.caa. 300 IN CAA 0 issue \"godaddy.com;\""), + { + {"@", + `{"a":{"ttl":300, "records":[{"ip":"1.2.3.4"}]}}`, + }, + {"www", + `{"a":{"ttl":300, "records":[{"ip":"1.2.3.4"}]}}`, + }, + {"www2", + `{"a":{"ttl":300, "records":[{"ip":"1.2.3.4"}]}}`, + }, + {"www3", + `{"a":{"ttl":300, "records":[{"ip":"1.2.3.4"}]}}`, + }, }, }, - { - Qname: "x.y.z.example.caa.", Qtype: dns.TypeCAA, - Answer: []dns.RR{ - test.CAA("x.y.z.example.caa. 300 IN CAA 0 issue \"godaddy.com;\""), + TestCases: []test.Case{ + { + Qname: "example.caa.", Qtype: dns.TypeCAA, + Answer: []dns.RR{ + test.CAA("example.caa. 300 IN CAA 0 issue \"godaddy.com;\""), + }, + }, + { + Qname: "a.b.c.d.example.caa.", Qtype: dns.TypeCAA, + Answer: []dns.RR{ + test.CNAME("a.b.c.d.example.caa. 300 IN CNAME b.c.d.example.caa."), + test.CNAME("b.c.d.example.caa. 300 IN CNAME c.d.example.caa."), + test.CNAME("c.d.example.caa. 300 IN CNAME d.example.caa."), + test.CNAME("d.example.caa. 300 IN CNAME example.caa."), + test.CAA("example.caa. 300 IN CAA 0 issue \"godaddy.com;\""), + }, + }, + { + Qname: "x.y.z.example.caa.", Qtype: dns.TypeCAA, + Answer: []dns.RR{ + test.CAA("x.y.z.example.caa. 300 IN CAA 0 issue \"godaddy.com;\""), + }, + }, + { + Qname: "h.example.caa.", Qtype: dns.TypeCAA, + Answer: []dns.RR{ + test.CAA("h.example.caa. 300 IN CAA 0 issue \"godaddy2.com;\""), + }, + }, + { + Qname: "g.h.example.caa.", Qtype: dns.TypeCAA, + Answer: []dns.RR{ + test.CAA("g.h.example.caa. 300 IN CAA 0 issue \"godaddy2.com;\""), + }, + }, + { + Qname: "j.g.h.example.caa.", Qtype: dns.TypeCAA, + Answer: []dns.RR{ + test.CAA("j.g.h.example.caa. 300 IN CAA 0 issue \"godaddy2.com;\""), + }, + }, + { + Qname: "nocaa.caa.", Qtype: dns.TypeCAA, + Ns: []dns.RR{ + test.SOA("nocaa.caa. 300 IN SOA ns1.nocaa.caa. hostmaster.nocaa.caa. 1570970363 44 55 66 100"), + }, + }, + { + Qname: "www.nocaa.caa.", Qtype: dns.TypeCAA, + Ns: []dns.RR{ + test.SOA("nocaa.caa. 300 IN SOA ns1.nocaa.caa. hostmaster.nocaa.caa. 1570970363 44 55 66 100"), + }, + }, + { + Qname: "www2.nocaa.caa.", Qtype: dns.TypeCAA, + Ns: []dns.RR{ + test.SOA("nocaa.caa. 300 IN SOA ns1.nocaa.caa. hostmaster.nocaa.caa. 1570970363 44 55 66 100"), + }, + }, + { + Qname: "www3.nocaa.caa.", Qtype: dns.TypeCAA, + Ns: []dns.RR{ + test.SOA("nocaa.caa. 300 IN SOA ns1.nocaa.caa. hostmaster.nocaa.caa. 1570970363 44 55 66 100"), + }, }, }, }, - // PTR Test { - { - Qname: "1.0.0.127.in-addr.arpa.", Qtype: dns.TypePTR, - Answer: []dns.RR{ - test.PTR("1.0.0.127.in-addr.arpa. 300 IN PTR localhost."), + Name: "PTR test", + Description: "basic ptr functionality", + Enabled: true, + Config: defaultConfig, + Initialize: defaultInitialize, + ApplyAndVerify: defaultApplyAndVerify, + Zones: []string{"0.0.127.in-addr.arpa.", "20.127.10.in-addr.arpa."}, + ZoneConfigs: []string{"", ""}, + Entries: [][][]string{ + { + {"1", + `{"ptr":{"ttl":300, "domain":"localhost"}}`, + }, + }, + { + {"54", + `{"ptr":{"ttl":300, "domain":"example.fff"}}`, + }, + }, + }, + TestCases: []test.Case{ + { + Qname: "1.0.0.127.in-addr.arpa.", Qtype: dns.TypePTR, + Answer: []dns.RR{ + test.PTR("1.0.0.127.in-addr.arpa. 300 IN PTR localhost."), + }, + }, + { + Qname: "54.20.127.10.in-addr.arpa.", Qtype: dns.TypePTR, + Answer: []dns.RR{ + test.PTR("54.20.127.10.in-addr.arpa. 300 IN PTR example.fff."), + }, }, }, }, { - { - Qname: "54.20.127.10.in-addr.arpa.", Qtype: dns.TypePTR, - Answer: []dns.RR{ - test.PTR("54.20.127.10.in-addr.arpa. 300 IN PTR example.fff."), + Name: "ANAME test", + Description: "test aname functionality", + Enabled: true, + Config: defaultConfig, + Initialize: defaultInitialize, + ApplyAndVerify: defaultApplyAndVerify, + Zones: []string{"arvancloud.com.", "arvan.an."}, + ZoneConfigs: []string{ + `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.arvancloud.com.","ns":"ns1.arvancloud.com.","refresh":44,"retry":55,"expire":66}}`, + `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.arvan.an.","ns":"ns1.arvan.an.","refresh":44,"retry":55,"expire":66}}`, + }, + Entries: [][][]string{ + { + {"@", + `{"aname":{"location":"aname.arvan.an."}}`, + }, + {"nxlocal", + `{"aname":{"location":"nx.arvancloud.com."}}`, + }, + {"empty", + `{"aname":{"location":"e.arvancloud.com."}}`, + }, + {"e", + `{"txt":{"ttl":300, "records":[{"text":"foo"}]}}`, + }, + {"upstream", + `{"aname":{"location":"dns.msftncsi.com."}}`, + }, + {"nxupstream", + `{"aname":{"location":"anamex.arvan.an."}}`, + }, + }, + { + {"aname", + `{"a":{"ttl":300, "records":[{"ip":"6.5.6.5"}]}, "aaaa":{"ttl":300, "records":[{"ip":"::1"}]}}`, + }, }, }, - }, -} - -var handlerTestConfig = HandlerConfig{ - MaxTtl: 300, - CacheTimeout: 60, - ZoneReload: 600, - Redis: uperdis.RedisConfig{ - Ip: "redis", - Port: 6379, - DB: 0, - Password: "", - Prefix: "test_", - Suffix: "_test", - ConnectTimeout: 0, - ReadTimeout: 0, - }, - Log: logger.LogConfig{ - Enable: false, - }, - Upstream: []UpstreamConfig{ - { - Ip: "1.1.1.1", - Port: 53, - Protocol: "udp", - Timeout: 1000, + TestCases: []test.Case{ + { + Qname: "arvancloud.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("arvancloud.com. 300 IN A 6.5.6.5"), + }, + }, + { + Qname: "arvancloud.com.", Qtype: dns.TypeAAAA, + Answer: []dns.RR{ + test.AAAA("arvancloud.com. 300 IN AAAA ::1"), + }, + }, + { + Qname: "upstream.arvancloud.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("upstream.arvancloud.com. 303 IN A 131.107.255.255"), + }, + }, + { + Qname: "upstream.arvancloud.com.", Qtype: dns.TypeAAAA, + Answer: []dns.RR{ + test.AAAA("upstream.arvancloud.com. 303 IN AAAA fd3e:4f5a:5b81::1"), + }, + }, + { + Qname: "nxlocal.arvancloud.com.", Qtype: dns.TypeA, + Rcode: dns.RcodeServerFailure, + }, + { + Qname: "nxlocal.arvancloud.com.", Qtype: dns.TypeAAAA, + Rcode: dns.RcodeServerFailure, + }, + { + Qname: "empty.arvancloud.com.", Qtype: dns.TypeA, + Ns: []dns.RR{ + test.SOA("arvancloud.com. 300 IN SOA ns1.arvancloud.com. hostmaster.arvancloud.com. 1570970363 44 55 66 100"), + }, + }, + { + Qname: "empty.arvancloud.com.", Qtype: dns.TypeAAAA, + Ns: []dns.RR{ + test.SOA("arvancloud.com. 300 IN SOA ns1.arvancloud.com. hostmaster.arvancloud.com. 1570970363 44 55 66 100"), + }, + }, + { + Qname: "nxupstream.arvancloud.com.", Qtype: dns.TypeA, + Rcode: dns.RcodeServerFailure, + }, + { + Qname: "nxupstream.arvancloud.com.", Qtype: dns.TypeAAAA, + Rcode: dns.RcodeServerFailure, + }, }, }, - GeoIp: GeoIpConfig{ - Enable: true, - CountryDB: "../geoCity.mmdb", - ASNDB: "../geoIsp.mmdb", - }, -} - -func TestLookup(t *testing.T) { - logger.Default = logger.NewLogger(&logger.LogConfig{}) - - h := NewHandler(&handlerTestConfig) - h.Redis.Del("*") - for i, zone := range lookupZones { - h.Redis.SAdd("redins:zones", zone) - for _, cmd := range lookupEntries[i] { - err := h.Redis.HSet("redins:zones:"+zone, cmd[0], cmd[1]) - if err != nil { - log.Printf("[ERROR] cannot connect to redis: %s", err) + { + Name: "weighted aname test", + Description: "weight filter should be applied on aname results as well", + Enabled: true, + Config: defaultConfig, + Initialize: defaultInitialize, + ApplyAndVerify: func(testCase *TestCase, handler *DnsRequestHandler, t *testing.T) { + ipsCount := []int{0, 0, 0} + for i := 0; i < 1000; i++ { + r := testCase.TestCases[0].Msg() + w := test.NewRecorder(&test.ResponseWriter{}) + state := NewRequestContext(w, r) + handler.HandleRequest(state) + + resp := w.Msg + if resp.Rcode != dns.RcodeSuccess { + fmt.Println("RcodeSuccess expected ", dns.RcodeToString[resp.Rcode], " received") + t.Fail() + } + if len(resp.Answer) == 0 { + fmt.Println("empty answer") + t.Fail() + } + a := resp.Answer[0].(*dns.A) + switch a.A.String() { + case "1.1.1.1": + ipsCount[0]++ + case "2.2.2.2": + ipsCount[1]++ + case "3.3.3.3": + ipsCount[2]++ + default: + fmt.Println("invalid ip : ", a.A.String()) + t.Fail() + } + } + if !(ipsCount[0] < ipsCount[1] && ipsCount[1] < ipsCount[2]) { + fmt.Println("bad ip weight balance") t.Fail() } - } - h.Redis.Set("redins:zones:"+zone+":config", lookupConfig[i]) - h.LoadZones() - for j, tc := range lookupTestCases[i] { - - r := tc.Msg() - w := test.NewRecorder(&test.ResponseWriter{}) - state := request.Request{W: w, Req: r} - h.HandleRequest(&state) - - resp := w.Msg - - if err := test.SortAndCheck(resp, tc); err != nil { - fmt.Println(i, j, err, tc.Qname, tc.Answer, resp.Answer) + ipsCount = []int{0, 0, 0} + for i := 0; i < 1000; i++ { + r := testCase.TestCases[1].Msg() + w := test.NewRecorder(&test.ResponseWriter{}) + state := NewRequestContext(w, r) + handler.HandleRequest(state) + + resp := w.Msg + if resp.Rcode != dns.RcodeSuccess { + fmt.Println("RcodeSuccess expected ", dns.RcodeToString[resp.Rcode], " received") + t.Fail() + } + if len(resp.Answer) == 0 { + fmt.Println("empty answer") + t.Fail() + } + aaaa := resp.Answer[0].(*dns.AAAA) + switch aaaa.AAAA.String() { + case "2001:db8::1": + ipsCount[0]++ + case "2001:db8::2": + ipsCount[1]++ + case "2001:db8::3": + ipsCount[2]++ + default: + fmt.Println("invalid ip : ", aaaa.AAAA.String()) + t.Fail() + } + } + if !(ipsCount[0] < ipsCount[1] && ipsCount[1] < ipsCount[2]) { + fmt.Println("bad ip weight balance") t.Fail() } - } - } - -} - -func TestWeight(t *testing.T) { - logger.Default = logger.NewLogger(&logger.LogConfig{}) - - // distribution - ips := []IP_RR{ - {Ip: net.ParseIP("1.2.3.4"), Weight: 4}, - {Ip: net.ParseIP("2.3.4.5"), Weight: 1}, - {Ip: net.ParseIP("3.4.5.6"), Weight: 5}, - {Ip: net.ParseIP("4.5.6.7"), Weight: 10}, - } - n := make([]int, 4) - for i := 0; i < 100000; i++ { - x := ChooseIp(ips, true) - switch ips[x].Ip.String() { - case "1.2.3.4": - n[0]++ - case "2.3.4.5": - n[1]++ - case "3.4.5.6": - n[2]++ - case "4.5.6.7": - n[3]++ - } - } - if n[0] > n[2] || n[2] > n[3] || n[1] > n[0] { - t.Fail() - } - - // all zero - for i := range ips { - ips[i].Weight = 0 - } - n[0], n[1], n[2], n[3] = 0, 0, 0, 0 - for i := 0; i < 100000; i++ { - x := ChooseIp(ips, true) - switch ips[x].Ip.String() { - case "1.2.3.4": - n[0]++ - case "2.3.4.5": - n[1]++ - case "3.4.5.6": - n[2]++ - case "4.5.6.7": - n[3]++ - } - } - for i := 0; i < 4; i++ { - if n[i] < 2000 && n[i] > 3000 { - t.Fail() - } - } - - // some zero - n[0], n[1], n[2], n[3] = 0, 0, 0, 0 - ips[0].Weight, ips[1].Weight, ips[2].Weight, ips[3].Weight = 0, 5, 7, 0 - for i := 0; i < 100000; i++ { - x := ChooseIp(ips, true) - switch ips[x].Ip.String() { - case "1.2.3.4": - n[0]++ - case "2.3.4.5": - n[1]++ - case "3.4.5.6": - n[2]++ - case "4.5.6.7": - n[3]++ - } - } - log.Println(n) - if n[0] > 0 || n[3] > 0 { - t.Fail() - } - - // weighted = false - n[0], n[1], n[2], n[3] = 0, 0, 0, 0 - ips[0].Weight, ips[1].Weight, ips[2].Weight, ips[3].Weight = 0, 5, 7, 0 - for i := 0; i < 100000; i++ { - x := ChooseIp(ips, false) - switch ips[x].Ip.String() { - case "1.2.3.4": - n[0]++ - case "2.3.4.5": - n[1]++ - case "3.4.5.6": - n[2]++ - case "4.5.6.7": - n[3]++ - } - } - log.Println(n) - for i := 0; i < 4; i++ { - if n[i] < 2000 && n[i] > 3000 { - t.Fail() - } - } -} - -var anameZones = []string{ - "arvancloud.com.", "arvan.an.", -} - -var anameConfig = []string{ - `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.arvancloud.com.","ns":"ns1.arvancloud.com.","refresh":44,"retry":55,"expire":66}}`, - `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.arvan.an.","ns":"ns1.arvan.an.","refresh":44,"retry":55,"expire":66}}`, -} - -var anameEntries = [][][]string{ - { - {"@", - `{"aname":{"location":"aname.arvan.an."}}`, }, - {"upstream", - `{"aname":{"location":"google.com."}}`, + Zones: []string{"arvancloud.com.", "arvan.an."}, + ZoneConfigs: []string{ + `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.arvancloud.com.","ns":"ns1.arvancloud.com.","refresh":44,"retry":55,"expire":66}}`, + `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.arvan.an.","ns":"ns1.arvan.an.","refresh":44,"retry":55,"expire":66}}`, + }, + Entries: [][][]string{ + { + {"upstream2", + `{"aname":{"location":"aname2.arvan.an."}}`, + }, + }, + { + {"aname2", + `{ + "a":{"ttl":300, "filter": {"count":"single", "order": "weighted", "geo_filter":"none"}, "records":[{"ip":"1.1.1.1", "weight":1},{"ip":"2.2.2.2", "weight":5},{"ip":"3.3.3.3", "weight":10}]}, + "aaaa":{"ttl":300, "filter": {"count":"single", "order": "weighted", "geo_filter":"none"}, "records":[{"ip":"2001:db8::1", "weight":1},{"ip":"2001:db8::2", "weight":5},{"ip":"2001:db8::3", "weight":10}]} + }`, + }, + }, }, - {"upstream2", - `{"aname":{"location":"aname2.arvan.an."}}`, + TestCases: []test.Case{ + { + Qname: "upstream2.arvancloud.com.", Qtype: dns.TypeA, + }, + { + Qname: "upstream2.arvancloud.com.", Qtype: dns.TypeAAAA, + }, }, }, { - {"aname", - `{"a":{"ttl":300, "records":[{"ip":"6.5.6.5"}]}, "aaaa":{"ttl":300, "records":[{"ip":"::1"}]}}`, + Name: "geofilter test", + Description: "test various geofilter scenarios", + Enabled: true, + Config: defaultConfig, + Initialize: defaultInitialize, + ApplyAndVerify: func(testCase *TestCase, handler *DnsRequestHandler, t *testing.T) { + var filterGeoSourceIps = []string{ + "127.0.0.1", + "127.0.0.1", + "127.0.0.1", + "127.0.0.1", + "127.0.0.1", + "94.76.229.204", // country = GB + "154.11.253.242", // location = CA near US + "212.83.32.45", // ASN = 47447 + "212.83.32.45", // country = DE, ASN = 47447 + "212.83.32.45", + "178.18.89.144", + "127.0.0.1", + "213.95.10.76", // DE + "94.76.229.204", // GB + "154.11.253.242", // CA + "127.0.0.1", + } + for i, tc := range testCase.TestCases { + sa := filterGeoSourceIps[i] + opt := &dns.OPT{ + Hdr: dns.RR_Header{Name: ".", Rrtype: dns.TypeOPT, Class: dns.ClassANY, Rdlength: 0, Ttl: 300}, + Option: []dns.EDNS0{ + &dns.EDNS0_SUBNET{ + Address: net.ParseIP(sa), + Code: dns.EDNS0SUBNET, + Family: 1, + SourceNetmask: 32, + SourceScope: 0, + }, + }, + } + r := tc.Msg() + r.Extra = append(r.Extra, opt) + w := test.NewRecorder(&test.ResponseWriter{}) + state := NewRequestContext(w, r) + handler.HandleRequest(state) + + resp := w.Msg + resp.Extra = nil + + if err := test.SortAndCheck(resp, tc); err != nil { + fmt.Println(err) + t.Fail() + } + } }, - {"aname2", - `{ - "a":{"ttl":300, "filter": {"count":"single", "order": "weighted", "geo_filter":"none"}, "records":[{"ip":"1.1.1.1", "weight":1},{"ip":"2.2.2.2", "weight":5},{"ip":"3.3.3.3", "weight":10}]}, - "aaaa":{"ttl":300, "filter": {"count":"single", "order": "weighted", "geo_filter":"none"}, "records":[{"ip":"2001:db8::1", "weight":1},{"ip":"2001:db8::2", "weight":5},{"ip":"2001:db8::3", "weight":10}]} - }`, + Zones: []string{"filtergeo.com."}, + ZoneConfigs: []string{`{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.filter.com.","ns":"ns1.filter.com.","refresh":44,"retry":55,"expire":66}}`}, + Entries: [][][]string{ + { + {"ww1", + `{ + "a":{"ttl":300, "records":[ + {"ip":"127.0.0.1", "country":""}, + {"ip":"127.0.0.2", "country":""}, + {"ip":"127.0.0.3", "country":""}, + {"ip":"127.0.0.4", "country":""}, + {"ip":"127.0.0.5", "country":""}, + {"ip":"127.0.0.6", "country":""} + ], + "filter":{"count":"multi","order":"none","geo_filter":"none"}} + }`, + }, + {"ww2", + `{ + "a":{"ttl":300, "records":[ + {"ip":"127.0.0.1", "country":"US"}, + {"ip":"127.0.0.2", "country":"GB"}, + {"ip":"127.0.0.3", "country":"ES"}, + {"ip":"127.0.0.4", "country":""}, + {"ip":"127.0.0.5", "country":""}, + {"ip":"127.0.0.6", "country":""} + ], + "filter":{"count":"multi","order":"none","geo_filter":"country"}} + }`, + }, + {"ww3", + `{ + "a":{"ttl":300, "records":[ + {"ip":"192.30.252.225"}, + {"ip":"94.76.229.204"}, + {"ip":"84.88.14.229"}, + {"ip":"192.168.0.1"} + ], + "filter":{"count":"multi","order":"none","geo_filter":"location"}} + }`, + }, + {"ww4", + `{ + "a":{"ttl":300, "records":[ + {"ip":"127.0.0.1", "asn":47447}, + {"ip":"127.0.0.2", "asn":20776}, + {"ip":"127.0.0.3", "asn":35470}, + {"ip":"127.0.0.4", "asn":0}, + {"ip":"127.0.0.5", "asn":0}, + {"ip":"127.0.0.6", "asn":0} + ], + "filter":{"count":"multi", "order":"none","geo_filter":"asn"}} + }`, + }, + {"ww5", + `{ + "a":{"ttl":300, "records":[ + {"ip":"127.0.0.1", "country":"DE", "asn":47447}, + {"ip":"127.0.0.2", "country":"DE", "asn":20776}, + {"ip":"127.0.0.3", "country":"DE", "asn":35470}, + {"ip":"127.0.0.4", "country":"GB", "asn":0}, + {"ip":"127.0.0.5", "country":"", "asn":0}, + {"ip":"127.0.0.6", "country":"", "asn":0} + ], + "filter":{"count":"multi", "order":"none","geo_filter":"asn+country"}} + }`, + }, + {"ww6", + `{ + "a":{"ttl":300, "records":[ + {"ip":"127.0.0.1", "asn":[47447,20776]}, + {"ip":"127.0.0.2", "asn":[0,35470]}, + {"ip":"127.0.0.3", "asn":35470}, + {"ip":"127.0.0.4", "asn":0}, + {"ip":"127.0.0.5", "asn":[]}, + {"ip":"127.0.0.6"} + ], + "filter":{"count":"multi", "order":"none","geo_filter":"asn"}} + }`, + }, + {"ww7", + `{ + "a":{"ttl":300, "records":[ + {"ip":"127.0.0.1", "country":["DE", "GB"]}, + {"ip":"127.0.0.2", "country":["", "DE"]}, + {"ip":"127.0.0.3", "country":"DE"}, + {"ip":"127.0.0.4", "country":"CA"}, + {"ip":"127.0.0.5", "country": ""}, + {"ip":"127.0.0.6", "country": []}, + {"ip":"127.0.0.7"} + ], + "filter":{"count":"multi", "order":"none","geo_filter":"country"}} + }`, + }, + }, }, - }, -} - -var anameTestCases = []test.Case{ - { - Qname: "arvancloud.com.", Qtype: dns.TypeA, - Answer: []dns.RR{ - test.A("arvancloud.com. 300 IN A 6.5.6.5"), + TestCases: []test.Case{ + { + Qname: "ww1.filtergeo.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("ww1.filtergeo.com. 300 IN A 127.0.0.1"), + test.A("ww1.filtergeo.com. 300 IN A 127.0.0.2"), + test.A("ww1.filtergeo.com. 300 IN A 127.0.0.3"), + test.A("ww1.filtergeo.com. 300 IN A 127.0.0.4"), + test.A("ww1.filtergeo.com. 300 IN A 127.0.0.5"), + test.A("ww1.filtergeo.com. 300 IN A 127.0.0.6"), + }, + }, + { + Qname: "ww2.filtergeo.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("ww2.filtergeo.com. 300 IN A 127.0.0.4"), + test.A("ww2.filtergeo.com. 300 IN A 127.0.0.5"), + test.A("ww2.filtergeo.com. 300 IN A 127.0.0.6"), + }, + }, + { + Qname: "ww3.filtergeo.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("ww3.filtergeo.com. 300 IN A 192.168.0.1"), + }, + }, + { + Qname: "ww4.filtergeo.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("ww4.filtergeo.com. 300 IN A 127.0.0.4"), + test.A("ww4.filtergeo.com. 300 IN A 127.0.0.5"), + test.A("ww4.filtergeo.com. 300 IN A 127.0.0.6"), + }, + }, + { + Qname: "ww5.filtergeo.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("ww5.filtergeo.com. 300 IN A 127.0.0.5"), + test.A("ww5.filtergeo.com. 300 IN A 127.0.0.6"), + }, + }, + { + Qname: "ww2.filtergeo.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("ww2.filtergeo.com. 300 IN A 127.0.0.2"), + }, + }, + { + Qname: "ww3.filtergeo.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("ww3.filtergeo.com. 300 IN A 192.30.252.225"), + }, + }, + { + Qname: "ww4.filtergeo.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("ww4.filtergeo.com. 300 IN A 127.0.0.1"), + }, + }, + { + Qname: "ww5.filtergeo.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("ww5.filtergeo.com. 300 IN A 127.0.0.1"), + }, + }, + { + Qname: "ww6.filtergeo.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("ww6.filtergeo.com. 300 IN A 127.0.0.1"), + }, + }, + { + Qname: "ww6.filtergeo.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("ww6.filtergeo.com. 300 IN A 127.0.0.2"), + test.A("ww6.filtergeo.com. 300 IN A 127.0.0.3"), + }, + }, + { + Qname: "ww6.filtergeo.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("ww6.filtergeo.com. 300 IN A 127.0.0.2"), + test.A("ww6.filtergeo.com. 300 IN A 127.0.0.4"), + test.A("ww6.filtergeo.com. 300 IN A 127.0.0.5"), + test.A("ww6.filtergeo.com. 300 IN A 127.0.0.6"), + }, + }, + { + Qname: "ww7.filtergeo.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("ww7.filtergeo.com. 300 IN A 127.0.0.1"), + test.A("ww7.filtergeo.com. 300 IN A 127.0.0.2"), + test.A("ww7.filtergeo.com. 300 IN A 127.0.0.3"), + }, + }, + { + Qname: "ww7.filtergeo.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("ww7.filtergeo.com. 300 IN A 127.0.0.1"), + }, + }, + { + Qname: "ww7.filtergeo.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("ww7.filtergeo.com. 300 IN A 127.0.0.4"), + }, + }, + { + Qname: "ww7.filtergeo.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("ww7.filtergeo.com. 300 IN A 127.0.0.2"), + test.A("ww7.filtergeo.com. 300 IN A 127.0.0.5"), + test.A("ww7.filtergeo.com. 300 IN A 127.0.0.6"), + test.A("ww7.filtergeo.com. 300 IN A 127.0.0.7"), + }, + }, }, }, { - Qname: "arvancloud.com.", Qtype: dns.TypeAAAA, - Answer: []dns.RR{ - test.AAAA("arvancloud.com. 300 IN AAAA ::1"), - }, - }, -} - -func TestANAME(t *testing.T) { - logger.Default = logger.NewLogger(&logger.LogConfig{}) - - h := NewHandler(&handlerTestConfig) - h.Redis.Del("*") - for i, zone := range anameZones { - h.Redis.SAdd("redins:zones", zone) - for _, cmd := range anameEntries[i] { - err := h.Redis.HSet("redins:zones:"+zone, cmd[0], cmd[1]) - if err != nil { - log.Printf("[ERROR] cannot connect to redis: %s", err) - t.Fail() + Name: "filter multi ip", + Description: "ip filter functionality for multiple value results", + Enabled: true, + Config: defaultConfig, + Initialize: defaultInitialize, + ApplyAndVerify: func(testCase *TestCase, handler *DnsRequestHandler, t *testing.T) { + for i := 0; i < 10; i++ { + tc := testCase.TestCases[0] + r := tc.Msg() + w := test.NewRecorder(&test.ResponseWriter{}) + state := NewRequestContext(w, r) + handler.HandleRequest(state) + + resp := w.Msg + + if err := test.SortAndCheck(resp, tc); err != nil { + fmt.Println(err) + t.Fail() + } } - } - h.Redis.Set("redins:zones:"+zone+":config", anameConfig[i]) - } - h.LoadZones() - for _, tc := range anameTestCases { - - r := tc.Msg() - w := test.NewRecorder(&test.ResponseWriter{}) - state := request.Request{W: w, Req: r} - h.HandleRequest(&state) - - resp := w.Msg - - if test.SortAndCheck(resp, tc) != nil { - t.Fail() - } - } -} - -func TestWeightedANAME(t *testing.T) { - logger.Default = logger.NewLogger(&logger.LogConfig{}) - - h := NewHandler(&handlerTestConfig) - h.Redis.Del("*") - for i, zone := range anameZones { - h.Redis.SAdd("redins:zones", zone) - for _, cmd := range anameEntries[i] { - err := h.Redis.HSet("redins:zones:"+zone, cmd[0], cmd[1]) - if err != nil { - log.Printf("[ERROR] cannot connect to redis: %s", err) + w1, w4, w10, w2, w20 := 0, 0, 0, 0, 0 + for i := 0; i < 10000; i++ { + tc := testCase.TestCases[1] + r := tc.Msg() + w := test.NewRecorder(&test.ResponseWriter{}) + state := NewRequestContext(w, r) + handler.HandleRequest(state) + + resp := w.Msg + if len(resp.Answer) != 5 { + fmt.Println("expected 5 results ", len(resp.Answer), " received") + t.Fail() + } + + resa := resp.Answer[0].(*dns.A) + + switch resa.A.String() { + case "127.0.0.1": + w1++ + case "127.0.0.2": + w4++ + case "127.0.0.3": + w10++ + case "127.0.0.4": + w2++ + case "127.0.0.5": + w20++ + } + } + // fmtPrintln(w1, w2, w4, w10, w20) + if w1 > w2 || w2 > w4 || w4 > w10 || w10 > w20 { + fmt.Println("bad ip weight balance") t.Fail() } - } - h.Redis.Set("redins:zones:"+zone+":config", anameConfig[i]) - } - h.LoadZones() - - tc := test.Case{ - Qname: "upstream2.arvancloud.com.", Qtype: dns.TypeA, - } - tc2 := test.Case{ - Qname: "upstream2.arvancloud.com.", Qtype: dns.TypeAAAA, - } - ip1 := 0 - ip2 := 0 - ip3 := 0 - for i := 0; i < 1000; i++ { - r := tc.Msg() - w := test.NewRecorder(&test.ResponseWriter{}) - state := request.Request{W: w, Req: r} - h.HandleRequest(&state) - resp := w.Msg - if resp.Rcode != dns.RcodeSuccess { - t.Fail() - } - if len(resp.Answer) == 0 { - t.Fail() - } - a := resp.Answer[0].(*dns.A) - switch a.A.String() { - case "1.1.1.1": - ip1++ - case "2.2.2.2": - ip2++ - case "3.3.3.3": - ip3++ - default: - t.Fail() - } - } - if !(ip1 < ip2 && ip2 < ip3) { - t.Fail() - } - ip61 := 0 - ip62 := 0 - ip63 := 0 - for i := 0; i < 1000; i++ { - r := tc2.Msg() - w := test.NewRecorder(&test.ResponseWriter{}) - state := request.Request{W: w, Req: r} - h.HandleRequest(&state) - - resp := w.Msg - if resp.Rcode != dns.RcodeSuccess { - t.Fail() - } - if len(resp.Answer) == 0 { - t.Fail() - } - aaaa := resp.Answer[0].(*dns.AAAA) - switch aaaa.AAAA.String() { - case "2001:db8::1": - ip61++ - case "2001:db8::2": - ip62++ - case "2001:db8::3": - ip63++ - default: - t.Fail() - } - } - if !(ip61 < ip62 && ip62 < ip63) { - t.Fail() - } -} - -var filterGeoZone = "filtergeo.com." - -var filterGeoConfig = `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.filter.com.","ns":"ns1.filter.com.","refresh":44,"retry":55,"expire":66}}` - -var filterGeoEntries = [][]string{ - {"ww1", - `{"a":{"ttl":300, "records":[ - {"ip":"127.0.0.1", "country":""}, - {"ip":"127.0.0.2", "country":""}, - {"ip":"127.0.0.3", "country":""}, - {"ip":"127.0.0.4", "country":""}, - {"ip":"127.0.0.5", "country":""}, - {"ip":"127.0.0.6", "country":""}], - "filter":{"count":"multi","order":"none","geo_filter":"none"}}}`, - }, - {"ww2", - `{"a":{"ttl":300, "records":[ - {"ip":"127.0.0.1", "country":"US"}, - {"ip":"127.0.0.2", "country":"GB"}, - {"ip":"127.0.0.3", "country":"ES"}, - {"ip":"127.0.0.4", "country":""}, - {"ip":"127.0.0.5", "country":""}, - {"ip":"127.0.0.6", "country":""}], - "filter":{"count":"multi","order":"none","geo_filter":"country"}}}`, - }, - {"ww3", - `{"a":{"ttl":300, "records":[ - {"ip":"192.30.252.225"}, - {"ip":"94.76.229.204"}, - {"ip":"84.88.14.229"}, - {"ip":"192.168.0.1"}], - "filter":{"count":"multi","order":"none","geo_filter":"location"}}}`, - }, - {"ww4", - `{"a":{"ttl":300, "records":[ - {"ip":"127.0.0.1", "asn":47447}, - {"ip":"127.0.0.2", "asn":20776}, - {"ip":"127.0.0.3", "asn":35470}, - {"ip":"127.0.0.4", "asn":0}, - {"ip":"127.0.0.5", "asn":0}, - {"ip":"127.0.0.6", "asn":0}], - "filter":{"count":"multi", "order":"none","geo_filter":"asn"}}}`, - }, - {"ww5", - `{"a":{"ttl":300, "records":[ - {"ip":"127.0.0.1", "country":"DE", "asn":47447}, - {"ip":"127.0.0.2", "country":"DE", "asn":20776}, - {"ip":"127.0.0.3", "country":"DE", "asn":35470}, - {"ip":"127.0.0.4", "country":"GB", "asn":0}, - {"ip":"127.0.0.5", "country":"", "asn":0}, - {"ip":"127.0.0.6", "country":"", "asn":0}], - "filter":{"count":"multi", "order":"none","geo_filter":"asn+country"}}}`, - }, - {"ww6", - `{"a":{"ttl":300, "records":[ - {"ip":"127.0.0.1", "asn":[47447,20776]}, - {"ip":"127.0.0.2", "asn":[0,35470]}, - {"ip":"127.0.0.3", "asn":35470}, - {"ip":"127.0.0.4", "asn":0}, - {"ip":"127.0.0.5", "asn":[]}, - {"ip":"127.0.0.6"}], - "filter":{"count":"multi", "order":"none","geo_filter":"asn"}}}`, - }, - {"ww7", - `{"a":{"ttl":300, "records":[ - {"ip":"127.0.0.1", "country":["DE", "GB"]}, - {"ip":"127.0.0.2", "country":["", "DE"]}, - {"ip":"127.0.0.3", "country":"DE"}, - {"ip":"127.0.0.4", "country":"CA"}, - {"ip":"127.0.0.5", "country": ""}, - {"ip":"127.0.0.6", "country": []}, - {"ip":"127.0.0.7"}], - "filter":{"count":"multi", "order":"none","geo_filter":"country"}}}`, - }, -} - -var filterGeoSourceIps = []string{ - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "127.0.0.1", - "94.76.229.204", // country = GB - "154.11.253.242", // location = CA near US - "212.83.32.45", // ASN = 47447 - "212.83.32.45", // country = DE, ASN = 47447 - "212.83.32.45", - "178.18.89.144", - "127.0.0.1", - "213.95.10.76", // DE - "94.76.229.204", // GB - "154.11.253.242", // CA - "127.0.0.1", -} - -var filterGeoTestCases = []test.Case{ - { - Qname: "ww1.filtergeo.com.", Qtype: dns.TypeA, - Answer: []dns.RR{ - test.A("ww1.filtergeo.com. 300 IN A 127.0.0.1"), - test.A("ww1.filtergeo.com. 300 IN A 127.0.0.2"), - test.A("ww1.filtergeo.com. 300 IN A 127.0.0.3"), - test.A("ww1.filtergeo.com. 300 IN A 127.0.0.4"), - test.A("ww1.filtergeo.com. 300 IN A 127.0.0.5"), - test.A("ww1.filtergeo.com. 300 IN A 127.0.0.6"), - }, - }, - { - Qname: "ww2.filtergeo.com.", Qtype: dns.TypeA, - Answer: []dns.RR{ - test.A("ww2.filtergeo.com. 300 IN A 127.0.0.4"), - test.A("ww2.filtergeo.com. 300 IN A 127.0.0.5"), - test.A("ww2.filtergeo.com. 300 IN A 127.0.0.6"), - }, - }, - { - Qname: "ww3.filtergeo.com.", Qtype: dns.TypeA, - Answer: []dns.RR{ - test.A("ww3.filtergeo.com. 300 IN A 192.168.0.1"), - }, - }, - { - Qname: "ww4.filtergeo.com.", Qtype: dns.TypeA, - Answer: []dns.RR{ - test.A("ww4.filtergeo.com. 300 IN A 127.0.0.4"), - test.A("ww4.filtergeo.com. 300 IN A 127.0.0.5"), - test.A("ww4.filtergeo.com. 300 IN A 127.0.0.6"), - }, - }, - { - Qname: "ww5.filtergeo.com.", Qtype: dns.TypeA, - Answer: []dns.RR{ - test.A("ww5.filtergeo.com. 300 IN A 127.0.0.5"), - test.A("ww5.filtergeo.com. 300 IN A 127.0.0.6"), - }, - }, - { - Qname: "ww2.filtergeo.com.", Qtype: dns.TypeA, - Answer: []dns.RR{ - test.A("ww2.filtergeo.com. 300 IN A 127.0.0.2"), - }, - }, - { - Qname: "ww3.filtergeo.com.", Qtype: dns.TypeA, - Answer: []dns.RR{ - test.A("ww3.filtergeo.com. 300 IN A 192.30.252.225"), + rr := make([]int, 5) + for i := 0; i < 10000; i++ { + tc := testCase.TestCases[2] + r := tc.Msg() + w := test.NewRecorder(&test.ResponseWriter{}) + state := NewRequestContext(w, r) + handler.HandleRequest(state) + + resp := w.Msg + if len(resp.Answer) != 5 { + fmt.Println("expected 5 results ", len(resp.Answer), " received") + t.Fail() + } + + resa := resp.Answer[0].(*dns.A) + + switch resa.A.String() { + case "127.0.0.1": + rr[0]++ + case "127.0.0.2": + rr[1]++ + case "127.0.0.3": + rr[2]++ + case "127.0.0.4": + rr[3]++ + case "127.0.0.5": + rr[4]++ + } + } + // fmt.Println(rr) + for i := range rr { + if rr[i] < 1500 || rr[i] > 2500 { + fmt.Println("bad ip weight balance") + t.Fail() + } + } }, - }, - { - Qname: "ww4.filtergeo.com.", Qtype: dns.TypeA, - Answer: []dns.RR{ - test.A("ww4.filtergeo.com. 300 IN A 127.0.0.1"), + Zones: []string{"filtermulti.com."}, + ZoneConfigs: []string{`{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.filtermulti.com.","ns":"ns1.filter.com.","refresh":44,"retry":55,"expire":66}}`}, + Entries: [][][]string{ + { + {"ww1", + `{ + "a":{"ttl":300, "records":[ + {"ip":"127.0.0.1", "country":""}, + {"ip":"127.0.0.2", "country":""}, + {"ip":"127.0.0.3", "country":""}, + {"ip":"127.0.0.4", "country":""}, + {"ip":"127.0.0.5", "country":""}, + {"ip":"127.0.0.6", "country":""} + ], + "filter":{"count":"multi","order":"none","geo_filter":"none"}} + }`, + }, + {"ww2", + `{ + "a":{"ttl":300, "records":[ + {"ip":"127.0.0.1", "country":"", "weight":1}, + {"ip":"127.0.0.2", "country":"", "weight":4}, + {"ip":"127.0.0.3", "country":"", "weight":10}, + {"ip":"127.0.0.4", "country":"", "weight":2}, + {"ip":"127.0.0.5", "country":"", "weight":20} + ], + "filter":{"count":"multi","order":"weighted","geo_filter":"none"}} + }`, + }, + {"ww3", + `{ + "a":{"ttl":300, "records":[ + {"ip":"127.0.0.1", "country":""}, + {"ip":"127.0.0.2", "country":""}, + {"ip":"127.0.0.3", "country":""}, + {"ip":"127.0.0.4", "country":""}, + {"ip":"127.0.0.5", "country":""} + ], + "filter":{"count":"multi","order":"rr","geo_filter":"none"}} + }`, + }, + }, }, - }, - { - Qname: "ww5.filtergeo.com.", Qtype: dns.TypeA, - Answer: []dns.RR{ - test.A("ww5.filtergeo.com. 300 IN A 127.0.0.1"), + TestCases: []test.Case{ + { + Qname: "ww1.filtermulti.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("ww1.filtermulti.com. 300 IN A 127.0.0.1"), + test.A("ww1.filtermulti.com. 300 IN A 127.0.0.2"), + test.A("ww1.filtermulti.com. 300 IN A 127.0.0.3"), + test.A("ww1.filtermulti.com. 300 IN A 127.0.0.4"), + test.A("ww1.filtermulti.com. 300 IN A 127.0.0.5"), + test.A("ww1.filtermulti.com. 300 IN A 127.0.0.6"), + }, + }, + { + Qname: "ww2.filtermulti.com.", Qtype: dns.TypeA, + Answer: []dns.RR{}, + }, + { + Qname: "ww3.filtermulti.com.", Qtype: dns.TypeA, + Answer: []dns.RR{}, + }, }, }, { - Qname: "ww6.filtergeo.com.", Qtype: dns.TypeA, - Answer: []dns.RR{ - test.A("ww6.filtergeo.com. 300 IN A 127.0.0.1"), + Name: "filter single ip", + Description: "ip filter functionality for single value results", + Enabled: true, + Config: defaultConfig, + Initialize: defaultInitialize, + ApplyAndVerify: func(testCase *TestCase, handler *DnsRequestHandler, t *testing.T) { + for i := 0; i < 10; i++ { + tc := testCase.TestCases[0] + r := tc.Msg() + w := test.NewRecorder(&test.ResponseWriter{}) + state := NewRequestContext(w, r) + handler.HandleRequest(state) + + resp := w.Msg + + if err := test.SortAndCheck(resp, tc); err != nil { + fmt.Println(err) + t.Fail() + } + } + + w1, w4, w10, w2, w20 := 0, 0, 0, 0, 0 + for i := 0; i < 10000; i++ { + tc := testCase.TestCases[1] + r := tc.Msg() + w := test.NewRecorder(&test.ResponseWriter{}) + state := NewRequestContext(w, r) + handler.HandleRequest(state) + + resp := w.Msg + if len(resp.Answer) != 1 { + fmt.Println("expected 1 answer ", len(resp.Answer), " received") + t.Fail() + } + + resa := resp.Answer[0].(*dns.A) + + switch resa.A.String() { + case "127.0.0.1": + w1++ + case "127.0.0.2": + w4++ + case "127.0.0.3": + w10++ + case "127.0.0.4": + w2++ + case "127.0.0.5": + w20++ + } + } + // fmt.Println(w1, w2, w4, w10, w20) + if w1 > w2 || w2 > w4 || w4 > w10 || w10 > w20 { + fmt.Println("bad ip weight balance") + t.Fail() + } + + rr := make([]int, 5) + for i := 0; i < 10000; i++ { + tc := testCase.TestCases[2] + r := tc.Msg() + w := test.NewRecorder(&test.ResponseWriter{}) + state := NewRequestContext(w, r) + handler.HandleRequest(state) + + resp := w.Msg + if len(resp.Answer) != 1 { + fmt.Println("expected 1 answer ", len(resp.Answer), " received") + t.Fail() + } + + resa := resp.Answer[0].(*dns.A) + + switch resa.A.String() { + case "127.0.0.1": + rr[0]++ + case "127.0.0.2": + rr[1]++ + case "127.0.0.3": + rr[2]++ + case "127.0.0.4": + rr[3]++ + case "127.0.0.5": + rr[4]++ + } + } + // fmt.Println(rr) + for i := range rr { + if rr[i] < 1500 || rr[i] > 2500 { + fmt.Println("bad ip weight balance") + t.Fail() + } + } }, - }, - { - Qname: "ww6.filtergeo.com.", Qtype: dns.TypeA, - Answer: []dns.RR{ - test.A("ww6.filtergeo.com. 300 IN A 127.0.0.2"), - test.A("ww6.filtergeo.com. 300 IN A 127.0.0.3"), + Zones: []string{"filtersingle.com."}, + ZoneConfigs: []string{`{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.filtersingle.com.","ns":"ns1.filter.com.","refresh":44,"retry":55,"expire":66}}`}, + Entries: [][][]string{ + { + {"ww1", + `{ + "a":{"ttl":300, "records":[ + {"ip":"127.0.0.1", "country":""}, + {"ip":"127.0.0.2", "country":""}, + {"ip":"127.0.0.3", "country":""}, + {"ip":"127.0.0.4", "country":""}, + {"ip":"127.0.0.5", "country":""}, + {"ip":"127.0.0.6", "country":""} + ], + "filter":{"count":"single","order":"none","geo_filter":"none"}} + }`, + }, + {"ww2", + `{ + "a":{"ttl":300, "records":[ + {"ip":"127.0.0.1", "country":"", "weight":1}, + {"ip":"127.0.0.2", "country":"", "weight":4}, + {"ip":"127.0.0.3", "country":"", "weight":10}, + {"ip":"127.0.0.4", "country":"", "weight":2}, + {"ip":"127.0.0.5", "country":"", "weight":20} + ], + "filter":{"count":"single","order":"weighted","geo_filter":"none"}} + }`, + }, + {"ww3", + `{ + "a":{"ttl":300, "records":[ + {"ip":"127.0.0.1", "country":""}, + {"ip":"127.0.0.2", "country":""}, + {"ip":"127.0.0.3", "country":""}, + {"ip":"127.0.0.4", "country":""}, + {"ip":"127.0.0.5", "country":""} + ], + "filter":{"count":"single","order":"rr","geo_filter":"none"}} + }`, + }, + }, }, - }, - { - Qname: "ww6.filtergeo.com.", Qtype: dns.TypeA, - Answer: []dns.RR{ - test.A("ww6.filtergeo.com. 300 IN A 127.0.0.2"), - test.A("ww6.filtergeo.com. 300 IN A 127.0.0.4"), - test.A("ww6.filtergeo.com. 300 IN A 127.0.0.5"), - test.A("ww6.filtergeo.com. 300 IN A 127.0.0.6"), + TestCases: []test.Case{ + { + Qname: "ww1.filtersingle.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("ww1.filtersingle.com. 300 IN A 127.0.0.1"), + }, + }, + { + Qname: "ww2.filtersingle.com.", Qtype: dns.TypeA, + Answer: []dns.RR{}, + }, + { + Qname: "ww3.filtersingle.com.", Qtype: dns.TypeA, + Answer: []dns.RR{}, + }, }, }, { - Qname: "ww7.filtergeo.com.", Qtype: dns.TypeA, - Answer: []dns.RR{ - test.A("ww7.filtergeo.com. 300 IN A 127.0.0.1"), - test.A("ww7.filtergeo.com. 300 IN A 127.0.0.2"), - test.A("ww7.filtergeo.com. 300 IN A 127.0.0.3"), + Name: "cname upstream", + Description: "cname should not leave authoritative zone", + Enabled: true, + Config: defaultConfig, + Initialize: defaultInitialize, + ApplyAndVerify: func(testCase *TestCase, handler *DnsRequestHandler, t *testing.T) { + tc := testCase.TestCases[0] + r := tc.Msg() + w := test.NewRecorder(&test.ResponseWriter{}) + state := NewRequestContext(w, r) + handler.HandleRequest(state) + + resp := w.Msg + // fmt.Println(resp) + if resp.Rcode != dns.RcodeSuccess { + fmt.Println("invalid rcode, expected : RcodeSuccess, received : ", dns.RcodeToString[resp.Rcode]) + t.Fail() + } + cname := resp.Answer[0].(*dns.CNAME) + if cname.Target != "www.google.com." { + fmt.Println("invalid cname target, expected : www.google.com. received : ", cname.Target) + t.Fail() + } }, - }, - { - Qname: "ww7.filtergeo.com.", Qtype: dns.TypeA, - Answer: []dns.RR{ - test.A("ww7.filtergeo.com. 300 IN A 127.0.0.1"), + Zones: []string{"upstreamcname.com."}, + ZoneConfigs: []string{`{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.upstreamcname.com.","ns":"ns1.upstreamcname.com.","refresh":44,"retry":55,"expire":66}}`}, + Entries: [][][]string{ + { + {"upstream", + `{"cname":{"ttl":300, "host":"www.google.com"}}`, + }, + }, }, - }, - { - Qname: "ww7.filtergeo.com.", Qtype: dns.TypeA, - Answer: []dns.RR{ - test.A("ww7.filtergeo.com. 300 IN A 127.0.0.4"), + TestCases: []test.Case{ + { + Qname: "upstream.upstreamcname.com.", Qtype: dns.TypeA, + }, }, }, { - Qname: "ww7.filtergeo.com.", Qtype: dns.TypeA, - Answer: []dns.RR{ - test.A("ww7.filtergeo.com. 300 IN A 127.0.0.2"), - test.A("ww7.filtergeo.com. 300 IN A 127.0.0.5"), - test.A("ww7.filtergeo.com. 300 IN A 127.0.0.6"), - test.A("ww7.filtergeo.com. 300 IN A 127.0.0.7"), - }, - }, -} - -func TestGeoFilter(t *testing.T) { - logger.Default = logger.NewLogger(&logger.LogConfig{Target: "file", Enable: true, Path: "/tmp/rtest.log", Format: "txt"}) - - h := NewHandler(&handlerTestConfig) - h.Redis.Del("*") - h.Redis.SAdd("redins:zones", filterGeoZone) - for _, cmd := range filterGeoEntries { - err := h.Redis.HSet("redins:zones:"+filterGeoZone, cmd[0], cmd[1]) - if err != nil { - log.Printf("[ERROR] cannot connect to redis: %s", err) - t.Fail() - } - } - h.Redis.Set("redins:zones:"+filterGeoZone+":config", filterGeoConfig) - h.LoadZones() - for i, tc := range filterGeoTestCases { - sa := filterGeoSourceIps[i] - opt := &dns.OPT{ - Hdr: dns.RR_Header{Name: ".", Rrtype: dns.TypeOPT, Class: dns.ClassANY, Rdlength: 0, Ttl: 300}, - Option: []dns.EDNS0{ - &dns.EDNS0_SUBNET{ - Address: net.ParseIP(sa), - Code: dns.EDNS0SUBNET, - Family: 1, - SourceNetmask: 32, - SourceScope: 0, + Name: "cname outside domain", + Description: "should follow cname between authoritative zones", + Enabled: true, + Config: defaultConfig, + Initialize: defaultInitialize, + ApplyAndVerify: defaultApplyAndVerify, + Zones: []string{"inside.cnm.", "outside.cnm.", "flattening.cnm."}, + ZoneConfigs: []string{ + `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.inside.cnm.","ns":"ns1.inside.cnm.","refresh":44,"retry":55,"expire":66}}`, + `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.outside.cnm.","ns":"ns1.outside.cnm.","refresh":44,"retry":55,"expire":66}}`, + `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.flatenning.cnm.","ns":"ns1.flatenning.cnm.","refresh":44,"retry":55,"expire":66},"cname_flattening":true}`, + }, + Entries: [][][]string{ + { + {"a", + `{"cname":{"ttl":300, "host":"b.inside.cnm."}}`, + }, + {"b", + `{"cname":{"ttl":300, "host":"a.outside.cnm."}}`, + }, + }, + { + {"@", + `{"a":{"ttl":300, "records":[{"ip":"127.0.0.6"}]}}`, + }, + {"a", + `{"cname":{"ttl":300, "host":"b.outside.cnm."}}`, + }, + {"b", + `{"cname":{"ttl":300, "host":"outside.cnm."}}`, + }, + }, + { + {"a", + `{"cname":{"ttl":300, "host":"a.inside.cnm."}}`, }, }, - } - r := tc.Msg() - r.Extra = append(r.Extra, opt) - w := test.NewRecorder(&test.ResponseWriter{}) - state := request.Request{W: w, Req: r} - h.HandleRequest(&state) - - resp := w.Msg - resp.Extra = nil - - if test.SortAndCheck(resp, tc) != nil { - t.Fail() - } - } -} - -var filterMultiZone = "filtermulti.com." - -var filterMultiConfig = `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.filtermulti.com.","ns":"ns1.filter.com.","refresh":44,"retry":55,"expire":66}}` - -var filterMultiEntries = [][]string{ - {"ww1", - `{"a":{"ttl":300, "records":[ - {"ip":"127.0.0.1", "country":""}, - {"ip":"127.0.0.2", "country":""}, - {"ip":"127.0.0.3", "country":""}, - {"ip":"127.0.0.4", "country":""}, - {"ip":"127.0.0.5", "country":""}, - {"ip":"127.0.0.6", "country":""}], - "filter":{"count":"multi","order":"none","geo_filter":"none"}}}`, - }, - {"ww2", - `{"a":{"ttl":300, "records":[ - {"ip":"127.0.0.1", "country":"", "weight":1}, - {"ip":"127.0.0.2", "country":"", "weight":4}, - {"ip":"127.0.0.3", "country":"", "weight":10}, - {"ip":"127.0.0.4", "country":"", "weight":2}, - {"ip":"127.0.0.5", "country":"", "weight":20}], - "filter":{"count":"multi","order":"weighted","geo_filter":"none"}}}`, - }, - {"ww3", - `{"a":{"ttl":300, "records":[ - {"ip":"127.0.0.1", "country":""}, - {"ip":"127.0.0.2", "country":""}, - {"ip":"127.0.0.3", "country":""}, - {"ip":"127.0.0.4", "country":""}, - {"ip":"127.0.0.5", "country":""}], - "filter":{"count":"multi","order":"rr","geo_filter":"none"}}}`, - }, -} - -var filterMultiTestCases = []test.Case{ - { - Qname: "ww1.filtermulti.com.", Qtype: dns.TypeA, - Answer: []dns.RR{ - test.A("ww1.filtermulti.com. 300 IN A 127.0.0.1"), - test.A("ww1.filtermulti.com. 300 IN A 127.0.0.2"), - test.A("ww1.filtermulti.com. 300 IN A 127.0.0.3"), - test.A("ww1.filtermulti.com. 300 IN A 127.0.0.4"), - test.A("ww1.filtermulti.com. 300 IN A 127.0.0.5"), - test.A("ww1.filtermulti.com. 300 IN A 127.0.0.6"), - }, - }, - { - Qname: "ww2.filtermulti.com.", Qtype: dns.TypeA, - Answer: []dns.RR{}, - }, - { - Qname: "ww3.filtermulti.com.", Qtype: dns.TypeA, - Answer: []dns.RR{}, - }, -} - -func TestMultiFilter(t *testing.T) { - logger.Default = logger.NewLogger(&logger.LogConfig{}) - - h := NewHandler(&handlerTestConfig) - h.Redis.Del("*") - h.Redis.SAdd("redins:zones", filterMultiZone) - for _, cmd := range filterMultiEntries { - err := h.Redis.HSet("redins:zones:"+filterMultiZone, cmd[0], cmd[1]) - if err != nil { - log.Printf("[ERROR] cannot connect to redis: %s", err) - log.Println("1") - t.Fail() - } - } - h.Redis.Set("redins:zones:"+filterMultiZone+":config", filterMultiConfig) - h.LoadZones() - - for i := 0; i < 10; i++ { - tc := filterMultiTestCases[0] - r := tc.Msg() - w := test.NewRecorder(&test.ResponseWriter{}) - state := request.Request{W: w, Req: r} - h.HandleRequest(&state) - - resp := w.Msg - - if test.SortAndCheck(resp, tc) != nil { - t.Fail() - } - } - - w1, w4, w10, w2, w20 := 0, 0, 0, 0, 0 - for i := 0; i < 10000; i++ { - tc := filterMultiTestCases[1] - r := tc.Msg() - w := test.NewRecorder(&test.ResponseWriter{}) - state := request.Request{W: w, Req: r} - h.HandleRequest(&state) - - resp := w.Msg - if len(resp.Answer) != 5 { - log.Println("2") - t.Fail() - } - - resa := resp.Answer[0].(*dns.A) - - switch resa.A.String() { - case "127.0.0.1": - w1++ - case "127.0.0.2": - w4++ - case "127.0.0.3": - w10++ - case "127.0.0.4": - w2++ - case "127.0.0.5": - w20++ - } - } - log.Println(w1, w2, w4, w10, w20) - if w1 > w2 || w2 > w4 || w4 > w10 || w10 > w20 { - log.Println("3") - t.Fail() - } - - rr := make([]int, 5) - for i := 0; i < 10000; i++ { - tc := filterMultiTestCases[2] - r := tc.Msg() - w := test.NewRecorder(&test.ResponseWriter{}) - state := request.Request{W: w, Req: r} - h.HandleRequest(&state) - - resp := w.Msg - if len(resp.Answer) != 5 { - log.Println("4") - t.Fail() - } - - resa := resp.Answer[0].(*dns.A) - - switch resa.A.String() { - case "127.0.0.1": - rr[0]++ - case "127.0.0.2": - rr[1]++ - case "127.0.0.3": - rr[2]++ - case "127.0.0.4": - rr[3]++ - case "127.0.0.5": - rr[4]++ - } - } - log.Println(rr) - for i := range rr { - if rr[i] < 1500 || rr[i] > 2500 { - log.Println("5") - t.Fail() - } - } -} - -var filterSingleZone = "filtersingle.com." -var filterSingleConfig = `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.filtersingle.com.","ns":"ns1.filter.com.","refresh":44,"retry":55,"expire":66}}` -var filterSingleEntries = [][]string{ - {"ww1", - `{"a":{"ttl":300, "records":[ - {"ip":"127.0.0.1", "country":""}, - {"ip":"127.0.0.2", "country":""}, - {"ip":"127.0.0.3", "country":""}, - {"ip":"127.0.0.4", "country":""}, - {"ip":"127.0.0.5", "country":""}, - {"ip":"127.0.0.6", "country":""}], - "filter":{"count":"single","order":"none","geo_filter":"none"}}}`, - }, - {"ww2", - `{"a":{"ttl":300, "records":[ - {"ip":"127.0.0.1", "country":"", "weight":1}, - {"ip":"127.0.0.2", "country":"", "weight":4}, - {"ip":"127.0.0.3", "country":"", "weight":10}, - {"ip":"127.0.0.4", "country":"", "weight":2}, - {"ip":"127.0.0.5", "country":"", "weight":20}], - "filter":{"count":"single","order":"weighted","geo_filter":"none"}}}`, - }, - {"ww3", - `{"a":{"ttl":300, "records":[ - {"ip":"127.0.0.1", "country":""}, - {"ip":"127.0.0.2", "country":""}, - {"ip":"127.0.0.3", "country":""}, - {"ip":"127.0.0.4", "country":""}, - {"ip":"127.0.0.5", "country":""}], - "filter":{"count":"single","order":"rr","geo_filter":"none"}}}`, - }, -} - -var filterSingleTestCases = []test.Case{ - { - Qname: "ww1.filtersingle.com.", Qtype: dns.TypeA, - Answer: []dns.RR{ - test.A("ww1.filtersingle.com. 300 IN A 127.0.0.1"), }, - }, - { - Qname: "ww2.filtersingle.com.", Qtype: dns.TypeA, - Answer: []dns.RR{}, - }, - { - Qname: "ww3.filtersingle.com.", Qtype: dns.TypeA, - Answer: []dns.RR{}, - }, -} - -func TestSingleFilter(t *testing.T) { - logger.Default = logger.NewLogger(&logger.LogConfig{}) - - h := NewHandler(&handlerTestConfig) - h.Redis.Del("*") - h.Redis.SAdd("redins:zones", filterSingleZone) - for _, cmd := range filterSingleEntries { - err := h.Redis.HSet("redins:zones:"+filterSingleZone, cmd[0], cmd[1]) - if err != nil { - log.Printf("[ERROR] cannot connect to redis: %s", err) - log.Println("1") - t.Fail() - } - } - h.Redis.Set("redins:zones:"+filterSingleZone+":config", filterSingleConfig) - h.LoadZones() - - for i := 0; i < 10; i++ { - tc := filterSingleTestCases[0] - r := tc.Msg() - w := test.NewRecorder(&test.ResponseWriter{}) - state := request.Request{W: w, Req: r} - h.HandleRequest(&state) - - resp := w.Msg - - if test.SortAndCheck(resp, tc) != nil { - t.Fail() - } - } - - w1, w4, w10, w2, w20 := 0, 0, 0, 0, 0 - for i := 0; i < 10000; i++ { - tc := filterSingleTestCases[1] - r := tc.Msg() - w := test.NewRecorder(&test.ResponseWriter{}) - state := request.Request{W: w, Req: r} - h.HandleRequest(&state) - - resp := w.Msg - if len(resp.Answer) != 1 { - log.Println("2") - t.Fail() - } - - resa := resp.Answer[0].(*dns.A) - - switch resa.A.String() { - case "127.0.0.1": - w1++ - case "127.0.0.2": - w4++ - case "127.0.0.3": - w10++ - case "127.0.0.4": - w2++ - case "127.0.0.5": - w20++ - } - } - log.Println(w1, w2, w4, w10, w20) - if w1 > w2 || w2 > w4 || w4 > w10 || w10 > w20 { - log.Println("3") - t.Fail() - } - - rr := make([]int, 5) - for i := 0; i < 10000; i++ { - tc := filterSingleTestCases[2] - r := tc.Msg() - w := test.NewRecorder(&test.ResponseWriter{}) - state := request.Request{W: w, Req: r} - h.HandleRequest(&state) - - resp := w.Msg - if len(resp.Answer) != 1 { - log.Println("4") - t.Fail() - } - - resa := resp.Answer[0].(*dns.A) - - switch resa.A.String() { - case "127.0.0.1": - rr[0]++ - case "127.0.0.2": - rr[1]++ - case "127.0.0.3": - rr[2]++ - case "127.0.0.4": - rr[3]++ - case "127.0.0.5": - rr[4]++ - } - } - log.Println(rr) - for i := range rr { - if rr[i] < 1500 || rr[i] > 2500 { - log.Println("5") - t.Fail() - } - } -} - -var upstreamCNAMEZone = "upstreamcname.com." -var upstreamCNAMEConfig = `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.upstreamcname.com.","ns":"ns1.upstreamcname.com.","refresh":44,"retry":55,"expire":66}}` -var upstreamCNAME = [][]string{ - {"upstream", - `{"cname":{"ttl":300, "host":"www.google.com"}}`, - }, -} - -var upstreamCNAMETestCases = []test.Case{ - { - Qname: "upstream.upstreamcname.com.", Qtype: dns.TypeA, - }, -} - -func TestUpstreamCNAME(t *testing.T) { - logger.Default = logger.NewLogger(&logger.LogConfig{}) - - h := NewHandler(&handlerTestConfig) - h.Redis.Del("*") - h.Redis.SAdd("redins:zones", upstreamCNAMEZone) - for _, cmd := range upstreamCNAME { - err := h.Redis.HSet("redins:zones:"+upstreamCNAMEZone, cmd[0], cmd[1]) - if err != nil { - log.Printf("[ERROR] cannot connect to redis: %s", err) - log.Println("1") - t.Fail() - } - } - h.Redis.Set("redins:zones:"+upstreamCNAMEZone+":config", upstreamCNAMEConfig) - h.LoadZones() - - h.Config.UpstreamFallback = false - { - tc := upstreamCNAMETestCases[0] - r := tc.Msg() - w := test.NewRecorder(&test.ResponseWriter{}) - state := request.Request{W: w, Req: r} - h.HandleRequest(&state) - - resp := w.Msg - log.Println(resp) - if resp.Rcode != dns.RcodeSuccess { - log.Println("1") - t.Fail() - } - cname := resp.Answer[0].(*dns.CNAME) - if cname.Target != "www.google.com." { - log.Println("2 ", cname) - t.Fail() - } - } - - h.Config.UpstreamFallback = true - { - tc := upstreamCNAMETestCases[0] - r := tc.Msg() - w := test.NewRecorder(&test.ResponseWriter{}) - state := request.Request{W: w, Req: r} - h.HandleRequest(&state) - - resp := w.Msg - log.Println(resp) - if resp.Rcode != dns.RcodeSuccess { - log.Println("3") - t.Fail() - } - cname := resp.Answer[0].(*dns.CNAME) - if cname.Target != "www.google.com." { - log.Println("4 ", cname) - t.Fail() - } - } -} - -var cnameOutsideZones = []string{"inside.cnm.", "outside.cnm."} -var cnameOutsideConfig = []string{ - `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.inside.cnm.","ns":"ns1.inside.cnm.","refresh":44,"retry":55,"expire":66}}`, - `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.outside.cnm.","ns":"ns1.outside.cnm.","refresh":44,"retry":55,"expire":66}}`, -} -var cnameOutsideEntries = [][][]string{ - { - {"upstream", - `{"cname":{"ttl":300, "host":"outside.cnm."}}`, + TestCases: []test.Case{ + { + Qname: "a.inside.cnm.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.CNAME("a.inside.cnm. 300 IN CNAME b.inside.cnm."), + test.CNAME("b.inside.cnm. 300 IN CNAME a.outside.cnm."), + }, + }, + { + Qname: "a.flattening.cnm.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.CNAME("a.flattening.cnm. 300 IN CNAME a.inside.cnm."), + }, + }, }, }, { - {"@", - `{"a":{"ttl":300, "records":[{"ip":"127.0.0.6"}]}}`, + Name: "cname loop", + Description: "should properly handle cname loop", + Enabled: true, + Config: defaultConfig, + Initialize: defaultInitialize, + ApplyAndVerify: defaultApplyAndVerify, + Zones: []string{"loop.cnm."}, + ZoneConfigs: []string{""}, + Entries: [][][]string{ + { + {"w", + `{"cname":{"ttl":300, "host":"w.loop.cnm."}}`, + }, + {"w1", + `{"cname":{"ttl":300, "host":"w2.loop.cnm."}}`, + }, + {"w2", + `{"cname":{"ttl":300, "host":"w1.loop.cnm."}}`, + }, + }, }, - }, -} - -var cnameOutsideTests = []test.Case{ - { - Qname: "upstream.inside.cnm.", Qtype: dns.TypeA, - Answer: []dns.RR{ - test.CNAME("upstream.inside.cnm. 300 IN CNAME outside.cnm."), + TestCases: []test.Case{ + { + Qname: "w.loop.cnm.", Qtype: dns.TypeA, + Rcode: dns.RcodeServerFailure, + }, + { + Qname: "w1.loop.cnm.", Qtype: dns.TypeA, + Rcode: dns.RcodeServerFailure, + }, + { + Qname: "w2.loop.cnm.", Qtype: dns.TypeA, + Rcode: dns.RcodeServerFailure, + }, }, }, -} - -func TestCNameOutsideZone(t *testing.T) { - logger.Default = logger.NewLogger(&logger.LogConfig{}) - - h := NewHandler(&handlerTestConfig) - h.Redis.Del("*") - for i, zone := range cnameOutsideZones { - h.Redis.SAdd("redins:zones", zone) - for _, cmd := range cnameOutsideEntries[i] { - err := h.Redis.HSet("redins:zones:"+zone, cmd[0], cmd[1]) - if err != nil { - log.Printf("[ERROR] cannot connect to redis: %s", err) - t.Fail() - } - } - } - h.LoadZones() - for j, tc := range cnameOutsideTests { - - r := tc.Msg() - w := test.NewRecorder(&test.ResponseWriter{}) - state := request.Request{W: w, Req: r} - h.HandleRequest(&state) - - resp := w.Msg - - fmt.Println(j, tc.Qname, tc.Answer, resp.Answer) - if err := test.SortAndCheck(resp, tc); err != nil { - t.Fail() - fmt.Println(err) - } - } -} - -var cnameLoopZone = "loop.cnm." - -var cnameLoopEntries = [][]string{ - {"w1", - `{"cname":{"ttl":300, "host":"w2.loop.cnm."}}`, - }, - {"w2", - `{"cname":{"ttl":300, "host":"w1.loop.cnm."}}`, - }, -} - -var cnameLoopTests = []test.Case{ { - Qname: "w1.loop.cnm.", Qtype: dns.TypeA, - Rcode: dns.RcodeServerFailure, - }, -} - -func TestCNameLoop(t *testing.T) { - logger.Default = logger.NewLogger(&logger.LogConfig{}) - - h := NewHandler(&handlerTestConfig) - h.Redis.Del("*") - h.Redis.SAdd("redins:zones", cnameLoopZone) - for _, cmd := range cnameLoopEntries { - err := h.Redis.HSet("redins:zones:"+cnameLoopZone, cmd[0], cmd[1]) - if err != nil { - log.Printf("[ERROR] cannot connect to redis: %s", err) - t.Fail() - } - } - - h.LoadZones() - for j, tc := range cnameLoopTests { - - r := tc.Msg() - w := test.NewRecorder(&test.ResponseWriter{}) - state := request.Request{W: w, Req: r} - h.HandleRequest(&state) - - resp := w.Msg - - fmt.Println(j, tc.Qname, tc.Answer, resp.Answer) - if err := test.SortAndCheck(resp, tc); err != nil { - t.Fail() - fmt.Println(err) - } - } -} - -var findZoneZones = []string{"zone.zon.", "sub1.zone.zon."} - -var findZoneEntries = [][][]string{ - { - {"sub3.sub2", - `{"a":{"ttl":300, "records":[{"ip":"1.1.1.1"}]}}`, + Name: "zone matching", + Description: "zone should match with longest prefix", + Enabled: true, + Config: defaultConfig, + Initialize: defaultInitialize, + ApplyAndVerify: defaultApplyAndVerify, + Zones: []string{"zone.zon.", "sub1.zone.zon."}, + ZoneConfigs: []string{"", ""}, + Entries: [][][]string{ + { + {"sub3.sub2", + `{"a":{"ttl":300, "records":[{"ip":"1.1.1.1"}]}}`, + }, + {"sub1", + `{"a":{"ttl":300, "records":[{"ip":"2.2.2.2"}]}}`, + }, + {"sub10", + `{"a":{"ttl":300, "records":[{"ip":"5.5.5.5"}]}}`, + }, + {"ub1", + `{"a":{"ttl":300, "records":[{"ip":"6.6.6.6"}]}}`, + }, + }, + { + {"@", + `{"a":{"ttl":300, "records":[{"ip":"3.3.3.3"}]}}`, + }, + {"sub2", + `{"a":{"ttl":300, "records":[{"ip":"4.4.4.4"}]}}`, + }, + }, }, - {"sub1", - `{"a":{"ttl":300, "records":[{"ip":"2.2.2.2"}]}}`, + TestCases: []test.Case{ + { + Qname: "sub1.zone.zon.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("sub1.zone.zon. 300 IN A 3.3.3.3"), + }, + }, + { + Qname: "sub10.zone.zon.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("sub10.zone.zon. 300 IN A 5.5.5.5"), + }, + }, + { + Qname: "sub3.sub2.zone.zon.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("sub3.sub2.zone.zon. 300 IN A 1.1.1.1"), + }, + }, + { + Qname: "sub2.sub1.zone.zon.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("sub2.sub1.zone.zon. 300 IN A 4.4.4.4"), + }, + }, + { + Qname: "ub1.zone.zon.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("ub1.zone.zon. 300 IN A 6.6.6.6"), + }, + }, }, - {"sub10", - `{"a":{"ttl":300, "records":[{"ip":"5.5.5.5"}]}}`, + }, + { + Name: "cname noauth", + Description: "cname following should stop and return results when reaching notauth zone", + Enabled: true, + Config: defaultConfig, + Initialize: defaultInitialize, + ApplyAndVerify: defaultApplyAndVerify, + Zones: []string{"auth.zon."}, + ZoneConfigs: []string{""}, + Entries: [][][]string{ + { + {"w1", + `{"cname":{"ttl":300, "host":"w2.auth.zon."}}`, + }, + {"w2", + `{"cname":{"ttl":300, "host":"noauth.zon."}}`, + }, + }, }, - {"ub1", - `{"a":{"ttl":300, "records":[{"ip":"6.6.6.6"}]}}`, + TestCases: []test.Case{ + { + Qname: "w1.auth.zon.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.CNAME("w1.auth.zon. 300 IN CNAME w2.auth.zon."), + test.CNAME("w2.auth.zon. 300 IN CNAME noauth.zon."), + }, + Rcode: dns.RcodeSuccess, + }, }, }, { - {"@", - `{"a":{"ttl":300, "records":[{"ip":"3.3.3.3"}]}}`, + Name: "delegation", + Description: "test subdomain delegation", + Enabled: true, + Config: defaultConfig, + Initialize: defaultInitialize, + ApplyAndVerify: defaultApplyAndVerify, + Zones: []string{"delegation.zon."}, + ZoneConfigs: []string{""}, + Entries: [][][]string{ + { + {"glue", + `{"ns":{"ttl":300, "records":[{"host":"ns1.glue.delegation.zon."},{"host":"ns2.glue.delegation.zon."}]}}`, + }, + {"noglue", + `{"ns":{"ttl":300, "records":[{"host":"ns1.delegated.zon."},{"host":"ns2.delegated.zon."}]}}`, + }, + {"ns1.glue", + `{"a":{"ttl":300, "records":[{"ip":"1.2.3.4"}]}}`, + }, + {"ns2.glue", + `{"a":{"ttl":300, "records":[{"ip":"5.6.7.8"}]}}`, + }, + {"cname", + `{"cname":{"ttl":300, "host":"glue.delegation.zon."}}`, + }, + }, }, - {"sub2", - `{"a":{"ttl":300, "records":[{"ip":"4.4.4.4"}]}}`, + TestCases: []test.Case{ + { + Qname: "glue.delegation.zon.", + Qtype: dns.TypeA, + Ns: []dns.RR{ + test.NS("glue.delegation.zon. 300 IN NS ns1.glue.delegation.zon."), + test.NS("glue.delegation.zon. 300 IN NS ns2.glue.delegation.zon."), + }, + Extra: []dns.RR{ + test.A("ns1.glue.delegation.zon. 300 IN A 1.2.3.4"), + test.A("ns2.glue.delegation.zon. 300 IN A 5.6.7.8"), + }, + }, + { + Qname: "noglue.delegation.zon.", + Qtype: dns.TypeA, + Ns: []dns.RR{ + test.NS("noglue.delegation.zon. 300 IN NS ns1.delegated.zon."), + test.NS("noglue.delegation.zon. 300 IN NS ns2.delegated.zon."), + }, + }, + { + Qname: "cname.delegation.zon.", + Qtype: dns.TypeA, + Answer: []dns.RR{ + test.CNAME("cname.delegation.zon. 300 IN CNAME glue.delegation.zon."), + }, + Ns: []dns.RR{ + test.NS("glue.delegation.zon. 300 IN NS ns1.glue.delegation.zon."), + test.NS("glue.delegation.zon. 300 IN NS ns2.glue.delegation.zon."), + }, + Extra: []dns.RR{ + test.A("ns1.glue.delegation.zon. 300 IN A 1.2.3.4"), + test.A("ns2.glue.delegation.zon. 300 IN A 5.6.7.8"), + }, + }, }, }, -} - -var findZoneTests = []test.Case{ { - Qname: "sub1.zone.zon.", Qtype: dns.TypeA, - Answer: []dns.RR{ - test.A("sub1.zone.zon. 300 IN A 3.3.3.3"), + Name: "label matching", + Description: "test correct label matching", + Enabled: true, + Config: defaultConfig, + Initialize: defaultInitialize, + ApplyAndVerify: defaultApplyAndVerify, + Zones: []string{"zone1.com.", "zone2.com.", "zone3.com."}, + ZoneConfigs: []string{"", "", ""}, + Entries: [][][]string{ + { + {"@", + `{"a":{"ttl":300, "records":[{"ip":"1.1.1.1"}]}}`, + }, + {"www", + `{"a":{"ttl":300, "records":[{"ip":"1.1.1.2"}]}}`, + }, + }, + { + {"@", + `{"a":{"ttl":300, "records":[{"ip":"2.2.2.1"}]}}`, + }, + {"www", + `{"a":{"ttl":300, "records":[{"ip":"2.2.2.2"}]}}`, + }, + {"zone1.com", + `{"a":{"ttl":300, "records":[{"ip":"2.2.2.3"}]}}`, + }, + {"www.zone1", + `{"a":{"ttl":300, "records":[{"ip":"2.2.2.4"}]}}`, + }, + {"www.zone1.com", + `{"a":{"ttl":300, "records":[{"ip":"2.2.2.5"}]}}`, + }, + }, + { + {"@", + `{"a":{"ttl":300, "records":[{"ip":"3.3.3.1"}]}}`, + }, + {"www", + `{"a":{"ttl":300, "records":[{"ip":"3.3.3.2"}]}}`, + }, + {"zone3.com", + `{"a":{"ttl":300, "records":[{"ip":"3.3.3.3"}]}}`, + }, + }, + }, + TestCases: []test.Case{ + { + Qname: "zone1.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("zone1.com. 300 IN A 1.1.1.1"), + }, + }, + { + Qname: "www.zone1.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("www.zone1.com. 300 IN A 1.1.1.2"), + }, + }, + { + Qname: "zone2.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("zone2.com. 300 IN A 2.2.2.1"), + }, + }, + { + Qname: "www.zone2.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("www.zone2.com. 300 IN A 2.2.2.2"), + }, + }, + { + Qname: "zone1.com.zone2.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("zone1.com.zone2.com. 300 IN A 2.2.2.3"), + }, + }, + { + Qname: "www.zone1.zone2.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("www.zone1.zone2.com. 300 IN A 2.2.2.4"), + }, + }, + { + Qname: "www.zone1.com.zone2.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("www.zone1.com.zone2.com. 300 IN A 2.2.2.5"), + }, + }, + { + Qname: "zone3.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("zone3.com. 300 IN A 3.3.3.1"), + }, + }, + { + Qname: "www.zone3.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("www.zone3.com. 300 IN A 3.3.3.2"), + }, + }, + { + Qname: "zone3.com.zone3.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("zone3.com.zone3.com. 300 IN A 3.3.3.3"), + }, + }, }, }, { - Qname: "sub10.zone.zon.", Qtype: dns.TypeA, - Answer: []dns.RR{ - test.A("sub10.zone.zon. 300 IN A 5.5.5.5"), + Name: "cname flattening leaving zone", + Description: "test correct response when reaching a cname pointing outside current zone", + Enabled: true, + Config: defaultConfig, + Initialize: defaultInitialize, + ApplyAndVerify: defaultApplyAndVerify, + Zones: []string{"flat.com.", "noflat.com."}, + ZoneConfigs: []string{ + `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.flat.com.","ns":"ns1.flat.com.","refresh":44,"retry":55,"expire":66},"cname_flattening":true}}`, + `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.noflat.com.","ns":"ns1.noflat.com.","refresh":44,"retry":55,"expire":66},,"cname_flattening":false}}`, + }, + Entries: [][][]string{ + { + {"a", + `{"cname":{"ttl":300, "host":"www.flat.com."}}`, + }, + {"www", + `{"cname":{"ttl":300, "host":"anotherzone.com."}}`, + }, + }, + { + {"a", + `{"cname":{"ttl":300, "host":"www.noflat.com."}}`, + }, + {"www", + `{"cname":{"ttl":300, "host":"anotherzone.com."}}`, + }, + }, + }, + TestCases: []test.Case{ + { + Qname: "a.flat.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.CNAME("a.flat.com. 300 IN CNAME anotherzone.com."), + }, + }, + { + Qname: "a.noflat.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.CNAME("a.noflat.com. 300 IN CNAME www.noflat.com."), + test.CNAME("www.noflat.com. 300 IN CNAME anotherzone.com."), + }, + }, }, }, { - Qname: "sub3.sub2.zone.zon.", Qtype: dns.TypeA, - Answer: []dns.RR{ - test.A("sub3.sub2.zone.zon. 300 IN A 1.1.1.1"), + Name: "ANAME ttl", + Description: "test ttl value for aname queries", + Enabled: true, + Config: defaultConfig, + Initialize: defaultInitialize, + ApplyAndVerify: defaultApplyAndVerify, + Zones: []string{"arvancloud.com.", "arvan.an."}, + ZoneConfigs: []string{ + `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.arvancloud.com.","ns":"ns1.arvancloud.com.","refresh":44,"retry":55,"expire":66}}`, + `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.arvan.an.","ns":"ns1.arvan.an.","refresh":44,"retry":55,"expire":66}}`, + }, + Entries: [][][]string{ + { + {"@", + `{"aname":{"location":"aname.arvan.an."}}`, + }, + {"upstream", + `{"aname":{"location":"dns.msftncsi.com."}}`, + }, + }, + { + {"aname", + `{"a":{"ttl":180, "records":[{"ip":"6.5.6.5"}]}, "aaaa":{"ttl":300, "records":[{"ip":"::1"}]}}`, + }, + }, + }, + TestCases: []test.Case{ + { + Qname: "arvancloud.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("arvancloud.com. 180 IN A 6.5.6.5"), + }, + }, + { + Qname: "upstream.arvancloud.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("upstream.arvancloud.com. 303 IN A 131.107.255.255"), + }, + }, }, }, { - Qname: "sub2.sub1.zone.zon.", Qtype: dns.TypeA, - Answer: []dns.RR{ - test.A("sub2.sub1.zone.zon. 300 IN A 4.4.4.4"), + Name: "malformed data", + Description: "test proper handling of malformed data", + Enabled: true, + Config: defaultConfig, + Initialize: defaultInitialize, + ApplyAndVerify: defaultApplyAndVerify, + Zones: []string{"arvancloud.mal."}, + ZoneConfigs: []string{ + `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.arvancloud.mal.","ns":"ns1.arvancloud.mal.","refresh":44,"retry":55,"expire":66}}`, + }, + Entries: [][][]string{ + { + {"@", + `{"aname":{"location":"mal1.arvancloud.mal."}}`, + }, + {"www", + `{"a":{"ttl":"300", "records":[{"ip":"3.3.3.1"}]}}`, + }, + {"mal1", + `!@#$$^$*^&^dfgsfdg@#@EWDS`, + }, + }, + }, + TestCases: []test.Case{ + { + Qname: "arvancloud.mal.", Qtype: dns.TypeA, + Rcode: dns.RcodeServerFailure, + }, + { + Qname: "www.arvancloud.mal.", Qtype: dns.TypeA, + Rcode: dns.RcodeServerFailure, + }, + { + Qname: "mal1.arvancloud.mal.", Qtype: dns.TypeA, + Rcode: dns.RcodeServerFailure, + }, }, }, { - Qname: "ub1.zone.zon.", Qtype: dns.TypeA, - Answer: []dns.RR{ - test.A("ub1.zone.zon. 300 IN A 6.6.6.6"), + Name: "implicit root location", + Description: "root location always exists", + Enabled: true, + Config: defaultConfig, + Initialize: defaultInitialize, + ApplyAndVerify: defaultApplyAndVerify, + Zones: []string{"arvancloud.root."}, + ZoneConfigs: []string{ + `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.arvancloud.root.","ns":"ns1.arvancloud.root.","refresh":44,"retry":55,"expire":66}}`, + }, + Entries: [][][]string{ + { + {"www", + `{"a":{"ttl":"300", "records":[{"ip":"3.3.3.1"}]}}`, + }, + }, + }, + TestCases: []test.Case{ + { + Qname: "arvancloud.root.", Qtype: dns.TypeA, + Ns: []dns.RR{ + test.SOA("arvancloud.root. 300 IN SOA ns1.arvancloud.root. hostmaster.arvancloud.root. 1460498836 44 55 66 100"), + }, + }, + { + Qname: "arvancloud.root.", Qtype: dns.TypeSOA, + Rcode: dns.RcodeSuccess, + Answer: []dns.RR{ + test.SOA("arvancloud.root. 300 IN SOA ns1.arvancloud.root. hostmaster.arvancloud.root. 1460498836 44 55 66 100"), + }, + }, + { + Qname: "arvancloud.root.", Qtype: dns.TypeTXT, + Ns: []dns.RR{ + test.SOA("arvancloud.root. 300 IN SOA ns1.arvancloud.root. hostmaster.arvancloud.root. 1460498836 44 55 66 100"), + }, + }, }, }, -} + { + Name: "cache stale", + Description: "use stale data from cache when redis is not available", + Enabled: true, + Config: defaultConfig, + Initialize: func(testCase *TestCase) (handler *DnsRequestHandler, e error) { + testCase.Config.Redis.Connection.WaitForConnection = false + testCase.Config.CacheTimeout = 1 + return defaultInitialize(testCase) + }, + ApplyAndVerify: func(testCase *TestCase, handler *DnsRequestHandler, t *testing.T) { + tc := testCase.TestCases[0] + r := tc.Msg() + w := test.NewRecorder(&test.ResponseWriter{}) + state := NewRequestContext(w, r) + handler.HandleRequest(state) -func TestFindZone(t *testing.T) { - logger.Default = logger.NewLogger(&logger.LogConfig{}) + resp := w.Msg - h := NewHandler(&handlerTestConfig) - h.Redis.Del("*") - for i, zone := range findZoneZones { - h.Redis.SAdd("redins:zones", zone) - for _, cmd := range findZoneEntries[i] { - err := h.Redis.HSet("redins:zones:"+zone, cmd[0], cmd[1]) - if err != nil { - log.Printf("[ERROR] cannot connect to redis: %s", err) + if err := test.SortAndCheck(resp, tc); err != nil { + fmt.Println(err, tc.Qname, tc.Answer, resp.Answer) t.Fail() } - } - h.Redis.Set("redins:zones:"+zone+":config", lookupConfig[i]) - h.LoadZones() - } - for _, tc := range findZoneTests { - r := tc.Msg() - w := test.NewRecorder(&test.ResponseWriter{}) - state := request.Request{W: w, Req: r} - h.HandleRequest(&state) - - resp := w.Msg - if err := test.SortAndCheck(resp, tc); err != nil { - fmt.Println(tc.Qname, tc.Answer, resp.Answer) - t.Fail() - } - } -} + for i := 0; i < testCase.Config.Redis.Connection.MaxActiveConnections; i++ { + handler.Redis.Pool.Get() + } + time.Sleep(time.Duration(1200) * time.Millisecond) -var subsZone = "zone1.com." + r = tc.Msg() + w = test.NewRecorder(&test.ResponseWriter{}) + state = NewRequestContext(w, r) + handler.HandleRequest(state) -var subsEntries = [][]string{ - { - "www", - `{"a":{"ttl":300, "records":[{"ip":"1.1.1.1"}]}}`, + resp = w.Msg + if err := test.SortAndCheck(resp, tc); err != nil { + fmt.Println(err, tc.Qname, tc.Answer, resp.Answer) + t.Fail() + } + }, + Zones: []string{"stale.com."}, + ZoneConfigs: []string{""}, + Entries: [][][]string{ + { + {"www", + `{"a":{"ttl":300, "records":[{"ip":"3.3.3.1"}]}}`, + }, + }, + }, + TestCases: []test.Case{ + { + Qname: "www.stale.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("www.stale.com. 300 IN A 3.3.3.1"), + }, + }, + }, }, -} - -var subsTestCases = []test.Case{ { - Qname: "www.zone1.com", Qtype: dns.TypeA, - }, -} - -func TestSubscribeZones(t *testing.T) { - logger.Default = logger.NewLogger(&logger.LogConfig{}) - - var handlerTestConfig = HandlerConfig{ - MaxTtl: 300, - CacheTimeout: 1, - ZoneReload: 600, - Redis: uperdis.RedisConfig{ - Ip: "redis", - Port: 6379, - DB: 0, - Password: "", - Prefix: "test_", - Suffix: "_test", - ConnectTimeout: 0, - ReadTimeout: 0, - }, - Log: logger.LogConfig{ - Enable: false, - }, - Upstream: []UpstreamConfig{ - { - Ip: "1.1.1.1", - Port: 53, - Protocol: "udp", - Timeout: 1000, - }, - }, - GeoIp: GeoIpConfig{ - Enable: true, - CountryDB: "../geoCity.mmdb", - ASNDB: "../geoIsp.mmdb", + Name: "zone list update", + Description: "test zone list update", + Enabled: true, + Config: defaultConfig, + Initialize: func(testCase *TestCase) (handler *DnsRequestHandler, e error) { + logger.Default = logger.NewLogger(&logger.LogConfig{}, nil) + testCase.Config.ZoneReload = 1 + h := NewHandler(&testCase.Config) + _ = h.Redis.SetConfig("notify-keyspace-events", "AKE") + if err := h.Redis.Del("*"); err != nil { + return nil, err + } + for i, zone := range testCase.Zones { + if err := h.Redis.SAdd("redins:zones", zone); err != nil { + return nil, err + } + for _, cmd := range testCase.Entries[i] { + err := h.Redis.HSet("redins:zones:"+zone, cmd[0], cmd[1]) + if err != nil { + return nil, errors.New(fmt.Sprintf("[ERROR] cannot connect to redis: %s", err)) + } + } + if err := h.Redis.Set("redins:zones:"+zone+":config", testCase.ZoneConfigs[i]); err != nil { + return nil, err + } + } + h.LoadZones() + time.Sleep(time.Second) + return h, nil }, - } - - rd := uperdis.NewRedis(&handlerTestConfig.Redis) - rd.SetConfig("notify-keyspace-events", "AK") - time.Sleep(time.Second) + ApplyAndVerify: func(testCase *TestCase, handler *DnsRequestHandler, t *testing.T) { + { + _ = handler.Redis.SRem("redins:zones", testCase.Zones[0]) + time.Sleep(time.Millisecond * 1200) - h := NewHandler(&handlerTestConfig) - h.Redis.Del("*") - for _, cmd := range subsEntries { - err := h.Redis.HSet("redins:zones:"+subsZone, cmd[0], cmd[1]) - if err != nil { - log.Printf("[ERROR] cannot connect to redis: %s", err) - log.Println("1") - t.Fail() - } - } + tc := testCase.TestCases[0] + r := tc.Msg() + w := test.NewRecorder(&test.ResponseWriter{}) + state := NewRequestContext(w, r) + handler.HandleRequest(state) - h.Redis.SAdd("redins:zones", subsZone) - time.Sleep(time.Millisecond * 10) - tc := subsTestCases[0] - r := tc.Msg() - w := test.NewRecorder(&test.ResponseWriter{}) - state := request.Request{W: w, Req: r} - h.HandleRequest(&state) + resp := w.Msg - resp := w.Msg - if resp.Rcode != dns.RcodeSuccess { - fmt.Println("1") - t.Fail() - } + if err := test.SortAndCheck(resp, tc); err != nil { + fmt.Println("1", err, tc.Qname, tc.Answer, resp.Answer) + t.Fail() + } + } - h.Redis.SRem("redins:zones", subsZone) - time.Sleep(time.Millisecond * 1500) - tc = subsTestCases[0] - r = tc.Msg() - w = test.NewRecorder(&test.ResponseWriter{}) - state = request.Request{W: w, Req: r} - h.HandleRequest(&state) + { + _ = handler.Redis.SAdd("redins:zones", testCase.Zones[0]) + time.Sleep(time.Millisecond * 1200) - resp = w.Msg - if resp.Rcode != dns.RcodeNotAuth { - fmt.Println("2 : ", resp.Rcode) - t.Fail() - } -} + tc := testCase.TestCases[1] + r := tc.Msg() + w := test.NewRecorder(&test.ResponseWriter{}) + state := NewRequestContext(w, r) + handler.HandleRequest(state) -var cnameNoAuthZone = "auth.zon." + resp := w.Msg -var cnameNoAuthEntries = [][]string{ - {"w1", - `{"cname":{"ttl":300, "host":"w2.auth.zon."}}`, - }, - {"w2", - `{"cname":{"ttl":300, "host":"noauth.zon."}}`, + if err := test.SortAndCheck(resp, tc); err != nil { + fmt.Println("2", err, tc.Qname, tc.Answer, resp.Answer) + t.Fail() + } + } + }, + Zones: []string{"zone1.zon.", "zone2.zon."}, + ZoneConfigs: []string{"", ""}, + Entries: [][][]string{ + { + {"www", + `{"a":{"ttl":300, "records":[{"ip":"1.2.3.4"}]}}`, + }, + }, + { + {"www", + `{"a":{"ttl":300, "records":[{"ip":"2.3.4.5"}]}}`, + }, + }, + }, + TestCases: []test.Case{ + { + Qname: "www.zone1.zon", Qtype: dns.TypeA, + Rcode: dns.RcodeNotAuth, + }, + { + Qname: "www.zone1.zon.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("www.zone1.zon. 300 IN A 1.2.3.4"), + }, + }, + }, }, -} - -var cnameNoAuthTests = []test.Case{ { - Qname: "w1.auth.zon.", Qtype: dns.TypeA, - Answer: []dns.RR { - test.CNAME("w1.auth.zon. 300 IN CNAME w2.auth.zon."), - test.CNAME("w2.auth.zon. 300 IN CNAME noauth.zon."), - }, - Rcode: dns.RcodeSuccess, + Name: "IDN zones", + Description: "test zone names with IDN values (internationalized domain names)", + Enabled: true, + Config: defaultConfig, + Initialize: defaultInitialize, + ApplyAndVerify: defaultApplyAndVerify, + Zones: []string{"ουτοπία.δπθ.gr.", "ascii.com."}, + ZoneConfigs: []string{"", ""}, + Entries: [][][]string{ + { + {"@", + `{"a":{"ttl":300, "records":[{"ip":"1.2.3.4"}]}}`, + }, + {"ουτοπία", + `{"a":{"ttl":300, "records":[{"ip":"2.3.4.5"}]}}`, + }, + }, + { + {"@", + `{"aname":{"location":"ουτοπία.δπθ.gr."}}`, + }, + {"www", + `{"cname":{"ttl":300, "host":"ουτοπία.δπθ.gr."}}`, + }, + {"ουτοπία", + `{"a":{"ttl":300, "records":[{"ip":"1.2.3.4"}]}}`, + }, + }, + }, + TestCases: []test.Case{ + { + Qname: "ουτοπία.δπθ.gr.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("ουτοπία.δπθ.gr. 300 IN A 1.2.3.4"), + }, + }, + { + Qname: "ουτοπία.ουτοπία.δπθ.gr.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("ουτοπία.ουτοπία.δπθ.gr. 300 IN A 2.3.4.5"), + }, + }, + { + Qname: "ascii.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("ascii.com. 300 IN A 1.2.3.4"), + }, + }, + { + Qname: "www.ascii.com.", Qtype: dns.TypeCNAME, + Answer: []dns.RR{ + test.CNAME("www.ascii.com. 300 IN CNAME ουτοπία.δπθ.gr."), + }, + }, + { + Qname: "ουτοπία.ascii.com.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("ουτοπία.ascii.com. 300 IN A 1.2.3.4"), + }, + }, + }, }, } -func TestCNameNoAuth(t *testing.T) { - logger.Default = logger.NewLogger(&logger.LogConfig{}) +func center(s string, w int) string { + return fmt.Sprintf("%[1]*s", -w, fmt.Sprintf("%[1]*s", (w+len(s))/2, s)) +} - h := NewHandler(&handlerTestConfig) - h.Redis.Del("*") - h.Redis.SAdd("redins:zones", cnameNoAuthZone) - h.Redis.Set("redins:zones:" + cnameNoAuthZone + ":config", "{\"cname_flattening\": false}") - for _, cmd := range cnameNoAuthEntries { - err := h.Redis.HSet("redins:zones:"+cnameNoAuthZone, cmd[0], cmd[1]) - if err != nil { - log.Printf("[ERROR] cannot connect to redis: %s", err) - t.Fail() +func TestAll(t *testing.T) { + for _, testCase := range testCases { + if !testCase.Enabled { + continue } - } - - h.LoadZones() - for j, tc := range cnameNoAuthTests { - - r := tc.Msg() - w := test.NewRecorder(&test.ResponseWriter{}) - state := request.Request{W: w, Req: r} - h.HandleRequest(&state) - - resp := w.Msg - - fmt.Println(j, tc.Qname, tc.Answer, resp.Answer) - if err := test.SortAndCheck(resp, tc); err != nil { + fmt.Println(">>> ", center(testCase.Name, 70), " <<<") + fmt.Println(testCase.Description) + fmt.Println(strings.Repeat("-", 80)) + h, err := testCase.Initialize(testCase) + if err != nil { + fmt.Println("initialization failed : ", err) t.Fail() - fmt.Println(err) } + testCase.ApplyAndVerify(testCase, h, t) + fmt.Println(strings.Repeat("-", 80)) } } diff --git a/handler/healthcheck.go b/handler/healthcheck.go index 0b461c0871c6ff2a3cd32b33c97f453a911dea28..9c25c0eed76b9beca60da9a35bbb99628effbe79 100644 --- a/handler/healthcheck.go +++ b/handler/healthcheck.go @@ -2,8 +2,8 @@ package handler import ( "crypto/tls" - "encoding/json" "fmt" + "github.com/json-iterator/go" "net" "net/http" "strings" @@ -119,10 +119,10 @@ func httpCheck(url string, host string, timeout time.Duration) error { // FIXME: ping check is not working properly func pingCheck(ip string, timeout time.Duration) error { c, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0") - c.SetDeadline(time.Now().Add(timeout)) if err != nil { return err } + c.SetDeadline(time.Now().Add(timeout)) defer c.Close() id := int(binary.BigEndian.Uint32(net.ParseIP(ip))) @@ -160,13 +160,13 @@ func pingCheck(ip string, timeout time.Duration) error { } type HealthcheckConfig struct { - Enable bool `json:"enable,omitempty"` - MaxRequests int `json:"max_requests,omitempty"` - MaxPendingRequests int `json:"max_pending_requests,omitempty"` - UpdateInterval int `json:"update_interval,omitempty"` - CheckInterval int `json:"check_interval,omitempty"` - RedisStatusServer uperdis.RedisConfig `json:"redis,omitempty"` - Log logger.LogConfig `json:"log,omitempty"` + Enable bool `json:"enable"` + MaxRequests int `json:"max_requests"` + MaxPendingRequests int `json:"max_pending_requests"` + UpdateInterval int `json:"update_interval"` + CheckInterval int `json:"check_interval"` + RedisStatusServer uperdis.RedisConfig `json:"redis"` + Log logger.LogConfig `json:"log"` } func NewHealthcheck(config *HealthcheckConfig, redisConfigServer *uperdis.Redis) *Healthcheck { @@ -187,7 +187,7 @@ func NewHealthcheck(config *HealthcheckConfig, redisConfigServer *uperdis.Redis) for i := 0; i < config.MaxRequests; i++ { h.dispatcher.AddWorker(HandleHealthCheck(h)) } - h.logger = logger.NewLogger(&config.Log) + h.logger = logger.NewLogger(&config.Log, nil) h.quit = make(chan struct{}, 1) } @@ -240,7 +240,7 @@ func (h *Healthcheck) loadItem(key string) *HealthCheckItem { logger.Default.Errorf("cannot load item %s : %s", key, err) return nil } - json.Unmarshal([]byte(itemStr), item) + jsoniter.Unmarshal([]byte(itemStr), item) if item.DownCount > 0 { item.DownCount = -item.DownCount } @@ -249,7 +249,7 @@ func (h *Healthcheck) loadItem(key string) *HealthCheckItem { func (h *Healthcheck) storeItem(item *HealthCheckItem) { key := item.Host + ":" + item.Ip - itemStr, err := json.Marshal(item) + itemStr, err := jsoniter.Marshal(item) if err != nil { logger.Default.Errorf("cannot marshal item to json : %s", err) return @@ -265,7 +265,7 @@ func (h *Healthcheck) getDomainId(zone string) string { logger.Default.Errorf("cannot load zone %s config : %s", zone, err) } if len(val) > 0 { - err := json.Unmarshal([]byte(val), &cfg) + err := jsoniter.Unmarshal([]byte(val), &cfg) if err != nil { logger.Default.Errorf("cannot parse zone config : %s", err) } @@ -281,6 +281,7 @@ func (h *Healthcheck) Start() { go h.Transfer() + ticker := time.NewTicker(h.checkInterval) for { itemKeys, err := h.redisStatusServer.GetKeys("redins:healthcheck:*") if err != nil { @@ -288,9 +289,10 @@ func (h *Healthcheck) Start() { } select { case <-h.quit: + ticker.Stop() h.quitWG.Done() return - case <-time.After(h.checkInterval): + case <-ticker.C: for i := range itemKeys { itemKey := strings.TrimPrefix(itemKeys[i], "redins:healthcheck:") item := h.loadItem(itemKey) @@ -420,7 +422,7 @@ func (h *Healthcheck) Transfer() { Enable: false, } record.AAAA = record.A - err = json.Unmarshal([]byte(recordStr), record) + err = jsoniter.Unmarshal([]byte(recordStr), record) if err != nil { logger.Default.Errorf("cannot parse json : zone -> %s, location -> %s, %s -> %s", domain, subdomain, recordStr, err) continue diff --git a/handler/healthcheck_test.go b/handler/healthcheck_test.go index bd0bd6791f8d919c669f47729374de165ea2a540..0ed52d0a106501919cc2bf34efd7e510e7af13f4 100644 --- a/handler/healthcheck_test.go +++ b/handler/healthcheck_test.go @@ -79,14 +79,21 @@ var config = HealthcheckConfig{ UpdateInterval: 600, CheckInterval: 600, RedisStatusServer: uperdis.RedisConfig{ - Ip: "redis", - Port: 6379, - DB: 0, - Password: "", - Prefix: "healthcheck_", - Suffix: "_healthcheck", - ConnectTimeout: 0, - ReadTimeout: 0, + Address: "redis:6379", + Net: "tcp", + DB: 0, + Password: "", + Prefix: "healthcheck_", + Suffix: "_healthcheck", + Connection: uperdis.RedisConnectionConfig{ + MaxIdleConnections: 10, + MaxActiveConnections: 10, + ConnectTimeout: 500, + ReadTimeout: 500, + IdleKeepAlive: 30, + MaxKeepAlive: 0, + WaitForConnection: true, + }, }, Log: logger.LogConfig{ Enable: true, @@ -95,19 +102,26 @@ var config = HealthcheckConfig{ } var configRedisConf = uperdis.RedisConfig{ - Ip: "redis", - Port: 6379, - DB: 0, - Password: "", - Prefix: "hcconfig_", - Suffix: "_hcconfig", - ConnectTimeout: 0, - ReadTimeout: 0, + Address: "redis:6379", + Net: "tcp", + DB: 0, + Password: "", + Prefix: "hcconfig_", + Suffix: "_hcconfig", + Connection: uperdis.RedisConnectionConfig{ + MaxIdleConnections: 10, + MaxActiveConnections: 10, + ConnectTimeout: 500, + ReadTimeout: 500, + IdleKeepAlive: 30, + MaxKeepAlive: 0, + WaitForConnection: true, + }, } func TestGet(t *testing.T) { log.Println("TestGet") - logger.Default = logger.NewLogger(&logger.LogConfig{}) + logger.Default = logger.NewLogger(&logger.LogConfig{}, nil) configRedis := uperdis.NewRedis(&configRedisConf) h := NewHealthcheck(&config, configRedis) @@ -131,7 +145,7 @@ func TestGet(t *testing.T) { func TestFilter(t *testing.T) { log.Println("TestFilter") - logger.Default = logger.NewLogger(&logger.LogConfig{}) + logger.Default = logger.NewLogger(&logger.LogConfig{}, nil) configRedis := uperdis.NewRedis(&configRedisConf) h := NewHealthcheck(&config, configRedis) @@ -267,7 +281,7 @@ func TestFilter(t *testing.T) { func TestSet(t *testing.T) { log.Println("TestSet") - logger.Default = logger.NewLogger(&logger.LogConfig{}) + logger.Default = logger.NewLogger(&logger.LogConfig{}, nil) configRedis := uperdis.NewRedis(&configRedisConf) h := NewHealthcheck(&config, configRedis) @@ -294,7 +308,7 @@ func TestSet(t *testing.T) { func TestTransfer(t *testing.T) { log.Printf("TestTransfer") - logger.Default = logger.NewLogger(&logger.LogConfig{}) + logger.Default = logger.NewLogger(&logger.LogConfig{}, nil) configRedis := uperdis.NewRedis(&configRedisConf) h := NewHealthcheck(&config, configRedis) @@ -359,14 +373,12 @@ var healthcheckConfig = HealthcheckConfig{ TimeFormat: "2006-01-02 15:04:05", }, RedisStatusServer: uperdis.RedisConfig{ - Ip: "redis", - Port: 6379, - DB: 0, - Password: "", - Prefix: "hcstattest_", - Suffix: "_hcstattest", - ConnectTimeout: 0, - ReadTimeout: 0, + Address: "redis:6379", + Net: "tcp", + DB: 0, + Password: "", + Prefix: "hcstattest_", + Suffix: "_hcstattest", }, CheckInterval: 1, UpdateInterval: 200, @@ -377,7 +389,7 @@ var healthcheckConfig = HealthcheckConfig{ var hcConfig = `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.google.com.","ns":"ns1.google.com.","refresh":44,"retry":55,"expire":66}}` var hcEntries = [][]string{ {"www", - `{"a":{"ttl":300, "health_check":{"enable":true,"protocol":"http","uri":"","port":80, "up_count": 3, "down_count": -3, "timeout":1000}, "records":[{"ip":"172.217.17.238"}]}}`, + `{"a":{"ttl":300, "health_check":{"enable":true,"protocol":"http","uri":"","port":80, "up_count": 3, "down_count": -3, "timeout":1000}, "records":[{"ip":"172.217.17.78"}]}}`, }, {"ddd", `{"a":{"ttl":300, "health_check":{"enable":true,"protocol":"http","uri":"/uri2","port":80, "up_count": 3, "down_count": -3, "timeout":1000}, "records":[{"ip":"3.3.3.3"}]}}`, @@ -394,7 +406,7 @@ var hcEntries = [][]string{ func TestHealthCheck(t *testing.T) { log.Println("TestHealthCheck") - logger.Default = logger.NewLogger(&logger.LogConfig{Enable: true, Target: "stdout", Format: "text"}) + logger.Default = logger.NewLogger(&logger.LogConfig{Enable: true, Target: "stdout", Format: "text"}, nil) configRedis := uperdis.NewRedis(&configRedisConf) hc := NewHealthcheck(&healthcheckConfig, configRedis) @@ -407,8 +419,8 @@ func TestHealthCheck(t *testing.T) { configRedis.Set("redins:zones:google.com.:config", hcConfig) go hc.Start() - time.Sleep(10 * time.Second) - h1 := hc.getStatus("www.google.com.", net.ParseIP("172.217.17.238")) + time.Sleep(12 * time.Second) + h1 := hc.getStatus("www.google.com.", net.ParseIP("172.217.17.78")) h2 := hc.getStatus("ddd.google.com.", net.ParseIP("3.3.3.3")) /* h3 := hc.getStatus("y.google.com.", net.ParseIP("4.2.2.4")) @@ -439,14 +451,12 @@ func TestExpire(t *testing.T) { UpdateInterval: 1, CheckInterval: 600, RedisStatusServer: uperdis.RedisConfig{ - Ip: "redis", - Port: 6379, - DB: 0, - Password: "", - Prefix: "healthcheck1_", - Suffix: "_healthcheck1", - ConnectTimeout: 0, - ReadTimeout: 0, + Address: "redis:6379", + Net: "tcp", + DB: 0, + Password: "", + Prefix: "healthcheck1_", + Suffix: "_healthcheck1", }, Log: logger.LogConfig{ Enable: true, @@ -455,7 +465,7 @@ func TestExpire(t *testing.T) { } log.Printf("TestExpire") - logger.Default = logger.NewLogger(&logger.LogConfig{}) + logger.Default = logger.NewLogger(&logger.LogConfig{}, nil) configRedis := uperdis.NewRedis(&configRedisConf) h := NewHealthcheck(&config, configRedis) diff --git a/handler/log_test.go b/handler/log_test.go new file mode 100644 index 0000000000000000000000000000000000000000..56cebd0318bd0dd2463290239bf30d29da7fc93c --- /dev/null +++ b/handler/log_test.go @@ -0,0 +1,337 @@ +package handler + +import ( + "arvancloud/redins/handler/logformat" + "arvancloud/redins/test" + "bytes" + "fmt" + "github.com/hawell/logger" + "github.com/hawell/uperdis" + jsoniter "github.com/json-iterator/go" + "github.com/miekg/dns" + "io/ioutil" + "log" + "net" + "os" + "testing" + "time" + capnp "zombiezen.com/go/capnproto2" +) + +var logTestConfig = DnsRequestHandlerConfig{ + MaxTtl: 300, + CacheTimeout: 60, + ZoneReload: 600, + LogSourceLocation: true, + Redis: uperdis.RedisConfig{ + Address: "redis:6379", + Net: "tcp", + DB: 0, + Password: "", + Prefix: "test_", + Suffix: "_test", + }, + Log: logger.LogConfig{ + Enable: true, + Path: "/tmp/test.log", + Format: "json", + Level: "info", + Target: "file", + Kafka: logger.KafkaConfig{ + Enable: false, + Compression: "none", + Brokers: []string{"127.0.0.1:9093"}, + Topic: "redins", + }, + }, + Upstream: []UpstreamConfig{ + { + Ip: "1.1.1.1", + Port: 53, + Protocol: "udp", + Timeout: 1000, + }, + }, + GeoIp: GeoIpConfig{ + Enable: true, + CountryDB: "../geoCity.mmdb", + ASNDB: "../geoIsp.mmdb", + }, +} + +var logZone = "zone.log." + +var logZoneConfig = `{"soa":{"ttl":300, "minttl":100, "mbox":"hostmaster.zone.log.","ns":"ns1.zone.log.","refresh":44,"retry":55,"expire":66},"domain_id":"d5cb15ec-cbfa-11e9-8ea5-9baaa1851180"}` + +var logZoneEntries = [][]string{ + {"www", + `{"a":{"ttl":300, "records":[{"ip":"127.0.0.1", "country":""}],"filter":{"count":"multi","order":"none","geo_filter":"none"}}}`, + }, + {"www2", + `{"a":{"ttl":300, "records":[{"ip":"127.0.0.1", "country":""}],"filter":{"count":"multi","order":"none","geo_filter":"none"}}}`, + }, +} + +func TestJsonLog(t *testing.T) { + logger.Default = logger.NewLogger(&logger.LogConfig{}, nil) + os.Remove("/tmp/test.log") + + logTestConfig.Log.Format = "json" + h := NewHandler(&logTestConfig) + h.Redis.Del("*") + h.Redis.SAdd("redins:zones", logZone) + for _, cmd := range logZoneEntries { + err := h.Redis.HSet("redins:zones:"+logZone, cmd[0], cmd[1]) + if err != nil { + log.Printf("[ERROR] cannot connect to redis: %s", err) + t.Fail() + } + } + h.Redis.Set("redins:zones:"+logZone+":config", logZoneConfig) + h.LoadZones() + tc := test.Case{ + Qname: "www.zone.log", + Qtype: dns.TypeA, + } + r := tc.Msg() + w := test.NewRecorder(&test.ResponseWriter{}) + state := NewRequestContext(w, r) + h.HandleRequest(state) + time.Sleep(time.Millisecond * 100) + b, _ := ioutil.ReadFile("/tmp/test.log") + m1 := map[string]interface{}{ + "client_subnet": "", + "domain_uuid": "d5cb15ec-cbfa-11e9-8ea5-9baaa1851180", + "level": "info", + "log_type": "request", + "msg": "dns request", + "record": "www.zone.log.", + "response_code": float64(0), + "source_ip": "10.240.0.1", + "type": "A", + } + m2 := make(map[string]interface{}) + jsoniter.Unmarshal(b, &m2) + for key := range m1 { + if m1[key] != m2[key] { + fmt.Println(key) + fmt.Printf("%v %T\n", m1[key], m1[key]) + fmt.Printf("%v %T\n", m2[key], m2[key]) + t.Fail() + } + } +} + +func TestCapnpLog(t *testing.T) { + logger.Default = logger.NewLogger(&logger.LogConfig{}, nil) + os.Remove("/tmp/test.log") + + logTestConfig.Log.Format = "capnp_request" + h := NewHandler(&logTestConfig) + h.Redis.Del("*") + h.Redis.SAdd("redins:zones", logZone) + for _, cmd := range logZoneEntries { + err := h.Redis.HSet("redins:zones:"+logZone, cmd[0], cmd[1]) + if err != nil { + log.Printf("[ERROR] cannot connect to redis: %s", err) + t.Fail() + } + } + h.Redis.Set("redins:zones:"+logZone+":config", logZoneConfig) + h.LoadZones() + tc := test.Case{ + Qname: "www2.zone.log", + Qtype: dns.TypeA, + } + r := tc.Msg() + w := test.NewRecorder(&test.ResponseWriter{}) + state := NewRequestContext(w, r) + h.HandleRequest(state) + h.HandleRequest(state) + time.Sleep(time.Millisecond * 100) + logFile, err := os.OpenFile("/tmp/test.log", os.O_RDONLY, 0666) + if err != nil { + fmt.Println(err) + t.Fail() + } + decoder := capnp.NewDecoder(logFile) + + for i := 0; i < 2; i++ { + msg, err := decoder.Decode() + if err != nil { + fmt.Println(err) + t.Fail() + } + requestLog, err := logformat.ReadRootRequestLog(msg) + if err != nil { + fmt.Println(err) + t.Fail() + } + record, err := requestLog.Record() + if err != nil { + fmt.Println(err) + t.Fail() + } + if record != "www2.zone.log." { + t.Fail() + } + } +} + +func TestCapnpLogNotAuth(t *testing.T) { + logger.Default = logger.NewLogger(&logger.LogConfig{}, nil) + os.Remove("/tmp/test.log") + + logTestConfig.Log.Format = "capnp_request" + h := NewHandler(&logTestConfig) + h.Redis.Del("*") + h.LoadZones() + tc := test.Case{ + Qname: "www2.zone.log", + Qtype: dns.TypeA, + } + r := tc.Msg() + w := test.NewRecorder(&test.ResponseWriter{}) + state := NewRequestContext(w, r) + h.HandleRequest(state) + time.Sleep(time.Millisecond * 100) + logFile, err := os.OpenFile("/tmp/test.log", os.O_RDONLY, 0666) + if err != nil { + fmt.Println(err) + t.Fail() + } + decoder := capnp.NewDecoder(logFile) + + msg, err := decoder.Decode() + if err != nil { + fmt.Println(err) + t.Fail() + } + requestLog, err := logformat.ReadRootRequestLog(msg) + if err != nil { + fmt.Println(err) + t.Fail() + } + resp := requestLog.Responsecode() + if resp != dns.RcodeNotAuth { + t.Fail() + } +} + +func TestKafkaCapnpLog(t *testing.T) { + t.Skip("skip kafka test") + + logger.Default = logger.NewLogger(&logger.LogConfig{}, nil) + os.Remove("/tmp/test.log") + + logTestConfig.Log.Format = "text" + logTestConfig.Log.Kafka.Enable = true + logTestConfig.Log.Kafka.Format = "capnp_request" + h := NewHandler(&logTestConfig) + h.Redis.Del("*") + h.Redis.SAdd("redins:zones", logZone) + for _, cmd := range logZoneEntries { + err := h.Redis.HSet("redins:zones:"+logZone, cmd[0], cmd[1]) + if err != nil { + log.Printf("[ERROR] cannot connect to redis: %s", err) + t.Fail() + } + } + opt := &dns.OPT{ + Hdr: dns.RR_Header{Name: ".", Rrtype: dns.TypeOPT, Class: dns.ClassANY, Rdlength: 0, Ttl: 300}, + Option: []dns.EDNS0{ + &dns.EDNS0_SUBNET{ + Address: net.ParseIP("94.76.229.204"), + Code: dns.EDNS0SUBNET, + Family: 1, + SourceNetmask: 32, + SourceScope: 0, + }, + }, + } + h.Redis.Set("redins:zones:"+logZone+":config", logZoneConfig) + h.LoadZones() + tc := test.Case{ + Qname: "www2.zone.log", + Qtype: dns.TypeA, + } + r := tc.Msg() + r.Extra = append(r.Extra, opt) + w := test.NewRecorder(&test.ResponseWriter{}) + state := NewRequestContext(w, r) + h.HandleRequest(state) + time.Sleep(time.Second) +} + +func TestUdpCapnpLog(t *testing.T) { + go func() { + pc, err := net.ListenPacket("udp", "localhost:9090") + if err != nil { + fmt.Println(err) + t.Fail() + return + } + for i := 0; i < 2; i++ { + buffer := make([]byte, 1024) + n, _, err := pc.ReadFrom(buffer) + fmt.Println("n = ", n) + if err != nil { + fmt.Println(err) + t.Fail() + return + } + r := bytes.NewReader(buffer) + decoder := capnp.NewDecoder(r) + + msg, err := decoder.Decode() + if err != nil { + fmt.Println(err) + t.Fail() + } + requestLog, err := logformat.ReadRootRequestLog(msg) + if err != nil { + fmt.Println(err) + t.Fail() + } + fmt.Println(requestLog) + record, err := requestLog.Record() + if err != nil { + fmt.Println(err) + t.Fail() + } + if record != "www2.zone.log." { + t.Fail() + } + } + pc.Close() + }() + + logger.Default = logger.NewLogger(&logger.LogConfig{}, nil) + os.Remove("/tmp/test.log") + + logTestConfig.Log.Format = "capnp_request" + logTestConfig.Log.Target = "udp" + logTestConfig.Log.Path = "localhost:9090" + h := NewHandler(&logTestConfig) + h.Redis.Del("*") + h.Redis.SAdd("redins:zones", logZone) + for _, cmd := range logZoneEntries { + err := h.Redis.HSet("redins:zones:"+logZone, cmd[0], cmd[1]) + if err != nil { + log.Printf("[ERROR] cannot connect to redis: %s", err) + t.Fail() + } + } + h.Redis.Set("redins:zones:"+logZone+":config", logZoneConfig) + h.LoadZones() + tc := test.Case{ + Qname: "www2.zone.log", + Qtype: dns.TypeA, + } + r := tc.Msg() + w := test.NewRecorder(&test.ResponseWriter{}) + state := NewRequestContext(w, r) + h.HandleRequest(state) + h.HandleRequest(state) + time.Sleep(time.Millisecond * 100) +} diff --git a/handler/logformat/logformat.go b/handler/logformat/logformat.go new file mode 100644 index 0000000000000000000000000000000000000000..a7ea80a49ac9807c207c6ddd8a67a66c706ce9dd --- /dev/null +++ b/handler/logformat/logformat.go @@ -0,0 +1,56 @@ +package logformat + +import ( + "bytes" + "github.com/sirupsen/logrus" + capnp "zombiezen.com/go/capnproto2" +) + +type CapnpRequestLogFormatter struct{} + +func (f *CapnpRequestLogFormatter) Format(entry *logrus.Entry) ([]byte, error) { + msg, seg, err := capnp.NewMessage(capnp.SingleSegment(nil)) + if err != nil { + return []byte{}, err + } + requestLog, err := NewRootRequestLog(seg) + if err != nil { + return []byte{}, err + } + requestLog.SetTimestamp(uint64(entry.Time.Unix())) + if err = requestLog.SetUuid(entry.Data["domain_uuid"].(string)); err != nil { + return []byte{}, err + } + if err = requestLog.SetRecord(entry.Data["record"].(string)); err != nil { + return []byte{}, err + } + if err = requestLog.SetType(entry.Data["type"].(string)); err != nil { + return []byte{}, err + } + requestLog.SetResponsecode(uint16(entry.Data["response_code"].(int))) + requestLog.SetProcesstime(uint16(entry.Data["process_time"].(int64))) + + clientSubnet, ok := entry.Data["client_subnet"] + if ok { + if err = requestLog.SetIp(clientSubnet.(string)); err != nil { + return []byte{}, err + } + } + sourceCountry, ok := entry.Data["source_country"] + if ok { + if err = requestLog.SetCountry(sourceCountry.(string)); err != nil { + return []byte{}, err + } + } + sourceAsn, ok := entry.Data["source_asn"] + if ok { + requestLog.SetAsn(uint32(sourceAsn.(uint))) + } + b := &bytes.Buffer{} + err = capnp.NewEncoder(b).Encode(msg) + if err != nil { + return []byte{}, err + } + + return b.Bytes(), err +} diff --git a/handler/logformat/request.capnp b/handler/logformat/request.capnp new file mode 100644 index 0000000000000000000000000000000000000000..1575fa1c4eb525d0caf992419871ac1a47c503c5 --- /dev/null +++ b/handler/logformat/request.capnp @@ -0,0 +1,18 @@ +using Go = import "/go.capnp"; + +@0x8d8d9fbdbf80710e; + +$Go.package("logformat"); +$Go.import("arvancloud/redins/handler/logformat"); + +struct RequestLog { + timestamp @0 :UInt64; + uuid @1 :Text; + record @2 :Text; + type @3 :Text; + ip @4 :Text; + country @5 :Text; + asn @6 :UInt32; + responsecode @7 :UInt16; + processtime @8 :UInt16; +} \ No newline at end of file diff --git a/handler/logformat/request.capnp.go b/handler/logformat/request.capnp.go new file mode 100644 index 0000000000000000000000000000000000000000..f32aac654b4bb78877f78361b582ae67bc83e9a1 --- /dev/null +++ b/handler/logformat/request.capnp.go @@ -0,0 +1,214 @@ +// Code generated by capnpc-go. DO NOT EDIT. + +package logformat + +import ( + capnp "zombiezen.com/go/capnproto2" + text "zombiezen.com/go/capnproto2/encoding/text" + schemas "zombiezen.com/go/capnproto2/schemas" +) + +type RequestLog struct{ capnp.Struct } + +// RequestLog_TypeID is the unique identifier for the type RequestLog. +const RequestLog_TypeID = 0xc3dd579d38a573e0 + +func NewRequestLog(s *capnp.Segment) (RequestLog, error) { + st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 16, PointerCount: 5}) + return RequestLog{st}, err +} + +func NewRootRequestLog(s *capnp.Segment) (RequestLog, error) { + st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 16, PointerCount: 5}) + return RequestLog{st}, err +} + +func ReadRootRequestLog(msg *capnp.Message) (RequestLog, error) { + root, err := msg.RootPtr() + return RequestLog{root.Struct()}, err +} + +func (s RequestLog) String() string { + str, _ := text.Marshal(0xc3dd579d38a573e0, s.Struct) + return str +} + +func (s RequestLog) Timestamp() uint64 { + return s.Struct.Uint64(0) +} + +func (s RequestLog) SetTimestamp(v uint64) { + s.Struct.SetUint64(0, v) +} + +func (s RequestLog) Uuid() (string, error) { + p, err := s.Struct.Ptr(0) + return p.Text(), err +} + +func (s RequestLog) HasUuid() bool { + p, err := s.Struct.Ptr(0) + return p.IsValid() || err != nil +} + +func (s RequestLog) UuidBytes() ([]byte, error) { + p, err := s.Struct.Ptr(0) + return p.TextBytes(), err +} + +func (s RequestLog) SetUuid(v string) error { + return s.Struct.SetText(0, v) +} + +func (s RequestLog) Record() (string, error) { + p, err := s.Struct.Ptr(1) + return p.Text(), err +} + +func (s RequestLog) HasRecord() bool { + p, err := s.Struct.Ptr(1) + return p.IsValid() || err != nil +} + +func (s RequestLog) RecordBytes() ([]byte, error) { + p, err := s.Struct.Ptr(1) + return p.TextBytes(), err +} + +func (s RequestLog) SetRecord(v string) error { + return s.Struct.SetText(1, v) +} + +func (s RequestLog) Type() (string, error) { + p, err := s.Struct.Ptr(2) + return p.Text(), err +} + +func (s RequestLog) HasType() bool { + p, err := s.Struct.Ptr(2) + return p.IsValid() || err != nil +} + +func (s RequestLog) TypeBytes() ([]byte, error) { + p, err := s.Struct.Ptr(2) + return p.TextBytes(), err +} + +func (s RequestLog) SetType(v string) error { + return s.Struct.SetText(2, v) +} + +func (s RequestLog) Ip() (string, error) { + p, err := s.Struct.Ptr(3) + return p.Text(), err +} + +func (s RequestLog) HasIp() bool { + p, err := s.Struct.Ptr(3) + return p.IsValid() || err != nil +} + +func (s RequestLog) IpBytes() ([]byte, error) { + p, err := s.Struct.Ptr(3) + return p.TextBytes(), err +} + +func (s RequestLog) SetIp(v string) error { + return s.Struct.SetText(3, v) +} + +func (s RequestLog) Country() (string, error) { + p, err := s.Struct.Ptr(4) + return p.Text(), err +} + +func (s RequestLog) HasCountry() bool { + p, err := s.Struct.Ptr(4) + return p.IsValid() || err != nil +} + +func (s RequestLog) CountryBytes() ([]byte, error) { + p, err := s.Struct.Ptr(4) + return p.TextBytes(), err +} + +func (s RequestLog) SetCountry(v string) error { + return s.Struct.SetText(4, v) +} + +func (s RequestLog) Asn() uint32 { + return s.Struct.Uint32(8) +} + +func (s RequestLog) SetAsn(v uint32) { + s.Struct.SetUint32(8, v) +} + +func (s RequestLog) Responsecode() uint16 { + return s.Struct.Uint16(12) +} + +func (s RequestLog) SetResponsecode(v uint16) { + s.Struct.SetUint16(12, v) +} + +func (s RequestLog) Processtime() uint16 { + return s.Struct.Uint16(14) +} + +func (s RequestLog) SetProcesstime(v uint16) { + s.Struct.SetUint16(14, v) +} + +// RequestLog_List is a list of RequestLog. +type RequestLog_List struct{ capnp.List } + +// NewRequestLog creates a new list of RequestLog. +func NewRequestLog_List(s *capnp.Segment, sz int32) (RequestLog_List, error) { + l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 16, PointerCount: 5}, sz) + return RequestLog_List{l}, err +} + +func (s RequestLog_List) At(i int) RequestLog { return RequestLog{s.List.Struct(i)} } + +func (s RequestLog_List) Set(i int, v RequestLog) error { return s.List.SetStruct(i, v.Struct) } + +func (s RequestLog_List) String() string { + str, _ := text.MarshalList(0xc3dd579d38a573e0, s.List) + return str +} + +// RequestLog_Promise is a wrapper for a RequestLog promised by a client call. +type RequestLog_Promise struct{ *capnp.Pipeline } + +func (p RequestLog_Promise) Struct() (RequestLog, error) { + s, err := p.Pipeline.Struct() + return RequestLog{s}, err +} + +const schema_8d8d9fbdbf80710e = "x\xda<\xca\xb1\xca\xd3P\x00\xc5\xf1s\xeeMrS" + + "\x88mC\xae N\"N\x82B7\xe9\xa28;x" + + "\x93\xc1\xb9\xa6\x17\x89\xd0$\xcdM\x86N\xfa\x02}\x04" + + "q\xf21\xa4\x83\xb8(\x0e>\x80\xe0\x03(8(T" + + "\xa8D.\x1f\xedv\xfe?\xce\xfc\xf3#\xb1\x08\x0f\x04" + + "\x8c\x0e\xa3\xf1\xbb{\xf7\xe0\xcd\xb3o\x1f`\xa6\x14\xe3" + + "t\xfb\xfa\xf0\xfe\xed~\x8f0T@\xfa\xf1S\xfaU" + + "\x01\x8b/#qo\xec\xecv\xb0\xae\xbf/\xcaU[" + + "\xb7\xcb\xfc*\x9f4/\x80\xa7\xa4\xb9#\x03 \x90" + + "\xfe\xcc\x01\xf3C\xd2\x1c\x05IMo\x7f\xee\x02\xe6\x97" + + "\xa49\x09\xa6\x82\x9a\x02H\xff.\x01\xf3[2\xa7`" + + "*\x85\xa6\x04\xd2\x7f\xfey\x94,\x02\xaf\x81\xd4\x0c\x80" + + "\x8c\xbc\x09\x98\x93d\x11{\x0e\x03\xcd\x10\xc8B>\x06" + + "rJ\x16\x89\xe7HhF@6\xe1m\xa0\x08\xbc\xcf" + + "\xbd\xabHS\x01\xd95\xbe\x04\x8a\xc4\xfb\x0d\xef\xb1\xd2" + + "\x8c\x81\xec:\x9f\x03\x85\xf6~\x8b\x82c_m\xac\xeb" + + "W\x1b\xb0\xe5\x04\x82\x13p6\x0c\xd5\x9a\x09\x04\x13\xf0" + + "ag\xcb\xa6\xbb\xe4\xac\xdf\xb5\xf6\x1c\xb2j\xcf\xf3U" + + "\xd9\x0cu\xdf\xed\xce\xadV\xaef\x0c\xc1\x18\x1c;\xeb" + + "\xda\xa6v\x16\xb3\xb2Y[*\x08*pl\xbb\xa6\xb4" + + "\xce\xf5P\xd5\xe6\xa2\xff\x03\x00\x00\xff\xffS&Q;" + +func init() { + schemas.Register(schema_8d8d9fbdbf80710e, + 0xc3dd579d38a573e0) +} diff --git a/handler/request.go b/handler/request.go new file mode 100644 index 0000000000000000000000000000000000000000..3108e38d4cb0cb2805baf14edf80adb8911a43e1 --- /dev/null +++ b/handler/request.go @@ -0,0 +1,106 @@ +package handler + +import ( + "github.com/coredns/coredns/request" + "github.com/miekg/dns" + "net" + "strings" + "time" +) + +type RequestContext struct { + request.Request + StartTime time.Time + LogData map[string]interface{} + Auth bool + Answer []dns.RR + Authority []dns.RR + Additional []dns.RR + + SourceIp net.IP + SourceSubnet string + + name string +} + +func NewRequestContext(w dns.ResponseWriter, r *dns.Msg) *RequestContext { + context := &RequestContext{ + Request: request.Request{ + Req: r, + W: w, + Zone: "", + }, + StartTime: time.Now(), + Auth: true, + name: "", + } + context.SourceIp = context.sourceIp() + context.SourceSubnet = context.sourceSubnet() + context.LogData = map[string]interface{}{ + "source_ip": context.SourceIp, + "record": context.RawName(), + "type": context.Type(), + "client_subnet": context.SourceSubnet, + "domain_uuid": "", + } + return context +} + +func (context *RequestContext) sourceIp() net.IP { + opt := context.Req.IsEdns0() + if opt != nil && len(opt.Option) != 0 { + for _, o := range opt.Option { + switch v := o.(type) { + case *dns.EDNS0_SUBNET: + return v.Address + } + } + } + return net.ParseIP(context.IP()) +} + +func (context *RequestContext) sourceSubnet() string { + opt := context.Req.IsEdns0() + if opt != nil && len(opt.Option) != 0 { + for _, o := range opt.Option { + switch o.(type) { + case *dns.EDNS0_SUBNET: + return o.String() + } + } + } + return "" +} + +func (context *RequestContext) RawName() string { + if context.name != "" { + return context.name + } + if context.Req == nil { + context.name = "." + return "." + } + if len(context.Req.Question) == 0 { + context.name = "." + return "." + } + + context.name = strings.ToLower(context.Req.Question[0].Name) + return context.name +} + +func (context *RequestContext) Response(rcode int) { + m := new(dns.Msg) + m.Authoritative, m.RecursionAvailable, m.Compress = context.Auth, false, true + m.SetRcode(context.Req, rcode) + m.Answer = append(m.Answer, context.Answer...) + m.Ns = append(m.Ns, context.Authority...) + m.Extra = append(m.Extra, context.Additional...) + + context.SizeAndDo(m) + m = context.Scrub(m) + if err := context.W.WriteMsg(m); err != nil { + // logger.Default.Error("write error : ", err, " msg : ", m.String()) + _ = context.W.Close() + } +} diff --git a/handler/server.go b/handler/server.go index 9ed50e3c1cf3e019ee583402263e01b1473a98e2..d6b4a6e24093e2241612ad6bacda9815dbbc64e8 100644 --- a/handler/server.go +++ b/handler/server.go @@ -3,24 +3,25 @@ package handler import ( "strconv" - "github.com/miekg/dns" "crypto/tls" "crypto/x509" + "github.com/miekg/dns" "io/ioutil" ) type TlsConfig struct { - Enable bool `json:"enable"` + Enable bool `json:"enable"` CertPath string `json:"cert_path"` KeyPath string `json:"key_path"` CaPath string `json:"ca_path"` } type ServerConfig struct { - Ip string `json:"ip,omitempty"` - Port int `json:"port,omitempty"` - Protocol string `json:"protocol,omitempty"` - Tls TlsConfig `json:"tls,omitempty"` + Ip string `json:"ip"` + Port int `json:"port"` + Protocol string `json:"protocol"` + Count int `json:"count"` + Tls TlsConfig `json:"tls"` } func loadRoots(caPath string) *x509.CertPool { @@ -55,14 +56,20 @@ func loadTlsConfig(cfg TlsConfig) *tls.Config { func NewServer(config []ServerConfig) []dns.Server { var servers []dns.Server for _, cfg := range config { - server := dns.Server{ - Addr: cfg.Ip + ":" + strconv.Itoa(cfg.Port), - Net: cfg.Protocol, + if cfg.Count < 1 { + cfg.Count = 1 } - if cfg.Tls.Enable { - server.TLSConfig = loadTlsConfig(cfg.Tls) + for i := 0; i < cfg.Count; i++ { + server := dns.Server{ + Addr: cfg.Ip + ":" + strconv.Itoa(cfg.Port), + Net: cfg.Protocol, + ReusePort: true, + } + if cfg.Tls.Enable { + server.TLSConfig = loadTlsConfig(cfg.Tls) + } + servers = append(servers, server) } - servers = append(servers, server) } return servers } diff --git a/handler/subnet_test.go b/handler/subnet_test.go index a2a523086929971998288d3a39113aa5d88f0596..3ae4f90725cb6c6599182c0fee50613aeced06af 100644 --- a/handler/subnet_test.go +++ b/handler/subnet_test.go @@ -2,7 +2,6 @@ package handler import ( "arvancloud/redins/test" - "github.com/coredns/coredns/request" "github.com/miekg/dns" "log" "net" @@ -33,14 +32,14 @@ func TestSubnet(t *testing.T) { t.Fail() } w := test.NewRecorder(&test.ResponseWriter{}) - state := request.Request{W: w, Req: r} + state := NewRequestContext(w, r) - subnet := GetSourceSubnet(&state) + subnet := state.SourceSubnet if subnet != sa+"/32/0" { log.Printf("subnet = %s should be %s\n", subnet, sa) t.Fail() } - address := GetSourceIp(&state) + address := state.SourceIp if address.String() != sa { log.Printf("address = %s should be %s\n", address.String(), sa) t.Fail() diff --git a/handler/upstream.go b/handler/upstream.go index 664b860c8b280fcab94273f079cc7123bd5cfc98..7f7994a7c63844c080f382c70cb7e0bdd13ebfb1 100644 --- a/handler/upstream.go +++ b/handler/upstream.go @@ -1,6 +1,8 @@ package handler import ( + "errors" + "golang.org/x/sync/singleflight" "strconv" "time" @@ -17,17 +19,20 @@ type UpstreamConnection struct { type Upstream struct { connections []*UpstreamConnection cache *cache.Cache + inflight *singleflight.Group } type UpstreamConfig struct { - Ip string `json:"ip,omitempty"` - Port int `json:"port,omitempty"` - Protocol string `json:"protocol,omitempty"` - Timeout int `json:"timeout,omitempty"` + Ip string `json:"ip"` + Port int `json:"port"` + Protocol string `json:"protocol"` + Timeout int `json:"timeout"` } func NewUpstream(config []UpstreamConfig) *Upstream { - u := &Upstream{} + u := &Upstream{ + inflight: new(singleflight.Group), + } u.cache = cache.New(time.Second*time.Duration(defaultCacheTtl), time.Second*time.Duration(defaultCacheTtl)*10) for _, upstreamConfig := range config { @@ -56,29 +61,41 @@ func (u *Upstream) Query(location string, qtype uint16) ([]dns.RR, int) { } return records, dns.RcodeSuccess } - m := new(dns.Msg) - m.SetQuestion(location, qtype) - for _, c := range u.connections { - r, _, err := c.client.Exchange(m, c.connectionStr) - if err != nil { - logger.Default.Errorf("failed to retrieve record %s from upstream %s : %s", location, c.connectionStr, err) - continue - } - if len(r.Answer) == 0 { - return []dns.RR{}, dns.RcodeNameError - } - minTtl := r.Answer[0].Header().Ttl - for _, record := range r.Answer { - if record.Header().Ttl < minTtl { - minTtl = record.Header().Ttl + answer, err, _ := u.inflight.Do(key, func() (interface{}, error) { + m := new(dns.Msg) + m.SetQuestion(location, qtype) + for _, c := range u.connections { + r, _, err := c.client.Exchange(m, c.connectionStr) + if err != nil { + logger.Default.Errorf("failed to retrieve record %s from upstream %s : %s", location, c.connectionStr, err) + continue } - } - u.cache.Set(key, r.Answer, time.Duration(minTtl)*time.Second) - u.connections[0], c = c, u.connections[0] + if r.Rcode != dns.RcodeSuccess { + logger.Default.Errorf("upstream error response : %s for %s", dns.RcodeToString[r.Rcode], location) + return r, nil + } + if len(r.Answer) == 0 { + return r, nil + } + minTtl := r.Answer[0].Header().Ttl + for _, record := range r.Answer { + if record.Header().Ttl < minTtl { + minTtl = record.Header().Ttl + } + } + u.cache.Set(key, r.Answer, time.Duration(minTtl)*time.Second) + u.connections[0], c = c, u.connections[0] - return r.Answer, dns.RcodeSuccess + return r, nil + } + return nil, errors.New("failed to retrieve data from upstream") + }) + if err != nil { + return []dns.RR{}, dns.RcodeServerFailure + } else { + msg := answer.(*dns.Msg) + return msg.Answer, msg.Rcode } - return []dns.RR{}, dns.RcodeServerFailure } const ( diff --git a/handler/upstream_test.go b/handler/upstream_test.go deleted file mode 100644 index cf2ebe388c20c3dde90157ea754ff4a96a97d3cc..0000000000000000000000000000000000000000 --- a/handler/upstream_test.go +++ /dev/null @@ -1,84 +0,0 @@ -package handler - -import ( - "log" - "testing" - - "arvancloud/redins/test" - "github.com/coredns/coredns/request" - "github.com/hawell/logger" - "github.com/hawell/uperdis" - "github.com/miekg/dns" -) - -var upstreamTestConfig = HandlerConfig{ - MaxTtl: 300, - CacheTimeout: 60, - ZoneReload: 600, - UpstreamFallback: true, - Redis: uperdis.RedisConfig{ - Ip: "redis", - Port: 6379, - DB: 0, - Password: "", - Prefix: "test_", - Suffix: "_test", - ConnectTimeout: 0, - ReadTimeout: 0, - }, - Log: logger.LogConfig{ - Enable: false, - }, - Upstream: []UpstreamConfig{ - { - Ip: "1.1.1.1", - Port: 53, - Protocol: "udp", - Timeout: 1000, - }, - }, - GeoIp: GeoIpConfig{ - Enable: true, - CountryDB: "../geoCity.mmdb", - }, -} - -func TestUpstream(t *testing.T) { - logger.Default = logger.NewLogger(&logger.LogConfig{}) - u := NewUpstream(upstreamTestConfig.Upstream) - rs, res := u.Query("google.com.", dns.TypeAAAA) - if len(rs) == 0 || res != 0 { - log.Printf("[ERROR] AAAA failed") - t.Fail() - } - rs, res = u.Query("google.com.", dns.TypeA) - if len(rs) == 0 || res != 0 { - log.Printf("[ERROR] A failed") - t.Fail() - } - rs, res = u.Query("google.com.", dns.TypeTXT) - if len(rs) == 0 || res != 0 { - log.Printf("[ERROR] TXT failed") - t.Fail() - } -} - -func TestFallback(t *testing.T) { - tc := test.Case{ - Qname: "google.com.", Qtype: dns.TypeAAAA, - } - logger.Default = logger.NewLogger(&logger.LogConfig{}) - - h := NewHandler(&upstreamTestConfig) - - r := tc.Msg() - w := test.NewRecorder(&test.ResponseWriter{}) - state := request.Request{W: w, Req: r} - h.HandleRequest(&state) - - resp := w.Msg - - if resp.Rcode != dns.RcodeSuccess { - t.Fail() - } -} diff --git a/handler/weight_test.go b/handler/weight_test.go new file mode 100644 index 0000000000000000000000000000000000000000..bbf362a362edc4ebfc737b85230b6de77399d499 --- /dev/null +++ b/handler/weight_test.go @@ -0,0 +1,106 @@ +package handler + +import ( + "log" + "net" + "testing" + + "github.com/hawell/logger" +) + +func TestWeight(t *testing.T) { + logger.Default = logger.NewLogger(&logger.LogConfig{}, nil) + + // distribution + ips := []IP_RR{ + {Ip: net.ParseIP("1.2.3.4"), Weight: 4}, + {Ip: net.ParseIP("2.3.4.5"), Weight: 1}, + {Ip: net.ParseIP("3.4.5.6"), Weight: 5}, + {Ip: net.ParseIP("4.5.6.7"), Weight: 10}, + } + n := make([]int, 4) + for i := 0; i < 100000; i++ { + x := ChooseIp(ips, true) + switch ips[x].Ip.String() { + case "1.2.3.4": + n[0]++ + case "2.3.4.5": + n[1]++ + case "3.4.5.6": + n[2]++ + case "4.5.6.7": + n[3]++ + } + } + if n[0] > n[2] || n[2] > n[3] || n[1] > n[0] { + t.Fail() + } + + // all zero + for i := range ips { + ips[i].Weight = 0 + } + n[0], n[1], n[2], n[3] = 0, 0, 0, 0 + for i := 0; i < 100000; i++ { + x := ChooseIp(ips, true) + switch ips[x].Ip.String() { + case "1.2.3.4": + n[0]++ + case "2.3.4.5": + n[1]++ + case "3.4.5.6": + n[2]++ + case "4.5.6.7": + n[3]++ + } + } + for i := 0; i < 4; i++ { + if n[i] < 2000 && n[i] > 3000 { + t.Fail() + } + } + + // some zero + n[0], n[1], n[2], n[3] = 0, 0, 0, 0 + ips[0].Weight, ips[1].Weight, ips[2].Weight, ips[3].Weight = 0, 5, 7, 0 + for i := 0; i < 100000; i++ { + x := ChooseIp(ips, true) + switch ips[x].Ip.String() { + case "1.2.3.4": + n[0]++ + case "2.3.4.5": + n[1]++ + case "3.4.5.6": + n[2]++ + case "4.5.6.7": + n[3]++ + } + } + log.Println(n) + if n[0] > 0 || n[3] > 0 { + t.Fail() + } + + // weighted = false + n[0], n[1], n[2], n[3] = 0, 0, 0, 0 + ips[0].Weight, ips[1].Weight, ips[2].Weight, ips[3].Weight = 0, 5, 7, 0 + for i := 0; i < 100000; i++ { + x := ChooseIp(ips, false) + switch ips[x].Ip.String() { + case "1.2.3.4": + n[0]++ + case "2.3.4.5": + n[1]++ + case "3.4.5.6": + n[2]++ + case "4.5.6.7": + n[3]++ + } + } + log.Println(n) + for i := 0; i < 4; i++ { + if n[i] < 2000 && n[i] > 3000 { + t.Fail() + } + } +} diff --git a/handler/zone.go b/handler/zone.go new file mode 100644 index 0000000000000000000000000000000000000000..d8e9f6bb0a544955b64b090452e73a0b24f214ea --- /dev/null +++ b/handler/zone.go @@ -0,0 +1,142 @@ +package handler + +import ( + "github.com/hawell/logger" + jsoniter "github.com/json-iterator/go" + "github.com/miekg/dns" + "strings" + "time" +) + +type Zone struct { + Name string + Config ZoneConfig + Locations map[string]struct{} + ZSK *ZoneKey + KSK *ZoneKey + DnsKeySig dns.RR +} + +type ZoneConfig struct { + DomainId string `json:"domain_id,omitempty"` + SOA *SOA_RRSet `json:"soa,omitempty"` + DnsSec bool `json:"dnssec,omitempty"` + CnameFlattening bool `json:"cname_flattening,omitempty"` +} + +func NewZone(name string, locations []string, config string) *Zone { + z := new(Zone) + z.Name = name + z.Locations = make(map[string]struct{}) + for _, val := range locations { + z.Locations[val] = struct{}{} + } + + z.Config = ZoneConfig{ + DnsSec: false, + CnameFlattening: false, + SOA: &SOA_RRSet{ + Ns: "ns1." + z.Name, + MinTtl: 300, + Refresh: 86400, + Retry: 7200, + Expire: 3600, + MBox: "hostmaster." + z.Name, + Serial: uint32(time.Now().Unix()), + Ttl: 300, + }, + } + if len(config) > 0 { + err := jsoniter.Unmarshal([]byte(config), &z.Config) + if err != nil { + logger.Default.Errorf("cannot parse zone config : %s", err) + } + } + z.Config.SOA.Ns = dns.Fqdn(z.Config.SOA.Ns) + z.Config.SOA.Data = &dns.SOA{ + Hdr: dns.RR_Header{Name: z.Name, Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: z.Config.SOA.Ttl, Rdlength: 0}, + Ns: z.Config.SOA.Ns, + Mbox: z.Config.SOA.MBox, + Refresh: z.Config.SOA.Refresh, + Retry: z.Config.SOA.Retry, + Expire: z.Config.SOA.Expire, + Minttl: z.Config.SOA.MinTtl, + Serial: z.Config.SOA.Serial, + } + return z +} + +const ( + ExactMatch = iota + WildCardMatch + NoMatch +) + +func (z *Zone) FindLocation(query string) (string, int) { + var ( + ok bool + closestEncloser string + sourceOfSynthesis string + ) + + // request for zone records + if query == z.Name { + return query, ExactMatch + } + + query = strings.TrimSuffix(query, "."+z.Name) + + if _, ok = z.Locations[query]; ok { + return query, ExactMatch + } + + closestEncloser, sourceOfSynthesis, ok = splitQuery(query) + for ok { + ceExists := z.keyMatches(closestEncloser) || z.keyExists(closestEncloser) + ssExists := z.keyExists(sourceOfSynthesis) + if ceExists { + if ssExists { + return sourceOfSynthesis, WildCardMatch + } else { + return "", NoMatch + } + } else { + closestEncloser, sourceOfSynthesis, ok = splitQuery(closestEncloser) + } + } + return "", NoMatch +} + +func (z *Zone) keyExists(key string) bool { + _, ok := z.Locations[key] + return ok +} + +func (z *Zone) keyMatches(key string) bool { + for value := range z.Locations { + if strings.HasSuffix(value, key) { + return true + } + } + return false +} + +func splitQuery(query string) (string, string, bool) { + if query == "" { + return "", "", false + } + var ( + splits []string + closestEncloser string + sourceOfSynthesis string + ) + splits = strings.SplitAfterN(query, ".", 2) + if len(splits) == 2 { + closestEncloser = splits[1] + sourceOfSynthesis = "*." + closestEncloser + } else { + closestEncloser = "" + sourceOfSynthesis = "*" + } + return closestEncloser, sourceOfSynthesis, true +} diff --git a/perf/bulk/bulk.go b/perf/bulk/bulk.go deleted file mode 100644 index c324a7b2483c9673740122cacb1ca0e2efd68521..0000000000000000000000000000000000000000 --- a/perf/bulk/bulk.go +++ /dev/null @@ -1,57 +0,0 @@ -package main - -import ( - "bufio" - "fmt" - "github.com/miekg/dns" - "os" - "time" -) - -func main() { - client := &dns.Client{ - Net: "udp", - Timeout: time.Millisecond * 100, - } - - fq, err := os.Open("../query.txt") - if err != nil { - fmt.Println("cannot open query.txt") - return - } - defer fq.Close() - rq := bufio.NewReader(fq) - var duration time.Duration - for { - line, err := rq.ReadString('\n') - if err != nil { - break - } - var queryAddr, queryResult string - // fmt.Println("line = ", line) - fmt.Sscan(line, &queryAddr, &queryResult) - // fmt.Println("addr = ", queryAddr, "result = ", queryResult) - m := new(dns.Msg) - m.SetQuestion(queryAddr, dns.TypeA) - r, rtt, err := client.Exchange(m, "localhost:1053") - if err != nil { - fmt.Println("error: ", err) - break - } - if r.Rcode != dns.RcodeSuccess { - fmt.Println("bad response : ", r.Rcode) - break - } - if len(r.Answer) == 0 { - fmt.Println("empty response") - break - } - a := r.Answer[0].(*dns.A) - if a.A.String() != queryResult { - fmt.Printf("error: incorrect answer : expected %s got %s", queryResult, a.A.String()) - break - } - duration += rtt - } - fmt.Println(duration) -} diff --git a/redins.go b/redins.go index af427022033bff773aacace44c2fed89261520e6..028fc287c83a5cc2f371bf4e3f729059455943ee 100644 --- a/redins.go +++ b/redins.go @@ -1,203 +1,587 @@ package main import ( - "encoding/json" + "errors" + "flag" + "fmt" + "github.com/Shopify/sarama" + "github.com/getsentry/raven-go" + "github.com/json-iterator/go" + "github.com/logrusorgru/aurora" + "github.com/oschwald/maxminddb-golang" "io/ioutil" "log" + "log/syslog" + "net" + "net/http" "os" "os/signal" + "strconv" + "strings" "syscall" "time" "arvancloud/redins/handler" - "github.com/coredns/coredns/request" "github.com/hawell/logger" "github.com/hawell/uperdis" "github.com/miekg/dns" + _ "net/http/pprof" ) var ( - s []dns.Server - h *handler.DnsRequestHandler - l *handler.RateLimiter + s []dns.Server + h *handler.DnsRequestHandler + l *handler.RateLimiter + configFile string ) func handleRequest(w dns.ResponseWriter, r *dns.Msg) { - // log.Printf("[DEBUG] handle request") - state := request.Request{W: w, Req: r} + context := handler.NewRequestContext(w, r) + logger.Default.Debugf("handle request: [%d] %s %s", r.Id, context.RawName(), context.Type()) - if l.CanHandle(state.IP()) { - h.HandleRequest(&state) + if l.CanHandle(context.IP()) { + h.HandleRequest(context) } else { - msg := new(dns.Msg) - msg.SetRcode(r, dns.RcodeRefused) - state.W.WriteMsg(msg) + context.Response(dns.RcodeRefused) } } type RedinsConfig struct { - Server []handler.ServerConfig `json:"server,omitempty"` - ErrorLog logger.LogConfig `json:"error_log,omitempty"` - Handler handler.HandlerConfig `json:"handler,omitempty"` - RateLimit handler.RateLimiterConfig `json:"ratelimit,omitempty"` + Server []handler.ServerConfig `json:"server"` + ErrorLog logger.LogConfig `json:"error_log"` + Handler handler.DnsRequestHandlerConfig `json:"handler"` + RateLimit handler.RateLimiterConfig `json:"ratelimit"` } -func LoadConfig(path string) *RedinsConfig { - config := &RedinsConfig{ - Server: []handler.ServerConfig{ +var redinsDefaultConfig = &RedinsConfig{ + Server: []handler.ServerConfig{ + { + Ip: "127.0.0.1", + Port: 1053, + Protocol: "udp", + Count: 1, + Tls: handler.TlsConfig{ + Enable: false, + CertPath: "", + KeyPath: "", + CaPath: "", + }, + }, + }, + Handler: handler.DnsRequestHandlerConfig{ + Upstream: []handler.UpstreamConfig{ { - Ip: "127.0.0.1", - Port: 1053, + Ip: "1.1.1.1", + Port: 53, Protocol: "udp", + Timeout: 400, }, }, - Handler: handler.HandlerConfig{ - Upstream: []handler.UpstreamConfig{ - { - Ip: "1.1.1.1", - Port: 53, - Protocol: "udp", - Timeout: 400, - }, - }, - GeoIp: handler.GeoIpConfig{ - Enable: false, - CountryDB: "geoCity.mmdb", - ASNDB: "geoIsp.mmdb", - }, - HealthCheck: handler.HealthcheckConfig{ - Enable: false, - MaxRequests: 10, - MaxPendingRequests: 100, - UpdateInterval: 600, - CheckInterval: 600, - RedisStatusServer: uperdis.RedisConfig{ - Ip: "127.0.0.1", - Port: 6379, - DB: 0, - Password: "", - Prefix: "redins_", - Suffix: "_redins", - ConnectTimeout: 0, - ReadTimeout: 0, - ActiveConnections: 10, - }, - Log: logger.LogConfig{ - Enable: true, - Target: "file", - Level: "info", - Path: "/tmp/healthcheck.log", - Format: "json", - TimeFormat: time.RFC3339, - Sentry: logger.SentryConfig{ - Enable: false, - }, - Syslog: logger.SyslogConfig{ - Enable: false, - }, + GeoIp: handler.GeoIpConfig{ + Enable: false, + CountryDB: "geoCity.mmdb", + ASNDB: "geoIsp.mmdb", + }, + HealthCheck: handler.HealthcheckConfig{ + Enable: false, + MaxRequests: 10, + MaxPendingRequests: 100, + UpdateInterval: 600, + CheckInterval: 600, + RedisStatusServer: uperdis.RedisConfig{ + Address: "127.0.0.1:6379", + Net: "tcp", + DB: 0, + Password: "", + Prefix: "redins_", + Suffix: "_redins", + Connection: uperdis.RedisConnectionConfig{ + MaxIdleConnections: 10, + MaxActiveConnections: 10, + ConnectTimeout: 500, + ReadTimeout: 500, + IdleKeepAlive: 30, + MaxKeepAlive: 0, + WaitForConnection: false, }, }, - MaxTtl: 3600, - CacheTimeout: 60, - ZoneReload: 600, - LogSourceLocation: false, - UpstreamFallback: false, - Redis: uperdis.RedisConfig{ - Ip: "127.0.0.1", - Port: 6379, - DB: 0, - Password: "", - Prefix: "redins_", - Suffix: "_redins", - ConnectTimeout: 0, - ReadTimeout: 0, - ActiveConnections: 10, - }, Log: logger.LogConfig{ Enable: true, Target: "file", Level: "info", - Path: "/tmp/redins.log", + Path: "/tmp/healthcheck.log", Format: "json", TimeFormat: time.RFC3339, Sentry: logger.SentryConfig{ Enable: false, + DSN: "", }, Syslog: logger.SyslogConfig{ - Enable: false, + Enable: false, + Protocol: "tcp", + Address: "localhost:514", }, + Kafka: logger.KafkaConfig{ + Enable: false, + Topic: "redins", + Brokers: []string{"127.0.0.1:9092"}, + Format: "json", + Compression: "none", + Timeout: 3000, + BufferSize: 1000, + }, + }, + }, + MaxTtl: 3600, + CacheTimeout: 60, + ZoneReload: 600, + LogSourceLocation: false, + Redis: uperdis.RedisConfig{ + Address: "127.0.0.1:6379", + Net: "tcp", + DB: 0, + Password: "", + Prefix: "redins_", + Suffix: "_redins", + Connection: uperdis.RedisConnectionConfig{ + MaxIdleConnections: 10, + MaxActiveConnections: 10, + ConnectTimeout: 500, + ReadTimeout: 500, + IdleKeepAlive: 30, + MaxKeepAlive: 0, + WaitForConnection: false, }, }, - ErrorLog: logger.LogConfig{ + Log: logger.LogConfig{ Enable: true, - Target: "stdout", + Target: "file", Level: "info", - Format: "text", + Path: "/tmp/redins.log", + Format: "json", TimeFormat: time.RFC3339, Sentry: logger.SentryConfig{ Enable: false, + DSN: "", }, Syslog: logger.SyslogConfig{ - Enable: false, + Enable: false, + Protocol: "tcp", + Address: "localhost:514", + }, + Kafka: logger.KafkaConfig{ + Enable: false, + Topic: "redins", + Brokers: []string{"127.0.0.1:9092"}, + Format: "json", + Compression: "none", + Timeout: 3000, + BufferSize: 1000, }, }, - RateLimit: handler.RateLimiterConfig{ - Enable: false, - Rate: 60, - Burst: 10, - BlackList: []string{}, - WhiteList: []string{}, + }, + ErrorLog: logger.LogConfig{ + Enable: true, + Target: "stdout", + Level: "info", + Path: "/tmp/error.log", + Format: "text", + TimeFormat: time.RFC3339, + Sentry: logger.SentryConfig{ + Enable: false, + DSN: "", }, - } - raw, err := ioutil.ReadFile(path) + Syslog: logger.SyslogConfig{ + Enable: false, + Protocol: "tcp", + Address: "locahost:514", + }, + Kafka: logger.KafkaConfig{ + Enable: false, + Topic: "redins", + Brokers: []string{"127.0.0.1:9092"}, + Format: "json", + Compression: "none", + Timeout: 3000, + BufferSize: 1000, + }, + }, + RateLimit: handler.RateLimiterConfig{ + Enable: false, + Rate: 60, + Burst: 10, + BlackList: []string{}, + WhiteList: []string{}, + }, +} + +func LoadConfig(path string) (*RedinsConfig, error) { + config := redinsDefaultConfig + configFile, err := os.Open(path) if err != nil { log.Printf("[ERROR] cannot load file %s : %s", path, err) log.Printf("[INFO] loading default config") - return config + return config, err } - err = json.Unmarshal(raw, config) + decoder := jsoniter.NewDecoder(configFile) + decoder.DisallowUnknownFields() + err = decoder.Decode(config) if err != nil { log.Printf("[ERROR] cannot load json file") log.Printf("[INFO] loading default config") - return config + return config, err } - return config + return config, nil } func Start() { - configFile := "config.json" - if len(os.Args) > 1 { - configFile = os.Args[1] - } - cfg := LoadConfig(configFile) + log.Printf("[INFO] loading config : %s", configFile) + cfg, _ := LoadConfig(configFile) - logger.Default = logger.NewLogger(&cfg.ErrorLog) + log.Printf("[INFO] loading logger...") + logger.Default = logger.NewLogger(&cfg.ErrorLog, nil) + log.Printf("[INFO] logger loaded") s = handler.NewServer(cfg.Server) + logger.Default.Info("starting handler...") h = handler.NewHandler(&cfg.Handler) + logger.Default.Info("handler started") l = handler.NewRateLimiter(&cfg.RateLimit) dns.HandleFunc(".", handleRequest) + logger.Default.Info("binding listeners...") for i := range s { - go s[i].ListenAndServe() - time.Sleep(1 * time.Second) + go func(i int) { + err := s[i].ListenAndServe() + if err != nil { + logger.Default.Errorf("listener error : %s", err) + } + }(i) } + logger.Default.Info("binding completed") } func Stop() { for i := range s { - s[i].Shutdown() + _ = s[i].Shutdown() } h.ShutDown() } +func Verify(configFile string) { + ok := aurora.Bold(aurora.Green("[ OK ]")) + fail := aurora.Bold(aurora.Red("[FAIL]")) + warn := aurora.Bold(aurora.Yellow("[WARN]")) + printResult := func(msg string, err error) { + if err == nil { + fmt.Printf("%-60s%s\n", msg, ok) + return + } else { + fmt.Printf("%-60s%s : %s\n", msg, fail, err) + } + } + printWarning := func(msg string, warning string) { + fmt.Printf("%-60s%s : %s\n", msg, warn, warning) + } + + checkAddress := func(protocol string, ip string, port int) { + msg := fmt.Sprintf("checking protocol : %s", protocol) + var err error = nil + if protocol != "tcp" && protocol != "udp" { + err = errors.New("invalid protocol") + } + printResult(msg, err) + + msg = fmt.Sprintf("checking ip address : %s", ip) + err = nil + if ip := net.ParseIP(ip); ip == nil { + err = errors.New("invalid ip address") + } + printResult(msg, err) + + msg = fmt.Sprintf("checking port number : %d", port) + err = nil + if port > 65535 || port < 1 { + err = errors.New("invalid port number") + } + printResult(msg, err) + } + + checkRedis := func(config *uperdis.RedisConfig) { + fmt.Println("checking redis...") + rd := uperdis.NewRedis(config) + msg := fmt.Sprintf("checking whether %s://%s is available", config.Net, config.Address) + err := rd.Ping() + printResult(msg, err) + msg = fmt.Sprintf("checking notify-keyspace-events") + err = nil + var nkse string + nkse, err = rd.GetConfig("notify-keyspace-events") + if err == nil { + if !strings.Contains(nkse, "K") { + err = errors.New("keyspace in not active") + } else if !strings.Contains(nkse, "A") && !strings.Contains(nkse, "s") { + err = errors.New("A or s should be active") + } + } + printResult(msg, err) + } + + checkLog := func(config *logger.LogConfig) { + fmt.Println("checking log...") + msg := fmt.Sprintf("checking target : %s", config.Path) + var err error = nil + if config.Target != "stdout" && config.Target != "stderr" && config.Target != "file" && config.Target != "udp" { + err = errors.New("invalid target : " + config.Target) + } + printResult(msg, err) + + if config.Target == "file" { + msg = fmt.Sprintf("checking file target : %s", config.Path) + var file *os.File + file, err = os.OpenFile(config.Target, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666) + if err == nil { + _ = file.Close() + } + printResult(msg, err) + } + if config.Target == "udp" { + msg = fmt.Sprintf("checking udp target : %s", config.Target) + err = nil + var raddr *net.UDPAddr + raddr, err = net.ResolveUDPAddr("udp", config.Path) + if err == nil { + var con *net.UDPConn + con, err = net.DialUDP("udp", nil, raddr) + if err == nil { + _ = con.Close() + } + } + printResult(msg, err) + } + + msg = fmt.Sprintf("checking log level : %s", config.Level) + err = nil + if config.Level != "debug" && config.Level != "info" && config.Level != "warning" && config.Level != "error" { + err = errors.New("invalid log level : " + config.Level) + } + printResult(msg, err) + + msg = fmt.Sprintf("checking format : %s", config.Format) + err = nil + if config.Format != "text" && config.Format != "json" && config.Format != "capnp_request" { + err = errors.New("invalid log format : " + config.Format) + } + printResult(msg, err) + + msg = fmt.Sprintf("checking time format : %s", config.TimeFormat) + err = nil + t1, _ := time.Parse(time.RFC3339, time.RFC3339) + timeStr := t1.Format(config.TimeFormat) + var t2 time.Time + t2, err = time.Parse(config.TimeFormat, timeStr) + if err == nil { + if t2 != t1 { + err = errors.New("invalid time format") + } + } + printResult(msg, err) + + if config.Kafka.Enable { + fmt.Println("checking kafka at ", config.Kafka.Brokers) + msg = fmt.Sprintf("checking kafka") + err = nil + cfg := sarama.NewConfig() + cfg.Producer.RequiredAcks = sarama.WaitForAll + cfg.Producer.Compression = sarama.CompressionNone + cfg.Producer.Flush.Frequency = 500 * time.Millisecond + cfg.Producer.Return.Errors = true + cfg.Producer.Return.Successes = true + + cfg.Metadata.Timeout = time.Duration(config.Kafka.Timeout) * time.Millisecond + + var producer sarama.SyncProducer + producerMessages := []*sarama.ProducerMessage{ + { + Topic: config.Kafka.Topic, + Value: sarama.StringEncoder("test message"), + Metadata: "test", + }, + } + producer, err = sarama.NewSyncProducer(config.Kafka.Brokers, cfg) + if err == nil { + err = producer.SendMessages(producerMessages) + } + printResult(msg, err) + } + if config.Sentry.Enable { + msg = fmt.Sprintf("checking sentry at %s", config.Sentry.DSN) + err = nil + var client *raven.Client + client, err = raven.New(config.Sentry.DSN) + if err == nil { + packet := raven.NewPacket("test message", nil) + eventID, ch := client.Capture(packet, nil) + if eventID != "" { + err = <-ch + } + if err == nil && eventID == "" { + err = errors.New("sentry test failed") + } + } + printResult(msg, err) + } + if config.Syslog.Enable { + msg = fmt.Sprintf("checking syslog at %s", config.Syslog.Address) + var w *syslog.Writer + w, err = syslog.Dial(config.Syslog.Protocol, config.Syslog.Address, syslog.LOG_ERR, "syslog test") + if err == nil { + err = w.Err("test message") + } + printResult(msg, err) + } + } + + fmt.Println("Starting Config Verification") + + msg := fmt.Sprintf("loading config file : %s", configFile) + config, err := LoadConfig(configFile) + printResult(msg, err) + + fmt.Println("checking listeners...") + for _, server := range config.Server { + checkAddress(server.Protocol, server.Ip, server.Port) + msg = fmt.Sprintf("checking port number : %d", server.Port) + if server.Port != 53 { + printWarning(msg, "using non-standard port") + } else { + printResult(msg, nil) + } + + address := server.Ip + ":" + strconv.Itoa(server.Port) + msg = fmt.Sprintf("checking whether %s://%s is available", server.Protocol, address) + err = nil + if server.Protocol == "udp" { + var ln net.PacketConn + ln, err = net.ListenPacket(server.Protocol, address) + if err == nil { + _ = ln.Close() + } + } else { + var ln net.Listener + ln, err = net.Listen(server.Protocol, address) + if err == nil { + _ = ln.Close() + } + } + printResult(msg, err) + } + fmt.Println("checking upstreams...") + for _, upstream := range config.Handler.Upstream { + checkAddress(upstream.Protocol, upstream.Ip, upstream.Port) + address := upstream.Ip + ":" + strconv.Itoa(upstream.Port) + msg = fmt.Sprintf("checking whether %s://%s is available", upstream.Protocol, address) + err = nil + client := &dns.Client{ + Net: upstream.Protocol, + Timeout: time.Duration(upstream.Timeout) * time.Millisecond, + } + m := new(dns.Msg) + m.SetQuestion("dns.msftncsi.com.", dns.TypeA) + var resp *dns.Msg + resp, _, err = client.Exchange(m, address) + if err == nil { + if len(resp.Answer) == 0 { + err = errors.New("empty response") + } else { + a, ok := resp.Answer[0].(*dns.A) + if !ok { + err = errors.New("bad response") + } else if a.A.String() != "131.107.255.255" { + err = errors.New("incorrect response") + } + } + } + printResult(msg, err) + } + checkRedis(&config.Handler.Redis) + if config.Handler.GeoIp.Enable { + fmt.Println("checking geoip...") + var countryRecord struct { + Location struct { + Latitude float64 `maxminddb:"latitude"` + LongitudeOffset uintptr `maxminddb:"longitude"` + } `maxminddb:"location"` + Country struct { + ISOCode string `maxminddb:"iso_code"` + } `maxminddb:"country"` + } + var asnRecord struct { + AutonomousSystemNumber uint `maxminddb:"autonomous_system_number"` + } + records := []interface{}{countryRecord, asnRecord} + for i, dbFile := range []string{config.Handler.GeoIp.CountryDB, config.Handler.GeoIp.ASNDB} { + msg = fmt.Sprintf("checking file stat : %s", dbFile) + _, err = os.Stat(dbFile) + printResult(msg, err) + if err == nil { + msg = fmt.Sprintf("checking db : %s", dbFile) + var db *maxminddb.Reader + db, err = maxminddb.Open(dbFile) + printResult(msg, err) + if err == nil { + msg = fmt.Sprintf("checking db query results") + err = db.Lookup(net.ParseIP("46.19.36.12"), &records[i]) + printResult(msg, err) + } + } + } + } + if config.ErrorLog.Enable { + checkLog(&config.ErrorLog) + } + if config.Handler.Log.Enable { + checkLog(&config.Handler.Log) + } +} + func main() { + configPtr := flag.String("c", "config.json", "path to config file") + verifyPtr := flag.Bool("t", false, "verify configuration") + generateConfigPtr := flag.String("g", "template-config.json", "generate template config file") + + flag.Parse() + flagset := make(map[string]bool) + flag.Visit(func(f *flag.Flag) { flagset[f.Name] = true }) + + configFile = *configPtr + if *verifyPtr { + Verify(configFile) + return + } + + if flagset["g"] { + data, err := jsoniter.MarshalIndent(redinsDefaultConfig, "", " ") + if err != nil { + fmt.Println("cannot unmarshal template config : ", err) + return + } + if err = ioutil.WriteFile(*generateConfigPtr, data, 0644); err != nil { + fmt.Printf("cannot save template config to file %s : %s\n", *generateConfigPtr, err) + } + return + } Start() + // TODO: this should be part of a general api + go func() { + log.Println(http.ListenAndServe("localhost:6060", nil)) + }() + c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGINT, syscall.SIGHUP) diff --git a/template-config.json b/template-config.json new file mode 100644 index 0000000000000000000000000000000000000000..11ee1fe29372087638c4594f9ec70f50ca3c145c --- /dev/null +++ b/template-config.json @@ -0,0 +1,166 @@ +{ + "server": [ + { + "ip": "127.0.0.1", + "port": 1053, + "protocol": "udp", + "tls": { + "enable": false, + "cert_path": "", + "key_path": "", + "ca_path": "" + } + } + ], + "error_log": { + "enable": true, + "target": "stdout", + "level": "info", + "path": "/tmp/error.log", + "format": "text", + "time_format": "2006-01-02T15:04:05Z07:00", + "sentry": { + "enable": false, + "dsn": "" + }, + "syslog": { + "enable": false, + "protocol": "tcp", + "address": "locahost:514" + }, + "kafka": { + "enable": false, + "topic": "redins", + "brokers": [ + "127.0.0.1:9092" + ], + "format": "json", + "compression": "none", + "timeout": 3000, + "buffer_size": 1000 + } + }, + "handler": { + "upstream": [ + { + "ip": "1.1.1.1", + "port": 53, + "protocol": "udp", + "timeout": 400 + } + ], + "geoip": { + "enable": false, + "country_db": "geoCity.mmdb", + "asn_db": "geoIsp.mmdb" + }, + "healthcheck": { + "enable": false, + "max_requests": 10, + "max_pending_requests": 100, + "update_interval": 600, + "check_interval": 600, + "redis": { + "address": "127.0.0.1:6379", + "net": "tcp", + "db": 0, + "password": "", + "prefix": "redins_", + "suffix": "_redins", + "connection": { + "max_idle_connections": 10, + "max_active_connections": 10, + "connect_timeout": 500, + "read_timeout": 500, + "idle_keep_alive": 30, + "max_keep_alive": 0, + "wait_for_connection": false + } + }, + "log": { + "enable": true, + "target": "file", + "level": "info", + "path": "/tmp/healthcheck.log", + "format": "json", + "time_format": "2006-01-02T15:04:05Z07:00", + "sentry": { + "enable": false, + "dsn": "" + }, + "syslog": { + "enable": false, + "protocol": "tcp", + "address": "localhost:514" + }, + "kafka": { + "enable": false, + "topic": "redins", + "brokers": [ + "127.0.0.1:9092" + ], + "format": "json", + "compression": "none", + "timeout": 3000, + "buffer_size": 1000 + } + } + }, + "max_ttl": 3600, + "cache_timeout": 60, + "zone_reload": 600, + "log_source_location": false, + "redis": { + "address": "127.0.0.1:6379", + "net": "tcp", + "db": 0, + "password": "", + "prefix": "redins_", + "suffix": "_redins", + "connection": { + "max_idle_connections": 10, + "max_active_connections": 10, + "connect_timeout": 500, + "read_timeout": 500, + "idle_keep_alive": 30, + "max_keep_alive": 0, + "wait_for_connection": false + } + }, + "log": { + "enable": true, + "target": "file", + "level": "info", + "path": "/tmp/redins.log", + "format": "json", + "time_format": "2006-01-02T15:04:05Z07:00", + "sentry": { + "enable": false, + "dsn": "" + }, + "syslog": { + "enable": false, + "protocol": "tcp", + "address": "localhost:514" + }, + "kafka": { + "enable": false, + "topic": "redins", + "brokers": [ + "127.0.0.1:9092" + ], + "format": "json", + "compression": "none", + "timeout": 3000, + "buffer_size": 1000 + } + } + }, + "ratelimit": { + "enable": false, + "burst": 10, + "rate": 60, + "whitelist": [], + "blacklist": [] + } +} \ No newline at end of file diff --git a/tools/perf/bulk/bulk.go b/tools/perf/bulk/bulk.go new file mode 100644 index 0000000000000000000000000000000000000000..4b8524b4d158e81b9173d56e203ced6d301f4edd --- /dev/null +++ b/tools/perf/bulk/bulk.go @@ -0,0 +1,90 @@ +package main + +import ( + "bufio" + "flag" + "fmt" + "github.com/miekg/dns" + "math/rand" + "os" + "time" +) + +type query struct { + queryAddr string + queryType int + resultCode int + queryResult string +} + +func main() { + numQueries := flag.Int64("num", 10000, "number of queries") + flag.Parse() + + client := &dns.Client{ + Net: "udp", + Timeout: time.Millisecond * 100, + } + + var queries []query + + fq, err := os.Open("../query.txt") + if err != nil { + fmt.Println("cannot open query.txt") + return + } + defer fq.Close() + rq := bufio.NewReader(fq) + var duration time.Duration + for { + line, err := rq.ReadString('\n') + if err != nil { + break + } + var q query + // fmt.Println("line = ", line) + fmt.Sscan(line, &q.queryAddr, &q.queryType, &q.resultCode, &q.queryResult) + // fmt.Println("addr = ", queryAddr, "result = ", queryResult) + queries = append(queries, q) + } + fmt.Println(*numQueries) + for i := int64(0); i < *numQueries; i++ { + if i%1000000 == 0 { + println(i) + } + q := queries[rand.Int()%len(queries)] + m := new(dns.Msg) + m.SetQuestion(q.queryAddr, uint16(q.queryType)) + r, rtt, err := client.Exchange(m, "localhost:1053") + if err != nil { + fmt.Println("error: ", err, " ", q.queryAddr) + continue + } + if r.Rcode != q.resultCode { + fmt.Println("bad response : ", r.Rcode) + break + } + if q.resultCode == dns.RcodeSuccess { + if len(r.Answer) == 0 { + fmt.Println("empty response") + break + } + switch uint16(q.queryType) { + case dns.TypeA: + a := r.Answer[0].(*dns.A) + if a.A.String() != q.queryResult { + fmt.Printf("error: incorrect answer : expected %s got %s", q.queryResult, a.A.String()) + break + } + case dns.TypeTXT: + txt := r.Answer[0].(*dns.TXT) + if txt.Txt[0] != q.queryResult { + fmt.Printf("error: incorrect answer : expected %s got %s", q.queryResult, txt.Txt[0]) + break + } + } + duration += rtt + } + } + fmt.Println(duration) +} diff --git a/perf/gen/gen.go b/tools/perf/gen/gen.go similarity index 55% rename from perf/gen/gen.go rename to tools/perf/gen/gen.go index 901c149f2845676fa74d538444990b9f0c25874b..181d76b1b24563815dc4df68348b1101b9cda1f1 100644 --- a/perf/gen/gen.go +++ b/tools/perf/gen/gen.go @@ -4,6 +4,7 @@ import ( "bufio" "flag" "fmt" + "github.com/miekg/dns" "math/rand" "os" "time" @@ -42,6 +43,7 @@ func RandomString(n int) string { func main() { zonesPtr := flag.Int("zones", 10, "number of zones") entriesPtr := flag.Int("entries", 100, "number of entries per zone") + typePtr := flag.String("type", "a", "record type") // missChancePtr := flag.Int("miss", 30, "miss chance") redisAddrPtr := flag.String("addr", "localhost:6379", "redis address") @@ -68,17 +70,17 @@ func main() { wq := bufio.NewWriter(fq) for i := 0; i < *zonesPtr; i++ { + fmt.Println("zone :", i) zoneName := RandomString(15) + suffix fz, err := os.Create("../" + zoneName) if err != nil { - fmt.Println("cannot open file " + zoneName) + fmt.Println("cannot open file "+zoneName, " : ", err) return } - defer fz.Close() con.Do("SADD", "redins:zones", zoneName) wz := bufio.NewWriter(fz) wz.WriteString("$ORIGIN " + zoneName + "\n" + - "$TTL 86400\n\n" + + "$TTL 300\n\n" + "@ SOA ns1 hostmaster (\n" + "1 ; serial\n" + "7200 ; refresh\n" + @@ -90,16 +92,51 @@ func main() { "ns1 A 1.2.3.4\n\n") for j := 0; j < *entriesPtr; j++ { - location := RandomString(15) - ip := fmt.Sprintf("%d.%d.%d.%d", rand.Intn(256), rand.Intn(256), rand.Intn(256), rand.Intn(256)) - con.Do("HSET", "redins:zones:"+zoneName, location, `{"a":{"ttl":300, "records":[{"ip":"`+ip+`"}]}}`) + fmt.Println("record :", j) + switch *typePtr { + case "cname": + location1 := RandomString(15) + location2 := RandomString(15) - wq.WriteString(location + "." + zoneName + " " + ip + "\n") + con.Do("HSET", "redins:zones:"+zoneName, location1, `{"cname":{"ttl":300, "host":"`+location2+"."+zoneName+`."}}`) + ip := fmt.Sprintf("%d.%d.%d.%d", rand.Intn(256), rand.Intn(256), rand.Intn(256), rand.Intn(256)) - wz.WriteString(location + " A " + ip + "\n") + con.Do("HSET", "redins:zones:"+zoneName, location2, `{"a":{"ttl":300, "records":[{"ip":"`+ip+`"}]}}`) + + wq.WriteString(fmt.Sprintf("%s.%s %d %d %s\n", location1, zoneName, dns.TypeA, dns.RcodeSuccess, ip)) + + wz.WriteString(location1 + " CNAME " + location2 + "\n") + wz.WriteString(location2 + " A " + ip + "\n") + + case "txt": + location := RandomString(15) + txt := RandomString(200) + + con.Do("HSET", "redins:zones:"+zoneName, location, `{"txt":{"ttl":300, "records:{"text":"`+txt+`"}"}}`) + + wq.WriteString(fmt.Sprintf("%s.%s %d %d %s\n", location, zoneName, dns.TypeTXT, dns.RcodeSuccess, txt)) + wz.WriteString(location + ` TXT "` + txt + `"`) + + case "nxdomain": + location := RandomString(15) + wq.WriteString(fmt.Sprintf("%s.%s %d %d\n", location, zoneName, dns.TypeA, dns.RcodeNameError)) + + case "a": + fallthrough + default: + location := RandomString(15) + + ip := fmt.Sprintf("%d.%d.%d.%d", rand.Intn(256), rand.Intn(256), rand.Intn(256), rand.Intn(256)) + + con.Do("HSET", "redins:zones:"+zoneName, location, `{"a":{"ttl":300, "records":[{"ip":"`+ip+`"}]}}`) + + wq.WriteString(fmt.Sprintf("%s.%s %d %d %s\n", location, zoneName, dns.TypeA, dns.RcodeSuccess, ip)) + wz.WriteString(location + " A " + ip + "\n") + } } wz.Flush() + fz.Close() } wq.Flush() } diff --git a/tools/query/query.go b/tools/query/query.go new file mode 100644 index 0000000000000000000000000000000000000000..998c221f27ebd5e8241a583d5e0734fbc991a3c5 --- /dev/null +++ b/tools/query/query.go @@ -0,0 +1,84 @@ +package main + +import ( + "arvancloud/redins/tools/query/query" + "arvancloud/redins/tools/query/source" + "arvancloud/redins/tools/query/tool" + "flag" + "fmt" + "github.com/hawell/workerpool" + "github.com/miekg/dns" + "time" +) + +func main() { + sourcePtr := flag.String("source", "redis", "data source: redis, file") + toolPtr := flag.String("tool", "bench", "tool: bench, compare") + s1AddrPtr := flag.String("s1", "localhost:1053", "server 1") + s2AddrPtr := flag.String("s2", "localhost:2053", "server 2") + sourceAddrPtr := flag.String("source-address", "localhost:6379", "source address") + MaxWorkersPtr := flag.Int("threads", 10, "number of threads") + MaxQueries := flag.Int("max-queries", 1000000, "maximum queries") + + flag.Parse() + + var g source.QueryGenerator + switch *sourcePtr { + case "redis": + g = source.NewRedisDumpQueryGenerator(*sourceAddrPtr) + case "file": + g = source.NewFileQueryGenerator(*sourceAddrPtr, *MaxQueries) + default: + fmt.Println("invalid query source : ", *sourcePtr) + return + } + + var t tool.QueryTool + switch *toolPtr { + case "compare": + t = tool.NewCompareTool(*s1AddrPtr, *s2AddrPtr) + case "bench": + t = tool.NewBenchTool(*s1AddrPtr) + default: + fmt.Println("invalid tool : ", *toolPtr) + return + } + + maxWorkers := *MaxWorkersPtr + + dispatcher := workerpool.NewDispatcher(10000, maxWorkers) + var count []int + var clients []*dns.Client + handler := func(worker *workerpool.Worker, job workerpool.Job) { + q := job.(query.Query) + count[worker.Id]++ + //fmt.Println(q) + t.Act(q,clients[worker.Id]) + } + for i := 0; i < maxWorkers; i++ { + client := &dns.Client{ + Net: "udp", + Timeout: time.Millisecond * 4000, + } + clients = append(clients, client) + dispatcher.AddWorker(handler) + count = append(count, 0) + } + + dispatcher.Run() + for i := 0; i < g.Count(); i++ { + dispatcher.Queue(g.GetQuery()) + } + var totalCount int + for totalCount != g.Count() { + totalCount = 0 + for _, c := range count { + totalCount += c + } + time.Sleep(time.Second) + } + time.Sleep(time.Second) + + fmt.Println("total : ", count) + t.Result() +} diff --git a/tools/query/query/query.go b/tools/query/query/query.go new file mode 100644 index 0000000000000000000000000000000000000000..7056d7a5546190b3c620352b1ba6289575412b2e --- /dev/null +++ b/tools/query/query/query.go @@ -0,0 +1,7 @@ +package query + +type Query struct { + QName string + QType string +} + diff --git a/tools/query/source/file.go b/tools/query/source/file.go new file mode 100644 index 0000000000000000000000000000000000000000..aca6f699ce324dc624a1cf27f2acd156cee0cf22 --- /dev/null +++ b/tools/query/source/file.go @@ -0,0 +1,58 @@ +package source + +import ( + "arvancloud/redins/tools/query/query" + "bufio" + "fmt" + "math/rand" + "os" +) + +type FileQueryGenerator struct { + queries []query.Query + count int + zipf *rand.Zipf +} + +func NewFileQueryGenerator(path string, count int) *FileQueryGenerator { + g := &FileQueryGenerator{ + count: count, + } + + f, _ := os.Open(path) + scanner := bufio.NewScanner(f) + scanner.Split(bufio.ScanLines) + + for scanner.Scan() { + var q query.Query + line := scanner.Text() + if _, err := fmt.Sscanf(line,"%s%s", &q.QName, &q.QType); err != nil { + fmt.Println(err) + } + // fmt.Println(q) + g.queries = append(g.queries, q) + } + + f.Close() + + source := rand.NewSource(rand.Int63n(30)) + r := rand.New(source) + g.zipf = rand.NewZipf(r, 1.00001, 1, uint64(len(g.queries)-1)) + + return g +} + +func (g *FileQueryGenerator) Init() { + +} + +func (g *FileQueryGenerator) Count() int { + return g.count +} + +func (g *FileQueryGenerator) GetQuery() query.Query { + z := g.zipf.Uint64() + // fmt.Println(z) + return g.queries[z] +} + diff --git a/tools/query/source/redis.go b/tools/query/source/redis.go new file mode 100644 index 0000000000000000000000000000000000000000..8ea428f7298614f710efcbb9493e51cb95936d70 --- /dev/null +++ b/tools/query/source/redis.go @@ -0,0 +1,73 @@ +package source + +import ( + "arvancloud/redins/tools/query/query" + "github.com/hawell/uperdis" +) + +type RedisDumpQueryGenerator struct { + redisAddress string + queries []query.Query + pos int +} + +func NewRedisDumpQueryGenerator(redisAddress string) *RedisDumpQueryGenerator { + redis := uperdis.NewRedis(&uperdis.RedisConfig{ + Address: redisAddress, + Net: "tcp", + DB: 0, + Password: "", + Prefix: "", + Suffix: "_dns2", + Connection: uperdis.RedisConnectionConfig{ + MaxIdleConnections: 10, + MaxActiveConnections: 10, + ConnectTimeout: 500, + ReadTimeout: 500, + IdleKeepAlive: 30, + MaxKeepAlive: 0, + WaitForConnection: false, + }, + }) + g := new(RedisDumpQueryGenerator) + zones, _ := redis.SMembers("redins:zones") + for _, zone := range zones { + locations, _ := redis.GetHKeys("redins:zones:" + zone) + for _, location := range locations { + qname := "" + if location == "@" { + qname = zone + } else { + qname = location + "." + zone + } + g.queries = append(g.queries, query.Query{QName: qname, QType: "A"}) + g.queries = append(g.queries, query.Query{QName: qname, QType: "AAAA"}) + g.queries = append(g.queries, query.Query{QName: qname, QType: "CNAME"}) + g.queries = append(g.queries, query.Query{QName: qname, QType: "NS"}) + g.queries = append(g.queries, query.Query{QName: qname, QType: "MX"}) + g.queries = append(g.queries, query.Query{QName: qname, QType: "SRV"}) + g.queries = append(g.queries, query.Query{QName: qname, QType: "TXT"}) + g.queries = append(g.queries, query.Query{QName: qname, QType: "PTR"}) + g.queries = append(g.queries, query.Query{QName: qname, QType: "CAA"}) + g.queries = append(g.queries, query.Query{QName: qname, QType: "TLSA"}) + g.queries = append(g.queries, query.Query{QName: qname, QType: "SOA"}) + g.queries = append(g.queries, query.Query{QName: qname, QType: "DNSKEY"}) + } + } + return g +} + +func (g *RedisDumpQueryGenerator) Init() { + +} + +func (g *RedisDumpQueryGenerator) Count() int { + return len(g.queries) +} + +func (g *RedisDumpQueryGenerator) GetQuery() query.Query { + q := g.queries[g.pos] + g.pos++ + return q +} + diff --git a/tools/query/source/source.go b/tools/query/source/source.go new file mode 100644 index 0000000000000000000000000000000000000000..8bba2ea35f9b681df35c65587d3ed8f858da84e1 --- /dev/null +++ b/tools/query/source/source.go @@ -0,0 +1,10 @@ +package source + +import "arvancloud/redins/tools/query/query" + +type QueryGenerator interface { + Init() + Count() int + GetQuery() query.Query +} + diff --git a/tools/query/tool/bench.go b/tools/query/tool/bench.go new file mode 100644 index 0000000000000000000000000000000000000000..bfd245e3435617cac4c9acf389e35d6c80a7100d --- /dev/null +++ b/tools/query/tool/bench.go @@ -0,0 +1,59 @@ +package tool + +import ( + "arvancloud/redins/tools/query/query" + "fmt" + "github.com/miekg/dns" + "sync" + "time" +) + +type BenchTool struct { + count int + totalTime time.Duration + min time.Duration + max time.Duration + mean float64 + stdev float64 + serverAddress string + mutex sync.Mutex +} + +func NewBenchTool(serverAddress string) *BenchTool { + return &BenchTool{ + min: 1000, + serverAddress: serverAddress, + } +} + +func (t *BenchTool) Act(q query.Query, client *dns.Client) { + m := new(dns.Msg) + m.SetQuestion(q.QName, dns.StringToType[q.QType]) + //fmt.Println(m) + _, rtt, err := client.Exchange(m, t.serverAddress) + if err != nil { + fmt.Println(err) + } + t.mutex.Lock() + prevMean := t.mean + t.count++ + x := rtt.Seconds() + t.mean = t.mean + (x-t.mean) / float64(t.count) + t.stdev = t.stdev + (x-t.mean)*(x-prevMean) + if rtt < t.min { + t.min = rtt + } + if rtt > t.max { + t.max = rtt + } + t.totalTime += rtt + t.mutex.Unlock() +} + +func (t *BenchTool) Result() { + fmt.Println("total time : ", t.totalTime) + fmt.Println("min : ", t.min) + fmt.Println("max : ", t.max) + fmt.Println("mean : ", t.mean*1000000, " us") + fmt.Println("stdev : ", t.stdev*1000, " ms") +} diff --git a/tools/query/tool/compare.go b/tools/query/tool/compare.go new file mode 100644 index 0000000000000000000000000000000000000000..7864649bb3cf93350bb0427f1b5efa48fa291ddf --- /dev/null +++ b/tools/query/tool/compare.go @@ -0,0 +1,55 @@ +package tool + +import ( + "arvancloud/redins/test" + "arvancloud/redins/tools/query/query" + "fmt" + "github.com/miekg/dns" + "sort" +) + +type CompareTool struct { + server1Address string + server2Address string +} + +func NewCompareTool(s1 string, s2 string) *CompareTool { + t := &CompareTool{ + server1Address: s1, + server2Address: s2, + } + return t +} + +func (t *CompareTool) Act(q query.Query, client *dns.Client) { + m := new(dns.Msg) + m.SetQuestion(q.QName, dns.StringToType[q.QType]) + //fmt.Println(m) + resp1, _, err1 := client.Exchange(m, t.server1Address) + resp2, _, err2 := client.Exchange(m, t.server2Address) + if err1 != err2 { + fmt.Println(q.QName, "->", q.QType, " : ", err1, " != ", err2) + } else if err1 == nil { + sort.Sort(test.RRSet(resp1.Answer)) + sort.Sort(test.RRSet(resp1.Ns)) + sort.Sort(test.RRSet(resp1.Extra)) + tc := test.Case{ + Qname: resp1.Question[0].Name, + Qtype: resp1.Question[0].Qtype, + Rcode: resp1.Rcode, + Do: false, + Answer: resp1.Answer, + Ns: resp1.Ns, + Extra: resp1.Extra, + Error: nil, + } + if err := test.SortAndCheck(resp2, tc); err != nil { + fmt.Println(q.QName, "->", q.QType, " : ", err) + } + } +} + +func (t *CompareTool) Result() { + +} + diff --git a/tools/query/tool/tool.go b/tools/query/tool/tool.go new file mode 100644 index 0000000000000000000000000000000000000000..c13ae9725a1a01df604041cf9657c6b68a3e93e9 --- /dev/null +++ b/tools/query/tool/tool.go @@ -0,0 +1,12 @@ +package tool + +import ( + "arvancloud/redins/tools/query/query" + "github.com/miekg/dns" +) + +type QueryTool interface { + Act(q query.Query, client *dns.Client) + Result() +} +