- delete embedded community resolver snapshot (6,760 lines) + EmbeddedServers + DNS_SERVERS=fresh; curated resolvers are the default. Simplify Warm (no sampling). - one generic internal/sample.Queue[T] replaces three near-identical random-sample queues (probe, pipeline, discover). - delete unreachable index.listOpenSearch (OpenSearch is never listable). - delete classifyCloudFront (its match is always discarded, !ResourceSpecific). - delete probe.Probe wrapper + Describe; tests use ProbeDetailed. - delete store.Pool(), model.SourceDNSAlias, discover.QueueLen. - drop dead config/no-op knobs: pipeline DirectWorkers, discover MaxPerDomain, discover S3/GCS/DO flags, BUCKET_SCAN_DO. ~-7,000 lines (mostly the resolver snapshot), 0 deps removed, tests green.
199 lines
5.4 KiB
Go
199 lines
5.4 KiB
Go
// Package classify maps a DNS hostname to a cloud/database/storage resource.
|
|
//
|
|
// The hot path is deliberately allocation-free: it never lowercases unless an
|
|
// uppercase byte is present, never splits the hostname into a []string, never
|
|
// uses regexp/JSON/reflection, and returns substrings of the input as the
|
|
// region/resource. See BenchmarkClassify* for the 0 allocs/op assertion.
|
|
package classify
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"tea.karlcom.de/karl/bucketeer/internal/model"
|
|
)
|
|
|
|
const maxLabels = 64
|
|
|
|
// scanner holds the byte offsets of each dot-separated label of a hostname.
|
|
// It is stack allocated by Classify and never heap allocated.
|
|
type scanner struct {
|
|
host string
|
|
n int
|
|
start [maxLabels]int32
|
|
}
|
|
|
|
func newScanner(host string) scanner {
|
|
var s scanner
|
|
s.host = host
|
|
start := int32(0)
|
|
for i := 0; i < len(host); i++ {
|
|
if host[i] == '.' {
|
|
if s.n >= maxLabels {
|
|
s.n = -1
|
|
return s
|
|
}
|
|
s.start[s.n] = start
|
|
s.n++
|
|
start = int32(i + 1)
|
|
}
|
|
}
|
|
if s.n >= maxLabels {
|
|
s.n = -1
|
|
return s
|
|
}
|
|
s.start[s.n] = start
|
|
s.n++
|
|
return s
|
|
}
|
|
|
|
// label returns the i-th label (0 = leftmost). Callers must ensure i < n.
|
|
func (s *scanner) label(i int) string {
|
|
end := int32(len(s.host))
|
|
if i+1 < s.n {
|
|
end = s.start[i+1] - 1
|
|
}
|
|
return s.host[s.start[i]:end]
|
|
}
|
|
|
|
func hasUpper(s string) bool {
|
|
for i := 0; i < len(s); i++ {
|
|
if s[i] >= 'A' && s[i] <= 'Z' {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func trimTrailingDots(s string) string {
|
|
for len(s) > 0 && s[len(s)-1] == '.' {
|
|
s = s[:len(s)-1]
|
|
}
|
|
return s
|
|
}
|
|
|
|
func validHost(host string) bool {
|
|
if host == "" || len(host) > 253 {
|
|
return false
|
|
}
|
|
for i := 0; i < len(host); i++ {
|
|
c := host[i]
|
|
switch {
|
|
case c >= 'a' && c <= 'z':
|
|
case c >= '0' && c <= '9':
|
|
case c == '-' || c == '.' || c == '_':
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Classify returns the provider resource described by host, if any.
|
|
// The returned Match's string fields alias host.
|
|
func Classify(host string) (model.Match, bool) {
|
|
host = trimTrailingDots(host)
|
|
if host == "" || len(host) > 253 {
|
|
return model.Match{}, false
|
|
}
|
|
// Wildcard certificates never describe a concrete resource.
|
|
if host[0] == '*' {
|
|
return model.Match{}, false
|
|
}
|
|
if hasUpper(host) {
|
|
host = strings.ToLower(host)
|
|
}
|
|
if !validHost(host) {
|
|
return model.Match{}, false
|
|
}
|
|
|
|
s := newScanner(host)
|
|
if s.n < 2 {
|
|
return model.Match{}, false
|
|
}
|
|
|
|
switch {
|
|
case strings.HasSuffix(host, ".amazonaws.com"):
|
|
return classifyAWS(&s)
|
|
case strings.HasSuffix(host, ".core.windows.net"):
|
|
return classifyAzureCore(&s)
|
|
case strings.HasSuffix(host, ".database.windows.net"):
|
|
return classifyAzureDatabase(&s)
|
|
case strings.HasSuffix(host, ".database.azure.com"):
|
|
return classifyAzureFlexibleServer(&s)
|
|
case strings.HasSuffix(host, ".documents.azure.com"):
|
|
return classifyAzureDocuments(&s)
|
|
case strings.HasSuffix(host, ".cosmos.azure.com"):
|
|
return classifyAzureCosmos(&s)
|
|
case strings.HasSuffix(host, ".cache.windows.net"):
|
|
return classifyAzureRedis(&s)
|
|
case strings.HasSuffix(host, ".redis.azure.net"):
|
|
return classifyAzureManagedRedis(&s)
|
|
case strings.HasSuffix(host, ".search.windows.net"):
|
|
return classifyAzureSearch(&s)
|
|
case strings.HasSuffix(host, ".storage.googleapis.com"):
|
|
return classifyGCS(&s)
|
|
case strings.HasSuffix(host, ".firebaseio.com"):
|
|
return classifyFirebaseIO(&s)
|
|
case strings.HasSuffix(host, ".firebasedatabase.app"):
|
|
return classifyFirebaseDB(&s)
|
|
case strings.HasSuffix(host, ".appspot.com"):
|
|
return classifyAppspot(&s)
|
|
case strings.HasSuffix(host, ".r2.cloudflarestorage.com"):
|
|
return classifyR2(&s)
|
|
case strings.HasSuffix(host, ".r2.dev"):
|
|
return classifyR2Dev(&s)
|
|
case strings.HasSuffix(host, ".digitaloceanspaces.com"):
|
|
return classifyDigitalOceanSpaces(&s)
|
|
case strings.HasSuffix(host, ".ondigitalocean.com"):
|
|
return classifyDigitalOceanDB(&s)
|
|
case strings.HasSuffix(host, ".supabase.co"):
|
|
return classifySupabase(&s)
|
|
case strings.HasSuffix(host, ".supabase.com"):
|
|
return classifySupabasePooler(&s)
|
|
case strings.HasSuffix(host, ".customer-oci.com"):
|
|
return classifyOCIResource(&s)
|
|
case strings.HasSuffix(host, ".oraclecloud.com"):
|
|
return classifyOCIGeneric(&s)
|
|
}
|
|
return model.Match{}, false
|
|
}
|
|
|
|
// ShouldEnrich reports whether a non-matching hostname is worth a DNS/CNAME
|
|
// lookup. It rejects wildcards, the certspotter unparsable placeholder, single
|
|
// labels and obvious provider service apexes.
|
|
func ShouldEnrich(host string) bool {
|
|
host = trimTrailingDots(host)
|
|
if len(host) < 3 || host[0] == '*' {
|
|
return false
|
|
}
|
|
if host[0] == '<' { // certspotter's "<unparsable>" placeholder
|
|
return false
|
|
}
|
|
if !strings.Contains(host, ".") {
|
|
return false
|
|
}
|
|
if hasUpper(host) {
|
|
host = strings.ToLower(host)
|
|
}
|
|
// Provider service apexes never yield a customer resource via CNAME.
|
|
switch {
|
|
case host == "amazonaws.com", strings.HasSuffix(host, ".amazonaws.com"):
|
|
return false
|
|
case host == "core.windows.net", strings.HasSuffix(host, ".windows.net"):
|
|
return false
|
|
case host == "azure.com", strings.HasSuffix(host, ".azure.com"):
|
|
return false
|
|
case host == "googleapis.com", strings.HasSuffix(host, ".googleapis.com"):
|
|
return false
|
|
case host == "cloudflarestorage.com", host == "r2.dev":
|
|
return false
|
|
case strings.HasSuffix(host, ".digitaloceanspaces.com"):
|
|
return false
|
|
case strings.HasSuffix(host, ".supabase.co"), strings.HasSuffix(host, ".supabase.com"):
|
|
return false
|
|
case strings.HasSuffix(host, ".oraclecloud.com"), strings.HasSuffix(host, ".customer-oci.com"):
|
|
return false
|
|
}
|
|
return true
|
|
}
|