Skip to content

net.ssl: add ALPN protocol support to mbedtls and openssl backends#27343

Merged
JalonSolov merged 2 commits into
vlang:masterfrom
quaesitor-scientiam:net-ssl-alpn
Jun 6, 2026
Merged

net.ssl: add ALPN protocol support to mbedtls and openssl backends#27343
JalonSolov merged 2 commits into
vlang:masterfrom
quaesitor-scientiam:net-ssl-alpn

Conversation

@quaesitor-scientiam

@quaesitor-scientiam quaesitor-scientiam commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

What

Adds ALPN (Application-Layer Protocol Negotiation) support to V's TLS layer:

  • A new alpn_protocols []string option on the SSL connection config.
  • A new negotiated_alpn() string method to read the protocol selected during
    the TLS handshake.

Both are exposed through the net.ssl facade automatically (embedded config +
SSLConn type alias) and implemented for both the mbedtls (default) and
OpenSSL (-d use_openssl) backends.

import net.ssl

mut conn := ssl.new_ssl_conn(alpn_protocols: ['h2', 'http/1.1'])!
conn.dial('example.com', 443)!
println(conn.negotiated_alpn()) // 'h2' or 'http/1.1'

Why

This is the first, self-contained piece of HTTP/2 support: a client must
advertise h2 via ALPN and confirm the server selected it before speaking the
HTTP/2 framing protocol. Splitting it out keeps the eventual HTTP/2 work
reviewable in small steps (per the discussion on #26776), and ALPN is useful on
its own for any protocol negotiation.

Details

mbedtls (vlib/net/mbedtls/ssl_connection.c.v)

  • Replaces the previously commented-out ALPN block in SSLConn.init with a
    working implementation driven by config.alpn_protocols.
  • Also fixes the memory leak the old block left behind: its malloc'd
    protocol array had a literal TODO free alpn_list and was never released.
    The array is now owned by the connection and freed in shutdown() (mbedtls
    stores the pointer without copying, so it must outlive the config).
  • Adds the same advertisement to SSLListener, so accepted server connections
    can negotiate a protocol.
  • Adds negotiated_alpn() via mbedtls_ssl_get_alpn_protocol.

openssl (vlib/net/openssl/ssl_connection.c.v)

  • Sets the ALPN list (length-prefixed wire format) in SSLConn.init via
    SSL_set_alpn_protos.
  • Adds negotiated_alpn() via SSL_get0_alpn_selected.
  • These calls are routed through small version-guarded shims in
    openssl_compat.h (the existing v_net_openssl_* pattern), so the module
    still links against OpenSSL versions older than 1.0.2 that predate ALPN.

Portability

  • All ALPN C interop compiles cleanly under -W -cstrict -cc clang
    (const-qualifier handling for the const char ** / const char * params and
    return values).

Compatibility

No behaviour change for existing callers: an empty alpn_protocols sends no
ALPN extension, exactly as before. The additions are purely additive (new
optional config field + new method).

Tests

Adds vlib/net/ssl/ssl_alpn_test.v, a hermetic test (no network) that stands up
a local mbedtls TLS server advertising ALPN and verifies, through the net.ssl
facade client:

  • negotiation of h2 when both sides offer it,
  • fallback to http/1.1,
  • empty result when the client advertises no protocols.

Passing locally on both backends:

./vnew test vlib/net/ssl/ssl_alpn_test.v                # mbedtls
./vnew -d use_openssl test vlib/net/ssl/ssl_alpn_test.v  # openssl

vlib/net/mbedtls/ and vlib/net/ssl/ suites pass on both backends, including
under -W -cstrict -cc clang; net.http compiles on both.

Notes

  • vschannel (Windows) ALPN is not included here; it can be added separately.
    The new method returns '' and the option is ignored on that path, so HTTP/1.1
    behaviour is unaffected.

🤖 Generated with Claude Code

Add an `alpn_protocols []string` option to the SSL connection config and a
`negotiated_alpn() string` method to query the protocol selected during the
TLS handshake. This is the foundation needed for HTTP/2 (ALPN `h2`) and other
protocol negotiation, without changing behaviour for existing callers (an
empty list sends no ALPN extension, exactly as before).

mbedtls:
- Finish the previously commented-out, leaky ALPN block in SSLConn.init,
  driving it from config.alpn_protocols and freeing the C array in shutdown.
- Add the same advertisement to SSLListener so accepted server connections
  can negotiate a protocol.
- Add `negotiated_alpn()` via mbedtls_ssl_get_alpn_protocol.

openssl:
- Set the ALPN list (length-prefixed wire format) in SSLConn.init via
  SSL_set_alpn_protos.
- Add `negotiated_alpn()` via SSL_get0_alpn_selected.

The option and method are exposed through the net.ssl facade automatically
(embedded config + SSLConn type alias).

Add a hermetic net.ssl test that stands up a local mbedtls TLS server
advertising ALPN and verifies negotiation for h2, http/1.1 fallback, and the
no-ALPN case. It passes on both the mbedtls and OpenSSL (-d use_openssl)
client backends.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0dba1f47fe

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread vlib/net/openssl/openssl.c.v Outdated
@JalonSolov

Copy link
Copy Markdown
Collaborator

People will definitely complain if this isn't supported on Windows, too, but it can be done in a separate PR.

@quaesitor-scientiam

Copy link
Copy Markdown
Contributor Author

People will definitely complain if this isn't supported on Windows, too, but it can be done in a separate PR.

Working on it

Two follow-up fixes to the ALPN support:

1. -cstrict / -Werror const-qualifier errors (clang):
   - mbedtls_ssl_conf_alpn_protocols takes `const char **` and
     mbedtls_ssl_get_alpn_protocol returns `const char *`; SSL_get0_alpn_selected
     takes `const unsigned char **`. Pass the nested-pointer arguments through
     voidptr at the call sites (void* converts cleanly to a const pointer in C)
     and cast the const char* return before use.

2. Linking against OpenSSL versions older than 1.0.2 (predating ALPN):
   route SSL_set_alpn_protos / SSL_get0_alpn_selected through small
   version-guarded shims in openssl_compat.h, matching the existing
   v_net_openssl_* pattern. On older OpenSSL the shims are no-ops (set returns
   non-zero -> a clear runtime error only if ALPN was actually requested; get
   reports no protocol), so HTTP/1.1-only users keep building and running.
   LibreSSL reports a high version number and uses the native path.

Verified with `./vnew -W -cstrict -cc clang test vlib/net/ssl/ssl_alpn_test.v`
on both the mbedtls and OpenSSL backends, plus the net.mbedtls suite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@quaesitor-scientiam

Copy link
Copy Markdown
Contributor Author

Reviewed the CI failures they all appear to be unrelated

@quaesitor-scientiam

Copy link
Copy Markdown
Contributor Author

Preparing for Issue #5017

@JalonSolov JalonSolov merged commit 8e3e67e into vlang:master Jun 6, 2026
76 of 83 checks passed
JalonSolov pushed a commit that referenced this pull request Jun 6, 2026
* net.http: add synchronous HTTP/2 client connection (H2Conn)

Build on the ALPN (#27343), HPACK (#27353), and frame-codec (#27356) PRs to add
a minimal, synchronous, single-stream HTTP/2 client. Additive only: new files in
the http module, no change to existing code paths. Nothing wires it into
http.fetch yet (that ALPN shim is a follow-up), so there is no user-visible
behaviour change.

- H2Transport interface — the byte transport the connection runs over. Its
  read/write signatures match net.ssl.SSLConn, so an ALPN-negotiated `h2` TLS
  socket satisfies it directly; tests use an in-memory mock, so the connection
  is exercised without a socket.
- Connection preface + SETTINGS handshake (sent lazily on the first request).
- H2Conn.do(req): HPACK-encodes the request headers, sends HEADERS (plus DATA
  for a body, chunked to the peer's max frame size and bounded by the
  connection flow-control window), then reads frames until the stream closes.
- Inline servicing of connection-level frames: SETTINGS (apply + ACK), PING
  (echo ACK), WINDOW_UPDATE (grow the send window), GOAWAY (fail). Receive-side
  flow control is replenished with a WINDOW_UPDATE per DATA frame.
- CONTINUATION reassembly for response header blocks; RST_STREAM on the request
  stream is surfaced as an error.

Hermetic tests over the mock transport cover: a basic GET, a multi-DATA
response, a POST with a body, GOAWAY, RST_STREAM, CONTINUATION-split response
headers, and PING ACK. Passes under -W -cstrict -cc clang; the full
vlib/net/http suite is green.

Not included here, by design (follow-ups): stream multiplexing with a
background reader thread, connection pooling, wiring into http.fetch via ALPN,
and the server side.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* net.http: HTTP/2 client header splitting and per-stream flow control

Address two send-path correctness gaps in H2Conn:

- Split request header blocks larger than the peer's SETTINGS_MAX_FRAME_SIZE
  into a HEADERS frame followed by CONTINUATION frames (RFC 7540 Section 4.3),
  via send_header_block. Previously a large header set (e.g. big Cookie or
  Authorization) was sent as one oversized HEADERS frame that a compliant peer
  would reject.

- Track the per-stream send window in addition to the connection window
  (RFC 7540 Section 6.9). DATA is now bounded by min(connection, stream)
  window, stream-level WINDOW_UPDATE credits the active stream, and a change to
  SETTINGS_INITIAL_WINDOW_SIZE retroactively adjusts the stream window by the
  delta (Section 6.9.2). Previously only the connection window was tracked, so
  a peer lowering the initial window or granting stream-level updates could be
  overrun or could stall the client.

Adds tests for a request whose header block spans HEADERS + CONTINUATION, and
for a body larger than the initial window that resumes after the peer grows
both windows (no DATA frame exceeding the max frame size).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Richard Wheeler <quaesitor.scientiam@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
quaesitor-scientiam pushed a commit to quaesitor-scientiam/v that referenced this pull request Jun 8, 2026
First half of vlang#27383: give the SChannel (vschannel) TLS backend the
ability to advertise ALPN protocols and read the protocol the server selected,
reaching parity with the mbedtls/OpenSSL backends (vlang#27343) on the negotiation
primitive.

thirdparty/vschannel/vschannel.c:
- TlsContext gains an ALPN protocol list to advertise and a slot for the
  negotiated protocol.
- perform_client_handshake() passes a SECBUFFER_APPLICATION_PROTOCOLS input
  buffer (SEC_APPLICATION_PROTOCOLS / SecApplicationProtocolNegotiationExt_ALPN)
  into the ClientHello when a list is configured; the path is unchanged when it
  is not, so existing HTTP/1.1 requests are byte-identical.
- After the handshake, SECPKG_ATTR_APPLICATION_PROTOCOL is queried and stored.
- New C entry points: vschannel_set_alpn(), vschannel_get_alpn(), and
  vschannel_alpn_probe() (handshake-only, no application data).

vlib/net/http (Windows only):
- vschannel_alpn_windows.c.v exposes the C API, an alpn_wire() encoder, and
  schannel_alpn_probe().
- vschannel_alpn_windows_test.v: a wire-encoding unit test plus `-d network`
  tests that probe a public HTTP/2 server and assert `h2` is negotiated (and
  that offering only http/1.1 falls back).

This deliberately does NOT change fetch(): the one-shot vschannel request() path
still speaks HTTP/1.1, so advertising `h2` in real requests waits for the
HTTP/2-driver wiring (the second half of vlang#27383). It satisfies the issue's
first acceptance criterion (a negotiated_alpn()-equivalent on SChannel).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
JalonSolov pushed a commit that referenced this pull request Jun 9, 2026
* net.http: add ALPN negotiation to the Windows SChannel backend

First half of #27383: give the SChannel (vschannel) TLS backend the
ability to advertise ALPN protocols and read the protocol the server selected,
reaching parity with the mbedtls/OpenSSL backends (#27343) on the negotiation
primitive.

thirdparty/vschannel/vschannel.c:
- TlsContext gains an ALPN protocol list to advertise and a slot for the
  negotiated protocol.
- perform_client_handshake() passes a SECBUFFER_APPLICATION_PROTOCOLS input
  buffer (SEC_APPLICATION_PROTOCOLS / SecApplicationProtocolNegotiationExt_ALPN)
  into the ClientHello when a list is configured; the path is unchanged when it
  is not, so existing HTTP/1.1 requests are byte-identical.
- After the handshake, SECPKG_ATTR_APPLICATION_PROTOCOL is queried and stored.
- New C entry points: vschannel_set_alpn(), vschannel_get_alpn(), and
  vschannel_alpn_probe() (handshake-only, no application data).

vlib/net/http (Windows only):
- vschannel_alpn_windows.c.v exposes the C API, an alpn_wire() encoder, and
  schannel_alpn_probe().
- vschannel_alpn_windows_test.v: a wire-encoding unit test plus `-d network`
  tests that probe a public HTTP/2 server and assert `h2` is negotiated (and
  that offering only http/1.1 falls back).

This deliberately does NOT change fetch(): the one-shot vschannel request() path
still speaks HTTP/1.1, so advertising `h2` in real requests waits for the
HTTP/2-driver wiring (the second half of #27383). It satisfies the issue's
first acceptance criterion (a negotiated_alpn()-equivalent on SChannel).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* thirdparty/vschannel: add ALPN type shim for older SDK headers (tcc)

tcc bundles Windows SDK headers that predate SChannel ALPN, so the ALPN structs, enums and constants were undeclared. Define them when the SDK headers do not, matching the official layout exactly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Richard Wheeler <quaesitor.scientiam@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Alexander Medvednikov <alexander@medvednikov.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants