Files
vibedns/internal/models/models.go
T
2026-08-16 21:18:45 -05:00

269 lines
8.7 KiB
Go

// 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"`
}