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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e2279831479628d7d0abcc6c8781589309dfb594ed6ea8181b6ec7456b31136e
|
|
4
|
+
data.tar.gz: 2e1c4b1100fb01b72378d4f9515e0983c0a1e365470e27504ac8a7d0fa75cf8c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5d448e3a389baca997afaeb13eec314889c47b3b7b9ba170af269d61789817ce2ef5d6c85b7627ebc41d7758d75fc3deba4a9dd5a7fccfe6feafad5338c4e09d
|
|
7
|
+
data.tar.gz: 5c110b414b7a24cbe5a4c96b188dcfae6a212be88a308088b60d1f2e87553cdbfc1ccc7cb1cc3fc8293fc38d4d155c26535ad38db391e6f853cdf0d988e017e7
|
data/HISTORY.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# History and durable decisions
|
|
2
|
+
|
|
3
|
+
This repository begins with a clean snapshot split from the original
|
|
4
|
+
QuietQUIC monorepo in July 2026. The mother repository preserves detailed Git
|
|
5
|
+
history; this document records the decisions that should guide maintenance.
|
|
6
|
+
The design and plan under `docs/` are historical records and retain some
|
|
7
|
+
original monorepo path references.
|
|
8
|
+
|
|
9
|
+
## 0.1.0.alpha.3 — unreleased
|
|
10
|
+
|
|
11
|
+
First public experimental alpha of the Ruby binding, aligned with
|
|
12
|
+
`quietquic` and `quietquic-proto` 0.1.0-alpha.3. This release is not production
|
|
13
|
+
quality. It provides the plain blocking/GVL-releasing Ruby interface and
|
|
14
|
+
cooperates with a Ruby `Fiber.scheduler` such as the `async` gem while native
|
|
15
|
+
network work runs on the binding's Tokio runtime.
|
|
16
|
+
|
|
17
|
+
The native extension uses the published crates.io packages
|
|
18
|
+
`quietquic` `0.1.0-alpha.3` and `quietquic-proto` `0.1.0-alpha.3`, built from
|
|
19
|
+
upstream QuietQUIC commit
|
|
20
|
+
[`d9580ba`](https://github.com/astounding/quietquic/commit/d9580bae624ca4edd536144956aaceae8699b9f6).
|
|
21
|
+
|
|
22
|
+
Stream reads are bounded: `Stream#read_to_end(limit:)` defaults to 1 MiB and
|
|
23
|
+
raises `QuietQUIC::StreamError` if an authenticated peer exceeds the limit.
|
|
24
|
+
`Stream#finish_and_wait(timeout:)` sends FIN and waits for the peer's QUIC
|
|
25
|
+
transport to acknowledge it; this proves transport receipt, not application
|
|
26
|
+
processing.
|
|
27
|
+
|
|
28
|
+
## Native bridge
|
|
29
|
+
|
|
30
|
+
The gem uses Magnus/rb-sys and a Rust native extension around the Tokio
|
|
31
|
+
QuietQUIC crate. A process-global Tokio runtime performs asynchronous work.
|
|
32
|
+
Each operation stores a Rust result and signals a pipe file descriptor; Ruby
|
|
33
|
+
waits with `IO#wait_readable`, which cooperates with a Fiber scheduler and
|
|
34
|
+
releases the GVL in blocking use.
|
|
35
|
+
|
|
36
|
+
Ruby `Value` objects must only be created on the Ruby VM thread. Producers
|
|
37
|
+
spawned on Tokio return Rust-owned, `Send` data, and conversion happens after
|
|
38
|
+
the waiting Ruby thread resumes.
|
|
39
|
+
|
|
40
|
+
The result slot and wake pipe form a happens-before boundary. Wake and dispose
|
|
41
|
+
must remain mutually exclusive to prevent writes to a recycled file descriptor.
|
|
42
|
+
A panic in a spawned operation is caught and converted to
|
|
43
|
+
`QuietQUIC::Error`; otherwise the waiting fiber could hang forever.
|
|
44
|
+
|
|
45
|
+
## Lifecycle corrections
|
|
46
|
+
|
|
47
|
+
`Server#close` must wake every accept operation already parked. The shutdown
|
|
48
|
+
state is latched before notification and checked while installing a waiter so
|
|
49
|
+
zero, one, or many concurrent accepts terminate without a missed-wakeup race.
|
|
50
|
+
|
|
51
|
+
The runtime is process-owned and unsafe to reuse after `fork(2)`. The gem
|
|
52
|
+
detects PID changes and raises a clear error; applications such as Puma or
|
|
53
|
+
Unicorn must initialize the runtime in the worker process.
|
|
54
|
+
|
|
55
|
+
Connection cleanup also depends on the Rust transport reaping locally drained
|
|
56
|
+
connections, not solely `ConnectionLost`. That core correction prevented
|
|
57
|
+
handle reuse from wedging repeated server lifecycles.
|
|
58
|
+
|
|
59
|
+
## Stream model
|
|
60
|
+
|
|
61
|
+
The Rust alpha.3 API split bidirectional streams into send and receive halves.
|
|
62
|
+
Ruby keeps one `Stream` facade around those halves: send operations serialize
|
|
63
|
+
with send operations, receive operations serialize with receive operations, and
|
|
64
|
+
`read_to_end(limit:)` remains bounded to prevent an authenticated peer from
|
|
65
|
+
growing receiver memory without limit.
|
|
66
|
+
|
|
67
|
+
## Packaging decision
|
|
68
|
+
|
|
69
|
+
The source gem does not vendor the Rust QuietQUIC implementation. The native
|
|
70
|
+
extension depends on an exact published `quietquic` crate version from
|
|
71
|
+
crates.io, and that crate selects the matching published `quietquic-proto`
|
|
72
|
+
version. This keeps the Ruby gem small and makes release provenance explicit,
|
|
73
|
+
at the cost of requiring Cargo registry access during native-extension builds.
|
|
74
|
+
|
|
75
|
+
## Platform record
|
|
76
|
+
|
|
77
|
+
The original combined tree was exercised on macOS, Linux/musl, and FreeBSD.
|
|
78
|
+
FreeBSD native builds require GNU make and LLVM/libclang in addition to Ruby,
|
|
79
|
+
Rust, and CMake. CI retains those prerequisites.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Copyright (C) 2026 by the quietquic contributors
|
|
2
|
+
|
|
3
|
+
Permission to use, copy, modify, and/or distribute this software for
|
|
4
|
+
any purpose with or without fee is hereby granted.
|
|
5
|
+
|
|
6
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
|
7
|
+
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
|
|
8
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
|
|
9
|
+
AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
|
|
10
|
+
DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
|
|
11
|
+
OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
|
12
|
+
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
13
|
+
PERFORMANCE OF THIS SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: 0BSD -->
|
|
2
|
+
# quietquic (Ruby bindings)
|
|
3
|
+
|
|
4
|
+
**DISCLAIMER:** This was 100% AI coded, prompted by the human "creator" but
|
|
5
|
+
planned and designed by multiple AI models including those from Anthropic,
|
|
6
|
+
Google, and OpenAI. Trust at your own peril.
|
|
7
|
+
|
|
8
|
+
Ruby bindings for the independent `quietquic` Rust project, a cloaked QUIC transport:
|
|
9
|
+
scanner-invisible (silent to any peer that doesn't prove PSK possession) and
|
|
10
|
+
camouflaged as ordinary QUIC v1/HTTP3 traffic on the wire. This gem is a
|
|
11
|
+
**thin native-extension wrapper** (Magnus + rb-sys) around the Rust
|
|
12
|
+
`quietquic` crate's public API — it adds no transport or cryptographic logic
|
|
13
|
+
of its own, only a Ruby-friendly client/server surface with first-class
|
|
14
|
+
`async`-gem cooperation.
|
|
15
|
+
|
|
16
|
+
> **Release status:** `0.1.0.alpha.3` is an experimental preview matching the
|
|
17
|
+
> `quietquic` and `quietquic-proto` 0.1.0-alpha.3 Rust components. It is **not
|
|
18
|
+
> production quality** and has not received an independent security audit.
|
|
19
|
+
|
|
20
|
+
See the [Rust crate's README](https://github.com/astounding/quietquic) for the full **threat model**
|
|
21
|
+
(what quietquic defeats and does not defeat) — read it before deploying;
|
|
22
|
+
nothing here repeats it. The design rationale for this gem specifically is in
|
|
23
|
+
[`docs/specs/2026-07-04-quietquic-ruby-design.md`](docs/specs/2026-07-04-quietquic-ruby-design.md).
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
The gem builds a native Rust extension at install time, so an installing
|
|
30
|
+
machine needs a **Rust toolchain** (stable, via `rustup` or a system package)
|
|
31
|
+
in addition to Ruby.
|
|
32
|
+
|
|
33
|
+
### From a checkout
|
|
34
|
+
|
|
35
|
+
Add this gem to a `Gemfile` pointing at this checkout (path source) or at the
|
|
36
|
+
repository directly (git source):
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
# Path source (e.g. this repo checked out alongside your app):
|
|
40
|
+
gem "quietquic", path: "../quietquicrb"
|
|
41
|
+
|
|
42
|
+
# ...or, after the repository is hosted, its git URL:
|
|
43
|
+
gem "quietquic", git: "https://github.com/astounding/quietquicrb.git"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Then:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
bundle install
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Bundler resolves `rb_sys`/`rake-compiler` (declared in the gemspec) and drives
|
|
53
|
+
`extconf.rb`, which compiles the bundled Rust extension crate via `cargo`.
|
|
54
|
+
The extension depends on the exact published `quietquic` `0.1.0-alpha.3`
|
|
55
|
+
crate from crates.io; it does not use a local QuietQUIC checkout or vendored
|
|
56
|
+
Rust source. This is the supported, tested install path — it's exactly what
|
|
57
|
+
this repo's own test suite and CI
|
|
58
|
+
(`.github/workflows/ci.yml`, covering Linux, macOS, and
|
|
59
|
+
FreeBSD) exercise.
|
|
60
|
+
|
|
61
|
+
### Standalone `gem install` — status
|
|
62
|
+
|
|
63
|
+
A built `.gem` (`rake build` / `gem build quietquic.gemspec`) packages only
|
|
64
|
+
the Ruby binding and its native extension sources. During installation, Cargo
|
|
65
|
+
downloads the published Rust crates from crates.io. `rb_sys` is declared as a
|
|
66
|
+
runtime dependency precisely so a plain `gem install quietquic` can resolve
|
|
67
|
+
everything it needs to compile the extension. What we have **not** set up yet
|
|
68
|
+
is publishing this gem to a public gem source (e.g. rubygems.org) — until
|
|
69
|
+
then, `gem install quietquic` "from the internet" has nothing to fetch. If
|
|
70
|
+
you have a built `.gem` file in hand (or a private gem server hosting it),
|
|
71
|
+
installing it directly works today; the Bundler path/git source above remains
|
|
72
|
+
the recommended flow for working from this repository.
|
|
73
|
+
|
|
74
|
+
### Platforms & prerequisites
|
|
75
|
+
|
|
76
|
+
- **Ruby 3.1+** (MRI). `Fiber.scheduler` / the `async` gem need a reasonably
|
|
77
|
+
modern Ruby; older Rubies are not supported.
|
|
78
|
+
- **macOS, Linux, FreeBSD.**
|
|
79
|
+
- **Rust 1.88+** reachable as `cargo`/`rustc` (matching the Rust crates'
|
|
80
|
+
minimum supported toolchain).
|
|
81
|
+
- **FreeBSD** needs a few extra system packages beyond the default toolchain,
|
|
82
|
+
because `rb_sys` generates a GNU Makefile (the base BSD `make` can't run it)
|
|
83
|
+
and its `bindgen` build script needs `libclang`:
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
pkg install -y ruby devel/ruby-gems rust cmake gmake llvm
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
and these environment variables set for the build:
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
export MAKE=gmake
|
|
93
|
+
export LIBCLANG_PATH="$(ls -d /usr/local/llvm*/lib | tail -1)"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
(`gmake` because `rb_sys`/rake-compiler emit a GNU-style Makefile; `llvm`
|
|
97
|
+
supplies `libclang.so` for rb-sys's `bindgen` step.) This is exactly what
|
|
98
|
+
`.github/workflows/ci.yml`'s `freebsd` job does.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Quickstart
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
require "quietquic"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Server
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
srv = QuietQUIC::Server.bind(
|
|
112
|
+
listen: "0.0.0.0:4433",
|
|
113
|
+
clients: { "laptop" => "aabb…(64 hex chars)" } # client_id => psk_hex
|
|
114
|
+
)
|
|
115
|
+
puts srv.local_address # => "0.0.0.0:4433" (the kernel-chosen port if you bound :0)
|
|
116
|
+
|
|
117
|
+
loop do
|
|
118
|
+
conn = srv.accept # parks the fiber under Async; blocks (GVL-released) otherwise
|
|
119
|
+
Thread.new do
|
|
120
|
+
stream = conn.accept_stream
|
|
121
|
+
data = stream.read_to_end
|
|
122
|
+
puts "got #{data.bytesize} bytes from #{conn.remote_address}"
|
|
123
|
+
|
|
124
|
+
# Reply on a second stream.
|
|
125
|
+
reply = conn.open_stream
|
|
126
|
+
reply.write_all("thanks: #{data}")
|
|
127
|
+
reply.finish_and_wait
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Or from a TOML secrets file (reuses the crate's own loader, including its
|
|
133
|
+
`chmod 600` group/world-readable warning):
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
srv = QuietQUIC::Server.bind_toml("server.toml")
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Client
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
conn = QuietQUIC::Client.connect(
|
|
143
|
+
client_id: "laptop",
|
|
144
|
+
psk: "aabb…(64 hex chars)",
|
|
145
|
+
server: "example.com:4433"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
out = conn.open_stream
|
|
149
|
+
out.write_all("hello over a cloaked pipe")
|
|
150
|
+
out.finish
|
|
151
|
+
|
|
152
|
+
reply_stream = conn.accept_stream
|
|
153
|
+
puts reply_stream.read_to_end
|
|
154
|
+
|
|
155
|
+
conn.close
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Or: `conn = QuietQUIC::Client.connect_toml("client.toml")`.
|
|
159
|
+
|
|
160
|
+
That's the whole alpha surface: `Server.bind`/`.bind_toml`/`#accept`/`#local_address`/`#close`,
|
|
161
|
+
`Client.connect`/`.connect_toml`, and `Connection#open_stream`/`#accept_stream`/`#remote_address`/`#close`,
|
|
162
|
+
`Stream#write_all`/`#finish`/`#finish_and_wait`/`#read_to_end`. Bytes in and out are binary
|
|
163
|
+
`String`s (`Encoding::BINARY`); PSKs are 64-hex-character `String`s.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Async cooperation
|
|
168
|
+
|
|
169
|
+
Every method that can wait (`Server#accept`, `Client.connect`/`.connect_toml`,
|
|
170
|
+
`Connection#open_stream`/`#accept_stream`, `Stream#write_all`/`#finish`/
|
|
171
|
+
`#finish_and_wait`/`#read_to_end`) goes through the same bridge:
|
|
172
|
+
|
|
173
|
+
- **Inside `Async do … end`** (the [`async`](https://github.com/socketry/async)
|
|
174
|
+
gem), the waiting call parks the current fiber via `Fiber.scheduler`'s
|
|
175
|
+
`io_wait` hook, so the reactor runs other tasks on the same thread while it
|
|
176
|
+
waits. Two concurrent connections/streams inside one `Async` block genuinely
|
|
177
|
+
interleave rather than serialize.
|
|
178
|
+
- **With no scheduler installed**, the same call blocks the calling thread
|
|
179
|
+
while releasing the GVL, so plain scripts work unmodified and other Ruby
|
|
180
|
+
`Thread`s keep making progress.
|
|
181
|
+
|
|
182
|
+
`async` is an optional peer dependency — this gem works with or without it
|
|
183
|
+
installed; it only changes behavior if a `Fiber.scheduler` is actually active
|
|
184
|
+
when you call in. Applications that want this mode must add `gem "async"` to
|
|
185
|
+
their Gemfile (or install it separately); it is not a runtime dependency of
|
|
186
|
+
`quietquic`.
|
|
187
|
+
|
|
188
|
+
### Two execution models
|
|
189
|
+
|
|
190
|
+
The ordinary two-terminal examples require no Ruby event-loop library:
|
|
191
|
+
|
|
192
|
+
```sh
|
|
193
|
+
ruby examples/echo_server.rb
|
|
194
|
+
ruby examples/echo_client.rb
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
QuietQUIC performs their network operations on its native process-global Tokio
|
|
198
|
+
runtime. Ruby waits through the native self-pipe bridge; without a fiber
|
|
199
|
+
scheduler the calling Ruby thread blocks with the GVL released.
|
|
200
|
+
|
|
201
|
+
The cooperative example keeps the Ruby application on one event-loop thread:
|
|
202
|
+
|
|
203
|
+
```sh
|
|
204
|
+
gem install async
|
|
205
|
+
ruby examples/cooperative_async.rb
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
It deliberately parks a server fiber in `accept`, runs a visible ticker on the
|
|
209
|
+
same Ruby thread, and then completes an echo round trip. “Single-threaded”
|
|
210
|
+
describes the Ruby application side: native Tokio worker threads still perform
|
|
211
|
+
the transport I/O. The example does not use `Thread.new`.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Fork safety
|
|
216
|
+
|
|
217
|
+
This gem runs async operations on a process-global multi-thread tokio runtime.
|
|
218
|
+
That runtime's worker threads **do not survive `fork(2)`** — only the forking
|
|
219
|
+
thread is duplicated into the child. So a pre-forking Ruby server
|
|
220
|
+
(Puma/Unicorn, or Spring) that touches the runtime in the **parent** and then
|
|
221
|
+
forks leaves each worker child with a runtime whose worker threads are gone;
|
|
222
|
+
the child's first await would otherwise hang forever.
|
|
223
|
+
|
|
224
|
+
To make that a loud, actionable failure instead of a silent hang, the gem
|
|
225
|
+
records the PID at runtime initialization and checks it before every async op.
|
|
226
|
+
A call from a process that inherited an already-initialized runtime raises:
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
QuietQUIC::Error: quietquic's async runtime does not survive fork();
|
|
230
|
+
initialize it after forking (e.g. in a Puma on_worker_boot / after_fork hook)
|
|
231
|
+
rather than before
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Workaround: initialize (and use) quietquic only *after* forking.** Don't
|
|
235
|
+
open connections/servers in the parent; do it in the worker-boot hook so each
|
|
236
|
+
child builds its own runtime:
|
|
237
|
+
|
|
238
|
+
```ruby
|
|
239
|
+
# config/puma.rb
|
|
240
|
+
on_worker_boot do
|
|
241
|
+
# First quietquic use in THIS child initializes its own runtime.
|
|
242
|
+
$qq_conn = QuietQUIC::Client.connect(client_id: "...", psk: "...", server: "...")
|
|
243
|
+
end
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
(Unicorn: `after_fork`. Spring: re-establish inside the forked worker.) A
|
|
247
|
+
parent that never touches quietquic before forking is fine — the child
|
|
248
|
+
initializes the runtime in its own process on first use.
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Streams
|
|
253
|
+
|
|
254
|
+
The Rust `quietquic` 0.1.0-alpha.3 API splits bidirectional streams into send
|
|
255
|
+
and receive halves. The Ruby API keeps a small `Stream` facade around those
|
|
256
|
+
halves: `write_all`, `finish`, and `finish_and_wait` operate on the send half;
|
|
257
|
+
`read_to_end(limit:)` operates on the receive half.
|
|
258
|
+
|
|
259
|
+
`finish` queues FIN locally. `finish_and_wait(timeout: 10)` queues FIN and waits
|
|
260
|
+
for the peer's QUIC transport to acknowledge the send half; that proves
|
|
261
|
+
transport receipt, not that the peer application processed the bytes. Use it
|
|
262
|
+
before immediately closing a connection whose final stream must reach the peer.
|
|
263
|
+
The timeout is an upper bound, not an unconditional delay.
|
|
264
|
+
|
|
265
|
+
`read_to_end(limit:)` parks until the peer sends FIN and defaults to a 1 MiB
|
|
266
|
+
limit so an authenticated peer cannot grow receiver memory without bound.
|
|
267
|
+
|
|
268
|
+
Send-half calls on the same `Stream` are serialized with each other, and
|
|
269
|
+
receive-half calls are serialized with each other. A read and a write on the
|
|
270
|
+
same `Stream` use separate native halves, but both sides of a protocol still
|
|
271
|
+
need sane ordering: if both peers wait to read before either peer finishes
|
|
272
|
+
writing, the application protocol has deadlocked.
|
|
273
|
+
|
|
274
|
+
The echo examples use two streams for a simple request/reply shape:
|
|
275
|
+
|
|
276
|
+
```ruby
|
|
277
|
+
out = conn.open_stream
|
|
278
|
+
out.write_all(payload)
|
|
279
|
+
out.finish
|
|
280
|
+
|
|
281
|
+
in_stream = conn.accept_stream
|
|
282
|
+
reply = in_stream.read_to_end
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Exceptions
|
|
288
|
+
|
|
289
|
+
All native-boundary errors raise under one hierarchy so a bare
|
|
290
|
+
`rescue QuietQUIC::Error` catches everything the gem can raise:
|
|
291
|
+
|
|
292
|
+
- `QuietQUIC::Error` — base class (`< StandardError`).
|
|
293
|
+
- `QuietQUIC::ConfigError` — bad config: malformed PSK hex, a bad listen/
|
|
294
|
+
server address, a missing/invalid TOML file.
|
|
295
|
+
- `QuietQUIC::ConnectError` — the client's handshake failed or timed out
|
|
296
|
+
(the crate's own internal connect timeout, ~10s, covers "nothing answered").
|
|
297
|
+
- `QuietQUIC::ConnectionLost` — a previously-established connection was
|
|
298
|
+
closed or dropped (locally or by the peer) while a call was waiting on it.
|
|
299
|
+
- `QuietQUIC::StreamError` — a stream-level failure: reset, stopped, or
|
|
300
|
+
refused.
|
|
301
|
+
|
|
302
|
+
A Rust panic in the extension is turned into a Ruby exception rather than
|
|
303
|
+
aborting the process — a bug in the extension should not crash your Ruby
|
|
304
|
+
process outright:
|
|
305
|
+
|
|
306
|
+
- A panic at a **synchronous** native boundary is converted to an exception by
|
|
307
|
+
Magnus's panic hook.
|
|
308
|
+
- A panic inside an **async** operation (a future spawned on the runtime) is
|
|
309
|
+
caught and surfaced as `QuietQUIC::Error` (base class) with the message
|
|
310
|
+
`"quietquic internal error: the async operation panicked"`, so the awaiting
|
|
311
|
+
call raises instead of crashing *or* hanging. (Without this, the result slot
|
|
312
|
+
would never be populated and the awaiting fiber/thread would park forever.)
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## License
|
|
317
|
+
|
|
318
|
+
0BSD (BSD Zero Clause), same as the Rust crate. See [`LICENSE`](LICENSE).
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# SPDX-License-Identifier: 0BSD
|
|
3
|
+
#
|
|
4
|
+
# A complete echo round trip driven by one cooperative Ruby event-loop thread.
|
|
5
|
+
# QuietQUIC performs network I/O on its native Tokio runtime; while a Ruby
|
|
6
|
+
# fiber is awaiting that work, Async can run other Ruby fibers on this thread.
|
|
7
|
+
#
|
|
8
|
+
# Prerequisite:
|
|
9
|
+
# gem install async
|
|
10
|
+
#
|
|
11
|
+
# Run:
|
|
12
|
+
# ruby examples/cooperative_async.rb
|
|
13
|
+
|
|
14
|
+
require "async"
|
|
15
|
+
require "timeout"
|
|
16
|
+
require "quietquic"
|
|
17
|
+
|
|
18
|
+
PSK = "00" * 31 + "07"
|
|
19
|
+
CLIENT_ID = "demo-client"
|
|
20
|
+
MESSAGE = "hello from cooperative Ruby"
|
|
21
|
+
|
|
22
|
+
ruby_thread = Thread.current
|
|
23
|
+
ticks = 0
|
|
24
|
+
echo_verified = false
|
|
25
|
+
server = nil
|
|
26
|
+
|
|
27
|
+
Timeout.timeout(10) do
|
|
28
|
+
Async do |task|
|
|
29
|
+
server = QuietQUIC::Server.bind(
|
|
30
|
+
listen: "127.0.0.1:0",
|
|
31
|
+
clients: { CLIENT_ID => PSK },
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
server_task = task.async do
|
|
35
|
+
connection = server.accept
|
|
36
|
+
begin
|
|
37
|
+
incoming = connection.accept_stream
|
|
38
|
+
payload = incoming.read_to_end
|
|
39
|
+
|
|
40
|
+
reply = connection.open_stream
|
|
41
|
+
reply.write_all(payload)
|
|
42
|
+
reply.finish
|
|
43
|
+
ensure
|
|
44
|
+
connection&.close
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# server.accept above is now pending. These ticks prove that waiting for
|
|
49
|
+
# native I/O parks that fiber instead of blocking this Ruby event loop.
|
|
50
|
+
ticker_task = task.async do
|
|
51
|
+
3.times do
|
|
52
|
+
task.sleep(0.05)
|
|
53
|
+
raise "ticker moved to another Ruby thread" unless Thread.current.equal?(ruby_thread)
|
|
54
|
+
|
|
55
|
+
ticks += 1
|
|
56
|
+
puts "Ruby event loop did other work (tick #{ticks})"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
ticker_task.wait
|
|
61
|
+
|
|
62
|
+
client_task = task.async do
|
|
63
|
+
connection = QuietQUIC::Client.connect(
|
|
64
|
+
client_id: CLIENT_ID,
|
|
65
|
+
psk: PSK,
|
|
66
|
+
server: server.local_address,
|
|
67
|
+
)
|
|
68
|
+
begin
|
|
69
|
+
outgoing = connection.open_stream
|
|
70
|
+
outgoing.write_all(MESSAGE)
|
|
71
|
+
outgoing.finish
|
|
72
|
+
|
|
73
|
+
response = connection.accept_stream.read_to_end
|
|
74
|
+
raise "echo mismatch" unless response == MESSAGE.b
|
|
75
|
+
|
|
76
|
+
echo_verified = true
|
|
77
|
+
puts "echo verified: #{response.inspect}"
|
|
78
|
+
ensure
|
|
79
|
+
connection&.close
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
client_task.wait
|
|
84
|
+
server_task.wait
|
|
85
|
+
ensure
|
|
86
|
+
server&.close
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
raise "cooperative task did not progress" unless ticks == 3
|
|
91
|
+
raise "echo task did not complete" unless echo_verified
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# SPDX-License-Identifier: 0BSD
|
|
3
|
+
#
|
|
4
|
+
# A minimal QuietQUIC echo client: connects to a running echo server (see
|
|
5
|
+
# echo_server.rb), opens a stream, sends a message, finishes the stream, then
|
|
6
|
+
# accepts the server's echoed reply on a second stream and prints it.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# ruby examples/echo_client.rb [server_addr] [psk_hex] [message]
|
|
10
|
+
#
|
|
11
|
+
# server_addr defaults to "127.0.0.1:4433" (must match echo_server.rb)
|
|
12
|
+
# psk_hex defaults to the same demo PSK echo_server.rb uses
|
|
13
|
+
# message defaults to "hello, quietquic"
|
|
14
|
+
|
|
15
|
+
require "quietquic"
|
|
16
|
+
|
|
17
|
+
SERVER = ARGV[0] || "127.0.0.1:4433"
|
|
18
|
+
PSK = ARGV[1] || "00" * 31 + "07"
|
|
19
|
+
MESSAGE = ARGV[2] || "hello, quietquic"
|
|
20
|
+
CLIENT_ID = "demo-client"
|
|
21
|
+
|
|
22
|
+
conn = QuietQUIC::Client.connect(client_id: CLIENT_ID, psk: PSK, server: SERVER)
|
|
23
|
+
puts "connected to #{conn.remote_address}"
|
|
24
|
+
|
|
25
|
+
begin
|
|
26
|
+
out = conn.open_stream
|
|
27
|
+
out.write_all(MESSAGE)
|
|
28
|
+
out.finish
|
|
29
|
+
puts "sent #{MESSAGE.bytesize} byte(s): #{MESSAGE.inspect}"
|
|
30
|
+
|
|
31
|
+
reply_stream = conn.accept_stream
|
|
32
|
+
reply = reply_stream.read_to_end
|
|
33
|
+
puts "received echo: #{reply.inspect}"
|
|
34
|
+
|
|
35
|
+
raise "echo mismatch!" unless reply == MESSAGE.b
|
|
36
|
+
|
|
37
|
+
puts "echo verified OK"
|
|
38
|
+
ensure
|
|
39
|
+
conn.close
|
|
40
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# SPDX-License-Identifier: 0BSD
|
|
3
|
+
#
|
|
4
|
+
# A minimal QuietQUIC echo server: binds to a listen address, accepts one
|
|
5
|
+
# connection, accepts one stream on it, reads the stream to end, and echoes
|
|
6
|
+
# the exact bytes back to the peer on a second stream.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# ruby examples/echo_server.rb [listen_addr] [psk_hex]
|
|
10
|
+
#
|
|
11
|
+
# listen_addr defaults to "127.0.0.1:4433"
|
|
12
|
+
# psk_hex defaults to a demo PSK (64 hex chars) -- DO NOT use this
|
|
13
|
+
# demo PSK for anything real; generate your own with e.g.
|
|
14
|
+
# `SecureRandom.hex(32)`.
|
|
15
|
+
#
|
|
16
|
+
# Run this first, then run echo_client.rb (same psk_hex if you override it)
|
|
17
|
+
# in another terminal.
|
|
18
|
+
|
|
19
|
+
require "quietquic"
|
|
20
|
+
|
|
21
|
+
$stdout.sync = true
|
|
22
|
+
|
|
23
|
+
LISTEN = ARGV[0] || "127.0.0.1:4433"
|
|
24
|
+
PSK = ARGV[1] || "00" * 31 + "07"
|
|
25
|
+
CLIENT_ID = "demo-client"
|
|
26
|
+
|
|
27
|
+
srv = QuietQUIC::Server.bind(listen: LISTEN, clients: { CLIENT_ID => PSK })
|
|
28
|
+
puts "listening on #{srv.local_address}"
|
|
29
|
+
|
|
30
|
+
begin
|
|
31
|
+
conn = srv.accept
|
|
32
|
+
puts "accepted connection from #{conn.remote_address}"
|
|
33
|
+
|
|
34
|
+
stream = conn.accept_stream
|
|
35
|
+
data = stream.read_to_end
|
|
36
|
+
puts "received #{data.bytesize} byte(s): #{data.inspect}"
|
|
37
|
+
|
|
38
|
+
echo = conn.open_stream
|
|
39
|
+
echo.write_all(data)
|
|
40
|
+
# `finish` only queues FIN locally. Wait for the peer to acknowledge it so
|
|
41
|
+
# closing below cannot discard the final echo on a slower or NATted path.
|
|
42
|
+
echo.finish_and_wait
|
|
43
|
+
puts "echoed #{data.bytesize} byte(s) back"
|
|
44
|
+
|
|
45
|
+
conn.close
|
|
46
|
+
ensure
|
|
47
|
+
srv.close
|
|
48
|
+
end
|