Package: dnsrewriteproxy
dns-rewrite-proxyA DNS proxy server that conditionally rewrites and filters A record requests. Written in Python, all code is in a single module, and there is a single dependency,aiodnsresolver.CNAMEs are followed and resolved by the proxy to IP addresses, and never returned to the client.InstallationpipinstalldnsrewriteproxyUsageBy default the proxy will listen on port 53, and proxy requests to the servers in/etc/resolv.conf. However, by default all requests are blocked without explicit rules, so to proxy requests you must configure at least one rewrite rule.fromdnsrewriteproxyimportDnsProxy# Proxy all incoming A record requests without any rewritingstart=DnsProxy(rules=((r'(^.*$)',r'\1'),))# Run proxy, accepting UDP requests on port 53awaitstart()Therulesparameter must be an iterable [e.g. a list or a tuple] of tuples, where each tuple is regex pattern/replacement pair, passed tore.subnunder the hood. On each incoming DNS request from downstream for a domainthis list is iterated over;the first rule that matches the incoming domain name is used to rewrite the domain, the upstream DNS server is queried for A records, and these records, or error code, is returned downstream;and if no rule matches a REFUSED response is returned downstream.The response of REFUSED is deliberate for clients to be able to help differentiate between a configuration issue on the proxy, the proxy not working or not being contactable, and a domain actually not existing.So to rewrite all queries forwww.source.comtowww.target.com, and torefuseto proxy any others, you can use the following configuration.start=DnsProxy(rules=((r'^www\.source\.com$',r'www.target.com'),))Alternatively, do the same rewriting, but toallowall other requests, you can use the following.start=DnsProxy(rules=((r'^www\.source\.com$',r'www.target.com'),(r'(^.*$)',r'\1'),))To proxy to a server other than that specified in/etc/resolv.conf, you can pass a customisedResolverviaget_resolver.fromaiodnsresolverimportResolverfromdnsrewriteproxyimportDnsProxydefget_resolver():asyncdefget_nameservers(_,__):for_inrange(0,5):yield(0.5,('8.8.8.8',53))returnResolver(get_nameserver=get_nameservers)start=DnsProxy(rules=((r'(^.*$)',r'\1'),),get_resolver=get_resolver,)Server lifecycleIn the above exampleawait start()completes just after the server has started listening. The coroutinestartreturns the underlyingtaskto give control over the server lifecycle. A task can be seen as an "asyncio thread"; this is exposed to allow the server to sit in a larger asyncio Python program that may have a specific startup/shutdown procedure.Run foreverYou can run the server forever [or until it hits some non-recoverable error] by awaiting this task.fromdnsrewriteproxyimportDnsProxystart=DnsProxy(rules=((r'(^.*$)',r'\1'),))server_task=awaitstart()# Waiting here until the server is stoppedawaitserver_taskStopping the serverTo stop the server, you cancancelthe returned task.fromdnsrewriteproxyimportDnsProxystart=DnsProxy(rules=((r'(^.*$)',r'\1'),))proxy_task=awaitstart()# ... Receive requests# Initiate stopping: new requests will not be processed...proxy_task.cancel()try:# ... and we wait until previously received requests have been processedawaitproxy_taskexceptasyncio.CancelledError:passGraceful shutdown exampleA full example of a server that would do a graceful shutdown on SIGINT or SIGTERM is below.importasyncioimportsignalfromdnsrewriteproxyimport(DnsProxy,)asyncdefasync_main():start=DnsProxy(rules=((r'(^.*$)',r'\1'),))proxy_task=awaitstart()loop=asyncio.get_running_loop()loop.add_signal_handler(signal.SIGINT,proxy_task.cancel)loop.add_signal_handler(signal.SIGTERM,proxy_task.cancel)try:awaitproxy_taskexceptasyncio.CancelledError:passasyncio.run(async_main())print('End of program')