Practical Port: DNS Ad Blocking Tutorial Based on Jail
-
• Original Link: https://freebsdfoundation.org/wp-content/uploads/2023/08/reuschling_practical_ports.pdf
-
• Author: BENEDICT REUSCHLING
-
• Translator: ykla & ChatGPT
One of the first things that attracted me to FreeBSD is its ability to easily run services. These services can be system services provided with the operating system (the simplest example might be the SSH daemon), or third-party software installed through pkg or ports. In either case, the process is the same: you add a line in /etc/rc.conf
to enable the service (which can be done with sysrc or service …… enable
), making it run at the next system boot. Next, there is usually a configuration file for custom settings to suit your needs. Typically, this requires entering the IP address or DNS hostname to listen on, network ports, and some specific details about the software. From that point, you can either start the service directly (using service …… start
) or have it start at the next reboot in case it requires a kernel module that kldload
cannot load (which is quite rare these days).
This process is straightforward, placing the configuration of all system services in a convenient location, and once you finish configuring one or two services, it is easy to replicate. Of course, running a whole set of services on a FreeBSD host system is also possible until things become more complicated. It is quite reasonable and not uncommon to run different versions of the same software in parallel. This is for testing purposes—checking if an upgrade works as expected, or whether certain software still requires an older version as a dependency. One example is trying to run different versions of the PostgreSQL database sequentially. In this case, both pkg and ports check the versions, and if they find binary files in the same location, they will cancel the installation and show a message indicating that certain binaries will be placed in the same location and therefore will overwrite each other. This is an undesirable situation where the user must choose one version, as they cannot coexist.
This situation arises unless virtualization or container technology is involved. This allows for process isolation in independent execution environments, using various methods to run multiple such systems on the same hardware. Virtualization adds an extra layer on top of the operating system, allowing the installation of the same or different operating systems and simulating hardware. Containers or jails achieve this by isolating processes using chroot(8). We will focus on the latter here because it is more lightweight in terms of resource usage and can start relatively quickly.
The benefits of this isolation are not only that various different versions can run in parallel but also for security reasons. When applications run in jail containers, by default, internal processes cannot access the host system. The application can discover all the usual devices (like networks), directory structures, and files in the correct locations, but in reality, it is an isolated environment that mimics the behavior and layout of the host system. When such a jail is compromised in some way, it is easy to stop it without affecting other jails or services in the host system. Access to them is strictly prohibited, keeping any intruders contained within a specific jail unit.
This also makes it easy to migrate systems to another host; simply stop, copy the jail’s directory structure to the new location, and restart there (for example, making some local modifications like a new IP address). Backups and restorations are also done in the same way. Typically, multiple such jails are managed by software that manages the jails, responsible for creating, modifying, and deleting jails.
One such jail manager is called Bastille, which is entirely written in shell scripts. In this article, we will delve deeper into the process of setting up the host system, creating a jail, and starting services within it based on templates. These templates allow for shared configurations in a central repository, so they can be applied without needing to understand the inner workings of the service. This way, even for those who want to quickly get something up and running, it is easy to set up for complex situations.
In this article, we will deploy a service called AdGuard, provided by AdGuard Software Limited. By running the AdGuard service on the network, clients that connect its DNS resolution to that service can filter out ads in web browsing activities. This helps avoid tracking by advertisers and the creation of user profiles, while also speeding up page load times as ads do not have to be transmitted alongside the content the user wants to view. AdGuard accomplishes this through filter lists and DNS sinkholing. Based on filter lists, AdGuard blocks known ad sites by sending invalid address responses before ads are rendered in the browser. There are multiple ways to use the AdGuard service— as a browser extension for personal devices, a desktop application, or running it as a recursive DNS resolver. Note that AdGuard cannot completely prevent all forms of ads (especially dynamically embedded ads in video sites), but it performs excellently in removing ads from web pages.
We start with a Raspberry Pi because this service has been running for a while, and we want low power consumption. I have a Raspberry Pi 3 here, but other devices that can run FreeBSD (including full servers) are also applicable. Install the operating system, apply the latest security patches, and lock down remote access using SSH keys.
Environment Setup
I connected an old 32GB solid-state drive to the Raspberry Pi, which performs most I/O intensive operations through a single-disk ZFS pool, rather than using a slower storage card. At the time of writing, I am running FreeBSD 13.2, and I am confident that future versions will perform equally well or only require minor adjustments.
# pkg install bastille git-lite
Since Bastille is a shell script, installation is quite quick and does not have additional dependencies. While it may not be as comprehensive in some functional aspects as other jail managers, it still works fine. To clone the AdGuard Home template (and other templates) from Bastille’s GitLab repository, Git needs to be installed. Next, we create a PF configuration for Bastille in /etc/pf.conf
, as follows:
ext_if="ue0" ## <- Change "ue0" to your machine's configuration.
set block-policy return
scrub in on $ext_if all fragment reassemble
set skip on lo
table <jails> persist
nat on $ext_if from <jails> to any -> ($ext_if:0)
rdr-anchor "rdr/*"
block in all
pass out quick keep state
pass in inet proto tcp from any to any port ssh flags S/SA keep state
pass in inet proto tcp from any to any port bootps flags S/SA keep state
pass in inet proto tcp from any to any port {9100,9124} flags S/SA modulate state
Make sure to change the top line, the “ext_if” line, to the interface you are using. On my Raspberry Pi, the Ethernet cable is connected to “ue0”, so I entered ue0. The pf.conf will create a table for our jail traffic (using NAT). Bastille supports multiple parameters for networking, making it flexible enough for both office and home networks, as well as networks provided by hosting services. These options are described in detail here: https://docs.bastillebsd.org/en/latest/chapters/networking.html.
I will be using a VNET-based jail because I have an available IP address on my local network. After editing the configuration file, we added an entry to start PF and pf log devices at boot, as well as other services. Bastille should also start, and we listed the name of the jail that will be created for AdGuard (my naming scheme is both legendary and boring).
# sysrc pf_enable=YES
# sysrc pflog_enable=YES
# sysrc bastille_enable=yes
# sysrc bastlle_list="adguard"
Before starting the firewall, it is a good practice to check for errors in the firewall ruleset. Use:
# pfctl -nvf /etc/pf.conf
for such checks. On success, it will echo the entire ruleset or display any errors you might encounter. Note that it cannot check for logical errors, such as blocking SSH port 22, which may be the only way to connect remotely. Fortunately, there is already a (default) rule that allows SSH traffic through. After checking is complete, start the PF service and begin filtering traffic:
# service pf start
# service pflog start
Expect your SSH connection to drop, so keep a separate terminal connection in case you cannot reconnect. After reconnecting, we need to edit a few more configuration files. Enabling VNET for the jail requires adding an entry in /etc/devfs.rules
(rather than .conf
), which may not exist on a newly installed system. Simply create that file and add the following rules:
[bastille_vnet=13]
add path ✔bpf*✔ unhide
This allows Bastille to see traffic on the VNET interface and connect the jail to the outside world. This may be an explanation for non-professionals about what is going on. Fortunately, we do not need to worry too much about it (maybe I need to delve into it in my next networking exam).
Another file we need access to is /etc/sysctl.conf
, where we need to add the following lines:
sysctl net.inet.ip.forwarding=1
sysctl net.link.bridge.pfil_bridge=0
sysctl net.link.bridge.pfil_onlyip=0
sysctl net.link.bridge.pfil_member=0
When Bastille runs, it dynamically creates a bridge between the external interface (ue0) of our Raspberry Pi and the jail’s network interface (vtnet). These two interfaces are connected by a virtual wire, one end connecting to the host’s interface and the other end to the jail, allowing traffic exchange.
To apply these changes to the running system without needing to restart, use the following command:
# sysctl -f /etc/sysctl.conf
After finishing the setup and rebooting the Raspberry Pi, I was very confused when I found that the jail could no longer access the network. After some thought, I learned the reason from the following exchange:
https://www.mail-archive.com/[email protected]/msg64577.html
This is in FreeBSD 13, and an additional line needs to be added in /boot/loader.conf
. This may drive you crazy, so before going mad, please add the following to ensure it works correctly on future reboots:
if_bridge_load="YES"
This has correctly loaded the bridge interface, which also caused sysctl to occur, allowing sysctl.conf to change them from the default value 1 to 0. Nevertheless, before we finish, we need to access one last file.
Bastille’s configuration file is located at /usr/local/etc/bastille/bastille.conf
. You can edit it directly (it has very detailed comments), or if you don’t mind entering a lot, you can use the sysrc command. Since I am running on a ZFS pool connected to my Raspberry Pi, I set bastille_zfs_enable
to specify the name of my pool.
# sysrc -f /usr/local/etc/bastille/bastille.conf bastille_zfs_enable=YES
# sysrc -f /usr/local/etc/bastille/bastille.conf bastille_zfs_zpool=rpi3
If your pool name is different from the one on the bastille_zfs_zpool
line, please change it to your pool name. I also changed one option, the parameter bastille_network_gateway=""
. I entered my default gateway address because I encountered some issues resolving jail names during subsequent processes. You may or may not need to set this option, but if you do encounter issues, revisit this option to see if it resolves the problem.
Bastille Bootstrapping
Now that all the settings are in place, it is time for Bastille to create the dataset structure on the pool we assigned to it. It will download a simple FreeBSD 13.2 RELEASE and update any patches in it later. Execute the following command until it completes:
# bastille bootstrap 13.2-RELEASE update
Bootstrapping FreeBSD distfiles...
/usr/local/bastille/cache/13.2-RELEASE/MANIFES 782 B 1670 kBps 00s
/usr/local/bastille/cache/13.2-RELEASE/base.tx 168 MB 6526 kBps 26s
Validated checksum for 13.2-RELEASE: base.txz
MANIFEST: 7d1b032a480647a73d6d7331139268a45e628c9f5ae52d22b110db65fdcb30ff
DOWNLOAD: 7d1b032a480647a73d6d7331139268a45e628c9f5ae52d22b110db65fdcb30ff
Extracting FreeBSD 13.2-RELEASE base.txz.
Bootstrap successful.
See ‘bastille —help’ for available commands.
src component not installed, skipped
Looking up update.FreeBSD.org mirrors... 2 mirrors found.
Fetching metadata signature for 13.2-RELEASE from update2.freebsd.org... done.
Fetching metadata index... done.
Inspecting system... done.
Preparing to download files... done.
The following files will be updated as part of updating to
13.2-RELEASE-p1:
/bin/freebsd-version
/usr/lib/libpam.a
/usr/lib/pam_krb5.so.6
/usr/share/locale/zh_CN.GB18030/LC_COLLATE
/usr/share/locale/zh_CN.GB18030/LC_CTYPE
/usr/share/man/man8/pam_krb5.8.gz
Installing updates...
Restarting sshd after upgrade
Performing sanity check on sshd configuration.
Stopping sshd.
Waiting for PIDS: 1063.
Performing sanity check on sshd configuration.
Starting sshd.
Scanning /usr/local/bastille/releases/13.2-RELEASE/usr/share/certs/blacklisted for certificates...
Scanning /usr/local/bastille/releases/13.2-RELEASE/usr/share/certs/trusted for certificates...
done.
After the bootstrapping operation, the following datasets were added to my pool:
# zfs list -r rpi3/bastille
NAME USED AVAIL REFER MOUNTPOINT
rpi3 621M 28.0G 24K /rpi3
rpi3/bastille 584M 28.0G 26K /usr/local/bastille
rpi3/bastille/backups 24K 28.0G 24K /usr/local/bastille/backups
rpi3/bastille/cache 169M 28.0G 24K /usr/local/bastille/cache
rpi3/bastille/cache/13.2-RELEASE 169M 28.0G 169M /usr/local/bastille/cache/13.2-RELEASE
rpi3/bastille/jails 24K 28.0G 24K /usr/local/bastille/jails
rpi3/bastille/logs 24K 28.0G 24K /var/log/bastille
rpi3/bastille/releases 414M 28.0G 24K /usr/local/bastille/releases
rpi3/bastille/releases/13.2-RELEASE 414M 28.0G 414M /usr/local/bastille/releases/13.2-RELEASE
rpi3/bastille/templates 24K 28.0G 24K /usr/local/bastille/templates
Let’s run another bootstrapping operation, this time to provide the AdGuard Home template.
# bastille bootstrap https://gitlab.com/bastillebsd-templates/adguardhome
warning: redirecting to https://gitlab.com/bastillebsd-templates/adguardhome.git/
Already up to date.
Detected Bastillefile hook.
[Bastillefile]:
PKG ca_root_nss adguardhome
CP usr /
SYSRC adguardhome_enable=YES
SERVICE adguardhome start
RDR tcp 80 80
RDR udp 53 53
Template ready to use.
This will complete quickly. Bastille has its own template language, which you can see in the uppercase commands (like PKG, CP, etc.). They have the same functional equivalence as their lowercase forms in the system. With these commands, services can be set up in the jail in the correct order. Most of them are self-explanatory. The last two RDR commands redirect network ports from the host system to the jail. All other ports remain protected by the firewall, so only port 80 is opened from the host to the jail (and vice versa), as well as DNS port 53. Check the line rdr-anchor "rdr/*"
in your /etc/pf.conf
. This is what makes it so flexible. There is no need to open ports for all jails, each jail can open the required ports while keeping other ports closed.
Now it is time to create and start our first Bastille jail. Since we are using VNET, we need to pass the -V
parameter in the bastille create command, along with the jail’s name, the version to run, followed by the IP address assigned to the jail on the local network and the host’s network interface for bridging. Combined, the command looks like this:
# bastille create -V adguard 13.2-RELEASE 192.168.2.55 ue0
Valid: (192.168.2.55).
Valid: (ue0).
Creating a thinjail...
[adguard]:
e0a_bastille0
e0b_bastille0
adguard: created
[adguard]:
Applying template: default/vnet...
[adguard]:
Applying template: default/base...
[adguard]:
[adguard]: 0
[adguard]:
syslogd_flags: -s -> -ss
[adguard]:
sendmail_enable: NO -> NO
[adguard]:
sendmail_submit_enable: YES -> NO
[adguard]:
sendmail_outbound_enable: YES -> NO
[adguard]:
sendmail_msp_queue_enable: YES -> NO
[adguard]:
cron_flags: -> -J 60
[adguard]:/etc/resolv.conf -> /usr/local/bastille/jails/adguard/root/etc/resolv.conf
Template applied: default/base
No value provided for arg: GATEWAY6
[adguard]:
ifconfig_e0b_bastille0_name: -> vnet0
[adguard]:
ifconfig_vnet0: -> inet 192.168.2.55
[adguard]:
defaultrouter: NO -> 192.168.2.1
[adguard]: 0
[adguard]:
[adguard]: 0
Template applied: default/vnet
[adguard]:
adguard: removed
no IP address found for -
[adguard]:
e0a_bastille0
e0b_bastille0
adguard: created
You can see the two ends of the virtual wire I mentioned earlier: e0a_bastile0 and e0b_bastile0 form the connection between the host system and the jail. Checking the ifconfig output on the host shows the new bridge created for traffic from the jail.
The settings applied during jail creation are quite standard, mainly disabling services we will not use. After the jail is created, there are also two datasets in my pool that store all the data for the jail:
# zfs list|grep adguard
rpi3/bastille/jails/adguard 2.36M 28.0G 26.5K /usr/local/bastille/jails/adguard
rpi3/bastille/jails/adguard/root 2.34M 28.0G 2.34M /usr/local/bastille/jails/adguard/
root
This constitutes the root filesystem of the jail and follows the layout of other jail managers. To copy files to or from the jail, simply use the prefix /usr/local/bastille/jails/adguard/root
to access the jail’s root directory.
The jls
command will list bastille jails and their settings:
# bastille list -a
JID State IP Address Published Ports Hostname Release Path
adguard Up 192.168.2.55 - adguard 13.2-RELEASE-p1 /usr/local/bastille/
jails/adguard/root
At this point, the jail is running. The only thing missing is the installation of AdGuard Home. Since we have already bootstrapped the template, we can apply it to the jail using the following command:
# bastille template adguard bastillebsd-templates/adguardhome
bastille template adguard bastillebsd-templates/adguardhome
[adguard]:
Applying template: bastillebsd-templates/adguardhome...
[adguard]:
Bootstrapping pkg from pkg+http://pkg.FreeBSD.org/FreeBSD:13:aarch64/quarterly, please wait...
Verifying signature with trusted certificate pkg.freebsd.org.2013102301... done
[adguard] Installing pkg-1.19.1_1...
[adguard] Extracting pkg-1.19.1_1: 100%
Updating FreeBSD repository catalogue...
[adguard] Fetching meta.conf: 100% 163 B 0.2kB/s 00:01
[adguard] Fetching packagesite.pkg: 100% 6 MiB 6.5MB/s 00:01
Processing entries: 100%
FreeBSD repository update completed. 31664 packages processed.
All repositories are up to date.
Updating database digests format: 100%
The following 2 package(s) will be affected (of 0 checked):
New packages to be INSTALLED:
adguardhome: 0.107.22_5
ca_root_nss: 3.89
Number of packages to be installed: 2
The process will require 41 MiB more space.
7 MiB to be downloaded.
[adguard] [1/2] Fetching adguardhome-0.107.22_5.pkg: 100% 6 MiB 6.7MB/s 00:01
[adguard] [2/2] Fetching ca_root_nss-3.89.pkg: 100% 266 KiB 272.1kB/s 00:01
Checking integrity... done (0 conflicting)
[adguard] [1/2] Installing ca_root_nss-3.89...
[adguard] [1/2] Extracting ca_root_nss-3.89: 100%
[adguard] [2/2] Installing adguardhome-0.107.22_5...
[adguard] [2/2] Extracting adguardhome-0.107.22_5: 100%
===== Message from ca_root_nss-3.89:
—
FreeBSD does not, and can not warrant that the certification authorities
whose certificates are included in this package have in any way been
audited for trustworthiness or RFC 3647 compliance.
Assessment and verification of trust is the complete responsibility of the system administrator.
This package installs symlinks to support root certificates discovery by
default for software that uses OpenSSL.
This enables SSL Certificate Verification by client software without manual
intervention.
If you prefer to do this manually, replace the following symlinks with
either an empty file or your site-local certificate bundle.
* /etc/ssl/cert.pem
* /usr/local/etc/ssl/cert.pem
* /usr/local/openssl/cert.pem
===== Message from adguardhome-0.107.22_5:
—
You installed AdGuardHome: Network-wide ads & trackers blocking DNS server.
In order to use it please start the service ‘adguardhome’ and
then access the URL http://0.0.0.0:3000/ in your favorite browser.
[adguard]:
/usr/local/bastille/templates/bastillebsd-templates/adguardhome/usr -> /usr/local/bastille/jails/
adguard/root/usr
/usr/local/bastille/templates/bastillebsd-templates/adguardhome/usr/local -> /usr/local/bastille/
jails/adguard/root/usr/local
/usr/local/bastille/templates/bastillebsd-templates/adguardhome/usr/local/bin -> /usr/local/bastille/jails/adguard/root/usr/local/bin
/usr/local/bastille/templates/bastillebsd-templates/adguardhome/usr/local/bin/AdGuardHome.yaml ->/usr/local/bastille/jails/adguard/root/usr/local/bin/AdGuardHome.yaml
[adguard]:
adguardhome_enable: -> YES
[adguard]:
moving old config /usr/local/bin/AdGuardHome.yaml to the new location /usr/local/etc/AdGuardHome.
yaml
Starting adguardhome.
stdin:2: syntax error
pfctl: Syntax error in config file: pf rules not loaded
tcp 80 80
stdin:2: syntax error
pfctl: Syntax error in config file: pf rules not loaded
udp 53 53
Template applied: bastillebsd-templates/adguardhome
It only needs to execute the instructions in the template within the jail. This is also a good test to see if the network is set up correctly. If it is not set up correctly, the jail will not be able to fetch packages from the repository. The final pfctl warning made me a bit worried, but despite these warnings, it worked fine.
Excited, I followed a message on the screen to open my browser and point it to the jail’s IP address. Sure enough, the AdGuard Home login interface appeared. But where are the credentials? I searched on the Bastille template website https://gitlab.com/bastillebsd-templates but found no valid information. A blog post from Bastille mentioned using AdGuard as the username, but the password was not applicable. So, I had to create my own credentials, which is actually more secure since the default password can be easily scanned by bad actors.
I opened a console in the jail with the following command:
# bastille console adguard
Inside the jail, I found that AdGuard placed its configuration file at /usr/local/etc/AdGuardHome.yaml
. Near the top, I found the following section:
users:
- name: adguard
password: some password not in clear text
After exiting again, I needed a way to create a BCrypt password. The htpasswd tool can do this, so I installed the apache24 web server that includes that tool:
# pkg install apache24
After running the “refresh” command, I could run the htpasswd tool. Looking at its man page, I had to construct a command line like this:
htpasswd -Bnb adguard BastilleBSD!
I used the -B
parameter to create a BCrypt password, followed by the user to whom this password applies (we already had this information in the configuration file, but perhaps you want other users or multiple users), and then the plaintext password. Yes, this is not a secure way since it would appear in your shell history. But did I ever state anywhere in this tutorial that it was ready for production? Quite the opposite, I did not. I diligently ran the htpasswd, which output the generated password on the command line, and I copied and pasted it into AdGuard’s configuration file.
Then I executed:
# service adguardhome restart
(Note that I was still in my jail) to restart the service and apply the new settings. The other settings in the file are documented in detail in the AdGuard Home Wiki: https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration Refreshing my web browser, I entered my new credentials and was redirected to the main AdGuard dashboard. Success!
At the top, there is a setup wizard showing how to use your new AdGuard service, whether for your router (to cover the entire network) or various devices, and it is described for both mobile and desktop operating systems. Awesome!
After doing this on my phone—for testing purposes—I browsed a bit, and I saw the statistics appear in the dashboard. This indicates that our setup is running, and we should rename the internet to SnooperNet. Almost every website tracks you or displays ads that you do not like to some extent. The Raspberry Pi is able to handle this load, and I fine-tuned the connection count in the AdGuardHome.yaml
ratelimit parameter.
You can find the logs AdGuard writes for the service in the jail’s /var/log/adguardhome.log
directory.
Conclusion
This is the content of this tutorial. I found AdGuard’s documentation to be very complete, and thanks to the work of the template creators, it is easy to get started. I have enjoyed leaving a smaller footprint on the internet and seeing fewer ads. The benefits of it being a DNS service are that any device on your network can use it: personal computers, laptops, smartphones, tablets, TVs, IoT devices, and even possibly the smart cat door at your neighbor’s house.
Bastille may require some initial configuration, but after that, creating jails is a simple process. You might find other services you want to run on Bastille templates: https://gitlab.com/bastillebsd-templates?
BENEDICT REUSCHLING is a documentation contributor to the FreeBSD project and a member of the documentation engineering team. In the past, he has served two terms as a member of the FreeBSD core team. He manages a large data cluster at the Darmstadt University of Applied Sciences in Germany. He also teaches an undergraduate course titled “Unix for Developers.” Benedict is also one of the hosts of the weekly bsdnow.tv podcast.