From b4f9c5cbf230aac091e24942ec524c36d3efe152 Mon Sep 17 00:00:00 2001 From: Andy Green Date: Fri, 19 Jun 2026 17:16:09 +0100 Subject: [PATCH 1/2] h2-h3-happy-eyeballs --- lib/core-net/client/connect2.c | 6 +- lib/core-net/client/connect3.c | 129 +++++- lib/core-net/private-lib-core-net.h | 5 + lib/core-net/service.c | 8 + lib/core/context.c | 12 + lib/core/private-lib-core.h | 8 + lib/roles/private-lib-roles.h | 4 + lib/roles/quic/ops-quic.c | 34 +- lib/system/async-dns/async-dns.c | 18 +- lib/tls/gnutls/gnutls-quic.c | 36 +- .../api-test-happy-eyeballs/CMakeLists.txt | 37 ++ .../api-tests/api-test-happy-eyeballs/main.c | 376 ++++++++++++++++++ 12 files changed, 647 insertions(+), 26 deletions(-) create mode 100644 minimal-examples-lowlevel/api-tests/api-test-happy-eyeballs/CMakeLists.txt create mode 100644 minimal-examples-lowlevel/api-tests/api-test-happy-eyeballs/main.c diff --git a/lib/core-net/client/connect2.c b/lib/core-net/client/connect2.c index 4ec38301e..bd1273806 100644 --- a/lib/core-net/client/connect2.c +++ b/lib/core-net/client/connect2.c @@ -490,7 +490,11 @@ lws_client_connect_2_dnsreq_MAY_CLOSE_WSI(struct lws *wsi) #if defined(LWS_WITH_TLS) - /* we will skip HTTPS DNS query on this wsi to prevent double binding */ + if (wsi->tls.use_ssl & LCCSCF_USE_SSL) { + lws_async_dns_query(wsi->a.context, wsi->tsi, adsin, + LWS_ADNS_RECORD_HTTPS, lws_client_connect_3_https_cb, + NULL, wsi, NULL); + } #endif n = lws_async_dns_query(wsi->a.context, wsi->tsi, adsin, LWS_ADNS_RECORD_A, lws_client_connect_3_connect, diff --git a/lib/core-net/client/connect3.c b/lib/core-net/client/connect3.c index e1f746e15..c9b742b91 100644 --- a/lib/core-net/client/connect3.c +++ b/lib/core-net/client/connect3.c @@ -73,6 +73,58 @@ lws_client_happy_eyeballs_cb(lws_sorted_usec_list_t *sul) lws_client_connect_3_connect(wsi, NULL, NULL, 0, NULL); } +void +lws_client_h3_grace_cb(lws_sorted_usec_list_t *sul) +{ + struct lws *wsi = lws_container_of(sul, struct lws, sul_h3_grace); + + lwsl_wsi_notice(wsi, "H3 grace timer expired, abandoning QUIC race"); + + /* Mark H3 as FAILED in cache with 5s TTL */ + if (wsi->a.context->h3_cap_cache && wsi->stash && wsi->stash->cis[CIS_HOST]) { + lws_h3_state_t state = LWS_H3_STATE_FAILED_IGNORE; + lws_cache_write_through(wsi->a.context->h3_cap_cache, wsi->stash->cis[CIS_HOST], + (const uint8_t *)&state, sizeof(state), + lws_now_usecs() + (5000000ll), NULL); + } + + /* Abort QUIC and revert to TCP */ + if (wsi->role_ops && strcmp(wsi->role_ops->name, "quic") == 0) { + if (lws_socket_is_valid(wsi->desc.sockfd)) { + struct lws_context_per_thread *pt = &wsi->a.context->pt[(int)wsi->tsi]; + lws_pt_lock(pt, __func__); + __remove_wsi_socket_from_fds(wsi); + lws_pt_unlock(pt); + compatible_close(wsi->desc.sockfd); + wsi->desc.sockfd = LWS_SOCK_INVALID; + } + + const struct lws_role_ops *r = lws_role_by_name("h2"); + if (!r) r = lws_role_by_name("h1"); + if (r) { + lws_role_transition(wsi, LWSIFR_CLIENT, LRS_WAITING_CONNECT, r); + } + + /* Promote the first parallel TCP connection */ + int first_valid = -1; + for (int i = 0; i < wsi->parallel_count; i++) { + if (wsi->parallel_conns[i].is_valid) { + first_valid = i; + break; + } + } + if (first_valid != -1) { + wsi->desc.sockfd = wsi->parallel_conns[first_valid].desc.sockfd; + wsi->position_in_fds_table = wsi->parallel_conns[first_valid].position_in_fds_table; + wsi->parallel_conns[first_valid].is_valid = 0; + /* We changed the primary fd, the event loop will trigger POLLOUT if it's connected */ + } else { + /* No TCP sockets survived? Fail connection. */ + lws_client_connect_3_connect(wsi, NULL, NULL, 0, NULL); + } + } +} + void lws_client_dns_retry_timeout(lws_sorted_usec_list_t *sul) { @@ -229,6 +281,21 @@ promote_parallel_fd(struct lws *wsi, int pidx) } #endif +struct lws * +lws_client_connect_3_https_cb(struct lws *wsi, const char *ads, + const struct addrinfo *result, int n, void *opaque) +{ + struct lws *real_wsi = (struct lws *)opaque; + if (n == 0 && result && real_wsi->a.context->h3_cap_cache) { + lws_h3_state_t state = LWS_H3_STATE_HTTPS_RECORD_EXISTS; + /* Cache the capability with a 1 hour TTL */ + lws_cache_write_through(real_wsi->a.context->h3_cap_cache, ads, + (const uint8_t *)&state, sizeof(state), + lws_now_usecs() + (3600ll * LWS_US_PER_SEC), NULL); + } + return real_wsi; +} + struct lws * lws_client_connect_3_connect(struct lws *wsi, const char *ads, const struct addrinfo *result, int n, void *opaque) @@ -323,9 +390,10 @@ lws_client_connect_3_connect(struct lws *wsi, const char *ads, struct lws_pollfd *pollfd = (struct lws_pollfd *)opaque; lws_sockfd_type check_fd = pollfd ? pollfd->fd : LWS_SOCK_INVALID; - if (lwsi_state(wsi) == LRS_WAITING_CONNECT && + int is_quic_race = (wsi->role_ops && !strcmp(wsi->role_ops->name, "quic") && wsi->sul_h3_grace.list.owner); + if ((lwsi_state(wsi) == LRS_WAITING_CONNECT || is_quic_race) && (lws_socket_is_valid(wsi->desc.sockfd) || wsi->parallel_count > 0)) { - if (!wsi->sul_connect_timeout.list.owner) + if (lwsi_state(wsi) == LRS_WAITING_CONNECT && !wsi->sul_connect_timeout.list.owner) /* no ongoing timeout for one */ goto connect_to; @@ -343,6 +411,19 @@ lws_client_connect_3_connect(struct lws *wsi, const char *ads, switch (lws_client_connect_check(wsi, check_fd, &real_errno)) { case LCCCR_CONNECTED: + if (is_quic_race && pidx != -1) { + int saved_pos_tmp = wsi->position_in_fds_table; + lws_sock_file_fd_type saved_fd_tmp = wsi->desc; + lwsl_wsi_notice(wsi, "TCP connected, waiting for QUIC grace"); + wsi->desc.sockfd = wsi->parallel_conns[pidx].desc.sockfd; + wsi->position_in_fds_table = wsi->parallel_conns[pidx].position_in_fds_table; + if (lws_change_pollfd(wsi, LWS_POLLOUT, 0)) { + /* ignore */ + } + wsi->desc = saved_fd_tmp; + wsi->position_in_fds_table = saved_pos_tmp; + return NULL; + } lws_sul_cancel(&wsi->sul_happy_eyeballs); if (pidx != -1) promote_parallel_fd(wsi, pidx); @@ -496,13 +577,13 @@ lws_client_connect_3_connect(struct lws *wsi, const char *ads, #endif { af = wsi->sa46_peer.sa4.sin_family; + int want_udp = 0; #if defined(LWS_WITH_UDP) - new_fd = socket(wsi->sa46_peer.sa4.sin_family, - (wsi->udp || !strcmp(wsi->role_ops->name, "quic")) ? SOCK_DGRAM : SOCK_STREAM, 0); + want_udp = wsi->udp || (wsi->role_ops && !strcmp(wsi->role_ops->name, "quic") && !is_parallel); #else - new_fd = socket(wsi->sa46_peer.sa4.sin_family, - (!strcmp(wsi->role_ops->name, "quic")) ? SOCK_DGRAM : SOCK_STREAM, 0); + want_udp = (wsi->role_ops && !strcmp(wsi->role_ops->name, "quic") && !is_parallel); #endif + new_fd = socket(wsi->sa46_peer.sa4.sin_family, want_udp ? SOCK_DGRAM : SOCK_STREAM, 0); } if (!lws_socket_is_valid(new_fd)) { @@ -871,6 +952,27 @@ lws_client_connect_3_connect(struct lws *wsi, const char *ads, #endif return wsi; + } else if (!is_parallel && wsi->role_ops && !strcmp(wsi->role_ops->name, "quic")) { + /* QUIC connect immediately succeeds. Schedule grace and happy eyeballs. */ + uint32_t grace_us = 200000; + if (wsi->a.context->h3_cap_cache && wsi->stash && wsi->stash->cis[CIS_HOST]) { + const void *item = NULL; + size_t item_len = 0; + if (!lws_cache_item_get(wsi->a.context->h3_cap_cache, wsi->stash->cis[CIS_HOST], &item, &item_len)) { + lws_h3_state_t state = *(lws_h3_state_t *)item; + if (state == LWS_H3_STATE_KNOWN_GOOD || state == LWS_H3_STATE_HTTPS_RECORD_EXISTS) + grace_us = 3000000; + } + } + lwsl_wsi_notice(wsi, "QUIC socket created, starting grace timer %uus", grace_us); + lws_sul_schedule(wsi->a.context, wsi->tsi, &wsi->sul_h3_grace, + lws_client_h3_grace_cb, grace_us); + + if (wsi->dns_sorted_list.count) { + extern void lws_client_happy_eyeballs_cb(lws_sorted_usec_list_t *sul); + lws_sul_schedule(wsi->a.context, wsi->tsi, &wsi->sul_happy_eyeballs, + lws_client_happy_eyeballs_cb, 1); + } } conn_good: @@ -891,13 +993,18 @@ lws_client_connect_3_connect(struct lws *wsi, const char *ads, } #if defined(LWS_WITH_CLIENT) - /* kill all remaining parallel connections */ - for (int i = 0; i < wsi->parallel_count; i++) { - if (wsi->parallel_conns[i].is_valid) { - lws_remove_parallel_fd_safely(wsi, i); + int is_quic_race = (wsi->role_ops && !strcmp(wsi->role_ops->name, "quic") && wsi->sul_h3_grace.list.owner); + if (!is_quic_race) { + /* kill all remaining parallel connections */ + for (int i = 0; i < wsi->parallel_count; i++) { + if (wsi->parallel_conns[i].is_valid) { + lws_remove_parallel_fd_safely(wsi, i); + } } + wsi->parallel_count = 0; + } else { + lwsl_wsi_notice(wsi, "QUIC reached conn_good, keeping %d parallel TCP sockets alive", wsi->parallel_count); } - wsi->parallel_count = 0; #endif /* diff --git a/lib/core-net/private-lib-core-net.h b/lib/core-net/private-lib-core-net.h index 12524f0ba..8de5a9b42 100644 --- a/lib/core-net/private-lib-core-net.h +++ b/lib/core-net/private-lib-core-net.h @@ -356,6 +356,7 @@ struct lws_context_per_thread { lws_sorted_usec_list_t sul_peer_limits; #endif + #if !defined(LWS_PLAT_FREERTOS) struct lws *fake_wsi; /* used for callbacks where there's no wsi */ #endif @@ -875,6 +876,7 @@ struct lws { struct lws_client_parallel_conn parallel_conns[LWS_MAX_PARALLEL_CONNS]; lws_sorted_usec_list_t sul_happy_eyeballs; + lws_sorted_usec_list_t sul_h3_grace; uint8_t parallel_count; @@ -1017,6 +1019,9 @@ struct lws { char tsi; /* thread service index we belong to */ char protocol_interpret_idx; char redirects; +#if defined(LWS_WITH_CLIENT) + char conn_race_log[32]; +#endif uint8_t rxflow_bitmap; uint8_t bound_vhost_index; uint8_t lsp_channel; /* which of stdin/out/err */ diff --git a/lib/core-net/service.c b/lib/core-net/service.c index 3d0a8b0c5..76079891f 100644 --- a/lib/core-net/service.c +++ b/lib/core-net/service.c @@ -146,6 +146,14 @@ lws_handle_POLLOUT_event(struct lws *wsi, struct lws_pollfd *pollfd) if (wsi->socket_is_permanently_unusable) return 0; +#if defined(LWS_WITH_CLIENT) + /* Intercept POLLOUT for parallel sockets if we are racing H3 */ + if (pollfd && pollfd->fd != wsi->desc.sockfd) { + lws_client_connect_3_connect(wsi, NULL, NULL, 0, pollfd); + return 0; + } +#endif + vwsi->leave_pollout_active = 0; vwsi->handling_pollout = 1; /* diff --git a/lib/core/context.c b/lib/core/context.c index bcd49c9cb..d2cd21cc8 100644 --- a/lib/core/context.c +++ b/lib/core/context.c @@ -1622,6 +1622,17 @@ lws_create_context(const struct lws_context_creation_info *info) lwsl_cx_err(context, "Failed to init ALPN cache"); goto bail; } + + cci.name = "H3CAP"; + cci.max_footprint = 4096; + cci.max_items = 256; + cci.max_payload = 8; /* Just an enum */ + + context->h3_cap_cache = lws_cache_create(&cci); + if (!context->h3_cap_cache) { + lwsl_cx_err(context, "Failed to init H3CAP cache"); + goto bail; + } } #endif @@ -2455,6 +2466,7 @@ lws_context_destroy(struct lws_context *context) #if defined(LWS_WITH_CLIENT) lws_cache_destroy(&context->alpn_cache); + lws_cache_destroy(&context->h3_cap_cache); #endif diff --git a/lib/core/private-lib-core.h b/lib/core/private-lib-core.h index baabff182..8fcab94d3 100644 --- a/lib/core/private-lib-core.h +++ b/lib/core/private-lib-core.h @@ -435,6 +435,13 @@ typedef struct lws_buflist { void *heap_alloc; } lws_buflist_t; +typedef enum { + LWS_H3_STATE_UNKNOWN, + LWS_H3_STATE_HTTPS_RECORD_EXISTS, + LWS_H3_STATE_ALTSVC_EXISTS, + LWS_H3_STATE_KNOWN_GOOD, + LWS_H3_STATE_FAILED_IGNORE +} lws_h3_state_t; /* * the rest is managed per-context, that includes @@ -588,6 +595,7 @@ struct lws_context { #if defined(LWS_WITH_CLIENT) struct lws_cache_ttl_lru *alpn_cache; + struct lws_cache_ttl_lru *h3_cap_cache; #endif #if defined(LWS_WITH_SYS_NTPCLIENT) diff --git a/lib/roles/private-lib-roles.h b/lib/roles/private-lib-roles.h index ce52e9ed4..e6c44cffd 100644 --- a/lib/roles/private-lib-roles.h +++ b/lib/roles/private-lib-roles.h @@ -457,3 +457,7 @@ lws_client_connect_4_established(struct lws *wsi, struct lws *wsi_piggyback, struct lws * lws_client_connect_3_connect(struct lws *wsi, const char *ads, const struct addrinfo *result, int n, void *opaque); + +struct lws * +lws_client_connect_3_https_cb(struct lws *wsi, const char *ads, + const struct addrinfo *result, int n, void *opaque); diff --git a/lib/roles/quic/ops-quic.c b/lib/roles/quic/ops-quic.c index bb050d960..05f7200dd 100644 --- a/lib/roles/quic/ops-quic.c +++ b/lib/roles/quic/ops-quic.c @@ -1038,7 +1038,15 @@ rops_handle_POLLOUT_quic(struct lws *wsi) #if defined(LWS_WITH_TLS) if (wsi->tls.use_ssl & LCCSCF_USE_SSL) { - /* The BIO was already created in connect.c, just init QUIC TLS */ + if (!wsi->tls.ssl) { + const char *cce = NULL; + if (lws_client_create_tls(wsi, &cce, 0) == CCTLS_RETURN_ERROR) { + lwsl_wsi_err(wsi, "Failed to create TLS BIO: %s", cce ? cce : "unknown"); + return LWS_HP_RET_BAIL_DIE; + } + } + + /* The BIO was already created, just init QUIC TLS */ if (lws_tls_quic_init(wsi, quic_secret_cb)) { lwsl_wsi_err(wsi, "Failed to init QUIC TLS"); return LWS_HP_RET_BAIL_DIE; @@ -2363,6 +2371,30 @@ rops_alpn_negotiated_quic(struct lws *wsi, const char *alpn) #endif nwsi->quic.qn->alpn_migrated = 1; +#if defined(LWS_WITH_CLIENT) + /* + * QUIC succeeded! Resolve the race by cancelling the grace timer + * and killing parallel TCP connections. + */ + if (lwsi_role_client(wsi)) { + lws_sul_cancel(&wsi->sul_h3_grace); + + for (int i = 0; i < wsi->parallel_count; i++) { + if (wsi->parallel_conns[i].is_valid) { + lws_remove_parallel_fd_safely(wsi, i); + } + } + wsi->parallel_count = 0; + + if (wsi->a.context->h3_cap_cache && wsi->stash && wsi->stash->cis[CIS_HOST]) { + lws_h3_state_t state = LWS_H3_STATE_KNOWN_GOOD; + lws_cache_write_through(wsi->a.context->h3_cap_cache, wsi->stash->cis[CIS_HOST], + (const uint8_t *)&state, sizeof(state), + lws_now_usecs() + (3600ll * LWS_US_PER_SEC), NULL); + } + } +#endif + /* * The quic child stream is migrating to be a child of nwsi. * So we disconnect it from the listener socket first. diff --git a/lib/system/async-dns/async-dns.c b/lib/system/async-dns/async-dns.c index b8da51712..5aeb7a2e3 100644 --- a/lib/system/async-dns/async-dns.c +++ b/lib/system/async-dns/async-dns.c @@ -1250,16 +1250,22 @@ lws_async_dns_query(struct lws_context *context, int tsi, const char *name, c = (qtype & LWS_ADNS_NOCACHE) ? NULL : lws_adns_get_cache(dns, name); qtype = (adns_query_type_t)(qtype & ~(uint32_t)LWS_ADNS_NOCACHE); - if (c && qtype != LWS_ADNS_RECORD_A && qtype != LWS_ADNS_RECORD_AAAA) { - lws_adns_rr_t *rr = c->rr_results; + if (c) { int found = 0; - while (rr) { - if (rr->type == qtype) { + if (qtype == LWS_ADNS_RECORD_A || qtype == LWS_ADNS_RECORD_AAAA) { + if (c->results) found = 1; - break; + } else { + lws_adns_rr_t *rr = c->rr_results; + while (rr) { + if (rr->type == qtype) { + found = 1; + break; + } + rr = rr->next; } - rr = rr->next; } + if (!found) { lwsl_cx_info(context, "%s: cached but missing 0x%x, bypassing", name, qtype); lws_dll2_remove(&c->list); /* Remove from cache list, let sul expire it */ diff --git a/lib/tls/gnutls/gnutls-quic.c b/lib/tls/gnutls/gnutls-quic.c index d4129a596..fba30144c 100644 --- a/lib/tls/gnutls/gnutls-quic.c +++ b/lib/tls/gnutls/gnutls-quic.c @@ -252,13 +252,17 @@ lws_tls_quic_init(struct lws *wsi, lws_tls_quic_secret_cb cb) struct gnutls_quic_bio *b; gnutls_session_t session; - if (!wsi->tls.ssl) + if (!wsi->tls.ssl) { + lwsl_err("lws_tls_quic_init: wsi->tls.ssl is NULL\n"); return -1; + } int ret; session = (gnutls_session_t)wsi->tls.ssl; - if (!session) + if (!session) { + lwsl_err("lws_tls_quic_init: session is NULL\n"); return -1; + } /* Enforce QUIC requirements: TLS 1.3 only, NO compatibility mode (empty legacy_session_id) */ ret = gnutls_priority_set_direct(session, "NORMAL:-VERS-ALL:+VERS-TLS1.3:%DISABLE_TLS13_COMPAT_MODE", NULL); @@ -302,10 +306,26 @@ lws_tls_quic_init(struct lws *wsi, lws_tls_quic_secret_cb cb) } if (wsi->alpn[0]) { - gnutls_datum_t alpn; - alpn.data = (uint8_t *)wsi->alpn; - alpn.size = (unsigned int)strlen(wsi->alpn); - gnutls_alpn_set_protocols(session, &alpn, 1, GNUTLS_ALPN_MANDATORY); + gnutls_datum_t alpn[4]; + unsigned int i = 0; + char *p = wsi->alpn; + char *end = p + strlen(p); + + while (p < end && i < 4) { + char *comma = strchr(p, ','); + alpn[i].data = (uint8_t *)p; + if (comma) { + alpn[i].size = (unsigned int)(comma - p); + p = comma + 1; + } else { + alpn[i].size = (unsigned int)(end - p); + p = end; + } + /* Replace comma with NUL for cleaner logging, though gnutls only uses size */ + if (comma) *comma = '\0'; + i++; + } + gnutls_alpn_set_protocols(session, alpn, i, GNUTLS_ALPN_MANDATORY); } else if (wsi->a.vhost && wsi->a.vhost->tls.alpn_ctx.len) { gnutls_datum_t alpn[4]; unsigned int i = 0, p = 0; @@ -321,8 +341,10 @@ lws_tls_quic_init(struct lws *wsi, lws_tls_quic_secret_cb cb) wsi->tls.quic_secret_cb = cb; b = lws_zalloc(sizeof(*b), "quic bio"); - if (!b) + if (!b) { + lwsl_err("lws_tls_quic_init: failed to allocate bio\n"); return -1; + } b->session = session; diff --git a/minimal-examples-lowlevel/api-tests/api-test-happy-eyeballs/CMakeLists.txt b/minimal-examples-lowlevel/api-tests/api-test-happy-eyeballs/CMakeLists.txt new file mode 100644 index 000000000..b01e2ab03 --- /dev/null +++ b/minimal-examples-lowlevel/api-tests/api-test-happy-eyeballs/CMakeLists.txt @@ -0,0 +1,37 @@ +project(lws-api-test-happy-eyeballs C) +cmake_minimum_required(VERSION 3.10) +find_package(libwebsockets CONFIG REQUIRED) +list(APPEND CMAKE_MODULE_PATH ${LWS_CMAKE_DIR}) +include(CheckCSourceCompiles) +include(LwsCheckRequirements) + +set(SAMP lws-api-test-happy-eyeballs) +set(SRCS main.c) + +set(requirements 1) +require_lws_config(LWS_ROLE_QUIC 1 requirements) +require_lws_config(LWS_ROLE_H2 1 requirements) +require_lws_config(LWS_WITH_CLIENT 1 requirements) +require_lws_config(LWS_WITH_SERVER 1 requirements) +require_lws_config(LWS_WITH_SYS_ASYNC_DNS 1 requirements) + +message("api-test-happy-eyeballs requirements: ${requirements}") + +if (requirements) + add_executable(${SAMP} ${SRCS}) + + if (websockets_shared) + target_link_libraries(${SAMP} websockets_shared ${LIBWEBSOCKETS_DEP_LIBS}) + add_dependencies(${SAMP} websockets_shared) + else() + target_link_libraries(${SAMP} websockets ${LIBWEBSOCKETS_DEP_LIBS}) + endif() + + add_test(NAME api-test-happy-eyeballs COMMAND lws-api-test-happy-eyeballs) + set_tests_properties(api-test-happy-eyeballs + PROPERTIES + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/minimal-examples-lowlevel/api-tests/api-test-happy-eyeballs + TIMEOUT 60) +endif() + + diff --git a/minimal-examples-lowlevel/api-tests/api-test-happy-eyeballs/main.c b/minimal-examples-lowlevel/api-tests/api-test-happy-eyeballs/main.c new file mode 100644 index 000000000..99b3b1c7d --- /dev/null +++ b/minimal-examples-lowlevel/api-tests/api-test-happy-eyeballs/main.c @@ -0,0 +1,376 @@ +/* + * lws-api-test-happy-eyeballs + * + * Written in 2010-2026 by Andy Green + * + * This file is made available under the Creative Commons CC0 1.0 + * Universal Public Domain Dedication. + */ + +#include +#include +#include + +static int interrupted; +static int result = 1; +static struct lws_context *context; +static struct lws_vhost *vh_quic_server; +static struct lws_vhost *vh_tcp_server; + + + +static const char * const test_cert = +"-----BEGIN CERTIFICATE-----\n" +"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\n" +"e2Hl/7M0c/lbZ4FvXgARlex2rkgS0Ka06HE=\n" +"-----END CERTIFICATE-----\n"; + +static const char * const test_key = +"-----BEGIN PRIVATE KEY-----\n" +"MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCjYtuWaICCY0tJ\n" +"PubxpIgIL+WWmz/fmK8IQr11Wtee6/IUyUlo5I602mq1qcLhT/kmpoR8Di3DAmHK\n" +"nSWdPWtn1BtXLErLlUiHgZDrZWInmEBjKM1DZf+CvNGZ+EzPgBv5nTekLWcfI5ZZ\n" +"toGuIP1Dl/IkNDw8zFz4cpiMe/BFGemyxdHhLrKHSm8Eo+nT734tItnHKT/m6DSU\n" +"0xlZ13d6ehLRm7/+Nx47M3XMTRH5qKP/7TTE2s0U6+M0tsGI2zpRi+m6jzhNyMBT\n" +"J1u58qAe3ZW5/+YAiuZYAB6n5bhUp4oFuB5wYbcBywVR8ujInpF8buWQUjy5N8pS\n" +"Np7szdYsnLJpvAd0sibrNPjC0FQCNrpNjgJmIK3+mKk4kXX7ZTwefoAzTK4l2pHN\n" +"uC53QVc/EF++GBLAxmvCDq9ZpMIYi7OmzkkAKKC9Ue6Ef217LFQCFIBKIzv9cgi9\n" +"fwPMLhrKleoVRNsecBsCP569WgJXhUnwf2lon4fEZr3+vRuc9shfqnV0nPN1IMSn\n" +"zXCast7I2fiuRXdIz96KjlGQpP4XfNVA+RGL7aMnWOFIaVrKWLzAtgzoGMTvP/Au\n" +"ehKXncBJhYtW0ltTioVx+5yTYSAZWl+IssmXjefxJqYi2/7QWmv1QC9psNcjTMaB\n" +"QLN03T1Qelbs7Y27sxdEnNUth4kI+wIDAQABAoICAFWe8MQZb37k2gdAV3Y6aq8f\n" +"qokKQqbCNLd3giGFwYkezHXoJfg6Di7oZxNcKyw35LFEghkgtQqErQqo35VPIoH+\n" +"vXUpWOjnCmM4muFA9/cX6mYMc8TmJsg0ewLdBCOZVw+wPABlaqz+0UOiSMMftpk9\n" +"fz9JwGd8ERyBsT+tk3Qi6D0vPZVsC1KqxxL/cwIFd3Hf2ZBtJXe0KBn1pktWht5A\n" +"Kqx9mld2Ovl7NjgiC1Fx9r+fZw/iOabFFwQA4dr+R8mEMK/7bd4VXfQ1o/QGGbMT\n" +"G+ulFrsiDyP+rBIAaGC0i7gDjLAIBQeDhP409ZhswIEc/GBtODU372a2CQK/u4Q/\n" +"HBQvuBtKFNkGUooLgCCbFxzgNUGc83GB/6IwbEM7R5uXqsFiE71LpmroDyjKTlQ8\n" +"YZkpIcLNVLw0usoGYHFm2rvCyEVlfsE3Ub8cFyTFk50SeOcF2QL2xzKmmbZEpXgl\n" +"xBHR0hjgon0IKJDGfor4bHO7Nt+1Ece8u2oTEKvpz5aIn44OeC5mApRGy83/0bvs\n" +"esnWjDE/bGpoT8qFuy+0urDEPNId44XcJm1IRIlG56ErxC3l0s11wrIpTmXXckqw\n" +"zFR9s2z7f0zjeyxqZg4NTPI7wkM3M8BXlvp2GTBIeoxrWB4V3YArwu8QF80QBgVz\n" +"mgHl24nTg00UH1OjZsABAoIBAQDOxftSDbSqGytcWqPYP3SZHAWDA0O4ACEM+eCw\n" +"au9ASutl0IDlNDMJ8nC2ph25BMe5hHDWp2cGQJog7pZ/3qQogQho2gUniKDifN77\n" +"40QdykllTzTVROqmP8+efreIvqlzHmuqaGfGs5oTkZaWj5su+B+bT+9rIwZcwfs5\n" +"YRINhQRx17qa++xh5mfE25c+M9fiIBTiNSo4lTxWMBShnK8xrGaMEmN7W0qTMbFH\n" +"PgQz5FcxRjCCqwHilwNBeLDTp/ZECEB7y34khVh531mBE2mNzSVIQcGZP1I/DvXj\n" +"W7UUNdgFwii/GW+6M0uUDy23UVQpbFzcV8o1C2nZc4Fb4zwBAoIBAQDKSJkFwwuR\n" +"naVJS6WxOKjX8MCu9/cKPnwBv2mmI2jgGxHTw5sr3ahmF5eTb8Zo19BowytN+tr6\n" +"2ZFoIBA9Ubc9esEAU8l3fggdfM82cuR9sGcfQVoCh8tMg6BP8IBLOmbSUhN3PG2m\n" +"39I802u0fFNVQCJKhx1m1MFFLOu7lVcDS9JN+oYVPb6MDfBLm5jOiPuYkFZ4gH79\n" +"J7gXI0/YKhaJ7yXthYVkdrSF6Eooer4RZgma62Dd1VNzSq3JBo6rYjF7Lvd+RwDC\n" +"R1thHrmf/IXplxpNVkoMVxtzbrrbgnC25QmvRYc0rlS/kvM4yQhMH3eA7IycDZMp\n" +"Y+0xm7I7jTT7AoIBAGKzKIMDXdCxBWKhNYJ8z7hiItNl1IZZMW2TPUiY0rl6yaCh\n" +"BVXjM9W0r07QPnHZsUiByqb743adkbTUjmxdJzjaVtxN7ZXwZvOVrY7I7fPWYnCE\n" +"fXCr4+IVpZI/ZHZWpGX6CGSgT6EOjCZ5IUufIvEpqVSmtF8MqfXO9o9uIYLokrWQ\n" +"x1dBl5UnuTLDqw8bChq7O5y6yfuWaOWvL7nxI8NvSsfj4y635gIa/0dFeBYZEfHI\n" +"UlGdNVomwXwYEzgE/c19ruIowX7HU/NgxMWTMZhpazlxgesXybel+YNcfDQ4e3RM\n" +"OMz3ZFiaMaJsGGNf4++d9TmMgk4Ns6oDs6Tb9AECggEBAJYzd+SOYo26iBu3nw3L\n" +"65uEeh6xou8pXH0Tu4gQrPQTRZZ/nT3iNgOwqu1gRuxcq7TOjt41UdqIKO8vN7/A\n" +"aJavCpaKoIMowy/aGCbvAvjNPpU3unU8jdl/t08EXs79S5IKPcgAx87sTTi7KDN5\n" +"SYt4tr2uPEe53NTXuSatilG5QCyExIELOuzWAMKzg7CAiIlNS9foWeLyVkBgCQ6S\n" +"me/L8ta+mUDy37K6vC34jh9vK9yrwF6X44ItRoOJafCaVfGI+175q/eWcqTX4q+I\n" +"G4tKls4sL4mgOJLq+ra50aYMxbcuommctPMXU6CrrYyQpPTHMNVDQy2ttFdsq9iK\n" +"TncCggEBAMmt/8yvPflS+xv3kg/ZBvR9JB1In2n3rUCYYD47ReKFqJ03Vmq5C9nY\n" +"56s9w7OUO8perBXlJYmKZQhO4293lvxZD2Iq4NcZbVSCMoHAUzhzY3brdgtSIxa2\n" +"gGveGAezZ38qKIU26dkz7deECY4vrsRkwhpTW0LGVCpjcQoaKvymAoCmAs8V2oMr\n" +"Ziw1YQ9uOUoWwOqm1wZqmVcOXvPIS2gWAs3fQlWjH9hkcQTMsUaXQDOD0aqkSY3E\n" +"NqOvbCV1/oUpRi3076khCoAXI1bKSn/AvR3KDP14B5toHI/F5OTSEiGhhHesgRrs\n" +"fBrpEY1IATtPq1taBZZogRqI3rOkkPk=\n" +"-----END PRIVATE KEY-----\n"; + +static int client_step = 0; +static struct lws *client_wsi = NULL; +static int established_success = 0; +static int next_step = 0; + +static void +start_client_connection(void) +{ + struct lws_client_connect_info i; + + memset(&i, 0, sizeof(i)); + i.context = context; + i.vhost = lws_get_vhost_by_name(context, "client"); + i.port = 7681; + i.address = "localhost"; + i.host = "localhost"; + i.origin = "localhost"; + i.ssl_connection = LCCSCF_USE_SSL | LCCSCF_ALLOW_SELFSIGNED | + LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK; + i.protocol = "http"; + if (client_step == 0) + i.alpn = "h2,http/1.1"; + else + i.alpn = "h3,h2"; + i.method = "GET"; + i.path = "/"; + + client_wsi = lws_client_connect_via_info(&i); + if (!client_wsi && !established_success) { + lwsl_err("Client connection failed for step %d\n", client_step); + result = 1; + interrupted = 1; + } else if (!client_wsi && established_success) { + established_success = 0; + client_step++; + next_step = 1; + } +} + +static int +callback_client(struct lws *wsi, enum lws_callback_reasons reason, + void *user, void *in, size_t len) +{ + switch (reason) { + case LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP: + lwsl_notice("CLIENT ESTABLISHED HTTP: step %d\n", client_step); + established_success = 1; + return -1; + + case LWS_CALLBACK_CLIENT_CONNECTION_ERROR: + lwsl_notice("CLIENT CONNECTION ERROR: %s\n", in ? (char *)in : "(null)"); + /* fallthru */ + case LWS_CALLBACK_CLIENT_CLOSED: + lwsl_notice("CLIENT CLOSED/ERROR: step %d\n", client_step); + client_wsi = NULL; + + if (established_success) { + established_success = 0; + client_step++; + next_step = 1; + } else { + lwsl_err("--- Failed to establish connection in step %d ---\n", client_step); + result = 1; + interrupted = 1; + } + break; + + default: + break; + } + + return lws_callback_http_dummy(wsi, reason, user, in, len); +} + +static struct lws_protocols protocols_client[] = { + { "http", callback_client, 0, 0, 0, NULL, 0 }, + LWS_PROTOCOL_LIST_TERM +}; + +static int +callback_quic_server(struct lws *wsi, enum lws_callback_reasons reason, + void *user, void *in, size_t len) +{ + switch (reason) { + case LWS_CALLBACK_HTTP: + { + uint8_t buf[LWS_PRE + 2048], *start = &buf[LWS_PRE], *p = start, + *end = &buf[sizeof(buf) - 1]; + + if (lws_add_http_common_headers(wsi, HTTP_STATUS_OK, + "text/html", + 13, &p, end)) + return 1; + if (lws_finalize_write_http_header(wsi, start, &p, end)) + return 1; + + uint8_t body[LWS_PRE + 16]; + memcpy(body + LWS_PRE, "hello from h3", 13); + lws_write(wsi, body + LWS_PRE, 13, LWS_WRITE_HTTP_FINAL); + if (lws_http_transaction_completed(wsi)) + return -1; + return 0; + } + default: + break; + } + return lws_callback_http_dummy(wsi, reason, user, in, len); +} + +static int +callback_tcp_server(struct lws *wsi, enum lws_callback_reasons reason, + void *user, void *in, size_t len) +{ + switch (reason) { + case LWS_CALLBACK_HTTP: + { + uint8_t buf[LWS_PRE + 2048], *start = &buf[LWS_PRE], *p = start, + *end = &buf[sizeof(buf) - 1]; + + if (lws_add_http_common_headers(wsi, HTTP_STATUS_OK, + "text/html", + 13, &p, end)) + return 1; + /* Inject Alt-Svc pointing to our QUIC vhost */ + if (lws_add_http_header_by_name(wsi, (unsigned char *)"alt-svc:", + (unsigned char *)"h3=\":7682\"", 9, &p, end)) + return 1; + if (lws_finalize_write_http_header(wsi, start, &p, end)) + return 1; + + uint8_t body[LWS_PRE + 16]; + memcpy(body + LWS_PRE, "hello from h2", 13); + lws_write(wsi, body + LWS_PRE, 13, LWS_WRITE_HTTP_FINAL); + if (lws_http_transaction_completed(wsi)) + return -1; + return 0; + } + default: + break; + } + return lws_callback_http_dummy(wsi, reason, user, in, len); +} + +static struct lws_protocols protocols_quic[] = { + { "http", callback_quic_server, 0, 0, 0, NULL, 0 }, + LWS_PROTOCOL_LIST_TERM +}; + +static struct lws_protocols protocols_tcp[] = { + { "http", callback_tcp_server, 0, 0, 0, NULL, 0 }, + LWS_PROTOCOL_LIST_TERM +}; + +void sigint_handler(int sig) +{ + interrupted = 1; +} + +int main(int argc, const char **argv) +{ + struct lws_context_creation_info info; + + lws_context_info_defaults(&info, NULL); + info.fd_limit_per_thread = 0; + lws_cmdline_option_handle_builtin(argc, argv, &info); + + signal(SIGINT, sigint_handler); + + info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT | + LWS_SERVER_OPTION_EXPLICIT_VHOSTS | + LWS_SERVER_OPTION_H2_JUST_FIX_WINDOW_UPDATE_OVERFLOW; + + context = lws_create_context(&info); + if (!context) { + lwsl_err("lws init failed\n"); + return 1; + } + + /* QUIC server */ + info.port = CONTEXT_PORT_NO_LISTEN_SERVER; + info.vhost_name = "quic-server"; + info.listen_accept_role = "quic"; + info.listen_accept_protocol = "http"; + info.alpn = "h3,lws-quic"; + info.protocols = protocols_quic; + info.server_ssl_cert_mem = test_cert; + info.server_ssl_cert_mem_len = (unsigned int)strlen(test_cert); + info.server_ssl_private_key_mem = test_key; + info.server_ssl_private_key_mem_len = (unsigned int)strlen(test_key); + + vh_quic_server = lws_create_vhost(context, &info); + if (!vh_quic_server) { + lwsl_err("Failed to create QUIC vhost\n"); + goto bail; + } + + if (!lws_create_adopt_udp(vh_quic_server, NULL, 7682, LWS_CAUDP_BIND, + "http", NULL, NULL, NULL, + NULL, "quic_listen")) { + lwsl_err("Failed to bind QUIC UDP listener\n"); + goto bail; + } + + /* TCP server */ + info.port = 7681; + info.vhost_name = "tcp-server"; + info.listen_accept_role = "h2"; + info.listen_accept_protocol = "http"; + info.alpn = "h3,h2,http/1.1"; + info.protocols = protocols_tcp; + + vh_tcp_server = lws_create_vhost(context, &info); + if (!vh_tcp_server) { + lwsl_err("Failed to create TCP vhost\n"); + goto bail; + } + + /* Client vhost */ + info.port = CONTEXT_PORT_NO_LISTEN; + info.vhost_name = "client"; + info.listen_accept_role = NULL; + info.listen_accept_protocol = NULL; + info.alpn = "h3,h2,http/1.1"; + info.protocols = protocols_client; + info.server_ssl_cert_mem = NULL; + info.server_ssl_cert_mem_len = 0; + info.server_ssl_private_key_mem = NULL; + info.server_ssl_private_key_mem_len = 0; + + if (!lws_create_vhost(context, &info)) { + lwsl_err("Failed to create Client vhost\n"); + goto bail; + } + + lwsl_notice("--- Starting Step 1: TCP connection for Alt-Svc ---\n"); + start_client_connection(); + + int n = 0; + while (n >= 0 && !interrupted) { + n = lws_service(context, 0); + + if (next_step && !client_wsi) { + next_step = 0; + if (client_step == 1) { + lwsl_notice("--- Starting Step 2: H3 success ---\n"); + start_client_connection(); + } else if (client_step == 2) { + lwsl_notice("--- Starting Step 3: H3 failure, TCP fallback ---\n"); + if (vh_quic_server) { + lws_vhost_destroy(vh_quic_server); + vh_quic_server = NULL; + } + start_client_connection(); + } else if (client_step == 3) { + lwsl_notice("--- Step 3 complete. Test passed. ---\n"); + result = 0; + interrupted = 1; + } + } + } + +bail: + lws_context_destroy(context); + return result; +} From dfaf09c613b254e3056c501926fce2298b413387 Mon Sep 17 00:00:00 2001 From: Vikram Dattu Date: Fri, 19 Jun 2026 23:20:44 +0530 Subject: [PATCH 2/2] fix(core-net): cast uint32_t grace_us for %u format specifier connect3.c logs grace_us (uint32_t) with %u. On toolchains where uint32_t is 'long unsigned int' (e.g. xtensa-esp-elf for ESP-IDF v5.3/v5.4) this trips -Werror=format and fails the build. Cast to unsigned int to match the specifier; the value (grace timer in us) always fits. --- lib/core-net/client/connect3.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/core-net/client/connect3.c b/lib/core-net/client/connect3.c index c9b742b91..4d7829f5a 100644 --- a/lib/core-net/client/connect3.c +++ b/lib/core-net/client/connect3.c @@ -964,7 +964,7 @@ lws_client_connect_3_connect(struct lws *wsi, const char *ads, grace_us = 3000000; } } - lwsl_wsi_notice(wsi, "QUIC socket created, starting grace timer %uus", grace_us); + lwsl_wsi_notice(wsi, "QUIC socket created, starting grace timer %uus", (unsigned int)grace_us); lws_sul_schedule(wsi->a.context, wsi->tsi, &wsi->sul_h3_grace, lws_client_h3_grace_cb, grace_us);