feat(config-repl): wire do_one_config_fetch() — full SSL roundtrip

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.
This commit is contained in:
Rongsong Shen 2026-05-21 13:38:21 +00:00
parent 4ef2089fd4
commit 2aae45f046

View file

@ -5,12 +5,17 @@
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <netdb.h>
#include <openssl/err.h>
#include <openssl/sha.h>
#include <openssl/ssl.h>
#include <poll.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
@ -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 ──────────────────── */