Ad-Free Apps: Using VPN and Unbound to Block Banners in Applications
Recently, I bought a Xiaomi phone and was happy with it—except for the ads in the stock apps. Yes, there’s a setting to disable them, but it doesn’t get rid of all the ads. That’s why I rarely use my phone for browsing, since you can’t install ad blockers in most mobile browsers.
Some people say ads help support free projects, and maybe that was true once. But now, ads are mostly about collecting data on you—your habits, search queries, and more. Just look at the traffic your phone generates during normal use. On my desktop, I’ve long used plugins like NoScript and uBlock, so I almost forgot about ads—until my phone kept reminding me.
The last straw was when I analyzed the traffic from the Xiaomi MiFit app and found links to suspicious APK files in the server responses (I won’t describe them here, but you can intercept these requests yourself—they use plain HTTP).
I had a spare server with a public IP, so I decided to set up a VPN on it and configure ad blocking for all connected devices—once and for all.
Preparation
You’ll need a server with a public IP running Linux (I used Ubuntu 16.10, but these steps work on other distros—just adjust the package manager commands as needed).
What to Do with an Old Ubuntu Version
If your server has outdated repository links, you’ll get errors when updating. The repositories have moved to the old-releases
subdomain. Fix and update with:
sed -i 's|us.archive|old-releases|' /etc/apt/sources.list
sed -i 's|//security|//old-releases|' /etc/apt/sources.list
apt update && apt upgrade -y && reboot
Installing OpenVPN
There are two ways:
- Easy: Use a ready-made Docker image (for those who don’t want to mess with configs).
- Advanced: Install and configure OpenVPN manually.
We’ll cover both, starting with the easy way.
OpenVPN Docker Image
- Download the image:
docker pull kylemanna/openvpn
- Create the VPN server config:
docker run -v /etc/openvpn:/etc/openvpn --log-driver=none --rm -it kylemanna/openvpn ovpn_genconfig -u udp://YOUR_SERVER_IP
Replace
YOUR_SERVER_IP
with your server’s public IP. - Generate certificates:
docker run -v /etc/openvpn:/etc/openvpn --log-driver=none --rm -it kylemanna/openvpn ovpn_initpki
- Add a user (replace
CLIENTNAME
with your client’s hostname):docker run -v /etc/openvpn:/etc/openvpn --log-driver=none --rm -it kylemanna/openvpn easyrsa build-client-full CLIENTNAME nopass
- Get the client config:
docker run -v /etc/openvpn:/etc/openvpn --log-driver=none --rm -it kylemanna/openvpn ovpn_getclient CLIENTNAME > /root/CLIENTNAME.ovpn
- Start OpenVPN as a daemon:
docker run -v /etc/openvpn:/etc/openvpn -d -p 1194:1194/udp --cap-add=NET_ADMIN kylemanna/openvpn
- On the client, connect with:
sudo openvpn --config CLIENTNAME.ovpn
If you see Initialization Sequence Completed
in the log and a tun0
interface appears, you’re good to go. Add the container to autostart and move on to DNS setup.
Manual OpenVPN Setup
- Install OpenVPN:
sudo apt install -y openvpn
- Clone easy-rsa:
sudo git clone https://github.com/OpenVPN/easy-rsa.git /etc/openvpn/easy-rsa
- Edit
/etc/openvpn/easy-rsa/easyrsa3/vars
to set variables like:set_var EASYRSA_KEY_SIZE 2048 set_var EASYRSA_CA_EXPIRE 3650 set_var EASYRSA_CERT_EXPIRE 3650
- Initialize PKI:
cd /etc/openvpn/easy-rsa/easyrsa3 && ./easyrsa init-pki
- Build CA:
./easyrsa build-ca
- Build server keys:
./easyrsa build-server-full vpnserver nopass
- Build client keys:
./easyrsa build-client-full user-pc nopass
- Optionally, generate Diffie-Hellman and TLS keys:
./easyrsa gen-dh openvpn --genkey --secret /etc/openvpn/easy-rsa/easyrsa3/pki/ta.key
Write your server config to /etc/openvpn/server.conf
(see the original article for a sample config). Adjust the systemd
service if needed, then enable and start OpenVPN:
sudo systemctl enable openvpn.service && sudo systemctl start openvpn.service
journalctl -uxe openvpn.service
To generate client configs, you can use a simple bash script (see the original article for details).
Setting Up Unbound DNS Resolver
I chose Unbound because it supports DNSSEC, caching, DNS over TLS (DoT), is easy to configure, and is faster and lighter than BIND.
Compiling Unbound
- Clone Unbound and install dependencies:
git clone https://github.com/NLnetLabs/unbound.git /opt/unbound sudo apt install -y protobuf-c-compiler libevent-dev libssl-dev libsodium-dev libfstrm-dev
- Compile and install:
cd /opt/unbound ./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var --sbindir=/usr/bin --disable-rpath --enable-dnscrypt --enable-dnstap --enable-pie --enable-relro-now --enable-subnet --enable-tfo-client --enable-tfo-server --with-conf-file=/etc/unbound/unbound.conf --with-pidfile=/run/unbound.pid --with-rootkey-file=/etc/trusted-key.key --with-libevent make sudo make install
- Create the
unbound
user:sudo useradd -s /bin/false -d /etc/unbound unbound
- Add the systemd service at
/lib/systemd/system/unbound.service
(see the original article for content). - Add a tmpfiles.d anchor:
echo 'C /etc/unbound/trusted-key.key - - - - /etc/trusted-key.key' > /usr/lib/tmpfiles.d/unbound.conf
Configuring DNS over TLS
DoT adds privacy and protects against spoofing. Here’s a sample /etc/unbound/unbound.conf
(see the original article for the full config):
- Listen on all interfaces
- Log to syslog
- Allow queries only from localhost and VPN subnets
- Forward all DNS queries to DoT-enabled servers (e.g., Cloudflare)
Generate DNSSEC trust anchors:
sudo unbound-anchor -a /etc/unbound/trusted-key.key
Integrating with NetworkManager
To update /etc/resolv.conf
, install openresolv
and create /etc/resolvconf.conf
:
resolv_conf=/etc/resolv.conf
name_servers="::1 127.0.0.1"
Update resolv.conf
with:
resolvconf -u
If you use NetworkManager, set rc-manager=resolvconf
in /etc/NetworkManager/conf.d/rc-manager.conf
.
Start Unbound:
sudo systemctl start unbound
Warning: Make sure no other DNS servers or resolvers are running before starting Unbound. Check errors with journalctl -xeu unbound
.
Ad Blocking
Now for the most important part. I found several domain lists for ad blocking, such as:
Create a script at /etc/unbound/local.d/update_ads_filters.py
to parse these lists and generate Unbound rules:
#!/usr/bin/python
import requests
response = requests.get('https://pgl.yoyo.org/adservers/serverlist.php?hostformat=unbound&showintro=0&mimetype=plaintext')
hosts = requests.get('http://www.malwaredomainlist.com/hostslist/hosts.txt')
def make_unbound_zone(host):
fmt = 'local-zone: "{0}" redirect\\nlocal-data: "{0} A 127.0.0.1"\\n'
return fmt.format(host)
if response and response.status_code == 200:
unbound_format = response.text
if hosts and hosts.status_code == 200:
for line in hosts.text.splitlines():
if not line or line.startswith('#'):
continue
_, host = line.strip().split()
if host not in unbound_format:
unbound_format += make_unbound_zone(host)
print(unbound_format)
This creates entries like:
local-zone: "HOSTNAME" redirect
local-data: "HOSTNAME A 127.0.0.1"
Alternatively, you can use always_nxdomain
to return NXDOMAIN:
local-zone: "HOSTNAME" always_nxdomain
Add this script to crontab
to run weekly:
/etc/unbound/local.d/update_ads_filters.py > /etc/unbound/local.d/adservers.conf && unbound-control reload
For manual blocking, create /etc/unbound/insert_filter_domain.sh
:
#!/bin/bash
DOMAIN=$1
FILTER_FILE="/etc/unbound/local.d/ruads.conf"
echo "local-zone: \\"${DOMAIN}\\" always_nxdomain" >> ${FILTER_FILE}
unbound-control reload
DNS Resolution for Clients
It’s nice to use hostnames instead of IPs, especially since OpenVPN’s built-in DHCP assigns dynamic IPs. To automate DNS records for clients, update your OpenVPN config (/etc/openvpn/server.conf
):
script-security 2
client-connect /etc/openvpn/connect.sh
client-disconnect /etc/openvpn/connect.sh
Then create /etc/openvpn/connect.sh
:
#!/bin/bash
add_to_unbound() {
host="$1"
ip="$2"
reverse_ip=$(echo $ip | awk ' BEGIN { FS="." ; OFS="." } { print $4,$3,$2,$1 }')
unbound-control local_data "${host} IN A $ip"
unbound-control local_data "${reverse_ip}.in-addr.arpa. IN PTR ${host}"
}
del_from_unbound() {
host="$1"
ip="$2"
reverse_ip=$(echo $ip | awk ' BEGIN { FS="." ; OFS="." } { print $4,$3,$2,$1 }')
unbound-control local_data_remove "${host}"
unbound-control local_data_remove "${reverse_ip}.in-addr.arpa."
}
if [ "$script_type" == "client-connect" ]; then
add_to_unbound "${common_name}" "${ifconfig_pool_remote_ip}"
elif [ "$script_type" == "client-disconnect" ]; then
del_from_unbound "${common_name}" "${ifconfig_pool_remote_ip}"
fi
This script uses OpenVPN’s environment variables to add or remove DNS records for each client as they connect or disconnect.
Also, create /etc/unbound/local.d/localzone.conf
for server and subnet records:
insecure-lan-zones: yes
local-zone: "10.in-addr.arpa." nodefault
local-zone: "8.10.in-addr.arpa." nodefault
local-data-ptr: "10.8.0.1 vpnserver"
local-data: "vpnserver A 10.8.0.1"
Conclusion
That’s it! Install OpenVPN on your phone, connect, and test. You should see a dramatic reduction in ads, both in browsers and apps like MiFit and Avito. Enjoy your ad-free experience!