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.
@@ -0,0 +1,47 @@
1
+ #ifndef QUIC_TLS_CONTEXT_H
2
+ #define QUIC_TLS_CONTEXT_H 1
3
+
4
+ #include "quic.h"
5
+
6
+ /* A server's TLS context: the certificate chain, the signer and the ALPN
7
+ list, read once and shared by every connection made from it. Reference
8
+ counted: the QUIC::TLSContext object holds one reference and each server
9
+ connection another, so a context replaced on an Endpoint lives on until
10
+ the last connection that uses it is freed. */
11
+ typedef struct {
12
+ /* Handed to ptls_server_new. certificates.list is malloc()ed by picotls
13
+ and freed with the context. */
14
+ ptls_context_t ctx;
15
+ /* Referenced by ctx.sign_certificate; holds a reference to the key. */
16
+ ptls_openssl_sign_certificate_t sign_cert;
17
+ bool sign_cert_initialized;
18
+ /* ctx.on_client_hello points here; the callback recovers this struct
19
+ from its self argument with offsetof, so it needs no connection. */
20
+ ptls_on_client_hello_t on_client_hello;
21
+ /* The ALPN protocols the server accepts, in its order of preference. The
22
+ entries point into alpn_buf. */
23
+ ptls_iovec_t *alpn;
24
+ size_t alpn_count;
25
+ uint8_t *alpn_buf;
26
+ long refcnt;
27
+ } quic_tls_ctx_t;
28
+
29
+ extern VALUE rb_cQUICTLSContext;
30
+
31
+ void Init_quic_tls_context(VALUE rb_mQUICArg);
32
+
33
+ /* QUIC::TLSContext.server, callable from C: a new QUIC::TLSContext object
34
+ holding the one reference to a new context. alpn_ary must be a non-empty
35
+ Array of 1-255 byte Strings. Raises ArgumentError naming what could not
36
+ be loaded or used. */
37
+ VALUE quic_tls_context_server_new(VALUE certificate_path,
38
+ VALUE private_key_path, VALUE alpn_ary);
39
+
40
+ /* The context behind a QUIC::TLSContext object. Raises TypeError for any
41
+ other object. */
42
+ quic_tls_ctx_t *quic_tls_context_get(VALUE tls_context);
43
+
44
+ void quic_tls_ctx_ref(quic_tls_ctx_t *t);
45
+ void quic_tls_ctx_unref(quic_tls_ctx_t *t);
46
+
47
+ #endif /* QUIC_TLS_CONTEXT_H */
@@ -4,46 +4,11 @@ require "socket"
4
4
 
5
5
  module QUIC
6
6
  module Connection
7
- class Client
8
- attr_reader :remote_address
9
-
10
- # Build a Client and pin a single peer address. address_family takes a
11
- # Symbol (:inet for IPv4, :inet6 for IPv6) or nil to defer family
12
- # selection to Addrinfo.udp's implicit resolution. Pin the resolved
13
- # Addrinfo into @remote_address so callers can match their UDPSocket's
14
- # connected sockaddr (otherwise DNS round-robin between Client.new and
15
- # sock.connect can produce a path that ngtcp2 silently drops).
16
- def self.new(host:, port:, address_family: nil, transport_params: nil, settings: nil)
17
- family = case address_family
18
- when nil then nil
19
- when :inet then Socket::AF_INET
20
- when :inet6 then Socket::AF_INET6
21
- else
22
- raise ArgumentError, "unknown address_family: #{address_family.inspect}"
23
- end
24
-
25
- remote_address = if family
26
- Addrinfo.getaddrinfo(host, port, family, :DGRAM, Socket::IPPROTO_UDP).first
27
- else
28
- Addrinfo.udp(host, port)
29
- end
30
-
31
- remote_sockaddr = remote_address.to_sockaddr
32
- local_sockaddr = Addrinfo.udp("0.0.0.0", 0).to_sockaddr
33
-
34
- client = _open(
35
- local_sockaddr: local_sockaddr,
36
- remote_sockaddr: remote_sockaddr,
37
- server_name: host,
38
- transport_params: transport_params || QUIC::TransportParams.default,
39
- settings: settings || QUIC::Settings.default
40
- )
41
- client.instance_variable_set(:@host, host)
42
- client.instance_variable_set(:@port, port)
43
- client.instance_variable_set(:@remote_address, remote_address)
44
- client
45
- end
46
-
7
+ # The Ruby side of the I/O loop, shared by Client and Server. It is an
8
+ # internal implementation detail: not meant to be included in other
9
+ # classes, and the ivars and methods it relies on are not a public
10
+ # contract.
11
+ module Pump
47
12
  # Attach an IO.select / #send / #recvfrom-compatible UDP socket so that
48
13
  # #run and Stream blocking I/O can drive the wire transparently. Caller
49
14
  # is responsible for socket lifetime; we do not close it. The socket
@@ -61,7 +26,7 @@ module QUIC
61
26
  # QUIC::Error::NotBound if #bind has not been called. Propagates any
62
27
  # QUIC::Error subclass raised by #read_pkt / #handle_expiry.
63
28
  def run
64
- raise QUIC::Error::NotBound, "QUIC::Connection::Client#bind(sock) has not been called" if @sock.nil?
29
+ raise QUIC::Error::NotBound, "#{self.class}#bind(sock) has not been called" if @sock.nil?
65
30
 
66
31
  pump_until { handshake_completed? }
67
32
  end
@@ -72,7 +37,7 @@ module QUIC
72
37
  # fire. Public-ish so Stream blocking ops can share it; documented as
73
38
  # "internal" for users.
74
39
  def pump_once(timeout: nil)
75
- raise QUIC::Error::NotBound, "QUIC::Connection::Client#bind(sock) has not been called" if @sock.nil?
40
+ raise QUIC::Error::NotBound, "#{self.class}#bind(sock) has not been called" if @sock.nil?
76
41
 
77
42
  while (pkt = write_pkt)
78
43
  @sock.send(pkt, 0)
@@ -82,10 +47,11 @@ module QUIC
82
47
  ready = IO.select([@sock], nil, nil, wait_for)
83
48
  if ready
84
49
  data, _addr = @sock.recvfrom(2048)
85
- # Reuse the same path tuple that Client.new / Client._open passed to
86
- # ngtcp2_conn_client_new so the read_pkt path matches the stored
87
- # connection path. local is 0.0.0.0:0 (Client.new convention) and
88
- # remote comes from the connected socket (assumes #connect was used).
50
+ # Reuse the same path tuple the connection was built with so the
51
+ # read_pkt path matches the stored connection path. A Client leaves
52
+ # @local_sockaddr unset, so local is 0.0.0.0:0 (Client.new
53
+ # convention); remote comes from the connected socket (assumes
54
+ # #connect was used).
89
55
  read_pkt(
90
56
  data,
91
57
  local_sockaddr: pump_local_sockaddr,
@@ -102,9 +68,9 @@ module QUIC
102
68
  pump_once until pred.call
103
69
  end
104
70
 
105
- # Pop the next peer-initiated (server) stream off the accept queue,
106
- # driving the I/O loop until one arrives. The queue is fed by the
107
- # recv_stream_data callback the first time a server stream carries data.
71
+ # Pop the next peer-initiated stream off the accept queue, driving the
72
+ # I/O loop until one arrives. The queue is fed by the recv_stream_data
73
+ # callback the first time a peer-initiated stream carries data.
108
74
  #
109
75
  # timeout: nil block until a stream is available (requires #bind)
110
76
  # timeout: 0 return immediately; nil if the queue is empty
@@ -112,7 +78,8 @@ module QUIC
112
78
  #
113
79
  # Returns a QUIC::Stream, or nil on timeout.
114
80
  def accept_stream(timeout: nil)
115
- stream = @accept_queue.shift
81
+ require_stream_mode
82
+ stream = take_accepted_stream
116
83
  return stream unless stream.nil?
117
84
  return nil if timeout == 0
118
85
 
@@ -125,22 +92,40 @@ module QUIC
125
92
  else
126
93
  pump_once
127
94
  end
128
- stream = @accept_queue.shift
95
+ stream = take_accepted_stream
129
96
  return stream unless stream.nil?
130
97
  end
131
98
  end
132
99
 
133
100
  # Non-blocking variant of #accept_stream. Returns the next queued
134
- # server stream, or raises QUIC::Error::WaitReadable (IO::WaitReadable
135
- # mixin) when the queue is empty. Does not require #bind.
101
+ # peer-initiated stream, or raises QUIC::Error::WaitReadable
102
+ # (IO::WaitReadable mixin) when the queue is empty. Does not require
103
+ # #bind.
136
104
  def accept_stream_nonblock
137
- stream = @accept_queue.shift
138
- raise QUIC::Error::WaitReadable, "no server-initiated stream available" if stream.nil?
105
+ require_stream_mode
106
+ stream = take_accepted_stream
107
+ raise QUIC::Error::WaitReadable, "no peer-initiated stream available" if stream.nil?
139
108
  stream
140
109
  end
141
110
 
142
111
  private
143
112
 
113
+ # A C stream handler (quic/ext.h) registered on the connection owns
114
+ # every stream, so no QUIC::Stream is ever queued for #accept_stream.
115
+ def require_stream_mode
116
+ return unless @stream_handler
117
+ raise QUIC::Error::InvalidState, "the connection's streams belong to a C stream handler"
118
+ end
119
+
120
+ # The next stream off the accept queue, or nil. Telling the stream it
121
+ # has been accepted lets the peer open another one in place of a
122
+ # stream that closed while it waited in the queue.
123
+ def take_accepted_stream
124
+ stream = @accept_queue.shift
125
+ stream&.__send__(:_accepted)
126
+ stream
127
+ end
128
+
144
129
  def pump_timeout(caller_timeout)
145
130
  ngtcp2_expiry_ns = expiry
146
131
  if ngtcp2_expiry_ns.nil?
@@ -153,11 +138,111 @@ module QUIC
153
138
  end
154
139
 
155
140
  def pump_local_sockaddr
156
- @pump_local_sockaddr ||= Addrinfo.udp("0.0.0.0", 0).to_sockaddr
141
+ @local_sockaddr ||= Addrinfo.udp("0.0.0.0", 0).to_sockaddr
157
142
  end
158
143
 
159
144
  def pump_remote_sockaddr
160
- @pump_remote_sockaddr ||= @sock.remote_address.to_sockaddr
145
+ @remote_sockaddr ||= @sock.remote_address.to_sockaddr
146
+ end
147
+ end
148
+
149
+ class Client
150
+ include Pump
151
+
152
+ attr_reader :remote_address
153
+
154
+ # Build a Client and pin a single peer address. address_family takes a
155
+ # Symbol (:inet for IPv4, :inet6 for IPv6) or nil to defer family
156
+ # selection to Addrinfo.udp's implicit resolution. Pin the resolved
157
+ # Addrinfo into @remote_address so callers can match their UDPSocket's
158
+ # connected sockaddr (otherwise DNS round-robin between Client.new and
159
+ # sock.connect can produce a path that ngtcp2 silently drops).
160
+ def self.new(host:, port:, address_family: nil, transport_params: nil, settings: nil)
161
+ family = case address_family
162
+ when nil then nil
163
+ when :inet then Socket::AF_INET
164
+ when :inet6 then Socket::AF_INET6
165
+ else
166
+ raise ArgumentError, "unknown address_family: #{address_family.inspect}"
167
+ end
168
+
169
+ remote_address = if family
170
+ Addrinfo.getaddrinfo(host, port, family, :DGRAM, Socket::IPPROTO_UDP).first
171
+ else
172
+ Addrinfo.udp(host, port)
173
+ end
174
+
175
+ remote_sockaddr = remote_address.to_sockaddr
176
+ local_sockaddr = Addrinfo.udp("0.0.0.0", 0).to_sockaddr
177
+ settings ||= QUIC::Settings.default
178
+ settings.check_qlog_dir
179
+
180
+ client = _open(
181
+ local_sockaddr: local_sockaddr,
182
+ remote_sockaddr: remote_sockaddr,
183
+ server_name: host,
184
+ transport_params: transport_params || QUIC::TransportParams.default,
185
+ settings: settings
186
+ )
187
+ client.instance_variable_set(:@host, host)
188
+ client.instance_variable_set(:@port, port)
189
+ client.instance_variable_set(:@remote_address, remote_address)
190
+ client
191
+ end
192
+ end
193
+
194
+ # One server-side connection. Built by Server.accept (or Server._accept)
195
+ # from the first datagram a client sent; there is no public Server.new,
196
+ # since an object without an ngtcp2 connection behind it would be
197
+ # unusable.
198
+ class Server
199
+ include Pump
200
+
201
+ attr_reader :remote_address
202
+
203
+ private_class_method :new
204
+
205
+ # Wait for the first datagram on sock and build a connection for its
206
+ # sender. sock must be bound to the listen address and not connected.
207
+ # It is connected to the first sender, so this Server serves exactly
208
+ # that one client and shares the I/O loop with Client as is. The
209
+ # handshake is left to #run.
210
+ #
211
+ # The wait for the first datagram has no timeout; close sock from
212
+ # another thread to give up. If anything fails after that datagram
213
+ # arrived, sock stays connected to its sender: discard it and accept on
214
+ # a new socket. accept never closes sock; its lifetime is the caller's.
215
+ #
216
+ # settings must name at least one ALPN protocol, so the default
217
+ # (QUIC::Settings.default) raises ArgumentError. A qlog_dir that is not
218
+ # a writable directory raises ArgumentError before the wait.
219
+ def self.accept(sock:, certificate_path:, private_key_path:, transport_params: nil, settings: nil)
220
+ settings ||= QUIC::Settings.default
221
+ settings.check_qlog_dir
222
+
223
+ data, sender = sock.recvfrom(2048)
224
+ sock.connect(sender[3], sender[1])
225
+ # Read both addresses back from the connected socket so that the path
226
+ # given to ngtcp2 here and every later read_pkt path come from one
227
+ # source and always match.
228
+ local_sockaddr = sock.local_address.to_sockaddr
229
+ remote_sockaddr = sock.remote_address.to_sockaddr
230
+
231
+ server = _accept(
232
+ initial_packet: data,
233
+ local_sockaddr: local_sockaddr,
234
+ remote_sockaddr: remote_sockaddr,
235
+ certificate_path: certificate_path,
236
+ private_key_path: private_key_path,
237
+ transport_params: transport_params || QUIC::TransportParams.default,
238
+ settings: settings
239
+ )
240
+ server.instance_variable_set(:@local_sockaddr, local_sockaddr)
241
+ server.instance_variable_set(:@remote_sockaddr, remote_sockaddr)
242
+ server.instance_variable_set(:@remote_address, sock.remote_address)
243
+ server.bind(sock)
244
+ server.read_pkt(data, local_sockaddr: local_sockaddr, remote_sockaddr: remote_sockaddr)
245
+ server
161
246
  end
162
247
  end
163
248
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+
5
+ module QUIC
6
+ # Many server connections on one UDP socket. The Endpoint routes each
7
+ # datagram to its connection by connection ID, makes connections for new
8
+ # clients and answers what no connection can (Version Negotiation,
9
+ # Stateless Reset, CONNECTION_REFUSED). The socket and the I/O loop stay
10
+ # with the caller:
11
+ #
12
+ # loop do
13
+ # data, addr = sock.recvfrom(65_535) # or recvmsg_nonblock
14
+ # endpoint.read_pkt(data, remote: addr.to_sockaddr)
15
+ # endpoint.handle_expiry # when endpoint.expiry is due
16
+ # while (pkt, to = endpoint.write_pkt)
17
+ # sock.send(pkt, 0, to)
18
+ # end
19
+ # endpoint.take_closed.each { |conn| ... }
20
+ # end
21
+ #
22
+ # The socket must not be connected, and must be bound to the address it
23
+ # serves (not 0.0.0.0): every path's local address is local_address.
24
+ #
25
+ # The Endpoint's own answers wait for #write_pkt in a queue of at most
26
+ # 1024; any beyond are dropped, so call #write_pkt often under load.
27
+ #
28
+ # A connection's failure never raises from the Endpoint's methods. It
29
+ # closes that connection alone, and Connection#error tells why. The
30
+ # connections have no socket, so only their non-blocking methods work;
31
+ # #close queues its CONNECTION_CLOSE for #write_pkt.
32
+ class Endpoint
33
+ attr_reader :local_address
34
+
35
+ # tls_context is a QUIC::TLSContext; its ALPN list is the server's, and
36
+ # settings.alpn is not read. max_connections counts connections in their
37
+ # closing or draining period too.
38
+ def self.new(local_address:, tls_context:, transport_params: nil, settings: nil, max_connections: 1024)
39
+ raise TypeError, "local_address must be an Addrinfo (got #{local_address.class})" unless local_address.is_a?(Addrinfo)
40
+ unless max_connections.is_a?(Integer)
41
+ raise TypeError, "max_connections must be an Integer (got #{max_connections.class})"
42
+ end
43
+ settings ||= QUIC::Settings.default
44
+ settings.check_qlog_dir
45
+
46
+ endpoint = _new(
47
+ local_sockaddr: local_address.to_sockaddr,
48
+ tls_context: tls_context,
49
+ transport_params: transport_params || QUIC::TransportParams.default,
50
+ settings: settings,
51
+ max_connections: max_connections
52
+ )
53
+ endpoint.instance_variable_set(:@local_address, local_address)
54
+ endpoint
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QUIC
4
+ # The directory holding quic/ext.h, the C function table other native
5
+ # extensions build against. An extconf.rb adds it to its include path:
6
+ #
7
+ # require "quic/ext_include"
8
+ # append_cppflags("-I#{QUIC::EXT_INCLUDE_DIR}")
9
+ #
10
+ # This file does not load the extension, so extconf.rb can require it.
11
+ EXT_INCLUDE_DIR = File.expand_path("../../ext/quic/include", __dir__)
12
+ end
data/lib/quic/settings.rb CHANGED
@@ -11,7 +11,8 @@ module QUIC
11
11
  :alpn,
12
12
  :verify_mode,
13
13
  :ca_file,
14
- :ca_path
14
+ :ca_path,
15
+ :qlog_dir
15
16
  )
16
17
 
17
18
  class Settings
@@ -25,7 +26,15 @@ module QUIC
25
26
  # it; later changes to those variables or to the CA bundle are not picked
26
27
  # up. Setting ca_file or ca_path trusts only the certificates found there.
27
28
  # With verify_mode :none, ca_file and ca_path are ignored.
28
- def initialize(verify_mode: :peer, ca_file: nil, ca_path: nil, **rest)
29
+ #
30
+ # Only a client uses verify_mode, ca_file and ca_path. A server ignores
31
+ # them, since it does not ask for a client certificate.
32
+ #
33
+ # qlog_dir (a String or Pathname) makes every connection write its qlog
34
+ # to <qlog_dir>/<its first source connection ID in hex>.sqlog. A file
35
+ # that cannot be opened when a connection is made only costs that
36
+ # connection's qlog.
37
+ def initialize(verify_mode: :peer, ca_file: nil, ca_path: nil, qlog_dir: nil, **rest)
29
38
  super
30
39
  end
31
40
 
@@ -40,8 +49,22 @@ module QUIC
40
49
  alpn: [],
41
50
  verify_mode: :peer,
42
51
  ca_file: nil,
43
- ca_path: nil
52
+ ca_path: nil,
53
+ qlog_dir: nil
44
54
  )
45
55
  end
56
+
57
+ # Raises ArgumentError unless qlog_dir is nil or a directory this
58
+ # process can write to. Client.new, Connection::Server.accept and
59
+ # Endpoint.new call it, so that a wrong qlog_dir shows up when they are
60
+ # set up rather than as qlog files that never appear.
61
+ def check_qlog_dir
62
+ return if qlog_dir.nil?
63
+
64
+ path = qlog_dir.respond_to?(:to_path) ? qlog_dir.to_path : qlog_dir
65
+ return if path.is_a?(String) && File.directory?(path) && File.writable?(path)
66
+
67
+ raise ArgumentError, "qlog_dir is not a writable directory: #{path}"
68
+ end
46
69
  end
47
70
  end
data/lib/quic/stream.rb CHANGED
@@ -4,7 +4,8 @@ module QUIC
4
4
  # QUIC::Stream is defined in the C extension (ext/quic/stream.c); this file
5
5
  # reopens it to add the bits that are easier to express in Ruby:
6
6
  # #initiator (a tiny lookup over @id) and the blocking #read which wraps
7
- # the C #read_nonblock with an internal Client#pump_once loop.
7
+ # the C #read_nonblock with an internal #pump_once loop on the owning
8
+ # connection (@client, a Client or Server).
8
9
  class Stream
9
10
  INITIATORS = %i[client_bidi server_bidi client_uni server_uni].freeze
10
11
 
@@ -17,8 +18,8 @@ module QUIC
17
18
  INITIATORS[@id & 0b11]
18
19
  end
19
20
 
20
- # IO#read-compatible blocking read. Requires the parent Client to have
21
- # been #bind'ed to a socket (raised via Client#pump_once otherwise).
21
+ # IO#read-compatible blocking read. Requires the owning connection to
22
+ # have been #bind'ed to a socket (raised via its #pump_once otherwise).
22
23
  #
23
24
  # read(length): block until at least 1 byte is available or EOF,
24
25
  # return up to `length` bytes, return nil at EOF.
data/lib/quic/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module QUIC
4
- VERSION = "0.0.2"
4
+ VERSION = "0.0.4"
5
5
  end
data/lib/quic.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "quic/version"
4
+ require_relative "quic/ext_include"
4
5
 
5
6
  module QUIC
6
7
  class Error < StandardError
@@ -12,4 +13,5 @@ require "quic/quic"
12
13
  require_relative "quic/transport_params"
13
14
  require_relative "quic/settings"
14
15
  require_relative "quic/connection"
16
+ require_relative "quic/endpoint"
15
17
  require_relative "quic/stream"