2 # Copyright 2010 Google Inc. All Rights Reserved.
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
8 # http://www.apache.org/licenses/LICENSE-2.0
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.
33 class DnsProxyException(Exception):
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 = dns.resolver.get_default_resolver()
43 self.resolver.nameservers = name_servers
44 self.dns_cache_lock = threading.Lock()
47 def _IsIPAddress(self, hostname):
49 socket.inet_aton(hostname)
54 def __call__(self, hostname, rdtype=dns.rdatatype.A):
55 """Return real IP for a host.
58 host: a hostname ending with a period (e.g. "www.google.com.")
59 rdtype: the query type (1 for 'A', 28 for 'AAAA')
61 the IP address as a string (e.g. "192.168.25.2")
63 if self._IsIPAddress(hostname):
65 self.dns_cache_lock.acquire()
66 ip = self.dns_cache.get(hostname)
67 self.dns_cache_lock.release()
71 answers = self.resolver.query(hostname, rdtype)
72 except dns.resolver.NXDOMAIN:
74 except dns.resolver.NoNameservers:
75 logging.debug('_real_dns_lookup(%s) -> No nameserver.',
78 except (dns.resolver.NoAnswer, dns.resolver.Timeout) as ex:
79 logging.debug('_real_dns_lookup(%s) -> None (%s)',
80 hostname, ex.__class__.__name__)
84 self.dns_cache_lock.acquire()
85 self.dns_cache[hostname] = ip
86 self.dns_cache_lock.release()
90 """Clear the dns cache."""
91 self.dns_cache_lock.acquire()
92 self.dns_cache.clear()
93 self.dns_cache_lock.release()
96 class ReplayDnsLookup(object):
97 """Resolve DNS requests to replay host."""
98 def __init__(self, replay_ip, filters=None):
99 self.replay_ip = replay_ip
100 self.filters = filters or []
102 def __call__(self, hostname):
104 for f in self.filters:
105 ip = f(hostname, default_ip=ip)
109 class PrivateIpFilter(object):
110 """Resolve private hosts to their real IPs and others to the Web proxy IP.
112 Hosts in the given http_archive will resolve to the Web proxy IP without
113 checking the real IP.
115 This only supports IPv4 lookups.
117 def __init__(self, real_dns_lookup, http_archive):
118 """Initialize PrivateIpDnsLookup.
121 real_dns_lookup: a function that resolves a host to an IP.
122 http_archive: an instance of a HttpArchive
123 Hosts is in the archive will always resolve to the web_proxy_ip
125 self.real_dns_lookup = real_dns_lookup
126 self.http_archive = http_archive
127 self.InitializeArchiveHosts()
129 def __call__(self, host, default_ip):
130 """Return real IPv4 for private hosts and Web proxy IP otherwise.
133 host: a hostname ending with a period (e.g. "www.google.com.")
135 IP address as a string or None (if lookup fails)
138 if host not in self.archive_hosts:
139 real_ip = self.real_dns_lookup(host)
141 if ipaddr.IPAddress(real_ip).is_private:
147 def InitializeArchiveHosts(self):
148 """Recompute the archive_hosts from the http_archive."""
149 self.archive_hosts = set('%s.' % req.host for req in self.http_archive)
152 class DelayFilter(object):
153 """Add a delay to replayed lookups."""
155 def __init__(self, is_record_mode, delay_ms):
156 self.is_record_mode = is_record_mode
157 self.delay_ms = int(delay_ms)
159 def __call__(self, host, default_ip):
160 if not self.is_record_mode:
161 time.sleep(self.delay_ms * 1000.0)
164 def SetRecordMode(self):
165 self.is_record_mode = True
167 def SetReplayMode(self):
168 self.is_record_mode = False
171 class UdpDnsHandler(SocketServer.DatagramRequestHandler):
172 """Resolve DNS queries to localhost.
174 Possible alternative implementation:
175 http://howl.play-bow.org/pipermail/dnspython-users/2010-February/000119.html
178 STANDARD_QUERY_OPERATION_CODE = 0
181 """Handle a DNS query.
183 IPv6 requests (with rdtype AAAA) receive mismatched IPv4 responses
184 (with rdtype A). To properly support IPv6, the http proxy would
185 need both types of addresses. By default, Windows XP does not
188 self.data = self.rfile.read()
189 self.transaction_id = self.data[0]
190 self.flags = self.data[1]
191 self.qa_counts = self.data[4:6]
193 operation_code = (ord(self.data[2]) >> 3) & 15
194 if operation_code == self.STANDARD_QUERY_OPERATION_CODE:
195 self.wire_domain = self.data[12:]
196 self.domain = self._domain(self.wire_domain)
198 logging.debug("DNS request with non-zero operation code: %s",
200 ip = self.server.dns_lookup(self.domain)
202 logging.debug('dnsproxy: %s -> NXDOMAIN', self.domain)
203 response = self.get_dns_no_such_name_response()
205 if ip == self.server.server_address[0]:
206 logging.debug('dnsproxy: %s -> %s (replay web proxy)', self.domain, ip)
208 logging.debug('dnsproxy: %s -> %s', self.domain, ip)
209 response = self.get_dns_response(ip)
210 self.wfile.write(response)
213 def _domain(cls, wire_domain):
216 length = ord(wire_domain[index])
218 domain += wire_domain[index + 1:index + length + 1] + '.'
220 length = ord(wire_domain[index])
223 def get_dns_response(self, ip):
227 self.transaction_id +
229 '\x81\x80' + # standard query response, no error
230 self.qa_counts * 2 + '\x00\x00\x00\x00' + # Q&A counts
232 '\xc0\x0c' # pointer to domain name
233 '\x00\x01' # resource record type ("A" host address)
234 '\x00\x01' # class of the data
235 '\x00\x00\x00\x3c' # ttl (seconds)
236 '\x00\x04' + # resource data length (4 bytes for ip)
241 def get_dns_no_such_name_response(self):
242 query_message = dns.message.from_wire(self.data)
243 response_message = dns.message.make_response(query_message)
244 response_message.flags |= dns.flags.AA | dns.flags.RA
245 response_message.set_rcode(dns.rcode.NXDOMAIN)
246 return response_message.to_wire()
249 class DnsProxyServer(SocketServer.ThreadingUDPServer,
250 daemonserver.DaemonServer):
251 # Increase the request queue size. The default value, 5, is set in
252 # SocketServer.TCPServer (the parent of BaseHTTPServer.HTTPServer).
253 # Since we're intercepting many domains through this single server,
254 # it is quite possible to get more than 5 concurrent requests.
255 request_queue_size = 128
257 # Don't prevent python from exiting when there is thread activity.
258 daemon_threads = True
260 def __init__(self, host='', port=53, dns_lookup=None):
261 """Initialize DnsProxyServer.
264 host: a host string (name or IP) to bind the dns proxy and to which
265 DNS requests will be resolved.
266 port: an integer port on which to bind the proxy.
267 dns_lookup: a list of filters to apply to lookup.
270 SocketServer.ThreadingUDPServer.__init__(
271 self, (host, port), UdpDnsHandler)
272 except socket.error, (error_number, msg):
273 if error_number == errno.EACCES:
274 raise DnsProxyException(
275 'Unable to bind DNS server on (%s:%s)' % (host, port))
277 self.dns_lookup = dns_lookup or (lambda host: self.server_address[0])
278 self.server_port = self.server_address[1]
279 logging.warning('DNS server started on %s:%d', self.server_address[0],
280 self.server_address[1])
285 except KeyboardInterrupt, e:
287 logging.info('Stopped DNS server')