quic 0.0.3 → 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 +4 -4
- data/.clang-format +1 -0
- data/.git-blame-ignore-revs +2 -0
- data/CHANGELOG.md +9 -0
- data/README.md +76 -2
- data/examples/demo_certificates.rb +67 -0
- data/examples/echo_endpoint_demo.rb +170 -0
- data/examples/echo_server_demo.rb +2 -58
- data/ext/quic/connection_client.c +77 -59
- data/ext/quic/connection_common.c +358 -365
- data/ext/quic/connection_common.h +20 -11
- data/ext/quic/connection_server.c +67 -48
- data/ext/quic/endpoint.c +206 -200
- data/ext/quic/endpoint.h +8 -5
- data/ext/quic/ext_api.c +41 -47
- data/ext/quic/include/quic/ext.h +26 -19
- data/ext/quic/quic.c +75 -67
- data/ext/quic/quic.h +4 -2
- data/ext/quic/stream.c +55 -74
- data/ext/quic/stream.h +5 -5
- data/ext/quic/tls_context.c +79 -70
- data/ext/quic/tls_context.h +2 -1
- data/lib/quic/version.rb +1 -1
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e019da100b72501ae5f3f097aa00344e763de5483e301b03e619c695e7ed269d
|
|
4
|
+
data.tar.gz: 922faa6c8b6b96e121849d4f17823e31cbfdeb05da3d93681603c3fb55840127
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c1eb1754a7e98f652825f641d0a1bfb2c131d54fcaceffe2867b8000107752d39c7fcc9e739125085c84bb76272306213e5774730e408b4d6cfc2bb6ce61b5c9
|
|
7
|
+
data.tar.gz: 365881fb77b46679c549f6e19dc5988cd94f62a3ab13ee937e0514d12202a45a63c17501c6bf492b6363b2b33c81894651965f74425e44af37cc2e24f37c0aa5
|
data/.clang-format
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
BasedOnStyle: LLVM
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
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
|
+
|
|
1
10
|
## [0.0.3] - 2026-10-13
|
|
2
11
|
|
|
3
12
|
### Added
|
data/README.md
CHANGED
|
@@ -51,6 +51,7 @@ More complete programs live in [`examples/`](examples/):
|
|
|
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
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.
|
|
54
55
|
- [`doq_demo.rb`](examples/doq_demo.rb): sends a DNS over QUIC query.
|
|
55
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.
|
|
56
57
|
- [`verify_demo.rb`](examples/verify_demo.rb): tries the certificate verification settings described below against a public server.
|
|
@@ -77,7 +78,46 @@ rescue QUIC::Error::Closed
|
|
|
77
78
|
end
|
|
78
79
|
```
|
|
79
80
|
|
|
80
|
-
`accept` connects the socket to the first client that sends to it and serves that one connection only. [`echo_server_demo.rb`](examples/echo_server_demo.rb) generates its own certificate, so it runs as is against `echo_demo.rb`.
|
|
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.
|
|
81
121
|
|
|
82
122
|
### Certificate verification
|
|
83
123
|
|
|
@@ -95,11 +135,45 @@ settings = QUIC::Settings.default.with(alpn: ["h3"], ca_file: "/path/to/ca.pem")
|
|
|
95
135
|
settings = QUIC::Settings.default.with(alpn: ["perf"], verify_mode: :none)
|
|
96
136
|
```
|
|
97
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
|
+
|
|
98
171
|
## Limitations
|
|
99
172
|
|
|
100
173
|
- **No certificate revocation checking.** CRLs and OCSP are not consulted.
|
|
101
174
|
- **No session resumption or 0-RTT.**
|
|
102
|
-
- **
|
|
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.
|
|
103
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.
|
|
104
178
|
|
|
105
179
|
## Development
|
|
@@ -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
|
|
@@ -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
|
|
@@ -20,73 +20,17 @@
|
|
|
20
20
|
|
|
21
21
|
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
22
22
|
require "quic"
|
|
23
|
-
require "ipaddr"
|
|
24
|
-
require "openssl"
|
|
25
23
|
require "socket"
|
|
26
24
|
require "tmpdir"
|
|
25
|
+
require_relative "demo_certificates"
|
|
27
26
|
|
|
28
27
|
BIND_HOST = ENV.fetch("QUIC_ECHO_HOST", "127.0.0.1")
|
|
29
28
|
BIND_PORT = Integer(ENV.fetch("QUIC_ECHO_PORT", "4433"))
|
|
30
29
|
ALPN = ENV.fetch("QUIC_ECHO_ALPN", "perf")
|
|
31
30
|
ACCEPT_TIMEOUT = 10
|
|
32
31
|
|
|
33
|
-
# localhost and 127.0.0.1, plus the bind address so that echo_demo.rb can
|
|
34
|
-
# verify the certificate against QUIC_ECHO_HOST.
|
|
35
|
-
def subject_alt_names(host)
|
|
36
|
-
names = ["DNS:localhost", "IP:127.0.0.1"]
|
|
37
|
-
entry = begin
|
|
38
|
-
IPAddr.new(host)
|
|
39
|
-
"IP:#{host}"
|
|
40
|
-
rescue IPAddr::InvalidAddressError
|
|
41
|
-
"DNS:#{host}"
|
|
42
|
-
end
|
|
43
|
-
names << entry unless names.include?(entry)
|
|
44
|
-
names.join(",")
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Write a CA and a server certificate it signed (EC P-256, valid for one
|
|
48
|
-
# day), with the server's private key, into dir. Returns the three paths.
|
|
49
|
-
def generate_certificates(dir, host)
|
|
50
|
-
now = Time.now
|
|
51
|
-
|
|
52
|
-
ca_key = OpenSSL::PKey::EC.generate("prime256v1")
|
|
53
|
-
ca_name = OpenSSL::X509::Name.parse("/CN=quic-ruby echo server demo CA")
|
|
54
|
-
ca_cert = OpenSSL::X509::Certificate.new
|
|
55
|
-
ca_cert.version = 2
|
|
56
|
-
ca_cert.serial = 1
|
|
57
|
-
ca_cert.subject = ca_name
|
|
58
|
-
ca_cert.issuer = ca_name
|
|
59
|
-
ca_cert.public_key = ca_key
|
|
60
|
-
ca_cert.not_before = now - 60
|
|
61
|
-
ca_cert.not_after = now + 86_400
|
|
62
|
-
factory = OpenSSL::X509::ExtensionFactory.new(ca_cert, ca_cert)
|
|
63
|
-
ca_cert.add_extension(factory.create_extension("basicConstraints", "CA:TRUE", true))
|
|
64
|
-
ca_cert.add_extension(factory.create_extension("keyUsage", "keyCertSign,cRLSign", true))
|
|
65
|
-
ca_cert.sign(ca_key, OpenSSL::Digest.new("SHA256"))
|
|
66
|
-
|
|
67
|
-
server_key = OpenSSL::PKey::EC.generate("prime256v1")
|
|
68
|
-
server_cert = OpenSSL::X509::Certificate.new
|
|
69
|
-
server_cert.version = 2
|
|
70
|
-
server_cert.serial = 2
|
|
71
|
-
server_cert.subject = OpenSSL::X509::Name.parse("/CN=localhost")
|
|
72
|
-
server_cert.issuer = ca_name
|
|
73
|
-
server_cert.public_key = server_key
|
|
74
|
-
server_cert.not_before = now - 60
|
|
75
|
-
server_cert.not_after = now + 86_400
|
|
76
|
-
factory = OpenSSL::X509::ExtensionFactory.new(ca_cert, server_cert)
|
|
77
|
-
server_cert.add_extension(factory.create_extension("subjectAltName", subject_alt_names(host)))
|
|
78
|
-
server_cert.add_extension(factory.create_extension("extendedKeyUsage", "serverAuth"))
|
|
79
|
-
server_cert.sign(ca_key, OpenSSL::Digest.new("SHA256"))
|
|
80
|
-
|
|
81
|
-
paths = %w[ca.pem server.pem server.key].map { |name| File.join(dir, name) }
|
|
82
|
-
File.write(paths[0], ca_cert.to_pem)
|
|
83
|
-
File.write(paths[1], server_cert.to_pem)
|
|
84
|
-
File.write(paths[2], server_key.private_to_pem)
|
|
85
|
-
paths
|
|
86
|
-
end
|
|
87
|
-
|
|
88
32
|
Dir.mktmpdir("quic-echo-server-") do |dir|
|
|
89
|
-
ca_file, certificate_path, private_key_path =
|
|
33
|
+
ca_file, certificate_path, private_key_path = DemoCertificates.generate(dir, BIND_HOST)
|
|
90
34
|
|
|
91
35
|
sock = UDPSocket.new(Socket::AF_INET)
|
|
92
36
|
sock.bind(BIND_HOST, BIND_PORT)
|