From fd6908133613bfeb95a9606b9402a323fbf895df Mon Sep 17 00:00:00 2001 From: Debian Date: Fri, 29 May 2026 17:25:38 +0200 Subject: [PATCH] =?UTF-8?q?fix(cluster):=20Joiner-IP=20sofort=20in=20@peer?= =?UTF-8?q?=5Fipv4=20=E2=80=94=20Henne-Ei-Problem=20gel=C3=B6st?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IssueCert registriert den beitretenden Node jetzt direkt in ha_nodes (public_ip = ClientIP, status='joining') und triggert den Firewall-Reload, bevor die Response zurückgeht. Damit ist Port 8443 schon offen wenn der Node im nächsten Schritt auto-register via mTLS versucht. Co-Authored-By: Claude Sonnet 4.6 --- VERSION | 2 +- cmd/edgeguard-api/main.go | 2 +- cmd/edgeguard-ctl/main.go | 2 +- cmd/edgeguard-scheduler/main.go | 2 +- internal/handlers/cluster.go | 64 +++++++++++++++++++++++++++++++-- 5 files changed, 66 insertions(+), 6 deletions(-) diff --git a/VERSION b/VERSION index 4b7ce3d..a10b76e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.152 +1.1.153 diff --git a/cmd/edgeguard-api/main.go b/cmd/edgeguard-api/main.go index 4254c91..dd8f51a 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.152" +var version = "1.1.153" func main() { addr := os.Getenv("EDGEGUARD_API_ADDR") diff --git a/cmd/edgeguard-ctl/main.go b/cmd/edgeguard-ctl/main.go index ce3568d..7828e55 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.152" +var version = "1.1.153" const usage = `edgeguard-ctl — EdgeGuard CLI diff --git a/cmd/edgeguard-scheduler/main.go b/cmd/edgeguard-scheduler/main.go index 602dfe5..7e1a1d8 100644 --- a/cmd/edgeguard-scheduler/main.go +++ b/cmd/edgeguard-scheduler/main.go @@ -41,7 +41,7 @@ import ( "git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts" ) -var version = "1.1.152" +var version = "1.1.153" const ( // renewTickInterval — how often we re-evaluate expiring certs. diff --git a/internal/handlers/cluster.go b/internal/handlers/cluster.go index 3c4153e..d9aef58 100644 --- a/internal/handlers/cluster.go +++ b/internal/handlers/cluster.go @@ -2,7 +2,10 @@ package handlers import ( "context" + "crypto/x509" "encoding/json" + "encoding/pem" + "fmt" "log/slog" "time" @@ -337,8 +340,8 @@ func (h *ClusterHandler) IssueCert(c *gin.Context) { } // consumedBy → Remote-IP. Audit-Trail wenn jemand Tokens stiehlt // und vom falschen Host einlöst. - consumedBy := c.ClientIP() - if _, err := h.Tokens.Consume(c.Request.Context(), req.Token, consumedBy); err != nil { + clientIP := c.ClientIP() + if _, err := h.Tokens.Consume(c.Request.Context(), req.Token, clientIP); err != nil { response.BadRequest(c, err) return } @@ -353,12 +356,69 @@ func (h *ClusterHandler) IssueCert(c *gin.Context) { response.Internal(c, err) return } + + // Pre-register the joining node so its IP lands in @peer_ipv4 + // immediately — otherwise port 8443 stays blocked and auto-register + // via mTLS can never succeed (chicken-and-egg). + if h.Store != nil && h.PeerReloader != nil { + go h.preRegisterJoiner(clientIP, req.CSR) + } + response.OK(c, issueCertResponse{ CACert: caPEM, PeerCert: peerCert, }) } +// preRegisterJoiner inserts a minimal ha_nodes row for the joining peer +// (using the CSR CN as FQDN and the HTTP client IP as public_ip), then +// triggers a firewall reload so @peer_ipv4 contains the new IP before +// the peer tries to call /agent/cluster/peers on port 8443. +func (h *ClusterHandler) preRegisterJoiner(clientIP, csrPEM string) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + fqdn := cnFromCSR(csrPEM) + if fqdn == "" { + fqdn = "joining-" + clientIP + } + nodeID := fmt.Sprintf("pre-%x", time.Now().UnixNano()) + + n := models.HANode{ + ID: nodeID, + Name: fqdn, + FQDN: fqdn, + APIURL: "https://" + fqdn + ":3443", + Role: "peer", + Status: "joining", + } + n.PublicIP = &clientIP + + if _, err := h.Store.UpsertSelf(ctx, n); err != nil { + slog.Warn("cluster: pre-register joiner failed", "fqdn", fqdn, "ip", clientIP, "error", err) + return + } + if err := h.PeerReloader(ctx); err != nil { + slog.Warn("cluster: PeerReloader failed after pre-register", "error", err) + return + } + slog.Info("cluster: joiner pre-registered, firewall updated", "fqdn", fqdn, "ip", clientIP) +} + +// cnFromCSR extracts the Subject Common Name from a PEM-encoded CSR. +// Returns empty string on any parse error. +func cnFromCSR(csrPEM string) string { + block, _ := pem.Decode([]byte(csrPEM)) + if block == nil { + return "" + } + csr, err := x509.ParseCertificateRequest(block.Bytes) + if err != nil { + return "" + } + return csr.Subject.CommonName +} + var errInvalidJoinRequest = simpleError("missing token or csr") type simpleError string