| OLD | NEW |
| (Empty) |
| 1 #!/usr/bin/env python | |
| 2 # Copyright 2010 Google Inc. All Rights Reserved. | |
| 3 # | |
| 4 # Licensed under the Apache License, Version 2.0 (the "License"); | |
| 5 # you may not use this file except in compliance with the License. | |
| 6 # You may obtain a copy of the License at | |
| 7 # | |
| 8 # http://www.apache.org/licenses/LICENSE-2.0 | |
| 9 # | |
| 10 # Unless required by applicable law or agreed to in writing, software | |
| 11 # distributed under the License is distributed on an "AS IS" BASIS, | |
| 12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 13 # See the License for the specific language governing permissions and | |
| 14 # limitations under the License. | |
| 15 | |
| 16 import daemonserver | |
| 17 import errno | |
| 18 import logging | |
| 19 import socket | |
| 20 import SocketServer | |
| 21 import threading | |
| 22 import time | |
| 23 | |
| 24 from third_party.dns import flags | |
| 25 from third_party.dns import message | |
| 26 from third_party.dns import rcode | |
| 27 from third_party.dns import resolver | |
| 28 from third_party.dns import rdatatype | |
| 29 from third_party import ipaddr | |
| 30 | |
| 31 | |
| 32 | |
| 33 class DnsProxyException(Exception): | |
| 34 pass | |
| 35 | |
| 36 | |
| 37 class RealDnsLookup(object): | |
| 38 def __init__(self, name_servers): | |
| 39 if '127.0.0.1' in name_servers: | |
| 40 raise DnsProxyException( | |
| 41 'Invalid nameserver: 127.0.0.1 (causes an infinte loop)') | |
| 42 self.resolver = resolver.get_default_resolver() | |
| 43 self.resolver.nameservers = name_servers | |
| 44 self.dns_cache_lock = threading.Lock() | |
| 45 self.dns_cache = {} | |
| 46 | |
| 47 @staticmethod | |
| 48 def _IsIPAddress(hostname): | |
| 49 try: | |
| 50 socket.inet_aton(hostname) | |
| 51 return True | |
| 52 except socket.error: | |
| 53 return False | |
| 54 | |
| 55 def __call__(self, hostname, rdtype=rdatatype.A): | |
| 56 """Return real IP for a host. | |
| 57 | |
| 58 Args: | |
| 59 host: a hostname ending with a period (e.g. "www.google.com.") | |
| 60 rdtype: the query type (1 for 'A', 28 for 'AAAA') | |
| 61 Returns: | |
| 62 the IP address as a string (e.g. "192.168.25.2") | |
| 63 """ | |
| 64 if self._IsIPAddress(hostname): | |
| 65 return hostname | |
| 66 self.dns_cache_lock.acquire() | |
| 67 ip = self.dns_cache.get(hostname) | |
| 68 self.dns_cache_lock.release() | |
| 69 if ip: | |
| 70 return ip | |
| 71 try: | |
| 72 answers = self.resolver.query(hostname, rdtype) | |
| 73 except resolver.NXDOMAIN: | |
| 74 return None | |
| 75 except resolver.NoNameservers: | |
| 76 logging.debug('_real_dns_lookup(%s) -> No nameserver.', | |
| 77 hostname) | |
| 78 return None | |
| 79 except (resolver.NoAnswer, resolver.Timeout) as ex: | |
| 80 logging.debug('_real_dns_lookup(%s) -> None (%s)', | |
| 81 hostname, ex.__class__.__name__) | |
| 82 return None | |
| 83 if answers: | |
| 84 ip = str(answers[0]) | |
| 85 self.dns_cache_lock.acquire() | |
| 86 self.dns_cache[hostname] = ip | |
| 87 self.dns_cache_lock.release() | |
| 88 return ip | |
| 89 | |
| 90 def ClearCache(self): | |
| 91 """Clear the dns cache.""" | |
| 92 self.dns_cache_lock.acquire() | |
| 93 self.dns_cache.clear() | |
| 94 self.dns_cache_lock.release() | |
| 95 | |
| 96 | |
| 97 class ReplayDnsLookup(object): | |
| 98 """Resolve DNS requests to replay host.""" | |
| 99 def __init__(self, replay_ip, filters=None): | |
| 100 self.replay_ip = replay_ip | |
| 101 self.filters = filters or [] | |
| 102 | |
| 103 def __call__(self, hostname): | |
| 104 ip = self.replay_ip | |
| 105 for f in self.filters: | |
| 106 ip = f(hostname, default_ip=ip) | |
| 107 return ip | |
| 108 | |
| 109 | |
| 110 class PrivateIpFilter(object): | |
| 111 """Resolve private hosts to their real IPs and others to the Web proxy IP. | |
| 112 | |
| 113 Hosts in the given http_archive will resolve to the Web proxy IP without | |
| 114 checking the real IP. | |
| 115 | |
| 116 This only supports IPv4 lookups. | |
| 117 """ | |
| 118 def __init__(self, real_dns_lookup, http_archive): | |
| 119 """Initialize PrivateIpDnsLookup. | |
| 120 | |
| 121 Args: | |
| 122 real_dns_lookup: a function that resolves a host to an IP. | |
| 123 http_archive: an instance of a HttpArchive | |
| 124 Hosts is in the archive will always resolve to the web_proxy_ip | |
| 125 """ | |
| 126 self.real_dns_lookup = real_dns_lookup | |
| 127 self.http_archive = http_archive | |
| 128 self.InitializeArchiveHosts() | |
| 129 | |
| 130 def __call__(self, host, default_ip): | |
| 131 """Return real IPv4 for private hosts and Web proxy IP otherwise. | |
| 132 | |
| 133 Args: | |
| 134 host: a hostname ending with a period (e.g. "www.google.com.") | |
| 135 Returns: | |
| 136 IP address as a string or None (if lookup fails) | |
| 137 """ | |
| 138 ip = default_ip | |
| 139 if host not in self.archive_hosts: | |
| 140 real_ip = self.real_dns_lookup(host) | |
| 141 if real_ip: | |
| 142 if ipaddr.IPAddress(real_ip).is_private: | |
| 143 ip = real_ip | |
| 144 else: | |
| 145 ip = None | |
| 146 return ip | |
| 147 | |
| 148 def InitializeArchiveHosts(self): | |
| 149 """Recompute the archive_hosts from the http_archive.""" | |
| 150 self.archive_hosts = set('%s.' % req.host.split(':')[0] | |
| 151 for req in self.http_archive) | |
| 152 | |
| 153 | |
| 154 class DelayFilter(object): | |
| 155 """Add a delay to replayed lookups.""" | |
| 156 | |
| 157 def __init__(self, is_record_mode, delay_ms): | |
| 158 self.is_record_mode = is_record_mode | |
| 159 self.delay_ms = int(delay_ms) | |
| 160 | |
| 161 def __call__(self, host, default_ip): | |
| 162 if not self.is_record_mode: | |
| 163 time.sleep(self.delay_ms * 1000.0) | |
| 164 return default_ip | |
| 165 | |
| 166 def SetRecordMode(self): | |
| 167 self.is_record_mode = True | |
| 168 | |
| 169 def SetReplayMode(self): | |
| 170 self.is_record_mode = False | |
| 171 | |
| 172 | |
| 173 class UdpDnsHandler(SocketServer.DatagramRequestHandler): | |
| 174 """Resolve DNS queries to localhost. | |
| 175 | |
| 176 Possible alternative implementation: | |
| 177 http://howl.play-bow.org/pipermail/dnspython-users/2010-February/000119.html | |
| 178 """ | |
| 179 | |
| 180 STANDARD_QUERY_OPERATION_CODE = 0 | |
| 181 | |
| 182 def handle(self): | |
| 183 """Handle a DNS query. | |
| 184 | |
| 185 IPv6 requests (with rdtype AAAA) receive mismatched IPv4 responses | |
| 186 (with rdtype A). To properly support IPv6, the http proxy would | |
| 187 need both types of addresses. By default, Windows XP does not | |
| 188 support IPv6. | |
| 189 """ | |
| 190 self.data = self.rfile.read() | |
| 191 self.transaction_id = self.data[0] | |
| 192 self.flags = self.data[1] | |
| 193 self.qa_counts = self.data[4:6] | |
| 194 self.domain = '' | |
| 195 operation_code = (ord(self.data[2]) >> 3) & 15 | |
| 196 if operation_code == self.STANDARD_QUERY_OPERATION_CODE: | |
| 197 self.wire_domain = self.data[12:] | |
| 198 self.domain = self._domain(self.wire_domain) | |
| 199 else: | |
| 200 logging.debug("DNS request with non-zero operation code: %s", | |
| 201 operation_code) | |
| 202 ip = self.server.dns_lookup(self.domain) | |
| 203 if ip is None: | |
| 204 logging.debug('dnsproxy: %s -> NXDOMAIN', self.domain) | |
| 205 response = self.get_dns_no_such_name_response() | |
| 206 else: | |
| 207 if ip == self.server.server_address[0]: | |
| 208 logging.debug('dnsproxy: %s -> %s (replay web proxy)', self.domain, ip) | |
| 209 else: | |
| 210 logging.debug('dnsproxy: %s -> %s', self.domain, ip) | |
| 211 response = self.get_dns_response(ip) | |
| 212 self.wfile.write(response) | |
| 213 | |
| 214 @classmethod | |
| 215 def _domain(cls, wire_domain): | |
| 216 domain = '' | |
| 217 index = 0 | |
| 218 length = ord(wire_domain[index]) | |
| 219 while length: | |
| 220 domain += wire_domain[index + 1:index + length + 1] + '.' | |
| 221 index += length + 1 | |
| 222 length = ord(wire_domain[index]) | |
| 223 return domain | |
| 224 | |
| 225 def get_dns_response(self, ip): | |
| 226 packet = '' | |
| 227 if self.domain: | |
| 228 packet = ( | |
| 229 self.transaction_id + | |
| 230 self.flags + | |
| 231 '\x81\x80' + # standard query response, no error | |
| 232 self.qa_counts * 2 + '\x00\x00\x00\x00' + # Q&A counts | |
| 233 self.wire_domain + | |
| 234 '\xc0\x0c' # pointer to domain name | |
| 235 '\x00\x01' # resource record type ("A" host address) | |
| 236 '\x00\x01' # class of the data | |
| 237 '\x00\x00\x00\x3c' # ttl (seconds) | |
| 238 '\x00\x04' + # resource data length (4 bytes for ip) | |
| 239 socket.inet_aton(ip) | |
| 240 ) | |
| 241 return packet | |
| 242 | |
| 243 def get_dns_no_such_name_response(self): | |
| 244 query_message = message.from_wire(self.data) | |
| 245 response_message = message.make_response(query_message) | |
| 246 response_message.flags |= flags.AA | flags.RA | |
| 247 response_message.set_rcode(rcode.NXDOMAIN) | |
| 248 return response_message.to_wire() | |
| 249 | |
| 250 | |
| 251 class DnsProxyServer(SocketServer.ThreadingUDPServer, | |
| 252 daemonserver.DaemonServer): | |
| 253 # Increase the request queue size. The default value, 5, is set in | |
| 254 # SocketServer.TCPServer (the parent of BaseHTTPServer.HTTPServer). | |
| 255 # Since we're intercepting many domains through this single server, | |
| 256 # it is quite possible to get more than 5 concurrent requests. | |
| 257 request_queue_size = 256 | |
| 258 | |
| 259 # Allow sockets to be reused. See | |
| 260 # http://svn.python.org/projects/python/trunk/Lib/SocketServer.py for more | |
| 261 # details. | |
| 262 allow_reuse_address = True | |
| 263 | |
| 264 # Don't prevent python from exiting when there is thread activity. | |
| 265 daemon_threads = True | |
| 266 | |
| 267 def __init__(self, host='', port=53, dns_lookup=None): | |
| 268 """Initialize DnsProxyServer. | |
| 269 | |
| 270 Args: | |
| 271 host: a host string (name or IP) to bind the dns proxy and to which | |
| 272 DNS requests will be resolved. | |
| 273 port: an integer port on which to bind the proxy. | |
| 274 dns_lookup: a list of filters to apply to lookup. | |
| 275 """ | |
| 276 try: | |
| 277 SocketServer.ThreadingUDPServer.__init__( | |
| 278 self, (host, port), UdpDnsHandler) | |
| 279 except socket.error, (error_number, msg): | |
| 280 if error_number == errno.EACCES: | |
| 281 raise DnsProxyException( | |
| 282 'Unable to bind DNS server on (%s:%s)' % (host, port)) | |
| 283 raise | |
| 284 self.dns_lookup = dns_lookup or (lambda host: self.server_address[0]) | |
| 285 self.server_port = self.server_address[1] | |
| 286 logging.warning('DNS server started on %s:%d', self.server_address[0], | |
| 287 self.server_address[1]) | |
| 288 | |
| 289 def cleanup(self): | |
| 290 try: | |
| 291 self.shutdown() | |
| 292 self.server_close() | |
| 293 except KeyboardInterrupt, e: | |
| 294 pass | |
| 295 logging.info('Stopped DNS server') | |
| OLD | NEW |