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 unboundanddnsmasqprocesses are terminated as they already listen on port 53 !!!ctrldstarts listeners on TCP/UDP port 53 on all interfaces/etc/resolv.conffile 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.dsoctrldcan 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 servicectrld 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.
- In the
[listener]block we define our.... listener with an IP + port. - 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. - 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. - 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. - Coming back to the
[listener.0.policy]block. It strings together the definednetworksandupstreams, as well as new concepts likerulesandmacsand defines which upstream should be used if there is a match. - The matching order is: rules => macs => networks
So for example:
- A DNS query from
10.0.0.5would be sent toupstream.0, while a query from10.0.99.123would be sent toupstream.1 - A DNS query for
my-host.cool.domainfrom any subnet would be sent toupstream.1(since host rules match first) - A DNS query from a device with MAC address
14:54:4a:8e:08:2dfrom any subnet would be sent toupstream.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.