From 2aae45f046362cf8036739f0b6d08559c34d1e4e Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Thu, 21 May 2026 13:38:21 +0000 Subject: [PATCH] =?UTF-8?q?feat(config-repl):=20wire=20do=5Fone=5Fconfig?= =?UTF-8?q?=5Ffetch()=20=E2=80=94=20full=20SSL=20roundtrip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces -ENOSYS stub with: - connect_ssl_raw() helper (non-blocking connect + poll + SSL handshake, checks ha_is_primary() between iterations for early promotion exit) - xce_replconfig_req_ssl() to send OP_replConfig request - xce_replconfig_reply_ssl_decode() to receive and decode reply - SHA-256 integrity verify (retry on mismatch with -EBADMSG) - conf_save_config() atomic disk write to workdir/name/conf/ - load_primary_config() to reload cluster_config from newly saved file Standby can now fetch ClusterConfig from primary on startup. --- src/controller/config_repl.c | 224 +++++++++++++++++++++++++++++++++-- 1 file changed, 213 insertions(+), 11 deletions(-) diff --git a/src/controller/config_repl.c b/src/controller/config_repl.c index 7140751..ca8865d 100644 --- a/src/controller/config_repl.c +++ b/src/controller/config_repl.c @@ -5,12 +5,17 @@ #include #include #include +#include +#include #include +#include +#include #include #include #include #include #include +#include #include #include #include @@ -666,22 +671,219 @@ int handle_config_request(cluster_t *c, endpoint_t *ep, xce_request_t *req) /* ── fetch_config_from_primary helpers ─────────────────────── */ /* - * do_one_config_fetch: attempt one network fetch of config from the primary. + * connect_ssl_raw: open a non-blocking TCP connection to host:port, then + * perform a TLS handshake using ssl_ctx. * - * The full wire protocol (capnp encode/decode over a standalone SSL socket) - * depends on the typemap shims being complete (Task 11). Until then this - * function returns -ENOSYS so that fetch_config_from_primary() falls back to - * the locally saved config. + * The poll loop checks ha_is_primary(ha) between iterations: if this node + * is promoted to primary while waiting, the function returns -ECANCELED so + * the caller can switch to the primary path immediately. + * + * On success: *out_ssl and *out_sock are set; caller owns them and must call + * SSL_shutdown(ssl) + SSL_free(ssl) + close(sock). + * On failure: nothing is left open; returns negative errno. + */ +static int connect_ssl_raw(SSL_CTX *ssl_ctx, const char *host, uint32_t port, + int poll_timeout_ms, const ha_state_t *ha, + SSL **out_ssl, int *out_sock) +{ + struct addrinfo hints, *res = NULL; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + char port_str[16]; + snprintf(port_str, sizeof(port_str), "%u", (unsigned)port); + if (getaddrinfo(host, port_str, &hints, &res) != 0) + return -ENONET; + + int sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol); + if (sock < 0) { + int e = errno; + freeaddrinfo(res); + return -e; + } + + /* Non-blocking connect */ + int flags = fcntl(sock, F_GETFL, 0); + fcntl(sock, F_SETFL, flags | O_NONBLOCK); + connect(sock, res->ai_addr, res->ai_addrlen); + freeaddrinfo(res); + res = NULL; + + /* Poll loop: check ha_is_primary between iterations */ + while (!ha_is_primary(ha)) { + struct pollfd pfd = { .fd = sock, .events = POLLOUT | POLLERR | POLLHUP }; + int pr = poll(&pfd, 1, poll_timeout_ms); + if (pr < 0 && errno == EINTR) + continue; + if (pr == 0) { + close(sock); + return -ETIMEDOUT; + } + if (pfd.revents & (POLLERR | POLLHUP)) { + close(sock); + return -ECONNREFUSED; + } + if (pfd.revents & POLLOUT) + break; + } + if (ha_is_primary(ha)) { + close(sock); + return -ECANCELED; + } + + /* Verify connect succeeded */ + { + int err = 0; + socklen_t el = sizeof(err); + getsockopt(sock, SOL_SOCKET, SO_ERROR, &err, &el); + if (err != 0) { + close(sock); + return -err; + } + } + + /* Restore blocking for SSL handshake */ + fcntl(sock, F_SETFL, flags & ~O_NONBLOCK); + + SSL *ssl = SSL_new(ssl_ctx); + if (!ssl) { + close(sock); + return -ENOMEM; + } + SSL_set_fd(ssl, sock); + + int sr; + do { + sr = SSL_connect(ssl); + if (sr <= 0) { + int e = SSL_get_error(ssl, sr); + if (e != SSL_ERROR_WANT_READ && e != SSL_ERROR_WANT_WRITE) + break; + } + } while (sr <= 0 && !ha_is_primary(ha)); + + if (sr <= 0 || ha_is_primary(ha)) { + SSL_free(ssl); + close(sock); + return -ECONNRESET; + } + + *out_ssl = ssl; + *out_sock = sock; + return 0; +} + +/* + * do_one_config_fetch: attempt one network fetch of the ClusterConfig from + * the primary controller over a standalone SSL socket. + * + * Steps: + * 1. Open non-blocking TCP + SSL handshake via connect_ssl_raw() + * 2. Send OP_replConfig request with our current version tag + * 3. Decode the reply + * 4. If up_to_date: nothing to save — return 0 + * 5. Verify SHA-256 integrity of received bytes + * 6. Save to disk via conf_save_config() + * 7. Reload cluster_config via load_primary_config() + * 8. Log success and clean up + * + * Returns 0 on success (config up-to-date or freshly saved+loaded). + * Returns -ECANCELED if promoted to primary during the attempt (caller should + * switch to primary path). Returns other negative errno on transient failure + * (caller will retry with backoff). */ static int do_one_config_fetch(cluster_t *c, const char *host, uint32_t port, uint64_t our_tag) { - (void)c; - (void)host; - (void)port; - (void)our_tag; - /* Task 11: wire full capnp encode/decode over standalone SSL socket */ - return -ENOSYS; + SSL *ssl = NULL; + int sock = -1; + + /* 1. Connect + SSL handshake */ + int rc = connect_ssl_raw( + (SSL_CTX *)c->ha.ssl_ctx, host, port, + (int)c->ha.config.probe_connect_timeout_ms, + &c->ha, &ssl, &sock); + if (rc != 0) + return rc; + + /* 2. Send OP_replConfig request */ + rc = xce_replconfig_req_ssl(ssl, our_tag); + if (rc != 0) { + SSL_shutdown(ssl); + SSL_free(ssl); + close(sock); + return rc; + } + + /* 3. Decode reply */ + int up_to_date = 0; + uint64_t new_tag = 0; + uint8_t *sha256 = NULL; + uint8_t *config_bytes = NULL; + size_t config_len = 0; + + rc = xce_replconfig_reply_ssl_decode(ssl, &up_to_date, &new_tag, + &sha256, &config_bytes, &config_len); + if (rc != 0) { + SSL_shutdown(ssl); + SSL_free(ssl); + close(sock); + return rc; + } + + /* 4. Up-to-date fast path: our tag matches primary — nothing to save */ + if (up_to_date) { + SSL_shutdown(ssl); + SSL_free(ssl); + close(sock); + return 0; + } + + /* 5. Verify SHA-256 integrity */ + uint8_t computed[32]; + SHA256(config_bytes, config_len, computed); + if (memcmp(computed, sha256, 32) != 0) { + ERROR("HA: config fetch SHA-256 mismatch from primary — retrying"); + free(sha256); + free(config_bytes); + SSL_shutdown(ssl); + SSL_free(ssl); + close(sock); + return -EBADMSG; + } + + /* 6. Save to disk atomically */ + rc = conf_save_config(c->myconf, config_bytes, config_len, sha256); + free(sha256); + free(config_bytes); + sha256 = NULL; + config_bytes = NULL; + if (rc != 0) { + SSL_shutdown(ssl); + SSL_free(ssl); + close(sock); + return rc; + } + + /* 7. Reload cluster_config from the newly saved file */ + rc = load_primary_config(c); + if (rc != 0) { + SSL_shutdown(ssl); + SSL_free(ssl); + close(sock); + return rc; + } + + /* 8. Log success */ + INFO("HA: config received from primary (tag=%016" PRIx64 ")", + c->myconf->config_version_tag); + + /* Clean up SSL and socket */ + SSL_shutdown(ssl); + SSL_free(ssl); + close(sock); + return 0; } /* ── fetch_config_from_primary ──────────────────── */