quicsilver 0.5.0-x86_64-linux
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +49 -0
- data/.github/workflows/cibuildgem.yaml +117 -0
- data/.gitignore +21 -0
- data/.gitmodules +3 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +175 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +67 -0
- data/LICENSE +21 -0
- data/README.md +261 -0
- data/Rakefile +130 -0
- data/benchmarks/baseline.rb +175 -0
- data/benchmarks/compare.rb +398 -0
- data/benchmarks/components.rb +191 -0
- data/benchmarks/helpers.rb +162 -0
- data/benchmarks/latency.rb +128 -0
- data/benchmarks/loss.rb +193 -0
- data/benchmarks/mobile.rb +125 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/docs/connection_identity.md +141 -0
- data/examples/README.md +58 -0
- data/examples/benchmark.rb +191 -0
- data/examples/connection_pool_demo.rb +47 -0
- data/examples/example_helper.rb +69 -0
- data/examples/falcon_middleware.rb +44 -0
- data/examples/feature_demo.rb +125 -0
- data/examples/grpc_style.rb +97 -0
- data/examples/latency_aware_routing.rb +382 -0
- data/examples/lifecycle_drain.rb +264 -0
- data/examples/minimal_http3_server.rb +14 -0
- data/examples/priorities.rb +60 -0
- data/examples/protocol_http_server.rb +31 -0
- data/examples/rack_http3_server.rb +42 -0
- data/examples/rails_feature_test.rb +280 -0
- data/examples/server_stats.rb +117 -0
- data/examples/simple_client_test.rb +26 -0
- data/examples/streaming_sse.rb +33 -0
- data/examples/trailers.rb +69 -0
- data/examples/webtransport_smoke.rb +546 -0
- data/examples/worker_load_balancing.rb +425 -0
- data/ext/quicsilver/extconf.rb +124 -0
- data/ext/quicsilver/include/msquic.h +2226 -0
- data/ext/quicsilver/include/msquic_posix.h +696 -0
- data/ext/quicsilver/include/quic_sal_stub.h +314 -0
- data/ext/quicsilver/msquic_patch.rb +68 -0
- data/ext/quicsilver/patches/reliable-reset.patch +723 -0
- data/ext/quicsilver/patches/stream-final-size.patch +47 -0
- data/ext/quicsilver/quicsilver.c +2741 -0
- data/lib/quicsilver/3.4/quicsilver.so +0 -0
- data/lib/quicsilver/4.0/quicsilver.so +0 -0
- data/lib/quicsilver/client/client.rb +708 -0
- data/lib/quicsilver/client/connection_pool.rb +202 -0
- data/lib/quicsilver/client/request.rb +190 -0
- data/lib/quicsilver/errors.rb +39 -0
- data/lib/quicsilver/libmsquic.so.2 +0 -0
- data/lib/quicsilver/protocol/adapter.rb +196 -0
- data/lib/quicsilver/protocol/capsule.rb +38 -0
- data/lib/quicsilver/protocol/connect_stream_decoder.rb +75 -0
- data/lib/quicsilver/protocol/control_stream_parser.rb +117 -0
- data/lib/quicsilver/protocol/datagram.rb +40 -0
- data/lib/quicsilver/protocol/frame_parser.rb +141 -0
- data/lib/quicsilver/protocol/frame_reader.rb +84 -0
- data/lib/quicsilver/protocol/frames.rb +454 -0
- data/lib/quicsilver/protocol/priority.rb +65 -0
- data/lib/quicsilver/protocol/qpack/decoder.rb +127 -0
- data/lib/quicsilver/protocol/qpack/encoder.rb +217 -0
- data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +140 -0
- data/lib/quicsilver/protocol/qpack/huffman.rb +459 -0
- data/lib/quicsilver/protocol/request_encoder.rb +50 -0
- data/lib/quicsilver/protocol/request_parser.rb +289 -0
- data/lib/quicsilver/protocol/response_encoder.rb +107 -0
- data/lib/quicsilver/protocol/response_parser.rb +147 -0
- data/lib/quicsilver/protocol/stream_input.rb +98 -0
- data/lib/quicsilver/protocol/stream_output.rb +59 -0
- data/lib/quicsilver/protocol/unidirectional_stream.rb +12 -0
- data/lib/quicsilver/protocol/web_transport.rb +214 -0
- data/lib/quicsilver/rack/context.rb +33 -0
- data/lib/quicsilver/response.rb +40 -0
- data/lib/quicsilver/server/listener_data.rb +14 -0
- data/lib/quicsilver/server/rack_adapter.rb +77 -0
- data/lib/quicsilver/server/receive_queue.rb +106 -0
- data/lib/quicsilver/server/request_handler.rb +149 -0
- data/lib/quicsilver/server/request_registry.rb +63 -0
- data/lib/quicsilver/server/scheduler.rb +50 -0
- data/lib/quicsilver/server/schedulers/fiber_scheduler.rb +79 -0
- data/lib/quicsilver/server/schedulers/thread_scheduler.rb +76 -0
- data/lib/quicsilver/server/server.rb +1080 -0
- data/lib/quicsilver/server/web_transport_flow_control.rb +270 -0
- data/lib/quicsilver/server/web_transport_manager.rb +281 -0
- data/lib/quicsilver/server/web_transport_registry.rb +40 -0
- data/lib/quicsilver/server/web_transport_session.rb +812 -0
- data/lib/quicsilver/server/web_transport_stream.rb +326 -0
- data/lib/quicsilver/transport/configuration.rb +234 -0
- data/lib/quicsilver/transport/connection.rb +594 -0
- data/lib/quicsilver/transport/connection_stats.rb +53 -0
- data/lib/quicsilver/transport/event_loop.rb +74 -0
- data/lib/quicsilver/transport/inbound_stream.rb +53 -0
- data/lib/quicsilver/transport/stream.rb +43 -0
- data/lib/quicsilver/transport/stream_event.rb +49 -0
- data/lib/quicsilver/transport/stream_id.rb +26 -0
- data/lib/quicsilver/version.rb +3 -0
- data/lib/quicsilver.rb +96 -0
- data/lib/rackup/handler/quicsilver.rb +96 -0
- data/quicsilver.gemspec +66 -0
- metadata +362 -0
|
@@ -0,0 +1,2741 @@
|
|
|
1
|
+
#include <ruby.h>
|
|
2
|
+
#include <ruby/thread.h>
|
|
3
|
+
#include <ruby/st.h>
|
|
4
|
+
#include <pthread.h>
|
|
5
|
+
#define QUIC_API_ENABLE_PREVIEW_FEATURES 1
|
|
6
|
+
#include "msquic.h"
|
|
7
|
+
|
|
8
|
+
// A reset stream's final size arrives on the PEER_SEND_ABORTED event, added
|
|
9
|
+
// by patches/stream-final-size.patch. This is the value MsQuic uses while it
|
|
10
|
+
// is unsettled, and what we pass to Ruby as "not known".
|
|
11
|
+
#define QUICSILVER_FINAL_SIZE_UNKNOWN UINT64_MAX
|
|
12
|
+
#include <errno.h>
|
|
13
|
+
#include <limits.h>
|
|
14
|
+
#include <stdlib.h>
|
|
15
|
+
#include <string.h>
|
|
16
|
+
#include <time.h>
|
|
17
|
+
#include <unistd.h>
|
|
18
|
+
|
|
19
|
+
#if __linux__
|
|
20
|
+
#include <sys/epoll.h>
|
|
21
|
+
#elif __APPLE__ || __FreeBSD__
|
|
22
|
+
#include <sys/event.h>
|
|
23
|
+
#endif
|
|
24
|
+
|
|
25
|
+
static VALUE mQuicsilver;
|
|
26
|
+
|
|
27
|
+
// Custom execution: app owns the event loop, MsQuic spawns no threads
|
|
28
|
+
static QUIC_EVENTQ EventQ = -1; // kqueue (macOS) / epoll (Linux)
|
|
29
|
+
static QUIC_EXECUTION* ExecContext = NULL;
|
|
30
|
+
|
|
31
|
+
#if __linux__
|
|
32
|
+
#include <sys/eventfd.h>
|
|
33
|
+
static int WakeFd = -1; // eventfd for waking epoll
|
|
34
|
+
#endif
|
|
35
|
+
#define WAKE_IDENT 0xCAFE // kqueue EVFILT_USER identifier (macOS only)
|
|
36
|
+
|
|
37
|
+
// Wake the event loop from another thread (e.g. after StreamSend)
|
|
38
|
+
static void
|
|
39
|
+
wake_event_loop(void)
|
|
40
|
+
{
|
|
41
|
+
if (EventQ == -1) return;
|
|
42
|
+
#if __linux__
|
|
43
|
+
if (WakeFd != -1) {
|
|
44
|
+
uint64_t val = 1;
|
|
45
|
+
write(WakeFd, &val, sizeof(val));
|
|
46
|
+
}
|
|
47
|
+
#elif __APPLE__ || __FreeBSD__
|
|
48
|
+
struct kevent kev;
|
|
49
|
+
EV_SET(&kev, WAKE_IDENT, EVFILT_USER, 0, NOTE_TRIGGER, 0, NULL);
|
|
50
|
+
kevent(EventQ, &kev, 1, NULL, 0, NULL);
|
|
51
|
+
#endif
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Global MSQUIC API table
|
|
55
|
+
static const QUIC_API_TABLE* MsQuic = NULL;
|
|
56
|
+
|
|
57
|
+
// Global registration handle
|
|
58
|
+
static HQUIC Registration = NULL;
|
|
59
|
+
|
|
60
|
+
// Registration configuration
|
|
61
|
+
static const QUIC_REGISTRATION_CONFIG RegConfig = { "quicsilver", QUIC_EXECUTION_PROFILE_LOW_LATENCY };
|
|
62
|
+
|
|
63
|
+
// Connection state tracking
|
|
64
|
+
typedef struct {
|
|
65
|
+
int connected;
|
|
66
|
+
int failed;
|
|
67
|
+
QUIC_STATUS error_status;
|
|
68
|
+
uint64_t error_code;
|
|
69
|
+
VALUE client_obj; // Ruby client object (Qnil for server connections)
|
|
70
|
+
char remote_address[INET6_ADDRSTRLEN];
|
|
71
|
+
uint16_t remote_port;
|
|
72
|
+
int session_resumed;
|
|
73
|
+
int reliable_reset_negotiated;
|
|
74
|
+
// 0-RTT resumption ticket (client-side)
|
|
75
|
+
uint8_t* resumption_ticket;
|
|
76
|
+
uint32_t resumption_ticket_length;
|
|
77
|
+
// Datagrams queued for Ruby and dropped for lack of room, per connection.
|
|
78
|
+
// Both maintained under EventQueue.lock, not StateLock.
|
|
79
|
+
size_t queued_datagrams;
|
|
80
|
+
uint64_t dropped_datagrams;
|
|
81
|
+
} ConnectionContext;
|
|
82
|
+
|
|
83
|
+
// Listener state tracking
|
|
84
|
+
typedef struct {
|
|
85
|
+
int started;
|
|
86
|
+
int stopped;
|
|
87
|
+
int failed;
|
|
88
|
+
QUIC_STATUS error_status;
|
|
89
|
+
HQUIC Configuration;
|
|
90
|
+
} ListenerContext;
|
|
91
|
+
|
|
92
|
+
// Stream state tracking
|
|
93
|
+
typedef struct {
|
|
94
|
+
HQUIC stream;
|
|
95
|
+
uint64_t token;
|
|
96
|
+
QUIC_STREAM_SHUTDOWN_FLAGS abort_flags;
|
|
97
|
+
uint32_t pending_priority;
|
|
98
|
+
uint64_t receive_credit;
|
|
99
|
+
uint64_t receive_chunks;
|
|
100
|
+
// Set under StateLock when a RECEIVE returned less than indicated, which
|
|
101
|
+
// makes MsQuic stop delivering. The next grant that leaves both credits
|
|
102
|
+
// positive clears it and calls StreamReceiveSetEnabled.
|
|
103
|
+
int receive_paused;
|
|
104
|
+
int receive_credit_enabled;
|
|
105
|
+
int receive_delivery_failed;
|
|
106
|
+
// Set when a RECEIVE_FIN has been queued for Ruby. Poll thread only.
|
|
107
|
+
int fin_delivered;
|
|
108
|
+
HQUIC connection;
|
|
109
|
+
void* connection_ctx; // ConnectionContext pointer (for building connection_data)
|
|
110
|
+
VALUE client_obj; // Ruby client object (copied from connection context)
|
|
111
|
+
char remote_address[INET6_ADDRSTRLEN];
|
|
112
|
+
uint16_t remote_port;
|
|
113
|
+
int session_resumed;
|
|
114
|
+
uint64_t stream_id; // QUIC stream ID, cached once after StreamStart
|
|
115
|
+
int started;
|
|
116
|
+
int shutdown;
|
|
117
|
+
int early_data; // Set when stream received 0-RTT data
|
|
118
|
+
QUIC_STATUS error_status;
|
|
119
|
+
} StreamContext;
|
|
120
|
+
|
|
121
|
+
// --- Threading model ---------------------------------------------------------
|
|
122
|
+
//
|
|
123
|
+
// Two threads touch this file's state, and neither holds the other's lock.
|
|
124
|
+
//
|
|
125
|
+
// The poll thread drives MsQuic (ExecutionPoll, kevent/epoll_wait, completion
|
|
126
|
+
// callbacks) with the GVL released for the whole loop. Nothing reachable from
|
|
127
|
+
// a StreamCallback or ConnectionCallback may call into Ruby. Callbacks push a
|
|
128
|
+
// RubyEvent onto EventQueue and return.
|
|
129
|
+
//
|
|
130
|
+
// The dispatcher thread, a Ruby Thread in Transport::EventLoop, waits on
|
|
131
|
+
// EventQueue without the GVL, then takes it to pop a batch and call
|
|
132
|
+
// Server.handle_stream / Client#handle_stream_event for each. It also does
|
|
133
|
+
// the releases that used to happen inside callbacks and either need the GVL
|
|
134
|
+
// (rb_gc_unregister_address) or free memory a Ruby thread could still be
|
|
135
|
+
// using (StreamContext, ConnectionContext, SendContext). Because every free
|
|
136
|
+
// happens on a thread holding the GVL, a Ruby thread that found a context
|
|
137
|
+
// cannot see it freed underneath it: the GVL is the lifetime guarantee, as it
|
|
138
|
+
// was before, just held by a different thread.
|
|
139
|
+
//
|
|
140
|
+
// StateLock covers LiveStreams and every context field one side writes and
|
|
141
|
+
// the other reads: receive credit and the paused flag, the lazily cached
|
|
142
|
+
// stream_id. Rule: never call MsQuic while holding StateLock. Callbacks run
|
|
143
|
+
// inside MsQuic's connection lock, so holding StateLock across an MsQuic call
|
|
144
|
+
// from a Ruby thread would invert the order and deadlock.
|
|
145
|
+
//
|
|
146
|
+
// Why the receive path can be asynchronous: MsQuic processes API calls on a
|
|
147
|
+
// stream (StreamReceiveSetEnabled, StreamSend) as queued operations on the
|
|
148
|
+
// connection's worker, which here is ExecutionPoll on the poll thread. A call
|
|
149
|
+
// made from a Ruby thread therefore runs after any callback in flight, never
|
|
150
|
+
// during it. So when a RECEIVE returns a partial length and MsQuic pauses the
|
|
151
|
+
// stream, a grant that observed receive_paused and queued SetEnabled is
|
|
152
|
+
// applied after the pause, not lost before it.
|
|
153
|
+
static pthread_mutex_t StateLock = PTHREAD_MUTEX_INITIALIZER;
|
|
154
|
+
#define STATE_LOCK() pthread_mutex_lock(&StateLock)
|
|
155
|
+
#define STATE_UNLOCK() pthread_mutex_unlock(&StateLock)
|
|
156
|
+
|
|
157
|
+
// Tokens are never reused. find_stream returns a pointer whose lifetime is
|
|
158
|
+
// guaranteed by the GVL: contexts are freed only on the dispatcher thread,
|
|
159
|
+
// which holds it. So a caller must hold the GVL from the lookup through the
|
|
160
|
+
// last use of the pointer and must not release it in between, including via
|
|
161
|
+
// rb_thread_call_without_gvl, rb_thread_schedule, or any Ruby call that can
|
|
162
|
+
// block. Inserting one of those between find_stream and the MsQuic call it
|
|
163
|
+
// guards is a use-after-free.
|
|
164
|
+
static st_table* LiveStreams;
|
|
165
|
+
|
|
166
|
+
typedef enum {
|
|
167
|
+
RELEASE_NONE = 0,
|
|
168
|
+
RELEASE_SEND, // SendContext*: drop the retained String
|
|
169
|
+
RELEASE_STREAM, // StreamContext*: StreamClose + free
|
|
170
|
+
RELEASE_STREAM_NOCLOSE, // StreamContext*: free only, app is closing
|
|
171
|
+
RELEASE_CONNECTION, // ConnectionContext*: unregister client_obj, free
|
|
172
|
+
} ReleaseKind;
|
|
173
|
+
|
|
174
|
+
// Most events carry a token or a token plus two integers; those fit inline
|
|
175
|
+
// and cost no malloc. RECEIVE payloads are handed over already allocated.
|
|
176
|
+
#define EVENT_INLINE_DATA 40
|
|
177
|
+
|
|
178
|
+
typedef struct {
|
|
179
|
+
HQUIC connection;
|
|
180
|
+
void* connection_ctx;
|
|
181
|
+
VALUE client_obj;
|
|
182
|
+
const char* event_type; // string literal, never freed
|
|
183
|
+
uint64_t stream_id;
|
|
184
|
+
char* data; // malloc'd when data_len > EVENT_INLINE_DATA
|
|
185
|
+
size_t data_len;
|
|
186
|
+
char inline_data[EVENT_INLINE_DATA];
|
|
187
|
+
int early_data;
|
|
188
|
+
int dropped; // superseded datagram: free, do not deliver
|
|
189
|
+
ReleaseKind release;
|
|
190
|
+
void* release_ptr;
|
|
191
|
+
|
|
192
|
+
// A pending receive: MsQuic owns the bytes until StreamReceiveComplete.
|
|
193
|
+
// Descriptors are copied (callback-scoped); data is NULL.
|
|
194
|
+
HQUIC stream;
|
|
195
|
+
uint64_t token;
|
|
196
|
+
uint64_t accepted;
|
|
197
|
+
QUIC_BUFFER* buffers; // inline_buffers, or malloc'd when more
|
|
198
|
+
uint32_t buffer_count;
|
|
199
|
+
QUIC_BUFFER inline_buffers[4];
|
|
200
|
+
} RubyEvent;
|
|
201
|
+
|
|
202
|
+
#define EVENT_INLINE_BUFFERS 4
|
|
203
|
+
|
|
204
|
+
static struct {
|
|
205
|
+
pthread_mutex_t lock;
|
|
206
|
+
pthread_cond_t ready;
|
|
207
|
+
RubyEvent* buf;
|
|
208
|
+
size_t cap, head, len;
|
|
209
|
+
int closed;
|
|
210
|
+
} EventQueue = { PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER, NULL, 0, 0, 0, 0 };
|
|
211
|
+
|
|
212
|
+
// The queue is unbounded for stream events, which stream limits and receive
|
|
213
|
+
// credit already bound. Datagrams have no flow control (RFC 9221 §5.3) and
|
|
214
|
+
// MAY be dropped when the receiver cannot process them, so each connection
|
|
215
|
+
// may have at most this many queued ahead of the dispatcher. Per connection,
|
|
216
|
+
// not global: one flooding peer must not cost every other peer its datagrams.
|
|
217
|
+
// 128 is quic-go's per-connection receive queue; neqo uses 10, quiche makes
|
|
218
|
+
// the application choose. When full the oldest is dropped, not the newest:
|
|
219
|
+
// for the signals datagrams carry (presence, positions) the stale one is the
|
|
220
|
+
// one nobody wants, which is quiche's and neqo's policy too. The count is of
|
|
221
|
+
// datagrams, so a backlog of stream events does not cost datagrams.
|
|
222
|
+
#define DATAGRAM_QUEUE_LIMIT 128
|
|
223
|
+
static uint64_t DroppedDatagrams = 0;
|
|
224
|
+
|
|
225
|
+
// Under EventQueue.lock. Finds the oldest undropped datagram for the
|
|
226
|
+
// connection and marks it; the dispatcher frees it without delivering.
|
|
227
|
+
static void
|
|
228
|
+
drop_oldest_datagram(void* connection_ctx)
|
|
229
|
+
{
|
|
230
|
+
for (size_t i = 0; i < EventQueue.len; i++) {
|
|
231
|
+
RubyEvent* ev = &EventQueue.buf[(EventQueue.head + i) % EventQueue.cap];
|
|
232
|
+
if (ev->connection_ctx == connection_ctx && !ev->dropped &&
|
|
233
|
+
strcmp(ev->event_type, "DATAGRAM_RECEIVED") == 0) {
|
|
234
|
+
ev->dropped = 1;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Grow by doubling, re-linearising the ring. Unbounded on purpose: a bounded
|
|
241
|
+
// queue that blocks the poll thread would deadlock against a Ruby thread
|
|
242
|
+
// calling StreamSend (needs the connection lock the blocked callback holds),
|
|
243
|
+
// and one that drops would lose stream data. Per-stream receive credit is
|
|
244
|
+
// what bounds memory, as before.
|
|
245
|
+
static int
|
|
246
|
+
event_queue_grow(void)
|
|
247
|
+
{
|
|
248
|
+
size_t cap = EventQueue.cap ? EventQueue.cap * 2 : 256;
|
|
249
|
+
RubyEvent* buf = malloc(cap * sizeof(RubyEvent));
|
|
250
|
+
if (!buf) return 0;
|
|
251
|
+
for (size_t i = 0; i < EventQueue.len; i++) {
|
|
252
|
+
RubyEvent* from = &EventQueue.buf[(EventQueue.head + i) % EventQueue.cap];
|
|
253
|
+
buf[i] = *from;
|
|
254
|
+
// Inline descriptors must point at their own copy.
|
|
255
|
+
if (from->buffers == from->inline_buffers) buf[i].buffers = buf[i].inline_buffers;
|
|
256
|
+
}
|
|
257
|
+
free(EventQueue.buf);
|
|
258
|
+
EventQueue.buf = buf;
|
|
259
|
+
EventQueue.cap = cap;
|
|
260
|
+
EventQueue.head = 0;
|
|
261
|
+
return 1;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
static int event_queue_push(RubyEvent* evp);
|
|
265
|
+
|
|
266
|
+
// `owned` is a malloc'd buffer the queue takes over; otherwise data is copied.
|
|
267
|
+
static int
|
|
268
|
+
queue_event_full(HQUIC connection, void* connection_ctx, VALUE client_obj,
|
|
269
|
+
const char* event_type, uint64_t stream_id,
|
|
270
|
+
const char* data, size_t data_len, int early_data,
|
|
271
|
+
char* owned, ReleaseKind release, void* release_ptr)
|
|
272
|
+
{
|
|
273
|
+
RubyEvent ev;
|
|
274
|
+
ev.connection = connection;
|
|
275
|
+
ev.connection_ctx = connection_ctx;
|
|
276
|
+
ev.client_obj = client_obj;
|
|
277
|
+
ev.event_type = event_type;
|
|
278
|
+
ev.stream_id = stream_id;
|
|
279
|
+
ev.data_len = data_len;
|
|
280
|
+
ev.early_data = early_data;
|
|
281
|
+
ev.release = release;
|
|
282
|
+
ev.release_ptr = release_ptr;
|
|
283
|
+
if (owned) {
|
|
284
|
+
ev.data = owned;
|
|
285
|
+
} else if (data_len <= EVENT_INLINE_DATA) {
|
|
286
|
+
ev.data = NULL;
|
|
287
|
+
if (data_len) memcpy(ev.inline_data, data, data_len);
|
|
288
|
+
} else {
|
|
289
|
+
ev.data = malloc(data_len);
|
|
290
|
+
if (!ev.data) return 0;
|
|
291
|
+
memcpy(ev.data, data, data_len);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
ev.dropped = 0;
|
|
295
|
+
ev.stream = NULL;
|
|
296
|
+
ev.buffers = NULL;
|
|
297
|
+
ev.buffer_count = 0;
|
|
298
|
+
return event_queue_push(&ev);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Copies the record into the ring; frees ev->data on failure.
|
|
302
|
+
static int
|
|
303
|
+
event_queue_push(RubyEvent* evp)
|
|
304
|
+
{
|
|
305
|
+
RubyEvent ev = *evp;
|
|
306
|
+
const char* event_type = ev.event_type;
|
|
307
|
+
void* connection_ctx = ev.connection_ctx;
|
|
308
|
+
int is_datagram = strcmp(event_type, "DATAGRAM_RECEIVED") == 0;
|
|
309
|
+
ConnectionContext* conn = (ConnectionContext*)connection_ctx;
|
|
310
|
+
pthread_mutex_lock(&EventQueue.lock);
|
|
311
|
+
if (is_datagram && conn && conn->queued_datagrams >= DATAGRAM_QUEUE_LIMIT) {
|
|
312
|
+
// Make room by dropping the oldest; the new one is queued below.
|
|
313
|
+
drop_oldest_datagram(conn);
|
|
314
|
+
conn->queued_datagrams--;
|
|
315
|
+
conn->dropped_datagrams++;
|
|
316
|
+
DroppedDatagrams++;
|
|
317
|
+
}
|
|
318
|
+
if (EventQueue.len == EventQueue.cap && !event_queue_grow()) {
|
|
319
|
+
pthread_mutex_unlock(&EventQueue.lock);
|
|
320
|
+
free(ev.data);
|
|
321
|
+
return 0;
|
|
322
|
+
}
|
|
323
|
+
RubyEvent* slot = &EventQueue.buf[(EventQueue.head + EventQueue.len) % EventQueue.cap];
|
|
324
|
+
*slot = ev;
|
|
325
|
+
if (ev.buffers == evp->inline_buffers) slot->buffers = slot->inline_buffers;
|
|
326
|
+
EventQueue.len++;
|
|
327
|
+
if (is_datagram && conn) conn->queued_datagrams++;
|
|
328
|
+
pthread_cond_signal(&EventQueue.ready);
|
|
329
|
+
pthread_mutex_unlock(&EventQueue.lock);
|
|
330
|
+
return 1;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
static int
|
|
334
|
+
queue_event(HQUIC connection, void* connection_ctx, VALUE client_obj,
|
|
335
|
+
const char* event_type, uint64_t stream_id,
|
|
336
|
+
const char* data, size_t data_len, int early_data)
|
|
337
|
+
{
|
|
338
|
+
return queue_event_full(connection, connection_ctx, client_obj, event_type, stream_id,
|
|
339
|
+
data, data_len, early_data, NULL, RELEASE_NONE, NULL);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// --- Zero-copy sends ---------------------------------------------------------
|
|
343
|
+
//
|
|
344
|
+
// A send used to malloc a QUIC_BUFFER plus a copy of the body and memcpy the
|
|
345
|
+
// body into it, with the GVL held, then free it on SEND_COMPLETE. For a 12MB
|
|
346
|
+
// response that is a 12MB allocate-and-copy during which no other Ruby thread
|
|
347
|
+
// runs. Instead the Ruby String is frozen and retained, and QUIC_BUFFER points
|
|
348
|
+
// straight at its bytes. MsQuic owns the pointer until SEND_COMPLETE, so the
|
|
349
|
+
// String must not move or be collected until then: LiveSends is a GC root
|
|
350
|
+
// that marks every retained String, and SEND_COMPLETE removes the entry.
|
|
351
|
+
//
|
|
352
|
+
// Freezing is what makes the pointer stable. A frozen String cannot be
|
|
353
|
+
// resized or have its buffer replaced, and the encoder hands these Strings
|
|
354
|
+
// over without reusing them. Compaction could still move an unfrozen embedded
|
|
355
|
+
// String; rb_gc_mark (not rb_gc_mark_movable) pins what it marks, so the
|
|
356
|
+
// retained Strings stay put.
|
|
357
|
+
//
|
|
358
|
+
// Every mutation of LiveSends happens with the GVL held: quicsilver_send_stream
|
|
359
|
+
// is a Ruby method, and the release for SEND_COMPLETE runs on the dispatcher
|
|
360
|
+
// thread, not in the callback. GC runs with the GVL. So the table needs no
|
|
361
|
+
// lock.
|
|
362
|
+
typedef struct {
|
|
363
|
+
QUIC_BUFFER buffer;
|
|
364
|
+
VALUE data;
|
|
365
|
+
} SendContext;
|
|
366
|
+
|
|
367
|
+
static st_table* LiveSends;
|
|
368
|
+
static VALUE LiveSendsRoot = Qnil;
|
|
369
|
+
// gc_mark_children skips a T_DATA's dmark when its data pointer is NULL, so
|
|
370
|
+
// the root wraps this instead of NULL. Its value is never read.
|
|
371
|
+
static int LiveSendsRootData;
|
|
372
|
+
|
|
373
|
+
static int
|
|
374
|
+
mark_live_send(st_data_t key, st_data_t value, st_data_t arg)
|
|
375
|
+
{
|
|
376
|
+
(void)key; (void)arg;
|
|
377
|
+
rb_gc_mark(((SendContext*)value)->data);
|
|
378
|
+
return ST_CONTINUE;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
static void
|
|
382
|
+
live_sends_mark(void* ptr)
|
|
383
|
+
{
|
|
384
|
+
(void)ptr;
|
|
385
|
+
if (LiveSends) st_foreach(LiveSends, mark_live_send, 0);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
static const rb_data_type_t live_sends_type = {
|
|
389
|
+
"Quicsilver::LiveSends",
|
|
390
|
+
{ live_sends_mark, NULL, NULL, },
|
|
391
|
+
0, 0, RUBY_TYPED_FREE_IMMEDIATELY
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
static SendContext*
|
|
395
|
+
send_context_new(VALUE data)
|
|
396
|
+
{
|
|
397
|
+
SendContext* ctx = malloc(sizeof(SendContext));
|
|
398
|
+
if (!ctx) return NULL;
|
|
399
|
+
ctx->data = data;
|
|
400
|
+
ctx->buffer.Buffer = (uint8_t*)RSTRING_PTR(data);
|
|
401
|
+
ctx->buffer.Length = (uint32_t)RSTRING_LEN(data);
|
|
402
|
+
st_insert(LiveSends, (st_data_t)ctx, (st_data_t)ctx);
|
|
403
|
+
return ctx;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
static void
|
|
407
|
+
send_context_release(void* raw)
|
|
408
|
+
{
|
|
409
|
+
if (!raw) return;
|
|
410
|
+
st_data_t key = (st_data_t)raw;
|
|
411
|
+
st_delete(LiveSends, &key, NULL);
|
|
412
|
+
free(raw);
|
|
413
|
+
}
|
|
414
|
+
static uint64_t NextStreamToken = 1;
|
|
415
|
+
|
|
416
|
+
static uint64_t
|
|
417
|
+
register_stream(StreamContext* ctx, HQUIC stream, QUIC_STREAM_SHUTDOWN_FLAGS flags)
|
|
418
|
+
{
|
|
419
|
+
ctx->stream = stream;
|
|
420
|
+
ctx->abort_flags = flags;
|
|
421
|
+
ctx->receive_credit = 0;
|
|
422
|
+
ctx->receive_chunks = 0;
|
|
423
|
+
ctx->receive_paused = 0;
|
|
424
|
+
ctx->receive_credit_enabled = 0;
|
|
425
|
+
ctx->receive_delivery_failed = 0;
|
|
426
|
+
ctx->fin_delivered = 0;
|
|
427
|
+
ctx->pending_priority = 0;
|
|
428
|
+
// The token is minted under the same lock that inserts it. Peer streams
|
|
429
|
+
// are registered on the poll thread and client streams on Ruby threads;
|
|
430
|
+
// with the poll thread no longer holding the GVL, a bare increment here
|
|
431
|
+
// handed two streams one token, and send_stream resolved to the wrong one.
|
|
432
|
+
// Found as a client receiving its own request frame as a response.
|
|
433
|
+
STATE_LOCK();
|
|
434
|
+
ctx->token = NextStreamToken++;
|
|
435
|
+
st_insert(LiveStreams, (st_data_t)ctx->token, (st_data_t)ctx);
|
|
436
|
+
STATE_UNLOCK();
|
|
437
|
+
return ctx->token;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
static StreamContext*
|
|
441
|
+
find_stream(uint64_t token)
|
|
442
|
+
{
|
|
443
|
+
st_data_t entry;
|
|
444
|
+
STATE_LOCK();
|
|
445
|
+
int found = st_lookup(LiveStreams, (st_data_t)token, &entry);
|
|
446
|
+
STATE_UNLOCK();
|
|
447
|
+
return found ? (StreamContext*)entry : NULL;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
static void
|
|
451
|
+
unregister_stream(uint64_t token)
|
|
452
|
+
{
|
|
453
|
+
st_data_t key = (st_data_t)token;
|
|
454
|
+
STATE_LOCK();
|
|
455
|
+
st_delete(LiveStreams, &key, NULL);
|
|
456
|
+
STATE_UNLOCK();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
static int
|
|
460
|
+
forget_connection_stream(st_data_t key, st_data_t value, st_data_t connection)
|
|
461
|
+
{
|
|
462
|
+
StreamContext* ctx = (StreamContext*)value;
|
|
463
|
+
if (ctx->connection != (HQUIC)connection) return ST_CONTINUE;
|
|
464
|
+
return ST_DELETE;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
static void
|
|
468
|
+
forget_connection_streams(HQUIC connection)
|
|
469
|
+
{
|
|
470
|
+
STATE_LOCK();
|
|
471
|
+
st_foreach(LiveStreams, forget_connection_stream, (st_data_t)connection);
|
|
472
|
+
STATE_UNLOCK();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
// rb_protect wrapper — catches Ruby exceptions so they don't longjmp
|
|
477
|
+
// through MsQuic callback frames (which would corrupt MsQuic state).
|
|
478
|
+
// All Ruby object construction AND the funcall happen inside rb_protect.
|
|
479
|
+
struct dispatch_ruby_args {
|
|
480
|
+
HQUIC connection;
|
|
481
|
+
void* connection_ctx;
|
|
482
|
+
VALUE client_obj;
|
|
483
|
+
// For a pending receive: gather these into the String after the token.
|
|
484
|
+
const QUIC_BUFFER* buffers;
|
|
485
|
+
uint32_t buffer_count;
|
|
486
|
+
uint64_t token;
|
|
487
|
+
uint64_t accepted;
|
|
488
|
+
VALUE payload; // Qundef, or a String built before dispatch
|
|
489
|
+
char remote_address[INET6_ADDRSTRLEN];
|
|
490
|
+
uint16_t remote_port;
|
|
491
|
+
int session_resumed;
|
|
492
|
+
const char* event_type;
|
|
493
|
+
uint64_t stream_id;
|
|
494
|
+
const char* data;
|
|
495
|
+
size_t data_len;
|
|
496
|
+
int early_data;
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
// [token][bytes], gathered straight from MsQuic's buffers.
|
|
500
|
+
static VALUE
|
|
501
|
+
gather_receive_string(struct dispatch_ruby_args* a)
|
|
502
|
+
{
|
|
503
|
+
VALUE str = rb_str_new(NULL, (long)(sizeof(a->token) + a->accepted));
|
|
504
|
+
char* out = RSTRING_PTR(str);
|
|
505
|
+
memcpy(out, &a->token, sizeof(a->token));
|
|
506
|
+
out += sizeof(a->token);
|
|
507
|
+
uint64_t remaining = a->accepted;
|
|
508
|
+
for (uint32_t b = 0; b < a->buffer_count && remaining; b++) {
|
|
509
|
+
size_t len = a->buffers[b].Length;
|
|
510
|
+
if (len > remaining) len = (size_t)remaining;
|
|
511
|
+
memcpy(out, a->buffers[b].Buffer, len);
|
|
512
|
+
out += len;
|
|
513
|
+
remaining -= len;
|
|
514
|
+
}
|
|
515
|
+
return str;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
static VALUE
|
|
519
|
+
gather_receive_protected(VALUE arg)
|
|
520
|
+
{
|
|
521
|
+
return gather_receive_string((struct dispatch_ruby_args*)arg);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
static VALUE
|
|
525
|
+
dispatch_ruby_body(VALUE arg)
|
|
526
|
+
{
|
|
527
|
+
struct dispatch_ruby_args* a = (struct dispatch_ruby_args*)arg;
|
|
528
|
+
VALUE payload = a->payload != Qundef ? a->payload : rb_str_new(a->data, a->data_len);
|
|
529
|
+
|
|
530
|
+
if (NIL_P(a->client_obj)) {
|
|
531
|
+
VALUE server_class = rb_const_get_at(mQuicsilver, rb_intern("Server"));
|
|
532
|
+
if (rb_class_real(CLASS_OF(server_class)) == rb_cClass) {
|
|
533
|
+
ConnectionContext* conn_ctx = (ConnectionContext*)a->connection_ctx;
|
|
534
|
+
VALUE connection_data = rb_ary_new2(3);
|
|
535
|
+
rb_ary_push(connection_data, ULL2NUM((uintptr_t)a->connection));
|
|
536
|
+
rb_ary_push(connection_data, ULL2NUM((uintptr_t)a->connection_ctx));
|
|
537
|
+
rb_ary_push(connection_data, conn_ctx && conn_ctx->session_resumed ? Qtrue : Qfalse);
|
|
538
|
+
VALUE argv[5] = {
|
|
539
|
+
connection_data,
|
|
540
|
+
ULL2NUM(a->stream_id),
|
|
541
|
+
rb_str_new_cstr(a->event_type),
|
|
542
|
+
payload,
|
|
543
|
+
a->early_data ? Qtrue : Qfalse
|
|
544
|
+
};
|
|
545
|
+
rb_funcallv(server_class, rb_intern("handle_stream"), 5, argv);
|
|
546
|
+
}
|
|
547
|
+
} else {
|
|
548
|
+
if (RB_TYPE_P(a->client_obj, T_OBJECT)) {
|
|
549
|
+
VALUE argv[4] = {
|
|
550
|
+
ULL2NUM(a->stream_id),
|
|
551
|
+
rb_str_new_cstr(a->event_type),
|
|
552
|
+
payload,
|
|
553
|
+
a->early_data ? Qtrue : Qfalse
|
|
554
|
+
};
|
|
555
|
+
rb_funcallv(a->client_obj, rb_intern("handle_stream_event"), 4, argv);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return Qnil;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
static VALUE
|
|
563
|
+
format_dispatch_error(VALUE err)
|
|
564
|
+
{
|
|
565
|
+
VALUE klass = rb_class_name(rb_obj_class(err));
|
|
566
|
+
VALUE msg = rb_funcall(err, rb_intern("message"), 0);
|
|
567
|
+
VALUE bt = rb_funcall(err, rb_intern("backtrace"), 0);
|
|
568
|
+
fprintf(stderr, "Quicsilver: exception in callback: %s: %s\n",
|
|
569
|
+
StringValueCStr(klass), StringValueCStr(msg));
|
|
570
|
+
if (RB_TYPE_P(bt, T_ARRAY) && RARRAY_LEN(bt) > 0) {
|
|
571
|
+
long bt_len = RARRAY_LEN(bt) < 5 ? RARRAY_LEN(bt) : 5;
|
|
572
|
+
for (long i = 0; i < bt_len; i++) {
|
|
573
|
+
VALUE line = rb_ary_entry(bt, i);
|
|
574
|
+
fprintf(stderr, " %s\n", StringValueCStr(line));
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return Qnil;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Deliver one event to Ruby, on the dispatcher thread, with the GVL. The body
|
|
581
|
+
// is wrapped in rb_protect so an exception in the handler is logged rather
|
|
582
|
+
// than unwinding the dispatcher. Returns the protect state: nonzero means the
|
|
583
|
+
// handler raised.
|
|
584
|
+
static int
|
|
585
|
+
dispatch_to_ruby(HQUIC connection, void* connection_ctx, VALUE client_obj,
|
|
586
|
+
const char* event_type, uint64_t stream_id,
|
|
587
|
+
const char* data, size_t data_len, int early_data,
|
|
588
|
+
VALUE prebuilt)
|
|
589
|
+
{
|
|
590
|
+
struct dispatch_ruby_args args;
|
|
591
|
+
args.buffers = NULL;
|
|
592
|
+
args.buffer_count = 0;
|
|
593
|
+
args.token = 0;
|
|
594
|
+
args.accepted = 0;
|
|
595
|
+
args.payload = prebuilt;
|
|
596
|
+
args.connection = connection;
|
|
597
|
+
args.connection_ctx = connection_ctx;
|
|
598
|
+
args.client_obj = client_obj;
|
|
599
|
+
args.event_type = event_type;
|
|
600
|
+
args.stream_id = stream_id;
|
|
601
|
+
args.data = data;
|
|
602
|
+
args.data_len = data_len;
|
|
603
|
+
args.early_data = early_data;
|
|
604
|
+
|
|
605
|
+
int state = 0;
|
|
606
|
+
rb_protect(dispatch_ruby_body, (VALUE)&args, &state);
|
|
607
|
+
if (state) {
|
|
608
|
+
VALUE err = rb_errinfo();
|
|
609
|
+
rb_set_errinfo(Qnil);
|
|
610
|
+
if (!NIL_P(err)) {
|
|
611
|
+
// #message and #backtrace are Ruby methods and can raise. Protect
|
|
612
|
+
// them too, or an exception in the handler's exception takes the
|
|
613
|
+
// dispatcher thread down.
|
|
614
|
+
int format_state = 0;
|
|
615
|
+
rb_protect(format_dispatch_error, err, &format_state);
|
|
616
|
+
if (format_state) {
|
|
617
|
+
rb_set_errinfo(Qnil);
|
|
618
|
+
fprintf(stderr, "Quicsilver: exception in callback (unprintable)\n");
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return state;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Unsigned credit argument: NUM2ULL wraps negatives, so check the sign first.
|
|
626
|
+
static uint64_t
|
|
627
|
+
credit_arg(VALUE value, const char* label)
|
|
628
|
+
{
|
|
629
|
+
if (!RB_INTEGER_TYPE_P(value)) rb_raise(rb_eTypeError, "%s must be an integer", label);
|
|
630
|
+
if (RTEST(rb_funcall(value, rb_intern("<"), 1, INT2FIX(0)))) {
|
|
631
|
+
rb_raise(rb_eArgError, "%s must not be negative", label);
|
|
632
|
+
}
|
|
633
|
+
return NUM2ULL(value);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
static int
|
|
637
|
+
resume_receive(StreamContext* ctx)
|
|
638
|
+
{
|
|
639
|
+
QUIC_STATUS status = MsQuic->StreamReceiveSetEnabled(ctx->stream, TRUE);
|
|
640
|
+
if (QUIC_FAILED(status)) return 0;
|
|
641
|
+
return 1;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Returns QUIC_STATUS_PENDING: MsQuic keeps the bytes until the dispatcher
|
|
645
|
+
// has copied them into the Ruby String and called StreamReceiveComplete. The
|
|
646
|
+
// bytes outlive this callback (freed at StreamClose, which the dispatcher does
|
|
647
|
+
// later); the QUIC_BUFFER descriptors do not and are copied. One receive per
|
|
648
|
+
// stream is pending at a time, so the queue holds at most one per stream.
|
|
649
|
+
//
|
|
650
|
+
// When credit enforcement is active (receive_credit_enabled, set by the first
|
|
651
|
+
// grant) we accept only what fits and complete with that length; MsQuic then
|
|
652
|
+
// disables further receives until StreamReceiveSetEnabled, which the next
|
|
653
|
+
// sufficient grant issues (see receive_paused). A partially accepted receive
|
|
654
|
+
// never carries FIN: it arrives with the suffix.
|
|
655
|
+
static QUIC_STATUS
|
|
656
|
+
receive_with_credit(StreamContext* ctx, QUIC_STREAM_EVENT* event)
|
|
657
|
+
{
|
|
658
|
+
uint64_t indicated = event->RECEIVE.TotalBufferLength;
|
|
659
|
+
uint64_t accepted = indicated;
|
|
660
|
+
|
|
661
|
+
// Decide and debit under one lock hold, so a concurrent grant sees either
|
|
662
|
+
// the state before this receive or after it, never the middle.
|
|
663
|
+
STATE_LOCK();
|
|
664
|
+
int failed_before = ctx->receive_delivery_failed;
|
|
665
|
+
if (ctx->receive_credit_enabled) {
|
|
666
|
+
if (accepted > ctx->receive_credit) accepted = ctx->receive_credit;
|
|
667
|
+
if (!ctx->receive_chunks) accepted = 0;
|
|
668
|
+
ctx->receive_credit -= accepted;
|
|
669
|
+
if (accepted) ctx->receive_chunks--;
|
|
670
|
+
if (accepted < indicated) ctx->receive_paused = 1;
|
|
671
|
+
}
|
|
672
|
+
STATE_UNLOCK();
|
|
673
|
+
|
|
674
|
+
int has_fin = (event->RECEIVE.Flags & QUIC_RECEIVE_FLAG_FIN) && accepted == indicated;
|
|
675
|
+
if (failed_before || (!accepted && !has_fin)) {
|
|
676
|
+
event->RECEIVE.TotalBufferLength = 0;
|
|
677
|
+
return QUIC_STATUS_SUCCESS;
|
|
678
|
+
}
|
|
679
|
+
if (accepted > LONG_MAX - sizeof(uint64_t)) goto failed;
|
|
680
|
+
|
|
681
|
+
RubyEvent ev;
|
|
682
|
+
memset(&ev, 0, sizeof(ev));
|
|
683
|
+
ev.connection = ctx->connection;
|
|
684
|
+
ev.connection_ctx = ctx->connection_ctx;
|
|
685
|
+
ev.client_obj = ctx->client_obj;
|
|
686
|
+
ev.event_type = has_fin ? "RECEIVE_FIN" : "RECEIVE";
|
|
687
|
+
ev.stream_id = ctx->stream_id;
|
|
688
|
+
ev.early_data = has_fin ? ctx->early_data : 0;
|
|
689
|
+
ev.stream = ctx->stream;
|
|
690
|
+
ev.token = ctx->token;
|
|
691
|
+
ev.accepted = accepted;
|
|
692
|
+
ev.data = NULL;
|
|
693
|
+
ev.data_len = sizeof(uint64_t) + (size_t)accepted;
|
|
694
|
+
|
|
695
|
+
uint32_t count = 0;
|
|
696
|
+
uint64_t covered = 0;
|
|
697
|
+
while (count < event->RECEIVE.BufferCount && covered < accepted) {
|
|
698
|
+
covered += event->RECEIVE.Buffers[count].Length;
|
|
699
|
+
count++;
|
|
700
|
+
}
|
|
701
|
+
ev.buffer_count = count;
|
|
702
|
+
if (count <= EVENT_INLINE_BUFFERS) {
|
|
703
|
+
ev.buffers = ev.inline_buffers;
|
|
704
|
+
} else {
|
|
705
|
+
ev.buffers = malloc(count * sizeof(QUIC_BUFFER));
|
|
706
|
+
if (!ev.buffers) goto failed;
|
|
707
|
+
}
|
|
708
|
+
memcpy(ev.buffers, event->RECEIVE.Buffers, count * sizeof(QUIC_BUFFER));
|
|
709
|
+
|
|
710
|
+
if (!event_queue_push(&ev)) {
|
|
711
|
+
if (ev.buffers != ev.inline_buffers) free(ev.buffers);
|
|
712
|
+
goto failed;
|
|
713
|
+
}
|
|
714
|
+
if (has_fin) ctx->fin_delivered = 1;
|
|
715
|
+
return QUIC_STATUS_PENDING;
|
|
716
|
+
|
|
717
|
+
failed:
|
|
718
|
+
// Last resort: a receive we could not hand to Ruby is data the stream can
|
|
719
|
+
// never deliver in order, so the stream ends here.
|
|
720
|
+
fprintf(stderr, "Quicsilver: receive delivery failed on stream %llu; aborting\n",
|
|
721
|
+
(unsigned long long)ctx->stream_id);
|
|
722
|
+
STATE_LOCK();
|
|
723
|
+
ctx->receive_delivery_failed = 1;
|
|
724
|
+
ctx->receive_credit = 0;
|
|
725
|
+
STATE_UNLOCK();
|
|
726
|
+
// Already in a native callback; inline abort avoids allocating a queued
|
|
727
|
+
// operation on this failure path. It may retire ctx before returning.
|
|
728
|
+
MsQuic->StreamShutdown(ctx->stream, ctx->abort_flags | QUIC_STREAM_SHUTDOWN_FLAG_INLINE, 0);
|
|
729
|
+
event->RECEIVE.TotalBufferLength = 0;
|
|
730
|
+
return QUIC_STATUS_SUCCESS;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Platform I/O wait — called without GVL so other Ruby threads can run
|
|
734
|
+
struct poll_args {
|
|
735
|
+
QUIC_EVENTQ eq;
|
|
736
|
+
QUIC_CQE events[64];
|
|
737
|
+
int max_events;
|
|
738
|
+
int timeout_ms;
|
|
739
|
+
int count;
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
static void*
|
|
743
|
+
eventq_wait_nogvl(void* arg)
|
|
744
|
+
{
|
|
745
|
+
struct poll_args* a = (struct poll_args*)arg;
|
|
746
|
+
#if __linux__
|
|
747
|
+
a->count = epoll_wait(a->eq, a->events, a->max_events, a->timeout_ms);
|
|
748
|
+
if (a->count < 0 && errno == EINTR) a->count = 0;
|
|
749
|
+
#elif __APPLE__ || __FreeBSD__
|
|
750
|
+
struct timespec ts;
|
|
751
|
+
ts.tv_sec = a->timeout_ms / 1000;
|
|
752
|
+
ts.tv_nsec = (a->timeout_ms % 1000) * 1000000;
|
|
753
|
+
a->count = kevent(a->eq, NULL, 0, a->events, a->max_events, &ts);
|
|
754
|
+
if (a->count < 0 && errno == EINTR) a->count = 0;
|
|
755
|
+
#endif
|
|
756
|
+
return NULL;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
static inline QUIC_SQE*
|
|
760
|
+
cqe_get_sqe(QUIC_CQE* cqe)
|
|
761
|
+
{
|
|
762
|
+
#if __linux__
|
|
763
|
+
return (QUIC_SQE*)cqe->data.ptr;
|
|
764
|
+
#elif __APPLE__ || __FreeBSD__
|
|
765
|
+
return (QUIC_SQE*)cqe->udata;
|
|
766
|
+
#endif
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Drive MsQuic execution: poll internal timers, wait for I/O, fire completions.
|
|
770
|
+
// Callbacks (StreamCallback, ConnectionCallback) fire HERE on the Ruby thread.
|
|
771
|
+
// One iteration of the MsQuic loop, with the GVL released for all of it.
|
|
772
|
+
//
|
|
773
|
+
// Before, only the kevent/epoll_wait sleep released the GVL; ExecutionPoll and
|
|
774
|
+
// the completion callbacks, which is where MsQuic decrypts packets, runs loss
|
|
775
|
+
// recovery and reassembles streams, ran with it held, because the callbacks
|
|
776
|
+
// called into Ruby. They no longer do, so none of this needs the lock, and
|
|
777
|
+
// the dispatcher and worker threads run in parallel with the datapath.
|
|
778
|
+
static void*
|
|
779
|
+
poll_nogvl(void* arg)
|
|
780
|
+
{
|
|
781
|
+
struct poll_args* a = (struct poll_args*)arg;
|
|
782
|
+
|
|
783
|
+
uint32_t wait_ms = MsQuic->ExecutionPoll(ExecContext);
|
|
784
|
+
// With wake_event_loop(), Ruby threads instantly unblock us when work is
|
|
785
|
+
// queued. Cap at 1s as a safety net for shutdown responsiveness.
|
|
786
|
+
a->timeout_ms = (int)((wait_ms == UINT32_MAX) ? 1000 : wait_ms);
|
|
787
|
+
a->count = 0;
|
|
788
|
+
eventq_wait_nogvl(a);
|
|
789
|
+
|
|
790
|
+
for (int i = 0; i < a->count; i++) {
|
|
791
|
+
#if __linux__
|
|
792
|
+
if (a->events[i].data.ptr == NULL) {
|
|
793
|
+
uint64_t val;
|
|
794
|
+
read(WakeFd, &val, sizeof(val)); // drain eventfd
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
#elif __APPLE__ || __FreeBSD__
|
|
798
|
+
if (a->events[i].filter == EVFILT_USER && a->events[i].ident == WAKE_IDENT) continue;
|
|
799
|
+
#endif
|
|
800
|
+
QUIC_SQE* sqe = cqe_get_sqe(&a->events[i]);
|
|
801
|
+
if (sqe && sqe->Completion) {
|
|
802
|
+
sqe->Completion(&a->events[i]);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return NULL;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
static VALUE
|
|
809
|
+
quicsilver_poll(VALUE self)
|
|
810
|
+
{
|
|
811
|
+
if (ExecContext == NULL) return INT2NUM(0);
|
|
812
|
+
|
|
813
|
+
struct poll_args args;
|
|
814
|
+
args.eq = EventQ;
|
|
815
|
+
args.max_events = 64;
|
|
816
|
+
rb_thread_call_without_gvl(poll_nogvl, &args, RUBY_UBF_IO, NULL);
|
|
817
|
+
return INT2NUM(args.count);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// --- Dispatcher side ---------------------------------------------------------
|
|
821
|
+
|
|
822
|
+
// Upper bound on events per dispatch_events call, and the size of the batch
|
|
823
|
+
// copied out under the queue lock. Fixed rather than caller-sized so the
|
|
824
|
+
// stack frame is known.
|
|
825
|
+
#define DISPATCH_BATCH_MAX 64
|
|
826
|
+
|
|
827
|
+
struct queue_wait_args {
|
|
828
|
+
int timeout_ms;
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
static void*
|
|
832
|
+
queue_wait_nogvl(void* arg)
|
|
833
|
+
{
|
|
834
|
+
struct queue_wait_args* a = (struct queue_wait_args*)arg;
|
|
835
|
+
struct timespec deadline;
|
|
836
|
+
clock_gettime(CLOCK_REALTIME, &deadline);
|
|
837
|
+
deadline.tv_sec += a->timeout_ms / 1000;
|
|
838
|
+
deadline.tv_nsec += (long)(a->timeout_ms % 1000) * 1000000L;
|
|
839
|
+
if (deadline.tv_nsec >= 1000000000L) { deadline.tv_sec++; deadline.tv_nsec -= 1000000000L; }
|
|
840
|
+
|
|
841
|
+
pthread_mutex_lock(&EventQueue.lock);
|
|
842
|
+
while (EventQueue.len == 0 && !EventQueue.closed) {
|
|
843
|
+
if (pthread_cond_timedwait(&EventQueue.ready, &EventQueue.lock, &deadline) == ETIMEDOUT) break;
|
|
844
|
+
}
|
|
845
|
+
pthread_mutex_unlock(&EventQueue.lock);
|
|
846
|
+
return NULL;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Thread#raise / Thread#kill against a dispatcher blocked in the wait.
|
|
850
|
+
static void
|
|
851
|
+
queue_wake_ubf(void* arg)
|
|
852
|
+
{
|
|
853
|
+
(void)arg;
|
|
854
|
+
pthread_mutex_lock(&EventQueue.lock);
|
|
855
|
+
pthread_cond_broadcast(&EventQueue.ready);
|
|
856
|
+
pthread_mutex_unlock(&EventQueue.lock);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// What a callback used to do after dispatch, done here with the GVL held.
|
|
860
|
+
static void
|
|
861
|
+
perform_release(RubyEvent* ev)
|
|
862
|
+
{
|
|
863
|
+
switch (ev->release) {
|
|
864
|
+
case RELEASE_NONE:
|
|
865
|
+
break;
|
|
866
|
+
case RELEASE_SEND:
|
|
867
|
+
send_context_release(ev->release_ptr);
|
|
868
|
+
break;
|
|
869
|
+
case RELEASE_STREAM:
|
|
870
|
+
case RELEASE_STREAM_NOCLOSE: {
|
|
871
|
+
StreamContext* ctx = (StreamContext*)ev->release_ptr;
|
|
872
|
+
unregister_stream(ctx->token);
|
|
873
|
+
if (ev->release == RELEASE_STREAM) MsQuic->StreamClose(ctx->stream);
|
|
874
|
+
free(ctx);
|
|
875
|
+
break;
|
|
876
|
+
}
|
|
877
|
+
case RELEASE_CONNECTION: {
|
|
878
|
+
ConnectionContext* ctx = (ConnectionContext*)ev->release_ptr;
|
|
879
|
+
forget_connection_streams(ev->connection);
|
|
880
|
+
if (!NIL_P(ctx->client_obj)) rb_gc_unregister_address(&ctx->client_obj);
|
|
881
|
+
free(ctx->resumption_ticket);
|
|
882
|
+
free(ctx);
|
|
883
|
+
break;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// A handler that raised on RECEIVE has broken the stream's byte order for
|
|
889
|
+
// good, so the stream is aborted, as the callback used to do inline.
|
|
890
|
+
static void
|
|
891
|
+
abort_after_failed_receive(RubyEvent* ev)
|
|
892
|
+
{
|
|
893
|
+
if (strncmp(ev->event_type, "RECEIVE", 7) != 0) return;
|
|
894
|
+
uint64_t token = ev->token;
|
|
895
|
+
StreamContext* ctx = find_stream(token);
|
|
896
|
+
if (!ctx) return;
|
|
897
|
+
STATE_LOCK();
|
|
898
|
+
ctx->receive_delivery_failed = 1;
|
|
899
|
+
ctx->receive_credit = 0;
|
|
900
|
+
STATE_UNLOCK();
|
|
901
|
+
MsQuic->StreamShutdown(ctx->stream, ctx->abort_flags, 0);
|
|
902
|
+
wake_event_loop();
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Wait up to timeout_ms for events, then deliver up to max_events of them to
|
|
906
|
+
// Ruby. Returns the number delivered, or nil once the queue is closed and
|
|
907
|
+
// empty, which is the dispatcher's signal to exit.
|
|
908
|
+
static VALUE
|
|
909
|
+
quicsilver_dispatch_events(VALUE self, VALUE max_events, VALUE timeout_ms)
|
|
910
|
+
{
|
|
911
|
+
long max = NUM2LONG(max_events);
|
|
912
|
+
if (max < 1) max = 1;
|
|
913
|
+
if (max > DISPATCH_BATCH_MAX) max = DISPATCH_BATCH_MAX;
|
|
914
|
+
struct queue_wait_args wait = { NUM2INT(timeout_ms) };
|
|
915
|
+
rb_thread_call_without_gvl(queue_wait_nogvl, &wait, queue_wake_ubf, NULL);
|
|
916
|
+
|
|
917
|
+
RubyEvent batch[DISPATCH_BATCH_MAX];
|
|
918
|
+
long n = 0;
|
|
919
|
+
int closed;
|
|
920
|
+
pthread_mutex_lock(&EventQueue.lock);
|
|
921
|
+
closed = EventQueue.closed;
|
|
922
|
+
while (n < max && EventQueue.len > 0) {
|
|
923
|
+
RubyEvent* ev = &EventQueue.buf[EventQueue.head];
|
|
924
|
+
if (!ev->dropped && ev->connection_ctx && strcmp(ev->event_type, "DATAGRAM_RECEIVED") == 0) {
|
|
925
|
+
((ConnectionContext*)ev->connection_ctx)->queued_datagrams--;
|
|
926
|
+
}
|
|
927
|
+
batch[n] = *ev;
|
|
928
|
+
if (ev->buffers == ev->inline_buffers) batch[n].buffers = batch[n].inline_buffers;
|
|
929
|
+
n++;
|
|
930
|
+
EventQueue.head = (EventQueue.head + 1) % EventQueue.cap;
|
|
931
|
+
EventQueue.len--;
|
|
932
|
+
}
|
|
933
|
+
pthread_mutex_unlock(&EventQueue.lock);
|
|
934
|
+
|
|
935
|
+
for (long i = 0; i < n; i++) {
|
|
936
|
+
RubyEvent* ev = &batch[i];
|
|
937
|
+
if (ev->dropped) { free(ev->data); continue; }
|
|
938
|
+
const char* data = ev->data ? ev->data : ev->inline_data;
|
|
939
|
+
VALUE payload = Qundef;
|
|
940
|
+
if (ev->buffers) {
|
|
941
|
+
// Complete before the handler runs. A partial completion pauses
|
|
942
|
+
// the stream; a grant from the handler queues the resume; the
|
|
943
|
+
// pause must be queued first.
|
|
944
|
+
struct dispatch_ruby_args gather = { .buffers = ev->buffers,
|
|
945
|
+
.buffer_count = ev->buffer_count, .token = ev->token, .accepted = ev->accepted };
|
|
946
|
+
int gather_state = 0;
|
|
947
|
+
payload = rb_protect(gather_receive_protected, (VALUE)&gather, &gather_state);
|
|
948
|
+
if (gather_state) { rb_set_errinfo(Qnil); payload = Qundef; }
|
|
949
|
+
if (MsQuic) MsQuic->StreamReceiveComplete(ev->stream, ev->accepted);
|
|
950
|
+
if (ev->buffers != ev->inline_buffers) free(ev->buffers);
|
|
951
|
+
if (payload == Qundef) { abort_after_failed_receive(ev); perform_release(ev); continue; }
|
|
952
|
+
}
|
|
953
|
+
int state = dispatch_to_ruby(ev->connection, ev->connection_ctx, ev->client_obj,
|
|
954
|
+
ev->event_type, ev->stream_id, data, ev->data_len, ev->early_data, payload);
|
|
955
|
+
if (state) abort_after_failed_receive(ev);
|
|
956
|
+
perform_release(ev);
|
|
957
|
+
free(ev->data);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
if (n == 0 && closed) return Qnil;
|
|
961
|
+
// The dispatcher does not yield here. Workers get the GVL when the
|
|
962
|
+
// server's admission queue is under pressure (Server#enqueue_work), which
|
|
963
|
+
// is the only reason to interrupt a batch; an unconditional yield per
|
|
964
|
+
// batch cost throughput and doubled tail latency for nothing.
|
|
965
|
+
return LONG2NUM(n);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// Datagrams dropped because the dispatcher was behind (DATAGRAM_QUEUE_LIMIT).
|
|
969
|
+
// A drop counter nobody can read is a drop debugged by guessing.
|
|
970
|
+
static VALUE
|
|
971
|
+
quicsilver_dropped_datagrams(VALUE self)
|
|
972
|
+
{
|
|
973
|
+
pthread_mutex_lock(&EventQueue.lock);
|
|
974
|
+
uint64_t dropped = DroppedDatagrams;
|
|
975
|
+
pthread_mutex_unlock(&EventQueue.lock);
|
|
976
|
+
return ULL2NUM(dropped);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
|
|
980
|
+
static VALUE
|
|
981
|
+
quicsilver_open_event_queue(VALUE self)
|
|
982
|
+
{
|
|
983
|
+
pthread_mutex_lock(&EventQueue.lock);
|
|
984
|
+
EventQueue.closed = 0;
|
|
985
|
+
pthread_mutex_unlock(&EventQueue.lock);
|
|
986
|
+
return Qnil;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Stops the dispatcher once it has drained what is already queued. Call after
|
|
990
|
+
// the poll thread has stopped, so nothing is pushed afterwards.
|
|
991
|
+
static VALUE
|
|
992
|
+
quicsilver_close_event_queue(VALUE self)
|
|
993
|
+
{
|
|
994
|
+
pthread_mutex_lock(&EventQueue.lock);
|
|
995
|
+
EventQueue.closed = 1;
|
|
996
|
+
pthread_cond_broadcast(&EventQueue.ready);
|
|
997
|
+
pthread_mutex_unlock(&EventQueue.lock);
|
|
998
|
+
return Qnil;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
QUIC_STATUS
|
|
1003
|
+
StreamCallback(HQUIC Stream, void* Context, QUIC_STREAM_EVENT* Event)
|
|
1004
|
+
{
|
|
1005
|
+
StreamContext* ctx = (StreamContext*)Context;
|
|
1006
|
+
|
|
1007
|
+
if (ctx == NULL) {
|
|
1008
|
+
return QUIC_STATUS_SUCCESS;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
uint64_t token = ctx->token;
|
|
1012
|
+
|
|
1013
|
+
// Lazily cache the QUIC stream ID on first callback.
|
|
1014
|
+
// Can't do this at StreamStart — MsQuic defers ID assignment with FLAG_NONE
|
|
1015
|
+
// until data is sent. By the first callback the ID is always assigned.
|
|
1016
|
+
if (ctx->stream_id == UINT64_MAX) {
|
|
1017
|
+
uint64_t id = UINT64_MAX;
|
|
1018
|
+
uint32_t id_len = sizeof(id);
|
|
1019
|
+
MsQuic->GetParam(Stream, QUIC_PARAM_STREAM_ID, &id_len, &id);
|
|
1020
|
+
STATE_LOCK();
|
|
1021
|
+
ctx->stream_id = id;
|
|
1022
|
+
STATE_UNLOCK();
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// MsQuic requires SetParam on its event thread.
|
|
1026
|
+
uint32_t pending = __atomic_exchange_n(&ctx->pending_priority, 0, __ATOMIC_ACQ_REL);
|
|
1027
|
+
if (pending) {
|
|
1028
|
+
uint16_t priority = (uint16_t)(pending - 1);
|
|
1029
|
+
MsQuic->SetParam(Stream, QUIC_PARAM_STREAM_PRIORITY, sizeof(priority), &priority);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
switch (Event->Type) {
|
|
1033
|
+
case QUIC_STREAM_EVENT_RECEIVE: {
|
|
1034
|
+
// Track 0-RTT early data for replay protection
|
|
1035
|
+
if (Event->RECEIVE.Flags & QUIC_RECEIVE_FLAG_0_RTT) {
|
|
1036
|
+
ctx->early_data = 1;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
return receive_with_credit(ctx, Event);
|
|
1040
|
+
}
|
|
1041
|
+
case QUIC_STREAM_EVENT_SEND_COMPLETE:
|
|
1042
|
+
// The retained String is dropped on the dispatcher, after Ruby has
|
|
1043
|
+
// seen the event; LiveSends is only ever touched under the GVL.
|
|
1044
|
+
if (!queue_event_full(ctx->connection, ctx->connection_ctx, ctx->client_obj,
|
|
1045
|
+
"SEND_COMPLETE", ctx->stream_id, (const char*)&token, sizeof(token), 0,
|
|
1046
|
+
NULL, RELEASE_SEND, Event->SEND_COMPLETE.ClientContext)) {
|
|
1047
|
+
// Out of memory. LiveSends is only touched under the GVL, so the
|
|
1048
|
+
// SendContext and the String it pins leak rather than race GC.
|
|
1049
|
+
fprintf(stderr, "Quicsilver: event queue allocation failed; leaking one send buffer\n");
|
|
1050
|
+
}
|
|
1051
|
+
break;
|
|
1052
|
+
case QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE:
|
|
1053
|
+
// Nothing is torn down here. The stream stays in LiveStreams, and
|
|
1054
|
+
// its handle stays open, until the dispatcher has delivered this
|
|
1055
|
+
// event: Ruby may still be handling an earlier RECEIVE for it (a
|
|
1056
|
+
// uni stream with FIN completes natively before Ruby sees its
|
|
1057
|
+
// data) and must be able to address it. MsQuic returns an error
|
|
1058
|
+
// for operations on a shut-down handle, so that is safe. The
|
|
1059
|
+
// dispatcher unregisters, closes and frees, holding the GVL, which
|
|
1060
|
+
// is what any Ruby thread that found ctx also holds.
|
|
1061
|
+
__atomic_store_n(&ctx->shutdown, 1, __ATOMIC_RELEASE);
|
|
1062
|
+
MsQuic->SetCallbackHandler(Stream, (void*)StreamCallback, NULL);
|
|
1063
|
+
if (!queue_event_full(ctx->connection, ctx->connection_ctx, ctx->client_obj,
|
|
1064
|
+
"STREAM_SHUTDOWN_COMPLETE", ctx->stream_id, (const char*)&token, sizeof(token), 0,
|
|
1065
|
+
NULL, Event->SHUTDOWN_COMPLETE.AppCloseInProgress ? RELEASE_STREAM_NOCLOSE : RELEASE_STREAM, ctx)) {
|
|
1066
|
+
// Out of memory. The context is not freed here: contexts are
|
|
1067
|
+
// freed only on a thread holding the GVL, so that a Ruby thread
|
|
1068
|
+
// between find_stream and its MsQuic call cannot have the
|
|
1069
|
+
// pointer freed underneath it, and this thread never holds the
|
|
1070
|
+
// GVL. Unregistering only stops future lookups. One context and
|
|
1071
|
+
// one handle leak on an allocation failure that is already a
|
|
1072
|
+
// terminal condition; the invariant stays whole.
|
|
1073
|
+
fprintf(stderr, "Quicsilver: event queue allocation failed; leaking stream %llu\n",
|
|
1074
|
+
(unsigned long long)ctx->stream_id);
|
|
1075
|
+
unregister_stream(token);
|
|
1076
|
+
}
|
|
1077
|
+
break;
|
|
1078
|
+
case QUIC_STREAM_EVENT_START_COMPLETE: {
|
|
1079
|
+
// Pack [stream_handle(8)][peer_accepted(1)] for Ruby
|
|
1080
|
+
uint8_t accepted = Event->START_COMPLETE.PeerAccepted ? 1 : 0;
|
|
1081
|
+
char payload[sizeof(token) + 1];
|
|
1082
|
+
memcpy(payload, &token, sizeof(token));
|
|
1083
|
+
payload[sizeof(token)] = accepted;
|
|
1084
|
+
queue_event(ctx->connection, ctx->connection_ctx, ctx->client_obj,
|
|
1085
|
+
"STREAM_START_COMPLETE", ctx->stream_id, payload, sizeof(payload), 0);
|
|
1086
|
+
break;
|
|
1087
|
+
}
|
|
1088
|
+
case QUIC_STREAM_EVENT_PEER_ACCEPTED:
|
|
1089
|
+
// Queued stream now accepted — peer raised MAX_STREAMS
|
|
1090
|
+
queue_event(ctx->connection, ctx->connection_ctx, ctx->client_obj,
|
|
1091
|
+
"STREAM_PEER_ACCEPTED", ctx->stream_id, (const char*)&token, sizeof(token), 0);
|
|
1092
|
+
break;
|
|
1093
|
+
case QUIC_STREAM_EVENT_PEER_SEND_SHUTDOWN:
|
|
1094
|
+
// If the FIN arrived while a receive was pending, MsQuic reports it
|
|
1095
|
+
// only here, never as a zero-length RECEIVE. Ruby expects RECEIVE_FIN.
|
|
1096
|
+
if (!ctx->fin_delivered && !ctx->receive_delivery_failed) {
|
|
1097
|
+
ctx->fin_delivered = 1;
|
|
1098
|
+
queue_event(ctx->connection, ctx->connection_ctx, ctx->client_obj,
|
|
1099
|
+
"RECEIVE_FIN", ctx->stream_id, (const char*)&token, sizeof(token), ctx->early_data);
|
|
1100
|
+
}
|
|
1101
|
+
break;
|
|
1102
|
+
case QUIC_STREAM_EVENT_PEER_SEND_ABORTED: {
|
|
1103
|
+
// Peer sent RESET_STREAM — pack
|
|
1104
|
+
// [stream_handle(8)][error_code(8)][final_size(8)]
|
|
1105
|
+
//
|
|
1106
|
+
// RESET_STREAM carries the stream's final size, and WebTransport
|
|
1107
|
+
// session accounting needs it: without it the two endpoints
|
|
1108
|
+
// disagree about how much credit a reset stream consumed
|
|
1109
|
+
// (draft-ietf-webtrans-http3-16 5.4, RFC 9000 4.5).
|
|
1110
|
+
uint64_t error_code = Event->PEER_SEND_ABORTED.ErrorCode;
|
|
1111
|
+
// Read straight off the event: calling back into the MsQuic API
|
|
1112
|
+
// from inside a callback risks re-entering locks the callback
|
|
1113
|
+
// already holds.
|
|
1114
|
+
uint64_t final_size = Event->PEER_SEND_ABORTED.FinalSize;
|
|
1115
|
+
char combined[sizeof(token) + sizeof(uint64_t) + sizeof(uint64_t)];
|
|
1116
|
+
memcpy(combined, &token, sizeof(token));
|
|
1117
|
+
memcpy(combined + sizeof(token), &error_code, sizeof(uint64_t));
|
|
1118
|
+
memcpy(combined + sizeof(token) + sizeof(uint64_t), &final_size, sizeof(uint64_t));
|
|
1119
|
+
queue_event(ctx->connection, ctx->connection_ctx, ctx->client_obj, "STREAM_RESET", ctx->stream_id, combined, sizeof(combined), 0);
|
|
1120
|
+
break;
|
|
1121
|
+
}
|
|
1122
|
+
case QUIC_STREAM_EVENT_PEER_RECEIVE_ABORTED: {
|
|
1123
|
+
// Peer sent STOP_SENDING — pack [stream_handle(8)][error_code(8)]
|
|
1124
|
+
uint64_t error_code = Event->PEER_RECEIVE_ABORTED.ErrorCode;
|
|
1125
|
+
char combined[sizeof(token) + sizeof(uint64_t)];
|
|
1126
|
+
memcpy(combined, &token, sizeof(token));
|
|
1127
|
+
memcpy(combined + sizeof(token), &error_code, sizeof(uint64_t));
|
|
1128
|
+
queue_event(ctx->connection, ctx->connection_ctx, ctx->client_obj, "STOP_SENDING", ctx->stream_id, combined, sizeof(combined), 0);
|
|
1129
|
+
break;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
return QUIC_STATUS_SUCCESS;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// Connection callback
|
|
1137
|
+
static QUIC_STATUS QUIC_API
|
|
1138
|
+
ConnectionCallback(HQUIC Connection, void* Context, QUIC_CONNECTION_EVENT* Event)
|
|
1139
|
+
{
|
|
1140
|
+
ConnectionContext* ctx = (ConnectionContext*)Context;
|
|
1141
|
+
HQUIC Stream;
|
|
1142
|
+
StreamContext* stream_ctx;
|
|
1143
|
+
|
|
1144
|
+
if (ctx == NULL) {
|
|
1145
|
+
return QUIC_STATUS_SUCCESS;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
switch (Event->Type) {
|
|
1149
|
+
case QUIC_CONNECTION_EVENT_RELIABLE_RESET_NEGOTIATED:
|
|
1150
|
+
ctx->reliable_reset_negotiated = Event->RELIABLE_RESET_NEGOTIATED.IsNegotiated;
|
|
1151
|
+
break;
|
|
1152
|
+
case QUIC_CONNECTION_EVENT_CONNECTED:
|
|
1153
|
+
ctx->connected = 1;
|
|
1154
|
+
ctx->failed = 0;
|
|
1155
|
+
ctx->session_resumed = Event->CONNECTED.SessionResumed;
|
|
1156
|
+
// Server: send resumption ticket so client can do 0-RTT on reconnect
|
|
1157
|
+
if (NIL_P(ctx->client_obj)) {
|
|
1158
|
+
MsQuic->ConnectionSendResumptionTicket(Connection, QUIC_SEND_RESUMPTION_FLAG_NONE, 0, NULL);
|
|
1159
|
+
}
|
|
1160
|
+
// Notify Ruby about new connection - pass ctx pointer for building connection_data
|
|
1161
|
+
queue_event(Connection, ctx, ctx->client_obj, "CONNECTION_ESTABLISHED", 0, (const char*)&Connection, sizeof(HQUIC), 0);
|
|
1162
|
+
break;
|
|
1163
|
+
case QUIC_CONNECTION_EVENT_RESUMPTION_TICKET_RECEIVED:
|
|
1164
|
+
// Client-only: store the ticket for future 0-RTT reconnection
|
|
1165
|
+
if (ctx->resumption_ticket) {
|
|
1166
|
+
free(ctx->resumption_ticket);
|
|
1167
|
+
}
|
|
1168
|
+
ctx->resumption_ticket_length = Event->RESUMPTION_TICKET_RECEIVED.ResumptionTicketLength;
|
|
1169
|
+
ctx->resumption_ticket = (uint8_t*)malloc(ctx->resumption_ticket_length);
|
|
1170
|
+
if (ctx->resumption_ticket) {
|
|
1171
|
+
memcpy(ctx->resumption_ticket, Event->RESUMPTION_TICKET_RECEIVED.ResumptionTicket, ctx->resumption_ticket_length);
|
|
1172
|
+
}
|
|
1173
|
+
break;
|
|
1174
|
+
case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT:
|
|
1175
|
+
ctx->connected = 0;
|
|
1176
|
+
ctx->failed = 1;
|
|
1177
|
+
ctx->error_status = Event->SHUTDOWN_INITIATED_BY_TRANSPORT.Status;
|
|
1178
|
+
ctx->error_code = Event->SHUTDOWN_INITIATED_BY_TRANSPORT.ErrorCode;
|
|
1179
|
+
break;
|
|
1180
|
+
case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_PEER:
|
|
1181
|
+
ctx->connected = 0;
|
|
1182
|
+
ctx->failed = 1;
|
|
1183
|
+
ctx->error_status = QUIC_STATUS_SUCCESS; // Peer initiated, not an error
|
|
1184
|
+
ctx->error_code = Event->SHUTDOWN_INITIATED_BY_PEER.ErrorCode;
|
|
1185
|
+
break;
|
|
1186
|
+
case QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE:
|
|
1187
|
+
ctx->connected = 0;
|
|
1188
|
+
// Freed on the dispatcher after Ruby has seen CONNECTION_CLOSED:
|
|
1189
|
+
// rb_gc_unregister_address needs the GVL, and Ruby may still hold
|
|
1190
|
+
// the context pointer as a handle until it processes this event.
|
|
1191
|
+
if (!queue_event_full(Connection, ctx, ctx->client_obj, "CONNECTION_CLOSED", 0,
|
|
1192
|
+
(const char*)&Connection, sizeof(HQUIC), 0, NULL, RELEASE_CONNECTION, ctx)) {
|
|
1193
|
+
// Out of memory, and the release needs the GVL
|
|
1194
|
+
// (rb_gc_unregister_address), which this thread never holds.
|
|
1195
|
+
// Leaking one small context is the safe failure: freeing it
|
|
1196
|
+
// would leave a GC root pointing at freed memory.
|
|
1197
|
+
fprintf(stderr, "Quicsilver: event queue allocation failed; leaking connection context\n");
|
|
1198
|
+
}
|
|
1199
|
+
break;
|
|
1200
|
+
case QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED:
|
|
1201
|
+
// Client opened a stream
|
|
1202
|
+
Stream = Event->PEER_STREAM_STARTED.Stream;
|
|
1203
|
+
stream_ctx = (StreamContext*)malloc(sizeof(StreamContext));
|
|
1204
|
+
if (stream_ctx != NULL) {
|
|
1205
|
+
stream_ctx->connection = Connection;
|
|
1206
|
+
stream_ctx->connection_ctx = ctx; // Store connection context pointer
|
|
1207
|
+
stream_ctx->client_obj = ctx->client_obj; // Copy from connection context
|
|
1208
|
+
stream_ctx->started = 1;
|
|
1209
|
+
stream_ctx->shutdown = 0;
|
|
1210
|
+
stream_ctx->stream_id = UINT64_MAX; // Lazily resolved on first callback
|
|
1211
|
+
stream_ctx->early_data = 0;
|
|
1212
|
+
stream_ctx->error_status = QUIC_STATUS_SUCCESS;
|
|
1213
|
+
|
|
1214
|
+
register_stream(stream_ctx, Stream,
|
|
1215
|
+
Event->PEER_STREAM_STARTED.Flags & QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL
|
|
1216
|
+
? QUIC_STREAM_SHUTDOWN_FLAG_ABORT_RECEIVE
|
|
1217
|
+
: QUIC_STREAM_SHUTDOWN_FLAG_ABORT);
|
|
1218
|
+
|
|
1219
|
+
// Set the stream callback handler to handle data events
|
|
1220
|
+
MsQuic->SetCallbackHandler(Stream, (void*)StreamCallback, stream_ctx);
|
|
1221
|
+
} else {
|
|
1222
|
+
MsQuic->StreamClose(Stream);
|
|
1223
|
+
}
|
|
1224
|
+
break;
|
|
1225
|
+
case QUIC_CONNECTION_EVENT_DATAGRAM_RECEIVED:
|
|
1226
|
+
// Bounded by DATAGRAM_QUEUE_LIMIT inside queue_event_full; past it
|
|
1227
|
+
// the datagram is dropped and counted. Datagrams are best-effort by
|
|
1228
|
+
// definition (RFC 9221), so that is a legitimate response to a
|
|
1229
|
+
// dispatcher that has fallen behind.
|
|
1230
|
+
queue_event(Connection, ctx, ctx->client_obj, "DATAGRAM_RECEIVED", 0,
|
|
1231
|
+
(const char*)Event->DATAGRAM_RECEIVED.Buffer->Buffer,
|
|
1232
|
+
Event->DATAGRAM_RECEIVED.Buffer->Length, 0);
|
|
1233
|
+
break;
|
|
1234
|
+
case QUIC_CONNECTION_EVENT_DATAGRAM_STATE_CHANGED:
|
|
1235
|
+
break;
|
|
1236
|
+
case QUIC_CONNECTION_EVENT_DATAGRAM_SEND_STATE_CHANGED:
|
|
1237
|
+
// Free the send buffer when the datagram reaches a final state
|
|
1238
|
+
if (QUIC_DATAGRAM_SEND_STATE_IS_FINAL(Event->DATAGRAM_SEND_STATE_CHANGED.State)) {
|
|
1239
|
+
if (Event->DATAGRAM_SEND_STATE_CHANGED.ClientContext != NULL) {
|
|
1240
|
+
free(Event->DATAGRAM_SEND_STATE_CHANGED.ClientContext);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
break;
|
|
1244
|
+
default:
|
|
1245
|
+
break;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
return QUIC_STATUS_SUCCESS;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// Listener callback to handle incoming connections
|
|
1252
|
+
static QUIC_STATUS QUIC_API
|
|
1253
|
+
ListenerCallback(HQUIC Listener, void* Context, QUIC_LISTENER_EVENT* Event)
|
|
1254
|
+
{
|
|
1255
|
+
ListenerContext* ctx = (ListenerContext*)Context;
|
|
1256
|
+
ConnectionContext* conn_ctx;
|
|
1257
|
+
|
|
1258
|
+
if (ctx == NULL) {
|
|
1259
|
+
return QUIC_STATUS_SUCCESS;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
switch (Event->Type) {
|
|
1263
|
+
case QUIC_LISTENER_EVENT_NEW_CONNECTION:
|
|
1264
|
+
// Create a connection context for the new connection
|
|
1265
|
+
conn_ctx = (ConnectionContext*)malloc(sizeof(ConnectionContext));
|
|
1266
|
+
if (conn_ctx != NULL) {
|
|
1267
|
+
conn_ctx->connected = 0;
|
|
1268
|
+
conn_ctx->failed = 0;
|
|
1269
|
+
conn_ctx->error_status = QUIC_STATUS_SUCCESS;
|
|
1270
|
+
conn_ctx->error_code = 0;
|
|
1271
|
+
conn_ctx->client_obj = Qnil; // Server connections have no client object
|
|
1272
|
+
conn_ctx->remote_address[0] = '\0';
|
|
1273
|
+
conn_ctx->remote_port = 0;
|
|
1274
|
+
conn_ctx->session_resumed = 0;
|
|
1275
|
+
conn_ctx->reliable_reset_negotiated = 0;
|
|
1276
|
+
conn_ctx->resumption_ticket = NULL;
|
|
1277
|
+
conn_ctx->queued_datagrams = 0;
|
|
1278
|
+
conn_ctx->dropped_datagrams = 0;
|
|
1279
|
+
conn_ctx->resumption_ticket_length = 0;
|
|
1280
|
+
|
|
1281
|
+
// Set the connection callback
|
|
1282
|
+
MsQuic->SetCallbackHandler(Event->NEW_CONNECTION.Connection, (void*)ConnectionCallback, conn_ctx);
|
|
1283
|
+
|
|
1284
|
+
// Accept the new connection with the server configuration
|
|
1285
|
+
QUIC_STATUS Status = MsQuic->ConnectionSetConfiguration(Event->NEW_CONNECTION.Connection, ctx->Configuration);
|
|
1286
|
+
if (QUIC_FAILED(Status)) {
|
|
1287
|
+
free(conn_ctx);
|
|
1288
|
+
return Status;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
} else {
|
|
1292
|
+
// Reject the connection if we can't allocate context
|
|
1293
|
+
return QUIC_STATUS_OUT_OF_MEMORY;
|
|
1294
|
+
}
|
|
1295
|
+
break;
|
|
1296
|
+
|
|
1297
|
+
case QUIC_LISTENER_EVENT_STOP_COMPLETE:
|
|
1298
|
+
ctx->stopped = 1;
|
|
1299
|
+
break;
|
|
1300
|
+
|
|
1301
|
+
default:
|
|
1302
|
+
break;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
return QUIC_STATUS_SUCCESS;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// Initialize MSQUIC
|
|
1309
|
+
static VALUE
|
|
1310
|
+
quicsilver_open(VALUE self)
|
|
1311
|
+
{
|
|
1312
|
+
QUIC_STATUS Status;
|
|
1313
|
+
|
|
1314
|
+
// Check if already initialized
|
|
1315
|
+
if (MsQuic != NULL && Registration != NULL) {
|
|
1316
|
+
return Qtrue;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// Open a handle to the library and get the API function table
|
|
1320
|
+
if (MsQuic == NULL && QUIC_FAILED(Status = MsQuicOpenVersion(2, (const void**)&MsQuic))) {
|
|
1321
|
+
rb_raise(rb_eRuntimeError, "MsQuicOpenVersion failed, 0x%x!", Status);
|
|
1322
|
+
return Qfalse;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// Custom execution MUST be set up BEFORE RegistrationOpen.
|
|
1326
|
+
// RegistrationOpen triggers MsQuic lazy init (LazyInitComplete=TRUE),
|
|
1327
|
+
// after which ExecutionCreate returns QUIC_STATUS_INVALID_STATE.
|
|
1328
|
+
#if __linux__
|
|
1329
|
+
EventQ = epoll_create1(0);
|
|
1330
|
+
#elif __APPLE__ || __FreeBSD__
|
|
1331
|
+
EventQ = kqueue();
|
|
1332
|
+
#endif
|
|
1333
|
+
if (EventQ == -1) {
|
|
1334
|
+
MsQuicClose(MsQuic);
|
|
1335
|
+
MsQuic = NULL;
|
|
1336
|
+
rb_raise(rb_eRuntimeError, "Failed to create event queue for custom execution");
|
|
1337
|
+
return Qfalse;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// PollingIdleTimeoutUs: how long MsQuic keeps spinning on an idle
|
|
1341
|
+
// execution context before it lets us sleep in kevent/epoll_wait. At zero
|
|
1342
|
+
// every idle gap is a full sleep-and-wake, which is the fixed latency floor
|
|
1343
|
+
// at low concurrency. A short spin trades CPU for that latency.
|
|
1344
|
+
// QUICSILVER_POLL_IDLE_US overrides for measurement; the default stays 0
|
|
1345
|
+
// until the trade is measured.
|
|
1346
|
+
uint32_t polling_idle_us = 0;
|
|
1347
|
+
const char* idle_env = getenv("QUICSILVER_POLL_IDLE_US");
|
|
1348
|
+
if (idle_env && *idle_env) polling_idle_us = (uint32_t)strtoul(idle_env, NULL, 10);
|
|
1349
|
+
|
|
1350
|
+
// One execution context. The receive path's correctness argument depends
|
|
1351
|
+
// on it: receive_with_credit sets receive_paused and returns a partial
|
|
1352
|
+
// length, and a grant from a Ruby thread calls StreamReceiveSetEnabled,
|
|
1353
|
+
// which MsQuic applies as a queued operation on the connection's worker.
|
|
1354
|
+
// With one context that worker is this ExecutionPoll, so the resume
|
|
1355
|
+
// always runs after the callback that paused. With more than one context
|
|
1356
|
+
// a connection still has exactly one worker, so the argument holds per
|
|
1357
|
+
// connection, but revisit it before changing this.
|
|
1358
|
+
QUIC_EXECUTION_CONFIG exec_config = { 0, &EventQ };
|
|
1359
|
+
Status = MsQuic->ExecutionCreate(
|
|
1360
|
+
QUIC_GLOBAL_EXECUTION_CONFIG_FLAG_NONE,
|
|
1361
|
+
polling_idle_us,
|
|
1362
|
+
1, // 1 execution context
|
|
1363
|
+
&exec_config,
|
|
1364
|
+
&ExecContext
|
|
1365
|
+
);
|
|
1366
|
+
if (QUIC_FAILED(Status)) {
|
|
1367
|
+
close(EventQ);
|
|
1368
|
+
EventQ = -1;
|
|
1369
|
+
MsQuicClose(MsQuic);
|
|
1370
|
+
MsQuic = NULL;
|
|
1371
|
+
rb_raise(rb_eRuntimeError, "ExecutionCreate failed, 0x%x!", Status);
|
|
1372
|
+
return Qfalse;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// Now open registration — MsQuic lazy init will see the custom execution
|
|
1376
|
+
// context and skip spawning its own worker threads.
|
|
1377
|
+
if (QUIC_FAILED(Status = MsQuic->RegistrationOpen(&RegConfig, &Registration))) {
|
|
1378
|
+
MsQuic->ExecutionDelete(1, &ExecContext);
|
|
1379
|
+
ExecContext = NULL;
|
|
1380
|
+
close(EventQ);
|
|
1381
|
+
EventQ = -1;
|
|
1382
|
+
MsQuicClose(MsQuic);
|
|
1383
|
+
MsQuic = NULL;
|
|
1384
|
+
rb_raise(rb_eRuntimeError, "RegistrationOpen failed, 0x%x!", Status);
|
|
1385
|
+
return Qfalse;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// Register wake source — Ruby threads can unblock the event loop
|
|
1389
|
+
#if __linux__
|
|
1390
|
+
WakeFd = eventfd(0, EFD_NONBLOCK);
|
|
1391
|
+
if (WakeFd != -1) {
|
|
1392
|
+
struct epoll_event ev = { .events = EPOLLIN, .data.ptr = NULL };
|
|
1393
|
+
epoll_ctl(EventQ, EPOLL_CTL_ADD, WakeFd, &ev);
|
|
1394
|
+
}
|
|
1395
|
+
#elif __APPLE__ || __FreeBSD__
|
|
1396
|
+
{
|
|
1397
|
+
struct kevent kev;
|
|
1398
|
+
EV_SET(&kev, WAKE_IDENT, EVFILT_USER, EV_ADD | EV_CLEAR, 0, 0, NULL);
|
|
1399
|
+
kevent(EventQ, &kev, 1, NULL, 0, NULL);
|
|
1400
|
+
}
|
|
1401
|
+
#endif
|
|
1402
|
+
|
|
1403
|
+
return Qtrue;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// Create a QUIC configuration (for client connections)
|
|
1407
|
+
static VALUE
|
|
1408
|
+
quicsilver_create_configuration(int argc, VALUE *argv, VALUE self)
|
|
1409
|
+
{
|
|
1410
|
+
VALUE unsecure, datagram_receive_enabled, reliable_reset_enabled, incoming_bidi, incoming_uni,
|
|
1411
|
+
idle_timeout_ms;
|
|
1412
|
+
rb_scan_args(argc, argv, "15", &unsecure, &datagram_receive_enabled,
|
|
1413
|
+
&reliable_reset_enabled, &incoming_bidi, &incoming_uni, &idle_timeout_ms);
|
|
1414
|
+
if (argc < 2) datagram_receive_enabled = Qtrue;
|
|
1415
|
+
if (argc < 3) reliable_reset_enabled = Qtrue;
|
|
1416
|
+
|
|
1417
|
+
if (MsQuic == NULL) {
|
|
1418
|
+
rb_raise(rb_eRuntimeError, "MSQUIC not initialized. Call Quicsilver.open_connection first.");
|
|
1419
|
+
return Qnil;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
QUIC_STATUS Status;
|
|
1423
|
+
HQUIC Configuration = NULL;
|
|
1424
|
+
|
|
1425
|
+
// Basic settings
|
|
1426
|
+
QUIC_SETTINGS Settings = {0};
|
|
1427
|
+
Settings.ReliableResetEnabled = RTEST(reliable_reset_enabled);
|
|
1428
|
+
Settings.IsSet.ReliableResetEnabled = TRUE;
|
|
1429
|
+
// The peer advertises its own idle timeout and the lower of the two applies.
|
|
1430
|
+
if (!NIL_P(idle_timeout_ms)) {
|
|
1431
|
+
Settings.IdleTimeoutMs = NUM2ULL(idle_timeout_ms);
|
|
1432
|
+
Settings.IsSet.IdleTimeoutMs = TRUE;
|
|
1433
|
+
}
|
|
1434
|
+
Settings.DatagramReceiveEnabled = RTEST(datagram_receive_enabled);
|
|
1435
|
+
Settings.IsSet.DatagramReceiveEnabled = TRUE;
|
|
1436
|
+
|
|
1437
|
+
// Optional credit for server-initiated streams. Leave native defaults alone
|
|
1438
|
+
// when omitted; the uni limit also covers HTTP/3 control and QPACK streams.
|
|
1439
|
+
if (!NIL_P(incoming_bidi)) {
|
|
1440
|
+
unsigned int count = NUM2UINT(incoming_bidi);
|
|
1441
|
+
if (count > UINT16_MAX) rb_raise(rb_eArgError, "max_incoming_bidi_streams must be between 0 and 65535");
|
|
1442
|
+
Settings.PeerBidiStreamCount = (uint16_t)count;
|
|
1443
|
+
Settings.IsSet.PeerBidiStreamCount = TRUE;
|
|
1444
|
+
}
|
|
1445
|
+
if (!NIL_P(incoming_uni)) {
|
|
1446
|
+
unsigned int count = NUM2UINT(incoming_uni);
|
|
1447
|
+
if (count > UINT16_MAX) rb_raise(rb_eArgError, "max_incoming_uni_streams must be between 0 and 65535");
|
|
1448
|
+
Settings.PeerUnidiStreamCount = (uint16_t)count;
|
|
1449
|
+
Settings.IsSet.PeerUnidiStreamCount = TRUE;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// Simple ALPN for now - Ruby can customize this later
|
|
1453
|
+
QUIC_BUFFER Alpn = { sizeof("h3") - 1, (uint8_t*)"h3" };
|
|
1454
|
+
|
|
1455
|
+
// Create configuration
|
|
1456
|
+
if (QUIC_FAILED(Status = MsQuic->ConfigurationOpen(Registration, &Alpn, 1, &Settings, sizeof(Settings), NULL, &Configuration))) {
|
|
1457
|
+
rb_raise(rb_eRuntimeError, "ConfigurationOpen failed, 0x%x!", Status);
|
|
1458
|
+
return Qnil;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// Set up credentials
|
|
1462
|
+
QUIC_CREDENTIAL_CONFIG CredConfig = {0};
|
|
1463
|
+
CredConfig.Type = QUIC_CREDENTIAL_TYPE_NONE;
|
|
1464
|
+
CredConfig.Flags = QUIC_CREDENTIAL_FLAG_CLIENT;
|
|
1465
|
+
|
|
1466
|
+
if (RTEST(unsecure)) {
|
|
1467
|
+
CredConfig.Flags |= QUIC_CREDENTIAL_FLAG_NO_CERTIFICATE_VALIDATION;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
if (QUIC_FAILED(Status = MsQuic->ConfigurationLoadCredential(Configuration, &CredConfig))) {
|
|
1471
|
+
MsQuic->ConfigurationClose(Configuration);
|
|
1472
|
+
rb_raise(rb_eRuntimeError, "ConfigurationLoadCredential failed, 0x%x!", Status);
|
|
1473
|
+
return Qnil;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
// Return the configuration handle as a Ruby integer (pointer)
|
|
1477
|
+
return ULL2NUM((uintptr_t)Configuration);
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
// Create a QUIC server configuration
|
|
1481
|
+
static VALUE
|
|
1482
|
+
quicsilver_create_server_configuration(VALUE self, VALUE config_hash)
|
|
1483
|
+
{
|
|
1484
|
+
if (MsQuic == NULL) {
|
|
1485
|
+
rb_raise(rb_eRuntimeError, "MSQUIC not initialized. Call Quicsilver.open_connection first.");
|
|
1486
|
+
return Qnil;
|
|
1487
|
+
}
|
|
1488
|
+
VALUE cert_file_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("cert_file")));
|
|
1489
|
+
VALUE key_file_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("key_file")));
|
|
1490
|
+
VALUE idle_timeout_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("idle_timeout_ms")));
|
|
1491
|
+
VALUE server_resumption_level_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("server_resumption_level")));
|
|
1492
|
+
VALUE max_concurrent_requests_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("max_concurrent_requests")));
|
|
1493
|
+
VALUE max_unidirectional_streams_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("max_unidirectional_streams")));
|
|
1494
|
+
VALUE alpn_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("alpn")));
|
|
1495
|
+
VALUE stream_receive_window_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("stream_receive_window")));
|
|
1496
|
+
VALUE stream_receive_buffer_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("stream_receive_buffer")));
|
|
1497
|
+
VALUE connection_flow_control_window_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("connection_flow_control_window")));
|
|
1498
|
+
VALUE pacing_enabled_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("pacing_enabled")));
|
|
1499
|
+
VALUE send_buffering_enabled_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("send_buffering_enabled")));
|
|
1500
|
+
VALUE initial_rtt_ms_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("initial_rtt_ms")));
|
|
1501
|
+
VALUE initial_window_packets_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("initial_window_packets")));
|
|
1502
|
+
VALUE max_ack_delay_ms_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("max_ack_delay_ms")));
|
|
1503
|
+
VALUE keep_alive_interval_ms_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("keep_alive_interval_ms")));
|
|
1504
|
+
VALUE congestion_control_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("congestion_control_algorithm")));
|
|
1505
|
+
VALUE migration_enabled_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("migration_enabled")));
|
|
1506
|
+
VALUE disconnect_timeout_ms_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("disconnect_timeout_ms")));
|
|
1507
|
+
VALUE handshake_idle_timeout_ms_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("handshake_idle_timeout_ms")));
|
|
1508
|
+
|
|
1509
|
+
QUIC_STATUS Status;
|
|
1510
|
+
HQUIC Configuration = NULL;
|
|
1511
|
+
|
|
1512
|
+
const char* cert_path = StringValueCStr(cert_file_val);
|
|
1513
|
+
const char* key_path = StringValueCStr(key_file_val);
|
|
1514
|
+
uint32_t idle_timeout_ms = NUM2INT(idle_timeout_val);
|
|
1515
|
+
uint32_t server_resumption_level = NUM2INT(server_resumption_level_val);
|
|
1516
|
+
uint32_t max_concurrent_requests = NUM2INT(max_concurrent_requests_val);
|
|
1517
|
+
uint32_t max_unidirectional_streams = NUM2INT(max_unidirectional_streams_val);
|
|
1518
|
+
const char* alpn_str = StringValueCStr(alpn_val);
|
|
1519
|
+
uint32_t stream_receive_window = NUM2UINT(stream_receive_window_val);
|
|
1520
|
+
uint32_t stream_receive_buffer = NUM2UINT(stream_receive_buffer_val);
|
|
1521
|
+
uint32_t connection_flow_control_window = NUM2UINT(connection_flow_control_window_val);
|
|
1522
|
+
uint8_t pacing_enabled = (uint8_t)NUM2INT(pacing_enabled_val);
|
|
1523
|
+
uint8_t send_buffering_enabled = (uint8_t)NUM2INT(send_buffering_enabled_val);
|
|
1524
|
+
uint32_t initial_rtt_ms = NUM2UINT(initial_rtt_ms_val);
|
|
1525
|
+
uint32_t initial_window_packets = NUM2UINT(initial_window_packets_val);
|
|
1526
|
+
uint32_t max_ack_delay_ms = NUM2UINT(max_ack_delay_ms_val);
|
|
1527
|
+
uint32_t keep_alive_interval_ms = NUM2UINT(keep_alive_interval_ms_val);
|
|
1528
|
+
uint16_t congestion_control = (uint16_t)NUM2INT(congestion_control_val);
|
|
1529
|
+
uint8_t migration_enabled = (uint8_t)NUM2INT(migration_enabled_val);
|
|
1530
|
+
uint32_t disconnect_timeout_ms = NUM2UINT(disconnect_timeout_ms_val);
|
|
1531
|
+
uint64_t handshake_idle_timeout_ms = NUM2ULL(handshake_idle_timeout_ms_val);
|
|
1532
|
+
|
|
1533
|
+
QUIC_SETTINGS Settings = {0};
|
|
1534
|
+
Settings.ReliableResetEnabled = TRUE;
|
|
1535
|
+
Settings.IsSet.ReliableResetEnabled = TRUE;
|
|
1536
|
+
Settings.IdleTimeoutMs = idle_timeout_ms;
|
|
1537
|
+
Settings.IsSet.IdleTimeoutMs = TRUE;
|
|
1538
|
+
Settings.ServerResumptionLevel = server_resumption_level;
|
|
1539
|
+
Settings.IsSet.ServerResumptionLevel = TRUE;
|
|
1540
|
+
Settings.PeerBidiStreamCount = max_concurrent_requests;
|
|
1541
|
+
Settings.IsSet.PeerBidiStreamCount = TRUE;
|
|
1542
|
+
Settings.PeerUnidiStreamCount = max_unidirectional_streams;
|
|
1543
|
+
Settings.IsSet.PeerUnidiStreamCount = TRUE;
|
|
1544
|
+
|
|
1545
|
+
// Flow control / backpressure settings
|
|
1546
|
+
Settings.StreamRecvWindowDefault = stream_receive_window;
|
|
1547
|
+
Settings.IsSet.StreamRecvWindowDefault = TRUE;
|
|
1548
|
+
Settings.StreamRecvBufferDefault = stream_receive_buffer;
|
|
1549
|
+
Settings.IsSet.StreamRecvBufferDefault = TRUE;
|
|
1550
|
+
Settings.ConnFlowControlWindow = connection_flow_control_window;
|
|
1551
|
+
Settings.IsSet.ConnFlowControlWindow = TRUE;
|
|
1552
|
+
Settings.DatagramReceiveEnabled = TRUE;
|
|
1553
|
+
Settings.IsSet.DatagramReceiveEnabled = TRUE;
|
|
1554
|
+
|
|
1555
|
+
// Throughput settings
|
|
1556
|
+
Settings.PacingEnabled = pacing_enabled;
|
|
1557
|
+
Settings.IsSet.PacingEnabled = TRUE;
|
|
1558
|
+
Settings.SendBufferingEnabled = send_buffering_enabled;
|
|
1559
|
+
Settings.IsSet.SendBufferingEnabled = TRUE;
|
|
1560
|
+
Settings.InitialRttMs = initial_rtt_ms;
|
|
1561
|
+
Settings.IsSet.InitialRttMs = TRUE;
|
|
1562
|
+
Settings.InitialWindowPackets = initial_window_packets;
|
|
1563
|
+
Settings.IsSet.InitialWindowPackets = TRUE;
|
|
1564
|
+
Settings.MaxAckDelayMs = max_ack_delay_ms;
|
|
1565
|
+
Settings.IsSet.MaxAckDelayMs = TRUE;
|
|
1566
|
+
|
|
1567
|
+
// Connection management
|
|
1568
|
+
Settings.KeepAliveIntervalMs = keep_alive_interval_ms;
|
|
1569
|
+
Settings.IsSet.KeepAliveIntervalMs = TRUE;
|
|
1570
|
+
Settings.CongestionControlAlgorithm = congestion_control;
|
|
1571
|
+
Settings.IsSet.CongestionControlAlgorithm = TRUE;
|
|
1572
|
+
Settings.MigrationEnabled = migration_enabled;
|
|
1573
|
+
Settings.IsSet.MigrationEnabled = TRUE;
|
|
1574
|
+
Settings.DisconnectTimeoutMs = disconnect_timeout_ms;
|
|
1575
|
+
Settings.IsSet.DisconnectTimeoutMs = TRUE;
|
|
1576
|
+
Settings.HandshakeIdleTimeoutMs = handshake_idle_timeout_ms;
|
|
1577
|
+
Settings.IsSet.HandshakeIdleTimeoutMs = TRUE;
|
|
1578
|
+
|
|
1579
|
+
QUIC_BUFFER Alpn = { (uint32_t)strlen(alpn_str), (uint8_t*)alpn_str };
|
|
1580
|
+
|
|
1581
|
+
// Create configuration
|
|
1582
|
+
if (QUIC_FAILED(Status = MsQuic->ConfigurationOpen(Registration, &Alpn, 1, &Settings, sizeof(Settings), NULL, &Configuration))) {
|
|
1583
|
+
rb_raise(rb_eRuntimeError, "Server ConfigurationOpen failed, 0x%x!", Status);
|
|
1584
|
+
return Qnil;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// Set up server credentials with certificate files
|
|
1588
|
+
QUIC_CREDENTIAL_CONFIG CredConfig = {0};
|
|
1589
|
+
QUIC_CERTIFICATE_FILE CertFile = {0};
|
|
1590
|
+
|
|
1591
|
+
CertFile.CertificateFile = cert_path;
|
|
1592
|
+
CertFile.PrivateKeyFile = key_path;
|
|
1593
|
+
|
|
1594
|
+
CredConfig.Type = QUIC_CREDENTIAL_TYPE_CERTIFICATE_FILE;
|
|
1595
|
+
CredConfig.CertificateFile = &CertFile;
|
|
1596
|
+
CredConfig.Flags = QUIC_CREDENTIAL_FLAG_NONE;
|
|
1597
|
+
|
|
1598
|
+
if (QUIC_FAILED(Status = MsQuic->ConfigurationLoadCredential(Configuration, &CredConfig))) {
|
|
1599
|
+
MsQuic->ConfigurationClose(Configuration);
|
|
1600
|
+
rb_raise(rb_eRuntimeError, "Server ConfigurationLoadCredential failed, 0x%x!", Status);
|
|
1601
|
+
return Qnil;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// Return the configuration handle as a Ruby integer (pointer)
|
|
1605
|
+
return ULL2NUM((uintptr_t)Configuration);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// Create a QUIC connection with context
|
|
1609
|
+
static VALUE
|
|
1610
|
+
quicsilver_create_connection(VALUE self, VALUE client_obj)
|
|
1611
|
+
{
|
|
1612
|
+
if (MsQuic == NULL) {
|
|
1613
|
+
rb_raise(rb_eRuntimeError, "MSQUIC not initialized. Call Quicsilver.open_connection first.");
|
|
1614
|
+
return Qnil;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
QUIC_STATUS Status;
|
|
1618
|
+
HQUIC Connection = NULL;
|
|
1619
|
+
|
|
1620
|
+
// Allocate and initialize connection context
|
|
1621
|
+
ConnectionContext* ctx = (ConnectionContext*)malloc(sizeof(ConnectionContext));
|
|
1622
|
+
if (ctx == NULL) {
|
|
1623
|
+
rb_raise(rb_eRuntimeError, "Failed to allocate connection context");
|
|
1624
|
+
return Qnil;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
ctx->connected = 0;
|
|
1628
|
+
ctx->failed = 0;
|
|
1629
|
+
ctx->error_status = QUIC_STATUS_SUCCESS;
|
|
1630
|
+
ctx->error_code = 0;
|
|
1631
|
+
ctx->client_obj = client_obj; // Store Ruby client object (Qnil for server)
|
|
1632
|
+
ctx->remote_address[0] = '\0';
|
|
1633
|
+
ctx->remote_port = 0;
|
|
1634
|
+
ctx->session_resumed = 0;
|
|
1635
|
+
ctx->reliable_reset_negotiated = 0;
|
|
1636
|
+
ctx->resumption_ticket = NULL;
|
|
1637
|
+
ctx->queued_datagrams = 0;
|
|
1638
|
+
ctx->dropped_datagrams = 0;
|
|
1639
|
+
ctx->resumption_ticket_length = 0;
|
|
1640
|
+
|
|
1641
|
+
// Protect from GC if it's a Ruby object
|
|
1642
|
+
if (!NIL_P(client_obj)) {
|
|
1643
|
+
rb_gc_register_address(&ctx->client_obj);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// Create connection with enhanced callback and context
|
|
1647
|
+
if (QUIC_FAILED(Status = MsQuic->ConnectionOpen(Registration, ConnectionCallback, ctx, &Connection))) {
|
|
1648
|
+
if (!NIL_P(client_obj)) {
|
|
1649
|
+
rb_gc_unregister_address(&ctx->client_obj);
|
|
1650
|
+
}
|
|
1651
|
+
free(ctx);
|
|
1652
|
+
rb_raise(rb_eRuntimeError, "ConnectionOpen failed, 0x%x!", Status);
|
|
1653
|
+
return Qnil;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// Return both the connection handle and context as an array
|
|
1657
|
+
VALUE result = rb_ary_new2(2);
|
|
1658
|
+
rb_ary_push(result, ULL2NUM((uintptr_t)Connection));
|
|
1659
|
+
rb_ary_push(result, ULL2NUM((uintptr_t)ctx));
|
|
1660
|
+
return result;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// Start a QUIC connection
|
|
1664
|
+
static VALUE
|
|
1665
|
+
quicsilver_start_connection(VALUE self, VALUE connection_handle, VALUE config_handle, VALUE hostname, VALUE port)
|
|
1666
|
+
{
|
|
1667
|
+
if (MsQuic == NULL) {
|
|
1668
|
+
rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
|
|
1669
|
+
return Qfalse;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle);
|
|
1673
|
+
HQUIC Configuration = (HQUIC)(uintptr_t)NUM2ULL(config_handle);
|
|
1674
|
+
const char* Target = StringValueCStr(hostname);
|
|
1675
|
+
uint16_t Port = (uint16_t)NUM2INT(port);
|
|
1676
|
+
|
|
1677
|
+
QUIC_STATUS Status;
|
|
1678
|
+
if (QUIC_FAILED(Status = MsQuic->ConnectionStart(Connection, Configuration, QUIC_ADDRESS_FAMILY_UNSPEC, Target, Port))) {
|
|
1679
|
+
rb_raise(rb_eRuntimeError, "ConnectionStart failed, 0x%x!", Status);
|
|
1680
|
+
return Qfalse;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
wake_event_loop();
|
|
1684
|
+
return Qtrue;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
// Sleep without GVL so other Ruby threads can run.
|
|
1688
|
+
struct sleep_args { int ms; };
|
|
1689
|
+
static void* sleep_nogvl(void* arg) {
|
|
1690
|
+
struct sleep_args* a = (struct sleep_args*)arg;
|
|
1691
|
+
struct timespec ts = { .tv_sec = a->ms / 1000, .tv_nsec = (a->ms % 1000) * 1000000 };
|
|
1692
|
+
nanosleep(&ts, NULL);
|
|
1693
|
+
return NULL;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
// Wait for connection to complete (connected or failed).
|
|
1697
|
+
// Waits only; the poll thread is the sole driver of MsQuic.
|
|
1698
|
+
static VALUE
|
|
1699
|
+
quicsilver_wait_for_connection(VALUE self, VALUE context_handle, VALUE timeout_ms)
|
|
1700
|
+
{
|
|
1701
|
+
ConnectionContext* ctx = (ConnectionContext*)(uintptr_t)NUM2ULL(context_handle);
|
|
1702
|
+
int timeout = NUM2INT(timeout_ms);
|
|
1703
|
+
int elapsed = 0;
|
|
1704
|
+
const int sleep_interval = 1; // 1ms
|
|
1705
|
+
|
|
1706
|
+
while (elapsed < timeout && !ctx->connected && !ctx->failed) {
|
|
1707
|
+
struct sleep_args sa = { .ms = sleep_interval };
|
|
1708
|
+
rb_thread_call_without_gvl(sleep_nogvl, &sa, RUBY_UBF_IO, NULL);
|
|
1709
|
+
elapsed += sleep_interval;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
if (ctx->connected) {
|
|
1713
|
+
return rb_hash_new();
|
|
1714
|
+
} else if (ctx->failed) {
|
|
1715
|
+
VALUE error_info = rb_hash_new();
|
|
1716
|
+
rb_hash_aset(error_info, rb_str_new_cstr("error"), Qtrue);
|
|
1717
|
+
rb_hash_aset(error_info, rb_str_new_cstr("status"), ULL2NUM(ctx->error_status));
|
|
1718
|
+
rb_hash_aset(error_info, rb_str_new_cstr("code"), ULL2NUM(ctx->error_code));
|
|
1719
|
+
|
|
1720
|
+
return error_info;
|
|
1721
|
+
} else {
|
|
1722
|
+
VALUE timeout_info = rb_hash_new();
|
|
1723
|
+
rb_hash_aset(timeout_info, rb_str_new_cstr("timeout"), Qtrue);
|
|
1724
|
+
return timeout_info;
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
// Check connection status
|
|
1729
|
+
static VALUE
|
|
1730
|
+
quicsilver_connection_status(VALUE self, VALUE context_handle)
|
|
1731
|
+
{
|
|
1732
|
+
ConnectionContext* ctx = (ConnectionContext*)(uintptr_t)NUM2ULL(context_handle);
|
|
1733
|
+
|
|
1734
|
+
VALUE status = rb_hash_new();
|
|
1735
|
+
rb_hash_aset(status, rb_str_new_cstr("connected"), ctx->connected ? Qtrue : Qfalse);
|
|
1736
|
+
rb_hash_aset(status, rb_str_new_cstr("failed"), ctx->failed ? Qtrue : Qfalse);
|
|
1737
|
+
|
|
1738
|
+
if (ctx->failed) {
|
|
1739
|
+
rb_hash_aset(status, rb_str_new_cstr("error_status"), ULL2NUM(ctx->error_status));
|
|
1740
|
+
rb_hash_aset(status, rb_str_new_cstr("error_code"), ULL2NUM(ctx->error_code));
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
return status;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
// Get QUIC connection statistics (QUIC_STATISTICS_V2)
|
|
1747
|
+
static VALUE
|
|
1748
|
+
quicsilver_connection_statistics(VALUE self, VALUE connection_handle_val)
|
|
1749
|
+
{
|
|
1750
|
+
if (MsQuic == NULL) {
|
|
1751
|
+
rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
|
|
1752
|
+
return Qnil;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle_val);
|
|
1756
|
+
if (Connection == NULL) return Qnil;
|
|
1757
|
+
|
|
1758
|
+
QUIC_STATISTICS_V2 stats;
|
|
1759
|
+
uint32_t stats_size = sizeof(stats);
|
|
1760
|
+
memset(&stats, 0, stats_size);
|
|
1761
|
+
|
|
1762
|
+
QUIC_STATUS status = MsQuic->GetParam(
|
|
1763
|
+
Connection,
|
|
1764
|
+
QUIC_PARAM_CONN_STATISTICS_V2,
|
|
1765
|
+
&stats_size,
|
|
1766
|
+
&stats);
|
|
1767
|
+
|
|
1768
|
+
if (QUIC_FAILED(status)) {
|
|
1769
|
+
return Qnil;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
VALUE result = rb_hash_new();
|
|
1773
|
+
|
|
1774
|
+
// RTT (microseconds)
|
|
1775
|
+
rb_hash_aset(result, rb_str_new_cstr("rtt"), UINT2NUM(stats.Rtt));
|
|
1776
|
+
rb_hash_aset(result, rb_str_new_cstr("min_rtt"), UINT2NUM(stats.MinRtt));
|
|
1777
|
+
rb_hash_aset(result, rb_str_new_cstr("max_rtt"), UINT2NUM(stats.MaxRtt));
|
|
1778
|
+
|
|
1779
|
+
// Handshake
|
|
1780
|
+
rb_hash_aset(result, rb_str_new_cstr("resumption_attempted"), stats.ResumptionAttempted ? Qtrue : Qfalse);
|
|
1781
|
+
rb_hash_aset(result, rb_str_new_cstr("resumption_succeeded"), stats.ResumptionSucceeded ? Qtrue : Qfalse);
|
|
1782
|
+
|
|
1783
|
+
// Send
|
|
1784
|
+
rb_hash_aset(result, rb_str_new_cstr("send_path_mtu"), UINT2NUM(stats.SendPathMtu));
|
|
1785
|
+
rb_hash_aset(result, rb_str_new_cstr("send_total_packets"), ULL2NUM(stats.SendTotalPackets));
|
|
1786
|
+
rb_hash_aset(result, rb_str_new_cstr("send_retransmittable_packets"), ULL2NUM(stats.SendRetransmittablePackets));
|
|
1787
|
+
rb_hash_aset(result, rb_str_new_cstr("send_suspected_lost_packets"), ULL2NUM(stats.SendSuspectedLostPackets));
|
|
1788
|
+
rb_hash_aset(result, rb_str_new_cstr("send_spurious_lost_packets"), ULL2NUM(stats.SendSpuriousLostPackets));
|
|
1789
|
+
rb_hash_aset(result, rb_str_new_cstr("send_total_bytes"), ULL2NUM(stats.SendTotalBytes));
|
|
1790
|
+
rb_hash_aset(result, rb_str_new_cstr("send_total_stream_bytes"), ULL2NUM(stats.SendTotalStreamBytes));
|
|
1791
|
+
rb_hash_aset(result, rb_str_new_cstr("send_congestion_count"), UINT2NUM(stats.SendCongestionCount));
|
|
1792
|
+
rb_hash_aset(result, rb_str_new_cstr("send_persistent_congestion_count"), UINT2NUM(stats.SendPersistentCongestionCount));
|
|
1793
|
+
rb_hash_aset(result, rb_str_new_cstr("send_congestion_window"), UINT2NUM(stats.SendCongestionWindow));
|
|
1794
|
+
|
|
1795
|
+
// Recv
|
|
1796
|
+
rb_hash_aset(result, rb_str_new_cstr("recv_total_packets"), ULL2NUM(stats.RecvTotalPackets));
|
|
1797
|
+
rb_hash_aset(result, rb_str_new_cstr("recv_reordered_packets"), ULL2NUM(stats.RecvReorderedPackets));
|
|
1798
|
+
rb_hash_aset(result, rb_str_new_cstr("recv_dropped_packets"), ULL2NUM(stats.RecvDroppedPackets));
|
|
1799
|
+
rb_hash_aset(result, rb_str_new_cstr("recv_duplicate_packets"), ULL2NUM(stats.RecvDuplicatePackets));
|
|
1800
|
+
rb_hash_aset(result, rb_str_new_cstr("recv_total_bytes"), ULL2NUM(stats.RecvTotalBytes));
|
|
1801
|
+
rb_hash_aset(result, rb_str_new_cstr("recv_total_stream_bytes"), ULL2NUM(stats.RecvTotalStreamBytes));
|
|
1802
|
+
rb_hash_aset(result, rb_str_new_cstr("recv_decryption_failures"), ULL2NUM(stats.RecvDecryptionFailures));
|
|
1803
|
+
rb_hash_aset(result, rb_str_new_cstr("recv_valid_ack_frames"), ULL2NUM(stats.RecvValidAckFrames));
|
|
1804
|
+
|
|
1805
|
+
// Misc
|
|
1806
|
+
rb_hash_aset(result, rb_str_new_cstr("key_update_count"), UINT2NUM(stats.KeyUpdateCount));
|
|
1807
|
+
|
|
1808
|
+
return result;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// Get global QUIC transport counters.
|
|
1812
|
+
//
|
|
1813
|
+
// These counters are process-wide transport state, not scoped to a single
|
|
1814
|
+
// Quicsilver::Server or QUIC connection. They complement QUIC_STATISTICS_V2,
|
|
1815
|
+
// which is exposed by quicsilver_connection_statistics for a specific
|
|
1816
|
+
// connection handle.
|
|
1817
|
+
//
|
|
1818
|
+
// MsQuic stores the values as a uint64_t array indexed by QUIC_PERF_COUNTER_*.
|
|
1819
|
+
// The public Ruby API uses transport-domain names instead of MsQuic enum names
|
|
1820
|
+
// so Server#stats does not leak the native implementation detail.
|
|
1821
|
+
static VALUE
|
|
1822
|
+
quicsilver_transport_counters(VALUE self)
|
|
1823
|
+
{
|
|
1824
|
+
if (MsQuic == NULL) {
|
|
1825
|
+
rb_raise(rb_eRuntimeError, "QUIC transport not initialized.");
|
|
1826
|
+
return Qnil;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
#ifdef QUIC_PARAM_GLOBAL_PERF_COUNTERS
|
|
1830
|
+
uint64_t counters[QUIC_PERF_COUNTER_MAX] = {0};
|
|
1831
|
+
uint32_t counters_size = sizeof(counters);
|
|
1832
|
+
|
|
1833
|
+
QUIC_STATUS status = MsQuic->GetParam(
|
|
1834
|
+
NULL,
|
|
1835
|
+
QUIC_PARAM_GLOBAL_PERF_COUNTERS,
|
|
1836
|
+
&counters_size,
|
|
1837
|
+
counters);
|
|
1838
|
+
|
|
1839
|
+
if (QUIC_FAILED(status)) {
|
|
1840
|
+
return Qnil;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
VALUE result = rb_hash_new();
|
|
1844
|
+
|
|
1845
|
+
#define ADD_COUNTER(key, counter) \
|
|
1846
|
+
rb_hash_aset(result, rb_str_new_cstr(key), ULL2NUM(counters[counter]))
|
|
1847
|
+
|
|
1848
|
+
// Connection lifecycle counters.
|
|
1849
|
+
ADD_COUNTER("connections_created", QUIC_PERF_COUNTER_CONN_CREATED);
|
|
1850
|
+
ADD_COUNTER("connections_handshake_failed", QUIC_PERF_COUNTER_CONN_HANDSHAKE_FAIL);
|
|
1851
|
+
ADD_COUNTER("connections_app_rejected", QUIC_PERF_COUNTER_CONN_APP_REJECT);
|
|
1852
|
+
ADD_COUNTER("connections_resumed", QUIC_PERF_COUNTER_CONN_RESUMED);
|
|
1853
|
+
ADD_COUNTER("connections_active", QUIC_PERF_COUNTER_CONN_ACTIVE);
|
|
1854
|
+
ADD_COUNTER("connections_connected", QUIC_PERF_COUNTER_CONN_CONNECTED);
|
|
1855
|
+
ADD_COUNTER("connections_protocol_errors", QUIC_PERF_COUNTER_CONN_PROTOCOL_ERRORS);
|
|
1856
|
+
ADD_COUNTER("connections_no_alpn", QUIC_PERF_COUNTER_CONN_NO_ALPN);
|
|
1857
|
+
ADD_COUNTER("connections_load_rejected", QUIC_PERF_COUNTER_CONN_LOAD_REJECT);
|
|
1858
|
+
|
|
1859
|
+
// Stream, packet, UDP, and application byte counters.
|
|
1860
|
+
ADD_COUNTER("streams_active", QUIC_PERF_COUNTER_STRM_ACTIVE);
|
|
1861
|
+
ADD_COUNTER("packets_suspected_lost", QUIC_PERF_COUNTER_PKTS_SUSPECTED_LOST);
|
|
1862
|
+
ADD_COUNTER("packets_dropped", QUIC_PERF_COUNTER_PKTS_DROPPED);
|
|
1863
|
+
ADD_COUNTER("packets_decryption_failed", QUIC_PERF_COUNTER_PKTS_DECRYPTION_FAIL);
|
|
1864
|
+
ADD_COUNTER("udp_datagrams_received", QUIC_PERF_COUNTER_UDP_RECV);
|
|
1865
|
+
ADD_COUNTER("udp_datagrams_sent", QUIC_PERF_COUNTER_UDP_SEND);
|
|
1866
|
+
ADD_COUNTER("udp_bytes_received", QUIC_PERF_COUNTER_UDP_RECV_BYTES);
|
|
1867
|
+
ADD_COUNTER("udp_bytes_sent", QUIC_PERF_COUNTER_UDP_SEND_BYTES);
|
|
1868
|
+
ADD_COUNTER("udp_receive_events", QUIC_PERF_COUNTER_UDP_RECV_EVENTS);
|
|
1869
|
+
ADD_COUNTER("udp_send_calls", QUIC_PERF_COUNTER_UDP_SEND_CALLS);
|
|
1870
|
+
ADD_COUNTER("app_bytes_sent", QUIC_PERF_COUNTER_APP_SEND_BYTES);
|
|
1871
|
+
ADD_COUNTER("app_bytes_received", QUIC_PERF_COUNTER_APP_RECV_BYTES);
|
|
1872
|
+
|
|
1873
|
+
// Queue/worker pressure counters. These are useful for distinguishing
|
|
1874
|
+
// transport saturation from Ruby scheduler saturation in Server#stats.
|
|
1875
|
+
ADD_COUNTER("connection_queue_depth", QUIC_PERF_COUNTER_CONN_QUEUE_DEPTH);
|
|
1876
|
+
ADD_COUNTER("connection_operations_queue_depth", QUIC_PERF_COUNTER_CONN_OPER_QUEUE_DEPTH);
|
|
1877
|
+
ADD_COUNTER("connection_operations_queued", QUIC_PERF_COUNTER_CONN_OPER_QUEUED);
|
|
1878
|
+
ADD_COUNTER("connection_operations_completed", QUIC_PERF_COUNTER_CONN_OPER_COMPLETED);
|
|
1879
|
+
ADD_COUNTER("worker_operations_queue_depth", QUIC_PERF_COUNTER_WORK_OPER_QUEUE_DEPTH);
|
|
1880
|
+
ADD_COUNTER("worker_operations_queued", QUIC_PERF_COUNTER_WORK_OPER_QUEUED);
|
|
1881
|
+
ADD_COUNTER("worker_operations_completed", QUIC_PERF_COUNTER_WORK_OPER_COMPLETED);
|
|
1882
|
+
|
|
1883
|
+
// Path validation and stateless response counters.
|
|
1884
|
+
ADD_COUNTER("paths_validated", QUIC_PERF_COUNTER_PATH_VALIDATED);
|
|
1885
|
+
ADD_COUNTER("paths_failed", QUIC_PERF_COUNTER_PATH_FAILURE);
|
|
1886
|
+
ADD_COUNTER("stateless_resets_sent", QUIC_PERF_COUNTER_SEND_STATELESS_RESET);
|
|
1887
|
+
ADD_COUNTER("stateless_retries_sent", QUIC_PERF_COUNTER_SEND_STATELESS_RETRY);
|
|
1888
|
+
|
|
1889
|
+
#undef ADD_COUNTER
|
|
1890
|
+
|
|
1891
|
+
return result;
|
|
1892
|
+
#else
|
|
1893
|
+
return Qnil;
|
|
1894
|
+
#endif
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
// Get the remote address of a QUIC connection. Called lazily from Ruby
|
|
1898
|
+
// so we don't block MsQuic's event thread during the CONNECTED callback.
|
|
1899
|
+
// Get the resumption ticket from a connection context (for 0-RTT)
|
|
1900
|
+
static VALUE
|
|
1901
|
+
quicsilver_get_resumption_ticket(VALUE self, VALUE context_handle_val)
|
|
1902
|
+
{
|
|
1903
|
+
ConnectionContext* ctx = (ConnectionContext*)(uintptr_t)NUM2ULL(context_handle_val);
|
|
1904
|
+
if (ctx == NULL || ctx->resumption_ticket == NULL || ctx->resumption_ticket_length == 0) {
|
|
1905
|
+
return Qnil;
|
|
1906
|
+
}
|
|
1907
|
+
return rb_str_new((const char*)ctx->resumption_ticket, ctx->resumption_ticket_length);
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
// Set a resumption ticket on a connection (for 0-RTT reconnection)
|
|
1911
|
+
static VALUE
|
|
1912
|
+
quicsilver_set_resumption_ticket(VALUE self, VALUE connection_handle_val, VALUE ticket_val)
|
|
1913
|
+
{
|
|
1914
|
+
if (MsQuic == NULL) return Qfalse;
|
|
1915
|
+
HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle_val);
|
|
1916
|
+
if (Connection == NULL || NIL_P(ticket_val)) return Qfalse;
|
|
1917
|
+
|
|
1918
|
+
const uint8_t* ticket = (const uint8_t*)RSTRING_PTR(ticket_val);
|
|
1919
|
+
uint32_t ticket_len = (uint32_t)RSTRING_LEN(ticket_val);
|
|
1920
|
+
|
|
1921
|
+
QUIC_STATUS Status = MsQuic->SetParam(
|
|
1922
|
+
Connection,
|
|
1923
|
+
QUIC_PARAM_CONN_RESUMPTION_TICKET,
|
|
1924
|
+
ticket_len,
|
|
1925
|
+
ticket);
|
|
1926
|
+
|
|
1927
|
+
return QUIC_SUCCEEDED(Status) ? Qtrue : Qfalse;
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
static VALUE
|
|
1931
|
+
connection_param_bytes(VALUE connection_handle_val, uint32_t param)
|
|
1932
|
+
{
|
|
1933
|
+
if (MsQuic == NULL) return Qnil;
|
|
1934
|
+
|
|
1935
|
+
HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle_val);
|
|
1936
|
+
if (Connection == NULL) return Qnil;
|
|
1937
|
+
|
|
1938
|
+
uint8_t buffer[256];
|
|
1939
|
+
uint32_t buffer_size = sizeof(buffer);
|
|
1940
|
+
memset(buffer, 0, sizeof(buffer));
|
|
1941
|
+
|
|
1942
|
+
QUIC_STATUS status = MsQuic->GetParam(Connection, param, &buffer_size, buffer);
|
|
1943
|
+
if (QUIC_FAILED(status) || buffer_size == 0) {
|
|
1944
|
+
return Qnil;
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
return rb_str_new((const char*)buffer, buffer_size);
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
// MsQuic derives this from the peer's max_datagram_frame_size transport parameter.
|
|
1951
|
+
static VALUE
|
|
1952
|
+
quicsilver_connection_datagram_send_enabled(VALUE self, VALUE connection_handle_val)
|
|
1953
|
+
{
|
|
1954
|
+
if (MsQuic == NULL) return Qfalse;
|
|
1955
|
+
|
|
1956
|
+
HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle_val);
|
|
1957
|
+
if (Connection == NULL) return Qfalse;
|
|
1958
|
+
|
|
1959
|
+
BOOLEAN enabled = FALSE;
|
|
1960
|
+
uint32_t size = sizeof(enabled);
|
|
1961
|
+
QUIC_STATUS status = MsQuic->GetParam(
|
|
1962
|
+
Connection, QUIC_PARAM_CONN_DATAGRAM_SEND_ENABLED, &size, &enabled);
|
|
1963
|
+
return QUIC_SUCCEEDED(status) && enabled ? Qtrue : Qfalse;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
static VALUE
|
|
1967
|
+
quicsilver_connection_reliable_reset_enabled(VALUE self, VALUE connection_handle_val)
|
|
1968
|
+
{
|
|
1969
|
+
if (MsQuic == NULL) return Qfalse;
|
|
1970
|
+
HQUIC connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle_val);
|
|
1971
|
+
if (!connection) return Qfalse;
|
|
1972
|
+
ConnectionContext* ctx = MsQuic->GetContext(connection);
|
|
1973
|
+
return ctx && ctx->reliable_reset_negotiated ? Qtrue : Qfalse;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
static VALUE
|
|
1977
|
+
quicsilver_set_stream_reliable_offset(VALUE self, VALUE stream_handle, VALUE offset_val)
|
|
1978
|
+
{
|
|
1979
|
+
uint64_t token = NUM2ULL(stream_handle);
|
|
1980
|
+
uint64_t offset = NUM2ULL(offset_val);
|
|
1981
|
+
StreamContext* ctx = find_stream(token);
|
|
1982
|
+
if (!MsQuic || !ctx) rb_raise(rb_eIOError, "QUIC stream is closed");
|
|
1983
|
+
QUIC_STATUS status = MsQuic->SetParam(
|
|
1984
|
+
ctx->stream, QUIC_PARAM_STREAM_RELIABLE_OFFSET, sizeof(offset), &offset);
|
|
1985
|
+
if (QUIC_FAILED(status)) rb_raise(rb_eRuntimeError, "Setting reliable offset failed, 0x%x", status);
|
|
1986
|
+
return offset_val;
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
// Returns [ip_string, port] or nil.
|
|
1990
|
+
static VALUE
|
|
1991
|
+
quicsilver_connection_remote_address(VALUE self, VALUE connection_handle_val)
|
|
1992
|
+
{
|
|
1993
|
+
if (MsQuic == NULL) return Qnil;
|
|
1994
|
+
|
|
1995
|
+
HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle_val);
|
|
1996
|
+
if (Connection == NULL) return Qnil;
|
|
1997
|
+
|
|
1998
|
+
QUIC_ADDR addr;
|
|
1999
|
+
uint32_t addr_size = sizeof(addr);
|
|
2000
|
+
if (QUIC_FAILED(MsQuic->GetParam(Connection, QUIC_PARAM_CONN_REMOTE_ADDRESS, &addr_size, &addr)))
|
|
2001
|
+
return Qnil;
|
|
2002
|
+
|
|
2003
|
+
char ip[INET6_ADDRSTRLEN];
|
|
2004
|
+
uint16_t port = 0;
|
|
2005
|
+
if (addr.Ip.sa_family == QUIC_ADDRESS_FAMILY_INET) {
|
|
2006
|
+
struct sockaddr_in *v4 = (struct sockaddr_in *)&addr;
|
|
2007
|
+
inet_ntop(AF_INET, &v4->sin_addr, ip, sizeof(ip));
|
|
2008
|
+
port = ntohs(v4->sin_port);
|
|
2009
|
+
} else {
|
|
2010
|
+
struct sockaddr_in6 *v6 = (struct sockaddr_in6 *)&addr;
|
|
2011
|
+
port = ntohs(v6->sin6_port);
|
|
2012
|
+
if (IN6_IS_ADDR_V4MAPPED(&v6->sin6_addr)) {
|
|
2013
|
+
struct in_addr v4;
|
|
2014
|
+
memcpy(&v4, &v6->sin6_addr.s6_addr[12], 4);
|
|
2015
|
+
inet_ntop(AF_INET, &v4, ip, sizeof(ip));
|
|
2016
|
+
} else {
|
|
2017
|
+
inet_ntop(AF_INET6, &v6->sin6_addr, ip, sizeof(ip));
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
VALUE result = rb_ary_new2(2);
|
|
2022
|
+
rb_ary_push(result, rb_str_new_cstr(ip));
|
|
2023
|
+
rb_ary_push(result, UINT2NUM(port));
|
|
2024
|
+
return result;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
// QUIC connection ID bytes for server-side routing/debugging.
|
|
2028
|
+
static VALUE
|
|
2029
|
+
quicsilver_connection_ids(VALUE self, VALUE connection_handle_val)
|
|
2030
|
+
{
|
|
2031
|
+
VALUE result = rb_hash_new();
|
|
2032
|
+
|
|
2033
|
+
VALUE odcid = connection_param_bytes(connection_handle_val, QUIC_PARAM_CONN_ORIG_DEST_CID);
|
|
2034
|
+
if (!NIL_P(odcid)) {
|
|
2035
|
+
rb_hash_aset(result, rb_str_new_cstr("original_destination_connection_id"), odcid);
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
return result;
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
static void
|
|
2042
|
+
close_connection(HQUIC connection)
|
|
2043
|
+
{
|
|
2044
|
+
// MsQuic 2.6 defaults to a blocking close outside the worker callback.
|
|
2045
|
+
// Our poll thread needs the GVL to complete that work, so queue it instead.
|
|
2046
|
+
BOOLEAN async = TRUE;
|
|
2047
|
+
QUIC_STATUS status = MsQuic->SetParam(connection, QUIC_PARAM_CONN_CLOSE_ASYNC, sizeof(async), &async);
|
|
2048
|
+
if (QUIC_FAILED(status)) {
|
|
2049
|
+
rb_raise(rb_eRuntimeError, "Enabling asynchronous connection close failed, 0x%x!", status);
|
|
2050
|
+
}
|
|
2051
|
+
forget_connection_streams(connection);
|
|
2052
|
+
MsQuic->ConnectionClose(connection);
|
|
2053
|
+
wake_event_loop();
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
// Close a QUIC connection and free context
|
|
2057
|
+
static VALUE
|
|
2058
|
+
quicsilver_close_connection_handle(VALUE self, VALUE connection_data)
|
|
2059
|
+
{
|
|
2060
|
+
if (MsQuic == NULL) {
|
|
2061
|
+
return Qnil;
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
// Extract connection handle and context from array
|
|
2065
|
+
VALUE connection_handle = rb_ary_entry(connection_data, 0);
|
|
2066
|
+
VALUE context_handle = rb_ary_entry(connection_data, 1);
|
|
2067
|
+
|
|
2068
|
+
HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle);
|
|
2069
|
+
(void)context_handle; // ctx freed by SHUTDOWN_COMPLETE, not here
|
|
2070
|
+
|
|
2071
|
+
if (Connection != NULL) {
|
|
2072
|
+
close_connection(Connection);
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
// Don't free ctx here — ConnectionClose is async and SHUTDOWN_COMPLETE
|
|
2076
|
+
// will fire on the next event loop poll, which still needs ctx.
|
|
2077
|
+
// SHUTDOWN_COMPLETE handles cleanup for both client and server.
|
|
2078
|
+
|
|
2079
|
+
return Qnil;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
// Close a server-side connection handle (context already freed in C callback)
|
|
2083
|
+
static VALUE
|
|
2084
|
+
quicsilver_close_server_connection(VALUE self, VALUE connection_handle)
|
|
2085
|
+
{
|
|
2086
|
+
if (MsQuic == NULL) return Qnil;
|
|
2087
|
+
|
|
2088
|
+
HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle);
|
|
2089
|
+
if (Connection != NULL) {
|
|
2090
|
+
close_connection(Connection);
|
|
2091
|
+
}
|
|
2092
|
+
return Qnil;
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
// Gracefully shutdown a QUIC connection (sends CONNECTION_CLOSE frame to peer)
|
|
2096
|
+
// silent = true: immediate shutdown without notifying peer
|
|
2097
|
+
// silent = false: graceful shutdown with CONNECTION_CLOSE frame
|
|
2098
|
+
static VALUE
|
|
2099
|
+
quicsilver_connection_shutdown(VALUE self, VALUE connection_handle, VALUE error_code, VALUE silent)
|
|
2100
|
+
{
|
|
2101
|
+
if (MsQuic == NULL) {
|
|
2102
|
+
rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
|
|
2103
|
+
return Qnil;
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle);
|
|
2107
|
+
uint64_t ErrorCode = NUM2ULL(error_code);
|
|
2108
|
+
|
|
2109
|
+
if (Connection != NULL) {
|
|
2110
|
+
QUIC_CONNECTION_SHUTDOWN_FLAGS flags = RTEST(silent)
|
|
2111
|
+
? QUIC_CONNECTION_SHUTDOWN_FLAG_SILENT
|
|
2112
|
+
: QUIC_CONNECTION_SHUTDOWN_FLAG_NONE;
|
|
2113
|
+
|
|
2114
|
+
MsQuic->ConnectionShutdown(Connection, flags, ErrorCode);
|
|
2115
|
+
wake_event_loop();
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
return Qtrue;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
// Close a QUIC configuration
|
|
2122
|
+
static VALUE
|
|
2123
|
+
quicsilver_close_configuration(VALUE self, VALUE config_handle)
|
|
2124
|
+
{
|
|
2125
|
+
if (MsQuic == NULL) {
|
|
2126
|
+
return Qnil;
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
HQUIC Configuration = (HQUIC)(uintptr_t)NUM2ULL(config_handle);
|
|
2130
|
+
MsQuic->ConfigurationClose(Configuration);
|
|
2131
|
+
return Qnil;
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
static VALUE
|
|
2135
|
+
quicsilver_close(VALUE self)
|
|
2136
|
+
{
|
|
2137
|
+
if (MsQuic != NULL) {
|
|
2138
|
+
st_clear(LiveStreams);
|
|
2139
|
+
if (Registration != NULL) {
|
|
2140
|
+
MsQuic->RegistrationClose(Registration);
|
|
2141
|
+
Registration = NULL;
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
if (ExecContext != NULL) {
|
|
2145
|
+
MsQuic->ExecutionDelete(1, &ExecContext);
|
|
2146
|
+
ExecContext = NULL;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
MsQuicClose(MsQuic);
|
|
2150
|
+
MsQuic = NULL;
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
#if __linux__
|
|
2154
|
+
if (WakeFd != -1) {
|
|
2155
|
+
close(WakeFd);
|
|
2156
|
+
WakeFd = -1;
|
|
2157
|
+
}
|
|
2158
|
+
#endif
|
|
2159
|
+
if (EventQ != -1) {
|
|
2160
|
+
close(EventQ);
|
|
2161
|
+
EventQ = -1;
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
return Qnil;
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
// Create a QUIC listener
|
|
2168
|
+
static VALUE
|
|
2169
|
+
quicsilver_create_listener(VALUE self, VALUE config_handle)
|
|
2170
|
+
{
|
|
2171
|
+
if (MsQuic == NULL) {
|
|
2172
|
+
rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
|
|
2173
|
+
return Qnil;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
HQUIC Configuration = (HQUIC)(uintptr_t)NUM2ULL(config_handle);
|
|
2177
|
+
HQUIC Listener = NULL;
|
|
2178
|
+
QUIC_STATUS Status;
|
|
2179
|
+
|
|
2180
|
+
// Create listener context
|
|
2181
|
+
ListenerContext* ctx = (ListenerContext*)malloc(sizeof(ListenerContext));
|
|
2182
|
+
if (ctx == NULL) {
|
|
2183
|
+
rb_raise(rb_eRuntimeError, "Failed to allocate listener context");
|
|
2184
|
+
return Qnil;
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
ctx->started = 0;
|
|
2188
|
+
ctx->stopped = 0;
|
|
2189
|
+
ctx->failed = 0;
|
|
2190
|
+
ctx->error_status = QUIC_STATUS_SUCCESS;
|
|
2191
|
+
ctx->Configuration = Configuration;
|
|
2192
|
+
|
|
2193
|
+
// Create listener
|
|
2194
|
+
if (QUIC_FAILED(Status = MsQuic->ListenerOpen(Registration, ListenerCallback, ctx, &Listener))) {
|
|
2195
|
+
free(ctx);
|
|
2196
|
+
rb_raise(rb_eRuntimeError, "ListenerOpen failed, 0x%x!", Status);
|
|
2197
|
+
return Qnil;
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
// Return listener handle and context
|
|
2201
|
+
VALUE result = rb_ary_new2(2);
|
|
2202
|
+
rb_ary_push(result, ULL2NUM((uintptr_t)Listener));
|
|
2203
|
+
rb_ary_push(result, ULL2NUM((uintptr_t)ctx));
|
|
2204
|
+
return result;
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
static uint32_t
|
|
2208
|
+
cibir_param_buffer(VALUE cibir_bytes_val, uint8_t* buffer)
|
|
2209
|
+
{
|
|
2210
|
+
StringValue(cibir_bytes_val);
|
|
2211
|
+
long cibir_len = RSTRING_LEN(cibir_bytes_val);
|
|
2212
|
+
if (cibir_len == 0 || cibir_len > 6) {
|
|
2213
|
+
rb_raise(rb_eArgError, "cibir bytes must be 1..6 bytes");
|
|
2214
|
+
return 0;
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
buffer[0] = 0;
|
|
2218
|
+
memcpy(buffer + 1, RSTRING_PTR(cibir_bytes_val), cibir_len);
|
|
2219
|
+
return (uint32_t)(1 + cibir_len);
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
// Configure the fixed Server ID bytes MsQuic places in generated QUIC
|
|
2223
|
+
// connection IDs. Must run before RegistrationOpen, because MsQuic only applies
|
|
2224
|
+
// this process-global setting safely before the library is in use.
|
|
2225
|
+
static VALUE
|
|
2226
|
+
quicsilver_apply_msquic_server_id(VALUE self, VALUE server_id_bytes_val)
|
|
2227
|
+
{
|
|
2228
|
+
QUIC_STATUS Status;
|
|
2229
|
+
|
|
2230
|
+
if (Registration != NULL || ExecContext != NULL) {
|
|
2231
|
+
rb_raise(rb_eRuntimeError, "MsQuic Server ID must be configured before Quicsilver is opened");
|
|
2232
|
+
return Qfalse;
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
if (MsQuic == NULL && QUIC_FAILED(Status = MsQuicOpenVersion(2, (const void**)&MsQuic))) {
|
|
2236
|
+
rb_raise(rb_eRuntimeError, "MsQuicOpenVersion failed, 0x%x!", Status);
|
|
2237
|
+
return Qfalse;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
QUIC_GLOBAL_SETTINGS Settings;
|
|
2241
|
+
memset(&Settings, 0, sizeof(Settings));
|
|
2242
|
+
|
|
2243
|
+
StringValue(server_id_bytes_val);
|
|
2244
|
+
if (RSTRING_LEN(server_id_bytes_val) != sizeof(Settings.FixedServerID)) {
|
|
2245
|
+
rb_raise(rb_eArgError, "Server ID must be exactly 4 bytes");
|
|
2246
|
+
return Qfalse;
|
|
2247
|
+
}
|
|
2248
|
+
Settings.IsSet.LoadBalancingMode = TRUE;
|
|
2249
|
+
Settings.LoadBalancingMode = QUIC_LOAD_BALANCING_SERVER_ID_FIXED;
|
|
2250
|
+
Settings.IsSet.FixedServerID = TRUE;
|
|
2251
|
+
memcpy(&Settings.FixedServerID, RSTRING_PTR(server_id_bytes_val), sizeof(Settings.FixedServerID));
|
|
2252
|
+
|
|
2253
|
+
Status = MsQuic->SetParam(
|
|
2254
|
+
NULL,
|
|
2255
|
+
QUIC_PARAM_GLOBAL_GLOBAL_SETTINGS,
|
|
2256
|
+
sizeof(Settings),
|
|
2257
|
+
&Settings);
|
|
2258
|
+
|
|
2259
|
+
if (QUIC_FAILED(Status)) {
|
|
2260
|
+
rb_raise(rb_eRuntimeError, "MsQuic Server ID configuration failed, 0x%x!", Status);
|
|
2261
|
+
return Qfalse;
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
return Qtrue;
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
// Configure listener-level CIBIR (Connection ID Based Implicit Routing) bytes.
|
|
2268
|
+
static VALUE
|
|
2269
|
+
quicsilver_configure_listener_cibir(VALUE self, VALUE listener_handle, VALUE cibir_bytes_val)
|
|
2270
|
+
{
|
|
2271
|
+
if (MsQuic == NULL) {
|
|
2272
|
+
rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
|
|
2273
|
+
return Qfalse;
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
uint8_t buffer[7];
|
|
2277
|
+
uint32_t buffer_len = cibir_param_buffer(cibir_bytes_val, buffer);
|
|
2278
|
+
|
|
2279
|
+
HQUIC Listener = (HQUIC)(uintptr_t)NUM2ULL(listener_handle);
|
|
2280
|
+
QUIC_STATUS Status = MsQuic->SetParam(
|
|
2281
|
+
Listener,
|
|
2282
|
+
QUIC_PARAM_LISTENER_CIBIR_ID,
|
|
2283
|
+
buffer_len,
|
|
2284
|
+
buffer);
|
|
2285
|
+
|
|
2286
|
+
if (QUIC_FAILED(Status)) {
|
|
2287
|
+
rb_raise(rb_eRuntimeError, "Listener CIBIR configuration failed, 0x%x!", Status);
|
|
2288
|
+
return Qfalse;
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
return Qtrue;
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
// Configure client-side CIBIR bytes before ConnectionStart.
|
|
2295
|
+
static VALUE
|
|
2296
|
+
quicsilver_configure_connection_cibir(VALUE self, VALUE connection_handle, VALUE cibir_bytes_val)
|
|
2297
|
+
{
|
|
2298
|
+
if (MsQuic == NULL) {
|
|
2299
|
+
rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
|
|
2300
|
+
return Qfalse;
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle);
|
|
2304
|
+
BOOLEAN share_binding = TRUE;
|
|
2305
|
+
QUIC_STATUS Status = MsQuic->SetParam(
|
|
2306
|
+
Connection,
|
|
2307
|
+
QUIC_PARAM_CONN_SHARE_UDP_BINDING,
|
|
2308
|
+
sizeof(share_binding),
|
|
2309
|
+
&share_binding);
|
|
2310
|
+
if (QUIC_FAILED(Status)) {
|
|
2311
|
+
rb_raise(rb_eRuntimeError, "Connection UDP binding configuration failed, 0x%x!", Status);
|
|
2312
|
+
return Qfalse;
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
uint8_t buffer[7];
|
|
2316
|
+
uint32_t buffer_len = cibir_param_buffer(cibir_bytes_val, buffer);
|
|
2317
|
+
Status = MsQuic->SetParam(
|
|
2318
|
+
Connection,
|
|
2319
|
+
QUIC_PARAM_CONN_CIBIR_ID,
|
|
2320
|
+
buffer_len,
|
|
2321
|
+
buffer);
|
|
2322
|
+
|
|
2323
|
+
if (QUIC_FAILED(Status)) {
|
|
2324
|
+
rb_raise(rb_eRuntimeError, "Connection CIBIR configuration failed, 0x%x!", Status);
|
|
2325
|
+
return Qfalse;
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
return Qtrue;
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
// Start listener on specific address and port
|
|
2332
|
+
static VALUE
|
|
2333
|
+
quicsilver_start_listener(VALUE self, VALUE listener_handle, VALUE address, VALUE port, VALUE alpn)
|
|
2334
|
+
{
|
|
2335
|
+
if (MsQuic == NULL) {
|
|
2336
|
+
rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
|
|
2337
|
+
return Qfalse;
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
HQUIC Listener = (HQUIC)(uintptr_t)NUM2ULL(listener_handle);
|
|
2341
|
+
uint16_t Port = (uint16_t)NUM2INT(port);
|
|
2342
|
+
const char* alpn_str = StringValueCStr(alpn);
|
|
2343
|
+
|
|
2344
|
+
// Setup address - properly initialize the entire structure
|
|
2345
|
+
QUIC_ADDR Address;
|
|
2346
|
+
memset(&Address, 0, sizeof(Address));
|
|
2347
|
+
|
|
2348
|
+
// Parse address string to determine family.
|
|
2349
|
+
// "::" or any address with ':' → INET6 (IPv6 only on macOS).
|
|
2350
|
+
// "0.0.0.0" → UNSPEC (dual-stack: accepts both IPv4 and IPv6).
|
|
2351
|
+
// Specific IPv4 (e.g. "127.0.0.1") → INET (IPv4 only).
|
|
2352
|
+
const char* addr_str = StringValueCStr(address);
|
|
2353
|
+
if (strchr(addr_str, ':') != NULL) {
|
|
2354
|
+
QuicAddrSetFamily(&Address, QUIC_ADDRESS_FAMILY_INET6);
|
|
2355
|
+
} else if (strcmp(addr_str, "0.0.0.0") == 0) {
|
|
2356
|
+
QuicAddrSetFamily(&Address, QUIC_ADDRESS_FAMILY_UNSPEC);
|
|
2357
|
+
} else {
|
|
2358
|
+
QuicAddrSetFamily(&Address, QUIC_ADDRESS_FAMILY_INET);
|
|
2359
|
+
}
|
|
2360
|
+
QuicAddrSetPort(&Address, Port);
|
|
2361
|
+
|
|
2362
|
+
QUIC_STATUS Status;
|
|
2363
|
+
|
|
2364
|
+
QUIC_BUFFER AlpnBuffer = { (uint32_t)strlen(alpn_str), (uint8_t*)alpn_str };
|
|
2365
|
+
|
|
2366
|
+
if (QUIC_FAILED(Status = MsQuic->ListenerStart(Listener, &AlpnBuffer, 1, &Address))) {
|
|
2367
|
+
rb_raise(rb_eRuntimeError, "ListenerStart failed, 0x%x!", Status);
|
|
2368
|
+
return Qfalse;
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
wake_event_loop();
|
|
2372
|
+
return Qtrue;
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
// Stop listener
|
|
2376
|
+
static VALUE
|
|
2377
|
+
quicsilver_stop_listener(VALUE self, VALUE listener_handle)
|
|
2378
|
+
{
|
|
2379
|
+
if (MsQuic == NULL) {
|
|
2380
|
+
return Qfalse;
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
HQUIC Listener = (HQUIC)(uintptr_t)NUM2ULL(listener_handle);
|
|
2384
|
+
MsQuic->ListenerStop(Listener);
|
|
2385
|
+
wake_event_loop();
|
|
2386
|
+
return Qtrue;
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
// Close listener
|
|
2390
|
+
static VALUE
|
|
2391
|
+
quicsilver_close_listener(VALUE self, VALUE listener_data)
|
|
2392
|
+
{
|
|
2393
|
+
if (MsQuic == NULL) {
|
|
2394
|
+
return Qnil;
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
VALUE listener_handle = rb_ary_entry(listener_data, 0);
|
|
2398
|
+
VALUE context_handle = rb_ary_entry(listener_data, 1);
|
|
2399
|
+
|
|
2400
|
+
HQUIC Listener = (HQUIC)(uintptr_t)NUM2ULL(listener_handle);
|
|
2401
|
+
ListenerContext* ctx = (ListenerContext*)(uintptr_t)NUM2ULL(context_handle);
|
|
2402
|
+
|
|
2403
|
+
MsQuic->ListenerClose(Listener);
|
|
2404
|
+
|
|
2405
|
+
if (ctx != NULL) {
|
|
2406
|
+
free(ctx);
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
return Qnil;
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
// Open a QUIC stream
|
|
2413
|
+
// Accepts connection_data array [connection_handle, context_handle]
|
|
2414
|
+
// Works uniformly for both client and server
|
|
2415
|
+
static VALUE
|
|
2416
|
+
quicsilver_open_stream(VALUE self, VALUE connection_data, VALUE unidirectional)
|
|
2417
|
+
{
|
|
2418
|
+
if (MsQuic == NULL) {
|
|
2419
|
+
rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
|
|
2420
|
+
return Qnil;
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
// Extract connection handle and context from array
|
|
2424
|
+
VALUE connection_handle = rb_ary_entry(connection_data, 0);
|
|
2425
|
+
VALUE context_handle = rb_ary_entry(connection_data, 1);
|
|
2426
|
+
|
|
2427
|
+
HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle);
|
|
2428
|
+
ConnectionContext* conn_ctx = (ConnectionContext*)(uintptr_t)NUM2ULL(context_handle);
|
|
2429
|
+
HQUIC Stream = NULL;
|
|
2430
|
+
|
|
2431
|
+
StreamContext* ctx = (StreamContext*)malloc(sizeof(StreamContext));
|
|
2432
|
+
if (ctx == NULL) {
|
|
2433
|
+
rb_raise(rb_eRuntimeError, "Failed to allocate stream context");
|
|
2434
|
+
return Qnil;
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
ctx->connection = Connection;
|
|
2438
|
+
ctx->connection_ctx = conn_ctx; // Store connection context pointer
|
|
2439
|
+
ctx->client_obj = conn_ctx ? conn_ctx->client_obj : Qnil;
|
|
2440
|
+
ctx->stream_id = UINT64_MAX; // Sentinel: lazily resolved on first callback
|
|
2441
|
+
ctx->started = 1;
|
|
2442
|
+
ctx->shutdown = 0;
|
|
2443
|
+
ctx->early_data = 0;
|
|
2444
|
+
ctx->error_status = QUIC_STATUS_SUCCESS;
|
|
2445
|
+
|
|
2446
|
+
// Use flag based on parameter
|
|
2447
|
+
QUIC_STREAM_OPEN_FLAGS flags = RTEST(unidirectional)
|
|
2448
|
+
? QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL
|
|
2449
|
+
: QUIC_STREAM_OPEN_FLAG_NONE;
|
|
2450
|
+
|
|
2451
|
+
// Create stream
|
|
2452
|
+
QUIC_STATUS Status = MsQuic->StreamOpen(Connection, flags, StreamCallback, ctx, &Stream);
|
|
2453
|
+
if (QUIC_FAILED(Status)) {
|
|
2454
|
+
free(ctx);
|
|
2455
|
+
rb_raise(rb_eRuntimeError, "StreamOpen failed, 0x%x!", Status);
|
|
2456
|
+
return Qnil;
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
uint64_t token = register_stream(ctx, Stream, RTEST(unidirectional)
|
|
2460
|
+
? QUIC_STREAM_SHUTDOWN_FLAG_ABORT_SEND : QUIC_STREAM_SHUTDOWN_FLAG_ABORT);
|
|
2461
|
+
|
|
2462
|
+
// Start the stream. INDICATE_PEER_ACCEPT gives us visibility into
|
|
2463
|
+
// stream flow control — fires PEER_ACCEPTED event when the peer
|
|
2464
|
+
// raises MAX_STREAMS if the stream was initially queued.
|
|
2465
|
+
Status = MsQuic->StreamStart(Stream,
|
|
2466
|
+
QUIC_STREAM_START_FLAG_INDICATE_PEER_ACCEPT | QUIC_STREAM_START_FLAG_SHUTDOWN_ON_FAIL);
|
|
2467
|
+
if (QUIC_FAILED(Status)) {
|
|
2468
|
+
unregister_stream(token);
|
|
2469
|
+
// StreamClose fires SHUTDOWN_COMPLETE synchronously which frees ctx
|
|
2470
|
+
MsQuic->StreamClose(Stream);
|
|
2471
|
+
rb_raise(rb_eRuntimeError, "StreamStart failed, 0x%x!", Status);
|
|
2472
|
+
return Qnil;
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
wake_event_loop();
|
|
2476
|
+
return ULL2NUM(token);
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
// Send data on a QUIC stream
|
|
2480
|
+
static VALUE
|
|
2481
|
+
quicsilver_send_stream(VALUE self, VALUE stream_handle, VALUE data, VALUE send_fin)
|
|
2482
|
+
{
|
|
2483
|
+
if (MsQuic == NULL) {
|
|
2484
|
+
rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
|
|
2485
|
+
return Qnil;
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
uint64_t token = NUM2ULL(stream_handle);
|
|
2489
|
+
StringValue(data);
|
|
2490
|
+
StreamContext* ctx = find_stream(token);
|
|
2491
|
+
if (!ctx || __atomic_load_n(&ctx->shutdown, __ATOMIC_ACQUIRE)) rb_raise(rb_eIOError, "QUIC stream is closed");
|
|
2492
|
+
HQUIC Stream = ctx->stream;
|
|
2493
|
+
|
|
2494
|
+
// MsQuic holds a pointer into the String until SEND_COMPLETE, so the
|
|
2495
|
+
// bytes must not change. A frozen String is sent as-is (zero copy); an
|
|
2496
|
+
// unfrozen one gets a frozen copy-on-write share, so a caller that keeps
|
|
2497
|
+
// mutating its buffer unshares on its own side and never sees a
|
|
2498
|
+
// FrozenError from here. Library callers freeze their output to get the
|
|
2499
|
+
// zero-copy path.
|
|
2500
|
+
data = rb_str_new_frozen(data);
|
|
2501
|
+
|
|
2502
|
+
SendContext* send = send_context_new(data);
|
|
2503
|
+
if (send == NULL) {
|
|
2504
|
+
rb_raise(rb_eRuntimeError, "SendContext allocation failed!");
|
|
2505
|
+
return Qnil;
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
// Use flag based on parameter (default to FIN for backwards compat)
|
|
2509
|
+
QUIC_SEND_FLAGS flags = (NIL_P(send_fin) || RTEST(send_fin))
|
|
2510
|
+
? QUIC_SEND_FLAG_FIN
|
|
2511
|
+
: QUIC_SEND_FLAG_NONE;
|
|
2512
|
+
|
|
2513
|
+
QUIC_STATUS Status = MsQuic->StreamSend(Stream, &send->buffer, 1, flags, send);
|
|
2514
|
+
if (QUIC_FAILED(Status)) {
|
|
2515
|
+
send_context_release(send);
|
|
2516
|
+
rb_raise(rb_eRuntimeError, "StreamSend failed, 0x%x!", Status);
|
|
2517
|
+
return Qfalse;
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
wake_event_loop();
|
|
2521
|
+
return Qtrue;
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
// Send an unreliable datagram on a QUIC connection (RFC 9221).
|
|
2525
|
+
// Datagrams are not retransmitted — best effort delivery.
|
|
2526
|
+
// Data must fit in a single QUIC packet (typically ~1200 bytes).
|
|
2527
|
+
static VALUE
|
|
2528
|
+
quicsilver_datagram_send(VALUE self, VALUE connection_data, VALUE data)
|
|
2529
|
+
{
|
|
2530
|
+
if (MsQuic == NULL) {
|
|
2531
|
+
rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
|
|
2532
|
+
return Qnil;
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
VALUE conn_handle_val = rb_ary_entry(connection_data, 0);
|
|
2536
|
+
HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(conn_handle_val);
|
|
2537
|
+
|
|
2538
|
+
StringValue(data);
|
|
2539
|
+
const char* data_str = RSTRING_PTR(data);
|
|
2540
|
+
uint32_t data_len = (uint32_t)RSTRING_LEN(data);
|
|
2541
|
+
|
|
2542
|
+
void* SendBufferRaw = malloc(sizeof(QUIC_BUFFER) + data_len);
|
|
2543
|
+
if (SendBufferRaw == NULL) {
|
|
2544
|
+
rb_raise(rb_eRuntimeError, "Datagram buffer allocation failed!");
|
|
2545
|
+
return Qnil;
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
QUIC_BUFFER* SendBuffer = (QUIC_BUFFER*)SendBufferRaw;
|
|
2549
|
+
SendBuffer->Buffer = (uint8_t*)SendBufferRaw + sizeof(QUIC_BUFFER);
|
|
2550
|
+
SendBuffer->Length = data_len;
|
|
2551
|
+
memcpy(SendBuffer->Buffer, data_str, data_len);
|
|
2552
|
+
|
|
2553
|
+
QUIC_STATUS Status = MsQuic->DatagramSend(Connection, SendBuffer, 1, QUIC_SEND_FLAG_NONE, SendBufferRaw);
|
|
2554
|
+
if (QUIC_FAILED(Status)) {
|
|
2555
|
+
free(SendBufferRaw);
|
|
2556
|
+
if (Status == QUIC_STATUS_BUFFER_TOO_SMALL) {
|
|
2557
|
+
rb_raise(rb_eArgError, "Datagram too large (%u bytes) - must fit in a single QUIC packet", data_len);
|
|
2558
|
+
} else {
|
|
2559
|
+
rb_raise(rb_eRuntimeError, "DatagramSend failed, 0x%x", Status);
|
|
2560
|
+
}
|
|
2561
|
+
return Qfalse;
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
wake_event_loop();
|
|
2565
|
+
return Qtrue;
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
// Get the QUIC stream ID for an open stream.
|
|
2569
|
+
// Must be called after data has been sent (MsQuic defers ID assignment
|
|
2570
|
+
// with QUIC_STREAM_START_FLAG_NONE until data flows).
|
|
2571
|
+
static VALUE
|
|
2572
|
+
quicsilver_get_stream_id(VALUE self, VALUE stream_handle)
|
|
2573
|
+
{
|
|
2574
|
+
if (MsQuic == NULL) {
|
|
2575
|
+
rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
|
|
2576
|
+
return Qnil;
|
|
2577
|
+
}
|
|
2578
|
+
StreamContext* ctx = find_stream(NUM2ULL(stream_handle));
|
|
2579
|
+
if (!ctx) return Qnil;
|
|
2580
|
+
HQUIC Stream = ctx->stream;
|
|
2581
|
+
uint64_t stream_id = 0;
|
|
2582
|
+
uint32_t stream_id_len = sizeof(stream_id);
|
|
2583
|
+
QUIC_STATUS Status = MsQuic->GetParam(Stream, QUIC_PARAM_STREAM_ID, &stream_id_len, &stream_id);
|
|
2584
|
+
if (QUIC_FAILED(Status)) {
|
|
2585
|
+
return Qnil;
|
|
2586
|
+
}
|
|
2587
|
+
return ULL2NUM(stream_id);
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
// Experimental native byte/chunk admission. First grant opts in (zero pauses on the
|
|
2591
|
+
// next data indication); subsequent grants add credit. This does not reserve
|
|
2592
|
+
// connection-level control credit or integrate application queue consumption.
|
|
2593
|
+
static VALUE
|
|
2594
|
+
quicsilver_grant_stream_receive_credit(VALUE self, VALUE stream_handle, VALUE bytes, VALUE chunks)
|
|
2595
|
+
{
|
|
2596
|
+
// Validate here, not in Transport::Stream: this module function is a public
|
|
2597
|
+
// boundary, and NUM2ULL silently wraps a negative Integer into huge credit.
|
|
2598
|
+
uint64_t token = NUM2ULL(stream_handle);
|
|
2599
|
+
uint64_t credit = credit_arg(bytes, "Receive credit");
|
|
2600
|
+
uint64_t chunk_credit = credit_arg(chunks, "Receive chunks");
|
|
2601
|
+
StreamContext* ctx = find_stream(token);
|
|
2602
|
+
if (!MsQuic || !ctx || !(ctx->abort_flags & QUIC_STREAM_SHUTDOWN_FLAG_ABORT_RECEIVE)) return Qfalse;
|
|
2603
|
+
|
|
2604
|
+
// Update under the lock; resume outside it (MsQuic call). A receive that
|
|
2605
|
+
// paused the stream set receive_paused under this same lock, so either it
|
|
2606
|
+
// ran before us and we see the flag, or it runs after us and sees the
|
|
2607
|
+
// credit. StreamReceiveSetEnabled is a queued MsQuic operation that runs
|
|
2608
|
+
// after any callback in flight, so the resume cannot land before the pause
|
|
2609
|
+
// it is meant to undo.
|
|
2610
|
+
int resume = 0;
|
|
2611
|
+
STATE_LOCK();
|
|
2612
|
+
if (ctx->receive_delivery_failed) { STATE_UNLOCK(); return Qfalse; }
|
|
2613
|
+
if (credit > UINT64_MAX - ctx->receive_credit) { STATE_UNLOCK(); rb_raise(rb_eRangeError, "Receive credit exceeds 64 bits"); }
|
|
2614
|
+
if (chunk_credit > UINT64_MAX - ctx->receive_chunks) { STATE_UNLOCK(); rb_raise(rb_eRangeError, "Receive chunks exceed 64 bits"); }
|
|
2615
|
+
ctx->receive_credit_enabled = 1;
|
|
2616
|
+
ctx->receive_credit += credit;
|
|
2617
|
+
ctx->receive_chunks += chunk_credit;
|
|
2618
|
+
if (ctx->receive_paused && ctx->receive_credit && ctx->receive_chunks) {
|
|
2619
|
+
ctx->receive_paused = 0;
|
|
2620
|
+
resume = 1;
|
|
2621
|
+
}
|
|
2622
|
+
STATE_UNLOCK();
|
|
2623
|
+
|
|
2624
|
+
if (resume && !resume_receive(ctx)) return Qfalse;
|
|
2625
|
+
wake_event_loop();
|
|
2626
|
+
return Qtrue;
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
|
|
2630
|
+
static VALUE
|
|
2631
|
+
shutdown_stream(VALUE stream_handle, VALUE error_code, QUIC_STREAM_SHUTDOWN_FLAGS flags)
|
|
2632
|
+
{
|
|
2633
|
+
uint64_t token = NUM2ULL(stream_handle);
|
|
2634
|
+
uint64_t code = NUM2ULL(error_code);
|
|
2635
|
+
StreamContext* ctx = find_stream(token);
|
|
2636
|
+
if (!MsQuic || !ctx) return Qfalse;
|
|
2637
|
+
|
|
2638
|
+
flags &= ctx->abort_flags;
|
|
2639
|
+
if (!flags) return Qfalse;
|
|
2640
|
+
QUIC_STATUS status = MsQuic->StreamShutdown(ctx->stream, flags, code);
|
|
2641
|
+
wake_event_loop();
|
|
2642
|
+
return QUIC_SUCCEEDED(status) ? Qtrue : Qfalse;
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
// Reset a QUIC stream (RESET_STREAM frame - abruptly terminates sending)
|
|
2646
|
+
static VALUE
|
|
2647
|
+
quicsilver_stream_reset(VALUE self, VALUE stream_handle, VALUE error_code)
|
|
2648
|
+
{
|
|
2649
|
+
return shutdown_stream(stream_handle, error_code, QUIC_STREAM_SHUTDOWN_FLAG_ABORT_SEND);
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
static VALUE
|
|
2653
|
+
quicsilver_set_stream_priority(VALUE self, VALUE stream_handle, VALUE priority)
|
|
2654
|
+
{
|
|
2655
|
+
uint64_t token = NUM2ULL(stream_handle);
|
|
2656
|
+
unsigned int value = NUM2UINT(priority);
|
|
2657
|
+
if (value > UINT16_MAX) rb_raise(rb_eArgError, "Stream priority must fit in 16 bits");
|
|
2658
|
+
StreamContext* ctx = find_stream(token);
|
|
2659
|
+
if (!MsQuic || !ctx) return Qfalse;
|
|
2660
|
+
__atomic_store_n(&ctx->pending_priority, value + 1, __ATOMIC_RELEASE);
|
|
2661
|
+
wake_event_loop();
|
|
2662
|
+
return Qtrue;
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
static VALUE
|
|
2666
|
+
quicsilver_stream_stop_sending(VALUE self, VALUE stream_handle, VALUE error_code)
|
|
2667
|
+
{
|
|
2668
|
+
return shutdown_stream(stream_handle, error_code, QUIC_STREAM_SHUTDOWN_FLAG_ABORT_RECEIVE);
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
static VALUE
|
|
2672
|
+
quicsilver_wake(VALUE self)
|
|
2673
|
+
{
|
|
2674
|
+
wake_event_loop();
|
|
2675
|
+
return Qnil;
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
// Initialize the extension
|
|
2679
|
+
void
|
|
2680
|
+
Init_quicsilver(void)
|
|
2681
|
+
{
|
|
2682
|
+
mQuicsilver = rb_define_module("Quicsilver");
|
|
2683
|
+
LiveStreams = st_init_numtable();
|
|
2684
|
+
LiveSends = st_init_numtable();
|
|
2685
|
+
LiveSendsRoot = TypedData_Wrap_Struct(rb_cObject, &live_sends_type, &LiveSendsRootData);
|
|
2686
|
+
rb_gc_register_mark_object(LiveSendsRoot);
|
|
2687
|
+
|
|
2688
|
+
// Core initialization
|
|
2689
|
+
rb_define_singleton_method(mQuicsilver, "open_connection", quicsilver_open, 0);
|
|
2690
|
+
rb_define_singleton_method(mQuicsilver, "close_connection", quicsilver_close, 0);
|
|
2691
|
+
|
|
2692
|
+
// Configuration management
|
|
2693
|
+
rb_define_singleton_method(mQuicsilver, "create_configuration", quicsilver_create_configuration, -1);
|
|
2694
|
+
rb_define_singleton_method(mQuicsilver, "create_server_configuration", quicsilver_create_server_configuration, 1);
|
|
2695
|
+
rb_define_singleton_method(mQuicsilver, "close_configuration", quicsilver_close_configuration, 1);
|
|
2696
|
+
|
|
2697
|
+
// Connection management
|
|
2698
|
+
rb_define_singleton_method(mQuicsilver, "create_connection", quicsilver_create_connection, 1);
|
|
2699
|
+
rb_define_singleton_method(mQuicsilver, "start_connection", quicsilver_start_connection, 4);
|
|
2700
|
+
rb_define_singleton_method(mQuicsilver, "wait_for_connection", quicsilver_wait_for_connection, 2);
|
|
2701
|
+
rb_define_singleton_method(mQuicsilver, "connection_status", quicsilver_connection_status, 1);
|
|
2702
|
+
rb_define_singleton_method(mQuicsilver, "connection_statistics", quicsilver_connection_statistics, 1);
|
|
2703
|
+
rb_define_singleton_method(mQuicsilver, "transport_counters", quicsilver_transport_counters, 0);
|
|
2704
|
+
rb_define_singleton_method(mQuicsilver, "connection_reliable_reset_enabled?", quicsilver_connection_reliable_reset_enabled, 1);
|
|
2705
|
+
rb_define_singleton_method(mQuicsilver, "set_stream_reliable_offset", quicsilver_set_stream_reliable_offset, 2);
|
|
2706
|
+
rb_define_singleton_method(mQuicsilver, "connection_datagram_send_enabled?", quicsilver_connection_datagram_send_enabled, 1);
|
|
2707
|
+
rb_define_singleton_method(mQuicsilver, "connection_remote_address", quicsilver_connection_remote_address, 1);
|
|
2708
|
+
rb_define_singleton_method(mQuicsilver, "connection_ids", quicsilver_connection_ids, 1);
|
|
2709
|
+
rb_define_singleton_method(mQuicsilver, "get_resumption_ticket", quicsilver_get_resumption_ticket, 1);
|
|
2710
|
+
rb_define_singleton_method(mQuicsilver, "set_resumption_ticket", quicsilver_set_resumption_ticket, 2);
|
|
2711
|
+
rb_define_singleton_method(mQuicsilver, "connection_shutdown", quicsilver_connection_shutdown, 3);
|
|
2712
|
+
rb_define_singleton_method(mQuicsilver, "close_connection_handle", quicsilver_close_connection_handle, 1);
|
|
2713
|
+
rb_define_singleton_method(mQuicsilver, "close_server_connection", quicsilver_close_server_connection, 1);
|
|
2714
|
+
|
|
2715
|
+
// Listener management
|
|
2716
|
+
rb_define_singleton_method(mQuicsilver, "apply_msquic_server_id", quicsilver_apply_msquic_server_id, 1);
|
|
2717
|
+
rb_define_singleton_method(mQuicsilver, "create_listener", quicsilver_create_listener, 1);
|
|
2718
|
+
rb_define_singleton_method(mQuicsilver, "configure_listener_cibir", quicsilver_configure_listener_cibir, 2);
|
|
2719
|
+
rb_define_singleton_method(mQuicsilver, "configure_connection_cibir", quicsilver_configure_connection_cibir, 2);
|
|
2720
|
+
rb_define_singleton_method(mQuicsilver, "start_listener", quicsilver_start_listener, 4);
|
|
2721
|
+
rb_define_singleton_method(mQuicsilver, "stop_listener", quicsilver_stop_listener, 1);
|
|
2722
|
+
rb_define_singleton_method(mQuicsilver, "close_listener", quicsilver_close_listener, 1);
|
|
2723
|
+
|
|
2724
|
+
// Stream management
|
|
2725
|
+
rb_define_singleton_method(mQuicsilver, "open_stream", quicsilver_open_stream, 2);
|
|
2726
|
+
rb_define_singleton_method(mQuicsilver, "send_stream", quicsilver_send_stream, 3);
|
|
2727
|
+
rb_define_singleton_method(mQuicsilver, "stream_reset", quicsilver_stream_reset, 2);
|
|
2728
|
+
rb_define_singleton_method(mQuicsilver, "stream_stop_sending", quicsilver_stream_stop_sending, 2);
|
|
2729
|
+
rb_define_singleton_method(mQuicsilver, "set_stream_priority", quicsilver_set_stream_priority, 2);
|
|
2730
|
+
rb_define_singleton_method(mQuicsilver, "get_stream_id", quicsilver_get_stream_id, 1);
|
|
2731
|
+
rb_define_singleton_method(mQuicsilver, "grant_stream_receive_credit", quicsilver_grant_stream_receive_credit, 3);
|
|
2732
|
+
rb_define_singleton_method(mQuicsilver, "datagram_send", quicsilver_datagram_send, 2);
|
|
2733
|
+
|
|
2734
|
+
// Event processing (custom execution — app drives MsQuic)
|
|
2735
|
+
rb_define_singleton_method(mQuicsilver, "poll", quicsilver_poll, 0);
|
|
2736
|
+
rb_define_singleton_method(mQuicsilver, "wake", quicsilver_wake, 0);
|
|
2737
|
+
rb_define_singleton_method(mQuicsilver, "dispatch_events", quicsilver_dispatch_events, 2);
|
|
2738
|
+
rb_define_singleton_method(mQuicsilver, "open_event_queue", quicsilver_open_event_queue, 0);
|
|
2739
|
+
rb_define_singleton_method(mQuicsilver, "close_event_queue", quicsilver_close_event_queue, 0);
|
|
2740
|
+
rb_define_singleton_method(mQuicsilver, "dropped_datagrams", quicsilver_dropped_datagrams, 0);
|
|
2741
|
+
}
|