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, &currentRecord.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, &currentRecord.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, &currentRecord.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, &currentRecord.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()
+}
+