Add opt-in connect timeout to tls-connect
tls-connect accepts an optional 3rd arg, connect-timeout-ms (positive integer). When set, the TCP connect is made non-blocking and each resolved address is tried under a select() deadline (Happy-Eyeballs- lite: a fair slice of the total budget per address), so a blackholed address cannot hang on the OS SYN-retransmit timeout (~127s on Linux) the way a serial blocking connect does. Omitted or <= 0 keeps the original blocking mbedtlsnetconnect, byte-identical. Ignored on Windows (blocking connect).
Fixes the Telegram poller's intermittent 30s watchdog kills: a fresh getUpdates whose connect hit a blackholed api.telegram.org CDN IP burned the full SYN timeout (connect precedes the read, so the read deadline never engaged). With a connect timeout the bad address is abandoned fast and a working one is reached.
native/tls.c | 146 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
package.sgl | 2 +-
src/sigil/tls.sgl | 12 ++++++++++--
3 files changed, 153 insertions(+), 7 deletions(-)native/tls.cmodified
#include <fcntl.h>#ifdef _WIN32#include <winsock2.h>#else#include <unistd.h>#include <time.h>#include <sys/types.h>#include <sys/socket.h>#include <sys/select.h>#include <netdb.h>#endif#include "mbedtls/ssl.h" return sigil_bool(is_tls_connection(args[0]));}#ifndef _WIN32/* * tls-connect hostname port -> tls-connection | #f * Connect to host:port with a bounded total deadline, populating ctx->fd. * * Resolves the host (which may yield several CDN addresses) and tries each * in turn with a NON-BLOCKING connect + select(), so a blackholed address * (SYN dropped) cannot burn the full OS SYN-retransmit timeout (~127s on * Linux). The whole operation is bounded by timeout_ms; on success the * socket is restored to blocking mode (mbedTLS drives it blocking after). * * Returns 0 on success (ctx->fd set), -1 on failure/timeout. This is the * opt-in path: native_tls_connect only calls it when a positive timeout is * supplied; otherwise the original blocking mbedtls_net_connect runs, * leaving the default behavior byte-identical. */static long sigil_tls_now_ms(void){ struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (long)ts.tv_sec * 1000L + ts.tv_nsec / 1000000L;}static int sigil_tls_connect_timeout(mbedtls_net_context *ctx, const char *host, const char *port, long timeout_ms){ struct addrinfo hints, *res = NULL, *cur; memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = IPPROTO_TCP; if (getaddrinfo(host, port, &hints, &res) != 0) { return -1; } long deadline = sigil_tls_now_ms() + timeout_ms; int sockfd = -1; int connected = 0; /* Count addresses so each gets a fair slice of the total deadline: a * blackholed first address can't consume the whole budget, leaving a * working address (e.g. IPv4 after a dead IPv6, or another CDN node) * still reachable within the same call (Happy-Eyeballs-lite). */ int addrs_remaining = 0; for (cur = res; cur != NULL; cur = cur->ai_next) addrs_remaining++; for (cur = res; cur != NULL; cur = cur->ai_next, addrs_remaining--) { long remaining = deadline - sigil_tls_now_ms(); if (remaining <= 0) break; long per_addr = remaining / (addrs_remaining > 0 ? addrs_remaining : 1); if (per_addr < 1) per_addr = 1; sockfd = socket(cur->ai_family, cur->ai_socktype, cur->ai_protocol); if (sockfd < 0) continue; int flags = fcntl(sockfd, F_GETFL, 0); if (flags == -1 || fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) == -1) { close(sockfd); sockfd = -1; continue; } int rc = connect(sockfd, cur->ai_addr, cur->ai_addrlen); if (rc == 0) { connected = 1; } else if (errno == EINPROGRESS) { fd_set wset; FD_ZERO(&wset); FD_SET(sockfd, &wset); struct timeval tv; tv.tv_sec = per_addr / 1000L; tv.tv_usec = (per_addr % 1000L) * 1000L; int sel = select(sockfd + 1, NULL, &wset, NULL, &tv); if (sel > 0 && FD_ISSET(sockfd, &wset)) { int so_err = 0; socklen_t len = sizeof(so_err); if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &so_err, &len) == 0 && so_err == 0) { connected = 1; } } /* sel == 0 -> this address timed out; sel < 0 -> select error */ } if (connected) { /* Restore blocking mode for mbedTLS's blocking I/O. */ fcntl(sockfd, F_SETFL, flags); break; } close(sockfd); sockfd = -1; } freeaddrinfo(res); if (!connected || sockfd < 0) { return -1; } ctx->fd = sockfd; return 0;}#endif /* !_WIN32 *//* * tls-connect hostname port [connect-timeout-ms] -> tls-connection | #f * Establish a TLS connection to the specified host and port. * Returns a TLS connection object on success, #f on failure. * * The optional connect-timeout-ms (a positive integer) bounds the TCP * connect phase via a non-blocking connect + select with try-next-address, * so a blackholed address can't hang on the OS SYN timeout. Omitted or <= 0 * keeps the original blocking mbedtls_net_connect (default behavior * unchanged). On Windows the timeout is ignored (blocking connect). */static Value native_tls_connect(SigilVM *vm, int argc, Value *args){ SigilString *host_str = (SigilString *)sigil_as_ptr(args[0]); int port = (int)sigil_as_fixnum(args[1]); /* Optional connect timeout (milliseconds); <= 0 or absent = blocking. */ long connect_timeout_ms = 0; if (argc >= 3 && sigil_is_fixnum(args[2])) { connect_timeout_ms = (long)sigil_as_fixnum(args[2]); } /* Null-terminate hostname */ char *hostname = malloc(host_str->byte_length + 1); if (!hostname) return SIGIL_FALSE; mbedtls_ssl_config_init(&conn->conf); mbedtls_x509_crt_init(&conn->cacert); /* Connect to server */ ret = mbedtls_net_connect(&conn->server_fd, hostname, port_str, MBEDTLS_NET_PROTO_TCP); /* Connect to server. With a positive connect timeout, use the bounded * non-blocking path; otherwise the original blocking connect (default * behavior unchanged). */#ifndef _WIN32 if (connect_timeout_ms > 0) { ret = sigil_tls_connect_timeout(&conn->server_fd, hostname, port_str, connect_timeout_ms); } else { ret = mbedtls_net_connect(&conn->server_fd, hostname, port_str, MBEDTLS_NET_PROTO_TCP); }#else (void)connect_timeout_ms; ret = mbedtls_net_connect(&conn->server_fd, hostname, port_str, MBEDTLS_NET_PROTO_TCP);#endif if (ret != 0) { error_stage = "TCP connect"; goto cleanup_error; /* Connection operations */ REGISTER_AND_EXPORT("tls-connect", native_tls_connect, SIGIL_ARITY_EXACT(2), "Connect to TLS server"); SIGIL_ARITY_RANGE(2, 3), "Connect to TLS server"); REGISTER_AND_EXPORT("tls-read", native_tls_read, SIGIL_ARITY_RANGE(1, 2), "Read from TLS connection"); REGISTER_AND_EXPORT("tls-read-bytevector", native_tls_read_bytevector,package.sglmodified
(package name: "sigil-tls" version: "0.16.1" version: "0.16.2" sigil: "^0.16" description: "TLS/SSL connections for Sigil" url: "https://codeberg.org/sigil/sigil-tls"src/sigil/tls.sglmodified
;;; Certificates are verified against system CA certificates by default. ;;; Set SIGIL_TLS_INSECURE=1 to skip verification (testing only). ;;; ;;; An optional `connect-timeout-ms` (positive integer milliseconds) ;;; bounds the TCP connect phase: the underlying connect is made ;;; non-blocking and each resolved address is tried with `select` ;;; under a shared deadline, so a blackholed address cannot hang on ;;; the OS SYN-retransmit timeout. Omitted or <= 0 keeps the original ;;; blocking connect (default behavior unchanged). Ignored on Windows. ;;; ;;; ```scheme ;;; (tls-connect "example.com" 443) ; => tls-connection | #f ;;; (tls-connect "example.com" 443) ; => tls-connection | #f ;;; (tls-connect "example.com" 443 10000) ; 10s connect timeout ;;; ``` (define-native (tls-connect hostname port) (define-native (tls-connect hostname port . connect-timeout-ms) (: string? integer? -> any?)) ;;; Read data from a TLS connection.