Commit Graph
24 Commits
Author SHA1 Message Date
Codescribe 0f310594b0 Redact provision token in logs 2026-08-24 15:14:59 +07:00
Codescribe 8330049b66 feat: add firewall mode DNS-resolved IP allowlist 2026-07-08 15:31:54 +07:00
Codescribe a4bc23d17e fix: allow intercept fallback for default listener 2026-06-22 17:00:57 +07:00
Codescribe 7a8450cc40 fix: flush pf states after forced DNS intercept reload 2026-06-22 17:00:47 +07:00
Codescribe 6c5489873b fix(windows): include domainless VPN DNS adapters in discovery 2026-05-29 13:38:52 +07:00
Codescribe 3c740b9693 fix: port Windows VPN DNS settling fix to master 2026-05-29 13:38:27 +07:00
Codescribe 1735d3d55b cmd/cli: skip upstream.os healthcheck when WFP loopback protect enabled
When WFP loopback protect is active, the upstream.os healthcheck will
always fail because an external WFP block filter is interfering with
plain DNS. This demotes those expected failures to debug level and
returns errOsHealthcheckSuppressed so the recovery loop treats them
as non-fatal, eliminating the log spam described in #526.
2026-05-07 19:37:42 +07:00
Codescribe 81aa6b237b dns_intercept: add WFP loopback protect for VPN block-outside-dns
When third-party VPN software (e.g., OpenVPN) installs WFP block filters via
block-outside-dns, all DNS traffic to non-tunnel interfaces is blocked —
including DNS to 127.0.0.1 (ctrld's NRPT target). This breaks DNS mode
interception because the NRPT catch-all rule routes queries to loopback,
but WFP blocks the connection before it reaches ctrld's listener.

Fix: after exhausting all NRPT recovery attempts, activate a minimal WFP
session with "hard permit" filters (FWPM_FILTER_FLAG_CLEAR_ACTION_RIGHT)
for DNS to localhost in a max-priority sublayer (weight 0xFFFF). This
overrides the VPN's block for loopback DNS only, while preserving the
VPN's DNS leak protection for all other (non-loopback) DNS traffic.

The loopback protect is:
- Only activated when NRPT probes fail (not preemptively)
- Harmless when no conflicting WFP blocks exist (permit-only, no blocks)
- Persistent until ctrld shutdown (survives VPN reconnect cycles)
- Cleaned up by the existing cleanupWFPFilters path on shutdown
2026-04-30 19:30:43 +07:00
Codescribe 8abeeea4c3 log: persist internal runtime logs to disk
Add file-backed persistence to the internal logWriter so runtime logs
survive service restarts. When internal logging is enabled (CD mode,
no explicit log_path), writes are teed to both the existing in-memory
ring buffer and a rotated file on disk (ctrld.log in the home directory).

File rotation: 5MB max with 1 backup (ctrld.log.1), so max ~10MB on disk.
Log view/send now reads from the persisted files (including backup) to
provide complete history across restarts. Live tail continues to use
the in-memory subscriber mechanism unchanged.

Activation: same conditions as existing internal logging — CD mode only,
no log_path configured. No new config options or dependencies.
2026-04-30 19:19:19 +07:00
Codescribe b3c670b17e dns: fix recovery race condition during rapid network transitions
When multiple network changes fire in quick succession (e.g., VPN
disconnect + interface swap), the second handleRecovery() call cancels
the first but inherits stale DoH transports, causing DNS blackouts
of up to 30 seconds.

Three changes to reduce worst-case recovery from ~30s to <3s:

1. ForceReBootstrap() on recovery entry — closes dead connections and
   creates fresh transports synchronously before probing, replacing the
   lazy ReBootstrap() flag that left stale connections for probes to hit.

2. Debounce handleRecovery() for network changes (500ms window) — only
   the recovery flow is debounced; all other state updates (IP, pf
   anchor, VPN DNS, tunnel checks) still run immediately on every event.
   This eliminates the cancel-and-restart race without missing state.

3. Combined effect: ForceReBootstrap closes old in-flight connections
   (closeTransports) and builds new ones (SetupTransport) atomically,
   so recovery probes never inherit dead connections from a prior
   recovery attempt.
2026-04-30 19:19:19 +07:00
Codescribe a92d20cef8 doq: configure QUIC keep-alive and retry on idle timeout
Pass a quic.Config with KeepAlivePeriod (15s) to DoQ dial calls instead
of nil, so pooled connections send periodic QUIC PINGs to stay alive and
detect dead paths proactively.

Also add IdleTimeoutError to the DoQ retry conditions alongside io.EOF,
so stale pooled connections trigger a transparent retry instead of
propagating as a query failure.
2026-04-30 19:19:19 +07:00
Codescribe a8821e6d00 fix(darwin): support non-standard listener port in intercept mode
When port 53 is taken (e.g. by mDNSResponder), ctrld failed with
'could not find available listen ip and port' instead of falling back
to port 5354. Root cause: tryUpdateListenerConfig() checked the
dnsIntercept bool, which is derived in prog.run() AFTER listener
config is resolved.

Fix: check interceptMode string directly (CLI flag + config fallback)
in a new tryUpdateListenerConfigIntercept() that tries 127.0.0.1:53
then 127.0.0.1:5354.

Also updates buildPFAnchorRules() to use the actual listener IP/port
from config instead of hardcoded 127.0.0.1:53, so pf rules redirect
to wherever ctrld is actually listening.
2026-04-30 19:19:19 +07:00
Codescribe a3880beec2 docs: port IPv6 learnings and comment fixes to master
- Update comment in ensurePFAnchorReference: pfctl -sn returns
  rdr-anchor only (nat-anchor not used by ctrld)
- Update nat-anchor table entry in pf-dns-intercept.md
- Add pf nuances 10-16 from investigation: cross-AF redirect,
  block return, sendmsg EINVAL, nat-on-lo0, raw sockets, DIOCNATLOOK,
  and the pragmatic IPv6 block solution
2026-04-30 19:19:19 +07:00
Codescribe d7124995d2 fix: bracket IPv6 addresses in VPN DNS upstream config
upstreamConfigFor() used strings.Contains(":") to decide whether to
append ":53", which always evaluates true for IPv6 addresses. This left
bare addresses like "2a0d:6fc0:9b0:3600::1" without brackets or port,
causing net.Dial to reject with "too many colons in address".

Use net.JoinHostPort() which handles IPv6 bracketing automatically,
producing "[2a0d:6fc0:9b0:3600::1]:53".
2026-04-30 19:19:19 +07:00
Codescribe 86dafc432d Add log tail command for live log streaming
This commit adds a new `ctrld log tail` subcommand that streams
runtime debug logs to the terminal in real-time, similar to `tail -f`.

Changes:
- log_writer.go: Add Subscribe/tailLastLines for fan-out to tail clients
- control_server.go: Add /log/tail endpoint with streaming response
  - Internal logging: subscribes to logWriter for live data
  - File-based logging: polls log file for new data (200ms interval)
  - Sends last N lines as initial context on connect
- commands.go: Add `log tail` cobra subcommand with --lines/-n flag
- control_client.go: Add postStream() with no timeout for long-lived connections

Usage:
  sudo ctrld log tail          # shows last 10 lines then follows
  sudo ctrld log tail -n 50    # shows last 50 lines then follows
  Ctrl+C to stop
2026-04-30 19:19:19 +07:00
Codescribe 0f2a930cf8 feat: robust username detection and CI updates
Add platform-specific username detection for Control D metadata:
- macOS: directory services (dscl) with console user fallback
- Linux: systemd loginctl, utmp, /etc/passwd traversal
- Windows: WTS session enumeration, registry, token lookup
2026-04-30 19:19:19 +07:00
Codescribe 5a6163142c feat: add VPN DNS split routing 2026-04-30 19:19:19 +07:00
Codescribe 402771bed6 feat: add Windows NRPT and WFP DNS interception 2026-04-30 19:19:19 +07:00
Codescribe a99dcca288 feat: add macOS pf DNS interception 2026-04-30 19:19:19 +07:00
Codescribe 395335162f feat: introduce DNS intercept mode infrastructure 2026-04-30 19:19:19 +07:00
Codescribe c56d4771de docs: add DNS Intercept Mode section to README 2026-04-30 19:19:19 +07:00
Codescribe ea48186d73 Fix dnsFromResolvConf not filtering loopback IPs
The continue statement only broke out of the inner loop, so
loopback/local IPs (e.g. 127.0.0.1) were never filtered.
This caused ctrld to use itself as bootstrap DNS when already
installed as the system resolver — a self-referential loop.

Use the same isLocal flag pattern as getDNSFromScutil() and
getAllDHCPNameservers().
2026-04-30 19:19:19 +07:00
Codescribe 846aaac27a fix: include hostname hints in metadata for API-side fallback
Send all available hostname sources (ComputerName, LocalHostName,
HostName, os.Hostname) in the metadata map when provisioning.
This allows the API to detect and repair generic hostnames like
'Mac' by picking the best available source server-side.

Belt and suspenders: preferredHostname() picks the right one
client-side, but metadata gives the API a second chance.
2026-04-30 19:19:19 +07:00
Codescribe f1e49a7ee6 fix(darwin): use scutil for provisioning hostname (#485)
macOS Sequoia with Private Wi-Fi Address enabled causes os.Hostname()
to return generic names like "Mac.lan" from DHCP instead of the real
computer name. The /utility provisioning endpoint sends this raw,
resulting in devices named "Mac-lan" in the dashboard.

Fallback chain: ComputerName → LocalHostName → os.Hostname()

LocalHostName can also be affected by DHCP. ComputerName is the
user-set display name from System Settings, fully immune to network state.
2026-04-30 19:19:19 +07:00