Table of Contents
ex_network_accept.c
What netw_accept_from_ex() hands back about the accepted connection
- link.ip / link.port of an accepted connection name the PEER: a client bound to a known local port must be seen on that port, not on the listening one.
- netw_setsockopt() takes SO_RCVTIMEO / SO_SNDTIMEO in msecs on every platform: 1500 reads back as 1.5 s, and a recv() with 250 gives up after about a quarter of a second, not 250 s.
- NETWORK_HANDSHAKE_TIMEOUT on a TLS listener bounds the server handshake run inside netw_accept_from_ex(): a client that connects and sends nothing, and one that drips a byte every 100 ms, are both dropped at the deadline with ETIMEDOUT; a real TLS client is still accepted, with the deadline set and without it, and its socket is blocking again. ```c
/*
- Nilorea Library
- Copyright (C) 2005-2026 Castagnier Mickael
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied. See the License for the specific language governing
- permissions and limitations under the License.
- SPDX-License-Identifier: Apache-2.0 */
#include "nilorea/n_common.h" #include "nilorea/n_log.h" #include "nilorea/n_network.h" #include "nilorea/n_str.h" #include "nilorea/n_time.h" #include "nilorea/n_x509.h"
#include <errno.h> #include <getopt.h> #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h>
static int failures = 0;
#define CHECK(cond, ...)
do {
if (cond) {
n_log(LOG_NOTICE, "ok: " VA_ARGS);
} else {
n_log(LOG_ERR, "FAIL: " VA_ARGS);
failures++;
}
} while (0)
/* Parse the -V LOG_LEVEL verbosity argument. / void process_args(int argc, char* argv) { int opt = 0; while ((opt = getopt(argc, argv, "hvV:")) != EOF) { switch (opt) { case 'V': if (!strncmp("LOG_NULL", optarg, 8)) set_log_level(LOG_NULL); else if (!strncmp("LOG_NOTICE", optarg, 10)) set_log_level(LOG_NOTICE); else if (!strncmp("LOG_INFO", optarg, 8)) set_log_level(LOG_INFO); else if (!strncmp("LOG_ERR", optarg, 7)) set_log_level(LOG_ERR); else if (!strncmp("LOG_DEBUG", optarg, 9)) set_log_level(LOG_DEBUG); break; case 'v': fprintf(stderr, "ex_network_accept\n"); exit(1); case 'h': default: fprintf(stderr, "usage: %s [-V LOG_LEVEL]\n", argv[0]); break; } } }
/* The port a socket is bound to, 0 on error. / static unsigned int local_port(SOCKET s) { struct sockaddr_in a; socklen_t len = sizeof(a); memset(&a, 0, sizeof(a)); if (getsockname(s, (struct sockaddr)&a, &len) != 0) return 0; return ntohs(a.sin_port); }
/* A plain TCP client socket bound to an ephemeral local port and connected to
- 127.0.0.1:port, INVALID_SOCKET on error. / static SOCKET raw_client(unsigned int port) { SOCKET s = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in a; if (s == INVALID_SOCKET) return INVALID_SOCKET; memset(&a, 0, sizeof(a)); a.sin_family = AF_INET; a.sin_addr.s_addr = htonl(INADDR_LOOPBACK); a.sin_port = 0; if (bind(s, (struct sockaddr)&a, sizeof(a)) != 0) { closesocket(s); return INVALID_SOCKET; } a.sin_port = htons((unsigned short)port); if (connect(s, (struct sockaddr*)&a, sizeof(a)) != 0) { closesocket(s); return INVALID_SOCKET; } return s; }
/* A listener on 127.0.0.1, on a port picked by the system. / static NETWORK make_listener(unsigned int* port) { NETWORK* l = NULL; if (netw_make_listening(&l, "127.0.0.1", "0", 8, NETWORK_IPV4) != TRUE || !l) return NULL; *port = local_port(l->link.sock); return l; }
/* link.port of an accepted connection is the peer's port. / static void test_peer_port(void) { unsigned int lport = 0; NETWORK l = make_listener(&lport); CHECK(l && lport > 0, "plain listener on 127.0.0.1:%u", lport); if (!l) return; SOCKET c = raw_client(lport); unsigned int cport = (c != INVALID_SOCKET) ? local_port(c) : 0; int ret = 0; NETWORK* a = netw_accept_from_ex(l, 0, 0, 2000, &ret); CHECK(a != NULL, "accepted the client (retval %d)", ret); if (a) { char want[8]; snprintf(want, sizeof(want), "%u", cport); CHECK(a->link.port && strcmp(a->link.port, want) == 0, "link.port '%s' is the peer's port %u, not the listener's %u", _str(a->link.port), cport, lport); CHECK(a->link.ip && strcmp(a->link.ip, "127.0.0.1") == 0, "link.ip '%s' is the peer's address", _str(a->link.ip)); CHECK(l->link.port && strcmp(l->link.port, "0") == 0, "the listener keeps its own link.port '%s'", _str(l->link.port)); netw_close(&a); } if (c != INVALID_SOCKET) closesocket(c); netw_close(&l); }
/* SO_RCVTIMEO / SO_SNDTIMEO are msecs (NETW_SOCKOPT_TIMEO_MS). / static void test_sockopt_timeo(void) { unsigned int lport = 0; NETWORK l = make_listener(&lport); CHECK(l != NULL, "timeouts: listener on 127.0.0.1:%u", lport); if (!l) return; SOCKET c = raw_client(lport); int ret = 0; NETWORK* a = netw_accept_from_ex(l, 0, 0, 2000, &ret); CHECK(a != NULL, "timeouts: accepted the client"); if (a) { CHECK(netw_setsockopt(a, SO_RCVTIMEO, 1500) == TRUE && netw_setsockopt(a, SO_SNDTIMEO, 1500) == TRUE, "timeouts: set to 1500"); #ifdef windows DWORD rcv = 0, snd = 0; int len = sizeof(rcv); getsockopt(a->link.sock, SOL_SOCKET, SO_RCVTIMEO, (char*)&rcv, &len); len = sizeof(snd); getsockopt(a->link.sock, SOL_SOCKET, SO_SNDTIMEO, (char*)&snd, &len); CHECK(rcv == 1500 && snd == 1500, "timeouts: 1500 reads back as %lu / %lu ms", (unsigned long)rcv, (unsigned long)snd); #else struct timeval rcv, snd; socklen_t len = sizeof(rcv); memset(&rcv, 0, sizeof(rcv)); memset(&snd, 0, sizeof(snd)); getsockopt(a->link.sock, SOL_SOCKET, SO_RCVTIMEO, &rcv, &len); len = sizeof(snd); getsockopt(a->link.sock, SOL_SOCKET, SO_SNDTIMEO, &snd, &len); CHECK(rcv.tv_sec == 1 && rcv.tv_usec == 500000 && snd.tv_sec == 1 && snd.tv_usec == 500000, "timeouts: 1500 reads back as %ld.%06ld / %ld.%06ld s", (long)rcv.tv_sec, (long)rcv.tv_usec, (long)snd.tv_sec, (long)snd.tv_usec); #endif /* the client sends nothing: the recv must give up after ~250 ms */ char b; N_TIME t; netw_setsockopt(a, SO_RCVTIMEO, 250); start_HiTimer(&t); ssize_t r = recv(a->link.sock, &b, 1, 0); time_t ms = get_msec(&t); CHECK(r < 0 && ms >= 200 && ms < 2000, "timeouts: recv with 250 gave up after %ld ms", (long)ms); CHECK(netw_setsockopt(a, SO_RCVTIMEO, 0) == TRUE && a->so_rcvtimeo == 0, "timeouts: 0 clears it"); netw_close(&a); } if (c != INVALID_SOCKET) closesocket(c); netw_close(&l); }
#ifdef HAVE_OPENSSL #define HS_TIMEOUT_MS 500 #define HS_LATEST_MS 1500
static N_STR *ca_cert = NULL, *ca_key = NULL, *leaf_cert = NULL, *leaf_key = NULL;
typedef struct CLIENT_JOB {
unsigned int port;
volatile int stop;
int connected;
} CLIENT_JOB;
/* A TLS listener on 127.0.0.1 with the test leaf, and @p timeout_ms set. / static NETWORK make_tls_listener(unsigned int* port, int timeout_ms) { NETWORK* l = make_listener(port); if (!l) return NULL; if (netw_set_crypto_pem(l, leaf_key->data, leaf_cert->data) != TRUE) { netw_close(&l); return NULL; } netw_setsockopt(l, NETWORK_HANDSHAKE_TIMEOUT, timeout_ms); return l; }
/* Sleep until the main thread sets job->stop, at most 10 s. / static void wait_stop(CLIENT_JOB job) { int i; for (i = 0; i < 1000 && !job->stop; i++) u_sleep(10000); }
/* Connect and send nothing. / static void silent_client(void* p) { CLIENT_JOB* job = (CLIENT_JOB*)p; SOCKET c = raw_client(job->port); wait_stop(job); if (c != INVALID_SOCKET) closesocket(c); return NULL; }
/* Connect and drip the start of a ClientHello, one byte every 100 ms: each
- byte would restart a per-read timeout, never the whole-handshake deadline. / static void drip_client(void* p) { static const unsigned char hello[] = {0x16, 0x03, 0x01, 0x02, 0x00, 0x01, 0x00, 0x01, 0xfc, 0x03, 0x03, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15}; CLIENT_JOB* job = (CLIENT_JOB*)p; SOCKET c = raw_client(job->port); size_t i; for (i = 0; c != INVALID_SOCKET && i < sizeof(hello) && !job->stop; i++) { if (send(c, (const char*)hello + i, 1, 0) != 1) break; u_sleep(100000); } wait_stop(job); if (c != INVALID_SOCKET) closesocket(c); return NULL; }
/* A real TLS client: handshake, read the server's one byte, close. / static void tls_client(void* p) { CLIENT_JOB* job = (CLIENT_JOB*)p; NETWORK* c = NULL; char port[8]; snprintf(port, sizeof(port), "%u", job->port); /* netw_ssl_connect_client() only connects and sets up the context; the * handshake is netw_ssl_do_handshake(). Verification is off by default. / if (netw_ssl_connect_client(&c, "127.0.0.1", port, NETWORK_IPV4) == TRUE && c && netw_ssl_do_handshake(c, "localhost") == TRUE) { char b = 0; job->connected = 1; SSL_read(c->ssl, &b, 1); } / no wait_stop() here: the server's netw_close() waits for this close */ if (c) netw_close(&c); return NULL; }
/* A client that never completes the handshake is dropped at the deadline. / static void test_handshake_dropped(const char what, void* (client)(void)) { unsigned int lport = 0; NETWORK* l = make_tls_listener(&lport, HS_TIMEOUT_MS); CHECK(l != NULL, "%s: TLS listener with a %d ms handshake deadline", what, HS_TIMEOUT_MS); if (!l) return; CHECK(l->handshake_timeout == HS_TIMEOUT_MS, "%s: netw_setsockopt stored the deadline (%d)", what, l->handshake_timeout); CLIENT_JOB job = {lport, 0, 0}; pthread_t th; pthread_create(&th, NULL, client, &job); int ret = 0; N_TIME t; start_HiTimer(&t); NETWORK* a = netw_accept_from_ex(l, 0, 0, 2000, &ret); time_t ms = get_msec(&t); CHECK(a == NULL, "%s: connection refused", what); CHECK(ret == ETIMEDOUT, "%s: retval is ETIMEDOUT (%d)", what, ret); CHECK(ms >= HS_TIMEOUT_MS - 50 && ms < HS_LATEST_MS, "%s: dropped after %ld ms (deadline %d ms)", what, (long)ms, HS_TIMEOUT_MS); if (a) netw_close(&a); job.stop = 1; pthread_join(th, NULL); netw_close(&l); }
/* A real client completes the handshake, deadline set or not. / static void test_handshake_ok(int timeout_ms) { unsigned int lport = 0; NETWORK l = make_tls_listener(&lport, timeout_ms); CHECK(l != NULL, "real client, deadline %d ms: TLS listener", timeout_ms); if (!l) return; CLIENT_JOB job = {lport, 0, 0}; pthread_t th; pthread_create(&th, NULL, tls_client, &job); int ret = 0; NETWORK* a = netw_accept_from_ex(l, 0, 0, 5000, &ret); CHECK(a != NULL, "real client, deadline %d ms: accepted (retval %d)", timeout_ms, ret); if (a) { CHECK(a->link.is_blocking == 1, "real client, deadline %d ms: socket blocking again", timeout_ms); CHECK(SSL_write(a->ssl, "x", 1) == 1, "real client, deadline %d ms: TLS write after the handshake", timeout_ms); } /* close our side before joining: the client's netw_close() waits for it */ if (a) netw_close(&a); job.stop = 1; pthread_join(th, NULL); CHECK(job.connected == 1, "real client, deadline %d ms: client side completed", timeout_ms); netw_close(&l); }
/* All the TLS handshake checks. */ static void test_handshake_timeout(void) { netw_init_openssl(); if (n_x509_generate_ca("Nilorea Accept Test CA", 30, &ca_cert, &ca_key) != 0 || n_x509_mint_host_cert("localhost", ca_cert, ca_key, 30, &leaf_cert, &leaf_key) != 0) { n_log(LOG_ERR, "FAIL: could not mint the test certificates"); failures++; } else { test_handshake_dropped("silent client", silent_client); test_handshake_dropped("dripping client", drip_client); test_handshake_ok(2000); test_handshake_ok(0); } free_nstr(&ca_cert); free_nstr(&ca_key); free_nstr(&leaf_cert); free_nstr(&leaf_key); netw_unload_openssl(); } #endif
int main(int argc, char** argv) { set_log_level(LOG_NOTICE); process_args(argc, argv); if (netw_init_wsa(1, 2, 2) == FALSE) { n_log(LOG_ERR, "WSA init failed"); return 1; }
test_peer_port();
test_sockopt_timeo();
#ifdef HAVE_OPENSSL test_handshake_timeout(); #else n_log(LOG_NOTICE, "TLS handshake checks skipped: built without OpenSSL"); #endif
netw_init_wsa(0, 2, 2);
if (failures) {
n_log(LOG_ERR, "%d check(s) failed", failures);
return 1;
}
n_log(LOG_NOTICE, "all checks passed");
return 0;
}
_Filename: ex_network_accept.c_
Modules
- AABB3D
- ACCEPT_POOL
- ALLEGRO4
- ALLEGRO5
- ANIM_LIB
- ASTAR
- ATLAS_LIB
- AVRO
- CLOCK_SYNC
- COMMONS
- CONFIGFILE
- CYPHER_BASE64
- CYPHER_VIGENERE
- DEAD_RECKONING
- ENUMS
- EXCEPTIONS
- GAME_ENV
- GIT
- GUI
- HASH_TABLE
- ISOMETRIC_ENGINE
- LIST
- LOG
- LOGNODUP
- LZ4
- NETWORKING
- NETWORK_MSG
- N_COOKIES
- N_DIFF
- N_DIGEST
- N_DISK_CACHE
- N_DNS
- N_ECC
- N_ENTROPY
- N_FILES
- N_FLUIDS
- N_GUI_COLOR
- N_GUI_COLORWHEEL
- N_GUI_CURVE
- N_GUI_GRID
- N_GUI_POPUP
- N_HEX
- N_HTTP3
- N_HTTP_COOKIE
- N_HTTP_FORM
- N_HTTP_MIME
- N_HTTP_REQ
- N_HTTP_RESP
- N_HTTP_ROUTER
- N_HTTP_SERVER
- N_ICC
- N_JSON
- N_KAFKA
- N_LINALG
- N_MODULE
- N_OPENSSL_COMPAT
- N_PRETTY
- N_PTH
- N_QUERY
- N_RANDOM
- N_SHA256
- N_STR
- N_TIFF
- N_TIME
- N_USER
- N_X509
- N_ZIP
- PARTICLES
- PCRE
- PHYSICS
- SIGNALS
- STACK
- THREADS
- TRAJECTORY
- TREE
- ZLIB