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 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