Files
hanzo-dev beefb70009 reference: bound one request, bound one tenant's bytes, keep the version a decision cites
resolve could spend the whole process on one authenticated request. Two
amplifiers composed. A key had a count bound and no BYTE bound, and the domain
matcher split a host into labels and re-joined every tail, so an L-label host
allocated a copy of each of its L suffixes: one 8 KB dotted key materialised
16.8 MB and 100 of them 1.7 GB, measured over the router. And `sets` had no
bound and no dedupe, so naming one set N times ran N times the answers.

  - maxKey bounds one key in bytes at every door it crosses — looked up,
    written, removed — and REFUSES rather than truncating, because a shortened
    key is a different key.
  - a domain suffix is now a slice of the host, not a join of its labels: the
    same answers in O(L) headers over one backing array.
  - a call may name each published set once, and a set named twice is consulted
    once.

The override write had the same hole from the other side: maxOverrides bounded
rows and nothing bounded a row, so 10,000 entries x 11 sets was gigabytes of
attacker-chosen bytes on the one volume every other organisation's store lives
on. The same maxKey closes it; an over-long note is refused rather than trimmed;
and what one organisation may occupy on that volume is now a figure the code
computes and a test pins, so raising rows, a key, a note or the catalog is an act
with its consequence next to it.

An override is a record, so it is now shipped before it is acknowledged, the way
apps/research and apps/books do it — this deployment is one replica with a
recreate rollout, and an unshipped write is a control an operator believes is in
force and is not.

prune spared ONE version: the call site passed the current version for both of
the statement's two placeholders, deleting the rows behind every citation taken
in the window before a refresh. What a take supersedes is now decided by
sweepOld over what the plane held before it, and proved against a warehouse
rather than against the text of the statement.

The publisher's end of the same amplifier is closed with the same door. A take
is refused whole if it carries a member longer than maxKey — one no lookup could
ever reach, so it is only weight in the warehouse, in every hydrate and in the
snapshot every request reads — or more members than a published set holds: swing
measures GROWTH against the version a take replaces, and a first take has nothing
to measure against, which after a cold start is every take. maxBody comes down to
six times the largest source in the catalog (measured: 2.6 MB), because the parse
allocates before any later gate can look at what it made. A publisher's redirect
must keep the two properties its origin already had, TLS and a destination
outside this network: this process runs in the cluster, where "wherever the
publisher says" reaches the pod network and the metadata address.

Also: a take whose size swings past 4x is refused and the previous version
stands (force is the operator lever); the disposable list is refused whole if it
names a mailbox provider, which is the one-row attack the size gate cannot see
on the one unpinned source; an attest receipt with no version or no designations
is recorded as a refusal instead of a current, fresh list; the plane sweeps at
cold start instead of refusing every set for six hours after a deploy; the one
cross-fleet aggregation states its own memory and time budget; every source
states a typed redistribution Basis from a closed vocabulary, on the wire, so
the licence position is an audit rather than a sentence; refresh reads the ONE
SuperAdmin predicate rather than restating it, and an admin of their own org is
refused there, because this route writes the baseline every org reads; and the
bridge sits on this app's own leaf, not on the /v1/ml parent two other products
answer under.

65 tests green under -race (one skipped: it dials the real publishers). Every fix
has a regression test that fails when that fix alone is reverted: 24 mutants, 24
killed.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-02 11:45:33 -07:00

478 lines
15 KiB
Go

package reference
// source.go turns a publisher's bytes into entries. One parser per publisher and
// no default: luxfi/aml pkg/screen fell back to one parser for four different
// list formats on the grounds that they looked similar, and three of the four
// then failed silently every night for months. A format this file does not know
// is an error, never an empty list.
//
// Every parser here is TOTAL over its input in one direction only: it refuses
// bytes it cannot read, and it skips individual rows it cannot read while
// counting them, so a publisher who changes one column does not turn the whole
// set into silence.
import (
"encoding/csv"
"encoding/json"
"fmt"
"net/netip"
"strconv"
"strings"
)
// maxEntries bounds one source's contribution, so a publisher who starts
// serving something enormous costs a refusal rather than the process.
const maxEntries = 200_000
// errEmpty is what every fetched parser returns for a source that yielded
// nothing. It is an error rather than an empty slice because no publisher's list
// of disposable domains, hosting ranges or crawler patterns is empty — zero
// entries means the fetch or the parse is wrong, and the previous version must
// stand.
func errEmpty(name string) error {
return fmt.Errorf("reference: %s parsed to no entries, which no published list is", name)
}
// parseLines reads a newline-delimited list, ignoring blanks and # comments. The
// value is the same for every member, because a bare list states membership and
// nothing else.
func parseLines(source string, value map[string]string) func([]byte) ([]Entry, error) {
return func(body []byte) ([]Entry, error) {
out := make([]Entry, 0, 1024)
for _, line := range strings.Split(string(body), "\n") {
line = strings.ToLower(strings.TrimSpace(line))
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if len(out) >= maxEntries {
return nil, fmt.Errorf("reference: %s carries more than %d entries", source, maxEntries)
}
out = append(out, Entry{Key: line, Value: value})
}
if len(out) == 0 {
return nil, errEmpty(source)
}
return out, nil
}
}
// mailbox is the closed list of domains that are NOT disposable-inbox providers
// and that no disposable-domain list may name.
//
// It exists because the disposable list is fetched from a public repository with
// no pin, no signature and no digest to check against — the honest state of that
// source — so anyone who can land a commit on it can change what every
// organisation's decisions read. The size gate (swung) catches a list that
// arrives at a fraction or a multiple of itself; it cannot catch ONE added row,
// and one added row is the whole attack: "gmail.com is disposable" refuses a
// large share of every tenant's legitimate signups at once.
//
// Bounded coverage, stated plainly: this is the blast radius of that one row, not
// a proof the list is honest. A publisher naming one of these is wrong about
// something we know, so the whole take is refused and the previous version stands
// — a poisoned list must not land at all rather than land minus the row we
// happened to recognise.
var mailbox = map[string]bool{
"gmail.com": true, "googlemail.com": true,
"outlook.com": true, "hotmail.com": true, "live.com": true, "msn.com": true,
"yahoo.com": true, "ymail.com": true,
"icloud.com": true, "me.com": true, "mac.com": true,
"aol.com": true, "gmx.com": true, "gmx.net": true, "mail.com": true,
"proton.me": true, "protonmail.com": true, "pm.me": true,
"zoho.com": true, "fastmail.com": true, "yandex.ru": true, "qq.com": true,
"163.com": true, "126.com": true, "naver.com": true, "web.de": true,
}
// parseDisposable reads the throwaway-inbox list and refuses a take that names a
// domain we know is a mailbox provider.
func parseDisposable(body []byte) ([]Entry, error) {
entries, err := parseLines("disposable", map[string]string{"class": "disposable"})(body)
if err != nil {
return nil, err
}
for _, e := range entries {
if mailbox[e.Key] {
return nil, fmt.Errorf("reference: the disposable list names %q, which hands out mailboxes rather than throwaway inboxes; the whole take is refused rather than landing a list that is wrong about something we can check", e.Key)
}
}
return entries, nil
}
// prefix normalises one CIDR (or a bare address, which is its own /32 or /128)
// into the canonical masked form entries are keyed by. A bare address is
// accepted because two of the publishers here list addresses rather than blocks.
func prefix(s string) (netip.Prefix, bool) {
s = strings.TrimSpace(s)
if s == "" {
return netip.Prefix{}, false
}
if p, err := netip.ParsePrefix(s); err == nil {
return p.Masked(), true
}
if a, err := netip.ParseAddr(s); err == nil {
return netip.PrefixFrom(a, a.BitLen()), true
}
return netip.Prefix{}, false
}
// block builds one network entry. class is what the address IS (hosting, tor,
// reserved) and operator is who runs it — two separate facts, because "this is a
// datacentre" and "this is Amazon" are different inputs to a rule.
func block(p netip.Prefix, class, operator, region string) Entry {
v := map[string]string{"class": class}
if operator != "" {
v["operator"] = operator
}
if region != "" {
v["region"] = region
}
return Entry{Key: p.String(), Value: v}
}
// parseCIDRs reads a newline-delimited CIDR list.
func parseCIDRs(source, class, operator string) func([]byte) ([]Entry, error) {
return func(body []byte) ([]Entry, error) {
out := make([]Entry, 0, 256)
for _, line := range strings.Split(string(body), "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
p, ok := prefix(line)
if !ok {
continue
}
out = append(out, block(p, class, operator, ""))
}
if len(out) == 0 {
return nil, errEmpty(source)
}
return out, nil
}
}
// parseTor reads the bulk exit list: one exit address per line. An exit address
// is not hosting — it is an address whose traffic arrived through a network
// designed to detach it from its origin, which is a different fact and gets its
// own class.
func parseTor(body []byte) ([]Entry, error) {
out := make([]Entry, 0, 2048)
for _, line := range strings.Split(string(body), "\n") {
p, ok := prefix(line)
if !ok {
continue
}
out = append(out, block(p, "tor", "tor", ""))
}
if len(out) == 0 {
return nil, errEmpty("tor")
}
return out, nil
}
func parseAWS(body []byte) ([]Entry, error) {
var doc struct {
Prefixes []struct {
IP string `json:"ip_prefix"`
Region string `json:"region"`
Service string `json:"service"`
} `json:"prefixes"`
V6 []struct {
IP string `json:"ipv6_prefix"`
Region string `json:"region"`
Service string `json:"service"`
} `json:"ipv6_prefixes"`
}
if err := json.Unmarshal(body, &doc); err != nil {
return nil, fmt.Errorf("reference: aws ranges: %w", err)
}
out := make([]Entry, 0, len(doc.Prefixes)+len(doc.V6))
// The AMAZON service row is the union of every other row, so keeping only it
// gives one entry per block instead of four saying the same thing.
for _, p := range doc.Prefixes {
if p.Service != "AMAZON" {
continue
}
if q, ok := prefix(p.IP); ok {
out = append(out, block(q, "hosting", "aws", p.Region))
}
}
for _, p := range doc.V6 {
if p.Service != "AMAZON" {
continue
}
if q, ok := prefix(p.IP); ok {
out = append(out, block(q, "hosting", "aws", p.Region))
}
}
if len(out) == 0 {
return nil, errEmpty("aws")
}
return out, nil
}
func parseGCP(body []byte) ([]Entry, error) {
var doc struct {
Prefixes []struct {
V4 string `json:"ipv4Prefix"`
V6 string `json:"ipv6Prefix"`
Scope string `json:"scope"`
} `json:"prefixes"`
}
if err := json.Unmarshal(body, &doc); err != nil {
return nil, fmt.Errorf("reference: gcp ranges: %w", err)
}
out := make([]Entry, 0, len(doc.Prefixes))
for _, p := range doc.Prefixes {
for _, raw := range []string{p.V4, p.V6} {
if q, ok := prefix(raw); ok {
out = append(out, block(q, "hosting", "gcp", p.Scope))
}
}
}
if len(out) == 0 {
return nil, errEmpty("gcp")
}
return out, nil
}
func parseOracle(body []byte) ([]Entry, error) {
var doc struct {
Regions []struct {
Region string `json:"region"`
CIDRs []struct {
CIDR string `json:"cidr"`
} `json:"cidrs"`
} `json:"regions"`
}
if err := json.Unmarshal(body, &doc); err != nil {
return nil, fmt.Errorf("reference: oracle ranges: %w", err)
}
var out []Entry
for _, r := range doc.Regions {
for _, c := range r.CIDRs {
if q, ok := prefix(c.CIDR); ok {
out = append(out, block(q, "hosting", "oracle", r.Region))
}
}
}
if len(out) == 0 {
return nil, errEmpty("oracle")
}
return out, nil
}
func parseFastly(body []byte) ([]Entry, error) {
var doc struct {
V4 []string `json:"addresses"`
V6 []string `json:"ipv6_addresses"`
}
if err := json.Unmarshal(body, &doc); err != nil {
return nil, fmt.Errorf("reference: fastly ranges: %w", err)
}
out := make([]Entry, 0, len(doc.V4)+len(doc.V6))
for _, raw := range append(append([]string{}, doc.V4...), doc.V6...) {
if q, ok := prefix(raw); ok {
out = append(out, block(q, "hosting", "fastly", ""))
}
}
if len(out) == 0 {
return nil, errEmpty("fastly")
}
return out, nil
}
// geofeed reads the RFC 8805 self-published geofeed both Linode and
// DigitalOcean serve: prefix, country, region, city, postal code. Only the first
// three columns are kept — a city is not a risk signal and a postal code is
// closer to personal data than to one.
func geofeed(source, operator string) func([]byte) ([]Entry, error) {
return func(body []byte) ([]Entry, error) {
var out []Entry
for _, line := range strings.Split(string(body), "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
cols := strings.Split(line, ",")
p, ok := prefix(cols[0])
if !ok {
continue
}
region := ""
if len(cols) > 1 {
region = strings.TrimSpace(cols[1])
}
out = append(out, block(p, "hosting", operator, region))
}
if len(out) == 0 {
return nil, errEmpty(source)
}
return out, nil
}
}
var (
parseLinode = geofeed("linode", "linode")
parseDigitalOcean = geofeed("digitalocean", "digitalocean")
)
// parseSpecial reads an IANA special-purpose address registry. These are the
// blocks no public host may legitimately be reached at, so an inbound
// connection claiming one is a claim about the world that is not true.
//
// A footnote marker rides on some blocks ("192.0.0.0/29[2]") and one row can
// carry several blocks in one field, so the cell is split and each part is
// stripped before parsing.
func parseSpecial(body []byte) ([]Entry, error) {
r := csv.NewReader(strings.NewReader(string(body)))
r.FieldsPerRecord = -1
r.LazyQuotes = true
rows, err := r.ReadAll()
if err != nil {
return nil, fmt.Errorf("reference: iana special registry: %w", err)
}
var out []Entry
for i, row := range rows {
if i == 0 || len(row) < 2 {
continue
}
name := strings.Trim(strings.TrimSpace(row[1]), `"`)
for _, part := range strings.Split(row[0], ",") {
if cut := strings.IndexByte(part, '['); cut >= 0 {
part = part[:cut]
}
p, ok := prefix(part)
if !ok {
continue
}
e := block(p, "reserved", "iana", "")
e.Value["name"] = name
out = append(out, e)
}
}
if len(out) == 0 {
return nil, errEmpty("iana special registry")
}
return out, nil
}
// parseASN reads the IANA autonomous system number registry. Rows address a
// RANGE of numbers ("1877-1901"), so entries are keyed by the range and matched
// numerically — see MatchRange.
//
// What this set answers is narrow and worth being precise about: whether a
// number has been delegated at all, and to which regional registry. It does NOT
// answer whether the operator behind it is trustworthy; that is the reputation
// seam.
func parseASN(body []byte) ([]Entry, error) {
r := csv.NewReader(strings.NewReader(string(body)))
r.FieldsPerRecord = -1
r.LazyQuotes = true
rows, err := r.ReadAll()
if err != nil {
return nil, fmt.Errorf("reference: iana as-numbers: %w", err)
}
var out []Entry
for i, row := range rows {
if i == 0 || len(row) < 2 {
continue
}
lo, hi, ok := span(row[0])
if !ok {
continue
}
desc := strings.TrimSpace(row[1])
out = append(out, Entry{
Key: fmt.Sprintf("%d-%d", lo, hi),
Value: map[string]string{
"registry": registry(desc),
"status": status(desc),
},
})
}
if len(out) == 0 {
return nil, errEmpty("iana as-numbers")
}
return out, nil
}
// span reads "1877-1901" or "0" into a closed numeric interval.
func span(s string) (lo, hi uint64, ok bool) {
s = strings.TrimSpace(s)
a, b, dash := strings.Cut(s, "-")
lo, err := strconv.ParseUint(strings.TrimSpace(a), 10, 32)
if err != nil {
return 0, 0, false
}
if !dash {
return lo, lo, true
}
hi, err = strconv.ParseUint(strings.TrimSpace(b), 10, 32)
if err != nil {
return 0, 0, false
}
if hi < lo {
return 0, 0, false
}
return lo, hi, true
}
// registry names the regional registry a block was delegated to, from the
// registry's own description ("Assigned by ARIN").
func registry(desc string) string {
for _, rir := range []string{"ARIN", "RIPE NCC", "APNIC", "LACNIC", "AFRINIC"} {
if strings.Contains(strings.ToUpper(desc), rir) {
return strings.ToLower(strings.ReplaceAll(rir, " NCC", ""))
}
}
return ""
}
// status is whether the block is delegated, reserved or held back. An
// autonomous system number that IANA has not delegated cannot legitimately
// appear in routing, so a claim naming one is false on its face.
func status(desc string) string {
d := strings.ToLower(desc)
switch {
case strings.Contains(d, "assigned by"):
return "delegated"
case strings.Contains(d, "reserved"):
return "reserved"
case strings.Contains(d, "unallocated"), strings.Contains(d, "available"):
return "unallocated"
default:
return "other"
}
}
// parseCrawlers reads the crawler-user-agents catalogue. Its `pattern` field is
// a regular expression by design, so it is carried through as one and compiled
// when the snapshot is built — rewriting it as a literal would drop the
// alternations and anchors the publisher put there on purpose.
func parseCrawlers(body []byte) ([]Entry, error) {
var doc []struct {
Pattern string `json:"pattern"`
URL string `json:"url"`
}
if err := json.Unmarshal(body, &doc); err != nil {
return nil, fmt.Errorf("reference: crawler patterns: %w", err)
}
out := make([]Entry, 0, len(doc))
for _, c := range doc {
p := strings.TrimSpace(c.Pattern)
if p == "" {
continue
}
e := Entry{Key: p, Value: map[string]string{"class": "crawler"}}
if c.URL != "" {
e.Value["about"] = c.URL
}
out = append(out, e)
}
if len(out) == 0 {
return nil, errEmpty("crawler patterns")
}
return out, nil
}