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.
Files changed (7) hide show
  1. checksums.yaml +7 -0
  2. data/DESIGN.md +164 -0
  3. data/README.en.md +125 -0
  4. data/README.md +118 -0
  5. data/lib/mnet/version.rb +5 -0
  6. data/lib/mnet.rb +1130 -0
  7. 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