A cloud-hosted blackhole for ads with a smörgåsbord of secure interfaces.
(tldr at the end)
As a pet project in 2019, I built all of this incrementally and a lot less cleanly, but it worked like a charm for me and mine. In Summer 2022, the public cloud VM in which I had built all of this - with limited version control - imploded. Virtually all was lost and the DNS on my devices was relegated to service from the likes of Comcast and spectated by the Chinese Communist Party.
So, I decided to do it again but do it in a way that's entirely reproduceable so when the next VM implodes, it would be a matter of minutes until I had everything up and running again on a new host.
The goal of this is to provide a low-maintenance setup to provide secure ad-blocking DNS service with a spectrum of access methods for a handful of known users and their devices.
Downtime is acceptable and in order to keep the installation within the bounds of the Oracle Cloud free tier, there is limited fault tolerance and redundancy. Expect an installation to have issues semi-annually that can be resolved with minimal debugging or maybe just a reinstallation or an update. Not worried about random users accessing the non-VPN endpoints so long as it's limited and without apparent malice.
Ubuntu on an AMD processor is the platform of choice and where I'm certain this works. Ubuntu because it's universal; AMD because it's one of the two flavors of free-tier hardware in Oracle Cloud's free tier and ARM is a lot less reliable for Docker builds at this point in time.
Docker was a key ingredient to this to keep portability high and not recreate infrastructure that already existed, but also rely on configuration over code to allow relatively low-tech people to have a chance to get this up and running for themselves and perhaps become slightly-higher-tech through the process.
Pihole is at the center of this. The primary value comes from having Pihole provide ad blocking and other custom domain blocking.
Wireguard provides tunneling and VPN functionality to allow a client device anywhere in the world to have access to all of the resources and functionality in a-hole
.
DNSProxy provides DoT and DoH processor endpoints to handle SSL and then farm it off to the internal DNS filtering and processing stack. I wish that dnsproxy published it's own certified docker container, but beggars can't be choosers.
Unbound fits in as a local upstream DNS server which adds a bit of security and privacy before resolving DNS with the broader Internet. I wish that unbound published it's own certified docker container, but beggars can't be choosers.
Certbot provides lifecycle management for creating and maintaining security certificates to facilitate TLS and HTTPS, specifically in DNS-over-TLS.
DuckDNS is a very simple, straightforward, reliable, popular DDNS provider, so we'll use it to manage our DDNS domain names to access a-hole
.
ddclient provides updates to many DDNS services to keep the host machine IP and the DDNS domain names in sync.
There are also a few scripts which make everything a snap:
remote-init.sh
streamlines setup
dns-test.sh
aids in debugging various DNS functions
control.py
simplifies control of an active installation
Ad-blocking DNS-over-TLS, DNS-over-HTTPS, and DNS-via-VPN. Instructions to get it running are below.
Prep time: 11 min
Baking time: 4 min
You'll need a cloud infrastructure provider where you can:
- Instantiate a host instance
- Configure the firewall rules to allow ports to pass through from the Internet into the instance
For example, Oracle Cloud as it has an "Always Free" tier that allows me to run this for free. Follow the steps below to check this off.
- https://cloud.oracle.com.
Sign Up
for an Oracle Cloud Infrastrcture account.- Choose a region that's reasonably proximate to where you live.
We've got all of the firewall management within the host handled, but we'll need to make sure that our cloud infrastructure provider allows a few ports through to our host.
For example, in Oracle Cloud we'll need to configure our Security List on our Virtual Cloud Network. Follow the steps below to check this off.
- In Oracle Cloud, in the search entry at the top, query for
Virtual Cloud Networks
and navigate into the service (hereforth referred to as VCN). - In the VCNs page, tap the button to
Create VCN
. - Specify a name. Specify an
IPv4 CIDR block
of10.0.0.0/16
. Save. - Tap the link for the new VCN.
- On the VCN detail page, tap
Create Subnet
. - Specify a name. Specify an
IPv4 CIDR block
of10.0.0.0/24
. Save. - Tap the link for the new Subnet.
- On the Subnet page at the bottom, tap the link for
Default Security List for ...
. - Repeatedly
Add Ingress Rules
for the ports you want to make accessible. - Adding TCP/80 example, on the add ingress rule page,
Source Type
=CIDR
,Source CIDR
=0.0.0.0/0
,IP Protocol
=TCP
,Source Port Range
=''
,Destination Port Range
=80
,Description
=LetsEncrypt challenge
. - When done,
Ingress Rules
should look something like this.
Ports are as follows...
Description | Protocol | Port | Mandatory? |
---|---|---|---|
SSH | TCP | 22 | Yes (pre-enabled) |
LetsEncrypt Challenge | TCP | 80 | Yes |
Wireguard | UDP | 51820 | Yes |
DNS-over-HTTPS | TCP | 443 | |
DNS-over-TLS | TCP & UDP | 853 | |
DNS-over-QUIC | TCP & UDP | 1853 | |
DNSCrypt | TCP & UDP | 2853 |
- In Oracle Cloud, in the search entry at teh top, query for
Internet Gateways
and navigate into the service (hereforth referred to as IG). - In the IGs page, tap the button to
Create Internet Gateway
. - Specify a name. Save.
- In Oracle Cloud, in the search entry at teh top, query for
Route Tables
and navigate into the service (hereforth referred to as IG). - Tap the link for the default route table.
- Tap the button to
Add Route Rule
. - For
Target Type
, specifyInternet Gateway
. ForDesination CIDR Block
, specify0.0.0.0/0
. ForTarget
, select the IG you created. Save withAdd Route Rule
.
We need our cloud host instance where a-hole
will run.
For example, in Oracle Cloud we'll need to create and configure a new instance. Follow the steps below to check this off.
- In Oracle Cloud, in the search entry at the top, query for
Instances
and navigate into the service. - In the Instances page, tap the
Create Instance
button. - Specify a name.
- In the
Image and shape
section, tapEdit
andChange image
toUbuntu
. Select image. - Below in the
Image and shape
section,Change shape
toVM.Standard.E2.1.Micro
which is under theSpecialty and previous generation
tab. Select shape. - In the
Add SSH keys
section, specify a key pair however you wish in order to access the host once it's built. If you're not familiar with SSH, take a moment to learn how to generate keys and what role they play in SSH. - At the very bottom of the page,
Create
. - On the
Instance details
page, after a little while, a public IP will be assigned and populate theInstance access
section next toPublic IP address
. Copy and save this IP address somewhere; it'll be needed later.
We need to use DDNS to identify our host by a domain name. If we used its IP, we would have to painstakingly update all client configurations in the event of the host being assigned a new IP.
For example, you can up a DDNS domain name via DuckDNS. Follow the steps below to check this off.
- Go to https://duckdns.org.
- Register an account.
- In the middle of the page, there is an entry for a new domain name and a button to
add domain
. Enter yours. - Your domain name should now be listed in the middle of the page. Above, there is a section that lists account details and included in that is
token
. Copy and save the token value somewhere; it'll be needed later.
When running remote-init.sh
, placeholder values are replaced with parameters provided. Placeholders in files are denoted by postpended !!!
, such as PLACEHOLDER!!!
. The following files also have some other optional tuneables:
docker-compose.yml
: Change TZ
from America/New_York to your own timezone.
unbound.conf
: Change forward-addr
from OpenDNS IPs to Internet DNS servers of your choice.
Peruse other files if you'd like and there may be some other modifications you'd like to make, but I've designed most other things to be tightly coupled.
Run remote-init.sh
from your local Linux shell which orchestrates various configurations of the cloud host instance to get it ready to run a-hole
. Take care in inputting your parameters properly as arguments. Escape special characters (i.e. in passwords) and wrap your peer list in double quotes:
> ./remote-init.sh IP EMAIL DDNS DDNS-PASS PI-PASS PEERS (COPY-FROM-DDNS)
> ./remote-init.sh <the IP of your host instance> i.e. 123.456.789.000
<your email address> i.e. me@gmail.com
<DDNS domain name> i.e. my.duckdns.org
<your DuckDNS token> i.e. 10tkcxmMC20kfj9aj903q04Gj94j2l4C9s8S
<your desired Pihole password> i.e. mypip4ss
<comma-separated list of peers> i.e. "pc, pixel, iphone"
(optional) <DDNS of instance to restore from> i.e. my-test.duckdns.org
> ./remote-init.sh 123.456.789.000 me@gmail.com my.duckdns.org 10tkcxmMC20kfj9aj903q04Gj94j2l4C9s8S mypip4ss "pc, pixel, iphone" my-test.duckdns.org
Once
remote-init.sh
has completed and you're SSH'd into your cloud host instance, you're up and running! Now you can usecontrol.py
to perform common operations...
SSH into your host instance from your local machine:
> ssh ubuntu@<your DDNS domain name>
> ssh ubuntu@my.duckdns.org
Set or reset the password to login to pihole:
> ./control.py pihole password <mypassword>
> ./control.py pihole password mYa-h0le!
[✓] New password set
Bring a-hole
up:
> ./control.py - up
Bring a-hole
down:
> ./control.py - down
View logs of a container:
> ./control.py <container> ?
> ./control.py certbot ?
Account registered.
Requesting a certificate...
Open an interactive shell to a container (if it supports a shell):
> ./control.py <container> /
> ./control.py nginx /
nginx >
Run an arbitrary command in a running container:
> ./control.py <container> - <arbitrary command>
> ./control.py - echo "hi a-hole"
hi a-hole
- On the cloud host, run the following command to print out the Wireguard client configuration:
> ./control.py wg config <client name> > ./control.py wg config pc [Interface] Address = 10.1.1.3 PrivateKey = 8Jzn+8E1OLkcbnuDkreBtaEk8Bvhj6HTQHv6zn4GK2Y= ListenPort = 51820 DNS = 10.1.1.1 [Peer] PublicKey = UnRXSLmmlcIU8uC2QUcO6Wxm9YVUCycVg9t4bBcY+CI= PresharedKey = Txrli+SFvQe4mR970/EqjCSgfy0A2/QpDhpKaNq/Mek= Endpoint = me.ddns.net:51820 AllowedIPs = 10.1.0.0/16
- Copy all of the above lines including
[Interface]
. - Open Wireguard on your PC and
Add empty tunnel...
. - Paste the above into tunnel configuration, name it
a-hole
, and save it. - Activate
a-hole
and the client device is now up and running.
Possible on Android devices but I believe iOS does not support DNS-over-TLS.
- This depends on specific Android OS, but generally, open
Settings
. - Search settings for
Private DNS
, open this section of Settings. - Select
Private DNS provider hostname
and enter your DDNS domain name i.e.my.duckdns.org
.
- On the cloud host, run the following command for a QR of the Wireguard client configuration:
> ./control.py wg qr <client name>
> ./control.py wg qr mobile
PEER mobile QR code:
█████████████████████████████████████████████████████████████████████
█████████████████████████████████████████████████████████████████████
████ ▄▄▄▄▄ █ ▄▀▀▀▄ █▀▄█▀▄ ▄▄▄▄█▀▄▀███▀ ▄█ █▀ ▄▄ ▄▄██ █ ▄▄▄▄▄ ████
████ █ █ ███ ▀▀ ▄█▀▄█▀▀▀▄▄▄▄█▄▀▄▄█▀▀▄▄▄▄▄███▀ ▄▀█ █ ▄ █ █ █ ████
████ █▄▄▄█ █ ▀ ▄▀███▄▄██ ▄█ ▀██ ▄▄▄ █ ▄▀▄ ▄██ ▄▀▄ ▄▄▀ ▄██ █▄▄▄█ ████
████▄▄▄▄▄▄▄█ █ █▄█ █ ▀▄█▄▀▄█ ▀▄██ ▀█ ▀ ▀ ▀▄█ █▄▀ █▄█ █▄▀ █▄▄▄▄▄▄▄████
████▄▄▄▀█▄▄▀ ▄▀▄ █ ▀█ ▄ █ ▀▄█▄█ ▄▄ █▄▄▄ ▄▀▄█▄▄ ▀██ ▀▄▀▄ ▄▄ ▀▀▄▄████
█████▄█ █ ▄▀ ▄█▄ ▄███ ▀ ▄▄ ▄▄▀█ ▄▄▀▄█ ▀▄ ██▀█ ▄ ███ ▀█ ▄█▀ ████
████ █ █ ▄ ▀█ ▀▄█▄ ▄█ ██▀▄ ██ ▀█████ █▀ ▄▄ ▄ ▄█ █▄▄▄▀▄ ██▄▀▄▀▀▄█████
████▄▀██▄▀▄█▄ ▄▀ ▄█▄███▄█▀▄█ ▀▄▄▄▀ █ ▀▄▄▄ ▄▄▀█ ▀▄ █ ▄▄ ▀▄██▀ ████
██████ █ ▄▄█▄ ▄▀▀ ▄█▄█ ███▀▄█▄▀ █ ▄ ▄█▄▀▄▀▄ ██ █▄ ▄ █ ▄▀▄▀▄▀▀██████
████▀██ ▄█▄█▄█▄▀ ▄ █▄▀▀▀█ ▀▄ ▄█ █ ▄█ ▀█▄▄▀▀ ▀ █▄█▄█ █▄▀▀ ▀▀▄▀ ████
████▀▄ ▀ █▄▄▄ ▄ ▄ ▄▀▄ ▄▀ █ ▄ ▄ ▄ ▄▄█▄ ▀▄▄▄▄▄█ ▄▄ █ █ ▄▄▀▄ ████
████▀▀▄ ▄ ▄▀ █▀█▄▄▄▄█▀▄ █▀█▄▄▄ █ █▄ ▄▄▄ █▀▄▀▀▀▄ █▀▄█ ██ ▄█▄▀▀▄█ ████
████▄▄▄█▀▄▄▀▀███ ▄ ▄███▄▀▄▄█▄▀▄▄▀▄▀▀▄▄ ▄▀██ ▄█ ▄█▄█ ▄ █▀▄█ █ █████
████▄▄▄█ ▀▄ ▀ ▄▀▀██▀▄▄██▄ ▀ ▄▀▄ █▄▄█▀ ▀▄▀ ▀▀▄ ▀▄ ▄ █ ▄▀▄▄ ▄▀ ████
████▀▀█▄ ▄▄▄ ██▀▀ ▀▄█ ▄█ ▀██ ▄█ ▀▄ ▀█▀ █▄▄ █ █▄▄ █▀ ▄▄▄ ▄▄ ████
██████▀██ ▀█ ▀▀█▀█ ▄ ▄▄ ▄█▀ ▀▄ █▄█ █▄█▀ ▄▄ ▀▄▄▄▄█▀ ▀▄█ █ ▀ █ ▄████
████▀▀ ▀ ▄▄▄ ▄▀▄▀▄█ █▄ █ ▄▄▄█ ▀▄ ▄ ▄ █ ▄ ▄█ ▀▄ ▄▄▄▀ ██▀▄▄ ▄▀▄▄ ████
████▀▀▀ ▄▄▄█▀█▀ ▀▀▀ ▀ ▄ █▄▄▄▀ ▄ █▀▄▄▀▀▀▄█ ▀█▀▀▀█▄ ██▄▄█▄▄▀▀▄▀▄████
█████▀▀▀▄█▄ █▄██▀ █▀█▀ ███ ▀█ ██▀▀▄▄ █▄ ▄▄▄▄ █▄▀ ▄ ▄▀█ ██ ▀▀ ████
█████▀ ▀▄▄▄▄ █▄█▀▀█▄ ▄█ ▄ ▀▄██ ▄▀▄▀██ ▀▀ █ ██ █▀▄ ▀█▀ ▀▄▀ ██ ████
█████ █▄ ▄▄▀▀█▀▄ ▄█▀█ ▄ ▀▄██ ▄ ▀ ▀▄ ▄▀▄▀▄▀▄██▄ ▀██▄ ▄ ▀█▄▀▄▀██ ████
████▀█▄█▀ ▄▄ ▄██▄█▀▄ ▄▀ ▀▀ ▄▄█▀ ▀▄▄ █▄▄█ ██▄ ▄▄▀▀ ▄▀ ██▀▀▀ ▄█ █ ▄████
████ █▀█▀█▄██ █ █▀ ▀▄ ███ █▀ █▀▀ ▄▀ ▄█▄▀▄ ▄███ ▄█ ▄ █ ▀█ ▄ ████
████▄▄███▀▄█▀▀█ ▀▄▄▄ ██ █▀ ▄▀▄▄█ ▄▄█▄▀▀▀ █▀▀█ ▀▄▄█ ▄▀ ▀█ ▀▄▀██ ████
█████▀▄█▄ ▄█▄▀ ▀█ ▄█▄▀▄ ▄██▄▄▀ ██ ██ █▄▄▀▄█▄▀█ ▄█▄▀▄▀▄▄███▄▄█▀▀█████
████▀▀ ▄ ▄▄▄ ▀▄▄ ▀█▄▄▄██ ▄▄▄▄▄▄▀▀▄▄█▄█▄ ▄▀ ▀███ ▀▀█▀█ ▀ ██▀▄████
████▄▄▄▄██▄█▀█▀▀ ▄ ██▄ ██▄ ▄▄ ▄ ▄▄█ ▀▄ ▄▄█ ▄▄█▄▄█▄█▄██ ▀▄▄▄ ▀█ ████
████ ▄▄▄▄▄ █▀ ▄▄▀▀█ ▄█▀▀▄█ ▀▄ ██ ▀▄█ █▀▄ █▀█ █▀▄▀██ ▄█ █ ▀▄ ▄▄ ████
████ █ █ ██ ▄███▄█████▄▀▄▄█▄▀ ▄ ▄ ▄ █▀ █▄█▄ █▄▄▀█▄▄ █▄ ▄▄ ▄▀█▄████
████ █▄▄▄█ █ ▀█▄▀▀▄▀▄█ █▄▀▄█ ▀▄█▄▄▄▄▀▀█▄▀ ▀ ▀█▄▄ ▄▀ ▀█▄▀▄█▄██▄▄████
████▄▄▄▄▄▄▄█▄█▄█▄██▄▄█▄███▄▄▄█▄█▄█▄█▄█▄█▄▄▄▄▄▄▄█▄▄▄█▄██▄▄▄▄▄████▄████
█████████████████████████████████████████████████████████████████████
█████████████████████████████████████████████████████████████████████
- On your mobile device, install the Wireguard app.
- Open the Wireguard app and tap the
+
in the corner. - Select
Scan from QR code
. - Point your camera at the QR, name it
a-hole
, and save it. - Enable
a-hole
and now this client is up and running. - You can optionally further configure your mobile device to automatically enable
a-hole
whenever the device is on.
- Open
docker-compose.yml
for editing on your host instance. - Find the environment variable
PEERS:
under theWireguard
section. - Postpend the name of your new peer(s) onto this comma-separated list, then save it.
- On your host instance, run the following command:
> ./control.py - up
- View the QR code or configuration of your new Wireguard client:
> ./control.py wg qr <new client name>
> ./control.py wg config <new client name>