This is an automated email from the ASF dual-hosted git repository. paziz pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit a197ef5989b11e3e52ea099dec2ee5e3ad9fda5b Author: Persia Aziz <per...@yahoo-inc.com> AuthorDate: Fri Aug 25 13:59:29 2017 -0500 microDNS --- tests/tools/microDNS/README.md | 46 ++++++++ tests/tools/microDNS/sample_zonefile.json | 9 ++ tests/tools/microDNS/uDNS.py | 175 ++++++++++++++++++++++++++++++ 3 files changed, 230 insertions(+) diff --git a/tests/tools/microDNS/README.md b/tests/tools/microDNS/README.md new file mode 100644 index 0000000..e655d31 --- /dev/null +++ b/tests/tools/microDNS/README.md @@ -0,0 +1,46 @@ +uDNS +===== + +uDNS is a small DNS server that takes in a pre-defined set of domain names and the IPs that each domain name maps to. The mappings should be inputted with a JSON file in a format described below. + +uDNS runs on localhost and serves whatever port is specified in the command line arguments. uDNS serves both UDP and TCP connections. + +If uDNS does not find the requested domain in the explicitly mapped section of the JSON, uDNS will respond with the IPs given in the `otherwise` section of the JSON. The `otherwise` section is mandatory. + + +JSON format +------ +```json +{ + "mappings": [ + {"domain1": ["ip1", "ip2", "etc"]}, + {"domain2": ["ip3", "ip4", "etc"]}, + {"domain3": ["ip5"]}, + ], + + "otherwise": ["defaultip1", "defaultip2", "etc"] +} +``` + +An example can be found in `sample_zonefile.json` + + +Caveat +------ +You should not include any two records like this: `host1.example.com` and `example.com` + +A DNS request for `host1.example.com` could return the A-record associated with `host1.example.com` or `example.com`, depending on your luck. + + +Running +------ +`python3 uDNS.py port zone_file [--rr]` + +For a detailed description of flags, see `python3 uDNS.py -h` + + +Use with Apache Traffic Server +------ +1. In `records.config`, add configuration lines: `CONFIG proxy.config.dns.nameservers STRING 127.0.0.1:PORT` and `CONFIG proxy.config.dns.round_robin_nameservers INT 0`, where `PORT` is whatever port you want uDNS to serve on. +2. Run uDNS on `PORT` +3. Now all domains mapped in the uDNS JSON config file should be mapped by ATS as well diff --git a/tests/tools/microDNS/sample_zonefile.json b/tests/tools/microDNS/sample_zonefile.json new file mode 100644 index 0000000..e4c8282 --- /dev/null +++ b/tests/tools/microDNS/sample_zonefile.json @@ -0,0 +1,9 @@ +{ + "mappings": [ + {"abc.xyz.com.": ["127.0.0.1","127.0.1.1"]}, + {"yahoo.com.": ["128.0.0.1", "128.0.1.0"]}, + {"yelp.com.": ["34.35.166.23", "129.0.0.1"]} + ], + + "otherwise": ["127.0.0.1", "127.1.1.1"] +} diff --git a/tests/tools/microDNS/uDNS.py b/tests/tools/microDNS/uDNS.py new file mode 100644 index 0000000..f82d48e --- /dev/null +++ b/tests/tools/microDNS/uDNS.py @@ -0,0 +1,175 @@ +# coding=utf-8 + +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import datetime +import sys +import time +import threading +import traceback +import socketserver +import argparse +import codecs +import json +from dnslib import * + +TTL = 60 * 5 # completely arbitrary TTL value +round_robin = False +default_records = list() +records = dict() + +class DomainName(str): + def __getattr__(self, item): + return DomainName(item + '.' + self) + +class BaseRequestHandler(socketserver.BaseRequestHandler): + + def get_data(self): + raise NotImplementedError + + def send_data(self, data): + raise NotImplementedError + + def handle(self): + now = datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f') + print("\n\n%s request %s (%s %s):" % (self.__class__.__name__[:3], now, self.client_address[0], + self.client_address[1])) + try: + data = self.get_data() + self.send_data(dns_response(data)) + except Exception: + traceback.print_exc(file=sys.stderr) + + +class TCPRequestHandler(BaseRequestHandler): + + def get_data(self): + data = self.request.recv(8192).strip() + sz = int(codecs.encode(data[:2], 'hex'), 16) + if sz < len(data) - 2: + raise Exception("Wrong size of TCP packet") + elif sz > len(data) - 2: + raise Exception("Too big TCP packet") + return data[2:] + + def send_data(self, data): + sz = codecs.decode(hex(len(data))[2:].zfill(4), 'hex') + return self.request.sendall(sz + data) + + +class UDPRequestHandler(BaseRequestHandler): + + def get_data(self): + return self.request[0].strip() + + def send_data(self, data): + return self.request[1].sendto(data, self.client_address) + +def build_domain_mappings(path): + with open(path) as f: + zone_file = json.load(f) + + for domain in zone_file['mappings']: + for d in iter(domain.keys()): + # this loop only runs once, kind of a hack to access the only key in the dict + domain_name = DomainName(d) + print("Domain name:",domain_name) + records[domain_name] = [A(x) for x in domain[domain_name]] + print(records[domain_name]) + + default_records.extend([A(d) for d in zone_file['otherwise']]) + +def add_authoritative_records(reply, domain): + # ns1 and ns1 are hardcoded in, change if necessary + reply.add_auth(RR(rname=domain, rtype=QTYPE.NS, rclass=1, ttl=TTL, rdata=NS(domain.ns1))) + reply.add_auth(RR(rname=domain, rtype=QTYPE.NS, rclass=1, ttl=TTL, rdata=NS(domain.ns2))) + +def dns_response(data): + ''' dns_response takes in the raw bytes from the socket and does all the logic behind what + RRs get returned as the response ''' + global default_records, records, TTL, round_robin + + request = DNSRecord.parse(data) + print(request) + + reply = DNSRecord(DNSHeader(id=request.header.id, qr=1, aa=1, ra=1), q=request.q) + qname = request.q.qname + qn = str(qname) + qtype = request.q.qtype + qt = QTYPE[qtype] + found_specific = False + + # first look for a specific mapping + for domain, rrs in records.items(): + if domain == qn or qn.endswith('.' + domain): + # we are the authoritative name server for this domain and all subdomains + for rdata in rrs: + # only include requested record types (ie. A, MX, etc) + rqt = rdata.__class__.__name__ + if qt in ['*', rqt]: + found_specific = True + reply.add_answer(RR(rname=qname, rtype=getattr(QTYPE, str(rqt)), rclass=1, ttl=TTL, rdata=rdata)) + + # rotate the A entries if round robin is on + if round_robin: + a_records = [x for x in rrs if type(x) == A] + records[domain] = a_records[1:] + a_records[:1] # rotate list + break + + # else if a specific mapping is not found, return default A-records + if not found_specific: + for a in default_records: + reply.add_answer(RR(rname=qname, rtype=QTYPE.A, rclass=1, ttl=TTL, rdata=a)) + + if round_robin: + default_records = default_records[1:] + default_records[:1] + print("---- Reply: ----\n", reply) + return reply.pack() + + +if __name__ == '__main__': + # handle cmd line args + parser = argparse.ArgumentParser() + parser.add_argument("ip_addr",type=str, help="Interface",default="127.0.0.1") + parser.add_argument("port", type=int, help="port uDNS should listen on") + parser.add_argument("zone_file", help="path to zone file") + parser.add_argument("--rr", action='store_true', help='round robin load balances if multiple IP addresses are present for 1 domain') + args = parser.parse_args() + + if args.rr: + round_robin = True + build_domain_mappings(args.zone_file) + + servers = [ + socketserver.ThreadingUDPServer((args.ip_addr, args.port), UDPRequestHandler), + socketserver.ThreadingTCPServer((args.ip_addr, args.port), TCPRequestHandler), + ] + + print("Starting DNS...") + for s in servers: + thread = threading.Thread(target=s.serve_forever) # that thread will start one more thread for each request + thread.daemon = True # exit the server thread when the main thread terminates + thread.start() + + try: + while 1: + time.sleep(1) + sys.stderr.flush() + sys.stdout.flush() + + except KeyboardInterrupt: + pass + finally: + for s in servers: + s.shutdown() -- To stop receiving notification emails like this one, please contact "commits@trafficserver.apache.org" <commits@trafficserver.apache.org>.