mnet 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/DESIGN.md +164 -0
- data/README.en.md +125 -0
- data/README.md +118 -0
- data/lib/mnet/version.rb +5 -0
- data/lib/mnet.rb +1130 -0
- metadata +60 -0
data/lib/mnet.rb
ADDED
|
@@ -0,0 +1,1130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Mnet -- a small reliable transport over UDP with connection migration.
|
|
4
|
+
#
|
|
5
|
+
# The idea (inspired by mosh's State Synchronization Protocol, which is what
|
|
6
|
+
# actually gives you the "switch wifi and keep the session" behaviour people
|
|
7
|
+
# attribute to tmux) is:
|
|
8
|
+
#
|
|
9
|
+
# * a session is identified by a 128-bit random token, NOT by the
|
|
10
|
+
# (source ip, source port) tuple the way TCP does.
|
|
11
|
+
# * every datagram carries that token, so the peer can simply update its
|
|
12
|
+
# record of "where this session now lives" when a packet arrives from a
|
|
13
|
+
# new address. This is what makes roaming/migration transparent.
|
|
14
|
+
# * reliability (ordering + retransmit + flow control) is layered on top,
|
|
15
|
+
# because plain UDP gives you none of that.
|
|
16
|
+
#
|
|
17
|
+
# Design goals here: many concurrent sessions multiplexed over a *single*
|
|
18
|
+
# UDP socket, one reader thread + one timer thread shared by all sessions
|
|
19
|
+
# (not thread-per-connection), blocking read/write with backpressure.
|
|
20
|
+
|
|
21
|
+
require "socket"
|
|
22
|
+
require "securerandom"
|
|
23
|
+
require "monitor"
|
|
24
|
+
require "openssl"
|
|
25
|
+
require "mnet/version"
|
|
26
|
+
require "kcp"
|
|
27
|
+
|
|
28
|
+
module Mnet
|
|
29
|
+
MAGIC = "MNET".b
|
|
30
|
+
MAGIC_LEN = 4
|
|
31
|
+
SESSION_ID_LEN = 16
|
|
32
|
+
|
|
33
|
+
# Conservative MTU for mobile networks / VPN tunnels (mosh uses 1280).
|
|
34
|
+
IP_MTU = 1280
|
|
35
|
+
# IP(20) + UDP(8) + our header + GCM tag(16) leaves ~1190 for a segment.
|
|
36
|
+
DEFAULT_MSS = 1200
|
|
37
|
+
|
|
38
|
+
# RTO bounds (mosh: MIN_RTO = 50ms, MAX_RTO = 1000ms).
|
|
39
|
+
MIN_RTO = 0.05
|
|
40
|
+
MAX_RTO = 1.0
|
|
41
|
+
|
|
42
|
+
# How long to keep a superseded (hopped-away-from) socket alive to catch
|
|
43
|
+
# delayed packets (mosh: MAX_OLD_SOCKET_AGE = 60000ms).
|
|
44
|
+
MAX_OLD_SOCKET_AGE = 60.0
|
|
45
|
+
|
|
46
|
+
# Message types (mirrors the enum-message-types pattern from tmux-protocol.h).
|
|
47
|
+
TYPE_SYN = 0x01
|
|
48
|
+
TYPE_SYNACK = 0x02
|
|
49
|
+
TYPE_DATA = 0x03
|
|
50
|
+
TYPE_ACK = 0x04
|
|
51
|
+
TYPE_FIN = 0x05
|
|
52
|
+
TYPE_PING = 0x06
|
|
53
|
+
TYPE_PONG = 0x07
|
|
54
|
+
|
|
55
|
+
FLAG_NONE = 0x00
|
|
56
|
+
FLAG_MIGRATE = 0x01
|
|
57
|
+
FLAG_KCP = 0x02 # session uses the KCP (C) engine instead of the Ruby ARQ
|
|
58
|
+
|
|
59
|
+
HEADER_LEN = MAGIC_LEN + SESSION_ID_LEN + 8 + 8 + 1 + 1 + 4
|
|
60
|
+
|
|
61
|
+
MAX_RETRIES = 10
|
|
62
|
+
|
|
63
|
+
TYPES = {
|
|
64
|
+
TYPE_SYN => "SYN", TYPE_SYNACK => "SYNACK", TYPE_DATA => "DATA",
|
|
65
|
+
TYPE_ACK => "ACK", TYPE_FIN => "FIN", TYPE_PING => "PING",
|
|
66
|
+
TYPE_PONG => "PONG"
|
|
67
|
+
}.freeze
|
|
68
|
+
|
|
69
|
+
Packet = Struct.new(:session_id, :seq, :ack, :type, :flags, :window, :payload)
|
|
70
|
+
|
|
71
|
+
module_function
|
|
72
|
+
|
|
73
|
+
def pack(session_id, seq, ack, type, flags, window, payload = "".b)
|
|
74
|
+
[MAGIC, session_id, seq, ack, type, flags, window, payload]
|
|
75
|
+
.pack("a4a16Q>Q>CCNa*")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def unpack(data)
|
|
79
|
+
return nil if data.bytesize < HEADER_LEN
|
|
80
|
+
magic, sid, seq, ack, type, flags, window = data.unpack("a4a16Q>Q>CCN")
|
|
81
|
+
return nil unless magic == MAGIC
|
|
82
|
+
Packet.new(sid, seq, ack, type, flags, window,
|
|
83
|
+
data.byteslice(HEADER_LEN, data.bytesize - HEADER_LEN) || "".b)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def type_name(type)
|
|
87
|
+
TYPES.fetch(type, "UNKNOWN(#{type})")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def addr_pair(addr)
|
|
91
|
+
[addr[3], addr[1]]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def fmt(addr)
|
|
95
|
+
"#{addr[0]}:#{addr[1]}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def now
|
|
99
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Session id is derived from the shared session key (mosh-style), so the id
|
|
103
|
+
# identifies the key and vice versa; without a key it is just random.
|
|
104
|
+
def session_id_from_key(key)
|
|
105
|
+
OpenSSL::Digest::SHA256.digest(key)[0, SESSION_ID_LEN]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# A connected stream socket pair, portable across Windows/Linux/macOS.
|
|
109
|
+
# Prefers a UNIX socketpair; falls back to a loopback TCP pair where AF_UNIX
|
|
110
|
+
# socketpair is unavailable (Windows).
|
|
111
|
+
def socket_pair
|
|
112
|
+
Socket.pair(:UNIX, :STREAM, 0)
|
|
113
|
+
rescue Errno::EAFNOSUPPORT, Errno::EOPNOTSUPP, Errno::EPROTONOSUPPORT, Errno::ENOTSUP, NotImplementedError
|
|
114
|
+
server = TCPServer.new("127.0.0.1", 0)
|
|
115
|
+
port = server.addr[1]
|
|
116
|
+
client = TCPSocket.new("127.0.0.1", port)
|
|
117
|
+
accepted = server.accept
|
|
118
|
+
server.close
|
|
119
|
+
[accepted, client]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# One segment in flight on the send side.
|
|
123
|
+
Frame = Struct.new(:seq, :len, :payload, :sent_at, :retries)
|
|
124
|
+
|
|
125
|
+
# IO-compatible interface so a Session can be passed directly to
|
|
126
|
+
# OpenSSL::SSL::SSLSocket -- this removes the socketpair bridge (and its two
|
|
127
|
+
# thread hops per message), which is the main Ruby-side throughput cost.
|
|
128
|
+
module SessionIO
|
|
129
|
+
def to_io
|
|
130
|
+
self
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def sync
|
|
134
|
+
true
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def sync=(_value)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Hook overridden by KcpSession to pull decoded bytes out of the engine.
|
|
141
|
+
def pump_recv
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Hook overridden by Session to advertise a reopened receive window.
|
|
145
|
+
def after_read
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def read(length = nil, outbuf = nil)
|
|
149
|
+
@m.synchronize do
|
|
150
|
+
loop do
|
|
151
|
+
pump_recv
|
|
152
|
+
unless @recv_buf.empty?
|
|
153
|
+
n = length.nil? ? @recv_buf.bytesize : [length, @recv_buf.bytesize].min
|
|
154
|
+
out = @recv_buf.byteslice(0, n)
|
|
155
|
+
@recv_buf = @recv_buf.byteslice(n, @recv_buf.bytesize - n) || "".b
|
|
156
|
+
after_read
|
|
157
|
+
return outbuf ? outbuf.replace(out) : out
|
|
158
|
+
end
|
|
159
|
+
return nil if @eof || @closed
|
|
160
|
+
@cv.wait
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def readpartial(maxlen, outbuf = nil)
|
|
166
|
+
raise ArgumentError, "non-positive maxlen" if maxlen <= 0
|
|
167
|
+
@m.synchronize do
|
|
168
|
+
loop do
|
|
169
|
+
pump_recv
|
|
170
|
+
unless @recv_buf.empty?
|
|
171
|
+
n = [maxlen, @recv_buf.bytesize].min
|
|
172
|
+
out = @recv_buf.byteslice(0, n)
|
|
173
|
+
@recv_buf = @recv_buf.byteslice(n, @recv_buf.bytesize - n) || "".b
|
|
174
|
+
after_read
|
|
175
|
+
return outbuf ? outbuf.replace(out) : out
|
|
176
|
+
end
|
|
177
|
+
raise EOFError, "end of file reached" if @eof || @closed
|
|
178
|
+
@cv.wait
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def read_nonblock(maxlen, outbuf = nil, exception: true)
|
|
184
|
+
@m.synchronize do
|
|
185
|
+
pump_recv
|
|
186
|
+
unless @recv_buf.empty?
|
|
187
|
+
n = [maxlen, @recv_buf.bytesize].min
|
|
188
|
+
out = @recv_buf.byteslice(0, n)
|
|
189
|
+
@recv_buf = @recv_buf.byteslice(n, @recv_buf.bytesize - n) || "".b
|
|
190
|
+
after_read
|
|
191
|
+
return outbuf ? outbuf.replace(out) : out
|
|
192
|
+
end
|
|
193
|
+
return nil if @eof || @closed
|
|
194
|
+
raise IO::WaitReadable if exception
|
|
195
|
+
:wait_readable
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def write_nonblock(data, exception: true)
|
|
200
|
+
write(data)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def sysread(maxlen, outbuf = nil)
|
|
204
|
+
readpartial(maxlen, outbuf)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def syswrite(data)
|
|
208
|
+
write(data)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def wait_readable(timeout = nil)
|
|
212
|
+
@m.synchronize do
|
|
213
|
+
pump_recv
|
|
214
|
+
return true unless @recv_buf.empty?
|
|
215
|
+
return nil if @eof || @closed
|
|
216
|
+
if timeout
|
|
217
|
+
@cv.wait(timeout)
|
|
218
|
+
else
|
|
219
|
+
@cv.wait
|
|
220
|
+
end
|
|
221
|
+
pump_recv # data may have arrived via the engine while we waited
|
|
222
|
+
!@recv_buf.empty?
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def wait_writable(_timeout = nil)
|
|
227
|
+
true
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Close the session-side end of the bridge so the @up pump thread gets EOF.
|
|
231
|
+
def close_bridge
|
|
232
|
+
return unless @bridge_transport
|
|
233
|
+
@bridge_transport.shutdown rescue nil
|
|
234
|
+
@bridge_transport.close rescue nil
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# A real kernel IO (socketpair bridge) wrapping this session, for the few
|
|
238
|
+
# consumers that require a genuine File/IO -- notably OpenSSL::SSL::SSLSocket
|
|
239
|
+
# (its C init does `Check_Type(io, T_FILE)`). Only used when needed; the
|
|
240
|
+
# session itself is already IO-compatible via the methods above.
|
|
241
|
+
def bridge
|
|
242
|
+
return @bridge_io if @bridge_io
|
|
243
|
+
|
|
244
|
+
app, transport = Mnet.socket_pair
|
|
245
|
+
@bridge_transport = transport
|
|
246
|
+
[app, transport].each do |s|
|
|
247
|
+
s.sync = true
|
|
248
|
+
s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) rescue nil
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
@down = Thread.new do
|
|
252
|
+
begin
|
|
253
|
+
while (data = read)
|
|
254
|
+
transport.write(data)
|
|
255
|
+
end
|
|
256
|
+
rescue IOError, EOFError, Errno::ECONNRESET, Errno::EPIPE
|
|
257
|
+
ensure
|
|
258
|
+
transport.close_write rescue nil
|
|
259
|
+
close rescue nil
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
@up = Thread.new do
|
|
264
|
+
begin
|
|
265
|
+
while (data = transport.readpartial(65_536))
|
|
266
|
+
write(data)
|
|
267
|
+
end
|
|
268
|
+
rescue EOFError, IOError, Errno::ECONNRESET, Errno::EPIPE
|
|
269
|
+
ensure
|
|
270
|
+
close rescue nil
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
@bridge_io = app
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
class Session
|
|
279
|
+
include SessionIO
|
|
280
|
+
|
|
281
|
+
attr_reader :id, :state, :peer_addr
|
|
282
|
+
|
|
283
|
+
def initialize(endpoint, id, role:, peer_addr: nil, key: nil, logger: nil, **opts)
|
|
284
|
+
@endpoint = endpoint
|
|
285
|
+
@id = id
|
|
286
|
+
@role = role
|
|
287
|
+
@peer_addr = peer_addr
|
|
288
|
+
@logger = logger
|
|
289
|
+
@key = key # optional 32-byte session key (AES-256-GCM); nil = plaintext
|
|
290
|
+
|
|
291
|
+
@m = Monitor.new
|
|
292
|
+
@cv = @m.new_cond
|
|
293
|
+
|
|
294
|
+
@mss = opts.fetch(:mss, Mnet::DEFAULT_MSS)
|
|
295
|
+
@recv_cap = opts.fetch(:recv_capacity, 256 * 1024)
|
|
296
|
+
@max_out = opts.fetch(:max_outstanding, 4 * 1024 * 1024)
|
|
297
|
+
@rto = opts.fetch(:rto, 0.3)
|
|
298
|
+
@ping_after = opts.fetch(:ping_after, 15.0)
|
|
299
|
+
@idle_timeout = opts.fetch(:idle_timeout, 60.0)
|
|
300
|
+
@syn_retry = opts.fetch(:syn_retry, 0.25)
|
|
301
|
+
|
|
302
|
+
@state = role == :client ? :connecting : :established
|
|
303
|
+
|
|
304
|
+
# Send side (byte-offset sequence numbers, like TCP).
|
|
305
|
+
@next_seq = 0
|
|
306
|
+
@send_buf = "".b # bytes queued but not yet transmitted
|
|
307
|
+
@inflight = [] # ordered list of Frame
|
|
308
|
+
@inflight_bytes = 0
|
|
309
|
+
@outstanding = 0 # queued + inflight bytes (backpressure)
|
|
310
|
+
|
|
311
|
+
@peer_window = 0 # last receive window advertised by peer
|
|
312
|
+
|
|
313
|
+
# RTT estimation (RFC 6298 SRTT/RTTVAR), driving RTO.
|
|
314
|
+
@srtt = nil
|
|
315
|
+
@rttvar = nil
|
|
316
|
+
|
|
317
|
+
# Receive side.
|
|
318
|
+
@next_exp = 0 # next expected byte offset (cumulative ack)
|
|
319
|
+
@recv_buf = "".b # delivered, ordered, not yet read by app
|
|
320
|
+
@reasm = {} # seq => payload (out of order)
|
|
321
|
+
@reasm_bytes = 0
|
|
322
|
+
@last_window = 0
|
|
323
|
+
|
|
324
|
+
# Liveness.
|
|
325
|
+
@last_recv = Mnet.now
|
|
326
|
+
@last_send = Mnet.now
|
|
327
|
+
|
|
328
|
+
@eof = false
|
|
329
|
+
@closed = false
|
|
330
|
+
@bridge_io = nil
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def established?
|
|
334
|
+
@state == :established
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def closed?
|
|
338
|
+
@closed
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def eof?
|
|
342
|
+
@eof
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# ---- Application API -------------------------------------------------
|
|
346
|
+
|
|
347
|
+
def write(data)
|
|
348
|
+
data = data.to_s.b
|
|
349
|
+
return 0 if data.empty?
|
|
350
|
+
|
|
351
|
+
@m.synchronize do
|
|
352
|
+
@cv.wait_while { !@closed && !@eof && (@outstanding >= @max_out || unsendable?) }
|
|
353
|
+
return 0 if @closed || @eof
|
|
354
|
+
@send_buf << data
|
|
355
|
+
@outstanding += data.bytesize
|
|
356
|
+
end
|
|
357
|
+
pump
|
|
358
|
+
data.bytesize
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def after_read
|
|
362
|
+
@cv.broadcast
|
|
363
|
+
maybe_advertise_window
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def close
|
|
367
|
+
@m.synchronize do
|
|
368
|
+
return if @closed
|
|
369
|
+
send_packet(Mnet::TYPE_FIN, 0, @next_exp) if @state != :connecting
|
|
370
|
+
@closed = true
|
|
371
|
+
@eof = true
|
|
372
|
+
@cv.broadcast
|
|
373
|
+
close_bridge
|
|
374
|
+
end
|
|
375
|
+
@endpoint.remove_session(@id)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# ---- Internal: called by Endpoint threads ----------------------------
|
|
379
|
+
|
|
380
|
+
def handle_packet(pkt, addr)
|
|
381
|
+
@m.synchronize do
|
|
382
|
+
return if @closed
|
|
383
|
+
@last_recv = Mnet.now
|
|
384
|
+
update_peer(addr)
|
|
385
|
+
|
|
386
|
+
if @key
|
|
387
|
+
header = Mnet.pack(pkt.session_id, pkt.seq, pkt.ack, pkt.type, pkt.flags, pkt.window, "")
|
|
388
|
+
pkt.payload = decrypt_payload(header, pkt.payload)
|
|
389
|
+
return if pkt.payload.nil? # failed authentication / wrong key
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
case pkt.type
|
|
393
|
+
when Mnet::TYPE_SYN then on_syn(pkt)
|
|
394
|
+
when Mnet::TYPE_SYNACK then on_synack(pkt)
|
|
395
|
+
when Mnet::TYPE_DATA then on_data(pkt)
|
|
396
|
+
when Mnet::TYPE_ACK then apply_ack(pkt.ack, pkt.window)
|
|
397
|
+
when Mnet::TYPE_FIN then on_fin
|
|
398
|
+
when Mnet::TYPE_PING then send_packet(Mnet::TYPE_PONG, 0, @next_exp)
|
|
399
|
+
when Mnet::TYPE_PONG then nil
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def tick(now)
|
|
405
|
+
@m.synchronize do
|
|
406
|
+
return if @closed
|
|
407
|
+
|
|
408
|
+
if @state == :connecting
|
|
409
|
+
# Handshake is not yet reliable: keep retrying SYN until SYNACK.
|
|
410
|
+
send_packet(Mnet::TYPE_SYN, 0, 0) if now - @last_send >= @syn_retry
|
|
411
|
+
return
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
if !@inflight.empty?
|
|
415
|
+
f = @inflight.first
|
|
416
|
+
if now - f.sent_at >= @rto
|
|
417
|
+
if f.retries >= Mnet::MAX_RETRIES
|
|
418
|
+
log("giving up after #{f.retries} retries")
|
|
419
|
+
teardown
|
|
420
|
+
return
|
|
421
|
+
end
|
|
422
|
+
@rto = [@rto * 2, Mnet::MAX_RTO].min
|
|
423
|
+
f.sent_at = now
|
|
424
|
+
f.retries += 1
|
|
425
|
+
send_packet(Mnet::TYPE_DATA, f.seq, @next_exp, f.payload)
|
|
426
|
+
end
|
|
427
|
+
elsif now - @last_send >= @ping_after
|
|
428
|
+
send_packet(Mnet::TYPE_PING, 0, @next_exp)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
teardown if now - @last_recv >= @idle_timeout
|
|
432
|
+
|
|
433
|
+
pump unless @send_buf.empty?
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def send_syn
|
|
438
|
+
@m.synchronize { send_packet(Mnet::TYPE_SYN, 0, 0) }
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def wait_established(timeout)
|
|
442
|
+
@m.synchronize { @cv.wait(timeout) if @state == :connecting }
|
|
443
|
+
raise "connect timed out" unless @state == :established
|
|
444
|
+
self
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def reanchor
|
|
448
|
+
@m.synchronize do
|
|
449
|
+
send_packet(Mnet::TYPE_PING, 0, @next_exp) if @state == :established
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
private
|
|
454
|
+
|
|
455
|
+
def pump
|
|
456
|
+
@m.synchronize do
|
|
457
|
+
return if @state != :established || @closed
|
|
458
|
+
|
|
459
|
+
loop do
|
|
460
|
+
break if @send_buf.empty?
|
|
461
|
+
limit = @peer_window
|
|
462
|
+
break if @inflight_bytes >= limit
|
|
463
|
+
size = [@mss, @send_buf.bytesize, limit - @inflight_bytes].min
|
|
464
|
+
break if size <= 0
|
|
465
|
+
|
|
466
|
+
payload = @send_buf.byteslice(0, size)
|
|
467
|
+
seq = @next_seq
|
|
468
|
+
|
|
469
|
+
break unless send_packet(Mnet::TYPE_DATA, seq, @next_exp, payload)
|
|
470
|
+
|
|
471
|
+
@send_buf = @send_buf.byteslice(size, @send_buf.bytesize - size) || "".b
|
|
472
|
+
@next_seq += size
|
|
473
|
+
@inflight << Mnet::Frame.new(seq, size, payload, Mnet.now, 0)
|
|
474
|
+
@inflight_bytes += size
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def unsendable?
|
|
480
|
+
!@send_buf.empty? && @inflight_bytes >= @peer_window
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def apply_ack(ack, window)
|
|
484
|
+
@peer_window = window if window
|
|
485
|
+
return if ack <= 0
|
|
486
|
+
|
|
487
|
+
before = @inflight_bytes
|
|
488
|
+
acked = 0
|
|
489
|
+
rtt_sample_at = nil
|
|
490
|
+
@inflight.reject! do |f|
|
|
491
|
+
next false unless f.seq + f.len <= ack
|
|
492
|
+
acked += f.len
|
|
493
|
+
@inflight_bytes -= f.len
|
|
494
|
+
@outstanding -= f.len
|
|
495
|
+
rtt_sample_at ||= f.sent_at
|
|
496
|
+
true
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
return unless @inflight_bytes < before
|
|
500
|
+
|
|
501
|
+
update_rtt(Mnet.now - rtt_sample_at) if rtt_sample_at
|
|
502
|
+
@cv.broadcast
|
|
503
|
+
pump
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# RFC 6298 SRTT/RTTVAR -> RTO (mosh-style: RTT-driven, no congestion window).
|
|
507
|
+
def update_rtt(sample)
|
|
508
|
+
if @srtt.nil?
|
|
509
|
+
@srtt = sample
|
|
510
|
+
@rttvar = sample / 2.0
|
|
511
|
+
else
|
|
512
|
+
@rttvar = 0.75 * @rttvar + 0.25 * (@srtt - sample).abs
|
|
513
|
+
@srtt = 0.875 * @srtt + 0.125 * sample
|
|
514
|
+
end
|
|
515
|
+
@rto = @srtt + 4.0 * @rttvar
|
|
516
|
+
@rto = Mnet::MIN_RTO if @rto < Mnet::MIN_RTO
|
|
517
|
+
@rto = Mnet::MAX_RTO if @rto > Mnet::MAX_RTO
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def on_syn(pkt)
|
|
521
|
+
@peer_window = pkt.window
|
|
522
|
+
send_packet(Mnet::TYPE_SYNACK, 0, @next_exp)
|
|
523
|
+
@state = :established
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
def on_synack(pkt)
|
|
527
|
+
@peer_window = pkt.window
|
|
528
|
+
@state = :established
|
|
529
|
+
@cv.broadcast
|
|
530
|
+
pump
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def on_data(pkt)
|
|
534
|
+
apply_ack(pkt.ack, pkt.window) if pkt.ack > 0
|
|
535
|
+
|
|
536
|
+
data = pkt.payload
|
|
537
|
+
return if data.empty?
|
|
538
|
+
|
|
539
|
+
seq = pkt.seq
|
|
540
|
+
if seq == @next_exp
|
|
541
|
+
@recv_buf << data
|
|
542
|
+
@next_exp += data.bytesize
|
|
543
|
+
while (seg = @reasm.delete(@next_exp))
|
|
544
|
+
@recv_buf << seg
|
|
545
|
+
@next_exp += seg.bytesize
|
|
546
|
+
@reasm_bytes -= seg.bytesize
|
|
547
|
+
end
|
|
548
|
+
@cv.broadcast
|
|
549
|
+
elsif seq > @next_exp
|
|
550
|
+
if (seq - @next_exp) <= @recv_cap && !@reasm.key?(seq)
|
|
551
|
+
@reasm[seq] = data
|
|
552
|
+
@reasm_bytes += data.bytesize
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
send_packet(Mnet::TYPE_ACK, 0, @next_exp)
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def on_fin
|
|
559
|
+
@eof = true
|
|
560
|
+
send_packet(Mnet::TYPE_FIN, 0, @next_exp)
|
|
561
|
+
@cv.broadcast
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def maybe_advertise_window
|
|
565
|
+
w = recv_window
|
|
566
|
+
if @state == :established && @peer_addr && w >= @last_window + @mss
|
|
567
|
+
send_packet(Mnet::TYPE_ACK, 0, @next_exp)
|
|
568
|
+
@last_window = w
|
|
569
|
+
end
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def recv_window
|
|
573
|
+
[@recv_cap - (@recv_buf.bytesize + @reasm_bytes), 0].max
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def send_packet(type, seq, ack, payload = "".b, flags = Mnet::FLAG_NONE)
|
|
577
|
+
if @key
|
|
578
|
+
header = Mnet.pack(@id, seq, ack, type, flags, recv_window, "")
|
|
579
|
+
packet = header + encrypt_payload(header, payload)
|
|
580
|
+
else
|
|
581
|
+
packet = Mnet.pack(@id, seq, ack, type, flags, recv_window, payload)
|
|
582
|
+
end
|
|
583
|
+
sent = @endpoint.send_raw(packet, *@peer_addr)
|
|
584
|
+
@last_send = Mnet.now if sent
|
|
585
|
+
sent
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
# AES-256-GCM payload encryption with the cleartext header as AAD, so both
|
|
589
|
+
# payload confidentiality/integrity and header integrity are enforced.
|
|
590
|
+
# Returns nil if authenticated decryption fails (drop the packet).
|
|
591
|
+
def encrypt_payload(header, payload)
|
|
592
|
+
return payload unless @key
|
|
593
|
+
nonce = SecureRandom.random_bytes(12)
|
|
594
|
+
cipher = OpenSSL::Cipher.new("aes-256-gcm")
|
|
595
|
+
cipher.encrypt
|
|
596
|
+
cipher.key = @key
|
|
597
|
+
cipher.iv = nonce
|
|
598
|
+
cipher.auth_data = header
|
|
599
|
+
ciphertext = cipher.update(payload) + cipher.final
|
|
600
|
+
nonce + ciphertext + cipher.auth_tag
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def decrypt_payload(header, blob)
|
|
604
|
+
return blob unless @key
|
|
605
|
+
return nil if blob.bytesize < 28 # 12-byte nonce + 16-byte tag minimum
|
|
606
|
+
|
|
607
|
+
nonce = blob.byteslice(0, 12)
|
|
608
|
+
ct = blob.byteslice(12, blob.bytesize - 12)
|
|
609
|
+
return nil if ct.bytesize < 16
|
|
610
|
+
|
|
611
|
+
cipher = OpenSSL::Cipher.new("aes-256-gcm")
|
|
612
|
+
cipher.decrypt
|
|
613
|
+
cipher.key = @key
|
|
614
|
+
cipher.iv = nonce
|
|
615
|
+
cipher.auth_tag = ct.byteslice(ct.bytesize - 16, 16)
|
|
616
|
+
cipher.auth_data = header
|
|
617
|
+
cipher.update(ct.byteslice(0, ct.bytesize - 16)) + cipher.final
|
|
618
|
+
rescue OpenSSL::Cipher::CipherError, OpenSSL::OpenSSLError
|
|
619
|
+
nil
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
def update_peer(addr)
|
|
623
|
+
pair = Mnet.addr_pair(addr)
|
|
624
|
+
if @peer_addr.nil?
|
|
625
|
+
@peer_addr = pair
|
|
626
|
+
elsif @peer_addr[0] != pair[0] || @peer_addr[1] != pair[1]
|
|
627
|
+
log("migrated #{Mnet.fmt(@peer_addr)} -> #{Mnet.fmt(pair)}")
|
|
628
|
+
@peer_addr = pair
|
|
629
|
+
end
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
def teardown
|
|
633
|
+
@closed = true
|
|
634
|
+
@eof = true
|
|
635
|
+
@cv.broadcast
|
|
636
|
+
@endpoint.remove_session(@id)
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
def log(msg)
|
|
640
|
+
@logger&.call("[#{short_id}] #{msg}")
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
def short_id
|
|
644
|
+
@id.unpack1("H*")[0, 8]
|
|
645
|
+
end
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
# Session with the reliable byte stream provided by the KCP (C) engine
|
|
649
|
+
# instead of the pure-Ruby ARQ. The migration layer (session token, peer
|
|
650
|
+
# address update, control handshake, socketpair bridge, optional AES-GCM)
|
|
651
|
+
# is identical to Session.
|
|
652
|
+
class KcpSession
|
|
653
|
+
include SessionIO
|
|
654
|
+
|
|
655
|
+
attr_reader :id, :state, :peer_addr
|
|
656
|
+
|
|
657
|
+
def initialize(endpoint, id, role:, peer_addr: nil, key: nil, logger: nil, **opts)
|
|
658
|
+
@endpoint = endpoint
|
|
659
|
+
@id = id
|
|
660
|
+
@role = role
|
|
661
|
+
@peer_addr = peer_addr
|
|
662
|
+
@key = key
|
|
663
|
+
@logger = logger
|
|
664
|
+
|
|
665
|
+
@conv = id.unpack1("N") # KCP conv derived from the session id
|
|
666
|
+
@engine = Kcp::Engine.new(@conv)
|
|
667
|
+
|
|
668
|
+
@m = Monitor.new
|
|
669
|
+
@cv = @m.new_cond
|
|
670
|
+
|
|
671
|
+
@ping_after = opts.fetch(:ping_after, 15.0)
|
|
672
|
+
@idle_timeout = opts.fetch(:idle_timeout, 60.0)
|
|
673
|
+
@syn_retry = opts.fetch(:syn_retry, 0.25)
|
|
674
|
+
|
|
675
|
+
@state = role == :client ? :connecting : :established
|
|
676
|
+
|
|
677
|
+
@recv_buf = "".b
|
|
678
|
+
@last_recv = Mnet.now
|
|
679
|
+
@last_send = Mnet.now
|
|
680
|
+
@eof = false
|
|
681
|
+
@closed = false
|
|
682
|
+
@bridge_io = nil
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
def established?
|
|
686
|
+
@state == :established
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
def closed?
|
|
690
|
+
@closed
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
def eof?
|
|
694
|
+
@eof
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
def write(data)
|
|
698
|
+
data = data.to_s.b
|
|
699
|
+
return 0 if data.empty?
|
|
700
|
+
|
|
701
|
+
@m.synchronize do
|
|
702
|
+
return 0 if @closed || @eof
|
|
703
|
+
@engine.send(data)
|
|
704
|
+
pump_output
|
|
705
|
+
end
|
|
706
|
+
# 窗口满(有积压)时让出 GVL,避免写线程紧循环饿死处理 ACK 的传输线程。
|
|
707
|
+
Thread.pass if @engine.waitsnd > 0
|
|
708
|
+
data.bytesize
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
def pump_recv
|
|
712
|
+
return if @closed # engine is freed in close
|
|
713
|
+
while (chunk = @engine.recv)
|
|
714
|
+
@recv_buf << chunk
|
|
715
|
+
end
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
def close
|
|
719
|
+
@m.synchronize do
|
|
720
|
+
return if @closed
|
|
721
|
+
send_control(Mnet::TYPE_FIN) if @state != :connecting
|
|
722
|
+
@closed = true
|
|
723
|
+
@eof = true
|
|
724
|
+
@engine.close # free the C engine while holding @m (no race with read)
|
|
725
|
+
@cv.broadcast
|
|
726
|
+
close_bridge
|
|
727
|
+
end
|
|
728
|
+
@endpoint.remove_session(@id)
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
def handle_packet(pkt, addr)
|
|
732
|
+
@m.synchronize do
|
|
733
|
+
return if @closed
|
|
734
|
+
@last_recv = Mnet.now
|
|
735
|
+
update_peer(addr)
|
|
736
|
+
|
|
737
|
+
case pkt.type
|
|
738
|
+
when Mnet::TYPE_SYN then on_syn(pkt)
|
|
739
|
+
when Mnet::TYPE_SYNACK then on_synack
|
|
740
|
+
when Mnet::TYPE_DATA then on_data(pkt)
|
|
741
|
+
when Mnet::TYPE_PING then send_control(Mnet::TYPE_PONG)
|
|
742
|
+
when Mnet::TYPE_PONG then nil
|
|
743
|
+
when Mnet::TYPE_FIN then on_fin
|
|
744
|
+
end
|
|
745
|
+
end
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
def tick(now)
|
|
749
|
+
@m.synchronize do
|
|
750
|
+
return if @closed
|
|
751
|
+
@engine.update(now_ms) # timer-based retransmit
|
|
752
|
+
pump_output
|
|
753
|
+
|
|
754
|
+
if @state == :connecting
|
|
755
|
+
send_syn if now - @last_send >= @syn_retry
|
|
756
|
+
elsif now - @last_send >= @ping_after
|
|
757
|
+
send_control(Mnet::TYPE_PING)
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
teardown if now - @last_recv >= @idle_timeout
|
|
761
|
+
end
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
def send_syn
|
|
765
|
+
@m.synchronize { send_control(Mnet::TYPE_SYN) }
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
def wait_established(timeout)
|
|
769
|
+
@m.synchronize { @cv.wait(timeout) if @state == :connecting }
|
|
770
|
+
raise "connect timed out" unless @state == :established
|
|
771
|
+
self
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
def reanchor
|
|
775
|
+
@m.synchronize do
|
|
776
|
+
send_control(Mnet::TYPE_PING) if @state == :established
|
|
777
|
+
end
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
private
|
|
781
|
+
|
|
782
|
+
def now_ms
|
|
783
|
+
(Mnet.now * 1000).to_i & 0xffffffff
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
def pump_output
|
|
787
|
+
@engine.flush
|
|
788
|
+
while (pkt = @engine.output)
|
|
789
|
+
send_data_packet(pkt)
|
|
790
|
+
end
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
def send_data_packet(kcp_payload)
|
|
794
|
+
header = Mnet.pack(@id, 0, 0, Mnet::TYPE_DATA, Mnet::FLAG_KCP, 0, "")
|
|
795
|
+
body = @key ? encrypt_payload(header, kcp_payload) : kcp_payload
|
|
796
|
+
@endpoint.send_raw(header + body, *@peer_addr)
|
|
797
|
+
@last_send = Mnet.now
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
def send_control(type)
|
|
801
|
+
header = Mnet.pack(@id, 0, 0, type, Mnet::FLAG_KCP, 0, "")
|
|
802
|
+
body = @key ? encrypt_payload(header, "") : ""
|
|
803
|
+
@endpoint.send_raw(header + body, *@peer_addr)
|
|
804
|
+
@last_send = Mnet.now
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
def on_syn(pkt)
|
|
808
|
+
@state = :established
|
|
809
|
+
send_control(Mnet::TYPE_SYNACK)
|
|
810
|
+
end
|
|
811
|
+
|
|
812
|
+
def on_synack
|
|
813
|
+
@state = :established
|
|
814
|
+
@cv.broadcast
|
|
815
|
+
end
|
|
816
|
+
|
|
817
|
+
def on_data(pkt)
|
|
818
|
+
header = Mnet.pack(pkt.session_id, pkt.seq, pkt.ack, pkt.type, pkt.flags, pkt.window, "")
|
|
819
|
+
payload = @key ? decrypt_payload(header, pkt.payload) : pkt.payload
|
|
820
|
+
return if payload.nil?
|
|
821
|
+
@engine.input(payload)
|
|
822
|
+
pump_output
|
|
823
|
+
@cv.broadcast
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
def on_fin
|
|
827
|
+
@eof = true
|
|
828
|
+
send_control(Mnet::TYPE_FIN)
|
|
829
|
+
@cv.broadcast
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
def encrypt_payload(header, payload)
|
|
833
|
+
return payload unless @key
|
|
834
|
+
nonce = SecureRandom.random_bytes(12)
|
|
835
|
+
cipher = OpenSSL::Cipher.new("aes-256-gcm")
|
|
836
|
+
cipher.encrypt
|
|
837
|
+
cipher.key = @key
|
|
838
|
+
cipher.iv = nonce
|
|
839
|
+
cipher.auth_data = header
|
|
840
|
+
nonce + cipher.update(payload) + cipher.final + cipher.auth_tag
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
def decrypt_payload(header, blob)
|
|
844
|
+
return blob unless @key
|
|
845
|
+
return nil if blob.bytesize < 28
|
|
846
|
+
|
|
847
|
+
nonce = blob.byteslice(0, 12)
|
|
848
|
+
ct = blob.byteslice(12, blob.bytesize - 12)
|
|
849
|
+
return nil if ct.bytesize < 16
|
|
850
|
+
|
|
851
|
+
cipher = OpenSSL::Cipher.new("aes-256-gcm")
|
|
852
|
+
cipher.decrypt
|
|
853
|
+
cipher.key = @key
|
|
854
|
+
cipher.iv = nonce
|
|
855
|
+
cipher.auth_tag = ct.byteslice(ct.bytesize - 16, 16)
|
|
856
|
+
cipher.auth_data = header
|
|
857
|
+
cipher.update(ct.byteslice(0, ct.bytesize - 16)) + cipher.final
|
|
858
|
+
rescue OpenSSL::Cipher::CipherError, OpenSSL::OpenSSLError
|
|
859
|
+
nil
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
def update_peer(addr)
|
|
863
|
+
pair = Mnet.addr_pair(addr)
|
|
864
|
+
if @peer_addr.nil?
|
|
865
|
+
@peer_addr = pair
|
|
866
|
+
elsif @peer_addr[0] != pair[0] || @peer_addr[1] != pair[1]
|
|
867
|
+
log("migrated #{Mnet.fmt(@peer_addr)} -> #{Mnet.fmt(pair)}")
|
|
868
|
+
@peer_addr = pair
|
|
869
|
+
end
|
|
870
|
+
end
|
|
871
|
+
|
|
872
|
+
def teardown
|
|
873
|
+
@closed = true
|
|
874
|
+
@eof = true
|
|
875
|
+
@cv.broadcast
|
|
876
|
+
@endpoint.remove_session(@id)
|
|
877
|
+
end
|
|
878
|
+
|
|
879
|
+
def log(msg)
|
|
880
|
+
@logger&.call("[#{short_id}] #{msg}")
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
def short_id
|
|
884
|
+
@id.unpack1("H*")[0, 8]
|
|
885
|
+
end
|
|
886
|
+
end
|
|
887
|
+
|
|
888
|
+
class Endpoint
|
|
889
|
+
TICK_INTERVAL = 0.05
|
|
890
|
+
attr_reader :socket
|
|
891
|
+
|
|
892
|
+
def initialize(logger: nil, keys: nil, **opts)
|
|
893
|
+
@opts = opts
|
|
894
|
+
@logger = logger
|
|
895
|
+
@socks = [] # [socket, created_at] pairs; newest last
|
|
896
|
+
@send_mutex = Mutex.new
|
|
897
|
+
@sessions = {}
|
|
898
|
+
@sessions_mutex = Mutex.new
|
|
899
|
+
@accept = Queue.new
|
|
900
|
+
@running = false
|
|
901
|
+
@bound = false
|
|
902
|
+
|
|
903
|
+
# Receive window (flow control): capped dynamically to the kernel's
|
|
904
|
+
# actual socket buffer size (Linux clamps via net.core.rmem_max).
|
|
905
|
+
@recv_cap = opts.fetch(:recv_capacity, 256 * 1024)
|
|
906
|
+
|
|
907
|
+
# Pre-shared session keys (optional): id -> key, derived like mosh.
|
|
908
|
+
@key_by_id = {}
|
|
909
|
+
(keys || []).each { |k| @key_by_id[Mnet.session_id_from_key(k)] = k }
|
|
910
|
+
end
|
|
911
|
+
|
|
912
|
+
def listen(host = "0.0.0.0", port = 0)
|
|
913
|
+
sock = UDPSocket.new
|
|
914
|
+
configure_socket(sock)
|
|
915
|
+
sock.bind(host, port)
|
|
916
|
+
@socks << [sock, Mnet.now]
|
|
917
|
+
@bound = true
|
|
918
|
+
start
|
|
919
|
+
self
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
def dial(host, port, timeout: 5, key: nil, proto: :mnet)
|
|
923
|
+
ensure_bound
|
|
924
|
+
id = key ? Mnet.session_id_from_key(key) : SecureRandom.random_bytes(Mnet::SESSION_ID_LEN)
|
|
925
|
+
klass = proto == :kcp ? KcpSession : Session
|
|
926
|
+
sess = klass.new(self, id, role: :client, peer_addr: [host, port],
|
|
927
|
+
key: key, logger: @logger, **@opts.merge(recv_capacity: @recv_cap))
|
|
928
|
+
register(sess)
|
|
929
|
+
begin
|
|
930
|
+
sess.send_syn
|
|
931
|
+
sess.wait_established(timeout)
|
|
932
|
+
rescue
|
|
933
|
+
remove_session(id)
|
|
934
|
+
raise
|
|
935
|
+
end
|
|
936
|
+
end
|
|
937
|
+
|
|
938
|
+
def accept
|
|
939
|
+
@accept.pop
|
|
940
|
+
end
|
|
941
|
+
|
|
942
|
+
def local_addr
|
|
943
|
+
@socks.last[0].addr
|
|
944
|
+
end
|
|
945
|
+
|
|
946
|
+
def close
|
|
947
|
+
@running = false
|
|
948
|
+
@socks.each { |s, _| s.close rescue nil }
|
|
949
|
+
end
|
|
950
|
+
|
|
951
|
+
# mosh-style port hop: open a fresh socket (new source address) and switch
|
|
952
|
+
# to it, keeping the old socket alive briefly to catch delayed packets.
|
|
953
|
+
# In real life you do NOT need to call this -- bind the client socket to
|
|
954
|
+
# 0.0.0.0 and the OS re-picks the source address when the route changes;
|
|
955
|
+
# this is the explicit fallback for testing/deterministic roaming.
|
|
956
|
+
def hop(local_host = "0.0.0.0")
|
|
957
|
+
new_sock = nil
|
|
958
|
+
@send_mutex.synchronize do
|
|
959
|
+
new_sock = UDPSocket.new
|
|
960
|
+
configure_socket(new_sock)
|
|
961
|
+
new_sock.bind(local_host, 0)
|
|
962
|
+
@socks << [new_sock, Mnet.now]
|
|
963
|
+
end
|
|
964
|
+
prune_sockets
|
|
965
|
+
@sessions_mutex.synchronize { @sessions.each_value(&:reanchor) }
|
|
966
|
+
new_sock
|
|
967
|
+
end
|
|
968
|
+
|
|
969
|
+
alias rebind hop
|
|
970
|
+
|
|
971
|
+
def send_raw(data, ip, port)
|
|
972
|
+
sock = @socks.last[0]
|
|
973
|
+
@send_mutex.synchronize do
|
|
974
|
+
sock.send(data, Socket::MSG_DONTWAIT, ip, port)
|
|
975
|
+
true
|
|
976
|
+
rescue IO::EAGAINWaitWritable, Errno::EAGAIN, Errno::EWOULDBLOCK, Errno::ENOBUFS
|
|
977
|
+
false
|
|
978
|
+
rescue IOError, Errno::EBADF, Errno::ECONNRESET, Errno::ECONNREFUSED
|
|
979
|
+
false
|
|
980
|
+
end
|
|
981
|
+
end
|
|
982
|
+
|
|
983
|
+
def remove_session(id)
|
|
984
|
+
@sessions_mutex.synchronize { @sessions.delete(id) }
|
|
985
|
+
end
|
|
986
|
+
|
|
987
|
+
private
|
|
988
|
+
|
|
989
|
+
def prune_sockets
|
|
990
|
+
cutoff = Mnet.now - Mnet::MAX_OLD_SOCKET_AGE
|
|
991
|
+
@send_mutex.synchronize do
|
|
992
|
+
@socks.reject! do |sock, created|
|
|
993
|
+
old = created < cutoff && sock != @socks.last[0]
|
|
994
|
+
sock.close rescue nil if old
|
|
995
|
+
old
|
|
996
|
+
end
|
|
997
|
+
end
|
|
998
|
+
end
|
|
999
|
+
|
|
1000
|
+
def configure_socket(sock)
|
|
1001
|
+
sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
|
|
1002
|
+
# Request buffers sized for the receive window. Linux clamps these to
|
|
1003
|
+
# net.core.rmem_max/wmem_max, so read back the real value and cap the
|
|
1004
|
+
# flow-control window to what the kernel can actually buffer -- otherwise
|
|
1005
|
+
# the sender overruns the socket buffer and the transfer stalls.
|
|
1006
|
+
sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDBUF, @recv_cap * 2)
|
|
1007
|
+
sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVBUF, @recv_cap)
|
|
1008
|
+
rcv = sock.getsockopt(Socket::SOL_SOCKET, Socket::SO_RCVBUF).int
|
|
1009
|
+
@recv_cap = rcv * 3 / 4 if rcv * 3 / 4 < @recv_cap
|
|
1010
|
+
rescue SystemCallError
|
|
1011
|
+
# Buffer sizing is best-effort; ignore if the OS clamps/rejects it.
|
|
1012
|
+
end
|
|
1013
|
+
|
|
1014
|
+
def register(sess)
|
|
1015
|
+
@sessions_mutex.synchronize { @sessions[sess.id] = sess }
|
|
1016
|
+
end
|
|
1017
|
+
|
|
1018
|
+
def ensure_bound
|
|
1019
|
+
return if @bound
|
|
1020
|
+
sock = UDPSocket.new
|
|
1021
|
+
configure_socket(sock)
|
|
1022
|
+
sock.bind("0.0.0.0", 0)
|
|
1023
|
+
@socks << [sock, Mnet.now]
|
|
1024
|
+
@bound = true
|
|
1025
|
+
start
|
|
1026
|
+
end
|
|
1027
|
+
|
|
1028
|
+
def start
|
|
1029
|
+
return if @running
|
|
1030
|
+
@running = true
|
|
1031
|
+
@reader = Thread.new { reader_loop }
|
|
1032
|
+
@ticker = Thread.new { ticker_loop }
|
|
1033
|
+
end
|
|
1034
|
+
|
|
1035
|
+
def reader_loop
|
|
1036
|
+
loop do
|
|
1037
|
+
socks = @socks.map(&:first)
|
|
1038
|
+
break if socks.empty?
|
|
1039
|
+
begin
|
|
1040
|
+
ready = IO.select(socks, nil, nil, 0.5)
|
|
1041
|
+
rescue IOError, Errno::EBADF
|
|
1042
|
+
break
|
|
1043
|
+
end
|
|
1044
|
+
next unless ready
|
|
1045
|
+
|
|
1046
|
+
# Drain each ready socket in a batch (up to a cap) instead of one
|
|
1047
|
+
# packet per select wakeup -- fewer syscalls/context switches.
|
|
1048
|
+
ready[0].each do |sock|
|
|
1049
|
+
drain_socket(sock)
|
|
1050
|
+
end
|
|
1051
|
+
end
|
|
1052
|
+
end
|
|
1053
|
+
|
|
1054
|
+
def drain_socket(sock, cap = 256)
|
|
1055
|
+
cap.times do
|
|
1056
|
+
data, addr = sock.recvfrom_nonblock(65_535)
|
|
1057
|
+
dispatch(data, addr)
|
|
1058
|
+
rescue IO::WaitReadable, Errno::EAGAIN, Errno::EWOULDBLOCK
|
|
1059
|
+
break
|
|
1060
|
+
rescue IOError, Errno::EBADF, Errno::ECONNRESET
|
|
1061
|
+
break
|
|
1062
|
+
rescue => e
|
|
1063
|
+
warn "mnet: #{e.class}: #{e.message}"
|
|
1064
|
+
break
|
|
1065
|
+
end
|
|
1066
|
+
end
|
|
1067
|
+
|
|
1068
|
+
def dispatch(data, addr)
|
|
1069
|
+
pkt = Mnet.unpack(data)
|
|
1070
|
+
return unless pkt
|
|
1071
|
+
|
|
1072
|
+
sess = @sessions_mutex.synchronize { @sessions[pkt.session_id] }
|
|
1073
|
+
if sess
|
|
1074
|
+
sess.handle_packet(pkt, addr)
|
|
1075
|
+
elsif pkt.type == Mnet::TYPE_SYN
|
|
1076
|
+
key = @key_by_id[pkt.session_id]
|
|
1077
|
+
klass = (pkt.flags & Mnet::FLAG_KCP) != 0 ? KcpSession : Session
|
|
1078
|
+
sess = klass.new(self, pkt.session_id, role: :server, key: key, logger: @logger,
|
|
1079
|
+
**@opts.merge(recv_capacity: @recv_cap))
|
|
1080
|
+
register(sess)
|
|
1081
|
+
@accept << sess
|
|
1082
|
+
sess.handle_packet(pkt, addr)
|
|
1083
|
+
end
|
|
1084
|
+
end
|
|
1085
|
+
|
|
1086
|
+
def ticker_loop
|
|
1087
|
+
loop do
|
|
1088
|
+
sleep TICK_INTERVAL
|
|
1089
|
+
now = Mnet.now
|
|
1090
|
+
sessions = @sessions_mutex.synchronize { @sessions.values }
|
|
1091
|
+
sessions.each { |s| s.tick(now) }
|
|
1092
|
+
prune_sockets if @socks.size > 1
|
|
1093
|
+
rescue IOError
|
|
1094
|
+
break
|
|
1095
|
+
end
|
|
1096
|
+
end
|
|
1097
|
+
end
|
|
1098
|
+
|
|
1099
|
+
# A TCPServer-compatible listener over the reliable UDP transport, so
|
|
1100
|
+
# `OpenSSL::SSL::SSLServer.new(server, ctx)` works unchanged. `accept`
|
|
1101
|
+
# returns a plain IO (the app end of the per-connection bridge), which the
|
|
1102
|
+
# SSL layer can wrap directly.
|
|
1103
|
+
class Server
|
|
1104
|
+
def initialize(host = "0.0.0.0", port = 0, **opts)
|
|
1105
|
+
@endpoint = Endpoint.new(**opts)
|
|
1106
|
+
@endpoint.listen(host, port)
|
|
1107
|
+
end
|
|
1108
|
+
|
|
1109
|
+
def accept
|
|
1110
|
+
@endpoint.accept.bridge
|
|
1111
|
+
end
|
|
1112
|
+
|
|
1113
|
+
# Raw session (IO-compatible, no socketpair bridge) for the non-TLS path.
|
|
1114
|
+
def accept_session
|
|
1115
|
+
@endpoint.accept
|
|
1116
|
+
end
|
|
1117
|
+
|
|
1118
|
+
def close
|
|
1119
|
+
@endpoint.close
|
|
1120
|
+
end
|
|
1121
|
+
|
|
1122
|
+
def addr
|
|
1123
|
+
@endpoint.local_addr
|
|
1124
|
+
end
|
|
1125
|
+
|
|
1126
|
+
def endpoint
|
|
1127
|
+
@endpoint
|
|
1128
|
+
end
|
|
1129
|
+
end
|
|
1130
|
+
end
|