resolverprofile

package
v0.213.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 3, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package resolverprofile is the OS-agnostic Tier-2 resolver-profile MODEL plus the per-OS appliers behind it. A Profile says WHAT this machine's DNS should be (the DoH template, the dedicated :53 resolver /128s, the match domains); Render turns it into the exact, inspectable plan for one OS - the files to write and the commands to run, with their revert - as pure data. `whisper resolver --print` shows the plan verbatim; the registered applier for the running OS executes it.

The server's live per-OS setup surfaces are the SPEC this package mirrors, so the CLI apply and the web-script apply converge on ONE mechanism: - Linux: the server's Linux setup template (systemd-resolved drop-in, resolv.conf fallback, cloudflared/dnscrypt-proxy DoH forwarder, bounded wait). - Windows: the server's Windows setup template (DoH server registration + NRPT). - macOS: the server's mobileconfig builder (com.apple.dnsSettings.managed).

Index

Constants

View Source
const (
	// LinuxDropIn is the systemd-resolved drop-in this tool owns (created by
	// apply, removed by revert). The name is ours alone - the web setup script
	// writes whisper-dns.conf, so the two never fight over one file.
	LinuxDropIn = "/etc/systemd/resolved.conf.d/whisper-resolver.conf"
	// LinuxForwarderUnit is the local DoH forwarder service (same unit name as
	// the server's Linux setup script - idempotent convergence, one mechanism).
	LinuxForwarderUnit  = "/etc/systemd/system/whisper-dns.service"
	LinuxDNSCryptConfig = "/etc/dnscrypt-proxy/whisper-dns.toml"
	LinuxResolvConf     = "/etc/resolv.conf"
	// LinuxResolvBackup matches the server script's backup path so either
	// surface can restore what the other saved.
	LinuxResolvBackup = "/etc/resolv.conf.whisper-backup"
	// LinuxLoopback is where the DoH forwarder listens (the server template's
	// 127.0.0.1:53 contract).
	LinuxLoopback = "127.0.0.1"
)

Canonical Linux paths (shared by the renderer, the applier, and the tests - one source of truth so display and execution can never drift).

View Source
const BareDoHURL = "https://doh.whisper.online/dns-query"

BareDoHURL is the bare Whisper DoH endpoint (no device token in the path). Tier-2 DoH is KEYED BY DESIGN: the resolver must identify the tenant to load and apply THEIR policy, so the endpoint answers only the per-tenant form (/<device-token>/dns-query) and refuses anonymous queries (403). The bare form exists as a template shape and a revert matcher - it is never an applyable profile, and the CLI never wires it into OS config.

Variables

View Source
var ErrNeedsRoot = errors.New("root privileges required to change system DNS")

ErrNeedsRoot is returned by an applier when the plan mutates system state but the process is not privileged. The caller prints the exact block to run instead of failing opaquely (Postel: a clear, helpful path, never a dead end).

Functions

func AppleMobileconfig

func AppleMobileconfig(dohURL string) string

AppleMobileconfig builds the com.apple.dnsSettings.managed configuration profile for a DoH URL - the server's mobileconfig payload (DNSProtocol HTTPS + ServerURL, top-level PayloadScope System), with STABLE identifiers so `--off` can address the profile by name. Deterministic for a given URL (same UUIDs every render - idempotent, Apple upgrades in place).

func AppleMobileconfigWithSearch

func AppleMobileconfigWithSearch(dohURL string, search []string) string

AppleMobileconfigWithSearch is the same profile carrying a real suffix SEARCH list: macOS completes a bare hostname from SearchDomains in the DNS payload, which is the per-OS half of "ping db-01 just works". An empty list renders the byte-identical profile AppleMobileconfig always produced, and the PayloadUUIDs stay keyed on the URL alone so a re-render is still idempotent and still upgrades the installed profile in place rather than adding a second one.

func DoHStamp

func DoHStamp(dohURL string) string

DoHStamp encodes a DoH URL as an RFC-draft DNS stamp (sdns://...) - the form dnscrypt-proxy configures a static DoH server with. Byte-identical to the server's own DoH stamp: protocol 0x02, no informal property claims, empty addr (resolve the hostname at run time), empty hashes (trust the WebPKI), then LP(host) and LP(path).

func Register

func Register(goos string, a Applier)

Register wires the applier for one GOOS (called from build-tagged init funcs).

Types

type Applier

type Applier interface {
	// DetectHost returns the real facts about this machine (forwarder on PATH,
	// systemd-resolved active, elevation) for an accurate render. It never needs
	// privileges.
	DetectHost() Host
	// Apply executes r.Apply in order. Returns ErrNeedsRoot (wrapped) when the
	// plan mutates system state and the process is not privileged - the caller
	// then prints the exact block instead.
	Apply(r Rendered) error
	// Revert executes r.Revert in order; guards make it idempotent (safe twice,
	// safe when nothing was ever applied).
	Revert(r Rendered) error
}

Applier executes a rendered plan on THIS machine. One implementation per GOOS, self-registered from its build-tagged file (apply_linux.go registers "linux", apply_windows.go "windows", apply_darwin.go "darwin") - the registry is the seam, so adding an OS never touches shared code.

func For

func For(goos string) (Applier, bool)

For returns the applier registered for goos, if any. A missing applier is not an error path: the caller falls back to printing the exact plan (Postel - the user always gets a working next step).

type FileSpec

type FileSpec struct {
	Path    string      `json:"path"`
	Content string      `json:"content"`
	Mode    fs.FileMode `json:"-"`
}

FileSpec is one file the plan writes, byte-exact.

type Host

type Host struct {
	// ResolvedActive: systemd-resolved is running (Linux only).
	ResolvedActive bool
	// ForwarderPath/ForwarderKind: a USABLE local DoH forwarder (Linux DoH needs
	// one - systemd-resolved cannot speak DoH). Kind is "cloudflared",
	// "dnscrypt-proxy", or "".
	ForwarderPath string
	ForwarderKind string
	// ForwarderVersion is the detected version of that forwarder ("2.0.45",
	// "2.1.7"); the render adapts the emitted config to what the shipped
	// version actually accepts (stock Ubuntu 24.04 ships dnscrypt-proxy 2.0.45,
	// which FATALs on the 2.1 config key - proven live).
	ForwarderVersion string
	// ForwarderRejected explains a forwarder that was found but cannot serve
	// (e.g. cloudflared >= 2026.2.0, which removed proxy-dns); rendered as a
	// note so "install a forwarder" guidance never reads as blind.
	ForwarderRejected string
	// Elevated: root (euid 0) / an elevated shell.
	Elevated bool
}

Host carries the detected facts about the machine a plan will run on. For a cross-OS --print they are sensible defaults; the applier supplies real ones.

type Mode

type Mode string

Mode selects which Tier-2 transport the profile configures.

const (
	// ModeDoH is the default: encrypted DNS-over-HTTPS (conservative out).
	ModeDoH Mode = "doh"
	// ModeDNS53 points the OS at the dedicated per-tenant /128 on plain :53
	// (no forwarder, no GUI approval - the simplest apply; plaintext on-path).
	ModeDNS53 Mode = "dns53"
)

type Profile

type Profile struct {
	Mode Mode
	// OS is the render target: a GOOS - linux | windows | darwin.
	OS string
	// DohTemplate is the DoH URL - the public keyless endpoint or the per-tenant
	// keyed one (the token in the path is a resolve-only device credential).
	DohTemplate string
	// ResolverIPs are the dedicated per-tenant :53 resolver /128s (usually one).
	ResolverIPs []string
	// MatchDomains says which names route through Whisper; ["."] (the default
	// when empty) means everything.
	MatchDomains []string
	// SearchDomains are real suffix SEARCH domains: the list the OS APPENDS to a
	// bare hostname, so `ping db-01` reaches db-01.<suffix>. It is a different
	// thing from MatchDomains, which only says which names ROUTE to Whisper -
	// systemd-resolved's "~example.com" is routing-only and never completes a
	// bare name, which is why the search-domain support had to add this rather than reuse the
	// field that looked like it. Empty means the OS keeps whatever
	// search list it already had; we only ever ADD ours.
	SearchDomains []string
	// AppleProfileURL, when set, is the CMS-SIGNED .mobileconfig URL for this
	// profile's token (keyed darwin renders download it instead of generating an
	// unsigned local plist).
	AppleProfileURL string
}

Profile is the OS-agnostic description of the desired Tier-2 resolver state.

func (Profile) Render

func (p Profile) Render(h Host) Rendered

Render turns the profile into the exact plan for p.OS given the host facts. Pure - no I/O, fully table-testable; this IS what --print shows.

type Rendered

type Rendered struct {
	OS         string `json:"os"`
	Mode       Mode   `json:"mode"`
	DoHURL     string `json:"doh_url,omitempty"`
	ResolverIP string `json:"resolver_ip,omitempty"`
	// Summary is the one-line human description of what apply will do.
	Summary string `json:"summary"`
	Apply   []Step `json:"apply"`
	Revert  []Step `json:"revert"`
	// Notes carry the plain-spoken caveats (plaintext :53, the macOS approval
	// step, a missing forwarder's install line) - stated, never hidden.
	Notes []string `json:"notes,omitempty"`
}

Rendered is the concrete, inspectable plan for one OS: pure data, no I/O.

func (Rendered) ApplyScript

func (r Rendered) ApplyScript() string

ApplyScript renders the apply plan as ONE runnable block - bash on linux/darwin, PowerShell on windows - identical to what the applier executes. This is what `whisper resolver --print` emits on stdout, so a not-root/not-elevated caller can pipe or paste it (Postel: the exact block, never a dead end).

func (Rendered) RevertScript

func (r Rendered) RevertScript() string

RevertScript renders the revert plan the same way (what `--off --print` emits).

type Step

type Step struct {
	// File writes File.Path with File.Content (creating parent dirs).
	File *FileSpec `json:"file,omitempty"`
	// Cmd is an argv executed directly (no shell) - the precise form.
	Cmd []string `json:"cmd,omitempty"`
	// Shell is a shell-language line (PowerShell on windows, sh on unix) for
	// actions that are inherently shell-shaped; displayed and applied verbatim.
	Shell string `json:"shell,omitempty"`
	// Remove deletes a file (missing is fine - idempotent).
	Remove string `json:"remove,omitempty"`
	// WaitFor blocks (bounded) until host:port answers a TCP connect - the
	// server template's "never leave DNS half-configured" gate.
	WaitFor string `json:"wait_for,omitempty"`
	// IfPresent runs the step only when this path exists.
	IfPresent string `json:"if_present,omitempty"`
	// IfAbsent runs the step only when this path does NOT exist.
	IfAbsent string `json:"if_absent,omitempty"`
}

Step is one ordered action of a plan. Exactly one of File / Cmd / Shell / Remove / WaitFor is set. Guards make every step idempotent and every revert safe to run twice.

func (Step) String

func (s Step) String() string

String renders the step as one legible line (what --print's summary shows).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL