Files
edgeguard-native/internal/services/dns/dns.go
Debian 1d06b28064 feat: HA-Cluster v1.2.x — Split-Brain, TOTP, Enterprise-FW, Drift-Fix, VIP-Recovery
- keepalived: pg_role='standby' hat Vorrang vor role für BACKUP-Bestimmung
- keepalived-master.sh: gecrasht Dienste beim MASTER-Übergang starten (nicht nur reload)
- confighash: ip_addresses per Interface-Name hashen statt per FK (Cross-Node-Drift-Fix)
- TOTP/2FA: RFC 6238 — Setup-Flow, QR-Code, Admin-Disable; two-step Login
- Firewall-UI: Enterprise-Design — auto-Beschreibung, icon-only Actions, zero-hit Indikator
- fe80-Filter: Link-local IPv6 aus NTP/DNS Listen-Dropdowns entfernen
- VIP-Dashboard, Dual-Path VRRP, GW-Tracking (Migrations 0033/0034)
- Forward Proxy + DNS erweiterte Einstellungen (Migrations 0031/0032)
- unbound-control: edgeguard in unbound-Gruppe via postinst

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 18:18:31 +02:00

271 lines
8.3 KiB
Go

// Package dns provides CRUD against dns_zones, dns_records and the
// single-row dns_settings table. Renderer in internal/unbound consumes
// the same data to emit /etc/edgeguard/unbound/edgeguard.conf.
package dns
import (
"context"
"errors"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
)
var (
ErrZoneNotFound = errors.New("dns zone not found")
ErrRecordNotFound = errors.New("dns record not found")
)
type Repo struct {
Pool *pgxpool.Pool
}
func New(pool *pgxpool.Pool) *Repo { return &Repo{Pool: pool} }
// ── Zones ──────────────────────────────────────────────────────
const zoneSelect = `
SELECT id, name, zone_type, description, managed_by, forward_to, active,
created_at, updated_at
FROM dns_zones
`
func (r *Repo) ListZones(ctx context.Context) ([]models.DNSZone, error) {
rows, err := r.Pool.Query(ctx, zoneSelect+" ORDER BY name")
if err != nil {
return nil, err
}
defer rows.Close()
out := make([]models.DNSZone, 0, 8)
for rows.Next() {
z, err := scanZone(rows)
if err != nil {
return nil, err
}
out = append(out, *z)
}
return out, rows.Err()
}
func (r *Repo) GetZone(ctx context.Context, id int64) (*models.DNSZone, error) {
row := r.Pool.QueryRow(ctx, zoneSelect+" WHERE id = $1", id)
z, err := scanZone(row)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrZoneNotFound
}
return nil, err
}
return z, nil
}
func (r *Repo) CreateZone(ctx context.Context, z models.DNSZone) (*models.DNSZone, error) {
if z.ManagedBy == "" {
z.ManagedBy = "user"
}
row := r.Pool.QueryRow(ctx, `
INSERT INTO dns_zones (name, zone_type, description, managed_by, forward_to, active)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, name, zone_type, description, managed_by, forward_to, active,
created_at, updated_at`,
z.Name, z.ZoneType, z.Description, z.ManagedBy, z.ForwardTo, z.Active)
return scanZone(row)
}
func (r *Repo) UpdateZone(ctx context.Context, id int64, z models.DNSZone) (*models.DNSZone, error) {
row := r.Pool.QueryRow(ctx, `
UPDATE dns_zones SET name=$1, zone_type=$2, description=$3, forward_to=$4, active=$5,
updated_at=NOW()
WHERE id=$6
RETURNING id, name, zone_type, description, managed_by, forward_to, active,
created_at, updated_at`,
z.Name, z.ZoneType, z.Description, z.ForwardTo, z.Active, id)
out, err := scanZone(row)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrZoneNotFound
}
return nil, err
}
return out, nil
}
func (r *Repo) DeleteZone(ctx context.Context, id int64) error {
tag, err := r.Pool.Exec(ctx, `DELETE FROM dns_zones WHERE id=$1`, id)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return ErrZoneNotFound
}
return nil
}
// ── Records ────────────────────────────────────────────────────
const recordSelect = `
SELECT id, zone_id, name, record_type, value, ttl, active,
created_at, updated_at
FROM dns_records
`
func (r *Repo) ListRecordsForZone(ctx context.Context, zoneID int64) ([]models.DNSRecord, error) {
rows, err := r.Pool.Query(ctx, recordSelect+" WHERE zone_id=$1 ORDER BY name, record_type", zoneID)
if err != nil {
return nil, err
}
defer rows.Close()
out := make([]models.DNSRecord, 0, 8)
for rows.Next() {
rec, err := scanRecord(rows)
if err != nil {
return nil, err
}
out = append(out, *rec)
}
return out, rows.Err()
}
func (r *Repo) ListAllRecords(ctx context.Context) ([]models.DNSRecord, error) {
rows, err := r.Pool.Query(ctx, recordSelect+" ORDER BY zone_id, name")
if err != nil {
return nil, err
}
defer rows.Close()
out := make([]models.DNSRecord, 0, 16)
for rows.Next() {
rec, err := scanRecord(rows)
if err != nil {
return nil, err
}
out = append(out, *rec)
}
return out, rows.Err()
}
func (r *Repo) GetRecord(ctx context.Context, id int64) (*models.DNSRecord, error) {
row := r.Pool.QueryRow(ctx, recordSelect+" WHERE id=$1", id)
rec, err := scanRecord(row)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrRecordNotFound
}
return nil, err
}
return rec, nil
}
func (r *Repo) CreateRecord(ctx context.Context, rec models.DNSRecord) (*models.DNSRecord, error) {
if rec.TTL == 0 {
rec.TTL = 300
}
row := r.Pool.QueryRow(ctx, `
INSERT INTO dns_records (zone_id, name, record_type, value, ttl, active)
VALUES ($1,$2,$3,$4,$5,$6)
RETURNING id, zone_id, name, record_type, value, ttl, active,
created_at, updated_at`,
rec.ZoneID, rec.Name, rec.RecordType, rec.Value, rec.TTL, rec.Active)
return scanRecord(row)
}
func (r *Repo) UpdateRecord(ctx context.Context, id int64, rec models.DNSRecord) (*models.DNSRecord, error) {
row := r.Pool.QueryRow(ctx, `
UPDATE dns_records SET name=$1, record_type=$2, value=$3, ttl=$4, active=$5,
updated_at=NOW()
WHERE id=$6
RETURNING id, zone_id, name, record_type, value, ttl, active,
created_at, updated_at`,
rec.Name, rec.RecordType, rec.Value, rec.TTL, rec.Active, id)
out, err := scanRecord(row)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrRecordNotFound
}
return nil, err
}
return out, nil
}
func (r *Repo) DeleteRecord(ctx context.Context, id int64) error {
tag, err := r.Pool.Exec(ctx, `DELETE FROM dns_records WHERE id=$1`, id)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return ErrRecordNotFound
}
return nil
}
// ── Settings (single row, id=1) ────────────────────────────────
func (r *Repo) GetSettings(ctx context.Context) (*models.DNSSettings, error) {
row := r.Pool.QueryRow(ctx, `
SELECT id, listen_addresses, listen_port, upstream_forwards, access_acl,
dnssec, qname_minimisation, cache_min_ttl, cache_max_ttl,
prefetch, serve_expired, msg_cache_size_mb, rrset_cache_size_mb,
updated_at
FROM dns_settings WHERE id=1`)
var s models.DNSSettings
if err := row.Scan(&s.ID, &s.ListenAddresses, &s.ListenPort, &s.UpstreamForwards,
&s.AccessACL, &s.DNSSEC, &s.QNameMinimisation,
&s.CacheMinTTL, &s.CacheMaxTTL,
&s.Prefetch, &s.ServeExpired, &s.MsgCacheSizeMB, &s.RRSetCacheSizeMB,
&s.UpdatedAt); err != nil {
return nil, err
}
return &s, nil
}
func (r *Repo) UpdateSettings(ctx context.Context, s models.DNSSettings) (*models.DNSSettings, error) {
row := r.Pool.QueryRow(ctx, `
UPDATE dns_settings SET
listen_addresses=$1, listen_port=$2, upstream_forwards=$3, access_acl=$4,
dnssec=$5, qname_minimisation=$6, cache_min_ttl=$7, cache_max_ttl=$8,
prefetch=$9, serve_expired=$10, msg_cache_size_mb=$11, rrset_cache_size_mb=$12,
updated_at=NOW()
WHERE id=1
RETURNING id, listen_addresses, listen_port, upstream_forwards, access_acl,
dnssec, qname_minimisation, cache_min_ttl, cache_max_ttl,
prefetch, serve_expired, msg_cache_size_mb, rrset_cache_size_mb,
updated_at`,
s.ListenAddresses, s.ListenPort, s.UpstreamForwards, s.AccessACL,
s.DNSSEC, s.QNameMinimisation, s.CacheMinTTL, s.CacheMaxTTL,
s.Prefetch, s.ServeExpired, s.MsgCacheSizeMB, s.RRSetCacheSizeMB)
var out models.DNSSettings
if err := row.Scan(&out.ID, &out.ListenAddresses, &out.ListenPort, &out.UpstreamForwards,
&out.AccessACL, &out.DNSSEC, &out.QNameMinimisation,
&out.CacheMinTTL, &out.CacheMaxTTL,
&out.Prefetch, &out.ServeExpired, &out.MsgCacheSizeMB, &out.RRSetCacheSizeMB,
&out.UpdatedAt); err != nil {
return nil, err
}
return &out, nil
}
// ── scan helpers ───────────────────────────────────────────────
func scanZone(row interface{ Scan(...any) error }) (*models.DNSZone, error) {
var z models.DNSZone
if err := row.Scan(
&z.ID, &z.Name, &z.ZoneType, &z.Description, &z.ManagedBy,
&z.ForwardTo, &z.Active, &z.CreatedAt, &z.UpdatedAt,
); err != nil {
return nil, err
}
return &z, nil
}
func scanRecord(row interface{ Scan(...any) error }) (*models.DNSRecord, error) {
var rec models.DNSRecord
if err := row.Scan(
&rec.ID, &rec.ZoneID, &rec.Name, &rec.RecordType, &rec.Value, &rec.TTL,
&rec.Active, &rec.CreatedAt, &rec.UpdatedAt,
); err != nil {
return nil, err
}
return &rec, nil
}