fqdn

package
v1.801.466 Latest Latest
Warning

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

Go to latest
Published: Aug 5, 2026 License: Apache-2.0 Imports: 6 Imported by: 0

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

View Source
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 Challenge

func Challenge(h string) string

Challenge is the record name a claimant publishes the token at.

func Clean

func Clean(raw string) string

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

func Proven(txts []string, token string) bool

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

func Token() (string, error)

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

func Valid(h string) bool

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

func Verify(ctx context.Context, r Resolver, h, token string) error

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.

func Records

func Records(h, token, target string) []Record

Records are the exact records a customer publishes for a custom host: the TXT ownership token, plus the CNAME that routes traffic at their Hanzo host once the claim verifies.

type Resolver

type Resolver interface {
	LookupTXT(ctx context.Context, name string) ([]string, error)
}

Resolver is the minimal DNS surface ownership proof needs. *net.Resolver satisfies it, so production needs no adapter; tests inject a fake when they want to exercise the lookup, though the rule itself (Proven) needs none.

Jump to

Keyboard shortcuts

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