quic 0.0.2 → 0.0.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2598a7b8f6d545c4f092bc5885644fdc6a6cc19406489ebba6dcd40e90d63394
4
- data.tar.gz: 71076bbc240ea79b361bac74443748dcbc725e6cb5fcc22ff6587935a1afb9e1
3
+ metadata.gz: e019da100b72501ae5f3f097aa00344e763de5483e301b03e619c695e7ed269d
4
+ data.tar.gz: 922faa6c8b6b96e121849d4f17823e31cbfdeb05da3d93681603c3fb55840127
5
5
  SHA512:
6
- metadata.gz: 6b882c40642e5efdf6a45717c6dba5fe890c26dbe7bbae66ef9b3871e17958773dd7c41877a5b9decf40e0fd6ba03359f632b8f9ca6dc393cabf36600d03acf1
7
- data.tar.gz: 29a203887f07455ae6d8d6c76d553bb82fe9c1d72aa2233f797f60093e353dbf028b8b8d3b8f6e40a645b9535cb6b089f4971cf763e96a45af7cc7a494e03a2b
6
+ metadata.gz: c1eb1754a7e98f652825f641d0a1bfb2c131d54fcaceffe2867b8000107752d39c7fcc9e739125085c84bb76272306213e5774730e408b4d6cfc2bb6ce61b5c9
7
+ data.tar.gz: 365881fb77b46679c549f6e19dc5988cd94f62a3ab13ee937e0514d12202a45a63c17501c6bf492b6363b2b33c81894651965f74425e44af37cc2e24f37c0aa5
data/.clang-format ADDED
@@ -0,0 +1 @@
1
+ BasedOnStyle: LLVM
@@ -0,0 +1,2 @@
1
+ # Format the C sources with clang-format
2
+ cb0c551641efd2c29377ffcc1f8e1b9999926544
data/CHANGELOG.md CHANGED
@@ -1,3 +1,36 @@
1
+ ## [0.0.4] - 2026-10-22
2
+
3
+ ### Added
4
+ - `examples/echo_endpoint_demo.rb`, the echo server for `examples/echo_demo.rb` on a `QUIC::Endpoint`. It serves many clients at once from an I/O loop of its own, and writes qlog when `QUIC_ECHO_QLOG_DIR` is set. ([#18](https://github.com/unasuke/quic-ruby/pull/18))
5
+ - The README documents `QUIC::Endpoint`, `QUIC::TLSContext`, `Settings#qlog_dir` and `quic/ext.h`. Its Limitations no longer say that a server handles one connection per socket, and list instead the lack of Retry and address validation, and of client certificate authentication. ([#18](https://github.com/unasuke/quic-ruby/pull/18))
6
+
7
+ ### Fixed
8
+ - A connection's qlog file is closed once the connection ends, instead of when the connection is garbage collected. ([#19](https://github.com/unasuke/quic-ruby/pull/19))
9
+
10
+ ## [0.0.3] - 2026-10-13
11
+
12
+ ### Added
13
+ - `QUIC::Connection::Server` serves a single connection per UDP socket. `Server.accept(sock:, certificate_path:, private_key_path:, transport_params: nil, settings: nil)` waits for the first datagram, connects the socket to its sender and returns a Server whose handshake `#run` completes; streams, `#accept_stream` and `#close` then work as on a Client. The certificate file is PEM, leaf first, and `settings` must name at least one ALPN protocol; `verify_mode`, `ca_file` and `ca_path` are ignored, since no client certificate is requested. A certificate or key that cannot be loaded or used, or an empty ALPN list, raises `ArgumentError`; a first datagram that cannot start a connection raises `QUIC::Error::Proto`. There is no Retry, address validation or Version Negotiation, and `Server.new` is private. ([#9](https://github.com/unasuke/quic-ruby/pull/9))
14
+ - `QUIC::Stream#closed?` returns true once both directions of the stream have finished and everything sent has been acknowledged. It stays false if the connection closes first. ([#9](https://github.com/unasuke/quic-ruby/pull/9))
15
+ - A C function table for other native extensions, declared in `quic/ext.h` (ABI 1.0). An extension looks up `quic_ext_get_api` with `rb_ext_resolve_symbol` (Ruby 3.3 or later) and registers a C stream handler on a connection; the handler then owns every stream of that connection, receiving stream data and handing out what to send as `#write_pkt` builds each packet. `QUIC::EXT_INCLUDE_DIR` tells an extconf.rb where the header is. quic.so now exports `quic_ext_get_api` besides `Init_quic`; ngtcp2 and picotls stay hidden. ([#12](https://github.com/unasuke/quic-ruby/pull/12))
16
+ - `QUIC::Error::InvalidState`, raised by `#open_bidi_stream`, `#open_uni_stream`, `#accept_stream` and `#accept_stream_nonblock` of a connection whose streams belong to a C stream handler. ([#12](https://github.com/unasuke/quic-ruby/pull/12))
17
+ - `QUIC::TLSContext.server(certificate_path:, private_key_path:, alpn:)` reads a server's certificate chain and private key once and keeps them with its ALPN protocols, for every connection made from it to share. A connection holds on to the context it started with. The checks and `ArgumentError` messages are those of `Server.accept`, which now builds one inside; `TLSContext.new` raises `TypeError`. ([#13](https://github.com/unasuke/quic-ruby/pull/13))
18
+ - `QUIC::Endpoint` serves many connections on one unconnected UDP socket, leaving the socket and the I/O loop to the caller. `Endpoint.new(local_address:, tls_context:, transport_params: nil, settings: nil, max_connections: 1024)`; `#read_pkt(packet, remote:, local: nil)` routes a datagram by its connection ID and returns the `Connection::Server` it went to, making one for a new client; `#write_pkt` returns `[packet, sockaddr]` pairs until nil; `#expiry` and `#handle_expiry` run every connection's timers; `#take_closed` hands out the connections that have finished; `#connections`, `#stop_accepting` and `#tls_context=` complete it. It answers an unsupported version with Version Negotiation, a packet for an unknown connection with a Stateless Reset (at most 100 in a burst, then one a second), and a new connection beyond `max_connections` or after `#stop_accepting` with CONNECTION_REFUSED. At most 1024 of these answers wait for `#write_pkt`; any beyond are dropped. A connection's own failure closes that connection alone and never raises from the Endpoint. Its connections have no socket: `#close` queues the CONNECTION_CLOSE for `#write_pkt`, and only non-blocking stream methods work. There is no Retry or address validation. ([#14](https://github.com/unasuke/quic-ruby/pull/14))
19
+ - `QUIC::Connection::Server#error` (and `Client#error`) tells why an Endpoint closed the connection: nil when the peer closed it, it timed out idle or `#close` was called, the exception itself when Ruby code in a callback raised, and otherwise a `QUIC::Error` made from ngtcp2's error code. It is always nil for a connection no Endpoint made. ([#14](https://github.com/unasuke/quic-ruby/pull/14))
20
+ - `QUIC::Settings#qlog_dir` (a String or Pathname, nil by default) makes every connection write its qlog to `<qlog_dir>/<its first source connection ID in hex>.sqlog`. `Client.new`, `Connection::Server.accept` and `Endpoint.new` raise `ArgumentError` unless it is a writable directory; a file that cannot be opened later, when a connection is made, only costs that connection its qlog. ([#15](https://github.com/unasuke/quic-ruby/pull/15))
21
+
22
+ ### Changed
23
+ - The extension no longer uses the ngtcp2 functions and callbacks deprecated in 1.22.0 and 1.23.0, and calls their `*2` replacements instead. Behavior is unchanged. ([#8](https://github.com/unasuke/quic-ruby/pull/8))
24
+ - `#accept_stream_nonblock` now raises `QUIC::Error::WaitReadable` with "no peer-initiated stream available" (was "no server-initiated stream available"), as it serves `QUIC::Connection::Server` too. ([#9](https://github.com/unasuke/quic-ruby/pull/9))
25
+ - picotls's `lib/pembase64.c`, a PEM loader nothing uses, is no longer built at install time, so every picotls source compiled into the gem is MIT licensed. ([#10](https://github.com/unasuke/quic-ruby/pull/10))
26
+
27
+ ### Fixed
28
+ - `QUIC::Stream#read` and `#read_nonblock` give the peer flow control credit back for what they hand to the application. Before, a connection stopped receiving once `initial_max_data` (1 MiB by default) had been used up. ([#11](https://github.com/unasuke/quic-ruby/pull/11))
29
+ - When a stream the peer opened closes, the peer may open another one. Before, it could open only `initial_max_streams_bidi` bidirectional streams over the whole connection. A stream that closes while it waits for `#accept_stream` is given back only once it has been accepted, so that the streams waiting there stay within that limit. ([#11](https://github.com/unasuke/quic-ruby/pull/11))
30
+ - An exception raised by Ruby code inside an ngtcp2 callback (for example a `FrozenError`) comes out of `#read_pkt`, `#write_pkt` or `#handle_expiry` instead of unwinding through ngtcp2. ([#11](https://github.com/unasuke/quic-ruby/pull/11))
31
+ - `QUIC::Stream#write` and `#write_nonblock` count the bytes already queued but not yet sent against the peer's window, and `#write_pkt` no longer raises `QUIC::Error::FlowControl` when that window is full; the data waits until the peer extends it. ([#11](https://github.com/unasuke/quic-ruby/pull/11))
32
+ - `#read_pkt`, `Client._open` and `Server._accept` raise `ArgumentError` for a sockaddr String other than an AF_INET or AF_INET6 sockaddr of that family's exact size, as `Addrinfo#to_sockaddr` makes. ngtcp2 copied an address of any length into room for a `sockaddr_in6`, so a longer String overran the heap. ([#14](https://github.com/unasuke/quic-ruby/pull/14))
33
+
1
34
  ## [0.0.2] - 2026-09-27
2
35
 
3
36
  ### Added
@@ -57,9 +57,8 @@ picotls f07f1c8 -- https://github.com/h2o/picotls
57
57
  -------------------------------------------------
58
58
 
59
59
  picotls has no top-level license file; the terms are stated in the header of
60
- each source file. Of the files compiled into libpicotls.a, lib/picotls.c,
61
- lib/hpke.c and lib/openssl.c are MIT licensed, while lib/pembase64.c is under
62
- an ISC-style license. Both texts are reproduced below.
60
+ each source file. The files compiled into libpicotls.a, lib/picotls.c,
61
+ lib/hpke.c and lib/openssl.c, are MIT licensed; the text is reproduced below.
63
62
 
64
63
  The MIT License
65
64
 
@@ -83,19 +82,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
83
82
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
84
83
  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
85
84
  IN THE SOFTWARE.
86
-
87
- The ISC-style license
88
-
89
- Copyright (c) 2016 Christian Huitema <huitema@huitema.net>
90
-
91
- Permission to use, copy, modify, and distribute this software for any
92
- purpose with or without fee is hereby granted, provided that the above
93
- copyright notice and this permission notice appear in all copies.
94
-
95
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
96
- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
97
- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
98
- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
99
- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
100
- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
101
- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
data/README.md CHANGED
@@ -50,10 +50,75 @@ More complete programs live in [`examples/`](examples/):
50
50
 
51
51
  - [`handshake_demo.rb`](examples/handshake_demo.rb): prints the library versions and times a handshake.
52
52
  - [`echo_demo.rb`](examples/echo_demo.rb): exchanges data on a stream with a local echo server.
53
+ - [`echo_server_demo.rb`](examples/echo_server_demo.rb): the echo server for `echo_demo.rb`, generating its own certificate.
54
+ - [`echo_endpoint_demo.rb`](examples/echo_endpoint_demo.rb): the same echo server on a `QUIC::Endpoint`, serving many clients at once from an I/O loop of its own, and writing qlog when `QUIC_ECHO_QLOG_DIR` is set.
53
55
  - [`doq_demo.rb`](examples/doq_demo.rb): sends a DNS over QUIC query.
54
56
  - [`io_loop_demo.rb`](examples/io_loop_demo.rb): sends the same query from an I/O loop the script owns, calling `#write_pkt` / `#read_pkt` / `#handle_expiry` itself.
55
57
  - [`verify_demo.rb`](examples/verify_demo.rb): tries the certificate verification settings described below against a public server.
56
58
 
59
+ ### Server
60
+
61
+ ```ruby
62
+ require "quic"
63
+ require "socket"
64
+
65
+ sock = UDPSocket.new(Socket::AF_INET)
66
+ sock.bind("127.0.0.1", 4433)
67
+ server = QUIC::Connection::Server.accept(
68
+ sock: sock, certificate_path: "server.pem", private_key_path: "server.key",
69
+ settings: QUIC::Settings.default.with(alpn: ["perf"])
70
+ )
71
+ server.run # completes the handshake
72
+ stream = server.accept_stream
73
+ stream.write(stream.read, fin: true) # echo everything up to the client's FIN
74
+ begin
75
+ server.pump_until { stream.closed? }
76
+ rescue QUIC::Error::Closed
77
+ # the client closed the connection before the stream finished
78
+ end
79
+ ```
80
+
81
+ `accept` connects the socket to the first client that sends to it and serves that one connection only; to serve many clients on one socket, use a `QUIC::Endpoint` (below). [`echo_server_demo.rb`](examples/echo_server_demo.rb) generates its own certificate, so it runs as is against `echo_demo.rb`.
82
+
83
+ ### Many connections on one socket
84
+
85
+ `QUIC::Endpoint` serves many connections on one UDP socket. It routes each datagram to its connection by connection ID, makes a connection for each new client, and answers what no connection can: Version Negotiation for a version it does not support, a Stateless Reset for a packet of a connection it does not know, and CONNECTION_REFUSED beyond `max_connections` (1024 by default) or after `#stop_accepting`. The socket and the I/O loop stay with you:
86
+
87
+ ```ruby
88
+ require "quic"
89
+ require "socket"
90
+
91
+ tls = QUIC::TLSContext.server(
92
+ certificate_path: "server.pem", private_key_path: "server.key", alpn: ["perf"]
93
+ )
94
+ sock = UDPSocket.new(Socket::AF_INET)
95
+ sock.bind("127.0.0.1", 4433) # the address it serves, not 0.0.0.0
96
+ endpoint = QUIC::Endpoint.new(local_address: sock.local_address, tls_context: tls)
97
+
98
+ loop do
99
+ expiry = endpoint.expiry # CLOCK_MONOTONIC in nanoseconds, or nil
100
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
101
+ if IO.select([sock], nil, nil, expiry && [expiry - now, 0].max / 1e9)
102
+ data, sender = sock.recvmsg(65_535)
103
+ endpoint.read_pkt(data, remote: sender.to_sockaddr) # the connection it went to
104
+ end
105
+ endpoint.handle_expiry
106
+
107
+ endpoint.connections.each do |conn|
108
+ # conn.accept_stream_nonblock, Stream#read_nonblock, Stream#write_nonblock
109
+ end
110
+
111
+ while (packet, to = endpoint.write_pkt)
112
+ sock.send(packet, 0, to)
113
+ end
114
+ endpoint.take_closed.each { |conn| warn conn.error.inspect if conn.error }
115
+ end
116
+ ```
117
+
118
+ The connections are `QUIC::Connection::Server`s without a socket of their own, so only their non-blocking methods work, and `#close` queues its CONNECTION_CLOSE for `Endpoint#write_pkt`. A connection's failure never raises from the Endpoint: it closes that connection alone, and the connection's `#error` tells why. The Endpoint's own answers wait for `#write_pkt` in a queue of at most 1024, and any beyond are dropped, so call it often under load. [`echo_endpoint_demo.rb`](examples/echo_endpoint_demo.rb) fills in the streams and runs against `echo_demo.rb`.
119
+
120
+ `QUIC::TLSContext.server` reads the certificate chain (PEM, leaf first) and the private key once, for every connection made from it to share. To put a renewed certificate in place, assign a new context: `endpoint.tls_context = QUIC::TLSContext.server(...)`. New connections use it, and existing ones keep the context they started with.
121
+
57
122
  ### Certificate verification
58
123
 
59
124
  The server certificate is verified by default, against the system's trusted CAs.
@@ -70,11 +135,45 @@ settings = QUIC::Settings.default.with(alpn: ["h3"], ca_file: "/path/to/ca.pem")
70
135
  settings = QUIC::Settings.default.with(alpn: ["perf"], verify_mode: :none)
71
136
  ```
72
137
 
138
+ ### qlog
139
+
140
+ `qlog_dir` makes every connection write its [qlog](https://datatracker.ietf.org/doc/draft-ietf-quic-qlog-main-schema/) to `<qlog_dir>/<its first source connection ID in hex>.sqlog`:
141
+
142
+ ```ruby
143
+ settings = QUIC::Settings.default.with(alpn: ["h3"], qlog_dir: "/tmp/qlog")
144
+ ```
145
+
146
+ It applies to clients, `Connection::Server.accept` and every connection of an Endpoint. `Client.new`, `Server.accept` and `Endpoint.new` raise `ArgumentError` unless it is a writable directory. A file that cannot be opened later, when a connection is made, only costs that connection its qlog. A connection's file is closed as soon as the connection ends, not when the connection is garbage collected.
147
+
148
+ ### From other native extensions
149
+
150
+ Another native extension can drive a connection's streams from C instead of through `QUIC::Stream`, as the [http3](https://github.com/unasuke/http3-ruby) gem does. `quic/ext.h` declares the function table (ABI 1.0), and `QUIC::EXT_INCLUDE_DIR` tells an `extconf.rb` where it is:
151
+
152
+ ```ruby
153
+ require "mkmf"
154
+ require "quic/ext_include"
155
+
156
+ append_cppflags("-I#{QUIC::EXT_INCLUDE_DIR}")
157
+ ```
158
+
159
+ At run time, look the table up once `quic` is loaded. `rb_ext_resolve_symbol` needs Ruby 3.3 or later:
160
+
161
+ ```c
162
+ #include <quic/ext.h>
163
+
164
+ rb_require("quic");
165
+ quic_ext_get_api_fn get = (quic_ext_get_api_fn)rb_ext_resolve_symbol("quic/quic", "quic_ext_get_api");
166
+ const quic_ext_api *api = get ? get(QUIC_EXT_ABI_MAJOR) : NULL; /* NULL: a different ABI major */
167
+ ```
168
+
169
+ A connection given a stream handler with `set_stream_handler` makes no `QUIC::Stream`: the handler receives every stream's data and hands out what to send as `#write_pkt` builds each packet, and `#open_bidi_stream`, `#accept_stream` and the like raise `QUIC::Error::InvalidState`. The comment at the top of [`ext.h`](ext/quic/include/quic/ext.h) states the rules on versions, exceptions and lifetimes.
170
+
73
171
  ## Limitations
74
172
 
75
173
  - **No certificate revocation checking.** CRLs and OCSP are not consulted.
76
174
  - **No session resumption or 0-RTT.**
77
- - **No server side.** Only the client (`QUIC::Connection::Client`) exists; there is no listen/accept.
175
+ - **No Retry or address validation.** An Endpoint makes a connection for any Initial packet without checking that its sender owns the source address, so Initial packets with forged addresses can fill `max_connections` for `handshake_timeout` (10 seconds by default) and keep real clients out meanwhile.
176
+ - **No client certificate authentication.** A server does not ask for a client certificate.
78
177
  - **Key exchanges and cipher suites are fixed.** X25519, secp256r1 and secp384r1 with AES-128-GCM, AES-256-GCM and ChaCha20-Poly1305. They cannot be selected from Ruby.
79
178
 
80
179
  ## Development
@@ -91,7 +190,7 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/unasuk
91
190
 
92
191
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
93
192
 
94
- The gem ships no third-party binaries: [ngtcp2](https://github.com/ngtcp2/ngtcp2) (MIT, with portions under the Chromium BSD-3-Clause license) and [picotls](https://github.com/h2o/picotls) (MIT, with one file under an ISC-style license) are downloaded and built on the installing machine, and libcrypto is the host's. Their license texts are in [LICENSE-DEPENDENCIES.txt](LICENSE-DEPENDENCIES.txt) for reference.
193
+ The gem ships no third-party binaries: [ngtcp2](https://github.com/ngtcp2/ngtcp2) (MIT, with an embedded PCG random number generator under Apache-2.0 OR MIT) and [picotls](https://github.com/h2o/picotls) (MIT) are downloaded and built on the installing machine, and libcrypto is the host's. Their license texts are in [LICENSE-DEPENDENCIES.txt](LICENSE-DEPENDENCIES.txt) for reference.
95
194
 
96
195
  ## Code of Conduct
97
196
 
data/Rakefile CHANGED
@@ -17,4 +17,11 @@ Rake::ExtensionTask.new("quic", GEMSPEC) do |ext|
17
17
  ext.lib_dir = "lib/quic"
18
18
  end
19
19
 
20
+ # A test-only extension that drives quic/ext.h like the http3 gem does
21
+ # (test/test_quic_ext_api.rb). It lands in test/ext/ and is never packaged.
22
+ Rake::ExtensionTask.new("quic_ext_echo") do |ext|
23
+ ext.ext_dir = "test/ext/quic_ext_echo"
24
+ ext.lib_dir = "test/ext"
25
+ end
26
+
20
27
  task default: %i[clobber compile test standard]
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Throwaway certificates for the server demos (echo_server_demo.rb and
4
+ # echo_endpoint_demo.rb), so that they run without any certificate files.
5
+
6
+ require "ipaddr"
7
+ require "openssl"
8
+
9
+ module DemoCertificates
10
+ # Write a CA and a server certificate it signed (EC P-256, valid for one
11
+ # day), with the server's private key, into dir. The server certificate
12
+ # names localhost, 127.0.0.1 and host. Returns the paths of the CA, the
13
+ # server certificate and the key.
14
+ def self.generate(dir, host)
15
+ now = Time.now
16
+
17
+ ca_key = OpenSSL::PKey::EC.generate("prime256v1")
18
+ ca_name = OpenSSL::X509::Name.parse("/CN=quic-ruby echo server demo CA")
19
+ ca_cert = OpenSSL::X509::Certificate.new
20
+ ca_cert.version = 2
21
+ ca_cert.serial = 1
22
+ ca_cert.subject = ca_name
23
+ ca_cert.issuer = ca_name
24
+ ca_cert.public_key = ca_key
25
+ ca_cert.not_before = now - 60
26
+ ca_cert.not_after = now + 86_400
27
+ factory = OpenSSL::X509::ExtensionFactory.new(ca_cert, ca_cert)
28
+ ca_cert.add_extension(factory.create_extension("basicConstraints", "CA:TRUE", true))
29
+ ca_cert.add_extension(factory.create_extension("keyUsage", "keyCertSign,cRLSign", true))
30
+ ca_cert.sign(ca_key, OpenSSL::Digest.new("SHA256"))
31
+
32
+ server_key = OpenSSL::PKey::EC.generate("prime256v1")
33
+ server_cert = OpenSSL::X509::Certificate.new
34
+ server_cert.version = 2
35
+ server_cert.serial = 2
36
+ server_cert.subject = OpenSSL::X509::Name.parse("/CN=localhost")
37
+ server_cert.issuer = ca_name
38
+ server_cert.public_key = server_key
39
+ server_cert.not_before = now - 60
40
+ server_cert.not_after = now + 86_400
41
+ factory = OpenSSL::X509::ExtensionFactory.new(ca_cert, server_cert)
42
+ server_cert.add_extension(factory.create_extension("subjectAltName", subject_alt_names(host)))
43
+ server_cert.add_extension(factory.create_extension("extendedKeyUsage", "serverAuth"))
44
+ server_cert.sign(ca_key, OpenSSL::Digest.new("SHA256"))
45
+
46
+ paths = %w[ca.pem server.pem server.key].map { |name| File.join(dir, name) }
47
+ File.write(paths[0], ca_cert.to_pem)
48
+ File.write(paths[1], server_cert.to_pem)
49
+ File.write(paths[2], server_key.private_to_pem)
50
+ paths
51
+ end
52
+
53
+ # localhost and 127.0.0.1, plus the bind address so that echo_demo.rb can
54
+ # verify the certificate against QUIC_ECHO_HOST.
55
+ def self.subject_alt_names(host)
56
+ names = ["DNS:localhost", "IP:127.0.0.1"]
57
+ entry = begin
58
+ IPAddr.new(host)
59
+ "IP:#{host}"
60
+ rescue IPAddr::InvalidAddressError
61
+ "DNS:#{host}"
62
+ end
63
+ names << entry unless names.include?(entry)
64
+ names.join(",")
65
+ end
66
+ private_class_method :subject_alt_names
67
+ end
@@ -3,8 +3,7 @@
3
3
  # Stream API demo: open a bidirectional stream, write a small payload with
4
4
  # FIN, then block on Stream#read until the peer echoes the bytes back and
5
5
  # closes its side. Requires a local QUIC echo server (default: 127.0.0.1:4433
6
- # with ALPN "perf"); ngtcp2 ships an examples/server binary that does this
7
- # when built with --enable-examples.
6
+ # with ALPN "perf"), such as examples/echo_server_demo.rb.
8
7
  #
9
8
  # Override the target via env vars:
10
9
  # QUIC_ECHO_HOST (default 127.0.0.1)
@@ -12,10 +11,10 @@
12
11
  # QUIC_ECHO_ALPN (default perf)
13
12
  # QUIC_ECHO_CA_FILE (default unset: the server certificate is not verified)
14
13
  #
15
- # The echo server usually runs with a self-signed certificate, so
16
- # verification is off unless QUIC_ECHO_CA_FILE names a CA to trust. When it
17
- # is set, the certificate must match QUIC_ECHO_HOST, which means an IP SAN
18
- # of 127.0.0.1 with the default host.
14
+ # examples/echo_server_demo.rb prints the path of the CA it generated; pass
15
+ # it as QUIC_ECHO_CA_FILE to verify the certificate. Verification is off
16
+ # without it. When it is set, the certificate must match QUIC_ECHO_HOST,
17
+ # which means an IP SAN of 127.0.0.1 with the default host.
19
18
  #
20
19
  # Run with: bundle exec ruby examples/echo_demo.rb
21
20
 
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Echo server for examples/echo_demo.rb that serves many clients at once
4
+ # through QUIC::Endpoint: one UDP socket, an I/O loop this script owns, and
5
+ # the connections' non-blocking stream methods. Like echo_server_demo.rb, it
6
+ # echoes every stream back (reads up to its FIN, then writes it all back
7
+ # with FIN). It runs until Ctrl-C, which closes every connection.
8
+ #
9
+ # No certificate files are needed. A throwaway CA and a server certificate it
10
+ # signed are generated at startup (see demo_certificates.rb), and the CA's
11
+ # path is printed; the files are removed when the server exits.
12
+ #
13
+ # echo_demo.rb reads the same env vars, so it connects without any setup:
14
+ # QUIC_ECHO_HOST address to bind (default 127.0.0.1); also added to
15
+ # the certificate's SAN. Not 0.0.0.0: an Endpoint
16
+ # needs the address it serves.
17
+ # QUIC_ECHO_PORT (default 4433)
18
+ # QUIC_ECHO_ALPN (default perf)
19
+ # QUIC_ECHO_QLOG_DIR a directory to write each connection's qlog to
20
+ # (default unset: no qlog)
21
+ #
22
+ # Run with: bundle exec ruby examples/echo_endpoint_demo.rb
23
+ # then, from other terminals: bundle exec ruby examples/echo_demo.rb
24
+
25
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
26
+ require "quic"
27
+ require "socket"
28
+ require "tmpdir"
29
+ require_relative "demo_certificates"
30
+
31
+ BIND_HOST = ENV.fetch("QUIC_ECHO_HOST", "127.0.0.1")
32
+ BIND_PORT = Integer(ENV.fetch("QUIC_ECHO_PORT", "4433"))
33
+ ALPN = ENV.fetch("QUIC_ECHO_ALPN", "perf")
34
+ QLOG_DIR = ENV["QUIC_ECHO_QLOG_DIR"]
35
+ # Datagrams read in one round of the loop at most, so that a flood of them
36
+ # does not hold up the timers and the sending.
37
+ RECV_BATCH = 64
38
+
39
+ # One client stream: read it up to its FIN, then write it all back with FIN.
40
+ class EchoStream
41
+ attr_reader :size
42
+
43
+ def initialize(stream)
44
+ @stream = stream
45
+ @data = +""
46
+ @reading = true
47
+ end
48
+
49
+ def id
50
+ @stream.id
51
+ end
52
+
53
+ # Moves the echo on as far as it can without blocking. Returns true once
54
+ # all of it, FIN included, is queued for sending.
55
+ def step
56
+ read while @reading
57
+ write
58
+ rescue QUIC::Error::WaitReadable, QUIC::Error::WaitWritable
59
+ false
60
+ end
61
+
62
+ private
63
+
64
+ def read
65
+ @data << @stream.read_nonblock(65_536)
66
+ rescue EOFError
67
+ @reading = false
68
+ @size = @data.bytesize
69
+ end
70
+
71
+ # write_nonblock queues as much as the client's flow control window has
72
+ # room for, and sends the FIN only along with the last byte.
73
+ def write
74
+ loop do
75
+ queued = @stream.write_nonblock(@data, fin: true)
76
+ return true if queued == @data.bytesize
77
+
78
+ @data = @data.byteslice(queued..)
79
+ end
80
+ end
81
+ end
82
+
83
+ # The streams conn's client has opened since the last call.
84
+ def accepted_streams(conn)
85
+ streams = []
86
+ loop { streams << conn.accept_stream_nonblock }
87
+ rescue QUIC::Error::WaitReadable
88
+ streams
89
+ end
90
+
91
+ def now_ns
92
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
93
+ end
94
+
95
+ def flush(endpoint, sock)
96
+ while (packet, to = endpoint.write_pkt)
97
+ sock.send(packet, 0, to)
98
+ end
99
+ end
100
+
101
+ Dir.mktmpdir("quic-echo-endpoint-") do |dir|
102
+ ca_file, certificate_path, private_key_path = DemoCertificates.generate(dir, BIND_HOST)
103
+ tls_context = QUIC::TLSContext.server(
104
+ certificate_path: certificate_path, private_key_path: private_key_path, alpn: [ALPN]
105
+ )
106
+ settings = QUIC::Settings.default
107
+ settings = settings.with(qlog_dir: QLOG_DIR) if QLOG_DIR
108
+
109
+ sock = UDPSocket.new(Socket::AF_INET)
110
+ sock.bind(BIND_HOST, BIND_PORT)
111
+ endpoint = QUIC::Endpoint.new(local_address: sock.local_address, tls_context: tls_context, settings: settings)
112
+ puts "listening on #{sock.local_address.inspect_sockaddr} (UDP), ALPN #{ALPN}"
113
+ puts "CA certificate: #{ca_file}"
114
+ puts "try a verified connection with:"
115
+ puts " QUIC_ECHO_CA_FILE=#{ca_file} bundle exec ruby examples/echo_demo.rb"
116
+ puts "(without QUIC_ECHO_CA_FILE, echo_demo.rb does not verify the certificate)"
117
+ puts "writing qlog to #{QLOG_DIR}" if QLOG_DIR
118
+ puts
119
+
120
+ peers = {} # connection => its client's address
121
+ echoes = Hash.new { |hash, conn| hash[conn] = [] } # connection => its unfinished EchoStreams
122
+
123
+ begin
124
+ loop do
125
+ expiry = endpoint.expiry
126
+ timeout = expiry && [expiry - now_ns, 0].max / 1e9
127
+ if IO.select([sock], nil, nil, timeout)
128
+ RECV_BATCH.times do
129
+ data, sender = sock.recvmsg_nonblock(65_535, exception: false)
130
+ break if data == :wait_readable
131
+
132
+ conn = endpoint.read_pkt(data, remote: sender.to_sockaddr)
133
+ if conn && !peers.key?(conn)
134
+ peers[conn] = sender.inspect_sockaddr
135
+ puts "#{peers[conn]}: connected"
136
+ end
137
+ end
138
+ end
139
+ endpoint.handle_expiry
140
+
141
+ # The connections have no socket of their own, so only their
142
+ # non-blocking methods work.
143
+ endpoint.connections.each do |conn|
144
+ echoes[conn].concat(accepted_streams(conn).map { |stream| EchoStream.new(stream) })
145
+ echoes[conn].reject! do |echo|
146
+ next false unless echo.step
147
+
148
+ puts "#{peers[conn]}: stream #{echo.id}: echoed #{echo.size} bytes"
149
+ true
150
+ end
151
+ end
152
+
153
+ flush(endpoint, sock)
154
+
155
+ endpoint.take_closed.each do |conn|
156
+ echoes.delete(conn)
157
+ reason = conn.error ? " (#{conn.error.class}: #{conn.error.message})" : ""
158
+ puts "#{peers.delete(conn)}: closed#{reason}"
159
+ end
160
+ end
161
+ rescue Interrupt
162
+ puts
163
+ puts "closing #{endpoint.connections.size} connection(s)"
164
+ endpoint.stop_accepting
165
+ endpoint.connections.each(&:close)
166
+ flush(endpoint, sock)
167
+ end
168
+
169
+ sock.close
170
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Echo server for examples/echo_demo.rb: accept one QUIC connection, echo
4
+ # every stream back to the client (read up to its FIN, then write it all
5
+ # back with FIN), and exit once the client closes the connection, goes away,
6
+ # or opens no new stream for 10 seconds.
7
+ #
8
+ # No certificate files are needed. A throwaway CA and a server certificate it
9
+ # signed are generated at startup, and the CA's path is printed; the files
10
+ # are removed when the server exits.
11
+ #
12
+ # echo_demo.rb reads the same env vars, so it connects without any setup:
13
+ # QUIC_ECHO_HOST address to bind (default 127.0.0.1); also added to the
14
+ # certificate's SAN
15
+ # QUIC_ECHO_PORT (default 4433)
16
+ # QUIC_ECHO_ALPN (default perf)
17
+ #
18
+ # Run with: bundle exec ruby examples/echo_server_demo.rb
19
+ # then, from another terminal: bundle exec ruby examples/echo_demo.rb
20
+
21
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
22
+ require "quic"
23
+ require "socket"
24
+ require "tmpdir"
25
+ require_relative "demo_certificates"
26
+
27
+ BIND_HOST = ENV.fetch("QUIC_ECHO_HOST", "127.0.0.1")
28
+ BIND_PORT = Integer(ENV.fetch("QUIC_ECHO_PORT", "4433"))
29
+ ALPN = ENV.fetch("QUIC_ECHO_ALPN", "perf")
30
+ ACCEPT_TIMEOUT = 10
31
+
32
+ Dir.mktmpdir("quic-echo-server-") do |dir|
33
+ ca_file, certificate_path, private_key_path = DemoCertificates.generate(dir, BIND_HOST)
34
+
35
+ sock = UDPSocket.new(Socket::AF_INET)
36
+ sock.bind(BIND_HOST, BIND_PORT)
37
+ puts "listening on #{sock.local_address.inspect_sockaddr} (UDP), ALPN #{ALPN}"
38
+ puts "CA certificate: #{ca_file}"
39
+ puts "try a verified connection with:"
40
+ puts " QUIC_ECHO_CA_FILE=#{ca_file} bundle exec ruby examples/echo_demo.rb"
41
+ puts "(without QUIC_ECHO_CA_FILE, echo_demo.rb does not verify the certificate)"
42
+ puts
43
+
44
+ server = QUIC::Connection::Server.accept(
45
+ sock: sock, certificate_path: certificate_path, private_key_path: private_key_path,
46
+ settings: QUIC::Settings.default.with(alpn: [ALPN])
47
+ )
48
+ puts "accepted #{server.remote_address.inspect_sockaddr}"
49
+ server.run
50
+ puts "handshake completed"
51
+
52
+ begin
53
+ while (stream = server.accept_stream(timeout: ACCEPT_TIMEOUT))
54
+ data = stream.read
55
+ stream.write(data, fin: true)
56
+ puts "stream #{stream.id}: echoed #{data.bytesize} bytes"
57
+ server.pump_until { stream.closed? }
58
+ end
59
+ puts "no new stream for #{ACCEPT_TIMEOUT} seconds"
60
+ rescue QUIC::Error::Closed
61
+ puts "client closed the connection"
62
+ rescue Errno::ECONNREFUSED
63
+ # The socket is connected to the client, so once the client's socket is
64
+ # gone without a CONNECTION_CLOSE, the ICMP port unreachable surfaces
65
+ # here, from sending to it.
66
+ puts "client went away without closing the connection"
67
+ end
68
+
69
+ server.close
70
+ sock.close
71
+ end