Documentation
¶
Overview ¶
Package fqdn is the ONE place a public hostname is normalized, validated, and proven to be under a caller's control.
Two subsystems bind customer hostnames — /v1/platform apps (clients/platform) and PaaS sites (clients/projects) — and both must answer the same three questions: what is the canonical form of this name, is it syntactically a hostname at all, and does the caller actually control it. Each carried its own copy of the answer, and the copies had already drifted: both compiled the same hostname regexp independently, and only the apps path stripped a trailing root dot before matching, so `example.com.` normalized on one endpoint and was rejected as malformed on the other. One concept, one implementation, here.
Ownership proof is the DNS-01 model: the claimant publishes a per-claim token as a TXT record at `_hanzo-challenge.<host>`. Only someone who controls that zone's DNS can, so a matching token proves control. This is the boundary that stops a tenant claiming a host it does not own, so the check is deliberately small enough to read in one sitting.
Everything here is pure except Verify (one DNS read) and Token (one rng read): no store, no HTTP, no service state. That is what lets both callers share it.
Index ¶
Constants ¶
const ChallengePrefix = "_hanzo-challenge."
ChallengePrefix is the label under which the TXT ownership token is published. A leading underscore keeps it out of the space of real service hostnames, per the RFC 8552 convention for attribute leaves.
Variables ¶
This section is empty.
Functions ¶
func Clean ¶
Clean canonicalizes a hostname as typed by a human: surrounding space removed, lowercased, and the optional trailing root dot stripped (`example.com.` and `example.com` name the same host, and only one of them can be a map key).
func Proven ¶
Proven is the ownership rule, and nothing else: does token appear among the TXT records published at the challenge name?
It is a pure predicate over the DNS answer — no context, no resolver, no clock. That is deliberate. This one expression is the boundary that stops a tenant claiming a host it does not control, so it is worth being able to read, review, and exhaust by table test without standing up a fake resolver.
It fails CLOSED: an empty answer, or records none of which match, are "not proven". An empty token is never proven — otherwise a zone publishing an empty TXT record would hand ownership to a caller whose token was never minted.
A zone legitimately carries many TXT records at one name, so every answer is checked rather than just the first, and each is trimmed because resolvers and zone files differ on surrounding whitespace.
func Token ¶
Token mints a fresh per-claim ownership token. 128 bits of rand: a claimant must publish the exact value, so it only has to be unguessable, not long.
func Valid ¶
Valid reports whether h is a syntactically well-formed public hostname. It expects Clean's output — Valid does not normalize, so that a caller cannot accidentally store one form and match another.
func Verify ¶
Verify reads the challenge records for h and applies Proven to them. It returns nil exactly when ownership is proven, and *ErrUnproven otherwise.
A lookup error is not surfaced as a distinct outcome: a SERVFAIL and an absent record are the same fact to a claimant — the proof is not visible — and collapsing them keeps "not proven" a single path that cannot be mistaken for "proven" by a caller that forgets to check one of two error returns.
Types ¶
type ErrUnproven ¶
type ErrUnproven struct {
Name string // the record name to publish at
Token string // the value it must carry
}
ErrUnproven is returned by Verify when ownership is not (yet) demonstrated. It is an expected outcome, not a fault: a customer who has just claimed a host has not published the record yet, and the caller renders this as "pending" rather than as an error. Callers test with errors.Is; its message is customer-facing and names exactly what to publish.
func (*ErrUnproven) Error ¶
func (e *ErrUnproven) Error() string
type Record ¶
type Record struct {
Type string `json:"type"` // TXT | CNAME
Name string `json:"name"` // the record name the customer creates
Value string `json:"value"` // the record value
}
Record is one DNS record a claimant must publish, as rendered to the customer.