- 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>
271 lines
8.3 KiB
Go
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
|
|
}
|