hostutil

package
v1.0.217 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 4 Imported by: 0

Documentation

Overview

Package hostutil provides pure host-string normalization helpers shared across the proxy (policy matching, category lookups, scan/bypass keys). It is a self-contained seam (stdlib + golang.org/x/net/idna, no Culvert coupling) extracted from package main per ADR-0002 / ADR-0003 to unblock the catdb and scan clusters, which both depend on these helpers.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func MatchFQDN added in v1.0.19

func MatchFQDN(pattern, host string) bool

MatchFQDN reports whether host matches pattern under the proxy's canonical FQDN-glob semantics, normalizing both inputs first. Moved from package main's policy engine (ADR-0002, policy.go decomposition Phase B) — shared by policy rule matching and the SSL-bypass matcher, whose agreement is pinned by main's policy_bypass_security_test.go.

func MatchFQDNNorm added in v1.0.19

func MatchFQDNNorm(pattern, host string) bool

MatchFQDNNorm is MatchFQDN's core, operating on inputs that are ALREADY IDNA-normalized. The policy hot path passes a once-normalized host and a rule's precomputed normalized pattern, avoiding the two per-rule NormalizeHost allocations.

── Why the bare-domain branch does not build "."+pattern ─────────────────────

This function is called ONCE PER RULE, PER REQUEST: evalAccessRules walks the enabled rulebase in priority order and every rule carrying a DestFQDN reaches here (policy.go matchDestNorm), and the SSL-bypass matcher walks its own pattern list the same way per CONNECT (internal/sslbypass). So its cost is multiplied by the rule count on every proxied request, and an allowed request pays the MISS on every rule ahead of the one that matches.

The bare-domain branch used to decide its suffix test as strings.HasSuffix(host, "."+pattern). That concatenation exists only to be compared and thrown away, and Go hands a non-escaping concatenation a 32-byte stack buffer — so a pattern of 31 bytes or less cost a copy, and anything longer cost a HEAP ALLOCATION. Long is the ordinary shape for the bare-domain rules operators actually write ("assets.cdn.example-corporation.com" is 34), which put the allocation on the wrong side of the common case: one per rule, per request. A 100-rule policy of such patterns burned ~4.8 KB of garbage on every proxied request, scaling with the rulebase.

Deciding the same test by index costs neither. Measured on this machine (Go 1.26, 100 rules that all miss — what an allowed request pays before it reaches its catch-all; hostutil_matchfqdn_bench_test.go):

pattern  │ before                          │ after
─────────┼─────────────────────────────────┼──────────────────────────
26 bytes │ 2160 ns/op     0 B    0 allocs  │  660 ns/op  0 B  0 allocs
37 bytes │ 5171 ns/op  4800 B  100 allocs  │  492 ns/op  0 B  0 allocs
54 bytes │ 5363 ns/op  6400 B  100 allocs  │  422 ns/op  0 B  0 allocs

The 26-byte row is the one that never allocated, and it still gets 3.3x faster: dropping runtime.concatstring2 and its copy is most of the win, so the gain does not depend on a policy being written with long patterns.

The equivalence is exact, not approximate. strings.HasSuffix(host, "."+p) is true exactly when len(host) >= len(p)+1, host's last len(p) bytes equal p, and the byte before them is '.'. With n = len(host)-len(p) that is n > 0 && host[n-1] == '.' && host[n:] == p — including the boundary shapes (empty pattern, pattern longer than host, equal lengths). Pinned against the verbatim pre-fix body by TestMatchFQDNNorm_MatchesPreOptimizationBehaviour and FuzzMatchFQDNNorm.

The "*." branch is left alone: pattern[1:] is a substring, which never allocated.

func NormalizeHost

func NormalizeHost(host string) string

NormalizeHost applies IDNA2008 normalization (RFC 5890) to a hostname, converting Unicode/Punycode domains to their canonical ASCII form. This prevents IDN homograph attacks where visually similar Unicode characters (e.g., Cyrillic 'а' vs Latin 'a') bypass blocklists and policy rules.

Returns the lowercased, IDNA-normalized host. If normalization fails the input is returned lowercased — fail-open, which is acceptable ONLY for canonicalizing admin-entered patterns and store keys (a pattern that fails IDNA simply matches literally, so nothing is admitted that a valid pattern would have blocked). Request-path host validation must use NormalizeHostStrict so a malformed host is REJECTED, not passed through (RISK-013).

func NormalizeHostStrict added in v1.0.32

func NormalizeHostStrict(host string) (norm string, ok bool)

NormalizeHostStrict is NormalizeHost's fail-closed core: it reports ok=false when IDNA conversion fails instead of falling back to the raw input. Security gates on the request path (proxy dispatch, SOCKS5) use it to reject hosts that cannot be canonicalized — a host the normalizer cannot map to canonical ASCII would otherwise flow into FQDN/blocklist/ category matching un-normalized, and a fail-open in the canonicalization step is exactly the asymmetry evasion techniques target (RISK-013). Empty input returns ok=true: emptiness is a separate upstream validity concern, and rejecting it here would change unrelated dispatch behavior.

func StripHostPort

func StripHostPort(host string) string

StripHostPort removes a trailing :port and IPv6 brackets from a host value, accepting all shapes that reach scan/bypass lookups: "host:port", "[v6]:port", "[v6]", bare "v6", and bare "host". A naive LastIndex(host, ":") cut corrupts bare IPv6 literals (already de-bracketed by net.SplitHostPort upstream) — "2001:db8::1" would become "2001:db8:".

Types

This section is empty.

Jump to

Keyboard shortcuts

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