processCDFlags retries the resolver-config fetch indefinitely by design: a device that has no working network at boot must eventually come up. The loop had no cancellation, so a stop request arriving while the API is unreachable was ignored - the process kept retrying long after the service reported itself stopped, doing work on behalf of a service the OS considers stopped. Thread a context through processCDFlags and derive it from p.stopCh, in both the startup preflight and the config-reload path. The loop now returns as soon as the context is cancelled, checked both before a retry and after backoff returns (backoff can wake up on cancellation). A stop during preflight exits the way a normal stop does, without Fatal, so the service manager does not treat it as a failed start and apply its restart policy to a service the operator just asked to stop. Bind the two API requests themselves as well, so a stop does not have to wait out an in-flight request. Without this the loop honours a stop only between attempts, which leaves up to defaultTimeout (20s) of a request the service is no longer interested in - the same "still working after Service stopped" the loop change exists to end, one layer down. Doing so means a context parameter on FetchResolverConfig, FetchResolverUID, UpdateCustomLastFailed and SendLogs, since all four reach a request builder. The callers that have no context pass context.Background(), which is what master effectively does at those sites: its loggerCtx carries a logger, not cancellation. doWithFallback needs no parameter, because it clones the request with req.Context() and so inherits the binding. This also repairs internal/controld/controld_test.go, which is behind //go:build controld and had already been written against the context-taking signature, so it could not compile. Cover the cancellation paths; removing either check makes the tests hang until timeout. Sampling the stop state is the whole point of runAPIPreflight rather than doing this inline. A stop and a failure need opposite handling - one exits quietly, the other self-uninstalls a deleted device, surfaces the error to a mobile app, and reports a failed start - so the two must not be confused. Reading it from the context after cancelling would report "stopped" for every failure, since CancelFunc sets ctx.Err() regardless of whether anyone asked to stop; the stop channel is read directly instead, which also does not depend on the context watcher goroutine having been scheduled.
ctrld
A highly configurable DNS forwarding proxy with support for:
- Multiple listeners for incoming queries
- Multiple upstreams with fallbacks
- Multiple network policy driven DNS query steering (via network cidr, MAC address or FQDN)
- Policy driven domain based "split horizon" DNS with wildcard support
- Integrations with common router vendors and firmware
- LAN client discovery via DHCP, mDNS, ARP, NDP, hosts file parsing
- Prometheus metrics exporter
TLDR
Proxy legacy DNS traffic to secure DNS upstreams in highly configurable ways.
All DNS protocols are supported, including:
UDP 53DNS-over-HTTPSDNS-over-TLSDNS-over-HTTP/3(DOH3)DNS-over-QUIC
Use Cases
- Use secure DNS protocols on networks and devices that don't natively support them (legacy routers, legacy OSes, TVs, smart toasters).
- Create source IP based DNS routing policies with variable secure DNS upstreams. Subnet 1 (admin) uses upstream resolver A, while Subnet 2 (employee) uses upstream resolver B.
- Create destination IP based DNS routing policies with variable secure DNS upstreams. Listener 1 uses upstream resolver C, while Listener 2 uses upstream resolver D.
- Create domain level "split horizon" DNS routing policies to send internal domains (*.company.int) to a local DNS server, while everything else goes to another upstream.
- Deploy on a router and create LAN client specific DNS routing policies from a web GUI (When using ControlD.com).
OS Support
- Windows (386, amd64, arm)
- Windows Server (386, amd64)
- MacOS (amd64, arm64)
- Linux (386, amd64, arm, mips)
- FreeBSD (386, amd64, arm)
- Common routers (See below)
Supported Routers
You can run ctrld on any supported router. The list of supported routers and firmware includes:
- Asus Merlin
- DD-WRT
- Firewalla
- FreshTomato
- GL.iNet
- OpenWRT
- pfSense / OPNsense
- Synology
- Ubiquiti (UniFi, EdgeOS)
ctrld will attempt to interface with dnsmasq (or Windows Server) whenever possible and set itself as the upstream, while running on port 5354. On FreeBSD based OSes, ctrld will terminate dnsmasq and unbound in order to be able to listen on port 53 directly.
Install
There are several ways to download and install ctrld.
Quick Install
The simplest way to download and install ctrld is to use the following installer command on any UNIX-like platform:
sh -c 'sh -c "$(curl -sL https://api.controld.com/dl)"'
Windows user and prefer Powershell (who doesn't)? No problem, execute this command instead in administrative PowerShell:
(Invoke-WebRequest -Uri 'https://api.controld.com/dl/ps1' -UseBasicParsing).Content | Set-Content "$env:TEMPctrld_install.ps1"; Invoke-Expression "& '$env:TEMPctrld_install.ps1'"
Or you can pull and run a Docker container from Docker Hub
docker run -d --name=ctrld -p 127.0.0.1:53:53/tcp -p 127.0.0.1:53:53/udp controldns/ctrld:latest
Download Manually
Alternatively, if you know what you're doing you can download pre-compiled binaries from the Releases section for the appropriate platform.
Build
Lastly, you can build ctrld from source which requires go1.21+:
go build ./cmd/ctrld
or
go install github.com/Control-D-Inc/ctrld/cmd/ctrld@latest
or
docker build -t controldns/ctrld . -f docker/Dockerfile
Usage
The cli is self documenting, so feel free to run --help on any sub-command to get specific usages.
Arguments
__ .__ .___
_____/ |________| | __| _/
_/ ___\ __\_ __ \ | / __ |
\ \___| | | | \/ |__/ /_/ |
\___ >__| |__| |____/\____ |
\/ dns forwarding proxy \/
Usage:
ctrld [command]
Available Commands:
run Run the DNS proxy server
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
service Manage ctrld service
clients Manage clients
upgrade Upgrading ctrld to latest version
log Manage runtime debug logs
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.
Basic Run Mode
This is the most basic way to run ctrld, in foreground mode. Unless you already have a config file, a default one will be generated.
Command
Windows (Admin Shell)
ctrld.exe run
Linux or Macos
sudo ctrld run
You can then run a test query using a DNS client, for example, dig:
$ dig verify.controld.com @127.0.0.1 +short
api.controld.com.
147.185.34.1
If verify.controld.com resolves, you're successfully using the default Control D upstream. From here, you can start editing the config file that was generated. To enforce a new config, restart the server.
Service Mode
This mode will run the application as a background system service on any Windows, MacOS, Linux, FreeBSD distribution or supported router. This will create a generic ctrld.toml file in the C:\ControlD directory (on Windows) or /etc/controld/ (almost everywhere else), start the system service, and configure the listener on all physical network interface. Service will start on OS boot.
When Control D upstreams are used on a router type device, ctrld will relay your network topology to Control D (LAN IPs, MAC addresses, and hostnames), and you will be able to see your LAN devices in the web panel, view analytics and apply unique profiles to them.
Command
Windows (Admin Shell)
ctrld.exe start
Linux or Macos
sudo ctrld start
If ctrld is not in your system path (you installed it manually), you will need to run the above commands from the directory where you installed ctrld.
In order to stop the service, and restore your DNS to original state, simply run ctrld stop. If you wish to stop and uninstall the service permanently, run ctrld uninstall.
Unmanaged Service Mode
This mode functions similarly to the "Service Mode" above except it will simply start a system service and the config defined listeners, but will not make any changes to any network interfaces. You can then set the ctrld listener(s) IP on the desired network interfaces manually.
Command
Windows (Admin Shell)
ctrld.exe service start
Linux or Macos
sudo ctrld service start
Configuration
ctrld can be configured in variety of different ways, which include: API, local config file or via cli launch args.
API Based Auto Configuration
Application can be started with a specific Control D resolver config, instead of the default one. Simply supply your Resolver ID with a --cd flag, when using the start (service) mode. In this mode, the application will automatically choose a non-conflicting IP and/or port and configure itself as the upstream to whatever process is running on port 53 (like dnsmasq or Windows DNS Server). This mode is used when the 1 liner installer command from the Control D onboarding guide is executed.
The following command will use your own personal Control D Device resolver, and start the application in service mode. Your resolver ID is displayed on the "Show Resolvers" screen for the relevant Control D Endpoint.
Windows (Admin Shell)
ctrld.exe start --cd abcd1234
Linux or Macos
sudo ctrld start --cd abcd1234
Once you run the above command, the following things will happen:
- You resolver configuration will be fetched from the API, and config file templated with the resolver data
- Application will start as a service, and keep running (even after reboot) until you run the
stoporuninstallsub-commands - All physical network interface will be updated to use the listener started by the service or dnsmasq upstream will be switched to
ctrld - All DNS queries will be sent to the listener
Manual Configuration
ctrld is entirely config driven and can be configured in many different ways, please see Configuration Docs.
Example
[listener]
[listener.0]
ip = '0.0.0.0'
port = 53
[network]
[network.0]
cidrs = ["0.0.0.0/0"]
name = "Network 0"
[upstream]
[upstream.0]
bootstrap_ip = "76.76.2.11"
endpoint = "https://freedns.controld.com/p1"
name = "Control D - Anti-Malware"
timeout = 5000
type = "doh"
The above basic config will:
- Start listener on 0.0.0.0:53
- Accept queries from any source address
- Send all queries to
https://freedns.controld.com/p1using DoH protocol
CLI Args
If you're unable to use a config file, ctrld can be be supplied with basic configuration via launch arguments, in Ephemeral Mode.
Example
ctrld run --listen=127.0.0.1:53 --primary_upstream=https://freedns.controld.com/p2 --secondary_upstream=10.0.10.1:53 --domains=*.company.int,very-secure.local --log /path/to/log.log
The above will start a foreground process and:
- Listen on
127.0.0.1:53for DNS queries - Forward all queries to
https://freedns.controld.com/p2using DoH protocol, while... - Excluding
*.company.intandvery-secure.localmatching queries, that are forwarded to10.0.10.1:53 - Write a debug log to
/path/to/log.log
DNS Intercept Mode
When running ctrld alongside VPN software, DNS conflicts can cause intermittent failures, bypassed filtering, or configuration loops. DNS Intercept Mode prevents these issues by transparently capturing all DNS traffic on the system and routing it through ctrld, without modifying network adapter DNS settings.
When to Use
Enable DNS Intercept Mode if you:
- Use corporate VPN software (F5, Cisco AnyConnect, Palo Alto GlobalProtect, Zscaler)
- Run overlay networks like Tailscale or WireGuard
- Experience random DNS failures when VPN connects/disconnects
- See gaps in your Control D analytics when VPN is active
- Have endpoint security software that also manages DNS
Command
Windows (Admin Shell)
ctrld.exe start --intercept-mode dns --cd RESOLVER_ID_HERE
macOS
sudo ctrld start --intercept-mode dns --cd RESOLVER_ID_HERE
--intercept-mode dns automatically detects VPN internal domains and routes them to the VPN's DNS server, while Control D handles everything else.
To disable intercept mode on a service that already has it enabled:
Windows (Admin Shell)
ctrld.exe start --intercept-mode off
macOS
sudo ctrld start --intercept-mode off
This removes the intercept rules and reverts to standard interface-based DNS configuration.
Platform Support
| Platform | Supported | Mechanism |
|---|---|---|
| Windows | ✅ | NRPT (Name Resolution Policy Table) |
| macOS | ✅ | pf (packet filter) redirect |
| Linux | ❌ | Not currently supported |
Features
- VPN split routing — VPN-specific domains are automatically detected and forwarded to the VPN's DNS server
- Captive portal recovery — Wi-Fi login pages (hotels, airports, coffee shops) work automatically
- No network adapter changes — DNS settings stay untouched, eliminating conflicts entirely
- Automatic port 53 conflict resolution — if another process (e.g.,
mDNSResponderon macOS) is already using port 53,ctrldautomatically listens on a different port. OS-level packet interception redirects all DNS traffic toctrldtransparently, so no manual configuration is needed. This only applies to intercept mode.
Tested VPN Software
- F5 BIG-IP APM
- Cisco AnyConnect
- Palo Alto GlobalProtect
- Tailscale (including Exit Nodes)
- Windscribe
- WireGuard
For more details, see the DNS Intercept Mode documentation.
