Skip to content

pfSense and OPNsense Operations Guide

Yegor S edited this page Dec 14, 2023 · 3 revisions

Overview

This article will discuss how ctrld can be operated on FreeBSD based router/firewall devices with support for advanced use cases.

Since pfSense and OPNsense are very similar, this single guide applies to both (for the most part).

Install

The simplest and quickest way to get ctrld on your router machine is to run the 1-liner install command.

sh -c 'sh -c "$(curl -sSL https://api.controld.com/dl)"'

Take this command and execute it in the shell. This will download a totally safe bash script, and execute it with system privilege.

Manual Install

If you feel antsy about blindly running random bash scripts off the Internet with system privilege (don't blame you), you can simply download the appropriate FreeBSD binary from the Releases section for your CPU architecture. Put it into a nice folder that's in your system path, like /usr/local/bin/ and make it executable.

Basic Operations

Now that you got the binary downloaded, you can run it with no args and check out the commands you can use to operate it.

[2.7.0-RELEASE][root@pfSense.home.arpa]/var: ctrld
        __         .__       .___
  _____/  |________|  |    __| _/
_/ ___\   __\_  __ \  |   / __ |
\  \___|  |  |  | \/  |__/ /_/ |
 \___  >__|  |__|  |____/\____ |
     \/ dns forwarding proxy  \/

Usage:
  ctrld [command]

Available Commands:
  run         Run the DNS proxy server
  service     Manage ctrld service
  start       Quick start service and configure DNS on interface
  stop        Quick stop service and remove DNS from interface
  restart     Restart the ctrld service
  reload      Reload the ctrld service
  status      Show status of the ctrld service
  uninstall   Stop and uninstall the ctrld service
  clients     Manage clients

Flags:
  -h, --help            help for ctrld
  -s, --silent          do not write any log output
  -v, --verbose count   verbose log output, "-v" basic logging, "-vv" debug level logging
      --version         version for ctrld

Use "ctrld [command] --help" for more information about a command.

I also strongly encourage you to RTFM which documents all the params you can use while crafting a custom config. More on this later.

Yolo

The simplest way to run ctrld is using this command: ctrld start --cd RESOLVER_ID_HERE while templating your Device's unique DNS resolver ID from the web control panel. When you run this command, the following things will happen:

  • Basic config file is fetched from the API and written to /etc/controld/ctrld.toml
  • unbound and dnsmasq processes are terminated as they already listen on port 53 !!!
  • ctrld starts listeners on TCP/UDP port 53 on all interfaces
  • /etc/resolv.conf file is updated to point to the listener
  • DNS is updated on the main interface to use ctrld
  • Init script is added to /usr/local/etc/rc.d so ctrld can auto-start on reboot

The state of the machine will be something like this:

[2.7.0-RELEASE][root@pfSense.home.arpa]/var: sockstat -l | grep ctrld
root     ctrld      85084 3   stream /etc/controld/ctrld_control.sock
root     ctrld      85084 8   udp4   *:5353                *:*
root     ctrld      85084 18  tcp46  *:53                  *:*
root     ctrld      85084 19  udp46  *:53                  *:*
root     ctrld      85084 21  udp6   *:5353                *:*
[2.7.0-RELEASE][root@pfSense.home.arpa]/var: cat /etc/resolv.conf
# resolv.conf(5) file generated by ctrld
# DO NOT EDIT THIS FILE BY HAND -- CHANGES WILL BE OVERWRITTEN

nameserver 127.0.0.1
[2.7.0-RELEASE][root@pfSense.home.arpa]/var: cat /etc/controld/ctrld.toml
# AUTO-GENERATED VIA CD FLAG - DO NOT MODIFY

[listener]
  [listener.0]
    ip = '0.0.0.0'
    port = 53

[network]
  [network.0]
    name = 'Network 0'
    cidrs = ['0.0.0.0/0']

[upstream]
  [upstream.0]
    type = 'doh'
    endpoint = 'https://dns.controld.com/abcd1234'
    timeout = 5000
[2.7.0-RELEASE][root@pfSense.home.arpa]/var: ctrld clients list
+-----------------------------------------+--------------------------------------+-------------------+------------+
|                   IP                    |               Hostname               |        Mac        | Discovered |
+-----------------------------------------+--------------------------------------+-------------------+------------+
| 0.0.0.0                                 | 64d4f0da-6012-4483-adff-d0d434ef6476 |                   | mdns       |
| 10.0.10.1                               |                                      | 00:50:56:9f:0e:84 | arp        |
| 10.0.10.209                             | pfSense                              | 00:0c:29:f5:a3:55 | arp,dhcp   |
| 10.0.10.222                             | test-virtual-machine                 | 00:0c:29:4a:5c:57 | arp,mdns   |
| 10.0.10.238                             | Office-Box                           | 74:56:3c:44:eb:5e | arp,mdns   |
| 10.0.10.245                             | Test-W11                             |                   | mdns       |
| 127.0.0.1                               | pfSense                              | 00:0c:29:f5:a3:55 | dhcp       |
| ::1                                     | pfSense                              | 00:0c:29:f5:a3:55 | dhcp       |
+-----------------------------------------+--------------------------------------+-------------------+------------+

If all you wanted was to receive DNS queries and send them all to a single Control D upstream using DNS-over-HTTPS, you have succeeded. If you didn't like the whole "kill unbound" thing as you use it for other purposes, then read on!

Custom Config Mode

In order to be able to modify the generated config file on disk, you need to de-couple it from the API if you used the --cd RESOLVER_ID_HERE flag (if not, then you're already in this mode and can skip doing this). To do this, execute the following commands:

  • ctrld stop - this will stop the service
  • ctrld start - this will start the service in "local config mode" which enforces the config on disk

Now you can make changes to it, and execute ctrld reload command to enforce your changes.

Advanced Operations

If you're running ctrld in local config mode, some "yolo behavior" will no longer occur. Namely, you are now responsible for listener conflicts. If unbound and/or dnsmasq are listening on port 53, ctrld will not be able to start and you will be greeted with an error:

[2.7.0-RELEASE][root@pfSense.home.arpa]/var: sockstat -l | grep \:53
nobody   dnsmasq      644 4   udp4   *:53                  *:*
nobody   dnsmasq      644 5   tcp4   *:53                  *:*
nobody   dnsmasq      644 6   udp6   *:53                  *:*
nobody   dnsmasq      644 7   tcp6   *:53                  *:*
unbound  unbound    98986 3   udp6   ::1:53                *:*
unbound  unbound    98986 4   tcp6   ::1:53                *:*
unbound  unbound    98986 5   udp4   127.0.0.1:53          *:*
unbound  unbound    98986 6   tcp4   127.0.0.1:53          *:*
[2.7.0-RELEASE][root@pfSense.home.arpa]/var: ctrld start
Dec  8 01:51:35.000 NTC Reading config: /etc/controld/ctrld.toml
Dec  8 01:51:35.000 NTC Starting service
Dec  8 01:51:35.000 ERR ctrld service may not have started due to an error or misconfiguration, service log:
Dec  8 01:51:35.000 ??? ================================
Dec  8 01:51:35.000 ??? Dec  8 01:51:35.000 FTL listener.0 failed to listen: listen udp 0.0.0.0:53: bind: address already in use
listen tcp 0.0.0.0:53: bind: address already in use
Dec  8 01:51:35.000 ??? ================================
Dec  8 01:51:35.000 NTC Service uninstalled

As you're probably aware, if you need to still run either service at the same time as ctrld, you need to put them on a different port (or run ctrld on a different port instead).

Local Domains

One of the most common use cases is delegating local DNS resolution to unbound for all your LAN-local domains and PTR records, while sending all external DNS queries to the Control D upstream.

Do nothing option

If you're using v1.3.2 (or newer) of ctrld you technically don't have to do anything, as ctrld (with client discovery enabled) will resolve any LAN-local A or PTR record for you, using the data it has in the client list.

[2.7.0-RELEASE][root@pfSense.home.arpa]/var: dig +short Test-W11
10.0.10.245
[2.7.0-RELEASE][root@pfSense.home.arpa]/var: dig +short -x 10.0.10.245
Test-W11.

Well, that's pretty neat. However you may not be as impressed as I am, and still want to leverage your trusty unbound instance, or any other LAN-local DNS resolver. That's fine, we can do that too.

Custom Upstreams

I'm going to assume you already have unbound or another DNS server running on a non-standard port, say 5555 which is capable of resolving local domains and serving PTR records for devices on a different vlan. Let's steer traffic to it using a custom config, which would look something like this:

[listener]
  [listener.0]
    ip = '0.0.0.0'
    port = 53

    [listener.0.policy]
      name = 'My Policy'
      networks = [
          {'network.0' = ['upstream.0']},
          {'network.1' = ['upstream.1']}
      ]

      rules = [
        { '*.cool.domain' = ['upstream.1']},
        { '*.in-addr.arpa' = ['upstream.1']}
      ]

      macs = [
       {"14:54:4a:8e:08:2d" = ["upstream.1"]}
      ]

[network]
  [network.0]
    name = 'Main Subnets'
    cidrs = ['10.0.0.0/24', '10.0.1.0/24']

  [network.1]
    name = 'Secret Subnet'
    cidrs = ['10.0.99.0/24']

[upstream]
  [upstream.0]
    name = 'My Fancy CD Resolver'
    type = 'doh'
    endpoint = 'https://dns.controld.com/abcd1234'
    timeout = 5000
  
  [upstream.1]
    name = 'Custom Resolver'
    type = 'legacy'
    endpoint = '10.0.0.1:5555'
    timeout = 1000

Not all params are needed, and shown for illustrative purposes only. It's not as scary as it looks. Let's go over it.

  1. In the [listener] block we define our.... listener with an IP + port.
  2. In the [listener.0.policy] block we define the policy of how DNS traffic should be routed, let's skip that over for a second.
  3. In the [network] block we define our subnets if you want to leverage source IP based routing. If you do not, don't define any.
  4. In the [upstream] block we define our DNS upstreams where DNS traffic should be sent. You should have at least one of these, but here we have 2.
  5. Coming back to the [listener.0.policy] block. It strings together the defined networks and upstreams, as well as new concepts like rules and macs and defines which upstream should be used if there is a match.
  6. The matching order is: rules => macs => networks

So for example:

  • A DNS query from 10.0.0.5 would be sent to upstream.0, while a query from 10.0.99.123 would be sent to upstream.1
  • A DNS query for my-host.cool.domain from any subnet would be sent to upstream.1 (since host rules match first)
  • A DNS query from a device with MAC address 14:54:4a:8e:08:2d from any subnet would be sent to upstream.1 (since MAC rules match 2nd).
  • All PTR queries would be sent to upstream.1

You can find more example configs for different use cases in the Wiki.

Don't touch my configs

If your existing router configs are dear to your heart and you don't want no stinkin' 3rd party processes messing with them, I get you, and there is a solution to that as well.

If you simply want ctrld to spawn a listener (or multiple listeners) in order to receive DNS queries and follow the config defined logic, while not making any changes to the system (modifying DNS on interface, editing resolv.conf, etc), then the service start sub-command is your friend.

In this mode you're likely running ctrld on a non-standard port, so modify your config and set it:

[listener]
  [listener.0]
    ip = '0.0.0.0'
    port = 42069
.....

Then execute the service start command (it's just like start but with service before it).

[2.7.0-RELEASE][root@pfSense.home.arpa]/var: ctrld service start
Dec 14 00:02:08.000 NTC Reading config: /etc/controld/ctrld.toml
Dec 14 00:02:08.000 NTC Starting service
Dec 14 00:02:13.000 NTC Service started

You can check the listeners, and notice that ctrld is listening on the config defined port, as well as the mDNS port for client discovery (you can shut this off if you don't like it by setting the discover_mdns config param to false):

[2.7.0-RELEASE][root@pfSense.home.arpa]/var: sockstat -l | grep ctrld
root     ctrld      36674 3   stream /etc/controld/ctrld_control.sock
root     ctrld      36674 8   udp4   *:5353                *:*
root     ctrld      36674 11  udp6   *:5353                *:*
root     ctrld      36674 19  tcp46  *:42069               *:*
root     ctrld      36674 20  udp46  *:42069               *:*

If you send DNS queries to this port, they will be subject to your config defined rules, and will be sent to Control D (if that's what you want).

[2.7.0-RELEASE][root@pfSense.home.arpa]/var: dig verify.controld.com +short @127.0.0.1 -p 42069
api.controld.com.
147.185.34.1

Protip: verify.controld.com will only resolve if you're using a Control D upstream, so that's a good domain to VERIFY that it's working.

Troubleshooting

If you're having trouble you can always contact us and we'll help you. But you can also wear your big boy pants and check out the troubleshooting guide and likely self-resolve the issue in no time.