Tired of Annoying Ads and Privacy-Invading Trackers? Here’s How to Take Control
Are you frustrated with pop-up ads in your browser or ads cluttering news articles? Not to mention those pesky privacy-invading trackers? You’re not alone — but the good news is, you don’t have to put up with them.
First Protect Your Browser
The first step is to enable privacy settings in your browsers and install an ad-blocking extension like uBlock Origin. This will block ads in your browser, but that’s just one part of the solution. You can also take things a step further by setting up a DNS blackhole on your local network. This will send ads and trackers to a dead-end, keeping your devices protected across your entire home network.
A DNS blackhole works by redirecting requests for known ad-serving and tracker domains to a non-existent address, effectively blocking them. There are many options available, from free DIY solutions to paid DNS subscription services.
Add a DNS Blackhole to Your Network
If you’re running your own home network with a Unix-based server, you can easily integrate a DNS blackhole into a local BIND DNS service — and best of all, it’s completely free. If you’re already using BIND and know your way around it, you can use this script to manage BIND’s Response Policy Zone (RPZ) feature. RPZ is designed for DNS firewall/blocking purposes. The script should run on *BSD and Linux distros with proper pathnames configured. Out of the box, it has a FreeBSD default configuration.
The source of the blocked host list is provided by Steven Black on GitHub. See his page for full details.
Steps
Refer to the sample files below for these steps.
- Create a
/usr/local/etc/dns-blackhole
directory - Copy
dns-blackhole.sh
into this new directory - Create and configure your
dns-blackhole.conf
file here
Usage:
dns-blackhole.sh [OPTIONS] <on|off|status|update>
Options:
-c config_file Specify an alternate config file (default: dns-blackhole.conf)
-k Keep temporary working files (skip cleanup after update)
-q Quiet mode: suppress progress messages
-r Restart 'named' instead of reloading the RPZ
on|off Turn the blackhole on or off
status Report blackhole and 'named' status
update Fetch new blocked hosts data and rebuild zone files
- Run
dns-blackhole.sh
update
and make sure there are no errors before continuing - Add this entry inside of the
options
block in BIND’snamed.conf
:
response-policy { zone "rpz"; };
- Add this to the end of
named.conf
:
include "/usr/local/etc/namedb/dns-blackhole.zone";
- Restart
named
(e.g.,service named restart
) and check for errors - Enable the blackhole now with
dns-blackhole.sh
on
- Test to make sure it’s working (see Testing below)
Once you’re sure everything is working properly, add an entry in crontab or periodic to automate updates. Once per day is usually sufficient.
Files Added in Your namedb
Directory
dns-blackhole.zone
- included zone filedns-blackhole-enabled.rpz
- list of blocked hostnamesdns-blackhole-disabled.rpz
- empty list for disabled modedns-blackhole.rpz
- symlink (set byon
andoff
)
dns-blackhole.sh
Copy this script to your dns_blackhole_dir
directory.
#!/bin/sh
#
# dns-blackhole.sh - Manages a BIND DNS blackhole using Response Policy Zones (RPZ)
# https://www.morgandavis.net/post/simple-dns-blackhole/
#
# See below for options and usage details.
usage_and_exit() {
cat >&2 <<EOF
Usage:
$(basename "$0") [OPTIONS] <on|off|status|update>
Options:
-c config_file Specify an alternate config file (default: dns-blackhole.conf)
-k Keep temporary working files (skip cleanup after update)
-q Quiet mode: suppress progress messages
-r Restart 'named' instead of reloading the RPZ
on|off Turn the blackhole on or off
status Report blackhole and 'named' status
update Fetch new blocked hosts data and rebuild zone files
See also: https://www.morgandavis.net/post/simple-dns-blackhole/
EOF
exit 1
}
#
# Support functions
#
error_exit() {
echo "$@" >&2
exit 1
}
msg() {
if [ "$quiet" -eq 0 ]; then
echo "$@"
fi
}
missing_config() {
error_exit "Missing config '$1'."
}
fetch_file() {
url="$1"
out="$2"
if command -v fetch >/dev/null 2>&1; then
fetch -q -m -T "$fetch_timeout" -o "$out" "$url"
elif command -v curl >/dev/null 2>&1; then
curl --silent --show-error --max-time "$fetch_timeout" --output "$out" "$url"
else
error_exit "Neither fetch nor curl found. Cannot fetch files."
fi
}
make_zone() {
printf '; [built by %s]\n\n' "$0"
printf '$TTL %d\n\n' 604800
printf '@%*sIN%*sSOA%*slocalhost. root.localhost. (\n' 13 "" 5 "" 4 ""
printf '%*s%d ; Serial\n' 28 "" "$timestamp"
printf '%*s%d ; Refresh\n' 32 "" 604800
printf '%*s%d ; Retry\n' 33 "" 86400
printf '%*s%d ; Expire\n' 31 "" 2419200
printf '%*s%d ) ; Minimum\n\n' 32 "" 604800
printf '%*sIN%*sNS%*s%s.\n' 14 "" 5 "" 5 "" "$dns_server_hostname"
}
command_named() {
cmd="$1"
if command -v service >/dev/null 2>&1; then
service named "$cmd"
elif command -v systemctl >/dev/null 2>&1; then
systemctl "$cmd" named
else
error_exit "Cannot $cmd named service."
fi
}
get_symlink_target() {
link="$named_zone_files_dir/$switch_symlink"
if [ -L "$link" ]; then
basename "$(readlink "$link" 2>/dev/null || echo '')"
else
echo ''
fi
}
#
# Update zone data
#
do_update() {
# Initialize empty files if they don't exist
for f in blocked_hosts allowed_hosts; do [ -f "$f" ] || touch "$f"; done
# Create temporary working directory if it doesn't exist
[ ! -d "$tmp_dir" ] && mkdir -p "$tmp_dir"
msg "Fetching master host list..."
master_list="$tmp_dir/master_hosts_list"
attempt=0
while [ "$attempt" -lt "$max_attempts" ]; do
if [ "$attempt" -gt 0 ]; then
echo "Fetch failed. Retrying in $retry_seconds seconds..." >&2
sleep "$retry_seconds"
fi
if fetch_file "$master_host_list_url" "$master_list"; then
break
fi
attempt=$((attempt + 1))
done
if [ "$attempt" -ge "$max_attempts" ]; then
error_exit "Failed after $attempt/$max_attempts attempts."
fi
msg "Optimizing ..."
cat "$master_list" "$dns_blackhole_dir/blocked_hosts" |
sed -e 's/^[[:space:]]*//' |
grep -v '^#' |
awk '{print $2}' |
grep -Ev '^(0\.0\.0\.0|localhost)$' |
grep -v '^$' |
sort -u >"$tmp_dir/optimized_hosts"
msg "Excluding allowed hosts..."
sort "$dns_blackhole_dir/allowed_hosts" |
comm -23 "$tmp_dir/optimized_hosts" - >"$tmp_dir/blocked_hosts"
timestamp=$(date +%s)
msg "Installing enabled/disabled RPZ zone files..."
make_zone >"$tmp_dir/$disabled_rpz"
make_zone >"$tmp_dir/$enabled_rpz"
sed 's/.*/& CNAME ./' "$tmp_dir/blocked_hosts" >>"$tmp_dir/$enabled_rpz"
cp "$tmp_dir/$enabled_rpz" "$tmp_dir/$disabled_rpz" "$named_zone_files_dir/"
chmod 644 "$named_zone_files_dir/$enabled_rpz"
chmod 644 "$named_zone_files_dir/$disabled_rpz"
if [ ! -f "$named_includes_dir/$included_zone" ]; then
msg "Building included zone file..."
{
echo 'zone "rpz" {'
echo ' type master;'
echo ' file "'$switch_symlink'";'
echo '};'
} >"$named_includes_dir/$included_zone"
fi
if [ "$keep_temp" -eq 0 ]; then
msg "Cleaning up..."
rm "$tmp_dir"/* && rmdir "$tmp_dir"
fi
if [ "$(get_symlink_target)" = "" ]; then
# Create the symlink; defaulting to off
switch_blackhole "off"
else
show_status
fi
}
#
# Update RPZ zone serial
#
update_serial() {
timestamp=$(date +%s)
zone_file=$(get_symlink_target)
# Set sed in-place flag (GNU is -i alone; BSD is -i '')
in_place=$(sed --version >/dev/null 2>&1 && echo -i || echo "-i ''")
eval sed "$in_place" "'s/^\([[:space:]]*\)[0-9]\{1,\}\([[:space:]]*; Serial\)/\1'$timestamp'\2/'" "$named_zone_files_dir/$zone_file"
}
#
# Switch DNS blackhole state
#
switch_blackhole() {
state="$1"
tgt=$enabled_rpz
if [ ! -f "$named_zone_files_dir/$tgt" ]; then
error_exit "Not ready. Perform an update first."
fi
[ "$state" = off ] && tgt="$disabled_rpz"
[ "$(get_symlink_target)" = "$tgt" ] && {
msg "DNS blackhole already $state."
exit 0
}
ln -sf "$tgt" "$named_zone_files_dir/$switch_symlink"
msg "DNS blackhole switched $state."
}
#
# Show status
#
show_status() {
msg "DNS blackhole is $([ "$(get_symlink_target)" = "$enabled_rpz" ] && echo "on" || echo "off")."
}
#
# Main
#
set -eu
config_file="dns-blackhole.conf"
included_zone="dns-blackhole.zone"
switch_symlink="dns-blackhole.rpz"
enabled_rpz="dns-blackhole-enabled.rpz"
disabled_rpz="dns-blackhole-disabled.rpz"
# Change to the directory where this script resides (resolve symlinks if possible)
if command -v realpath >/dev/null 2>&1; then
SCRIPT_DIR=$(dirname "$(realpath "$0")")
elif readlink -f "$0" >/dev/null 2>&1; then
SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
else
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
fi
cd "$SCRIPT_DIR"
quiet=0
keep_temp=0
refresh="rndc reload rpz"
while getopts "c:kqr" opt; do
case "$opt" in
c) config_file=$OPTARG ;;
k) keep_temp=1 ;;
q) quiet=1 ;;
r) refresh="command_named restart" ;;
*)
usage_and_exit
;;
esac
done
shift $((OPTIND - 1))
# Validate operation argument
option="${1:-}"
case "$option" in
on | off | status | update) ;;
*)
usage_and_exit
;;
esac
[ -f "$config_file" ] || {
error_exit "Config file '$config_file' not found."
}
# shellcheck source=/dev/null
. "$config_file"
[ -n "$dns_blackhole_dir" ] || missing_config "dns_blackhole_dir"
[ -n "$named_includes_dir" ] || missing_config "named_includes_dir"
[ -n "$named_zone_files_dir" ] || missing_config "named_zone_files_dir"
[ -n "$tmp_dir" ] || missing_config "tmp_dir"
[ -n "$dns_server_hostname" ] || missing_config "dns_server_hostname"
[ -n "$fetch_timeout" ] || missing_config "fetch_timeout"
[ -n "$retry_seconds" ] || missing_config "retry_seconds"
[ -n "$max_attempts" ] || missing_config "max_attempts"
[ -n "$master_host_list_url" ] || missing_config "master_host_list_url"
for d in "$dns_blackhole_dir" "$named_includes_dir" "$named_zone_files_dir"; do
[ -d "$d" ] || error_exit "Directory '$d' missing."
done
case "$option" in
status)
show_status
command_named "status"
exit 0
;;
update)
do_update
;;
on | off)
switch_blackhole "$option"
update_serial
;;
esac
if [ $quiet -eq 1 ]; then
$refresh >/dev/null
else
$refresh
fi
Sample dns-blackhole.conf
Create this in your dns_blackhole_dir
directory.
#
# dns-blackhole.conf.dist
#
# Directory in which config and custom host files reside
dns_blackhole_dir="/usr/local/etc/dns-blackhole"
# Path to your BIND namedb directory where included files go
named_includes_dir="/usr/local/etc/namedb"
# Path to your BIND namedb directory where zone data files go
named_zone_files_dir="/usr/local/etc/namedb"
# Temporary directory in which to fetch and build zone files
tmp_dir="/var/tmp/dns-blackhole"
# The fully qualified hostname of your nameserver
dns_server_hostname="localhost"
# Seconds before fetch times out
fetch_timeout="15"
# Seconds to wait between fetch retries
retry_seconds="10"
# Maximum number of fetch attempts before giving up
max_attempts="3"
# Master host list URL
# See https://github.com/StevenBlack/hosts for full details
master_host_list_url="https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts"
allowed_hosts
#
# allowed_hosts.dist
#
# Add hostnames, one per line, to omit from the DNS blackhole.
blocked_hosts
#
# blocked_hosts.dist
#
# Add host entries, one per line, to add to the DNS blackhole.
# Format is similar to /etc/hosts with 0.0.0.0 addresses:
#
# 0.0.0.0 example.com
Sample Output
# ./dns-blackhole.sh update
Fetching master host list...
Optimizing ...
Excluding allowed hosts...
Installing enabled/disabled RPZ zone files...
Cleaning up...
DNS blackhole is on.
zone reload queued
# ./dns-blackhole.sh on
DNS blackhole switched on.
zone reload queued
# ./dns-blackhole.sh status
DNS blackhole is on.
named is running as pid 39227.
Sample Crontab Entry
#
#minute hour mday month wday command
#
15 4 * * * /usr/local/etc/dns-blackhole/dns-blackhole.sh -q update 2>&1 | mail -s "Update DNS blackhole zone" root
Testing
Simple test to see if it is working using one of the hostnames in the dns-blackhole-enabled.rpz
file.
# host 00fun.com
Host 00fun.com not found: 3(NXDOMAIN)
Yay! It’s blocked when using the local DNS resolver.
To test the opposite function (no blackhole DNS), you can switch the blackhole off and try the lookup again. Or keep it enabled but use an external nameserver like 1.1.1.1 or 8.8.8.8:
# host 00fun.com 1.1.1.1
Using domain server:
Name: 1.1.1.1
Address: 1.1.1.1#53
Aliases:
00fun.com has address 74.53.201.226
00fun.com mail is handled by 1 mx1.comspec.com.
This proves that it would otherwise resolve with the DNS blackhole disabled.
Forcing a Restart
Note that rndc reload rpz
(the default refresh command used after any changes) can sometimes delay the correct lookup results. If you want to ensure immediate results, include the -r
flag to restart named
completely.