diff --git a/VERSION b/VERSION index 9a03ea6..ead65f0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.93 +1.1.94 diff --git a/cmd/edgeguard-api/main.go b/cmd/edgeguard-api/main.go index 9801958..6aeb8d8 100644 --- a/cmd/edgeguard-api/main.go +++ b/cmd/edgeguard-api/main.go @@ -60,7 +60,7 @@ import ( usersvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/users" ) -var version = "1.1.93" +var version = "1.1.94" func main() { addr := os.Getenv("EDGEGUARD_API_ADDR") diff --git a/cmd/edgeguard-ctl/main.go b/cmd/edgeguard-ctl/main.go index ec313c3..2f4764e 100644 --- a/cmd/edgeguard-ctl/main.go +++ b/cmd/edgeguard-ctl/main.go @@ -11,7 +11,7 @@ import ( "git.netcell-it.de/projekte/edgeguard-native/internal/services/setup" ) -var version = "1.1.93" +var version = "1.1.94" const usage = `edgeguard-ctl — EdgeGuard CLI diff --git a/cmd/edgeguard-scheduler/main.go b/cmd/edgeguard-scheduler/main.go index b9561c9..2de08aa 100644 --- a/cmd/edgeguard-scheduler/main.go +++ b/cmd/edgeguard-scheduler/main.go @@ -35,7 +35,7 @@ import ( "git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts" ) -var version = "1.1.93" +var version = "1.1.94" const ( // renewTickInterval — how often we re-evaluate expiring certs. diff --git a/internal/handlers/dns.go b/internal/handlers/dns.go index 91bb5a3..bf0a704 100644 --- a/internal/handlers/dns.go +++ b/internal/handlers/dns.go @@ -4,6 +4,7 @@ import ( "context" "errors" "log/slog" + "os/exec" "github.com/gin-gonic/gin" @@ -54,6 +55,7 @@ func (h *DNSHandler) Register(rg *gin.RouterGroup) { g.GET("/settings", h.GetSettings) g.PUT("/settings", h.UpdateSettings) + g.POST("/flush-cache", h.FlushCache) } // ── Zones ────────────────────────────────────────────────────── @@ -287,6 +289,20 @@ func (h *DNSHandler) UpdateSettings(c *gin.Context) { h.reload(c.Request.Context(), "settings.update") } +// FlushCache runs `unbound-control flush_zone .` which discards all +// cached RRs from the resolver. Useful after DNS propagation or when +// stale records need to be evicted immediately. +func (h *DNSHandler) FlushCache(c *gin.Context) { + out, err := exec.CommandContext(c.Request.Context(), "unbound-control", "flush_zone", ".").CombinedOutput() + if err != nil { + slog.Error("dns flush-cache failed", "err", err, "out", string(out)) + response.Internal(c, err) + return + } + _ = h.Audit.Log(c.Request.Context(), actorOf(c), "dns.flush-cache", "unbound", nil, h.NodeID) + response.OK(c, gin.H{"message": "cache flushed", "output": string(out)}) +} + // ── Validation ───────────────────────────────────────────────── func validateZone(z *models.DNSZone) error { diff --git a/internal/handlers/ntp.go b/internal/handlers/ntp.go index 95f9c66..4aa71ee 100644 --- a/internal/handlers/ntp.go +++ b/internal/handlers/ntp.go @@ -41,6 +41,7 @@ func (h *NTPHandler) Register(rg *gin.RouterGroup) { g.GET("/settings", h.GetSettings) g.PUT("/settings", h.UpdateSettings) g.GET("/status", h.Status) + g.POST("/force-sync", h.ForceSync) p := g.Group("/pools") p.GET("", h.ListPools) @@ -234,6 +235,21 @@ func (h *NTPHandler) DeletePool(c *gin.Context) { h.reload(c.Request.Context(), "pool.delete") } +// ForceSync runs `chronyc makestep` which immediately adjusts the +// system clock to the current NTP reference. Useful after a long +// outage or VM migration where the clock has drifted by more than +// the 1ms default slew threshold. +func (h *NTPHandler) ForceSync(c *gin.Context) { + out, err := exec.Command("chronyc", "makestep").CombinedOutput() + if err != nil { + slog.Error("ntp force-sync failed", "err", err, "out", string(out)) + response.Internal(c, err) + return + } + _ = h.Audit.Log(c.Request.Context(), actorOf(c), "ntp.force-sync", "chrony", nil, h.NodeID) + response.OK(c, gin.H{"message": "clock stepped", "output": string(out)}) +} + func validateNTPPool(p *models.NTPPool) error { if p.Address == "" { return errors.New("address required") diff --git a/management-ui/src/i18n/locales/de/common.json b/management-ui/src/i18n/locales/de/common.json index 59a4ea2..f5085d9 100644 --- a/management-ui/src/i18n/locales/de/common.json +++ b/management-ui/src/i18n/locales/de/common.json @@ -838,7 +838,11 @@ "rtcsync": "RTC mit System-Time syncen", "rtcsyncExtra": "Hardware-Clock alle 11 min synchron halten — nach Reboot ist die Zeit grob korrekt.", "leapsectz": "Leap-Sec TZ", - "leapsectzExtra": "Optional, z.B. 'right/UTC' für leap-sec über tzdata." + "leapsectzExtra": "Optional, z.B. 'right/UTC' für leap-sec über tzdata.", + "forceSyncBtn": "Uhr sofort stellen", + "forceSyncTooltip": "System-Uhr sofort auf den NTP-Referenzwert setzen (chronyc makestep). Nützlich nach VM-Migration oder längerem Ausfall.", + "forceSyncOk": "Uhr erfolgreich gestellt", + "forceSyncFailed": "Force-Sync fehlgeschlagen" } }, "dns": { @@ -892,7 +896,11 @@ "cacheMax": "Cache max-TTL", "allIPv4": "alle IPv4-Interfaces", "allIPv6": "alle IPv6-Interfaces", - "loopback": "Loopback" + "loopback": "Loopback", + "flushCacheBtn": "DNS-Cache leeren", + "flushCacheTooltip": "Alle gecachten Einträge verwerfen (unbound-control flush_zone .). Verwenden wenn DNS-Änderungen sofort greifen sollen.", + "flushCacheOk": "DNS-Cache geleert", + "flushCacheFailed": "Cache-Flush fehlgeschlagen" } }, "fwd": { diff --git a/management-ui/src/i18n/locales/en/common.json b/management-ui/src/i18n/locales/en/common.json index 11144e6..d1a77bd 100644 --- a/management-ui/src/i18n/locales/en/common.json +++ b/management-ui/src/i18n/locales/en/common.json @@ -838,7 +838,11 @@ "rtcsync": "Sync RTC with system time", "rtcsyncExtra": "Keep hardware clock in sync every 11 min — after reboot time is roughly correct.", "leapsectz": "Leap-sec TZ", - "leapsectzExtra": "Optional, e.g. 'right/UTC' for leap-sec via tzdata." + "leapsectzExtra": "Optional, e.g. 'right/UTC' for leap-sec via tzdata.", + "forceSyncBtn": "Force clock step", + "forceSyncTooltip": "Immediately step the system clock to the NTP reference (chronyc makestep). Use after VM migration or long outage.", + "forceSyncOk": "Clock stepped successfully", + "forceSyncFailed": "Force sync failed" } }, "dns": { @@ -892,7 +896,11 @@ "cacheMax": "Cache max-TTL", "allIPv4": "all IPv4 interfaces", "allIPv6": "all IPv6 interfaces", - "loopback": "Loopback" + "loopback": "Loopback", + "flushCacheBtn": "Flush DNS cache", + "flushCacheTooltip": "Discard all cached records (unbound-control flush_zone .). Use after DNS changes have propagated.", + "flushCacheOk": "DNS cache flushed", + "flushCacheFailed": "Flush failed" } }, "fwd": { diff --git a/management-ui/src/pages/DNS/index.tsx b/management-ui/src/pages/DNS/index.tsx index 669379b..116e689 100644 --- a/management-ui/src/pages/DNS/index.tsx +++ b/management-ui/src/pages/DNS/index.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' import { Alert, Button, Drawer, Form, Input, InputNumber, Modal, Select, Space, Switch, Tabs, Tag, Tooltip, Typography, message } from 'antd' import type { ColumnsType } from 'antd/es/table' -import { CheckCircleOutlined, CloseCircleOutlined, GlobalOutlined, NodeIndexOutlined, PlusOutlined, SettingOutlined } from '@ant-design/icons' +import { CheckCircleOutlined, ClearOutlined, CloseCircleOutlined, GlobalOutlined, NodeIndexOutlined, PlusOutlined, SettingOutlined } from '@ant-design/icons' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' @@ -490,6 +490,12 @@ function SettingsTab() { onError: (e: Error) => message.error(e.message), }) + const flushCache = useMutation({ + mutationFn: async () => { await apiClient.post('/dns/flush-cache') }, + onSuccess: () => message.success(t('dns.settings.flushCacheOk')), + onError: (e: Error) => message.error(t('dns.settings.flushCacheFailed') + ': ' + e.message), + }) + if (isLoading) return null return (