Intro
In the new era of privacy, everyone becomes more cognizant of all personal information traces they leave behind while browsing the internet. While we made a huge leap forward by rapidly adopting HTTPS encryption worldwide, we still have some gaps that have a potential of leaking personal data to bad actors. One such gap is the DNS (Domain Name System) resolution process that is responsible for translating domain names into respective IP addresses. The main issue is that DNS resolution still largely remains unencrypted which leaves room for MitM attacks and DNS eavesdropping.
There are two main technologies which were introduced in the past couple years that try to combat the problem. They are DNSOverHTTPS and DNSOverTLS that offer DNS resolution through HTTPS and TLS protocols respectively. One of the largest proponents of which is CloudFlare that runs a public, encrypted DNS resolver.
Today, I would like to show how you could run your own DNSOverHTTPS upstream resolver on python using AWS Lambda (+ bonus at the end). Usually such resolvers are deployed using common DNS server software such as BIND, PowerDNS and Unbound which require complex configurations, certificate management, and a VPS or bare-metal machine.
Running DNSOverHTTPS on AWS Lambda in conjunction with API Gateway provides a quick and easy way of having your own encrypted DNS resolver that you have a full control over. It can have potential use-cases like:
- Observing DNS traffic from your devices, and identifying unrecognized and potentially malicious domains that your devices are trying to resolve
- Blocking unwanted domains from resolving on your devices such as malware, gambling, and fakenews websites
Requirements
- Personal AWS Account
- AWS CLI tool
- Node.JS >10.13.0
Our setup will consist of two AWS components which are API Gateway endpoint and AWS Lambda function. Both should stay within a free tier zone with 1,000,000 monthly, free API calls on API Gateway side and 1,000,000 monthly, free lambda invocations with 400,000 GB-seconds of compute time. Be aware, 1 device should roughly send 5000 DNS requests per day.
Setup
DNSOverHTTPS setup is deployed using a CDK (Cloud Development Kit). Thus, you would need to install CDK CLI tool using npm install -g aws-cdk system-wide. Additionally, you need to have your AWS account’s credentials stored under ~/.aws/credentials.
Initializing your CDK application
Create an empty directory where your CDK app will be placed at: mkdir doh-lambda && cd doh-lambda. Then, initialize your app using cdk init app --language=typescript command.
It should have a default structure like this:
.
├── README.md
├── bin
├── node_modules
├── lib
│ └── doh-lambda-stack.ts
├── test
├── cdk.json
├── package-lock.json
├── package.json
├── tsconfig.json
└── jest.config.jsWe are going to define all our AWS components under doh-lambda-stack.ts file. But for now, let’s first focus on our DNSOverHTTPS lambda code.
Preparing DNSOverHTTPS python environment
I decided to use python as it can be easily understood and tweaked by a developer of any level. We would need to create our python project under the CDK application, so that it could locate and package our code for deployment. Create an empty directory where we are going to put our DNSOverHTTPS lambda code: mkdir -p assets/doh.
We will need a few python dependencies defined under requirements.txt file:
pydantic==1.8.2
doh-proxy==0.0.9We will use pydantic for API Gateway input/output (de)serialization. We will also install doh-proxy which is a standalone DNSOverHTTPS server, but we will only use DNSClient class and some utility functions for upstream DNS query resolutions.
For development purposes, you can create a virtual environment and install these dependencies using pip install -r requirements.txt . However, during the deployment CDK application will locally download and package these dependencies under assets/doh directory.
Input/Output (de)serialization
API Gateway propagates a HTTP request in a form of a JSON proxy event (ex. event.json). Instead of working with a raw dict object in python, I decided to use pydantic to deserialize the event in a set of models for ease of use. Unfortunately, I could not find ready-to-use models for Python, so I implemented them in pydantic according to APIGatewayV2HTTPRequest.
Let’s create models.py and paste the following:
You might be wondering about this part:
Since, the event is coming in camel case, and I wanted to adhere to pythonic field-naming conventions, we need to convert field names from snake_case from/to camelCase during (de)serialization process using pydantic’s Alias Generator feature. We will define utils.to_camel function later.
Implementing DNSOverHTTPS lambda
Let’s define our entry point for the lambda under doh.py file using
As I mentioned previously, we receive API Gateway event in a raw, python dictionary form which we can deserialize using our APIGatewayV2HTTPEvent model.
Then, we need to extract DNS payload from the request based on the HTTP method type. DNSOverHTTPS rfc8484 spec allows both GET and POST methods to be used for DOH resolution:
DNS requests are encoded using Base64, so we need to decode the extracted body, then deserialize DNS request:
Once we have a DNS request object ready, we use above mentioned DNSClient to resolve the request:
Notice, you can provide your own upstream DNS resolver using UPSTREAM_RESOLVER environment variable while by default we will be using CloudFlare’s 1.1.1.1 DNS resolver.
Finally, when we received the DNS response, we need to encode it back into Base64 format, and put inside an API Gateway compatible response:
Once, we have all pieces in place, your doh.py file should look like this:
I also extracted encode and decode methods into a separate utils.py file:
Finally, I added a small exceptions.py file with one custom exception:
Defining DNSOverHTTPS CDK deployment stack
We left it off on having doh-lambda-stack.ts file where we are going to define our stack with two components: API Gateway and Lambda function. Before we can define these components, we need to install corresponding CDK packages.
While in doh-lambda directory, run the following command:npm install --save @aws-cdk/aws-lambda @aws-cdk/aws-apigatewayv2 @aws-cdk/aws-apigatewayv2-integrations
It will install these packages in a local node_modules directory and save the dependencies under package.json.
Now, we are ready to write our stack. First, let’s define DNSOverHTTPS lambda function indoh-lambda-stack.ts:
It is pretty straightforward, we specify the runtime for our lambda which will be python 3.8. We point to our code location (since doh-lambda-stack.ts lives under ./lib directory, we have to go one level up to locate our assets dir. Then, we specify the entry method inside our doh.py. Finally, we can pass environment variables to choose custom upstream resolver or change logging level. You could also add additional properties like lambda’s memorySize (128Mb default) or timeout (3 sec default). The list of all available properties can be found here.
Then, let’s define API Gateway component. Again, according to rfc8484 spec all requests should be directed to /dns-query endpoint where we allow only GET and POST requests.
For convenience, let’s also add API Gateway URL to the stack’s outputs:
The beauty of API Gateway is that it will provide us an HTTPS url with all certificates in place out of the box. So we do not have to worry about SSL certificate generation and signing.
Now, our doh-lambda-stack.ts should look like this:
With our final directory structure:
.
├── README.md
├── bin
├── node_modules
├── assets/
├── doh-lambda
│ ├── doh.py
│ ├── models.py
│ ├── utils.py
│ ├── exception.py
│ └── requirements.txt
├── lib
│ └── doh-lambda-stack.ts
├── tests
├── cdk.json
├── package-lock.json
├── package.json
├── tsconfig.json
└── jest.config.jsDeploying DNSOverHTTPS stack
Before we deploy our stack, let’s add a command to our package.json’s scripts section:
"scripts": {
...,
"prepare": "pip install -r assets/doh-lambda/requirements.txt --target assets/doh-lambda"
}After we run npm run prepare, it will download and dump all python dependencies under our assets directory.
Then, we need to bootstrap our AWS account which will provision resources used for stacks deployment. To do that, simply run cdk boostrap command.
Finally, run cdk deploy command and wait for your stack to be deployed on your personal AWS account!
You should see your stack successfully deployed with your URL under “Outputs” section:
Press enter or click to view image in full size
Testing
I use kdig for testing DOH or DOT resolvers. In our case, we simply copy the URL without https:// schema part, then can run:
You can also observe these requests in your lambda logs:
Epilogue
Today, we implemented and deployed a reliable DNSOverHTTPS resolver using AWS Lambda and API Gateway. The benefit of lambda is that it provides “infinite” scaling out of the box which can handle workloads of a few requests per day to hundreds of thousands per second.
Feel free to play around with code, add your custom logic, and observe your DNS traffic.
Github: https://github.com/yeralin/DNSOverHTTPSLambda
Bonus
For iOS devices, you can install DNSecure app where you can add your DOH server while also specifying rules like:
- Only use this DNS server while on a given Wi-Fi network
- Only use this DNS server while on Wi-Fi/Cellular
- Only use this DNS server for a list of given domains
- etc.
Then you can enable the DNS configuration in your Settings section. Cudos to Apple for implementing such a cool feature!
Thank you for reading this article!