feat(ipv6): IPv6-Support für HAProxy + Settings-Toggle
- setup.State.IPv6Enabled + Store.SetIPv6Enabled() - GET/POST /system/ipv6 im SystemHandler; HAProxy-Reload on save - HAProxy-Template: bind [::]:80, [::]:443, quic6@:443, [::]:3443 werden nur emittiert wenn IPv6Enabled=true - haproxy.View.IPv6Enabled aus SetupStore befüllt - Settings-UI: neues IPv6-Card (zwischen Auto-Update und Passwort) - i18n de+en ergänzt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -85,6 +85,8 @@ func (h *SystemHandler) Register(rg *gin.RouterGroup) {
|
|||||||
g.POST("/render-configs", h.RenderConfigs)
|
g.POST("/render-configs", h.RenderConfigs)
|
||||||
g.POST("/service-restart", h.ServiceRestart)
|
g.POST("/service-restart", h.ServiceRestart)
|
||||||
g.GET("/upgrade-status", h.UpgradeStatus)
|
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-
|
// RegisterAgent mountet die read-only System-Endpoints auf der mTLS-
|
||||||
@@ -413,6 +415,42 @@ type dbSizeResponse struct {
|
|||||||
Tables []dbSizeTable `json:"top_tables"`
|
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) {
|
func (h *SystemHandler) DBSize(c *gin.Context) {
|
||||||
if h.Pool == nil {
|
if h.Pool == nil {
|
||||||
response.Err(c, http.StatusServiceUnavailable, simpleErr("db pool unavailable"))
|
response.Err(c, http.StatusServiceUnavailable, simpleErr("db pool unavailable"))
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ defaults
|
|||||||
# Everything else redirects to HTTPS.
|
# Everything else redirects to HTTPS.
|
||||||
frontend public_http
|
frontend public_http
|
||||||
bind :80
|
bind :80
|
||||||
|
{{- if .IPv6Enabled}}
|
||||||
|
bind [::]:80
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
acl is_acme path_beg /.well-known/acme-challenge/
|
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
|
# gebaut; Browser fallen via Alt-Svc-Header (siehe unten) für
|
||||||
# Folge-Requests auf h3 zurück.
|
# Folge-Requests auf h3 zurück.
|
||||||
bind quic4@:443 ssl crt /etc/edgeguard/tls/ alpn h3
|
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.
|
# Alt-Svc: signalisiert dass h3 auf demselben Port verfügbar ist.
|
||||||
# ma=86400 = Browser darf den Hinweis 24h cachen.
|
# ma=86400 = Browser darf den Hinweis 24h cachen.
|
||||||
@@ -134,6 +141,9 @@ frontend public_https
|
|||||||
# (Hostname egal — default_backend), inkl. der direkten IP.
|
# (Hostname egal — default_backend), inkl. der direkten IP.
|
||||||
frontend mgmt_https
|
frontend mgmt_https
|
||||||
bind :3443 ssl crt /etc/edgeguard/tls/ alpn h2,http/1.1
|
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-response set-header Strict-Transport-Security "max-age=31536000"
|
||||||
http-request set-header X-Forwarded-Proto https
|
http-request set-header X-Forwarded-Proto https
|
||||||
http-request set-header X-Real-IP %[src]
|
http-request set-header X-Real-IP %[src]
|
||||||
|
|||||||
@@ -128,6 +128,10 @@ type View struct {
|
|||||||
// OS-Upgrades / Wartungsfenster.
|
// OS-Upgrades / Wartungsfenster.
|
||||||
GlobalMaintenance bool
|
GlobalMaintenance bool
|
||||||
GlobalMaintenanceMessage string
|
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 {
|
type DomainView struct {
|
||||||
@@ -270,6 +274,7 @@ func (g *Generator) loadView(ctx context.Context) (*View, error) {
|
|||||||
if v.GlobalMaintenanceMessage == "" && v.GlobalMaintenance {
|
if v.GlobalMaintenanceMessage == "" && v.GlobalMaintenance {
|
||||||
v.GlobalMaintenanceMessage = "EdgeGuard maintenance in progress."
|
v.GlobalMaintenanceMessage = "EdgeGuard maintenance in progress."
|
||||||
}
|
}
|
||||||
|
v.IPv6Enabled = st.IPv6Enabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return v, nil
|
return v, nil
|
||||||
|
|||||||
@@ -57,6 +57,11 @@ type State struct {
|
|||||||
// Compliance (z. B. SOX = 7 Jahre = 2555) oder reduzieren falls
|
// Compliance (z. B. SOX = 7 Jahre = 2555) oder reduzieren falls
|
||||||
// /var-Disk-Druck.
|
// /var-Disk-Druck.
|
||||||
AuditRetentionDays int `json:"audit_retention_days,omitempty"`
|
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.
|
// 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)
|
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
|
// SetContactEmails aktualisiert AdminEmail + ACMEEmail. Beide werden
|
||||||
// gegen mail.ParseAddress validiert. Lower-case + trim wie beim Setup-
|
// gegen mail.ParseAddress validiert. Lower-case + trim wie beim Setup-
|
||||||
// Wizard, damit Login-Vergleich (EqualFold) konsistent bleibt.
|
// Wizard, damit Login-Vergleich (EqualFold) konsistent bleibt.
|
||||||
|
|||||||
@@ -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.",
|
"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.",
|
"autoUpdateToggled": "Auto-Update-Einstellung gespeichert.",
|
||||||
"autoUpdateFailed": "Auto-Update-Toggle fehlgeschlagen",
|
"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",
|
"passwordCardTitle": "Admin-Passwort ändern",
|
||||||
"currentPassword": "Aktuelles Passwort",
|
"currentPassword": "Aktuelles Passwort",
|
||||||
"newPassword": "Neues Passwort",
|
"newPassword": "Neues Passwort",
|
||||||
|
|||||||
@@ -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.",
|
"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.",
|
"autoUpdateToggled": "Auto-update setting saved.",
|
||||||
"autoUpdateFailed": "Auto-update toggle failed",
|
"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",
|
"passwordCardTitle": "Change admin password",
|
||||||
"currentPassword": "Current password",
|
"currentPassword": "Current password",
|
||||||
"newPassword": "New password",
|
"newPassword": "New password",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Alert, Button, Card, Descriptions, Form, Input, InputNumber, Space, Spin, Switch, Typography, message } from 'antd'
|
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 { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
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({
|
const changePassword = useMutation({
|
||||||
mutationFn: async (v: { current_password: string; new_password: string }) => {
|
mutationFn: async (v: { current_password: string; new_password: string }) => {
|
||||||
const r = await apiClient.post('/auth/change-password', v)
|
const r = await apiClient.post('/auth/change-password', v)
|
||||||
@@ -606,6 +627,28 @@ export default function SettingsPage() {
|
|||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
title={<><GlobalOutlined /> {t('settings.ipv6CardTitle')}</>}
|
||||||
|
className="mb-12"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
||||||
|
<Space>
|
||||||
|
<Switch
|
||||||
|
checked={ipv6?.enabled ?? false}
|
||||||
|
loading={toggleIPv6.isPending}
|
||||||
|
onChange={(checked) => toggleIPv6.mutate(checked)}
|
||||||
|
/>
|
||||||
|
<Typography.Text>
|
||||||
|
{ipv6?.enabled ? t('settings.ipv6On') : t('settings.ipv6Off')}
|
||||||
|
</Typography.Text>
|
||||||
|
</Space>
|
||||||
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
|
{t('settings.ipv6Hint')}
|
||||||
|
</Typography.Text>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card title={<><LockOutlined /> {t('settings.passwordCardTitle')}</>} size="small">
|
<Card title={<><LockOutlined /> {t('settings.passwordCardTitle')}</>} size="small">
|
||||||
<Form<ChangePasswordValues>
|
<Form<ChangePasswordValues>
|
||||||
form={pwForm}
|
form={pwForm}
|
||||||
|
|||||||
Reference in New Issue
Block a user