quietquic 0.1.0.alpha.3
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/HISTORY.md +79 -0
- data/LICENSE +13 -0
- data/README.md +318 -0
- data/examples/cooperative_async.rb +91 -0
- data/examples/echo_client.rb +40 -0
- data/examples/echo_server.rb +48 -0
- data/ext/quietquic/Cargo.lock +1377 -0
- data/ext/quietquic/Cargo.toml +25 -0
- data/ext/quietquic/extconf.rb +7 -0
- data/ext/quietquic/src/client.rs +89 -0
- data/ext/quietquic/src/config.rs +292 -0
- data/ext/quietquic/src/conn.rs +283 -0
- data/ext/quietquic/src/errors.rs +140 -0
- data/ext/quietquic/src/lib.rs +59 -0
- data/ext/quietquic/src/pending.rs +386 -0
- data/ext/quietquic/src/runtime.rs +72 -0
- data/ext/quietquic/src/server.rs +259 -0
- data/lib/quietquic/await.rb +22 -0
- data/lib/quietquic/client.rb +40 -0
- data/lib/quietquic/connection.rb +55 -0
- data/lib/quietquic/server.rb +70 -0
- data/lib/quietquic/stream.rb +79 -0
- data/lib/quietquic/version.rb +4 -0
- data/lib/quietquic.rb +8 -0
- metadata +125 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// SPDX-License-Identifier: 0BSD
|
|
2
|
+
//! `QuietQUIC::Native::Server` — the FFI wrapper over the crate's cloaked QUIC
|
|
3
|
+
//! [`quietquic_core::server::Server`], plus a minimal [`NativeConnection`] that
|
|
4
|
+
//! Task 7 extends into the friendly stream-carrying `Connection`.
|
|
5
|
+
//!
|
|
6
|
+
//! ## Native-handle-through-the-bridge pattern (the template for Tasks 6/7)
|
|
7
|
+
//!
|
|
8
|
+
//! The crate's `Server` is a Rust value whose UDP/endpoint driver runs on the
|
|
9
|
+
//! shared tokio runtime (see [`crate::runtime`]). We must (a) hand a handle onto
|
|
10
|
+
//! it back to Ruby, (b) let an async `accept` drive it, and (c) let a sync
|
|
11
|
+
//! `close` stop it — all while the Ruby object is freely shared/`dup`ed.
|
|
12
|
+
//!
|
|
13
|
+
//! Sharing model:
|
|
14
|
+
//! - `NativeServer` wraps `Arc<tokio::sync::Mutex<Option<Server>>>`. The `Arc`
|
|
15
|
+
//! lets the same underlying server be driven by an `accept` future while the
|
|
16
|
+
//! Ruby object is shared; the `Option` lets `close` (and GC `free`) *take* and
|
|
17
|
+
//! drop the crate `Server`, which drops its `JoinHandle` and stops the driver.
|
|
18
|
+
//! - `accept` takes `&mut Server`, and the future must hold the server locked
|
|
19
|
+
//! across the `.await`. We therefore use a **`tokio::sync::Mutex`**, never a
|
|
20
|
+
//! `std::sync::Mutex` — holding a std mutex across `.await` is forbidden (it
|
|
21
|
+
//! would block a runtime worker and can deadlock). Only one `accept` can be
|
|
22
|
+
//! in flight at a time, which matches the crate's single-consumer `accept`.
|
|
23
|
+
//! - `local_addr` is cached as a plain `SocketAddr` at wrap time, so
|
|
24
|
+
//! `local_address` is lock-free and still answers after `close`.
|
|
25
|
+
//! - A parked `accept` holds the async mutex across `server.accept().await`,
|
|
26
|
+
//! which never resolves while no client connects — the normal idle state of an
|
|
27
|
+
//! accept loop. `close` must take that same mutex to drop the server, so it
|
|
28
|
+
//! would block forever (and, under a single-threaded `Async` reactor,
|
|
29
|
+
//! `blocking_lock` freezes the whole Ruby VM). To break this, `NativeServer`
|
|
30
|
+
//! carries a shared `shutdown: Arc<Notify>`: the accept future `select!`s on
|
|
31
|
+
//! it, so `close` can fire `notify_waiters()` FIRST to unpark a parked accept.
|
|
32
|
+
//! The woken accept drops its guard on the tokio worker thread, so `close`'s
|
|
33
|
+
//! subsequent `blocking_lock` acquires promptly instead of deadlocking.
|
|
34
|
+
//! - BUT `notify_waiters()` is EDGE-triggered: it wakes only accepts already
|
|
35
|
+
//! registered on the `Notify` at that instant. With CONCURRENT accepts, one
|
|
36
|
+
//! accept can still be queued on the FIFO async mutex (not yet in `select!`,
|
|
37
|
+
//! not yet registered) when `close` fires; it acquires the guard AFTER the
|
|
38
|
+
//! woken accept releases it, reaches `select!`, registers on the `Notify` too
|
|
39
|
+
//! late (the edge already passed), and parks forever — re-deadlocking `close`.
|
|
40
|
+
//! To close this window, `NativeServer` also carries a LATCHING
|
|
41
|
+
//! `closed: Arc<AtomicBool>`: `close` sets it `true` (Release) BEFORE firing
|
|
42
|
+
//! the notify, and every accept, right after acquiring the async-mutex guard
|
|
43
|
+
//! and BEFORE entering `select!`, checks it (Acquire) and returns
|
|
44
|
+
//! `ConnectionLost` immediately if set — never parking, releasing the guard at
|
|
45
|
+
//! once. The `Notify` still promptly unparks the already-parked accept; the
|
|
46
|
+
//! latch covers any accept that reaches the guard after the edge passed. This
|
|
47
|
+
//! is deadlock-free for 0, 1, or N concurrent parked accepts plus a `close`.
|
|
48
|
+
//!
|
|
49
|
+
//! FFI thread-safety: `spawn_op` runs the future on a tokio worker and yields
|
|
50
|
+
//! **plain Rust data** (`Server` / `Connection`); the Ruby `NativeServer` /
|
|
51
|
+
//! `NativeConnection` object is built by the `to_ruby` converter on the Ruby
|
|
52
|
+
//! thread. No magnus `Value` is ever constructed on a tokio worker thread.
|
|
53
|
+
|
|
54
|
+
use std::net::SocketAddr;
|
|
55
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
56
|
+
use std::sync::Arc;
|
|
57
|
+
|
|
58
|
+
use magnus::{function, method, prelude::*, Error, IntoValue, RModule, Ruby};
|
|
59
|
+
use tokio::sync::Mutex as AsyncMutex;
|
|
60
|
+
use tokio::sync::Notify;
|
|
61
|
+
|
|
62
|
+
use quietquic_core::conn::Connection as CoreConnection;
|
|
63
|
+
use quietquic_core::server::Server as CoreServer;
|
|
64
|
+
|
|
65
|
+
use crate::config::ServerConfigHandle;
|
|
66
|
+
use crate::conn::NativeConnection;
|
|
67
|
+
use crate::errors::{ErrorKind, MappedError};
|
|
68
|
+
use crate::pending::PendingOp;
|
|
69
|
+
|
|
70
|
+
/// Shared, drop-on-close handle onto the crate's running `Server`. `None` once
|
|
71
|
+
/// closed (or never bound). Cloned into the `accept` future so it can drive the
|
|
72
|
+
/// server; taken by `close`/`free` to stop the driver.
|
|
73
|
+
type SharedServer = Arc<AsyncMutex<Option<CoreServer>>>;
|
|
74
|
+
|
|
75
|
+
/// `QuietQUIC::Native::Server` — TypedData wrapper over the crate's `Server`.
|
|
76
|
+
///
|
|
77
|
+
/// `Send` (required for the GC free path and for moving the shared handle into
|
|
78
|
+
/// `accept` futures) because `Arc<tokio::sync::Mutex<Option<Server>>>` is `Send`
|
|
79
|
+
/// (the crate `Server` is `Send`) and `SocketAddr` is `Copy + Send`.
|
|
80
|
+
#[magnus::wrap(class = "QuietQUIC::Native::Server", free_immediately, size)]
|
|
81
|
+
pub(crate) struct NativeServer {
|
|
82
|
+
server: SharedServer,
|
|
83
|
+
/// Cached at bind time so `local_address` needs no lock and survives `close`.
|
|
84
|
+
local_addr: SocketAddr,
|
|
85
|
+
/// Fired by `close` to unpark a parked `accept`. The accept future `select!`s
|
|
86
|
+
/// on `notified()`, so a `close` racing an idle-waiting `accept` wakes it; the
|
|
87
|
+
/// woken accept then releases the async mutex on the tokio worker thread,
|
|
88
|
+
/// letting `close`'s `blocking_lock` proceed instead of deadlocking. Cloned
|
|
89
|
+
/// (as an `Arc`) into the accept future so both sides share one `Notify`.
|
|
90
|
+
shutdown: Arc<Notify>,
|
|
91
|
+
/// Latching shutdown flag. Set `true` by `close` (Release) BEFORE firing
|
|
92
|
+
/// `shutdown.notify_waiters()`; read by every accept (Acquire) after it
|
|
93
|
+
/// acquires the async-mutex guard and BEFORE it enters `select!`. Because
|
|
94
|
+
/// `notify_waiters()` is edge-triggered, an accept that only reaches the
|
|
95
|
+
/// guard AFTER `close`'s notify fired would otherwise register on the
|
|
96
|
+
/// `Notify` too late and park forever; this latch lets such a late accept
|
|
97
|
+
/// see closure and return `ConnectionLost` at once, releasing the guard so
|
|
98
|
+
/// `close`'s `blocking_lock` proceeds. Lock-free, so it is read before (not
|
|
99
|
+
/// across) the `.await` — no `std` lock is ever held across an await.
|
|
100
|
+
closed: Arc<AtomicBool>,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
impl NativeServer {
|
|
104
|
+
/// `QuietQUIC::Native::Server.bind(config) -> PendingOp`.
|
|
105
|
+
///
|
|
106
|
+
/// Spawns the crate's `Server::bind(secrets)` on the shared runtime. The op
|
|
107
|
+
/// yields a `NativeServer` handle — wrapped into the Ruby object by `to_ruby`
|
|
108
|
+
/// ON THE RUBY THREAD. A bind failure (socket bind / TLS setup, an
|
|
109
|
+
/// `io::Error`) maps to `QuietQUIC::ConnectError`.
|
|
110
|
+
fn bind(config: &ServerConfigHandle) -> Result<PendingOp, Error> {
|
|
111
|
+
// Clone the parsed secrets out of the shared Ruby handle: `Server::bind`
|
|
112
|
+
// consumes them by value, and we only hold `&ServerConfigHandle`.
|
|
113
|
+
let secrets = config.0.clone();
|
|
114
|
+
PendingOp::spawn_op(
|
|
115
|
+
async move {
|
|
116
|
+
CoreServer::bind(secrets)
|
|
117
|
+
.await
|
|
118
|
+
.map_err(|e| MappedError::new(ErrorKind::Connect, format!("bind failed: {e}")))
|
|
119
|
+
},
|
|
120
|
+
|server: CoreServer| {
|
|
121
|
+
// Build the Ruby object on the Ruby thread (never off-thread).
|
|
122
|
+
let ruby = Ruby::get().expect("to_ruby runs on the Ruby thread");
|
|
123
|
+
let local_addr = server.local_addr();
|
|
124
|
+
let native = NativeServer {
|
|
125
|
+
server: Arc::new(AsyncMutex::new(Some(server))),
|
|
126
|
+
local_addr,
|
|
127
|
+
shutdown: Arc::new(Notify::new()),
|
|
128
|
+
closed: Arc::new(AtomicBool::new(false)),
|
|
129
|
+
};
|
|
130
|
+
native.into_value_with(&ruby)
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// `NativeServer#accept_op -> PendingOp`.
|
|
136
|
+
///
|
|
137
|
+
/// Spawns `server.accept()`; yields a `NativeConnection`. Holds the server's
|
|
138
|
+
/// `tokio::sync::Mutex` across the `.await` (safe — async mutex), so exactly
|
|
139
|
+
/// one accept drives the server at a time. Maps a closed/lost server (accept
|
|
140
|
+
/// yielding `None`, or the server already closed) to `QuietQUIC::ConnectionLost`.
|
|
141
|
+
///
|
|
142
|
+
/// The accept `.await` parks indefinitely while no client connects — the
|
|
143
|
+
/// normal idle state of an accept loop — and holds the async mutex the whole
|
|
144
|
+
/// time. To let a concurrent `close` shut down an idle server, the future
|
|
145
|
+
/// `select!`s the accept against `shutdown.notified()`: when `close` fires
|
|
146
|
+
/// `notify_waiters()`, the shutdown arm wins, the future returns a mapped
|
|
147
|
+
/// `ConnectionLost`, and the guard is dropped HERE on the tokio worker
|
|
148
|
+
/// thread — unblocking `close`'s lock acquisition.
|
|
149
|
+
fn accept_op(&self) -> Result<PendingOp, Error> {
|
|
150
|
+
let shared = Arc::clone(&self.server);
|
|
151
|
+
let shutdown = Arc::clone(&self.shutdown);
|
|
152
|
+
let closed = Arc::clone(&self.closed);
|
|
153
|
+
PendingOp::spawn_op(
|
|
154
|
+
async move {
|
|
155
|
+
// Lock across the await: this is a tokio async mutex, so parking
|
|
156
|
+
// here yields the worker rather than blocking it.
|
|
157
|
+
let mut guard = shared.lock().await;
|
|
158
|
+
// Latching closed-check, BEFORE the select!. `close` sets `closed`
|
|
159
|
+
// Release-ordered before firing the edge-triggered notify, so any
|
|
160
|
+
// accept that reaches this guard after that edge sees `true` here
|
|
161
|
+
// and returns WITHOUT parking — releasing the guard at once so
|
|
162
|
+
// `close`'s `blocking_lock` proceeds. This is the window that a
|
|
163
|
+
// bare `notify_waiters()` leaves open for a concurrent accept.
|
|
164
|
+
// The AtomicBool is lock-free, so this read is not a lock held
|
|
165
|
+
// across an await.
|
|
166
|
+
if closed.load(Ordering::Acquire) {
|
|
167
|
+
return Err(MappedError::new(ErrorKind::ConnectionLost, "server is closed"));
|
|
168
|
+
}
|
|
169
|
+
let server = guard.as_mut().ok_or_else(|| {
|
|
170
|
+
MappedError::new(ErrorKind::ConnectionLost, "server is closed")
|
|
171
|
+
})?;
|
|
172
|
+
tokio::select! {
|
|
173
|
+
// Bias the accept arm so a connection that is already ready is
|
|
174
|
+
// preferred over a simultaneous shutdown signal; a purely idle
|
|
175
|
+
// accept still parks and yields to the shutdown arm.
|
|
176
|
+
biased;
|
|
177
|
+
res = server.accept() => match res {
|
|
178
|
+
Some(conn) => Ok(conn),
|
|
179
|
+
None => Err(MappedError::new(
|
|
180
|
+
ErrorKind::ConnectionLost,
|
|
181
|
+
"server stopped accepting (driver shut down)",
|
|
182
|
+
)),
|
|
183
|
+
},
|
|
184
|
+
() = shutdown.notified() => Err(MappedError::new(
|
|
185
|
+
ErrorKind::ConnectionLost,
|
|
186
|
+
"server closed while accepting",
|
|
187
|
+
)),
|
|
188
|
+
}
|
|
189
|
+
// `guard` is dropped here, on the tokio worker thread, releasing
|
|
190
|
+
// the async mutex so a waiting `close` can `take()` the server.
|
|
191
|
+
},
|
|
192
|
+
|conn: CoreConnection| {
|
|
193
|
+
let ruby = Ruby::get().expect("to_ruby runs on the Ruby thread");
|
|
194
|
+
NativeConnection::from_core(conn).into_value_with(&ruby)
|
|
195
|
+
},
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// `NativeServer#local_address -> String`. Synchronous, lock-free.
|
|
200
|
+
fn local_address(&self) -> String {
|
|
201
|
+
self.local_addr.to_string()
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/// `NativeServer#close`. Synchronous; drops the crate `Server`, stopping its
|
|
205
|
+
/// driver (which prunes CIDs and releases the socket). Idempotent: a second
|
|
206
|
+
/// close finds `None` and is a no-op.
|
|
207
|
+
///
|
|
208
|
+
/// Order matters, and there are THREE steps.
|
|
209
|
+
///
|
|
210
|
+
/// 1. Set the latching `closed` flag (Release). This must happen BEFORE the
|
|
211
|
+
/// notify so any accept that later acquires the async-mutex guard — even
|
|
212
|
+
/// one still queued FIFO on that mutex when `close` runs — observes
|
|
213
|
+
/// closure (Acquire) and returns immediately instead of parking. This is
|
|
214
|
+
/// the fix for the CONCURRENT-accept deadlock: `notify_waiters()` is
|
|
215
|
+
/// edge-triggered and cannot wake an accept that has not yet registered.
|
|
216
|
+
/// 2. Fire `shutdown.notify_waiters()`. This promptly unparks any accept
|
|
217
|
+
/// ALREADY parked in `select!` (registered on the `Notify`), so it does
|
|
218
|
+
/// not have to wait for step 1's guarantee — it returns and drops its
|
|
219
|
+
/// guard on the tokio worker thread right away.
|
|
220
|
+
/// 3. `blocking_lock` to `take()` + drop the server. If an `accept` is parked
|
|
221
|
+
/// on `server.accept().await` (idle-waiting for a client), it holds the
|
|
222
|
+
/// async mutex, and a bare `blocking_lock` here would block forever — and
|
|
223
|
+
/// under a single-threaded `Async` reactor `blocking_lock` does not
|
|
224
|
+
/// release the GVL, freezing the whole VM. Steps 1+2 guarantee every
|
|
225
|
+
/// parked or about-to-park accept releases the guard promptly (via the
|
|
226
|
+
/// notify if already parked, via the latch if it reaches the guard later),
|
|
227
|
+
/// so this lock acquisition always completes. `blocking_lock` is otherwise
|
|
228
|
+
/// safe: `close` runs on the Ruby thread, never on a runtime worker.
|
|
229
|
+
///
|
|
230
|
+
/// Idempotent + drop-safe: a second `close` re-stores `true` (harmless),
|
|
231
|
+
/// finds `None` (harmless `notify_waiters` on a `Notify` with no waiters is a
|
|
232
|
+
/// no-op, and `take()` yields `None`). Racing a still-in-flight accept cannot
|
|
233
|
+
/// double-drop: whoever holds the guard owns the `Some`, and `take()`
|
|
234
|
+
/// replaces it with `None`, so exactly one path drops the crate `Server`.
|
|
235
|
+
fn close(&self) {
|
|
236
|
+
// 1. Latch closure FIRST so a late/queued accept sees it and never parks.
|
|
237
|
+
self.closed.store(true, Ordering::Release);
|
|
238
|
+
// 2. Unpark any already-parked accept so it releases the async mutex.
|
|
239
|
+
self.shutdown.notify_waiters();
|
|
240
|
+
// 3. Take + drop the server. The lock now resolves promptly.
|
|
241
|
+
let taken = {
|
|
242
|
+
let mut guard = self.server.blocking_lock();
|
|
243
|
+
guard.take()
|
|
244
|
+
};
|
|
245
|
+
drop(taken);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/// Register `Server` and its methods on `QuietQUIC::Native`. The
|
|
250
|
+
/// `Connection`/`Stream` classes are registered by [`crate::conn`].
|
|
251
|
+
pub(crate) fn init(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
252
|
+
let server = native.define_class("Server", ruby.class_object())?;
|
|
253
|
+
server.define_singleton_method("bind", function!(NativeServer::bind, 1))?;
|
|
254
|
+
server.define_method("accept_op", method!(NativeServer::accept_op, 0))?;
|
|
255
|
+
server.define_method("local_address", method!(NativeServer::local_address, 0))?;
|
|
256
|
+
server.define_method("close", method!(NativeServer::close, 0))?;
|
|
257
|
+
|
|
258
|
+
Ok(())
|
|
259
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# SPDX-License-Identifier: 0BSD
|
|
2
|
+
module QuietQUIC
|
|
3
|
+
# Wait for an in-flight native operation (a +PendingOp+) to complete and
|
|
4
|
+
# return its result, raising the mapped +QuietQUIC::*+ exception on failure.
|
|
5
|
+
#
|
|
6
|
+
# Waiting goes through +IO#wait_readable+ on the op's self-pipe fd, which is
|
|
7
|
+
# fiber-scheduler aware (Ruby >= 3.0): under +Async+ it parks the fiber and
|
|
8
|
+
# lets the reactor run other tasks; with no scheduler it blocks while
|
|
9
|
+
# releasing the GVL.
|
|
10
|
+
#
|
|
11
|
+
# +autoclose: false+ is a hard invariant: Rust owns the fd and closes it on
|
|
12
|
+
# disposal, so the Ruby +IO+ must never autoclose it (that would double-close).
|
|
13
|
+
#
|
|
14
|
+
# The op is explicitly disposed in an +ensure+ so its pipe fds are freed at
|
|
15
|
+
# await-completion rather than whenever the GC eventually runs the finalizer.
|
|
16
|
+
def self.await(op)
|
|
17
|
+
IO.for_fd(op.fd, autoclose: false).wait_readable
|
|
18
|
+
op.take_result
|
|
19
|
+
ensure
|
|
20
|
+
op.close
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# SPDX-License-Identifier: 0BSD
|
|
2
|
+
module QuietQUIC
|
|
3
|
+
# Dial a cloaked QUIC server.
|
|
4
|
+
#
|
|
5
|
+
# Build a connection with {.connect} (from a client id + PSK + server
|
|
6
|
+
# address) or {.connect_toml} (from a client config file). Both parse and
|
|
7
|
+
# validate the config synchronously (raising {QuietQUIC::ConfigError} on a
|
|
8
|
+
# bad address or PSK), then +await+ the native connect on the shared
|
|
9
|
+
# runtime.
|
|
10
|
+
#
|
|
11
|
+
# The underlying crate connect has its own internal handshake timeout
|
|
12
|
+
# (about 10 seconds); a server that never responds surfaces as
|
|
13
|
+
# {QuietQUIC::ConnectError}, same as any other failed dial.
|
|
14
|
+
class Client
|
|
15
|
+
# Dial a server from this client's identity, PSK, and the server address.
|
|
16
|
+
#
|
|
17
|
+
# @param client_id [String] this client's id (must be authorized by the
|
|
18
|
+
# server).
|
|
19
|
+
# @param psk [String] the shared PSK, 64 hex chars (32 bytes).
|
|
20
|
+
# @param server [String] the server's +"ip:port"+ to dial.
|
|
21
|
+
# @return [Connection]
|
|
22
|
+
# @raise [QuietQUIC::ConfigError] on a bad address or PSK.
|
|
23
|
+
# @raise [QuietQUIC::ConnectError] if the connect fails or times out.
|
|
24
|
+
def self.connect(client_id:, psk:, server:)
|
|
25
|
+
handle = Native.client_config_from_parts(client_id, psk, server)
|
|
26
|
+
Connection.new(QuietQUIC.await(Native::Client.connect(handle)))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Dial a server using a client config TOML file.
|
|
30
|
+
#
|
|
31
|
+
# @param path [String] path to the client config TOML.
|
|
32
|
+
# @return [Connection]
|
|
33
|
+
# @raise [QuietQUIC::ConfigError] if the file is missing or invalid.
|
|
34
|
+
# @raise [QuietQUIC::ConnectError] if the connect fails or times out.
|
|
35
|
+
def self.connect_toml(path)
|
|
36
|
+
handle = Native.client_config_from_toml(path)
|
|
37
|
+
Connection.new(QuietQUIC.await(Native::Client.connect(handle)))
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# SPDX-License-Identifier: 0BSD
|
|
2
|
+
module QuietQUIC
|
|
3
|
+
# A single post-handshake, PSK-authenticated QUIC connection.
|
|
4
|
+
#
|
|
5
|
+
# Produced identically by {Server#accept} and {Client.connect} (both sides
|
|
6
|
+
# surface the same type). Carries bidirectional {Stream}s: {#open_stream}
|
|
7
|
+
# dials a new stream, {#accept_stream} awaits the next peer-opened one. Both
|
|
8
|
+
# park the fiber (under an +Async+ reactor) until they resolve.
|
|
9
|
+
#
|
|
10
|
+
# Dropping a +Connection+ does not tear the connection down; call {#close} to
|
|
11
|
+
# send a CONNECTION_CLOSE so the peer tears down promptly rather than idling
|
|
12
|
+
# out.
|
|
13
|
+
class Connection
|
|
14
|
+
# @param native [Native::Connection] the wrapped crate connection.
|
|
15
|
+
def initialize(native)
|
|
16
|
+
@native = native
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Open a new bidirectional {Stream}.
|
|
20
|
+
#
|
|
21
|
+
# @return [Stream]
|
|
22
|
+
# @raise [QuietQUIC::ConnectionLost] if the connection is closed or lost.
|
|
23
|
+
# @raise [QuietQUIC::StreamError] if the transport refuses the stream.
|
|
24
|
+
def open_stream
|
|
25
|
+
Stream.new(QuietQUIC.await(@native.open_stream_op))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Await and return the next bidirectional {Stream} the peer opens.
|
|
29
|
+
#
|
|
30
|
+
# Blocks until the peer initiates a stream. Under an +Async+ reactor it
|
|
31
|
+
# parks the fiber; otherwise it blocks with the GVL released.
|
|
32
|
+
#
|
|
33
|
+
# @return [Stream]
|
|
34
|
+
# @raise [QuietQUIC::ConnectionLost] if the connection is closed or lost
|
|
35
|
+
# while awaiting.
|
|
36
|
+
def accept_stream
|
|
37
|
+
Stream.new(QuietQUIC.await(@native.accept_stream_op))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The remote peer's address (e.g. +"127.0.0.1:54321"+).
|
|
41
|
+
#
|
|
42
|
+
# @return [String]
|
|
43
|
+
def remote_address
|
|
44
|
+
@native.remote_address
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Close the connection, sending a CONNECTION_CLOSE frame so the peer tears
|
|
48
|
+
# down promptly. Best-effort and idempotent.
|
|
49
|
+
#
|
|
50
|
+
# @return [void]
|
|
51
|
+
def close
|
|
52
|
+
@native.close
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# SPDX-License-Identifier: 0BSD
|
|
2
|
+
module QuietQUIC
|
|
3
|
+
# A running cloaked QUIC server.
|
|
4
|
+
#
|
|
5
|
+
# Build one with {.bind} (from a listen address + a +{client_id => psk}+ map)
|
|
6
|
+
# or {.bind_toml} (from a secrets file). Both parse and validate the config
|
|
7
|
+
# synchronously (raising {QuietQUIC::ConfigError} on a bad address or PSK),
|
|
8
|
+
# then +await+ the native bind on the shared runtime.
|
|
9
|
+
#
|
|
10
|
+
# {#accept} yields the next authenticated {Connection}; it blocks (parking the
|
|
11
|
+
# fiber under an +Async+ reactor) until a peer completes the cloaked handshake.
|
|
12
|
+
class Server
|
|
13
|
+
# Bind a server from a listen address and an authorized-clients map.
|
|
14
|
+
#
|
|
15
|
+
# @param listen [String] the +"ip:port"+ to bind (use port +0+ for an
|
|
16
|
+
# ephemeral port, then read {#local_address}).
|
|
17
|
+
# @param clients [Hash{String=>String}] +client_id => psk_hex+ (64 hex chars).
|
|
18
|
+
# @return [Server]
|
|
19
|
+
# @raise [QuietQUIC::ConfigError] on a bad address or PSK.
|
|
20
|
+
# @raise [QuietQUIC::ConnectError] if the socket cannot be bound.
|
|
21
|
+
def self.bind(listen:, clients:)
|
|
22
|
+
handle = Native.server_config_from_hash(listen, clients)
|
|
23
|
+
new(QuietQUIC.await(Native::Server.bind(handle)))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Bind a server from a TOML secrets file.
|
|
27
|
+
#
|
|
28
|
+
# @param path [String] path to the server secrets TOML.
|
|
29
|
+
# @return [Server]
|
|
30
|
+
# @raise [QuietQUIC::ConfigError] if the file is missing or invalid.
|
|
31
|
+
# @raise [QuietQUIC::ConnectError] if the socket cannot be bound.
|
|
32
|
+
def self.bind_toml(path)
|
|
33
|
+
handle = Native.server_config_from_toml(path)
|
|
34
|
+
new(QuietQUIC.await(Native::Server.bind(handle)))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @param native [Native::Server] the wrapped crate server.
|
|
38
|
+
def initialize(native)
|
|
39
|
+
@native = native
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Await and return the next authenticated {Connection}.
|
|
43
|
+
#
|
|
44
|
+
# Blocks until a peer completes the cloaked handshake. Under an +Async+
|
|
45
|
+
# reactor it parks the fiber; otherwise it blocks with the GVL released.
|
|
46
|
+
#
|
|
47
|
+
# @return [Connection]
|
|
48
|
+
# @raise [QuietQUIC::ConnectionLost] if the server has been closed or its
|
|
49
|
+
# driver has shut down.
|
|
50
|
+
def accept
|
|
51
|
+
Connection.new(QuietQUIC.await(@native.accept_op))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The address the server is actually listening on (e.g. +"127.0.0.1:54321"+).
|
|
55
|
+
# After binding to port +0+ this reports the kernel-chosen port.
|
|
56
|
+
#
|
|
57
|
+
# @return [String]
|
|
58
|
+
def local_address
|
|
59
|
+
@native.local_address
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Stop the server: drop the crate server, halting its driver and releasing
|
|
63
|
+
# the socket. Idempotent.
|
|
64
|
+
#
|
|
65
|
+
# @return [void]
|
|
66
|
+
def close
|
|
67
|
+
@native.close
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# SPDX-License-Identifier: 0BSD
|
|
2
|
+
module QuietQUIC
|
|
3
|
+
# A bidirectional QUIC stream on a {Connection}.
|
|
4
|
+
#
|
|
5
|
+
# Obtained from {Connection#open_stream} or {Connection#accept_stream}. Write
|
|
6
|
+
# bytes with {#write_all}, signal end-of-data with {#finish}, and read the
|
|
7
|
+
# peer's whole reply with {#read_to_end}. Each operation parks the fiber
|
|
8
|
+
# (under an +Async+ reactor) until it resolves.
|
|
9
|
+
#
|
|
10
|
+
# @note The native alpha.3 API has split send/receive halves. This Ruby class
|
|
11
|
+
# keeps one facade around those halves: send operations serialize with send
|
|
12
|
+
# operations, receive operations serialize with receive operations, and both
|
|
13
|
+
# still park through the same scheduler-aware bridge.
|
|
14
|
+
class Stream
|
|
15
|
+
# Default upper bound for {#read_to_end}, chosen to keep the convenience
|
|
16
|
+
# method safe against an authenticated peer sending an unbounded response.
|
|
17
|
+
DEFAULT_READ_LIMIT = 1024 * 1024
|
|
18
|
+
DEFAULT_FINISH_TIMEOUT = 10
|
|
19
|
+
|
|
20
|
+
# @param native [Native::Stream] the wrapped crate stream.
|
|
21
|
+
def initialize(native)
|
|
22
|
+
@native = native
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Write all of +bytes+ to the stream, waiting out flow-control
|
|
26
|
+
# back-pressure. The bytes are sent verbatim: +bytes+ is read as a raw byte
|
|
27
|
+
# sequence regardless of its String encoding.
|
|
28
|
+
#
|
|
29
|
+
# @param bytes [String] the bytes to send.
|
|
30
|
+
# @return [void]
|
|
31
|
+
# @raise [QuietQUIC::ConnectionLost] if the connection is closed or lost.
|
|
32
|
+
# @raise [QuietQUIC::StreamError] if the stream is reset or stopped.
|
|
33
|
+
def write_all(bytes)
|
|
34
|
+
QuietQUIC.await(@native.write_all_op(bytes))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Finish (send FIN on) the stream, signalling end-of-data to the peer.
|
|
38
|
+
#
|
|
39
|
+
# @return [void]
|
|
40
|
+
# @raise [QuietQUIC::ConnectionLost] if the connection is closed or lost.
|
|
41
|
+
# @raise [QuietQUIC::StreamError] if the stream is reset or stopped.
|
|
42
|
+
def finish
|
|
43
|
+
QuietQUIC.await(@native.finish_op)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Finish the stream and wait for the peer to acknowledge its FIN.
|
|
47
|
+
#
|
|
48
|
+
# Use this before immediately closing the connection when delivery of the
|
|
49
|
+
# final stream matters. A plain {#finish} only queues FIN locally.
|
|
50
|
+
#
|
|
51
|
+
# @param timeout [Numeric] maximum seconds to wait.
|
|
52
|
+
# @return [void]
|
|
53
|
+
def finish_and_wait(timeout: DEFAULT_FINISH_TIMEOUT)
|
|
54
|
+
unless timeout.is_a?(Numeric) && timeout.finite? && timeout.positive?
|
|
55
|
+
raise ArgumentError, "timeout must be a positive finite number"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
QuietQUIC.await(@native.finish_and_wait_op(timeout.to_f))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Read the stream to end-of-stream, returning up to +limit+ bytes.
|
|
62
|
+
#
|
|
63
|
+
# Parks until the peer sends FIN. The result is a +String+ with
|
|
64
|
+
# +Encoding::BINARY+ (ASCII-8BIT): exact bytes, never UTF-8-validated.
|
|
65
|
+
#
|
|
66
|
+
# @note This parks until the peer sends FIN. Avoid issuing multiple
|
|
67
|
+
# concurrent reads on the same Stream; they share one receive half.
|
|
68
|
+
# @param limit [Integer] maximum response size in bytes.
|
|
69
|
+
# @return [String] the received bytes, BINARY-encoded.
|
|
70
|
+
# @raise [QuietQUIC::ConnectionLost] if the connection is closed or lost.
|
|
71
|
+
# @raise [QuietQUIC::StreamError] if the stream is reset before FIN or
|
|
72
|
+
# exceeds +limit+.
|
|
73
|
+
def read_to_end(limit: DEFAULT_READ_LIMIT)
|
|
74
|
+
raise ArgumentError, "limit must be a non-negative Integer" unless limit.is_a?(Integer) && limit >= 0
|
|
75
|
+
|
|
76
|
+
QuietQUIC.await(@native.read_to_end_op(limit))
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
data/lib/quietquic.rb
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# SPDX-License-Identifier: 0BSD
|
|
2
|
+
require_relative "quietquic/version"
|
|
3
|
+
require "quietquic/quietquic" # the compiled extension
|
|
4
|
+
require_relative "quietquic/await"
|
|
5
|
+
require_relative "quietquic/stream"
|
|
6
|
+
require_relative "quietquic/connection"
|
|
7
|
+
require_relative "quietquic/server"
|
|
8
|
+
require_relative "quietquic/client"
|
metadata
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: quietquic
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0.alpha.3
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- quietquic contributors
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rb_sys
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.9'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.9'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake-compiler
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.2'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.2'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rspec
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.13'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.13'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: async
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '2.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '2.0'
|
|
68
|
+
description: Experimental alpha Ruby bindings (native extension) for the quietquic
|
|
69
|
+
Rust crate.
|
|
70
|
+
executables: []
|
|
71
|
+
extensions:
|
|
72
|
+
- ext/quietquic/extconf.rb
|
|
73
|
+
extra_rdoc_files: []
|
|
74
|
+
files:
|
|
75
|
+
- HISTORY.md
|
|
76
|
+
- LICENSE
|
|
77
|
+
- README.md
|
|
78
|
+
- examples/cooperative_async.rb
|
|
79
|
+
- examples/echo_client.rb
|
|
80
|
+
- examples/echo_server.rb
|
|
81
|
+
- ext/quietquic/Cargo.lock
|
|
82
|
+
- ext/quietquic/Cargo.toml
|
|
83
|
+
- ext/quietquic/extconf.rb
|
|
84
|
+
- ext/quietquic/src/client.rs
|
|
85
|
+
- ext/quietquic/src/config.rs
|
|
86
|
+
- ext/quietquic/src/conn.rs
|
|
87
|
+
- ext/quietquic/src/errors.rs
|
|
88
|
+
- ext/quietquic/src/lib.rs
|
|
89
|
+
- ext/quietquic/src/pending.rs
|
|
90
|
+
- ext/quietquic/src/runtime.rs
|
|
91
|
+
- ext/quietquic/src/server.rs
|
|
92
|
+
- lib/quietquic.rb
|
|
93
|
+
- lib/quietquic/await.rb
|
|
94
|
+
- lib/quietquic/client.rb
|
|
95
|
+
- lib/quietquic/connection.rb
|
|
96
|
+
- lib/quietquic/server.rb
|
|
97
|
+
- lib/quietquic/stream.rb
|
|
98
|
+
- lib/quietquic/version.rb
|
|
99
|
+
homepage: https://github.com/astounding/quietquicrb
|
|
100
|
+
licenses:
|
|
101
|
+
- 0BSD
|
|
102
|
+
metadata:
|
|
103
|
+
source_code_uri: https://github.com/astounding/quietquicrb
|
|
104
|
+
changelog_uri: https://github.com/astounding/quietquicrb/blob/main/HISTORY.md
|
|
105
|
+
documentation_uri: https://github.com/astounding/quietquicrb#readme
|
|
106
|
+
rubygems_mfa_required: 'true'
|
|
107
|
+
rdoc_options: []
|
|
108
|
+
require_paths:
|
|
109
|
+
- lib
|
|
110
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
111
|
+
requirements:
|
|
112
|
+
- - ">="
|
|
113
|
+
- !ruby/object:Gem::Version
|
|
114
|
+
version: '3.1'
|
|
115
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
116
|
+
requirements:
|
|
117
|
+
- - ">="
|
|
118
|
+
- !ruby/object:Gem::Version
|
|
119
|
+
version: '0'
|
|
120
|
+
requirements: []
|
|
121
|
+
rubygems_version: 4.0.15
|
|
122
|
+
specification_version: 4
|
|
123
|
+
summary: 'Cloaked QUIC transport: scanner-invisible, PSK-authenticated, camouflaged
|
|
124
|
+
as vanilla QUIC.'
|
|
125
|
+
test_files: []
|