Exploring the transition from Control D to Unifi Cloud Gateway for ad-blocking with OISD lists. Understand the inner workings of Unifi’s DNS filtering system and how to leverage OISD domain blocklists efficiently.
The reason
I’ve been a longtime user of Control D, primarily for bypassing geo-restricted video services and blocking ads for all users on my home network. In recent years, Control D has introduced community-based ad block lists. After trying many of them and receiving complaints from my partner, I settled on OISD.
One of my main issues with Control D is occasional slow DNS responses and, once or twice a month, complete service outages. This might be an issue on my end, but I wanted to address it.
Last month, I switched from my Fritzbox router to a Unifi Cloud Gateway, which also supports adblocking. I decided to give this new adblocking feature a try so that I can stop using Control D, as I no longer need it for bypassing geo-restricted video services.
Investigate how UCG is working with Ad blocking
Within the Unifi Network settings under Settings » Security, you can enable ad blocking for specific networks:
The Unifi Cloud Gateway (UCG) runs on a Unix-based operating system, enabling the use of standard tools like grep, vim, find, cron, sed, etc.
On the UCG, you can verify that DNS filtering is active for three networks using the ps command:
root@Router:~# ps aux | grep dns
root. 1215. 0.2. 0.5 1452968 17840 ? S<l. 11:15. 0:57 /usr/sbin/dnscrypt-proxy -config /run/dnscrypt-proxy.toml
root. 1251. 0.0. 0.0. 0. 0 ? S. 11:15. 0:00 [dns_thread]
nobody. 2466. 0.0. 0.0. 9080. 2900 ? S<. 11:15. 0:07 /usr/sbin/dnsmasq — conf-dir=/run/dnsmasq.conf.d/ — pid-file=/run/dnsmasq.pid
root. 2492. 0.0. 0.0. 8948. 1308 ? S<. 11:15. 0:00 /usr/sbin/dnsmasq — conf-dir=/run/dnsmasq.conf.d/ — pid-file=/run/dnsmasq.pid
nobody. 4269. 0.0. 0.0. 8948. 2504 ? S<. 11:15. 0:00 /usr/sbin/dnsmasq — conf-file=/run/dns.conf.d/dnsmasq-ppp0.conf — pid-file=/run/dnsmasq-ppp0.pid
nobody. 33076. 0.0. 0.0. 29188. 1212 ? S<. 12:01. 0:01 dnsmasq -r /run/dnsfilter/dns-172.31.4.161-resolv.conf -C /run/dnsfilter/dns-172.31.4.161-conf.conf — pid-file=/run/dnsfilter/dns-172.31.4.161.pid
nobody. 33084. 0.1. 0.6. 29188 21044 ? S<. 12:01. 0:28 dnsmasq -r /run/dnsfilter/dns-172.31.4.193-resolv.conf -C /run/dnsfilter/dns-172.31.4.193-conf.conf — pid-file=/run/dnsfilter/dns-172.31.4.193.pid
nobody. 33091. 0.0. 0.6. 29188 21036 ? S<. 12:01. 0:05 dnsmasq -r /run/dnsfilter/dns-172.31.4.1-resolv.conf -C /run/dnsfilter/dns-172.31.4.1-conf.conf — pid-file=/run/dnsfilter/dns-172.31.4.1.pid
root. 44750. 0.0. 0.3 240596 11628 ? S<l. 12:19. 0:00 /sbin/utm_dns_filter_capture -I br0 br2 br3 -V 6
root. 238514. 0.0. 0.0. 4924. 692 pts/0. S+. 18:23. 0:00 grep — color dns
The output displays processes related to dnsmasq, indicating DNS filtering is functioning on these networks.
There are two configuration files:
- dns resolver
- dns filtering
DNS Resolver
The content of the resolv.conf is:
root@Router:~# cat /run/dnsfilter/dns-172.31.4.1-resolv.conf
nameserver 203.0.113.1
The IP address 203.0.113.1 corresponds to a dedicated DNS filtering interface created by Unifi, as shown in the network interface details:
root@Router:~# ifconfig
dnsfilter: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>. mtu 1500
inet 203.0.113.1. netmask 255.255.255.0. broadcast 0.0.0.0
inet6 fe80::7475:5ff:fed3:2a93. prefixlen 64. scopeid 0x20<link>
inet6 2001:db8:1000::1. prefixlen 64. scopeid 0x0<global>
ether 96:72:03:90:fc:59. txqueuelen 1000. (Ethernet)
RX packets 11056. bytes 1425993 (1.3 MiB)
RX errors 0. dropped 0. overruns 0. frame 0
TX packets 13638. bytes 1676404 (1.5 MiB)
TX errors 0. dropped 0 overruns 0. carrier 0. collisions 0
Each network where DNS filtering is enabled has its own interface, like dnsfilter.
To redirect DNS requests to the filtering interface (203.0.113.1), Unifi uses iptables rules:
root@Router:~# iptables -L -t nat | grep 203.0.113.1
DNAT. tcp. — 172.31.4.0/27. anywhere. tcp dpt:domain to:203.0.113.1:53
DNAT. udp. — 172.31.4.0/27. anywhere. udp dpt:domain to:203.0.113.1:53
These iptables rules ensure that DNS requests from the specified networks are forwarded to the DNS filtering interface.
Now that we understand how Unifi implements ad blocking, the next question is: which domains are being blocked?
DNS Filtering
Let’s start by examining the configuration file to understand which lists are currently active:
root@Router:~# cat /run/dnsfilter/dns-172.31.4.1-conf.conf
Configuration of DNS Forwarder
interface=dnsfilter0
no-dhcp-interface=dnsfilter0
no-negcache
conf-file=/run/dnsfilter/dns-172.31.4.1-ads.list
conf-file=/run/dnsfilter/dns-172.31.4.1-black.list
conf-file=/run/dnsfilter/dns-172.31.4.1-white.list
Currently, both the black and white lists are empty. However, we may add domains to them in the future. The primary list used for blocking domains is the ads.list.
Now, let’s examine the domains that are being blocked by the ads.list:
root@Router:~# tail -n10 /run/dnsfilter/dns-172.31.4.1-ads.list
address=/www.rodepaudie.com/#
address=/dflinity.org/#
address=/steofenore.cyou/#
address=/clinicservicecare.com/#
address=/na1uren00n41.store/#
address=/www.b7d643c5c9cf4e4092783ef022a69fdf.vistvx.pl/#
address=/hotjar.com/#
address=/w55c.net/#
address=/crypto-group.org/#
address=/ezonn.com/#
address=/navi56.ru/#
But where does Unifi get this list from? If we search the entire filesystem for the ads.list, we will discover the bash script responsible for populating this list.
root@Router:/usr/share/ubios-udapi-server/utm# grep -Ril 'ads.list' /
/mnt/.rofs/usr/share/ubios-udapi-server/ips/bin/getsig.sh
/mnt/.rofs/usr/share/ubios-udapi-server/utm/ads.list
/mnt/.rofs/usr/share/ubios-udapi-server/utm/adsblockipv4.list
/mnt/.rofs/usr/share/ubios-udapi-server/utm/adsblockipv6.list
/mnt/.rofs/usr/share/ubios-udapi-server/utm/bin/ubios-dns-filter-ads.sh
/mnt/.rofs/usr/share/ubios-udapi-server/utm/bin/ubios-dns-filter-category.sh
/mnt/.rofs/usr/share/ubios-udapi-server/utm/bin/ubios-dns-filter-whitelist.sh
All six files seem really interesting. Let’s start with the sh scripts.
root@Router:/usr/share/ubios-udapi-server/utm# cat /mnt/.rofs/usr/share/ubios-udapi-server/utm/bin/ubios-dns-filter-ads.sh
#######################################
# Update DNS database.
#
# ARGUMENTS:
# None
#
# RETURN:
# 0 - Success (file is valid)
# 1 - Fail
# 2 - Already up-to-date
#######################################
update_dns_reputation() {
USER_AGENT="model/${DEVICEMODEL} version/${DEVICEVERSION} device_id/${DEVICE_ID}"
log "Ads start update."
OUTPUT="${ADSRUNPREFIX}/ads.list.gz"
URL="${UPDATEURL}/dns/ads.list.gz"
download_and_validate_file "${USER_AGENT}" "${URL}" "${URL}.hash" "${OUTPUT}"
RC=$?
if [ "${RC}" -ne "0" ]; then
log "The download will be retried on the next execution, skipping."
return ${RC}
fi
/bin/gzip -d "${OUTPUT}" -c >"${ADSRUNPREFIX}/ads.list.tmp"
GZRC=$?
if [ "${GZRC}" -ne "0" ]; then
log "${TYPE} extraction failed. Return code: ${GZRC}"
log "The update will be retried on the next execution, skipping."
return 1
fi
mv "${ADSRUNPREFIX}/ads.list.tmp" "${ADSRUNPREFIX}/ads.list"
log "Ads database extracted."
while IFS= read -r DOMAIN; do
# Ignore own domains
[[ "$DOMAIN" == *.ui.com ]] ||
[[ "$DOMAIN" == *.ubnt.com ]] && continue
echo "address=/$DOMAIN/#" >>"${ADSRUNPREFIX}/adsblockipv6.list.tmp"
echo "address=/$DOMAIN/#" >>"${ADSRUNPREFIX}/adsblockipv4.list.tmp"
done <"${ADSRUNPREFIX}/ads.list"
mv "${ADSRUNPREFIX}/adsblockipv4.list.tmp" "${ADSRUNPREFIX}/adsblockipv4.list"
mv "${ADSRUNPREFIX}/adsblockipv6.list.tmp" "${ADSRUNPREFIX}/adsblockipv6.list"
# Update completed, save to the Persistent disk
cp "${ADSRUNPREFIX}/ads.list" "${ADSFIRMWAREPREFIX}/ads.list"
cp "${ADSRUNPREFIX}/adsblockipv4.list" "${ADSFIRMWAREPREFIX}/adsblockipv4.list"
cp "${ADSRUNPREFIX}/adsblockipv6.list" "${ADSFIRMWAREPREFIX}/adsblockipv6.list"
# Restart utm_dns_filter_capture
if [ -s /run/utm_dns_filter_capture.pid ]; then
UTM_DNS_FILTER_CAPTURE_PID=$(cat /run/utm_dns_filter_capture.pid)
log "utm_dns_filter_capture PID file found, restart service."
if ! /bin/kill -SIGTERM "${UTM_DNS_FILTER_CAPTURE_PID}" >/dev/null 2>&1; then
log "utm_dns_filter_capture fail to sent signal."
fi
fi
rm -f "${OUTPUT}" 2>&1
log "Ads update finished."
return 0
}
Yes, we found the script we were looking for. Unifi retrieves the list from the following location: https://assets.unifi-ai.com/ads.list.gz. The script extracts the file and copies the content to adsblockipv4.list and adsblockipv6.list.
Afterward, it kills the dnsfilter process, prompting the system to restart the process.
The next question is: how often is this list being updated? Let’s perform another grep.
root@Router:/usr/share/ubios-udapi-server/utm# grep -Ril 'getsig.sh' /
/etc/cron.d/ips-service-alien
/etc/cron.d/ips-service-tor
/etc/cron.d/ips-service-ads
/etc/cron.d/ips-service-rules
Interesting. There are cronjobs that initiate this process:
root@Router:/usr/share/ubios-udapi-server/utm# cat /etc/cron.d/ips-service-ads
MAILTO=""
0 */24. * * * root /usr/share/ubios-udapi-server/ips/bin/getsig.sh 'ads' 'xx:xx:xx:xx:xx:xx' 'UDRULT.ipq5322.v3.2.12.7765dbb.240126.0152' 'UDRULT' 'a748' 'splay'
So, does it start the process every day at 00:00?
Upon further examination of the script, I discovered that the splay option introduces a random sleep
How we use OISD as domain blocking list
You can find all the lists on the OISD website, including the comprehensive OISD big list:
We’re particularly interested in the dnsmasq2 file since Unifi utilizes DNSMasq version 2.86.
root@Router:~# df -hT
Filesystem. Type. Size. Used Avail Use% Mounted on
udev. devtmpfs. 1.5G. 0. 1.5G. 0% /dev
tmpfs. tmpfs. 296M. 107M. 189M. 37% /run
/dev/disk/by-partlabel/root. ext4. 2.0G. 1.2G. 688M. 63% /boot/firmware
/dev/loop0. squashfs. 568M. 568M. 0 100% /mnt/.rofs
/dev/disk/by-partlabel/overlay. ext4. 9.3G. 1.3G. 7.6G. 15% /mnt/.rwfs
overlayfs-root. overlay. 9.3G. 1.3G. 7.6G. 15% /
/dev/disk/by-partlabel/log. ext4. 974M. 101M. 807M. 12% /var/log
/dev/disk/by-partlabel/persistent ext4. 2.0G. 128M. 1.7G. 7% /persistent
tmpfs. tmpfs. 1.5G. 28K. 1.5G. 1% /dev/shm
tmpfs. tmpfs. 5.0M. 0. 5.0M. 0% /run/lock
tmpfs. tmpfs. 738M. 44K. 738M. 1% /tmp
tmpfs. tmpfs. 16M. 0. 16M. 0% /var/log/ulog
tmpfs. tmpfs. 64M. 1.4M. 63M. 3% /var/opt/unifi/tmp
All data mounted on the /persistent volume (as the name suggests) will be preserved during firmware updates, reboots, and other operations.
I’ve developed a script that accomplishes the following tasks:
- Downloads the dnsmasq2 file from oisd.nl.
- Modifies the file to match the syntax of the original file.
- Copies the content to adsblockipv4.list and adsblockipv6.list.
- Restarts the utm_dns_filter_capture.
- Restarts dnsmasq.
Download and alter the dnsmasq2 file from oisd.nl
Here’s an example layout of the downloaded file:
# Version: 202405081506
# Title: oisd small
# Description: Block. Don't break.
# Syntax: DNSMasq ver 2.86 and above
# Entries: 48515
# Last modified: 2024–05–08T15:06:31+0000
# Expires: 1 hours
# License: https://github.com/sjhgvr/oisd/blob/main/LICENSE
# Maintainer: Stephan van Ruth
# Homepage: https://oisd.nl
# Contact: contact@oisd.nl
local=/0-02.net/
local=/0.101tubeporn.com/
local=/0.code.cotsta.ru/
local=/000.gaysexe.free.fr/
local=/000free.us/
local=/000tristanprod.free.fr/
local=/000webhostapp.com/
local=/002777.xyz/
local=/00280181d0.com/
local=/00518b6f0c.com/
curl -s -o "${BASE}"/dnsmasq2 https://big.oisd.nl/dnsmasq2
sed -i '/^$/d' "${BASE}"/dnsmasq2 #removes all empty lines
sed -i '/^#/d' "${BASE}"/dnsmasq2 #removes all lines starting wwith #
sed -i 's/local=/address=/g' "${BASE}"/dnsmasq2 #replace local with address
sed -i s/$/#/ "${BASE}"/dnsmasq2 #add # after ther last /
Here’s a formatted display of the result:
address=/0-02.net/#
address=/0.101tubeporn.com/#
address=/0.code.cotsta.ru/#
address=/000.gaysexe.free.fr/#
address=/000free.us/#
address=/000tristanprod.free.fr/#
address=/000webhostapp.com/#
address=/002777.xyz/#
address=/00280181d0.com/#
address=/00518b6f0c.com/#
Copy the content to adsblockipv4.list and adsblockipv6.list
To copy the content over to the adsblockipv4.list and adsblockipv6.list files, you can use the following commands:
cat "${BASE}"/dnsmasq2 > /run/utm/adsblockipv4.list
cat "${BASE}"/dnsmasq2 > /run/utm/adsblockipv6.list
Super easy, be that is needed to fill the file /run/dnsfilter/dns-172.31.4.1-ads.list
Restart utm_dns_filter_capture
# Restart utm_dns_filter_capture
if [ -s /run/utm_dns_filter_capture.pid ]; then
UTM_DNS_FILTER_CAPTURE_PID=$(cat /run/utm_dns_filter_capture.pid)
log "utm_dns_filter_capture PID file found, restart service."
if ! /bin/kill -SIGTERM "${UTM_DNS_FILTER_CAPTURE_PID}" >/dev/null 2>&1; then
log "utm_dns_filter_capture fail to sent signal."
fi
fi
In this script snippet inspired by UniFi, we’re checking for the utm_dns_filter_capture process by its PID (Process ID). If the process is found, the script terminates it. Subsequently, the system automatically restarts the daemon.
Restart dnsmasq
Finally, we need to restart DNSMasq to load the updated list and enable domain blocking.
restartdnsfilter() {
for killdns in $(cat /run/dnsfilter/*.pid 2>/dev/null); do
kill -9 "${killdns}"
done
if [ -f "/run/dnsfilter/dnsfilter" ]; then
sleep 5
for restartns in $(cat /run/dnsfilter/dnsfilter); do
ip netns exec "${restartns}" dnsmasq -r /run/dnsfilter/"${restartns}"-resolv.conf -C /run/dnsfilter/"${restartns}"-conf.conf --pid-file=/run/dnsfilter/"${restartns}".pid
done
fi
}
restartdnsfilter
This section of the script is also adapted from the Unifi script. Similarly, it terminates the process, but the key distinction is that it initiates the process independently rather than relying on the system to do so.
the complete script
Putting it all together will make the following script:
#!/bin/bash
BASE="/persistent/scripts"
log() {
echo "$*"
/usr/bin/logger -t "ads" "$*"
}
backupDate=$(date +%s)
restartdnsfilter() {
for killdns in $(cat /run/dnsfilter/*.pid 2>/dev/null); do
kill -9 "${killdns}"
done
if [ -f "/run/dnsfilter/dnsfilter" ]; then
sleep 5
for restartns in $(cat /run/dnsfilter/dnsfilter); do
ip netns exec "${restartns}" dnsmasq -r /run/dnsfilter/"${restartns}"-resolv.conf -C /run/dnsfilter/"${restartns}"-conf.conf --pid-file=/run/dnsfilter/"${restartns}".pid
done
fi
}
log "Start updating ads block list"
curl -s -o "${BASE}"/dnsmasq2 https://big.oisd.nl/dnsmasq2
sed -i '/^$/d' "${BASE}"/dnsmasq2
sed -i '/^#/d' "${BASE}"/dnsmasq2
sed -i 's/local=/address=/g' "${BASE}"/dnsmasq2
sed -i s/$/#/ "${BASE}"/dnsmasq2
cp /run/utm/adsblockipv4.list /run/utm/adsblockipv4.${backupDate}.list
cp /run/utm/adsblockipv6.list /run/utm/adsblockipv6.${backupDate}.list
cp "${BASE}"/dnsmasq2 /run/utm/adsblockipv4.list
cp "${BASE}"/dnsmasq2 /run/utm/adsblockipv6.list
rm -rf "${BASE}"/dnsmasq2
# Restart utm_dns_filter_capture
if [ -s /run/utm_dns_filter_capture.pid ]; then
UTM_DNS_FILTER_CAPTURE_PID=$(cat /run/utm_dns_filter_capture.pid)
log "utm_dns_filter_capture PID file found, restart service."
if ! /bin/kill -SIGTERM "${UTM_DNS_FILTER_CAPTURE_PID}" >/dev/null 2>&1; then
log "utm_dns_filter_capture fail to sent signal."
fi
fi
sleep 20
restartdnsfilter
log "OISD Updated"
Automate the script to run during the night
The last part is to run this script every night. This as well is the easy part
root@Router:~# cat /etc/cron.d/ips-service-ads
MAILTO=""
0 */24. * * * root /usr/share/ubios-udapi-server/ips/bin/getsig.sh 'ads' 'xx:xx:xx:xx:xx:xx' 'UDRULT.ipq5322.v3.2.12.7765dbb.240126.0152' 'UDRULT' 'a748' 'splay' && /persistent/scripts/oisd.sh
Next step is to make the script runabale:
root@Router:~# chmod u+x /persistent/scripts/oisd.sh
This line grants permission to the script owner (root) to execute the script.
I added the new script at the end with a random timer using splay and the && operator. This setup ensures that our script will run only if the first part executes successfully.
NOTE: Please be aware that after every reboot or firmware update, the cronjob adjustments will be reset.
Testing
After manually running the script or waiting until the next day, we can test if it is working. This test should be conducted from a network where ad blocking has been enabled.
Server: 172.31.4.193
Address: 172.31.4.193#53
Name: zzztest.oisd.nl
Address: 0.0.0.0