// Package models holds the plain data types shared between the storage layer, // the DNS engine and the HTTP layer. Nothing here depends on a database or on // the DNS library, which keeps the type definitions usable from every package. package models import "time" // ZoneKind distinguishes forward lookup zones from the two reverse families. type ZoneKind string const ( ZoneForward ZoneKind = "forward" ZoneReverse4 ZoneKind = "reverse4" ZoneReverse6 ZoneKind = "reverse6" ) // Valid reports whether k is a kind the application understands. func (k ZoneKind) Valid() bool { switch k { case ZoneForward, ZoneReverse4, ZoneReverse6: return true } return false } // Label returns a human readable name for the zone kind. func (k ZoneKind) Label() string { switch k { case ZoneReverse4: return "Reverse IPv4" case ZoneReverse6: return "Reverse IPv6" default: return "Forward" } } // Zone is an authoritative DNS zone. type Zone struct { ID int64 `json:"id"` Name string `json:"name"` // normalised FQDN, always ends in "." Kind ZoneKind `json:"kind"` Description string `json:"description"` Enabled bool `json:"enabled"` DefaultTTL uint32 `json:"default_ttl"` PrimaryNS string `json:"primary_ns"` AdminEmail string `json:"admin_email"` Serial uint32 `json:"serial"` Refresh uint32 `json:"refresh"` Retry uint32 `json:"retry"` Expire uint32 `json:"expire"` Minimum uint32 `json:"minimum"` AutoSerial bool `json:"auto_serial"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` // RecordCount is populated by list queries; it is not stored. RecordCount int `json:"record_count,omitempty"` } // Record is a single resource record inside a zone. Name is stored relative to // the zone apex ("@" for the apex itself) and Data holds the rdata in the usual // zone-file presentation format. type Record struct { ID int64 `json:"id"` ZoneID int64 `json:"zone_id"` Name string `json:"name"` Type string `json:"type"` Data string `json:"data"` TTL *uint32 `json:"ttl"` // nil means "inherit the zone default" Enabled bool `json:"enabled"` Comment string `json:"comment"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` // ZoneName is filled in by cross-zone queries for display purposes. ZoneName string `json:"zone_name,omitempty"` } // EffectiveTTL resolves the record TTL against the zone default. func (r Record) EffectiveTTL(zoneDefault uint32) uint32 { if r.TTL != nil { return *r.TTL } return zoneDefault } // BlockAction is what a policy does with a query that matched a blacklist. type BlockAction string const ( BlockNXDOMAIN BlockAction = "nxdomain" BlockRefused BlockAction = "refused" BlockSinkhole BlockAction = "sinkhole" ) // Valid reports whether a is a supported block action. func (a BlockAction) Valid() bool { switch a { case BlockNXDOMAIN, BlockRefused, BlockSinkhole: return true } return false } // Label returns a display name for the block action. func (a BlockAction) Label() string { switch a { case BlockRefused: return "REFUSED" case BlockSinkhole: return "Sinkhole" default: return "NXDOMAIN" } } // Network is a client subnet that policies can be attached to. type Network struct { ID int64 `json:"id"` Name string `json:"name"` CIDR string `json:"cidr"` Description string `json:"description"` Enabled bool `json:"enabled"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` Policies []Policy `json:"policies,omitempty"` } // Policy groups a set of blacklists and allowlists with a block action. type Policy struct { ID int64 `json:"id"` Name string `json:"name"` Description string `json:"description"` Enabled bool `json:"enabled"` BlockAction BlockAction `json:"block_action"` SinkholeIPv4 string `json:"sinkhole_ipv4"` SinkholeIPv6 string `json:"sinkhole_ipv6"` BlockTTL uint32 `json:"block_ttl"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` BlacklistIDs []int64 `json:"blacklist_ids,omitempty"` AllowlistIDs []int64 `json:"allowlist_ids,omitempty"` NetworkCount int `json:"network_count,omitempty"` BlacklistName []string `json:"blacklist_names,omitempty"` AllowlistName []string `json:"allowlist_names,omitempty"` } // DomainList is either a blacklist or an allowlist; the Kind field says which. type DomainList struct { ID int64 `json:"id"` Kind string `json:"kind"` // "blacklist" or "allowlist" Name string `json:"name"` Description string `json:"description"` Enabled bool `json:"enabled"` SourceURL string `json:"source_url"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DomainCount int `json:"domain_count"` UsedBy []string `json:"used_by,omitempty"` } // List kinds. const ( KindBlacklist = "blacklist" KindAllowlist = "allowlist" ) // DomainEntry is one domain inside a DomainList. type DomainEntry struct { ID int64 `json:"id"` ListID int64 `json:"list_id"` Domain string `json:"domain"` MatchSubdomains bool `json:"match_subdomains"` Enabled bool `json:"enabled"` Comment string `json:"comment"` CreatedAt time.Time `json:"created_at"` } // APIToken is a bearer credential for automation. The secret itself is never // stored: only a prefix (for lookup) and an Argon2id hash. type APIToken struct { ID int64 `json:"id"` Name string `json:"name"` Description string `json:"description"` Prefix string `json:"prefix"` Enabled bool `json:"enabled"` CreatedAt time.Time `json:"created_at"` LastUsedAt *time.Time `json:"last_used_at"` // Secret is populated only in the response that creates the token. Secret string `json:"secret,omitempty"` } // Admin is the single administrator account. type Admin struct { Username string `json:"username"` PasswordHash string `json:"-"` MustChangePassword bool `json:"must_change_password"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` LastLoginAt *time.Time `json:"last_login_at"` } // QueryLogEntry is one logged DNS query. type QueryLogEntry struct { ID int64 `json:"id"` Timestamp time.Time `json:"timestamp"` ClientIP string `json:"client_ip"` NetworkID *int64 `json:"network_id"` NetworkName string `json:"network_name"` QName string `json:"qname"` QType string `json:"qtype"` Rcode string `json:"rcode"` Source string `json:"source"` CacheHit bool `json:"cache_hit"` Blocked bool `json:"blocked"` PolicyID *int64 `json:"policy_id"` PolicyName string `json:"policy_name"` BlacklistID *int64 `json:"blacklist_id"` BlacklistName string `json:"blacklist_name"` MatchedRule string `json:"matched_rule"` Protocol string `json:"protocol"` DurationUS int64 `json:"duration_us"` AnswerCount int `json:"answer_count"` } // DurationMS renders the query duration in milliseconds for the UI. func (q QueryLogEntry) DurationMS() float64 { return float64(q.DurationUS) / 1000.0 } // Answer source values recorded in the query log and exported in metrics. const ( SourceAuthoritative = "authoritative" SourceCache = "cache" SourceStale = "stale" SourceRecursive = "recursive" SourceBlocked = "blocked" SourceRefused = "refused" SourceRateLimited = "ratelimited" SourceError = "error" SourceLocal = "local" ) // AuditEntry records one administrative change. type AuditEntry struct { ID int64 `json:"id"` Timestamp time.Time `json:"timestamp"` Actor string `json:"actor"` Source string `json:"source"` // web | api | cli | system ClientIP string `json:"client_ip"` Action string `json:"action"` ObjectType string `json:"object_type"` ObjectID string `json:"object_id"` ObjectName string `json:"object_name"` Details string `json:"details"` } // ImportSummary reports the outcome of a bulk domain import. type ImportSummary struct { LinesProcessed int `json:"lines_processed"` Imported int `json:"imported"` Duplicates int `json:"duplicates"` Invalid int `json:"invalid"` Ignored int `json:"ignored"` // comments and blank lines InvalidSamples []string `json:"invalid_samples,omitempty"` }