Fix ctrld setup: ask subnet/domain/resolver, newbie-friendly OPNsense instructions
- setup_ctrld() now asks for each VLAN's actual subnet (was hardcoded
as 192.168.{id}.0/24, wrong for any non-default addressing)
- Asks for local domain suffix (default: lan) with explanation of what
it is and where to find it in OPNsense
- Asks for local resolver address (default: 127.0.0.1:5353) with a
full plain-English explanation of WHY Unbound must move off port 53,
why both .lan and .local rules are needed, and what breaks without them
- _ctrld_opnsense_setup() now walks through the Unbound port change
step-by-step with exact OPNsense UI paths, pausing for confirmation
at each stage before proceeding
- All three install paths (_local, _opnsense, _manual) now pass
local_domain and local_resolver through to _build_toml()
- Intro text rewritten for someone unfamiliar with DNS filtering,
VLANs, or Resolver IDs — explains what each thing is before asking
https://claude.ai/code/session_01JR2EMK7rwrZJowpstcaxQ6
This commit is contained in:
+246
-65
@@ -903,34 +903,47 @@ def setup_ctrld(vlans_config: list):
|
|||||||
step(21, "DNS Filtering — Control D (optional)")
|
step(21, "DNS Filtering — Control D (optional)")
|
||||||
sep()
|
sep()
|
||||||
print(f"""
|
print(f"""
|
||||||
{bold('Control D')} filters DNS per VLAN — ads, malware, adult content,
|
{bold('What is DNS filtering?')}
|
||||||
social media, and more. Each VLAN gets its own filtering profile.
|
Every time a device opens a website or app, it first asks a DNS server
|
||||||
|
"what is the address of google.com?" — before any data is sent.
|
||||||
|
DNS filtering intercepts that question and can block ads, malware, adult
|
||||||
|
content, social media, or anything else — invisibly, for every device
|
||||||
|
on your network, without installing anything on the devices themselves.
|
||||||
|
|
||||||
{cyan('ctrld')} is a local DNS proxy daemon that:
|
{bold('Control D')} is a DNS filtering service. Each of your VLANs gets
|
||||||
- Listens on port 53 for normal DNS from your devices
|
its own filtering profile — so your Staff VLAN can browse freely while
|
||||||
- Routes queries by source VLAN subnet to the right profile
|
your Guest VLAN blocks everything except basic browsing.
|
||||||
- Forwards upstream via encrypted DoH3 to Control D
|
|
||||||
- Runs as a system service, starts on boot
|
|
||||||
|
|
||||||
{bold('You need a Control D account:')} {dim('https://controld.com')}
|
{cyan('ctrld')} is a small program that runs on your router or this computer.
|
||||||
Create a Device (type: Router) per VLAN to get a Resolver ID.
|
It sits between your devices and the internet for DNS only:
|
||||||
|
Your device → ctrld (port 53) → Control D cloud → answer back
|
||||||
|
Nothing else about your traffic is touched.
|
||||||
|
|
||||||
{bold('Three deployment options:')}
|
{bold('Before you start — you need a free Control D account:')}
|
||||||
|
{cyan('https://controld.com')}
|
||||||
|
|
||||||
|
{dim('Once logged in: go to Endpoints → Add Device → type: Router')}
|
||||||
|
{dim('Create one Device per VLAN. Each gets a short Resolver ID like "p-abc123".')}
|
||||||
|
{dim('That ID is what this script asks for below.')}
|
||||||
|
|
||||||
|
{bold('Three ways to run ctrld:')}
|
||||||
|
|
||||||
{cyan('A — On this management computer')} {dim('(fully automated)')}
|
{cyan('A — On this management computer')} {dim('(fully automated)')}
|
||||||
ctrld runs here alongside the switch manager.
|
ctrld runs on the same computer as the switch manager.
|
||||||
Pros: self-contained, no other machine needed.
|
Good if you do not have OPNsense or just want to get started.
|
||||||
Cons: if this machine is down, DNS filtering stops
|
Downside: if this computer sleeps or shuts down, filtering stops
|
||||||
(devices still get IPs, just no filtering).
|
(your devices can still get on the internet, just unfiltered).
|
||||||
|
|
||||||
{cyan('B — On OPNsense')} {dim('(semi-automated — one SSH command)')}
|
{cyan('B — On OPNsense')} {dim('(recommended — one SSH command + two OPNsense clicks)')}
|
||||||
ctrld runs on your router.
|
ctrld runs on your router, which is always on.
|
||||||
Pros: DNS filtering survives management computer outage.
|
Filtering survives this computer being off or rebooted.
|
||||||
Cons: requires SSH access to OPNsense.
|
Requires: SSH access to OPNsense (one command to install).
|
||||||
|
Also requires moving OPNsense's built-in DNS off port 53 first
|
||||||
|
— this script explains exactly how to do that.
|
||||||
|
|
||||||
{cyan('C — Manual / skip')} {dim('(config generated, you install)')}
|
{cyan('C — Generate config only')} {dim('(copy-paste the file yourself)')}
|
||||||
Site generates ctrld.toml and install commands.
|
Produces a ready-to-use ctrld.toml you can nano/copy anywhere.
|
||||||
Install wherever you want, whenever you want.
|
Good if you want to review the file before anything is changed.
|
||||||
""")
|
""")
|
||||||
sep()
|
sep()
|
||||||
|
|
||||||
@@ -938,32 +951,105 @@ def setup_ctrld(vlans_config: list):
|
|||||||
info("Skipping — you can configure this in the DNS Filtering tab later")
|
info("Skipping — you can configure this in the DNS Filtering tab later")
|
||||||
return
|
return
|
||||||
|
|
||||||
choice = ask("Option (A/B/C)", "A").strip().upper()
|
choice = ask("Option (A/B/C)", "B").strip().upper()
|
||||||
|
|
||||||
if choice not in ("A","B","C"):
|
if choice not in ("A", "B", "C"):
|
||||||
warn("Invalid choice — skipping"); return
|
warn("Invalid choice — skipping"); return
|
||||||
|
|
||||||
# Collect Resolver IDs per VLAN
|
# ── Local domain name ────────────────────────────────────────────────────
|
||||||
sep()
|
sep()
|
||||||
print(f" {bold('Enter your Control D Resolver ID for each VLAN.')}")
|
print(f"""
|
||||||
print(f" {dim('Find it at: controld.com → Endpoints → your device → Resolver ID')}")
|
{bold('Local domain name')}
|
||||||
print(f" {dim('Press Enter to skip a VLAN (it will use the first configured profile as fallback).')}")
|
OPNsense gives every device on your network a name ending in your local
|
||||||
print()
|
domain — for example "johns-laptop.lan" or "printer.lan".
|
||||||
|
The default in OPNsense is {cyan('lan')} (so devices get names like device.lan).
|
||||||
|
|
||||||
|
{dim('You can check yours in OPNsense → System → General → Domain.')}
|
||||||
|
{dim('If you have not changed it, it is "lan". Just press Enter.')}
|
||||||
|
""")
|
||||||
|
local_domain = ask(" Your OPNsense local domain", "lan").strip().strip(".")
|
||||||
|
|
||||||
|
# ── Local resolver (Unbound on OPNsense or skip) ─────────────────────────
|
||||||
|
sep()
|
||||||
|
print(f"""
|
||||||
|
{bold('Local device name resolution (e.g. printer.lan, devlaptop.lan)')}
|
||||||
|
ctrld will handle ALL DNS queries on port 53 — but it does not know
|
||||||
|
about your local devices. OPNsense's built-in DNS (called Unbound)
|
||||||
|
tracks those: it watches DHCP leases and knows "johns-laptop = 192.168.10.5".
|
||||||
|
|
||||||
|
{bold('The problem:')} both Unbound and ctrld want to listen on port 53.
|
||||||
|
They cannot share it. One must move.
|
||||||
|
|
||||||
|
{bold('The solution:')} move Unbound to port 5353 (internal only), ctrld takes 53.
|
||||||
|
ctrld then forwards any query ending in .{cyan(local_domain)} or .local back to
|
||||||
|
Unbound at 127.0.0.1:5353. Everything works exactly as before, just routed.
|
||||||
|
|
||||||
|
{dim('Why .local too? Apple devices (iPhones, Macs, iPads) and many printers')}
|
||||||
|
{dim('use .local automatically via a protocol called mDNS/Bonjour.')}
|
||||||
|
{dim('Without this rule, your Mac would not find your printer by name.')}
|
||||||
|
|
||||||
|
{bold('If you are NOT using OPNsense')} or want to skip local name forwarding,
|
||||||
|
press Enter to leave this blank. Device names like printer.lan will not
|
||||||
|
resolve — you would use IP addresses instead.
|
||||||
|
""")
|
||||||
|
if choice == "B":
|
||||||
|
local_resolver = ask(
|
||||||
|
f" Unbound address after port move (OPNsense → 127.0.0.1:5353)",
|
||||||
|
"127.0.0.1:5353"
|
||||||
|
).strip()
|
||||||
|
else:
|
||||||
|
local_resolver = ask(
|
||||||
|
" Local DNS address for .lan/.local names (blank to skip)",
|
||||||
|
""
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
# ── Collect per-VLAN subnet + Resolver ID ────────────────────────────────
|
||||||
|
sep()
|
||||||
|
print(f"""
|
||||||
|
{bold('VLAN subnets and Control D Resolver IDs')}
|
||||||
|
|
||||||
|
For each VLAN you want filtered, you need two things:
|
||||||
|
|
||||||
|
{cyan('Subnet')} — the IP address range for that VLAN.
|
||||||
|
Example: if your Staff VLAN is 192.168.10.x, the subnet is 192.168.10.0/24.
|
||||||
|
The "/24" means "all addresses from .0 to .255 in that group".
|
||||||
|
ctrld uses this to know which VLAN a query is coming from.
|
||||||
|
|
||||||
|
{cyan('Resolver ID')} — the short code from Control D for that VLAN's profile.
|
||||||
|
Find it at: {dim('controld.com → Endpoints → your device → copy the Resolver ID')}
|
||||||
|
It looks like: {dim('p-abc123')} or {dim('abcd1234')}
|
||||||
|
|
||||||
|
{dim('Press Enter on Resolver ID to skip a VLAN (no filtering for that VLAN).')}
|
||||||
|
""")
|
||||||
|
|
||||||
profiles = []
|
profiles = []
|
||||||
for vlan in (vlans_config or [{"id":10,"name":"Staff"},{"id":20,"name":"Servers"},
|
for vlan in (vlans_config or [{"id": 10, "name": "Staff"},
|
||||||
{"id":30,"name":"IoT"},{"id":40,"name":"Guest"},
|
{"id": 20, "name": "Servers"},
|
||||||
{"id":50,"name":"Cameras"}]):
|
{"id": 30, "name": "IoT"},
|
||||||
rid = ask(f" VLAN {vlan['id']} ({vlan['name']}) Resolver ID", "").strip()
|
{"id": 40, "name": "Guest"},
|
||||||
|
{"id": 50, "name": "Cameras"}]):
|
||||||
|
vid = vlan["id"]
|
||||||
|
name = vlan["name"]
|
||||||
|
print(f"\n {bold(f'VLAN {vid} — {name}')}")
|
||||||
|
suggested_subnet = f"192.168.{vid}.0/24"
|
||||||
|
subnet = ask(
|
||||||
|
f" Subnet (the IP range for this VLAN)",
|
||||||
|
suggested_subnet
|
||||||
|
).strip()
|
||||||
|
rid = ask(
|
||||||
|
f" Control D Resolver ID (press Enter to skip filtering for this VLAN)",
|
||||||
|
""
|
||||||
|
).strip()
|
||||||
if rid:
|
if rid:
|
||||||
profiles.append({
|
profiles.append({
|
||||||
"vlan_id": vlan["id"],
|
"vlan_id": vid,
|
||||||
"name": vlan["name"],
|
"name": name,
|
||||||
"subnet": f"192.168.{vlan['id']}.0/24",
|
"subnet": subnet,
|
||||||
"resolver_id": rid,
|
"resolver_id": rid,
|
||||||
})
|
})
|
||||||
|
ok(f"VLAN {vid} ({name}) — {subnet} → ControlD profile {rid}")
|
||||||
else:
|
else:
|
||||||
info(f"Skipping VLAN {vlan['id']} — will use fallback")
|
info(f"VLAN {vid} ({name}) — skipped (no filtering)")
|
||||||
|
|
||||||
if not profiles:
|
if not profiles:
|
||||||
warn("No Resolver IDs entered — skipping DNS setup")
|
warn("No Resolver IDs entered — skipping DNS setup")
|
||||||
@@ -972,11 +1058,11 @@ def setup_ctrld(vlans_config: list):
|
|||||||
sep()
|
sep()
|
||||||
|
|
||||||
if choice == "A":
|
if choice == "A":
|
||||||
_ctrld_install_local_setup(profiles)
|
_ctrld_install_local_setup(profiles, local_domain, local_resolver)
|
||||||
elif choice == "B":
|
elif choice == "B":
|
||||||
_ctrld_opnsense_setup(profiles)
|
_ctrld_opnsense_setup(profiles, local_domain, local_resolver)
|
||||||
else:
|
else:
|
||||||
_ctrld_manual_setup(profiles)
|
_ctrld_manual_setup(profiles, local_domain, local_resolver)
|
||||||
|
|
||||||
|
|
||||||
def _build_toml(profiles: list, local_domain: str = "lan",
|
def _build_toml(profiles: list, local_domain: str = "lan",
|
||||||
@@ -1088,7 +1174,9 @@ def _build_toml(profiles: list, local_domain: str = "lan",
|
|||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
def _ctrld_install_local_setup(profiles: list):
|
def _ctrld_install_local_setup(profiles: list,
|
||||||
|
local_domain: str = "lan",
|
||||||
|
local_resolver: str = ""):
|
||||||
info("Installing ctrld on this machine...")
|
info("Installing ctrld on this machine...")
|
||||||
first_rid = profiles[0]["resolver_id"]
|
first_rid = profiles[0]["resolver_id"]
|
||||||
|
|
||||||
@@ -1107,7 +1195,7 @@ def _ctrld_install_local_setup(profiles: list):
|
|||||||
# Write multi-VLAN config
|
# Write multi-VLAN config
|
||||||
cfg_path = Path("/etc/controld/ctrld.toml")
|
cfg_path = Path("/etc/controld/ctrld.toml")
|
||||||
cfg_path.parent.mkdir(parents=True, exist_ok=True)
|
cfg_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
toml = _build_toml(profiles)
|
toml = _build_toml(profiles, local_domain=local_domain, local_resolver=local_resolver)
|
||||||
cfg_path.write_text(toml)
|
cfg_path.write_text(toml)
|
||||||
ok(f"Config written to {cfg_path}")
|
ok(f"Config written to {cfg_path}")
|
||||||
|
|
||||||
@@ -1127,67 +1215,160 @@ def _ctrld_install_local_setup(profiles: list):
|
|||||||
print(f"""
|
print(f"""
|
||||||
{bold('ctrld is running on this machine at:')} {cyan(mgmt_ip)}
|
{bold('ctrld is running on this machine at:')} {cyan(mgmt_ip)}
|
||||||
|
|
||||||
{bold('Next:')} In the switch manager DHCP tab, set DNS (option 6)
|
{bold('Next — tell your router to use this machine for DNS:')}
|
||||||
to {cyan(mgmt_ip)} for each VLAN pool. The switch will then hand
|
In OPNsense: Services → DHCPv4 → [each VLAN interface]
|
||||||
out this machine as the DNS server and ctrld will route each
|
Set {bold('DNS Server')} to {cyan(mgmt_ip)} and save.
|
||||||
VLAN to its Control D profile automatically.
|
|
||||||
|
Or in the switch manager DHCP tab, set DNS (option 6)
|
||||||
|
to {cyan(mgmt_ip)} for each VLAN pool.
|
||||||
|
|
||||||
|
Once done, every device that renews its DHCP lease will
|
||||||
|
start using ctrld automatically — no changes on the devices.
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
|
||||||
def _ctrld_opnsense_setup(profiles: list):
|
def _ctrld_opnsense_setup(profiles: list,
|
||||||
|
local_domain: str = "lan",
|
||||||
|
local_resolver: str = "127.0.0.1:5353"):
|
||||||
opnsense_ip = ask("OPNsense IP address", "192.168.99.1")
|
opnsense_ip = ask("OPNsense IP address", "192.168.99.1")
|
||||||
first_rid = profiles[0]["resolver_id"]
|
first_rid = profiles[0]["resolver_id"]
|
||||||
install_cmd = f'sh -c \'sh -c "$(curl -sL https://api.controld.com/dl)" -s {first_rid} forced\''
|
install_cmd = f'sh -c \'sh -c "$(curl -sL https://api.controld.com/dl)" -s {first_rid} forced\''
|
||||||
ssh_cmd = f"ssh root@{opnsense_ip} '{install_cmd}'"
|
ssh_cmd = f"ssh root@{opnsense_ip} '{install_cmd}'"
|
||||||
toml = _build_toml(profiles)
|
toml = _build_toml(profiles, local_domain=local_domain, local_resolver=local_resolver)
|
||||||
cfg_path = "/usr/local/etc/controld/ctrld.toml"
|
cfg_path = "/usr/local/etc/controld/ctrld.toml"
|
||||||
|
|
||||||
sep()
|
sep()
|
||||||
print(f"""
|
print(f"""
|
||||||
{bold('Step 1 — Run this command (installs ctrld on OPNsense):')}
|
{bold('━━━ Before you run ctrld — move Unbound off port 53 ━━━')}
|
||||||
|
|
||||||
|
OPNsense has a built-in DNS server called Unbound (currently on port 53).
|
||||||
|
ctrld also needs port 53. They cannot share it, so Unbound must move first.
|
||||||
|
|
||||||
|
{bold('In OPNsense web interface:')}
|
||||||
|
1. Go to {cyan('Services → Unbound DNS → General')}
|
||||||
|
2. Change {bold('Listen Port')} from {cyan('53')} to {cyan('5353')}
|
||||||
|
3. Change {bold('Network Interfaces')} to {cyan('Loopback')} only
|
||||||
|
{dim('(this means Unbound only answers from within OPNsense itself,')}
|
||||||
|
{dim(' not directly from your network devices — ctrld handles those)')}
|
||||||
|
4. Click {cyan('Apply')}
|
||||||
|
|
||||||
|
{dim('Why loopback only? After this change, ctrld is the one answering your')}
|
||||||
|
{dim('devices on port 53. Unbound only needs to answer ctrld, which runs on')}
|
||||||
|
{dim('the same machine. Loopback = same machine only.')}
|
||||||
|
|
||||||
|
{bold('Do this now, then press Enter to continue.')}
|
||||||
|
""")
|
||||||
|
input(" Press Enter when Unbound has been moved to port 5353... ")
|
||||||
|
|
||||||
|
sep()
|
||||||
|
print(f"""
|
||||||
|
{bold('━━━ Step 1 — Install ctrld on OPNsense (SSH command) ━━━')}
|
||||||
|
|
||||||
|
Open a terminal and run this command:
|
||||||
{cyan(ssh_cmd)}
|
{cyan(ssh_cmd)}
|
||||||
|
|
||||||
{bold('Step 2 — Replace the config file on OPNsense:')}
|
{dim('This connects to OPNsense over SSH and runs the official ctrld installer.')}
|
||||||
{dim(f'scp your-toml-file root@{opnsense_ip}:{cfg_path}')}
|
{dim('If you get a "permission denied" error, check that root SSH is enabled')}
|
||||||
{dim('then: ssh root@{opnsense_ip} "ctrld restart"')}
|
{dim('in OPNsense: System → Settings → Administration → Secure Shell.')}
|
||||||
|
|
||||||
{bold('Step 3 — In the switch manager DHCP tab:')}
|
{bold('Do this now, then press Enter to continue.')}
|
||||||
Set DNS (option 6) to {cyan(opnsense_ip)} for each VLAN pool.
|
|
||||||
""")
|
""")
|
||||||
|
input(" Press Enter when ctrld is installed on OPNsense... ")
|
||||||
|
|
||||||
|
# Write the toml locally so we can scp it
|
||||||
toml_file = HERE / "ctrld.toml"
|
toml_file = HERE / "ctrld.toml"
|
||||||
toml_file.write_text(toml)
|
toml_file.write_text(toml)
|
||||||
ok(f"ctrld.toml written to {toml_file}")
|
ok(f"ctrld.toml written to: {toml_file}")
|
||||||
|
|
||||||
if ask_yn("Copy command to clipboard?", False):
|
scp_cmd = f"scp {toml_file} root@{opnsense_ip}:{cfg_path}"
|
||||||
|
restart_cmd = f"ssh root@{opnsense_ip} 'ctrld restart'"
|
||||||
|
|
||||||
|
sep()
|
||||||
|
print(f"""
|
||||||
|
{bold('━━━ Step 2 — Copy the config to OPNsense ━━━')}
|
||||||
|
|
||||||
|
Run these two commands in order:
|
||||||
|
|
||||||
|
{cyan(scp_cmd)}
|
||||||
|
{cyan(restart_cmd)}
|
||||||
|
|
||||||
|
{dim('scp copies the generated ctrld.toml to OPNsense.')}
|
||||||
|
{dim('ctrld restart applies the new per-VLAN configuration.')}
|
||||||
|
|
||||||
|
{bold('Do this now, then press Enter to continue.')}
|
||||||
|
""")
|
||||||
|
input(" Press Enter when ctrld has been restarted on OPNsense... ")
|
||||||
|
|
||||||
|
sep()
|
||||||
|
print(f"""
|
||||||
|
{bold('━━━ Step 3 — Tell your network to use ctrld for DNS ━━━')}
|
||||||
|
|
||||||
|
In OPNsense: Services → DHCPv4 → [each VLAN interface]
|
||||||
|
Set {bold('DNS Server')} to {cyan(opnsense_ip)} and save.
|
||||||
|
(OPNsense's own IP — ctrld is now running there on port 53.)
|
||||||
|
|
||||||
|
{dim('When a device renews its DHCP lease (or you reconnect it),')}
|
||||||
|
{dim('it will receive OPNsense as its DNS server and queries will')}
|
||||||
|
{dim('flow through ctrld automatically.')}
|
||||||
|
|
||||||
|
{bold('━━━ How it all works now ━━━')}
|
||||||
|
|
||||||
|
Device asks DNS question
|
||||||
|
↓
|
||||||
|
ctrld on OPNsense (port 53)
|
||||||
|
├── *.{local_domain} or *.local → Unbound (port 5353, local names)
|
||||||
|
└── everything else (by VLAN subnet) → Control D cloud (filtered)
|
||||||
|
|
||||||
|
{bold('Verify it is working:')}
|
||||||
|
{cyan(f'ssh root@{opnsense_ip} "ctrld status"')}
|
||||||
|
""")
|
||||||
|
|
||||||
|
if ask_yn("Copy the SSH install command to clipboard?", False):
|
||||||
try:
|
try:
|
||||||
import subprocess
|
import subprocess
|
||||||
subprocess.run(["xclip","-selection","clipboard"],
|
subprocess.run(["xclip", "-selection", "clipboard"],
|
||||||
input=ssh_cmd.encode(), check=False)
|
input=ssh_cmd.encode(), check=False)
|
||||||
ok("Copied")
|
ok("Copied to clipboard")
|
||||||
except Exception:
|
except Exception:
|
||||||
info("xclip not available — copy manually from above")
|
info("xclip not available — copy the command manually from above")
|
||||||
|
|
||||||
|
|
||||||
def _ctrld_manual_setup(profiles: list):
|
def _ctrld_manual_setup(profiles: list,
|
||||||
toml = _build_toml(profiles)
|
local_domain: str = "lan",
|
||||||
|
local_resolver: str = ""):
|
||||||
|
toml = _build_toml(profiles, local_domain=local_domain, local_resolver=local_resolver)
|
||||||
toml_file = HERE / "ctrld.toml"
|
toml_file = HERE / "ctrld.toml"
|
||||||
toml_file.write_text(toml)
|
toml_file.write_text(toml)
|
||||||
first_rid = profiles[0]["resolver_id"]
|
first_rid = profiles[0]["resolver_id"]
|
||||||
|
|
||||||
sep()
|
sep()
|
||||||
_install_cmd = "sh -c 'sh -c \"$(curl -sL https://api.controld.com/dl)\" -s " + first_rid + " forced'"
|
_install_cmd = "sh -c 'sh -c \"$(curl -sL https://api.controld.com/dl)\" -s " + first_rid + " forced'"
|
||||||
|
unbound_note = ""
|
||||||
|
if local_resolver:
|
||||||
|
unbound_note = f"""
|
||||||
|
{bold('IMPORTANT — before installing on OPNsense:')}
|
||||||
|
Move Unbound off port 53 first, or ctrld will fail to start.
|
||||||
|
In OPNsense: Services → Unbound DNS → General
|
||||||
|
Change Listen Port to {cyan('5353')}, Network Interfaces to {cyan('Loopback')}, Apply.
|
||||||
|
"""
|
||||||
print(f"""
|
print(f"""
|
||||||
{bold('ctrld.toml written to:')} {cyan(str(toml_file))}
|
{bold('ctrld.toml written to:')} {cyan(str(toml_file))}
|
||||||
|
|
||||||
{bold('Install ctrld on any Linux/Mac/OPNsense machine:')}
|
Open it with: {cyan(f'cat {toml_file}')}
|
||||||
|
Or nano it: {cyan(f'nano {toml_file}')}
|
||||||
|
{unbound_note}
|
||||||
|
{bold('To install ctrld on any Linux / Mac / OPNsense machine:')}
|
||||||
{cyan(_install_cmd)}
|
{cyan(_install_cmd)}
|
||||||
|
|
||||||
{bold('Replace the default config with the generated ctrld.toml')}
|
{bold('Copy the config file to the machine where ctrld is installed:')}
|
||||||
{bold('then restart: ctrld restart')}
|
{dim('OPNsense path: /usr/local/etc/controld/ctrld.toml')}
|
||||||
|
{dim('Linux path: /etc/controld/ctrld.toml')}
|
||||||
|
|
||||||
{bold('Set switch DHCP option 6 to the IP where ctrld is running.')}
|
{bold('Then apply it:')} {cyan('ctrld restart')}
|
||||||
{bold('Configure this in the DNS Filtering tab once the site is running.')}
|
|
||||||
|
{bold('Finally — set DNS for each VLAN:')}
|
||||||
|
In OPNsense: Services → DHCPv4 → [each VLAN] → DNS Server = IP of ctrld machine.
|
||||||
|
Once devices renew their DHCP lease, they will use ctrld automatically.
|
||||||
|
|
||||||
Full docs: {dim('https://docs.controld.com/docs/ctrld')}
|
Full docs: {dim('https://docs.controld.com/docs/ctrld')}
|
||||||
""")
|
""")
|
||||||
|
|||||||
Reference in New Issue
Block a user