diff --git a/src/frontend/apiserver/backend/clib.go b/src/frontend/apiserver/backend/clib.go index 79dba7a4..86d2fa46 100644 --- a/src/frontend/apiserver/backend/clib.go +++ b/src/frontend/apiserver/backend/clib.go @@ -76,11 +76,15 @@ var idCtxOnce sync.Once // Must be set via StartNotificationListener before RegisterToBackend. var gHandler NotificationHandler -// tryConnect attempts to connect to a single controller candidate. -// Returns nil on success. The C strings passed in are owned by the caller. -func tryConnect(host string, port int, cFrom, cKey *C.char) error { +func tryConnect(host string, port int, from, key string) error { cAddr := C.CString(host) - defer C.free(unsafe.Pointer(cAddr)) + cFrom := C.CString(from) + cKey := C.CString(key) + defer func() { + C.free(unsafe.Pointer(cAddr)) + C.free(unsafe.Pointer(cFrom)) + C.free(unsafe.Pointer(cKey)) + }() var cConfig C.xce_client_config_t cConfig.nmasters = 1 @@ -99,10 +103,6 @@ func tryConnect(host string, port int, cFrom, cKey *C.char) error { // RegisterToBackend initialises the XCE client library and joins as an API // server. StartNotificationListener should be called first so that push // events have a handler ready before the first push can arrive. -// -// When config.Peers is non-empty the function iterates each "host:port" entry -// in order and returns as soon as one succeeds (HA candidate fallback). -// When Peers is empty it connects to config.Address:config.Port directly. func RegisterToBackend(config *BackendConfig) error { from := config.From if from == "" { @@ -113,14 +113,6 @@ func RegisterToBackend(config *BackendConfig) error { key = "keepsecret$" } - cFrom := C.CString(from) - cKey := C.CString(key) - defer func() { - C.free(unsafe.Pointer(cFrom)) - C.free(unsafe.Pointer(cKey)) - }() - - // HA mode: iterate the peer list and connect to the first responsive primary. if len(config.Peers) > 0 { var lastErr error for _, peer := range config.Peers { @@ -130,7 +122,7 @@ func RegisterToBackend(config *BackendConfig) error { lastErr = err continue } - if err := tryConnect(host, port, cFrom, cKey); err != nil { + if err := tryConnect(host, port, from, key); err != nil { logger.Warn("Candidate unreachable, trying next", zap.String("host", host), zap.Int("port", port), zap.Error(err)) lastErr = err @@ -140,14 +132,10 @@ func RegisterToBackend(config *BackendConfig) error { zap.String("host", host), zap.Int("port", port)) return nil } - if lastErr != nil { - return fmt.Errorf("all HA candidates failed: %w", lastErr) - } - return fmt.Errorf("no HA candidates configured") + return fmt.Errorf("all HA candidates failed: %w", lastErr) } - // Single-host mode (no peers configured). - if err := tryConnect(config.Address, config.Port, cFrom, cKey); err != nil { + if err := tryConnect(config.Address, config.Port, from, key); err != nil { return err } logger.Info("Backend connection established", diff --git a/src/frontend/apiserver/backend/peers.go b/src/frontend/apiserver/backend/peers.go index 60873f57..3b5e1782 100644 --- a/src/frontend/apiserver/backend/peers.go +++ b/src/frontend/apiserver/backend/peers.go @@ -2,21 +2,23 @@ package backend import ( "fmt" + "net" "strconv" - "strings" ) -// parsePeer splits a "host:port" string into host and port components. -// Returns an error if the format is invalid or the port is out of range. +// parsePeer splits a "host:port" string. Uses net.SplitHostPort so IPv6 +// bracket notation (e.g. "[::1]:9990") is handled correctly. func parsePeer(peer string) (host string, port int, err error) { - idx := strings.LastIndex(peer, ":") - if idx <= 0 { - return "", 0, fmt.Errorf("invalid peer %q: expected host:port", peer) + h, portStr, err := net.SplitHostPort(peer) + if err != nil { + return "", 0, fmt.Errorf("invalid peer %q: %w", peer, err) } - host = peer[:idx] - port, err = strconv.Atoi(peer[idx+1:]) + if h == "" { + return "", 0, fmt.Errorf("invalid peer %q: empty host", peer) + } + port, err = strconv.Atoi(portStr) if err != nil || port <= 0 || port > 65535 { return "", 0, fmt.Errorf("invalid port in peer %q", peer) } - return host, port, nil + return h, port, nil } diff --git a/src/frontend/apiserver/utils/config.go b/src/frontend/apiserver/utils/config.go index 7cbb9ec1..24b5845a 100644 --- a/src/frontend/apiserver/utils/config.go +++ b/src/frontend/apiserver/utils/config.go @@ -18,10 +18,10 @@ type BackendConfig struct { From string `yaml:"from"` Key string `yaml:"key"` // Peers lists additional controller candidates for HA mode in "host:port" - // format. When non-empty, RegisterToBackend iterates all candidates and - // connects to whichever responds as primary. Address+Port remain the - // single-host fallback when Peers is empty. - Peers []string `yaml:"peers"` + // format. When non-empty, RegisterToBackend tries each candidate in order + // and connects to whichever accepts the join as primary. Address+Port + // serve as a single-host fallback when Peers is empty. + Peers []string `yaml:"peers"` } type AppServerConfig struct {