Skip to content

Commit ed17e5f

Browse files
quaesitor-scientiamRichard Wheelerclaudemedvednikov
authored
net.http: add ALPN negotiation to the Windows SChannel backend (#27395)
* 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>
1 parent a063235 commit ed17e5f

4 files changed

Lines changed: 283 additions & 2 deletions

File tree

thirdparty/vschannel/vschannel.c

Lines changed: 185 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,55 @@
11
#include <vschannel.h>
22
#include <sspi.h>
33

4+
// ALPN (RFC 7301) compatibility shim. Older toolchain headers (notably the
5+
// ones bundled with tcc) predate the SChannel ALPN additions, so the structs,
6+
// enums and constants below are missing there. Define them ourselves when the
7+
// SDK headers did not. SECPKG_ATTR_APPLICATION_PROTOCOL guards the schannel.h
8+
// types; SECBUFFER_APPLICATION_PROTOCOLS guards the sspi.h buffer constant.
9+
#ifndef ANYSIZE_ARRAY
10+
#define ANYSIZE_ARRAY 1
11+
#endif
12+
13+
#ifndef SECPKG_ATTR_APPLICATION_PROTOCOL
14+
#define SECPKG_ATTR_APPLICATION_PROTOCOL 35
15+
16+
typedef enum _SEC_APPLICATION_PROTOCOL_NEGOTIATION_EXT {
17+
SecApplicationProtocolNegotiationExt_None,
18+
SecApplicationProtocolNegotiationExt_NPN,
19+
SecApplicationProtocolNegotiationExt_ALPN
20+
} SEC_APPLICATION_PROTOCOL_NEGOTIATION_EXT, *PSEC_APPLICATION_PROTOCOL_NEGOTIATION_EXT;
21+
22+
typedef struct _SEC_APPLICATION_PROTOCOL_LIST {
23+
SEC_APPLICATION_PROTOCOL_NEGOTIATION_EXT ProtoNegoExt;
24+
unsigned short ProtocolListSize;
25+
unsigned char ProtocolList[ANYSIZE_ARRAY];
26+
} SEC_APPLICATION_PROTOCOL_LIST, *PSEC_APPLICATION_PROTOCOL_LIST;
27+
28+
typedef struct _SEC_APPLICATION_PROTOCOLS {
29+
unsigned long ProtocolListsSize;
30+
SEC_APPLICATION_PROTOCOL_LIST ProtocolLists[ANYSIZE_ARRAY];
31+
} SEC_APPLICATION_PROTOCOLS, *PSEC_APPLICATION_PROTOCOLS;
32+
33+
typedef enum _SEC_APPLICATION_PROTOCOL_NEGOTIATION_STATUS {
34+
SecApplicationProtocolNegotiationStatus_None,
35+
SecApplicationProtocolNegotiationStatus_Success,
36+
SecApplicationProtocolNegotiationStatus_SelectedClientOnly
37+
} SEC_APPLICATION_PROTOCOL_NEGOTIATION_STATUS, *PSEC_APPLICATION_PROTOCOL_NEGOTIATION_STATUS;
38+
39+
#define MAX_PROTOCOL_ID_SIZE 0xff
40+
41+
typedef struct _SecPkgContext_ApplicationProtocol {
42+
SEC_APPLICATION_PROTOCOL_NEGOTIATION_STATUS ProtoNegoStatus;
43+
SEC_APPLICATION_PROTOCOL_NEGOTIATION_EXT ProtoNegoExt;
44+
unsigned char ProtocolIdSize;
45+
unsigned char ProtocolId[MAX_PROTOCOL_ID_SIZE];
46+
} SecPkgContext_ApplicationProtocol, *PSecPkgContext_ApplicationProtocol;
47+
#endif // SECPKG_ATTR_APPLICATION_PROTOCOL
48+
49+
#ifndef SECBUFFER_APPLICATION_PROTOCOLS
50+
#define SECBUFFER_APPLICATION_PROTOCOLS 18
51+
#endif
52+
453
// Proxy
554
WCHAR * psz_proxy_server = L"proxy";
655
INT i_proxy_port = 80;
@@ -28,6 +77,15 @@ struct TlsContext {
2877
BOOL validate_server_certificate;
2978
BOOL creds_initialized;
3079
BOOL context_initialized;
80+
// ALPN protocol list to advertise, in the standard ALPN wire format (each
81+
// name 1-byte length-prefixed), e.g. "\x02h2\x08http/1.1". alpn_wire_len == 0
82+
// means "do not advertise ALPN".
83+
unsigned char alpn_wire[256];
84+
unsigned long alpn_wire_len;
85+
// Negotiated application protocol name (e.g. "h2"); negotiated_alpn_len == 0
86+
// when the server selected none.
87+
char negotiated_alpn[256];
88+
unsigned long negotiated_alpn_len;
3189
};
3290

3391
TlsContext new_tls_context() {
@@ -38,10 +96,63 @@ TlsContext new_tls_context() {
3896
.validate_server_certificate = TRUE,
3997
.creds_initialized = FALSE,
4098
.context_initialized = FALSE,
41-
.p_pemote_cert_context = NULL
99+
.p_pemote_cert_context = NULL,
100+
.alpn_wire_len = 0,
101+
.negotiated_alpn_len = 0
42102
};
43103
};
44104

105+
// vschannel_set_alpn configures the ALPN protocol list to advertise during the
106+
// next handshake. `wire` is the standard ALPN wire format (each protocol name
107+
// preceded by a 1-byte length), e.g. "\x02h2\x08http/1.1". Passing len == 0
108+
// disables ALPN advertisement.
109+
void vschannel_set_alpn(TlsContext *tls_ctx, const char *wire, INT len) {
110+
if (len < 0) {
111+
len = 0;
112+
}
113+
if (len > (INT)sizeof(tls_ctx->alpn_wire)) {
114+
len = (INT)sizeof(tls_ctx->alpn_wire);
115+
}
116+
if (len > 0) {
117+
memcpy(tls_ctx->alpn_wire, wire, (size_t)len);
118+
}
119+
tls_ctx->alpn_wire_len = (unsigned long)len;
120+
}
121+
122+
// vschannel_get_alpn copies the protocol the server selected via ALPN (e.g.
123+
// "h2") into `out` and returns its length, or 0 if none was negotiated.
124+
INT vschannel_get_alpn(TlsContext *tls_ctx, char *out, INT out_cap) {
125+
unsigned long n = tls_ctx->negotiated_alpn_len;
126+
if (out_cap < 0) {
127+
out_cap = 0;
128+
}
129+
if (n > (unsigned long)out_cap) {
130+
n = (unsigned long)out_cap;
131+
}
132+
if (n > 0) {
133+
memcpy(out, tls_ctx->negotiated_alpn, (size_t)n);
134+
}
135+
return (INT)n;
136+
}
137+
138+
// vschannel_capture_alpn queries the negotiated ALPN protocol from a completed
139+
// handshake and stores it on the context for vschannel_get_alpn().
140+
static void vschannel_capture_alpn(TlsContext *tls_ctx) {
141+
SecPkgContext_ApplicationProtocol appproto;
142+
SECURITY_STATUS st;
143+
144+
tls_ctx->negotiated_alpn_len = 0;
145+
st = tls_ctx->sspi->QueryContextAttributes(&tls_ctx->h_context,
146+
SECPKG_ATTR_APPLICATION_PROTOCOL, &appproto);
147+
if (st == SEC_E_OK
148+
&& appproto.ProtoNegoStatus == SecApplicationProtocolNegotiationStatus_Success
149+
&& appproto.ProtocolIdSize > 0
150+
&& appproto.ProtocolIdSize <= sizeof(tls_ctx->negotiated_alpn)) {
151+
memcpy(tls_ctx->negotiated_alpn, appproto.ProtocolId, appproto.ProtocolIdSize);
152+
tls_ctx->negotiated_alpn_len = appproto.ProtocolIdSize;
153+
}
154+
}
155+
45156
static void vschannel_clear_last_error(TlsContext *tls_ctx) {
46157
tls_ctx->last_error_code = 0;
47158
}
@@ -136,6 +247,9 @@ INT request(TlsContext *tls_ctx, INT iport, LPWSTR host, CHAR *req, DWORD req_le
136247
}
137248
tls_ctx->context_initialized = TRUE;
138249

250+
// Record the ALPN protocol the server selected (if any).
251+
vschannel_capture_alpn(tls_ctx);
252+
139253
if(tls_ctx->validate_server_certificate) {
140254
// Authenticate server's credentials.
141255

@@ -194,6 +308,43 @@ INT request(TlsContext *tls_ctx, INT iport, LPWSTR host, CHAR *req, DWORD req_le
194308
return resp_length;
195309
}
196310

311+
// vschannel_alpn_probe connects to host:iport, performs the TLS handshake while
312+
// advertising whatever ALPN list was configured via vschannel_set_alpn(),
313+
// captures the protocol the server selected into `out` (up to out_cap bytes),
314+
// and disconnects without sending an application request. Returns the
315+
// negotiated protocol length (0 = handshake succeeded but no protocol selected),
316+
// or -1 on connect/handshake failure (see vschannel_last_error). Intended for
317+
// tests and capability checks, since request() only speaks HTTP/1.1.
318+
INT vschannel_alpn_probe(TlsContext *tls_ctx, INT iport, LPWSTR host, char *out, INT out_cap) {
319+
SecBuffer ExtraData;
320+
SECURITY_STATUS Status;
321+
322+
protocol = SP_PROT_TLS1_2_CLIENT;
323+
port_number = iport;
324+
vschannel_clear_last_error(tls_ctx);
325+
326+
if(connect_to_server(tls_ctx, host, port_number)) {
327+
vschannel_cleanup(tls_ctx);
328+
return -1;
329+
}
330+
331+
Status = perform_client_handshake(tls_ctx, host, &ExtraData);
332+
if(Status) {
333+
vschannel_set_last_error(tls_ctx, Status);
334+
vschannel_cleanup(tls_ctx);
335+
return -1;
336+
}
337+
tls_ctx->context_initialized = TRUE;
338+
339+
vschannel_capture_alpn(tls_ctx);
340+
341+
disconnect_from_server(tls_ctx);
342+
tls_ctx->context_initialized = FALSE;
343+
tls_ctx->socket = INVALID_SOCKET;
344+
345+
return vschannel_get_alpn(tls_ctx, out, out_cap);
346+
}
347+
197348

198349
static SECURITY_STATUS create_credentials(TlsContext *tls_ctx) {
199350
TimeStamp tsExpiry;
@@ -453,6 +604,38 @@ static SECURITY_STATUS perform_client_handshake(TlsContext *tls_ctx, WCHAR *host
453604
ISC_REQ_ALLOCATE_MEMORY |
454605
ISC_REQ_STREAM;
455606

607+
//
608+
// Optionally advertise ALPN protocols in the ClientHello. SChannel takes
609+
// this as a SECBUFFER_APPLICATION_PROTOCOLS input buffer holding a
610+
// SEC_APPLICATION_PROTOCOLS record. The backing store is a 4-byte-aligned
611+
// unsigned long array so the struct cast is well aligned on every compiler.
612+
//
613+
SecBuffer InBuffers[1];
614+
SecBufferDesc InBuffer;
615+
SecBufferDesc *pInput = NULL;
616+
unsigned long alpn_store[80]; // 320 bytes; alpn_wire is at most 256
617+
if (tls_ctx->alpn_wire_len > 0) {
618+
SEC_APPLICATION_PROTOCOLS *protos = (SEC_APPLICATION_PROTOCOLS *)alpn_store;
619+
SEC_APPLICATION_PROTOCOL_LIST *list = &protos->ProtocolLists[0];
620+
unsigned long wlen = tls_ctx->alpn_wire_len;
621+
622+
list->ProtoNegoExt = SecApplicationProtocolNegotiationExt_ALPN;
623+
list->ProtocolListSize = (unsigned short)wlen;
624+
memcpy(list->ProtocolList, tls_ctx->alpn_wire, (size_t)wlen);
625+
protos->ProtocolListsSize =
626+
(unsigned long)(FIELD_OFFSET(SEC_APPLICATION_PROTOCOL_LIST, ProtocolList) + wlen);
627+
628+
InBuffers[0].pvBuffer = protos;
629+
InBuffers[0].cbBuffer =
630+
(unsigned long)(FIELD_OFFSET(SEC_APPLICATION_PROTOCOLS, ProtocolLists) + protos->ProtocolListsSize);
631+
InBuffers[0].BufferType = SECBUFFER_APPLICATION_PROTOCOLS;
632+
633+
InBuffer.cBuffers = 1;
634+
InBuffer.pBuffers = InBuffers;
635+
InBuffer.ulVersion = SECBUFFER_VERSION;
636+
pInput = &InBuffer;
637+
}
638+
456639
//
457640
// Initiate a ClientHello message and generate a token.
458641
//
@@ -472,7 +655,7 @@ static SECURITY_STATUS perform_client_handshake(TlsContext *tls_ctx, WCHAR *host
472655
dwSSPIFlags,
473656
0,
474657
SECURITY_NATIVE_DREP,
475-
NULL,
658+
pInput,
476659
0,
477660
&tls_ctx->h_context,
478661
&OutBuffer,

thirdparty/vschannel/vschannel.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ typedef struct TlsContext TlsContext;
2626

2727
TlsContext new_tls_context();
2828

29+
// ALPN (RFC 7301) support. `wire` is the standard ALPN wire format: each
30+
// protocol name preceded by a 1-byte length, e.g. "\x02h2\x08http/1.1".
31+
void vschannel_set_alpn(TlsContext *tls_ctx, const char *wire, INT len);
32+
INT vschannel_get_alpn(TlsContext *tls_ctx, char *out, INT out_cap);
33+
INT vschannel_alpn_probe(TlsContext *tls_ctx, INT iport, LPWSTR host, char *out, INT out_cap);
34+
2935
static void vschannel_init(TlsContext *tls_ctx, BOOL validate_server_certificate);
3036

3137
static void vschannel_cleanup(TlsContext *tls_ctx);
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
2+
// Use of this source code is governed by an MIT license
3+
// that can be found in the LICENSE file.
4+
module http
5+
6+
// ALPN (RFC 7301) support for the Windows SChannel backend. The handshake-level
7+
// plumbing lives in thirdparty/vschannel/vschannel.c; this exposes it to V.
8+
//
9+
// This adds the *capability* to advertise ALPN and read the negotiated protocol
10+
// on SChannel. The one-shot vschannel request() path still speaks HTTP/1.1, so
11+
// it is not wired into fetch() yet (advertising `h2` without an HTTP/2 driver
12+
// would let a server pick a protocol we cannot speak). Negotiation is exercised
13+
// via schannel_alpn_probe(). Wiring the request path to HTTP/2 is the follow-up.
14+
// See vlang/v#27383.
15+
16+
fn C.vschannel_set_alpn(tls_ctx &C.TlsContext, wire &char, len int)
17+
fn C.vschannel_get_alpn(tls_ctx &C.TlsContext, out &char, out_cap int) int
18+
fn C.vschannel_alpn_probe(tls_ctx &C.TlsContext, iport int, host &u16, out &char, out_cap int) int
19+
20+
// alpn_wire encodes ALPN protocol names into the wire format SChannel expects:
21+
// each name preceded by a single length byte, e.g. `['h2', 'http/1.1']` becomes
22+
// "\x02h2\x08http/1.1". Empty names, or names longer than 255 bytes, are skipped.
23+
fn alpn_wire(protocols []string) []u8 {
24+
mut out := []u8{}
25+
for p in protocols {
26+
if p.len == 0 || p.len > 255 {
27+
continue
28+
}
29+
out << u8(p.len)
30+
out << p.bytes()
31+
}
32+
return out
33+
}
34+
35+
// schannel_alpn_probe performs a TLS handshake to `host`:`port` advertising
36+
// `protocols` via ALPN and returns the protocol the server selected (e.g. 'h2'),
37+
// or '' if none was negotiated. It sends no application data, so it works as a
38+
// pure ALPN-negotiation check independent of the HTTP/1.1 request path.
39+
// Windows/SChannel only. Pass `validate` = false to skip certificate validation
40+
// (e.g. against a local test server with a self-signed cert).
41+
fn schannel_alpn_probe(host string, port int, protocols []string, validate bool) string {
42+
mut ctx := C.new_tls_context()
43+
C.vschannel_use_tls12_client_protocol()
44+
C.vschannel_init(&ctx, C.BOOL(if validate { 1 } else { 0 }))
45+
wire := alpn_wire(protocols)
46+
if wire.len > 0 {
47+
C.vschannel_set_alpn(&ctx, &char(wire.data), wire.len)
48+
}
49+
mut buf := []u8{len: 256}
50+
n := C.vschannel_alpn_probe(&ctx, port, host.to_wide(), &char(buf.data), buf.len)
51+
C.vschannel_cleanup(&ctx)
52+
if n <= 0 {
53+
return ''
54+
}
55+
return buf[..n].bytestr()
56+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
module http
2+
3+
// ALPN negotiation on the Windows SChannel backend (vlang/v#27383).
4+
//
5+
// The wire-encoding test runs anywhere on Windows. The negotiation tests are
6+
// network-dependent: run with `-d network`, e.g.
7+
// v -d network test vlib/net/http/vschannel_alpn_windows_test.v
8+
9+
fn test_alpn_wire_encoding() {
10+
mut want := [u8(0x02)]
11+
want << 'h2'.bytes()
12+
want << 0x08
13+
want << 'http/1.1'.bytes()
14+
assert alpn_wire(['h2', 'http/1.1']) == want
15+
// Empty and over-long (>255) names are skipped.
16+
assert alpn_wire([]string{}) == []u8{}
17+
assert alpn_wire(['', 'h2']) == [u8(0x02), 0x68, 0x32] // 0x68='h', 0x32='2'
18+
}
19+
20+
fn test_schannel_alpn_negotiates_h2() {
21+
$if !network ? {
22+
return
23+
}
24+
// A public HTTP/2 server must select `h2` when offered.
25+
selected := schannel_alpn_probe('www.google.com', 443, ['h2', 'http/1.1'], false)
26+
assert selected == 'h2', 'expected h2, got "${selected}"'
27+
}
28+
29+
fn test_schannel_alpn_falls_back_to_http1() {
30+
$if !network ? {
31+
return
32+
}
33+
// Offer only HTTP/1.1: the server must not select h2.
34+
selected := schannel_alpn_probe('www.google.com', 443, ['http/1.1'], false)
35+
assert selected == 'http/1.1', 'expected http/1.1, got "${selected}"'
36+
}

0 commit comments

Comments
 (0)