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("/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"))
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user