rss logo

Setting Up a Web Filtering Server with Unbound and RPZ on Linux

Unbound Logo

I was looking for a way to implement Web Filtering on GNU/Linux. I knew it was possible to use squidguard for this purpose, but it turned out to be rather complicated to set up and particularly difficult to deploy automatically on each workstation, especially when not managed by an Active Directory domain. That's when I came across the DynFi Open Source firewall: https://dynfi.com, which is capable of Web Filtering and uses RPZ: https://en.wikipedia.org/ to do so. So I started investigating this solution and found a way to implement Web Filtering with RPZ.

Network diagram

In this architecture, a Debian server acts as both DNS and web server. When a client requests a forbidden site (according to a pre-established block list), he is redirected to the web server, where a blocked web page message will be displayed in his browser.

  • Prerequisites:
    • Block port 53 (UDP and TCP) on your gateway to prevent workstations from making requests to external DNS servers.
Network diagram illustrating a blocked web request from a workstation to an Unbound DNS server

Debian Server

Debian Logo

As described above, we'll have two services running on our Debian server: a web server to display a simple text message informing users that they are attempting to connect to a forbidden web site, and a DNS service to provide clients with correct or modified DNS responses. For the web server, I will use micro-httpd, a lightweight HTTP server that meets our needs, and Unbound as the DNS server.

micro-httpd

To inform users that the requested page is blocked, we'll need a web server that will display a denied web page, informing them that the website they're trying to access is forbidden.

Installation

  • Install the micro-httpd package:
root@host:~# apt install micro-httpd

Configuration

  • micro-httpd configuration can be found inside the /lib/systemd/system/micro-httpd@.service file:
[Unit] Description=micro-httpd Documentation=man:micro-httpd(8) [Service] User=nobody Group=www-data ExecStart=-/usr/sbin/micro-httpd /var/www/html StandardInput=socket
  • And the /lib/systemd/system/micro-httpd.socket file:
[Unit] Description=micro-httpd Documentation=man:micro-httpd(8) [Socket] ListenStream=0.0.0.0:80 Accept=true [Install] WantedBy=sockets.target
  • Create a file /var/www/html/index.html and set the appropriate permissions:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Access Forbidden</title> </head> <body> <h1>Access Forbidden</h1> <p>Sorry, but you do not have permission to access this page.</p> </body> </html> root@host:~# chown -R www-data:www-data /var/www/html
  • If necessary, we can use the systemctl command to restart the micro-httpd service:
root@host:~# systemctl restart micro-httpd.socket

Open a web browser and go to http://192.168.0.200/ to check wether you can see the blocked web page.

Unbound

Installation

  • Install the unbound package:
root@host:~# apt install unbound

Configuration

  • Create a file /etc/unbound/unbound.conf.d/rpz.conf:
server: module-config: "respip validator iterator" # Load respip, validator, and iterator modules. Needed for rpz interface: 192.168.0.200 # Network interface used for DNS queries interface: 127.0.0.1 # Loopback interface used for local DNS queries do-ip4: yes # Enable IPv4 support do-ip6: no # Disable IPv6 support do-udp: yes # Enable UDP support for DNS queries do-tcp: yes # Enable TCP support for DNS queries do-daemonize: yes # Run Unbound as a daemon (in the background) access-control: 0.0.0.0/0 allow # Allow all IP addresses to make DNS queries local-zone: "std.priv." static # Define a local zone for DNS queries local-data: "denied.std.priv. IN A 192.168.0.200" # Define a local DNS entry for a specific domain local-data-ptr: "192.168.0.200 denied.std.priv." # Define a local DNS PTR entry for a specific IP address rpz: name: rpz.std.rocks # RPZ zone name zonefile: /etc/unbound/blacklist.zone # RPZ zone file used for DNS query filtering
  • Create a file /etc/unbound/blacklist.zone where, for testing purposes, we'll redirect all requests for orange.fr and google.fr to our blocked web page:
*.orange.fr IN A 192.168.0.200 *.google.fr IN A 192.168.0.200
  • Restart the unbound service for the changes to take effect:
root@host:~# systemctl restart unbound

Workstation

  • From your workstation, try opening www.google.fr. You should be redirected to the blocking page:
Screenshot of Firefox browser displaying 'Access Forbidden' page
  • When we run a DNS query, we see that www.google.fr and orange.fr are redirected to the address 192.168.0.200:
Screenshot of Windows Command Prompt (CMD) window with two 'nslookup' commands

Downloading and applying a Block List

Now that we've built our web filtering system, it's time to make it useful. To do this, we'll download a block list file and format it to work with our architecture. Many different lists are available on the Internet (search for RPZ block list). Let's try one of the lists available at: https://github.com/hagezi/dns-blocklists.

  • RPZ lists are available in this format:
website.to.block CNAME .
  • However, to be usable in our configuration, we need to format the lines in this way:
website.to.block IN A 192.168.0.200

So, how do we translate the default format into a format that is useful in our architecture, as shown above? There are various approaches, but personally, I'll be using the sed editor. Let's see how it's done!

  • First, download one of the RPZ lists:
root@host:~# wget https://raw.githubusercontent.com/hagezi/dns-blocklists/main/rpz/multi.txt
  • Then format it using sed:
root@host:~# sed -i 's/CNAME.*/IN A 192.168.0.200/' multi.txt

Note: To prevent users from bypassing the policy, I recommend adding a DoH/VPN/TOR/Proxy list and blocking DoH IP addresses on your firewall. (Check this list: https://github.com/crypt0rr/public-doh-servers).

Go Further

It can be useful to use access control to apply different filters to the network or hosts. Let's see how it works.

  • Edit the configuration file /etc/unbound/unbound.conf.d/rpz.conf:
server: module-config: "respip validator iterator" # Load respip, validator, and iterator modules interface: 192.168.0.200 # Network interface used for DNS queries interface: 127.0.0.1 # Loopback interface used for local DNS queries do-ip4: yes # Enable IPv4 support do-ip6: no # Disable IPv6 support do-udp: yes # Enable UDP support for DNS queries do-tcp: yes # Enable TCP support for DNS queries do-daemonize: yes # Run Unbound as a daemon (in the background) access-control: 0.0.0.0/0 allow # Allow all IP addresses to make DNS queries local-zone: "std.priv." static # Define a local zone for DNS queries local-data: "denied.std.priv. IN A 192.168.0.200" # Define a local DNS entry for a specific domain local-data-ptr: "192.168.0.200 denied.std.priv." # Define a local DNS PTR entry for a specific IP address define-tag: "social adult dnsbypass" access-control-tag: 192.168.10.0/24 "social adult dnsbypass" access-control-tag: 192.168.10.200/32 "social adult" access-control-tag: 192.168.20.0/24 "adult dnsbypass" rpz: name: rpz.social.std.rocks # RPZ zone name zonefile: /var/lib/unbound/social_networks/blacklist.zone tags: "social" rpz: name: rpz.adult.std.rocks # RPZ zone name zonefile: /var/lib/unbound/adult/blacklist.zone tags: "adult" rpz: name: rpz.dnsbypass.std.rocks # RPZ zone name zonefile: /var/lib/unbound/dns_bypass/blacklist.zone tags: "dnsbypass"

As we can see, we've created three different web filtering lists: social, adult, and dnsbypass. We applied social adult dnsbypass to network 192.168.10.0/24, social adult to host 192.168.10.200, and adult dnsbypass to network 192.168.20.0/24.

Troubleshooting

Using lists with more than hundreds of thousands of entries can cause an error when starting unbound. In general, this is because the service takes too long to start and is automatically stopped by the system. Here's how to change the time allowed for starting the unbound service.

  • Error message when starting unbound:
root@host:~# systemctl restart unbound Job for unbound.service failed because a timeout was exceeded. See "systemctl status unbound.service" and "journalctl -xeu unbound.service" for details.
  • (Optionnal) Set your default editor, I personally use vim:
root@host:~# export EDITOR=vim
  • Modify systemctl parameters for unbound service:
root@host:~# systemctl edit unbound.service
  • Add these three lines to increase the time the system allows the unbound service to start:
### Editing /etc/systemd/system/unbound.service.d/override.conf ### Anything between here and the comment below will become the new contents of the file [Service] TimeoutStartSec=300 TimeoutStopSec=300 ### Lines below this comment will be discarded ### /lib/systemd/system/unbound.service # [Unit] # Description=Unbound DNS server # Documentation=man:unbound(8) # After=network.target # Before=nss-lookup.target # Wants=nss-lookup.target # # [Service] # Type=notify # Restart=on-failure # EnvironmentFile=-/etc/default/unbound # ExecStartPre=-/usr/libexec/unbound-helper chroot_setup # ExecStartPre=-/usr/libexec/unbound-helper root_trust_anchor_update # ExecStart=/usr/sbin/unbound -d -p $DAEMON_OPTS # ExecStopPost=-/usr/libexec/unbound-helper chroot_teardown # ExecReload=+/bin/kill -HUP $MAINPID # # [Install] # WantedBy=multi-user.target
  • The unbound service should now be able to start without error:
root@host:~# systemctl restart unbound
Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Contact :

contact mail address