Commite67c3b00Recorded30 Jun 2026Repositorysigil-tls

Add opt-in connect timeout to tls-connect

Message

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.

Changed
 native/tls.c      | 146 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
 package.sgl       |   2 +-
 src/sigil/tls.sgl |  12 ++++++++++--
 3 files changed, 153 insertions(+), 7 deletions(-)
Diff
native/tls.cmodified
@@ -35,6 +35,13 @@ void sigil__init_sigil_tls_module(SigilVM *vm)
35
#include <fcntl.h>
36
#ifdef _WIN32
37
#include <winsock2.h>
+38
#else
+39
#include <unistd.h>
+40
#include <time.h>
+41
#include <sys/types.h>
+42
#include <sys/socket.h>
+43
#include <sys/select.h>
+44
#include <netdb.h>
45
#endif
46
47
#include "mbedtls/ssl.h"
@@ -221,10 +228,121 @@ static Value native_tls_connectionp(SigilVM *vm, int argc, Value *args)
228
return sigil_bool(is_tls_connection(args[0]));
229
}
230
+231
#ifndef _WIN32
232
/*
225
* tls-connect hostname port -> tls-connection | #f
+233
* Connect to host:port with a bounded total deadline, populating ctx->fd.
+234
*
+235
* Resolves the host (which may yield several CDN addresses) and tries each
+236
* in turn with a NON-BLOCKING connect + select(), so a blackholed address
+237
* (SYN dropped) cannot burn the full OS SYN-retransmit timeout (~127s on
+238
* Linux). The whole operation is bounded by timeout_ms; on success the
+239
* socket is restored to blocking mode (mbedTLS drives it blocking after).
+240
*
+241
* Returns 0 on success (ctx->fd set), -1 on failure/timeout. This is the
+242
* opt-in path: native_tls_connect only calls it when a positive timeout is
+243
* supplied; otherwise the original blocking mbedtls_net_connect runs,
+244
* leaving the default behavior byte-identical.
+245
*/
+246
static long sigil_tls_now_ms(void)
+247
{
+248
struct timespec ts;
+249
clock_gettime(CLOCK_MONOTONIC, &ts);
+250
return (long)ts.tv_sec * 1000L + ts.tv_nsec / 1000000L;
+251
}
+252
+253
static int sigil_tls_connect_timeout(mbedtls_net_context *ctx,
+254
const char *host, const char *port,
+255
long timeout_ms)
+256
{
+257
struct addrinfo hints, *res = NULL, *cur;
+258
memset(&hints, 0, sizeof(hints));
+259
hints.ai_family = AF_UNSPEC;
+260
hints.ai_socktype = SOCK_STREAM;
+261
hints.ai_protocol = IPPROTO_TCP;
+262
+263
if (getaddrinfo(host, port, &hints, &res) != 0) {
+264
return -1;
+265
}
+266
+267
long deadline = sigil_tls_now_ms() + timeout_ms;
+268
int sockfd = -1;
+269
int connected = 0;
+270
+271
/* Count addresses so each gets a fair slice of the total deadline: a
+272
* blackholed first address can't consume the whole budget, leaving a
+273
* working address (e.g. IPv4 after a dead IPv6, or another CDN node)
+274
* still reachable within the same call (Happy-Eyeballs-lite). */
+275
int addrs_remaining = 0;
+276
for (cur = res; cur != NULL; cur = cur->ai_next) addrs_remaining++;
+277
+278
for (cur = res; cur != NULL; cur = cur->ai_next, addrs_remaining--) {
+279
long remaining = deadline - sigil_tls_now_ms();
+280
if (remaining <= 0) break;
+281
long per_addr = remaining / (addrs_remaining > 0 ? addrs_remaining : 1);
+282
if (per_addr < 1) per_addr = 1;
+283
+284
sockfd = socket(cur->ai_family, cur->ai_socktype, cur->ai_protocol);
+285
if (sockfd < 0) continue;
+286
+287
int flags = fcntl(sockfd, F_GETFL, 0);
+288
if (flags == -1 || fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) == -1) {
+289
close(sockfd);
+290
sockfd = -1;
+291
continue;
+292
}
+293
+294
int rc = connect(sockfd, cur->ai_addr, cur->ai_addrlen);
+295
if (rc == 0) {
+296
connected = 1;
+297
} else if (errno == EINPROGRESS) {
+298
fd_set wset;
+299
FD_ZERO(&wset);
+300
FD_SET(sockfd, &wset);
+301
struct timeval tv;
+302
tv.tv_sec = per_addr / 1000L;
+303
tv.tv_usec = (per_addr % 1000L) * 1000L;
+304
int sel = select(sockfd + 1, NULL, &wset, NULL, &tv);
+305
if (sel > 0 && FD_ISSET(sockfd, &wset)) {
+306
int so_err = 0;
+307
socklen_t len = sizeof(so_err);
+308
if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &so_err, &len) == 0
+309
&& so_err == 0) {
+310
connected = 1;
+311
}
+312
}
+313
/* sel == 0 -> this address timed out; sel < 0 -> select error */
+314
}
+315
+316
if (connected) {
+317
/* Restore blocking mode for mbedTLS's blocking I/O. */
+318
fcntl(sockfd, F_SETFL, flags);
+319
break;
+320
}
+321
+322
close(sockfd);
+323
sockfd = -1;
+324
}
+325
+326
freeaddrinfo(res);
+327
+328
if (!connected || sockfd < 0) {
+329
return -1;
+330
}
+331
ctx->fd = sockfd;
+332
return 0;
+333
}
+334
#endif /* !_WIN32 */
+335
+336
/*
+337
* tls-connect hostname port [connect-timeout-ms] -> tls-connection | #f
338
* Establish a TLS connection to the specified host and port.
339
* Returns a TLS connection object on success, #f on failure.
+340
*
+341
* The optional connect-timeout-ms (a positive integer) bounds the TCP
+342
* connect phase via a non-blocking connect + select with try-next-address,
+343
* so a blackholed address can't hang on the OS SYN timeout. Omitted or <= 0
+344
* keeps the original blocking mbedtls_net_connect (default behavior
+345
* unchanged). On Windows the timeout is ignored (blocking connect).
346
*/
347
static Value native_tls_connect(SigilVM *vm, int argc, Value *args)
348
{
@@ -246,6 +364,12 @@ static Value native_tls_connect(SigilVM *vm, int argc, Value *args)
364
SigilString *host_str = (SigilString *)sigil_as_ptr(args[0]);
365
int port = (int)sigil_as_fixnum(args[1]);
366
+367
/* Optional connect timeout (milliseconds); <= 0 or absent = blocking. */
+368
long connect_timeout_ms = 0;
+369
if (argc >= 3 && sigil_is_fixnum(args[2])) {
+370
connect_timeout_ms = (long)sigil_as_fixnum(args[2]);
+371
}
+372
373
/* Null-terminate hostname */
374
char *hostname = malloc(host_str->byte_length + 1);
375
if (!hostname) return SIGIL_FALSE;
@@ -272,8 +396,22 @@ static Value native_tls_connect(SigilVM *vm, int argc, Value *args)
396
mbedtls_ssl_config_init(&conn->conf);
397
mbedtls_x509_crt_init(&conn->cacert);
398
275
/* Connect to server */
276
ret = mbedtls_net_connect(&conn->server_fd, hostname, port_str, MBEDTLS_NET_PROTO_TCP);
+399
/* Connect to server. With a positive connect timeout, use the bounded
+400
* non-blocking path; otherwise the original blocking connect (default
+401
* behavior unchanged). */
+402
#ifndef _WIN32
+403
if (connect_timeout_ms > 0) {
+404
ret = sigil_tls_connect_timeout(&conn->server_fd, hostname, port_str,
+405
connect_timeout_ms);
+406
} else {
+407
ret = mbedtls_net_connect(&conn->server_fd, hostname, port_str,
+408
MBEDTLS_NET_PROTO_TCP);
+409
}
+410
#else
+411
(void)connect_timeout_ms;
+412
ret = mbedtls_net_connect(&conn->server_fd, hostname, port_str,
+413
MBEDTLS_NET_PROTO_TCP);
+414
#endif
415
if (ret != 0) {
416
error_stage = "TCP connect";
417
goto cleanup_error;
@@ -771,7 +909,7 @@ void sigil__init_sigil_tls_module(SigilVM *vm)
909
910
/* Connection operations */
911
REGISTER_AND_EXPORT("tls-connect", native_tls_connect,
774
SIGIL_ARITY_EXACT(2), "Connect to TLS server");
+912
SIGIL_ARITY_RANGE(2, 3), "Connect to TLS server");
913
REGISTER_AND_EXPORT("tls-read", native_tls_read,
914
SIGIL_ARITY_RANGE(1, 2), "Read from TLS connection");
915
REGISTER_AND_EXPORT("tls-read-bytevector", native_tls_read_bytevector,
package.sglmodified
@@ -9,7 +9,7 @@
9
10
(package
11
name: "sigil-tls"
12
version: "0.16.1"
+12
version: "0.16.2"
13
sigil: "^0.16"
14
description: "TLS/SSL connections for Sigil"
15
url: "https://codeberg.org/sigil/sigil-tls"
src/sigil/tls.sglmodified
@@ -36,10 +36,18 @@
36
;;; Certificates are verified against system CA certificates by default.
37
;;; Set SIGIL_TLS_INSECURE=1 to skip verification (testing only).
38
;;;
+39
;;; An optional `connect-timeout-ms` (positive integer milliseconds)
+40
;;; bounds the TCP connect phase: the underlying connect is made
+41
;;; non-blocking and each resolved address is tried with `select`
+42
;;; under a shared deadline, so a blackholed address cannot hang on
+43
;;; the OS SYN-retransmit timeout. Omitted or <= 0 keeps the original
+44
;;; blocking connect (default behavior unchanged). Ignored on Windows.
+45
;;;
46
;;; ```scheme
40
;;; (tls-connect "example.com" 443) ; => tls-connection | #f
+47
;;; (tls-connect "example.com" 443) ; => tls-connection | #f
+48
;;; (tls-connect "example.com" 443 10000) ; 10s connect timeout
49
;;; ```
42
(define-native (tls-connect hostname port)
+50
(define-native (tls-connect hostname port . connect-timeout-ms)
51
(: string? integer? -> any?))
52
53
;;; Read data from a TLS connection.