├── fixed_zone ├── primary.txt └── tests.txt ├── run_server.sh ├── burrow_logging.py ├── .gitignore ├── README.md ├── session.py └── server.py /fixed_zone/primary.txt: -------------------------------------------------------------------------------- 1 | ns1.burrow.tech. 60 IN A 131.215.172.230 2 | ns2.burrow.tech. 60 IN A 131.215.172.230 3 | burrow.tech. 60 IN TXT 'Go away!!' 4 | -------------------------------------------------------------------------------- /run_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | trap clean_exit SIGINT 4 | 5 | function clean_exit() { 6 | echo "Shutting down..." 7 | pkill -x -f "python server.py" 8 | exit 0 9 | } 10 | 11 | while [ 1 ] 12 | do 13 | # start python script and detach 14 | echo "Starting python server" 15 | python server.py & 16 | echo "Python server started as $!" 17 | # wait until the script is modified 18 | echo "Waiting for the file to be modified" 19 | inotifywait -e modify server.py session.py burrow_logging.py 20 | echo "The file was modified!" 21 | # send SIGTERM to the script 22 | echo "Sending SIGTERM to the script" 23 | pgrep -x -f "python server.py" -l 24 | kill $! 25 | echo "Waiting two seconds..." 26 | sleep 2 27 | pgrep -x -f "server.py" -l 28 | done 29 | 30 | -------------------------------------------------------------------------------- /burrow_logging.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | import sys 3 | 4 | # This background thread is responsible for recording information from the 5 | # transmission layer and printing it to both console and log.txt. 6 | def bg_log(q): 7 | with open("log.txt", "a") as f: 8 | while True: 9 | item = q.get() 10 | if item is None: 11 | continue 12 | else: 13 | print(item) 14 | sys.stdout.flush() 15 | f.write(item + "\n") 16 | f.flush() 17 | 18 | logevent_queue = multiprocessing.Queue() 19 | logger_process = multiprocessing.Process(target=bg_log, args=(logevent_queue,)) 20 | print("Starting background logging thread.") 21 | logger_process.start() 22 | 23 | def burrow_log(string_to_log, number_of_spaces): 24 | logevent_queue.put(" " * number_of_spaces + string_to_log) 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.swp 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask instance folder 60 | instance/ 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # IPython Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # dotenv 81 | .env 82 | 83 | # virtualenv 84 | venv/ 85 | ENV/ 86 | 87 | # Spyder project settings 88 | .spyderproject 89 | 90 | # Rope project settings 91 | .ropeproject 92 | -------------------------------------------------------------------------------- /fixed_zone/tests.txt: -------------------------------------------------------------------------------- 1 | constant.test.burrow.tech. 60 IN TXT 'I am the constant record.' 2 | bacon.test.burrow.tech. 60 IN TXT "Bacon ipsum dolor amet frankfurter filet mignon tenderloin, jowl short loin corned beef jerky beef ribs spare ribs. Kevin bresaola venison jowl filet mignon. Turducken pork belly pig ball tip tail, alcatra brisket leberkas tri-tip " "fatback jerky pancetta filet mignon tenderloin. Landjaeger cupim drumstick rump shankle doner cow. Meatball prosciutto tri-tip, doner bresaola landjaeger ball tip andouille pork chop cupim ground round ribeye drumstick pastrami. " "Cow tenderloin picanha prosciutto pancetta, fatback andouille shoulder. Pig drumstick cow, landjaeger short loin chuck beef ribs. Andouille swine leberkas jowl ribeye doner biltong cupim ball tip prosciutto corned beef. T-bone sirloin filet" " mignon tongue alcatra shank pig short ribs pork belly tenderloin ribeye. Beef picanha pork t-bone bacon tail salami fatback frankfurter ribeye doner turducken. Porchetta doner rump short loin turducken tenderloin sausage pork. Tenderloin t-bone " "tri-tip shankle. Tri-tip ground round pork belly, landjaeger ham pancetta bresaola meatball ribeye strip steak pig alcatra. Alcatra sausage tri-tip biltong shoulder bresaola. Shankle swine cow, sausage brisket short loin picanha kielbasa" " turkey strip steak t-bone tongue hamburger. Shank ham hock pork loin, fatback alcatra andouille prosciutto short loin pastrami shankle hamburger. Boudin ham hamburger filet mignon bacon drumstick. Pork chop prosciutto capicola" 3 | babies.test.burrow.tech. 60 IN TXT "Hello world 1" 4 | babies.test.burrow.tech. 60 IN TXT "Hello world 2" 5 | babies.test.burrow.tech. 60 IN TXT "Hello world 3" 6 | babies.test.burrow.tech. 60 IN TXT "Hello world 4" 7 | babies.test.burrow.tech. 60 IN TXT "Hello world 5" 8 | babies.test.burrow.tech. 60 IN TXT "Hello world 6" 9 | babies.test.burrow.tech. 60 IN TXT "Hello world 7" 10 | babies.test.burrow.tech. 60 IN TXT "Hello world 8" 11 | babies.test.burrow.tech. 60 IN TXT "Hello world 9" 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Burrow Server 2 | 3 | Burrow operates using two layers. The Transmission layer handles communicating 4 | arbitrary amounts of data between the server and the client. The Session layer, which is built 5 | atop the Transmission layer, handles forwarding packets from the client and returning response packets to the client. 6 | 7 | ## Transmission layer 8 | Here's how we would send the message "thisissomesampledataforustouse" to the server, across 3 separate DNS lookups. 9 | 10 | **1) Begin the transmission** 11 | ``` 12 | dig -t txt .begin.burrow.tech 13 | ``` 14 | (the garbage is required to defeat caching of the naked begin endpoint) 15 | 16 | The server will return a transmission ID like `2a591c8b`. 17 | 18 | **2) Continue the transmission** 19 | ``` 20 | dig -t txt thisissome.0.2a591c8b.continue.burrow.tech 21 | dig -t txt sampledata.1.2a591c8b.continue.burrow.tech 22 | dig -t txt forustouse.2.2a591c8b.continue.burrow.tech 23 | ``` 24 | The indices (0, 1, 2) are required because DNS lookups are sometimes duplicated, and to allow all lookups to be done in parallel. 25 | 26 | Note that any data sent through the transmission layer must be domain-safe. 27 | 28 | **3) End the transmission** 29 | ``` 30 | dig -t txt 3.2a591c8b.end.burrow.tech 31 | ``` 32 | The length (3) is there for error detection - if it doesn't match the number of `continue` lookups received, the transmission will fail. 33 | 34 | Of course, we would never send the message "thisissomesampledataforustouse" to the server. Instead, the messages we send to the server have the following special format. 35 | 36 | 37 | ## Session layer 38 | 39 | ### Message Types 40 | 41 | The following messages are used to set up, utilize, and tear down a Burrow tunnel. 42 | 43 | | Message | Client Message Format | Server Response Format | 44 | |-----------------|--------------------------------------------|--------------------------| 45 | | Begin Session | `b` | `s-[session identifier]` | 46 | | Forward Packets | `f-[session identifier]-[packet data]-...` | `s` | 47 | | Request Packets | `r-[session identifier]` | `s-[packet data]-...` | 48 | | End Session | `e-[session identifier]` | `s` | 49 | | Test (reverse) | `test-helloworld` | `dlrowolleh-tset` | 50 | 51 | In both cases, packet data is Base64-encoded. 52 | 53 | The first dash-separated component of the client message identifies the message type, and the following components 54 | are the arguments of the message. 55 | 56 | The server response doesn't contain a message type since it is sent in response to a client message. 57 | Instead, the server response uses the first component to indicate success or failure. 58 | If the first component is `s`, the operation was successful and the rest of the components 59 | contain the information returned in response to this message type. 60 | If the first component is `f` however, the response indicates a failure. 61 | 62 | | Result | Server Format | 63 | |---------|---------------------------------------------| 64 | | Success | `s-[information...]` | 65 | | Failure | `f-[error code]-[reason]-[associated data]` | 66 | 67 | 68 | #### Error Codes 69 | 70 | The following error codes exist. Note that new error codes should be added to the end of the list in order to maintain 71 | compatibility. If error codes must be added earlier in the list, both the client and server must be 72 | updated. 73 | 74 | | Error Code | Error Type | 75 | |------------|----------------------------| 76 | | 0 | Unknown Failure | 77 | | 1 | Unknown Message Type | 78 | | 2 | Unknown Session Identifier | 79 | 80 | -------------------------------------------------------------------------------- /session.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | import base64 3 | import multiprocessing 4 | import Queue 5 | import sys 6 | 7 | from scapy import route 8 | from scapy.layers.inet import IP 9 | from scapy.layers.inet import TCP 10 | from scapy.layers.inet import UDP 11 | from scapy.all import sr 12 | 13 | from burrow_logging import burrow_log 14 | def LOG(s): 15 | burrow_log(s, 8) 16 | 17 | NO_ERROR = 0 18 | INVALID_PACKET = 1 19 | NO_FREE_PORTS = 2 20 | 21 | # Technically, DNS responses can be up to 64KB. We aren't looking to 22 | # find that limit here, though. This would be a good value to experiment 23 | # with optimizing. 24 | MAX_RESPONSE_SIZE = 8000 25 | 26 | SR_TIMEOUT = 60 # seconds 27 | 28 | SERVER_IP = "131.215.172.230" 29 | available_ports = range(30000,50000) #ports will be removed from this list while in use 30 | sessions = {} 31 | 32 | 33 | def sizeof_list(l): 34 | size = 0 35 | for i in l: 36 | size += sys.getsizeof(i) 37 | size += sys.getsizeof(l) 38 | return size 39 | 40 | class Session: 41 | def __init__(self, id): 42 | self.id = id 43 | self.pending_response_packets = multiprocessing.Queue() 44 | 45 | def request(self): 46 | response_packets = [] 47 | while sizeof_list(response_packets) < MAX_RESPONSE_SIZE: 48 | try: 49 | r_pkt = self.pending_response_packets.get_nowait() 50 | response_packets.append(r_pkt) 51 | except Queue.Empty: 52 | break 53 | return response_packets 54 | 55 | def sendreceive_packet_with_timeout(self, secs, packet, original_src, original_sport, protocol, spoofed_sport): 56 | p = multiprocessing.Process(target=self.sendreceive_packet, args=(packet, original_src, original_sport, protocol, spoofed_sport,)) 57 | p.start() 58 | p.join(secs) 59 | if p.is_alive(): 60 | LOG("Warning: long-running sr process terminated.") 61 | p.terminate() 62 | p.join() 63 | 64 | def sendreceive_packet(self, packet, original_src, original_sport, protocol, spoofed_sport): 65 | #send packet and receive responses. 66 | #self.ans will contain a list of tuples of sent packets and their responses 67 | #self.unans will contain a list of unanswered packets 68 | LOG("About to forward packet for " + self.id) 69 | ans, unans = sr(packet, verbose=0) 70 | #un-spoof the source IP address and port, 71 | #then add to the list of packets waiting to be sent back 72 | for pair in ans: 73 | LOG("Received response packet for " + self.id) 74 | response = pair[1] 75 | response[IP].src = original_src 76 | response[protocol].sport = original_sport 77 | response = IP(str(response)) #recalculate all the checksums 78 | self.pending_response_packets.put(base64.b64encode(str(response))) 79 | LOG("Appended response packet, session " + self.id + " (" + str(id(self)) + ") now has " + str(self.pending_response_packets.qsize()) + " packets waiting to be pulled from list " + str(id(self.pending_response_packets))) 80 | available_ports.append(spoofed_sport) #return port to available pool 81 | 82 | def forward(self, message): 83 | pkt = IP(message) #parse the binary data to a scapy IP packet 84 | # pkt.show2() 85 | 86 | if IP not in pkt: 87 | return INVALID_PACKET 88 | # LOG("Forwarding packet to IP address " + str(pkt[IP].dst)) 89 | original_src = pkt[IP].src #store the original source IP 90 | pkt[IP].src = SERVER_IP #spoof the source IP so the packet comes back to us 91 | del pkt[IP].chksum #invalidate the checksum 92 | if len(available_ports) == 0: 93 | return NO_FREE_PORTS 94 | port = available_ports.pop(0) #get a port from our pool of available ports 95 | if TCP in pkt: 96 | protocol = TCP 97 | original_sport = pkt[TCP].sport #store the original source port 98 | pkt[TCP].sport = port #spoof the source port 99 | #pkt[TCP].dport = ____ 100 | del pkt[TCP].chksum #invalidate the checksum 101 | elif UDP in pkt: 102 | protocol = UDP 103 | original_sport = pkt[UDP].sport #ditto 104 | pkt[UDP].sport = port 105 | #pkt[UDP].dport = ____ 106 | del pkt[UDP].chksum 107 | else: 108 | return INVALID_PACKET 109 | 110 | pkt = IP(str(pkt)) #recalculate all the checksums 111 | 112 | # print "After spoofing, packet looks like:" 113 | # pkt.show2() 114 | 115 | p = multiprocessing.Process(target=self.sendreceive_packet_with_timeout, args=(SR_TIMEOUT, pkt, original_src, original_sport, protocol, port,)) 116 | p.start() 117 | 118 | return NO_ERROR 119 | 120 | 121 | def handle_message(message): 122 | response = "" 123 | components = iter(message.split('-')) 124 | type = components.next() 125 | if (type == 'b'): 126 | response = got_begin_session() 127 | elif (type == 'f'): 128 | response = got_forward_packets(components) 129 | elif (type == 'r'): 130 | response = got_request_packets(components) 131 | elif (type == 'e'): 132 | response = got_end_session(components) 133 | elif (type == 'test'): 134 | # reverse the string 135 | response = message[::-1] 136 | LOG("Session layer received test message, responding with " + response) 137 | else: 138 | # This should never happen 139 | response = "f-1-Message_type_`" + str(type) + "`_is_unkown." 140 | return response 141 | 142 | def got_begin_session(): 143 | session_id = uuid.uuid4().hex[-8:] 144 | sessions[session_id] = Session(session_id) 145 | LOG("Began session with id: " + str(session_id)) 146 | return "s-" + str(session_id) 147 | 148 | def got_forward_packets(components): 149 | session_id = components.next() 150 | if session_id not in sessions: 151 | return "f-2-Session_identifier_`" + str(session_id) + "`_is_unknown." 152 | session = sessions[session_id] 153 | packets = map(base64.b64decode, components) 154 | LOG("Forwarding " + str(len(packets)) + " packets for session " + str(session_id)) 155 | for packet in packets: 156 | # TODO: This only takes care of the last error? 157 | err = session.forward(packet) 158 | if err == NO_ERROR: 159 | return "s" 160 | elif err == INVALID_PACKET: 161 | LOG("Failed to forward invalid packet for session " + str(session_id)) 162 | return "f-0-Packet_is_Invalid" 163 | elif err == NO_FREE_PORT: 164 | LOG("Could not find a free port to forward packet for session " + str(session_id)) 165 | return "f-0-Could_not_find_a_free_port" 166 | 167 | def got_request_packets(components): 168 | session_id = components.next() 169 | if session_id not in sessions: 170 | return "f-2-Session_identifier_`" + str(session_id) + "`_is_unknown." 171 | session = sessions[session_id] 172 | data = session.request() 173 | LOG("Session " + str(session_id) + " requested packets, replying with " + str(len(data)) + " packets in " + str(sizeof_list(data)) + " bytes.") 174 | response = "s" 175 | for packet in data: 176 | response += "-" + packet 177 | return response 178 | 179 | def got_end_session(components): 180 | session_id = components.next() 181 | if session_id not in sessions: 182 | return "f-2-Session_identifier_`" + str(session_id) + "`_is_unknown." 183 | session = sessions[session_id] 184 | LOG("Ending session: " + str(session_id)) 185 | del sessions[session_id] 186 | return "s" 187 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import copy 4 | import json 5 | import uuid 6 | import collections 7 | import re 8 | import sys 9 | import multiprocessing 10 | 11 | from dnslib import RR,RCODE 12 | from dnslib.label import DNSLabel 13 | from dnslib.server import DNSServer, DNSHandler, BaseResolver, DNSLogger 14 | from expiringdict import ExpiringDict 15 | 16 | import session 17 | from burrow_logging import burrow_log 18 | def LOG(s): 19 | burrow_log(s, 4) 20 | 21 | # This function parses incoming DNS requests as per the Transmission 22 | # API format documented in README.md. 23 | # The Begin, Continue, and End API endpoints are the only endpoints 24 | # recognized. Requests that are properly formatted but refer to 25 | # unknown API endpoints are categorized as Other. 26 | # Incorrectly formatted requests are categorized as Failure. 27 | Begin = collections.namedtuple('Begin', 'prefix') 28 | Continue = collections.namedtuple('Continue', 'data index id') 29 | End = collections.namedtuple('End', 'length id') 30 | Other = collections.namedtuple('Other', 'host') 31 | Failure = collections.namedtuple('Failure', 'host') 32 | def parse_url(url): 33 | try: 34 | copy = url 35 | assert(isinstance(url, DNSLabel)) 36 | assert(url.matchSuffix("burrow.tech")) 37 | url = url.stripSuffix("burrow.tech") 38 | if url.matchSuffix("begin"): 39 | url = url.stripSuffix("begin") 40 | if len(url.label) < 1: 41 | raise ValueError 42 | return Begin(url.label[-1]) 43 | elif url.matchSuffix("continue"): 44 | url = url.stripSuffix("continue") 45 | if len(url.label) < 3: 46 | raise ValueError 47 | data = "".join(url.label[:-2]).replace(".", "") 48 | return Continue(data, int(url.label[-2]), url.label[-1]) 49 | elif url.matchSuffix("end"): 50 | url = url.stripSuffix("end") 51 | if len(url.label) < 2: 52 | raise ValueError 53 | return End(int(url.label[-2]), url.label[-1]) 54 | else: 55 | return Other(copy) 56 | except ValueError: 57 | return Failure(copy) 58 | 59 | 60 | def dict_to_attributes(d): 61 | # Implement the standard way of representing attributes 62 | # in TXT records, see RFC 1464 63 | # Essentially turns {a: b, c: d} into ["a=b","c=d"] 64 | output = [] 65 | for (key, value) in d.iteritems(): 66 | output.append(str(key) + "=" + str(value)) 67 | output.append("$count=" + str(len(d))) 68 | return output 69 | 70 | # These two functions are a somewhat clunky way of getting DNS 71 | # information into dnslib by turning it into text in the Zone File 72 | # format, which can then be easily parsed by dnslib. 73 | # TODO (low-pri): this should be eliminated and we should switch 74 | # to directly getting DNS information into dnslib using the built-in functions. 75 | def generate_TXT_zone_line(host, text): 76 | assert(host.endswith(".burrow.tech.")) 77 | # Split the text into 250-char substrings if necessary 78 | split_text = [text[i:i+250] for i in range(0, len(text), 250)] 79 | prepared_text = '"' + '" "'.join(split_text) + '"\n' 80 | zone = host + " 60 IN TXT " + prepared_text 81 | return zone 82 | def generate_TXT_zone(host, text_list): 83 | output = "" 84 | for t in text_list: 85 | output += generate_TXT_zone_line(host, t) 86 | return output 87 | 88 | def is_domain_safe(s): 89 | domain_safe_matcher = re.compile(r'[A-Za-z0-9-+/]').search 90 | return bool(domain_safe_matcher(s)) 91 | 92 | class Transmission: 93 | """ 94 | Represents an incoming transmission from a client. 95 | """ 96 | def __init__(self, id): 97 | self.id = id 98 | self.data = {} # {index: data} 99 | self.final_contents = "" # this is filled in by the end() method 100 | 101 | def add_data(self, data, index): 102 | # Indices can arrive out of order 103 | # Ignore data from indices we've already seen 104 | if (index not in self.data): 105 | self.data[index] = data 106 | 107 | def end(self, length): 108 | if all (k in self.data for k in range(length)): 109 | for i in range(length): 110 | self.final_contents += self.data[i] 111 | return True 112 | else: 113 | return False 114 | 115 | def __repr__(self): 116 | return "" 117 | 118 | class BurrowResolver(BaseResolver): 119 | """ 120 | Respond with fixed response to some requests, and treat others as Transmission layer traffic. 121 | """ 122 | def __init__(self): 123 | fixed_zone = open("fixed_zone/primary.txt").read() + open("fixed_zone/tests.txt").read() 124 | self.fixedrrs = RR.fromZone(fixed_zone) 125 | self.active_transmissions = {} # Dictionary of Transmission objects. 126 | # Their ID's are the keys, for easy/quick lookup. 127 | self.cache = ExpiringDict(max_len=100000, max_age_seconds=70) 128 | self.transmission_handler_lock = multiprocessing.Lock() 129 | 130 | def resolve(self,request,handler): 131 | reply = request.reply() # the object that this function will return in the end, with modifications 132 | qname = request.q.qname # the domain that was looked up 133 | 134 | burrow_log("Request for " + str(DNSLabel(qname.label[-5:])), 0) 135 | 136 | # First, we make sure the domain ends in burrow.tech. 137 | # If not, we return an NXDOMAIN result. 138 | if not qname.matchSuffix("burrow.tech"): 139 | reply.header.rcode = RCODE.NXDOMAIN 140 | return reply 141 | 142 | # Next, we try to look up the domain in our list of fixed test records. 143 | found_fixed_rr = False 144 | for rr in self.fixedrrs: 145 | a = copy.copy(rr) 146 | if (a.rname == qname): 147 | found_fixed_rr = True 148 | LOG("Found a fixed record for " + str(a.rname)) 149 | reply.add_answer(a) 150 | if found_fixed_rr: 151 | return reply 152 | 153 | # Alright, if we've gotten here it must be a Transmission API message! 154 | assert(not found_fixed_rr) 155 | response_dict = self.handle_transmission_api_message(qname) 156 | zone = generate_TXT_zone(str(qname), dict_to_attributes(response_dict)) 157 | rrs = RR.fromZone(zone) 158 | for rr in rrs: 159 | reply.add_answer(rr) 160 | return reply 161 | 162 | def handle_transmission_api_message(self, qname): 163 | self.transmission_handler_lock.acquire() 164 | # If we recently responded to this lookup, we don't want to re-handle it. 165 | if qname in self.cache: 166 | LOG("Cache hit!") 167 | response_dict = self.cache[qname] 168 | self.transmission_handler_lock.release() 169 | return response_dict 170 | 171 | # Otherwise, handle as a new lookup. 172 | parsed = parse_url(qname) 173 | 174 | if isinstance(parsed, Failure): 175 | response_dict = {'success': False, 'error': "You used the API incorrectly."} 176 | 177 | elif isinstance(parsed, Other): 178 | response_dict = {'success': False, 'error': "This is not an API endpoint"} 179 | 180 | elif isinstance(parsed, Begin): 181 | transmission_id = uuid.uuid4().hex[-8:] 182 | self.active_transmissions[transmission_id] = Transmission(transmission_id) 183 | LOG("Began transmission with id: " + str(transmission_id)) 184 | response_dict = {'success': True, 'transmission_id': transmission_id} 185 | 186 | elif isinstance(parsed, Continue): 187 | try: 188 | self.active_transmissions[parsed.id].add_data(parsed.data, parsed.index) 189 | LOG("Continuing transmission " + str(parsed.id)) 190 | response_dict = {'success': True} 191 | except KeyError: 192 | LOG("Error: tried to continue a transmission that doesn't exist: " + str(parsed.id)) 193 | response_dict = {'success': False, 'error': "Tried to continue a transmission that doesn't exist."} 194 | 195 | elif isinstance(parsed, End): 196 | transmission = self.active_transmissions.get(parsed.id) 197 | response_dict = None 198 | if transmission is not None: 199 | del self.active_transmissions[parsed.id] 200 | if transmission.end(parsed.length): 201 | final_contents = transmission.final_contents 202 | LOG("Ending transmission " + str(parsed.id) + 203 | ". Final contents: " + ((final_contents[:15] + '...') if len(final_contents) > 15 else final_contents)) 204 | response = session.handle_message(final_contents) 205 | assert(is_domain_safe(response)) 206 | response_dict = {'success': True, 'contents': response} 207 | else: 208 | response_dict = {'success': False, 'error': ".end called with length that didn't match number of .continue's received."} 209 | else: 210 | LOG("Error: tried to end a transmission that doesn't exist: " + str(parsed.id)) 211 | response_dict = {'success': False, 'error': "Tried to end a transmission that doesn't exist."} 212 | 213 | # Cache the response in case of duplicate lookups 214 | self.cache[qname] = response_dict 215 | self.transmission_handler_lock.release() 216 | return response_dict 217 | 218 | 219 | 220 | if __name__ == '__main__': 221 | 222 | import argparse,sys,time 223 | 224 | p = argparse.ArgumentParser(description="Burrow DNS Resolver") 225 | p.add_argument("--port","-p",type=int,default=53, 226 | metavar="", 227 | help="Server port (default:53)") 228 | p.add_argument("--address","-a",default="", 229 | metavar="
", 230 | help="Listen address (default:all)") 231 | p.add_argument("--udplen","-u",type=int,default=0, 232 | metavar="", 233 | help="Max UDP packet length (default:0)") 234 | p.add_argument("--notcp",action='store_true',default=False, 235 | help="UDP server only (default: UDP and TCP)") 236 | p.add_argument("--log",default="truncated,error", 237 | help="Log hooks to enable (default: -request,-reply,+truncated,+error,-recv,-send,-data)") 238 | p.add_argument("--log-prefix",action='store_true',default=False, 239 | help="Log prefix (timestamp/handler/resolver) (default: False)") 240 | args = p.parse_args() 241 | 242 | resolver = BurrowResolver() 243 | logger = DNSLogger(args.log,args.log_prefix) 244 | 245 | burrow_log("", 0) 246 | burrow_log("Starting Burrow Resolver (%s:%d) [%s]" % ( 247 | args.address or "*", 248 | args.port, 249 | "UDP" if args.notcp else "UDP/TCP"), 0) 250 | 251 | if args.udplen: 252 | DNSHandler.udplen = args.udplen 253 | 254 | udp_server = DNSServer(resolver, 255 | port=args.port, 256 | address=args.address, 257 | logger=logger) 258 | udp_server.start_thread() 259 | 260 | if not args.notcp: 261 | tcp_server = DNSServer(resolver, 262 | port=args.port, 263 | address=args.address, 264 | tcp=True, 265 | logger=logger) 266 | tcp_server.start_thread() 267 | 268 | while udp_server.isAlive(): 269 | time.sleep(1) 270 | 271 | --------------------------------------------------------------------------------