diff --git a/VERSION b/VERSION index 439ba98..b30a005 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.56 +1.1.57 diff --git a/internal/handlers/system.go b/internal/handlers/system.go index a3606f7..ffbf582 100644 --- a/internal/handlers/system.go +++ b/internal/handlers/system.go @@ -85,6 +85,8 @@ func (h *SystemHandler) Register(rg *gin.RouterGroup) { g.POST("/render-configs", h.RenderConfigs) g.POST("/service-restart", h.ServiceRestart) g.GET("/upgrade-status", h.UpgradeStatus) + g.GET("/ipv6", h.IPv6) + g.POST("/ipv6", h.SetIPv6) } // RegisterAgent mountet die read-only System-Endpoints auf der mTLS- @@ -413,6 +415,42 @@ type dbSizeResponse struct { Tables []dbSizeTable `json:"top_tables"` } +func (h *SystemHandler) IPv6(c *gin.Context) { + enabled := false + if h.Setup != nil { + if st, err := h.Setup.Load(); err == nil && st != nil { + enabled = st.IPv6Enabled + } + } + response.OK(c, gin.H{"enabled": enabled}) +} + +func (h *SystemHandler) SetIPv6(c *gin.Context) { + if h.Setup == nil { + response.Err(c, http.StatusServiceUnavailable, simpleErr("setup not initialised")) + return + } + var req struct { + Enabled bool `json:"enabled"` + } + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, err) + return + } + if err := h.Setup.SetIPv6Enabled(req.Enabled); err != nil { + response.BadRequest(c, err) + return + } + if h.HAProxyReloader != nil { + _ = h.HAProxyReloader(c.Request.Context()) + } + if h.Audit != nil { + _ = h.Audit.Log(c.Request.Context(), actorOf(c), "system.ipv6", + "", gin.H{"enabled": req.Enabled}, h.NodeID) + } + response.OK(c, gin.H{"enabled": req.Enabled}) +} + func (h *SystemHandler) DBSize(c *gin.Context) { if h.Pool == nil { response.Err(c, http.StatusServiceUnavailable, simpleErr("db pool unavailable")) diff --git a/internal/haproxy/haproxy.cfg.tpl b/internal/haproxy/haproxy.cfg.tpl index 74564fa..f3fd3fd 100644 --- a/internal/haproxy/haproxy.cfg.tpl +++ b/internal/haproxy/haproxy.cfg.tpl @@ -35,6 +35,9 @@ defaults # Everything else redirects to HTTPS. frontend public_http bind :80 + {{- if .IPv6Enabled}} + bind [::]:80 + {{- end}} acl is_acme path_beg /.well-known/acme-challenge/ @@ -59,6 +62,10 @@ frontend public_https # gebaut; Browser fallen via Alt-Svc-Header (siehe unten) für # Folge-Requests auf h3 zurück. bind quic4@:443 ssl crt /etc/edgeguard/tls/ alpn h3 + {{- if .IPv6Enabled}} + bind [::]:443 ssl crt /etc/edgeguard/tls/ alpn h2,http/1.1 + bind quic6@:443 ssl crt /etc/edgeguard/tls/ alpn h3 + {{- end}} # Alt-Svc: signalisiert dass h3 auf demselben Port verfügbar ist. # ma=86400 = Browser darf den Hinweis 24h cachen. @@ -134,6 +141,9 @@ frontend public_https # (Hostname egal — default_backend), inkl. der direkten IP. frontend mgmt_https bind :3443 ssl crt /etc/edgeguard/tls/ alpn h2,http/1.1 + {{- if .IPv6Enabled}} + bind [::]:3443 ssl crt /etc/edgeguard/tls/ alpn h2,http/1.1 + {{- end}} http-response set-header Strict-Transport-Security "max-age=31536000" http-request set-header X-Forwarded-Proto https http-request set-header X-Real-IP %[src] diff --git a/internal/haproxy/haproxy.go b/internal/haproxy/haproxy.go index 5cf7d1f..f360830 100644 --- a/internal/haproxy/haproxy.go +++ b/internal/haproxy/haproxy.go @@ -128,6 +128,10 @@ type View struct { // OS-Upgrades / Wartungsfenster. GlobalMaintenance bool GlobalMaintenanceMessage string + + // IPv6Enabled: wenn true fügt das Template zusätzliche + // bind-Direktiven für [::]:80, [::]:443 und [::]:3443 hinzu. + IPv6Enabled bool } type DomainView struct { @@ -270,6 +274,7 @@ func (g *Generator) loadView(ctx context.Context) (*View, error) { if v.GlobalMaintenanceMessage == "" && v.GlobalMaintenance { v.GlobalMaintenanceMessage = "EdgeGuard maintenance in progress." } + v.IPv6Enabled = st.IPv6Enabled } } return v, nil diff --git a/internal/services/setup/setup.go b/internal/services/setup/setup.go index 0b74566..0e63e2e 100644 --- a/internal/services/setup/setup.go +++ b/internal/services/setup/setup.go @@ -57,6 +57,11 @@ type State struct { // Compliance (z. B. SOX = 7 Jahre = 2555) oder reduzieren falls // /var-Disk-Druck. AuditRetentionDays int `json:"audit_retention_days,omitempty"` + + // IPv6Enabled: wenn true bindet HAProxy zusätzlich auf [::]:80, + // [::]:443 und [::]:3443. Default false weil nicht alle Deployments + // IPv6 haben. Nach Änderung wird HAProxy neu geladen. + IPv6Enabled bool `json:"ipv6_enabled,omitempty"` } // Request is the JSON body POST /api/v1/setup/complete accepts. @@ -192,6 +197,19 @@ func (s *Store) SetAuditRetention(days int) error { return s.Save(prev) } +// SetIPv6Enabled schaltet IPv6-Binds in HAProxy an oder aus. +func (s *Store) SetIPv6Enabled(enabled bool) error { + prev, err := s.Load() + if err != nil { + return err + } + if prev == nil { + return errors.New("setup not completed — cannot edit IPv6 before initial setup") + } + prev.IPv6Enabled = enabled + return s.Save(prev) +} + // SetContactEmails aktualisiert AdminEmail + ACMEEmail. Beide werden // gegen mail.ParseAddress validiert. Lower-case + trim wie beim Setup- // Wizard, damit Login-Vergleich (EqualFold) konsistent bleibt. diff --git a/management-ui/src/i18n/locales/de/common.json b/management-ui/src/i18n/locales/de/common.json index 2f681a4..b05a931 100644 --- a/management-ui/src/i18n/locales/de/common.json +++ b/management-ui/src/i18n/locales/de/common.json @@ -558,6 +558,12 @@ "autoUpdateHint": "Whitelist umfasst nur edgeguard, edgeguard-api, edgeguard-ui. Andere Pakete bleiben unter manueller Kontrolle. Verlangt unattended-upgrades (Distro-Standard auf Trixie). Conf-File: /etc/apt/apt.conf.d/52edgeguard-auto-updates.", "autoUpdateToggled": "Auto-Update-Einstellung gespeichert.", "autoUpdateFailed": "Auto-Update-Toggle fehlgeschlagen", + "ipv6CardTitle": "IPv6", + "ipv6On": "Aktiviert — HAProxy bindet zusätzlich zu IPv4 auf [::]:80, [::]:443 und [::]:3443.", + "ipv6Off": "Deaktiviert — HAProxy lauscht nur auf IPv4.", + "ipv6Hint": "Erfordert IPv6-Konfiguration auf den Netzwerk-Interfaces dieses Servers. HAProxy wird nach der Änderung automatisch neu geladen.", + "ipv6Toggled": "IPv6-Einstellung gespeichert.", + "ipv6Failed": "IPv6-Toggle fehlgeschlagen", "passwordCardTitle": "Admin-Passwort ändern", "currentPassword": "Aktuelles Passwort", "newPassword": "Neues Passwort", diff --git a/management-ui/src/i18n/locales/en/common.json b/management-ui/src/i18n/locales/en/common.json index e20f744..1391083 100644 --- a/management-ui/src/i18n/locales/en/common.json +++ b/management-ui/src/i18n/locales/en/common.json @@ -558,6 +558,12 @@ "autoUpdateHint": "Whitelist covers edgeguard, edgeguard-api, edgeguard-ui only. Other packages stay under manual control. Requires unattended-upgrades (Trixie distro default). Conf file: /etc/apt/apt.conf.d/52edgeguard-auto-updates.", "autoUpdateToggled": "Auto-update setting saved.", "autoUpdateFailed": "Auto-update toggle failed", + "ipv6CardTitle": "IPv6", + "ipv6On": "Enabled — HAProxy binds on [::]:80, [::]:443 and [::]:3443 in addition to IPv4.", + "ipv6Off": "Disabled — HAProxy listens on IPv4 only.", + "ipv6Hint": "Requires IPv6 to be configured on this server's network interfaces. HAProxy reloads automatically after changing this setting.", + "ipv6Toggled": "IPv6 setting saved.", + "ipv6Failed": "IPv6 toggle failed", "passwordCardTitle": "Change admin password", "currentPassword": "Current password", "newPassword": "New password", diff --git a/management-ui/src/pages/Settings/index.tsx b/management-ui/src/pages/Settings/index.tsx index 413eed1..5e341e7 100644 --- a/management-ui/src/pages/Settings/index.tsx +++ b/management-ui/src/pages/Settings/index.tsx @@ -1,5 +1,5 @@ import { Alert, Button, Card, Descriptions, Form, Input, InputNumber, Space, Spin, Switch, Typography, message } from 'antd' -import { CloudDownloadOutlined, CloudSyncOutlined, DatabaseOutlined, ExclamationCircleOutlined, FileSearchOutlined, LockOutlined, MailOutlined, ReloadOutlined, SettingOutlined, StopOutlined, ToolOutlined } from '@ant-design/icons' +import { CloudDownloadOutlined, CloudSyncOutlined, DatabaseOutlined, ExclamationCircleOutlined, FileSearchOutlined, GlobalOutlined, LockOutlined, MailOutlined, ReloadOutlined, SettingOutlined, StopOutlined, ToolOutlined } from '@ant-design/icons' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -239,6 +239,27 @@ export default function SettingsPage() { }, }) + const { data: ipv6 } = useQuery({ + queryKey: ['system', 'ipv6'], + queryFn: async () => { + const r = await apiClient.get('/system/ipv6') + return isEnvelope(r.data) ? (r.data.data as { enabled: boolean }) : { enabled: false } + }, + }) + const toggleIPv6 = useMutation({ + mutationFn: async (enabled: boolean) => { + const r = await apiClient.post('/system/ipv6', { enabled }) + return r.data + }, + onSuccess: () => { + msg.success(t('settings.ipv6Toggled')) + void qc.invalidateQueries({ queryKey: ['system', 'ipv6'] }) + }, + onError: (e: Error) => { + msg.error(t('settings.ipv6Failed') + ': ' + e.message) + }, + }) + const changePassword = useMutation({ mutationFn: async (v: { current_password: string; new_password: string }) => { const r = await apiClient.post('/auth/change-password', v) @@ -606,6 +627,28 @@ export default function SettingsPage() { + {t('settings.ipv6CardTitle')}} + className="mb-12" + size="small" + > + + + toggleIPv6.mutate(checked)} + /> + + {ipv6?.enabled ? t('settings.ipv6On') : t('settings.ipv6Off')} + + + + {t('settings.ipv6Hint')} + + + + {t('settings.passwordCardTitle')}} size="small"> form={pwForm}