feat: add secure device DNS backend
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"rmm-openwrt/server/internal/model"
|
||||
)
|
||||
|
||||
const dnsRecordSelect = `
|
||||
SELECT r.device_id, d.dns_label, r.ipv4, r.ipv6, r.ttl, r.enabled,
|
||||
r.last_agent_update_at, r.created_at, r.updated_at
|
||||
FROM device_dns_records r
|
||||
JOIN devices d ON d.id = r.device_id
|
||||
`
|
||||
|
||||
func (s *Store) ListDNSRecordsForUser(ctx context.Context, userID string, admin bool) ([]model.DNSRecord, error) {
|
||||
query := dnsRecordSelect + ` ORDER BY d.created_at DESC`
|
||||
args := []any{}
|
||||
if !admin {
|
||||
query = dnsRecordSelect + ` WHERE d.owner_user_id = ? ORDER BY d.created_at DESC`
|
||||
args = append(args, userID)
|
||||
}
|
||||
rows, err := s.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
records := make([]model.DNSRecord, 0)
|
||||
for rows.Next() {
|
||||
record, err := scanDNSRecord(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
records = append(records, record)
|
||||
}
|
||||
return records, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) ListPublishableDNSRecords(ctx context.Context) ([]model.DNSRecord, error) {
|
||||
rows, err := s.db.QueryContext(ctx, dnsRecordSelect+`
|
||||
WHERE r.enabled = 1 AND d.dns_label != '' AND (r.ipv4 != '' OR r.ipv6 != '')
|
||||
ORDER BY d.dns_label
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
records := make([]model.DNSRecord, 0)
|
||||
for rows.Next() {
|
||||
record, err := scanDNSRecord(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
records = append(records, record)
|
||||
}
|
||||
return records, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) GetDNSRecord(ctx context.Context, deviceID string) (model.DNSRecord, bool, error) {
|
||||
record, err := scanDNSRecord(s.db.QueryRowContext(ctx, dnsRecordSelect+` WHERE r.device_id = ?`, deviceID))
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return model.DNSRecord{}, false, nil
|
||||
}
|
||||
return record, err == nil, err
|
||||
}
|
||||
|
||||
func (s *Store) UpdateDNSRecordSettings(ctx context.Context, deviceID string, dnsLabel *string, ttl *int, enabled *bool) (model.DNSRecord, bool, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return model.DNSRecord{}, false, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
var currentLabel string
|
||||
if err := tx.QueryRowContext(ctx, `SELECT dns_label FROM devices WHERE id = ?`, deviceID).Scan(¤tLabel); errors.Is(err, sql.ErrNoRows) {
|
||||
return model.DNSRecord{}, false, nil
|
||||
} else if err != nil {
|
||||
return model.DNSRecord{}, false, err
|
||||
}
|
||||
if dnsLabel != nil && *dnsLabel != currentLabel {
|
||||
var reserved bool
|
||||
if err := tx.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM enrollment_grants WHERE dns_label = ? AND used_at IS NULL AND julianday(expires_at) > julianday(?))`, *dnsLabel, nowText()).Scan(&reserved); err != nil {
|
||||
return model.DNSRecord{}, false, err
|
||||
}
|
||||
if reserved {
|
||||
return model.DNSRecord{}, false, errors.New("dns label is reserved by an active enrollment grant")
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `UPDATE devices SET dns_label = ? WHERE id = ?`, *dnsLabel, deviceID); err != nil {
|
||||
return model.DNSRecord{}, false, err
|
||||
}
|
||||
}
|
||||
now := nowText()
|
||||
if _, err := tx.ExecContext(ctx, `INSERT OR IGNORE INTO device_dns_records (device_id, created_at, updated_at) VALUES (?, ?, ?)`, deviceID, now, now); err != nil {
|
||||
return model.DNSRecord{}, false, err
|
||||
}
|
||||
if ttl != nil {
|
||||
if _, err := tx.ExecContext(ctx, `UPDATE device_dns_records SET ttl = ?, updated_at = ? WHERE device_id = ?`, *ttl, now, deviceID); err != nil {
|
||||
return model.DNSRecord{}, false, err
|
||||
}
|
||||
}
|
||||
if enabled != nil {
|
||||
if _, err := tx.ExecContext(ctx, `UPDATE device_dns_records SET enabled = ?, updated_at = ? WHERE device_id = ?`, *enabled, now, deviceID); err != nil {
|
||||
return model.DNSRecord{}, false, err
|
||||
}
|
||||
}
|
||||
if dnsLabel != nil && ttl == nil && enabled == nil {
|
||||
if _, err := tx.ExecContext(ctx, `UPDATE device_dns_records SET updated_at = ? WHERE device_id = ?`, now, deviceID); err != nil {
|
||||
return model.DNSRecord{}, false, err
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return model.DNSRecord{}, false, err
|
||||
}
|
||||
return s.GetDNSRecord(ctx, deviceID)
|
||||
}
|
||||
|
||||
func (s *Store) UpdateDNSAddresses(ctx context.Context, deviceID, ipv4, ipv6, source string) (model.DNSRecord, bool, bool, error) {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return model.DNSRecord{}, false, false, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
var exists bool
|
||||
if err := tx.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM devices WHERE id = ?)`, deviceID).Scan(&exists); err != nil {
|
||||
return model.DNSRecord{}, false, false, err
|
||||
}
|
||||
if !exists {
|
||||
return model.DNSRecord{}, false, false, nil
|
||||
}
|
||||
now := nowText()
|
||||
if _, err := tx.ExecContext(ctx, `INSERT OR IGNORE INTO device_dns_records (device_id, created_at, updated_at) VALUES (?, ?, ?)`, deviceID, now, now); err != nil {
|
||||
return model.DNSRecord{}, false, false, err
|
||||
}
|
||||
var currentIPv4, currentIPv6 string
|
||||
if err := tx.QueryRowContext(ctx, `SELECT ipv4, ipv6 FROM device_dns_records WHERE device_id = ?`, deviceID).Scan(¤tIPv4, ¤tIPv6); err != nil {
|
||||
return model.DNSRecord{}, false, false, err
|
||||
}
|
||||
changed := currentIPv4 != ipv4 || currentIPv6 != ipv6
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
UPDATE device_dns_records
|
||||
SET ipv4 = ?, ipv6 = ?, last_agent_update_at = ?, updated_at = ?
|
||||
WHERE device_id = ?
|
||||
`, ipv4, ipv6, now, now, deviceID); err != nil {
|
||||
return model.DNSRecord{}, false, false, err
|
||||
}
|
||||
if changed {
|
||||
historyID, err := randomID("dns")
|
||||
if err != nil {
|
||||
return model.DNSRecord{}, false, false, err
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `INSERT INTO dns_address_history (id, device_id, ipv4, ipv6, source, created_at) VALUES (?, ?, ?, ?, ?, ?)`, historyID, deviceID, ipv4, ipv6, source, now); err != nil {
|
||||
return model.DNSRecord{}, false, false, err
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
DELETE FROM dns_address_history
|
||||
WHERE device_id = ? AND id IN (
|
||||
SELECT id FROM dns_address_history WHERE device_id = ? ORDER BY created_at DESC LIMIT -1 OFFSET 100
|
||||
)
|
||||
`, deviceID, deviceID); err != nil {
|
||||
return model.DNSRecord{}, false, false, err
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return model.DNSRecord{}, false, false, err
|
||||
}
|
||||
record, found, err := s.GetDNSRecord(ctx, deviceID)
|
||||
return record, found, changed, err
|
||||
}
|
||||
|
||||
func (s *Store) ListDNSAddressHistory(ctx context.Context, deviceID string, limit int) ([]model.DNSAddressHistory, bool, error) {
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 25
|
||||
}
|
||||
var exists bool
|
||||
if err := s.db.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM devices WHERE id = ?)`, deviceID).Scan(&exists); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if !exists {
|
||||
return nil, false, nil
|
||||
}
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
SELECT id, device_id, ipv4, ipv6, source, created_at
|
||||
FROM dns_address_history
|
||||
WHERE device_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?
|
||||
`, deviceID, limit)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
defer rows.Close()
|
||||
history := make([]model.DNSAddressHistory, 0)
|
||||
for rows.Next() {
|
||||
var item model.DNSAddressHistory
|
||||
var createdAt string
|
||||
if err := rows.Scan(&item.ID, &item.DeviceID, &item.IPv4, &item.IPv6, &item.Source, &createdAt); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
item.CreatedAt = parseTime(createdAt)
|
||||
history = append(history, item)
|
||||
}
|
||||
return history, true, rows.Err()
|
||||
}
|
||||
|
||||
func scanDNSRecord(s scanner) (model.DNSRecord, error) {
|
||||
var record model.DNSRecord
|
||||
var enabled bool
|
||||
var lastAgentUpdate sql.NullString
|
||||
var createdAt, updatedAt string
|
||||
if err := s.Scan(&record.DeviceID, &record.DNSLabel, &record.IPv4, &record.IPv6, &record.TTL, &enabled, &lastAgentUpdate, &createdAt, &updatedAt); err != nil {
|
||||
return record, err
|
||||
}
|
||||
record.Enabled = enabled
|
||||
record.CreatedAt = parseTime(createdAt)
|
||||
record.UpdatedAt = parseTime(updatedAt)
|
||||
if lastAgentUpdate.Valid {
|
||||
value := parseTime(lastAgentUpdate.String)
|
||||
record.LastAgentUpdateAt = &value
|
||||
}
|
||||
return record, nil
|
||||
}
|
||||
Reference in New Issue
Block a user