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">