sping 1.0.3 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +3 -1
- data/lib/errors.rb +44 -0
- data/lib/last_acks.rb +32 -23
- data/lib/session.rb +102 -17
- data/lib/session_manager.rb +89 -43
- metadata +8 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7dc97d43dd1c7805574269a6ebbe8113c49c431bdfa46783b8e43ebef14063a5
|
4
|
+
data.tar.gz: ffb03def610179c609ffd4046cd5a7da5659e73b83ae867a123a2fcf62309b72
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 637dc51c01d6f89bb26a6175d3ac0f0a16921398db3244164efea4b1de457cd3ef1329f3b4ee6eed69a1a57961a6c0d64c31fab7f4fb1fed150183212d830b96
|
7
|
+
data.tar.gz: 5bc1661058eb425277ed0d37429b5b0e8376690a6665aeb85e8bda35b96b85fd3a4025f0b4cc2020aef7b9e1f847b24845b6db9e841705749a7db3a20e4e9ee3
|
data/README.md
CHANGED
@@ -1,3 +1,5 @@
|
|
1
1
|
# sping
|
2
2
|
|
3
|
-
|
3
|
+
Program for measuring asymmetric latencies.
|
4
|
+
|
5
|
+
This is a reimplementation in Ruby of the reference implementation of the sping protocol in Go. The program provides both the client and server part to measure asymmetric latencies between two peers.
|
data/lib/errors.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# sharable_constant_value: literal
|
3
|
+
|
4
|
+
# SPing / Splitted Ping is a protocol for measuring asymmetric latencies.
|
5
|
+
module SPing
|
6
|
+
# Generic SPing error.
|
7
|
+
class SPingError < StandardError; end
|
8
|
+
|
9
|
+
# Error that occurred while managing the sessions.
|
10
|
+
class SessionManagerError < SPingError; end
|
11
|
+
|
12
|
+
# Error that is thrown when there are not enough session IDs available to establish a
|
13
|
+
# connection to a peer or to accept new ones.
|
14
|
+
# This indicates either a programmer error or an overload of the server.
|
15
|
+
class OutOfSessions < SessionManagerError; end
|
16
|
+
|
17
|
+
# Error that occurs in connection with a session.
|
18
|
+
class SessionError < SPingError; end
|
19
|
+
|
20
|
+
# Error during the TCP handshake
|
21
|
+
class TCPHandshakeError < SessionError; end
|
22
|
+
|
23
|
+
# Error due to the peer (i.e. not self-inflicted).
|
24
|
+
class PeerError < SPingError; end
|
25
|
+
|
26
|
+
# Package was not coded correctly.
|
27
|
+
class InvalidPacketError < PeerError; end
|
28
|
+
|
29
|
+
# The package was coded correctly, but the content is invalid.
|
30
|
+
class InvalidMessageError < InvalidPacketError; end
|
31
|
+
|
32
|
+
# A packet has been received which cannot be clearly assigned to SPing
|
33
|
+
# and therefore cannot be processed further.
|
34
|
+
class UnknownPacketError < InvalidPacketError; end
|
35
|
+
|
36
|
+
# The peer has signaled an error.
|
37
|
+
class SignalizedError < PeerError; end
|
38
|
+
|
39
|
+
# The peer uses a version of sping that is not supported.
|
40
|
+
class UnsupportedVersionError < PeerError; end
|
41
|
+
|
42
|
+
# The package could not be assigned to a current session.
|
43
|
+
class UnknownSessionError < PeerError; end
|
44
|
+
end
|
data/lib/last_acks.rb
CHANGED
@@ -1,34 +1,43 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
# sharable_constant_value: literal
|
3
3
|
|
4
|
-
|
5
|
-
|
4
|
+
# SPing / Splitted Ping is a protocol for measuring asymmetric latencies.
|
5
|
+
module SPing
|
6
|
+
# A kind of circular buffer, which consists of 32 acks at any given time. If no acks are available,
|
7
|
+
# empty ones are created. If a new ack is added (which would result in 33 acks), one is removed.
|
8
|
+
class LastAcks
|
9
|
+
# Creates a new circular buffer for acks
|
10
|
+
def initialize
|
11
|
+
# Array in which the acks are stored.
|
12
|
+
@acks = []
|
13
|
+
# Mutex, which ensures that the array is not accessed simultaneously.
|
14
|
+
@acks_mutex = Mutex.new
|
6
15
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
'U' => Time.at(0),
|
16
|
-
'X' => Time.at(0)
|
17
|
-
}
|
16
|
+
# Initiallize the circular buffer with 32 empty acks.
|
17
|
+
32.times do |index|
|
18
|
+
@acks[index] = {
|
19
|
+
'R' => 0,
|
20
|
+
'U' => Time.at(0),
|
21
|
+
'X' => Time.at(0)
|
22
|
+
}
|
23
|
+
end
|
18
24
|
end
|
19
|
-
end
|
20
25
|
|
21
|
-
|
22
|
-
@
|
23
|
-
|
26
|
+
# Returns a copy of the last 32 acks.
|
27
|
+
# @return [Array] Last 32 Acks
|
28
|
+
def acks
|
29
|
+
@acks_mutex.synchronize do
|
30
|
+
return @acks.dup
|
31
|
+
end
|
24
32
|
end
|
25
|
-
end
|
26
|
-
|
27
|
-
def add_ack(ack)
|
28
|
-
@acks_mutex.synchronize do
|
29
|
-
@acks << ack
|
30
33
|
|
31
|
-
|
34
|
+
# Adds a new ack and deletes the oldest one
|
35
|
+
# @param ack [Hash] New ack to be added
|
36
|
+
def add_ack(ack)
|
37
|
+
@acks_mutex.synchronize do
|
38
|
+
@acks << ack
|
39
|
+
@acks.shift
|
40
|
+
end
|
32
41
|
end
|
33
42
|
end
|
34
43
|
end
|
data/lib/session.rb
CHANGED
@@ -1,69 +1,118 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
# sharable_constant_value: literal
|
3
3
|
|
4
|
+
# SPing / Splitted Ping is a protocol for measuring asymmetric latencies.
|
4
5
|
module SPing
|
5
|
-
|
6
|
-
|
6
|
+
# Inclusion of all possible SPing-specific errors.
|
7
|
+
require_relative 'errors'
|
7
8
|
|
9
|
+
# Models or represents a session with a peer.
|
8
10
|
class Session
|
9
|
-
|
10
|
-
|
11
|
+
# Time when the session was created. This is necessary so that the session GC knows when a
|
12
|
+
# non-initialized session can be deleted.
|
13
|
+
# @return [Time]
|
14
|
+
attr_reader :created
|
15
|
+
# Time when the last ping packet was received by the peer. This is important so that the session
|
16
|
+
# GC knows when a session is considered inactive and can be deleted.
|
17
|
+
# @return [Time]
|
18
|
+
attr_reader :last_rx
|
19
|
+
# Indicates whether the session has been initialized. true if we have initiated it.
|
20
|
+
# false if the peer has initiated it.
|
21
|
+
# @return [TrueClass, FalseClass]
|
22
|
+
attr_reader :madebyme
|
23
|
+
# Indicates whether a TCP handshake has been carried out (successfully).
|
24
|
+
# @return [TrueClass, FalseClass]
|
25
|
+
attr_reader :tcp_handshake_complete
|
26
|
+
# Indicates whether a UDP handshake has been carried out (successfully).
|
27
|
+
# @return [TrueClass, FalseClass]
|
28
|
+
attr_reader :udp_handshake_complete
|
29
|
+
# The session ID in the range from 1 to 2**32 - 1.
|
30
|
+
# @return [Integer]
|
31
|
+
attr_reader :session_id
|
11
32
|
|
12
33
|
require 'socket'
|
13
34
|
require 'timeout'
|
14
35
|
require 'msgpack'
|
15
36
|
require_relative 'last_acks'
|
16
37
|
|
38
|
+
# Creates a new session
|
39
|
+
# @param host [#to_s] Host of the peer
|
40
|
+
# @param port [#to_i] Port of the peer
|
41
|
+
# @param socket [UDPSocket] UDP socket via which the pings and the UDP handshake are to be sent.
|
42
|
+
# @param madebyme [TrueClass, FalseClass] Indicates whether we have initiated the session.
|
17
43
|
def initialize(host, port, socket, madebyme)
|
18
|
-
|
19
|
-
@
|
44
|
+
# Assign the passed parameters to instance variables.
|
45
|
+
@host = host.to_s
|
46
|
+
@port = port.to_i
|
20
47
|
@socket = socket
|
21
|
-
@created = Time.now
|
22
48
|
@madebyme = madebyme
|
23
49
|
|
50
|
+
# Initialize assignment of other instance variables
|
51
|
+
@created = Time.now
|
52
|
+
|
24
53
|
@tcp_handshake_complete = false
|
25
54
|
@udp_handshake_complete = false
|
26
55
|
|
27
|
-
@last_acks = LastAcks.new
|
56
|
+
@last_acks = SPing::LastAcks.new
|
28
57
|
end
|
29
58
|
|
59
|
+
# Initiates a TCP handshake.
|
60
|
+
# @param socket [TCPSocket] Socket via which the handshake is to be sent
|
61
|
+
# (and responses are to be expected).
|
62
|
+
# @param session_id [Integer] Session ID which is to be sent to the peer
|
63
|
+
# and which is to be used for the current session.
|
30
64
|
def do_tcp_handshake1(socket, session_id)
|
65
|
+
# Send the banner
|
31
66
|
socket.write "sping-0.3-https://codeberg.org/mark22k/sping\r\n"
|
67
|
+
# and make sure that it is no longer in the send buffer.
|
32
68
|
socket.flush
|
33
69
|
|
70
|
+
# See if the peer invites us to create a session with them.
|
34
71
|
invite = socket.readpartial 6
|
35
|
-
|
36
72
|
raise TCPHandshakeError, 'Peer didn\'t invite us.' unless invite.chomp == 'INVITE'
|
37
73
|
|
74
|
+
# Send the session ID
|
38
75
|
@session_id = session_id
|
39
|
-
|
40
76
|
socket.write "#{session_id}\r\n"
|
77
|
+
# and make sure that it is no longer in the send buffer.
|
41
78
|
socket.flush
|
42
79
|
|
80
|
+
# This means that the TCP handshake is successful. The socket can be closed
|
81
|
+
# and the corresponding instance variable can be set.
|
43
82
|
socket.close
|
44
83
|
|
45
84
|
@tcp_handshake_complete = true
|
46
85
|
end
|
47
86
|
|
87
|
+
# Receives a TCP handshake from a peer.
|
88
|
+
# The peer specified when the session is created is consulted for this purpose.
|
48
89
|
def do_tcp_handshake2
|
90
|
+
# Establish a connection and receive the banner.
|
49
91
|
socket = TCPSocket.new @host, @port
|
92
|
+
|
93
|
+
# If the banner is too large, close the socket and throw an error.
|
94
|
+
# The handshake was not successful.
|
50
95
|
banner = socket.readpartial 9001
|
51
96
|
if banner.length > 9000
|
52
97
|
socket.close
|
53
98
|
raise TCPHandshakeError, 'Host banner too big'
|
54
99
|
end
|
55
100
|
|
101
|
+
# If the banner does not match the SPing service, close the socket and
|
102
|
+
# throw an error. The handshake was not successful.
|
56
103
|
unless banner.start_with? 'sping-0.3-'
|
57
104
|
socket.close
|
58
|
-
raise TCPHandshakeError, 'Host banner not sping'
|
105
|
+
raise TCPHandshakeError, 'Host banner not sping or unsupported version of sping.'
|
59
106
|
end
|
60
107
|
|
61
108
|
@remote_version = banner.chomp
|
62
109
|
$logger.info "Peer uses the following program version: #{@remote_version.dump}"
|
63
110
|
|
111
|
+
# Invite the peer to start a session with us.
|
64
112
|
socket.write "INVITE\r\n"
|
65
113
|
socket.flush
|
66
114
|
|
115
|
+
# If the session ID is too long or none was received, close the socket and throw an error.
|
67
116
|
invite_buf = socket.readpartial 32
|
68
117
|
if invite_buf.length > 31 || invite_buf.empty?
|
69
118
|
socket.close
|
@@ -77,15 +126,21 @@ module SPing
|
|
77
126
|
@tcp_handshake_complete = true
|
78
127
|
end
|
79
128
|
|
129
|
+
# Sets the endpoint consisting of the host and port of the remote end. This is done
|
130
|
+
# each time a new packet is received and ensures that the current endpoint is always available.
|
131
|
+
# @param host [#to_s]
|
132
|
+
# @param port [#to_i]
|
80
133
|
def set_endpoint(host, port)
|
81
|
-
@host = host
|
82
|
-
@port = port
|
134
|
+
@host = host.to_s
|
135
|
+
@port = port.to_i
|
83
136
|
end
|
84
137
|
|
138
|
+
# Starts a thread which sends the UDP handshake at regular intervals.
|
139
|
+
# @param send_interval [#to_i] The interval at which the UDP handshakes are to be sent.
|
85
140
|
def start_udp_handshake_sender(send_interval = 5)
|
86
141
|
raise 'UDP handshake sender is already running.' if @udp_handshake_sender
|
87
142
|
|
88
|
-
@udp_handshake_sender = Thread.new(send_interval) do |th_send_interval|
|
143
|
+
@udp_handshake_sender = Thread.new(send_interval.to_i) do |th_send_interval|
|
89
144
|
loop do
|
90
145
|
send_udp_handshake
|
91
146
|
sleep th_send_interval
|
@@ -93,6 +148,7 @@ module SPing
|
|
93
148
|
end
|
94
149
|
end
|
95
150
|
|
151
|
+
# Send a single UDP handshake
|
96
152
|
def send_udp_handshake
|
97
153
|
packet = {
|
98
154
|
'Y' => 'h'.ord,
|
@@ -105,6 +161,7 @@ module SPing
|
|
105
161
|
@socket.send packet, 0, @host, @port
|
106
162
|
end
|
107
163
|
|
164
|
+
# Stop the UDP handshake sender. UDP handshakes are then no longer sent at any interval.
|
108
165
|
def stop_udp_handshake_sender
|
109
166
|
raise 'UDP handshake sender is not running and therefore cannot be terminated.' unless @udp_handshake_sender
|
110
167
|
|
@@ -112,11 +169,13 @@ module SPing
|
|
112
169
|
@udp_handshake_sender = nil
|
113
170
|
end
|
114
171
|
|
172
|
+
# Informs the session that a UDP handshake has been received.
|
115
173
|
def udp_handshake_recived
|
116
174
|
stop_udp_handshake_sender if @madebyme
|
117
175
|
@udp_handshake_complete = true
|
118
176
|
end
|
119
177
|
|
178
|
+
# Starts a thread which sends ping or time messages to the peer at one-second intervals.
|
120
179
|
def start_pinger
|
121
180
|
raise 'Pinger is already running.' if @pinger
|
122
181
|
|
@@ -128,6 +187,7 @@ module SPing
|
|
128
187
|
end
|
129
188
|
end
|
130
189
|
|
190
|
+
# Stops the thread, which sends ping or time messages to the peer at regular intervals.
|
131
191
|
def stop_pinger
|
132
192
|
raise 'Pinger sender is not running and therefore cannot be terminated.' unless @pinger
|
133
193
|
|
@@ -135,6 +195,7 @@ module SPing
|
|
135
195
|
@pinger = nil
|
136
196
|
end
|
137
197
|
|
198
|
+
# Sends a ping message to the peer.
|
138
199
|
def ping
|
139
200
|
current_id = (Time.now.to_i % 255) + 1
|
140
201
|
data = {
|
@@ -151,21 +212,45 @@ module SPing
|
|
151
212
|
@socket.send data, 0, @host, @port
|
152
213
|
end
|
153
214
|
|
215
|
+
# Stops all threads associated with the session. This means that the session to the peer is as good as dead.
|
154
216
|
def stop
|
155
217
|
@pinger&.kill
|
156
218
|
@udp_handshake_sender&.kill
|
157
219
|
end
|
158
220
|
|
221
|
+
# Handler that receives and processes a receiving ping packet or time message. The current statistics are output.
|
222
|
+
# @param packet [Hash]
|
223
|
+
# @param rxtime [Time]
|
224
|
+
# @param peeraddr [#to_s]
|
159
225
|
def handle_ping(packet, rxtime, peeraddr)
|
160
|
-
if packet[
|
161
|
-
|
162
|
-
|
226
|
+
if !(packet.keys - %w[M Y E I T A S]).empty? ||
|
227
|
+
# M, Y are already checked in handle_packet from session manager
|
228
|
+
!packet['E'].is_a?(Integer) ||
|
229
|
+
!packet['I'].is_a?(Integer) ||
|
230
|
+
!packet['T'].is_a?(Time) ||
|
231
|
+
!packet['A'].is_a?(Array)
|
232
|
+
raise InvalidPacketError, 'The peer has sent an invalid message. The ping packet is incorrectly coded.'
|
163
233
|
end
|
164
234
|
|
235
|
+
raise SignalizedError, "Package displays an error message: #{packet['E']} Processing aborted." if packet['E'] != 0
|
236
|
+
|
165
237
|
id = packet['I']
|
166
238
|
txtime = packet['T']
|
167
239
|
remote_last_acks = packet['A']
|
168
240
|
|
241
|
+
if remote_last_acks.length != 32
|
242
|
+
raise InvalidMessageError, 'The peer has sent an invalid message. It does not contain any 32-Acks.'
|
243
|
+
end
|
244
|
+
|
245
|
+
remote_last_acks.each do |block_ack|
|
246
|
+
if !(block_ack.keys - %w[R U X]).empty? ||
|
247
|
+
!block_ack['R'].is_a?(Integer) ||
|
248
|
+
!block_ack['U'].is_a?(Time) ||
|
249
|
+
!block_ack['X'].is_a?(Time)
|
250
|
+
raise InvalidMessageError, 'The peer has sent an invalid message. An Ack is formatted invalid.'
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
169
254
|
ack = {
|
170
255
|
'R' => id,
|
171
256
|
'U' => txtime,
|
data/lib/session_manager.rb
CHANGED
@@ -1,18 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
# sharable_constant_value: literal
|
3
3
|
|
4
|
+
# SPing / Splitted Ping is a protocol for measuring asymmetric latencies.
|
4
5
|
module SPing
|
5
|
-
|
6
|
-
|
6
|
+
# Inclusion of all possible SPing-specific errors.
|
7
|
+
require_relative 'errors'
|
7
8
|
|
9
|
+
# Container, which contains a collection of sessions and manages them.
|
8
10
|
class SessionManager
|
9
11
|
require 'socket'
|
10
12
|
require 'timeout'
|
11
13
|
require_relative 'session'
|
12
14
|
|
15
|
+
# Creates a new Session Manager, which can (and will) manage a number of sessions.
|
16
|
+
# @param host [#to_s] Host to which messages are to be bound and from which messages are sent accordingly.
|
17
|
+
# @param port [#to_i] Port
|
13
18
|
def initialize(host = '::', port = 6924)
|
14
|
-
@host = host
|
15
|
-
@port = port
|
19
|
+
@host = host.to_s
|
20
|
+
@port = port.to_i
|
16
21
|
|
17
22
|
@sessions = {}
|
18
23
|
@sessions_mutex = Mutex.new
|
@@ -21,9 +26,12 @@ module SPing
|
|
21
26
|
@socket.bind @host, @port
|
22
27
|
end
|
23
28
|
|
29
|
+
# Initiates a new session with a peer.
|
30
|
+
# @param host [#to_s]
|
31
|
+
# @param port [#to_s, #to_i]
|
24
32
|
def new_session(host, port = 6924)
|
25
33
|
$logger.info "Add new session for host #{host} port #{port}."
|
26
|
-
session = SPing::Session.new host, port, @socket, true
|
34
|
+
session = SPing::Session.new host.to_s, port.to_i, @socket, true
|
27
35
|
|
28
36
|
counter = 0
|
29
37
|
loop do
|
@@ -49,6 +57,8 @@ module SPing
|
|
49
57
|
$logger.error "Out of session: #{e.message}"
|
50
58
|
end
|
51
59
|
|
60
|
+
# Stops all threads connected to a session ID and removes the session from the Session Manager administration.
|
61
|
+
# @param session_id [Integer] Session ID of the session to be removed.
|
52
62
|
def del_session(session_id)
|
53
63
|
$logger.debug "Delete session with session id #{session_id}"
|
54
64
|
|
@@ -58,18 +68,21 @@ module SPing
|
|
58
68
|
end
|
59
69
|
end
|
60
70
|
|
71
|
+
# Stops all sessions or the threads that are connected to them.
|
61
72
|
def stop_sessions
|
62
73
|
@sessions.each do |_session_id, session|
|
63
74
|
session.stop
|
64
75
|
end
|
65
76
|
end
|
66
77
|
|
78
|
+
# Waits and blocks until the Session Manager is no longer running.
|
67
79
|
def join
|
68
80
|
@runner&.join
|
69
81
|
@gc_thread&.join
|
70
82
|
@server_thread&.join
|
71
83
|
end
|
72
84
|
|
85
|
+
# Starts the Session Manager.
|
73
86
|
def run
|
74
87
|
raise 'Client is already running.' if @runner
|
75
88
|
raise 'GC is already running.' if @gc
|
@@ -89,6 +102,7 @@ module SPing
|
|
89
102
|
end
|
90
103
|
end
|
91
104
|
|
105
|
+
# Starts the server functionality of the Session Manager.
|
92
106
|
def run_server
|
93
107
|
raise 'Server is already running.' if @server_thread
|
94
108
|
raise 'Server already exist.' if @server
|
@@ -106,6 +120,7 @@ module SPing
|
|
106
120
|
return @server_thread
|
107
121
|
end
|
108
122
|
|
123
|
+
# Stops the server functionality of the Session Manager.
|
109
124
|
def stop_server
|
110
125
|
raise 'Server thread is not running.' unless @server_thread
|
111
126
|
|
@@ -118,6 +133,7 @@ module SPing
|
|
118
133
|
@server = nil
|
119
134
|
end
|
120
135
|
|
136
|
+
# Stops the Session Manager. The server functionality must be stopped separately.
|
121
137
|
def stop
|
122
138
|
raise 'Session manager is not running.' unless @runner
|
123
139
|
|
@@ -130,6 +146,12 @@ module SPing
|
|
130
146
|
@gc_thread = nil
|
131
147
|
end
|
132
148
|
|
149
|
+
private
|
150
|
+
|
151
|
+
# Generates a new session ID that was not yet in use at the time of the check.
|
152
|
+
# If none could be generated within half a minute - for example because all
|
153
|
+
# session IDs have already been assigned - an error is thrown.
|
154
|
+
# @return [Integer]
|
133
155
|
def generate_session_id
|
134
156
|
Timeout.timeout(30, OutOfSessions, 'No session ID could be generated which has not yet been used.') do
|
135
157
|
loop do
|
@@ -139,19 +161,23 @@ module SPing
|
|
139
161
|
end
|
140
162
|
end
|
141
163
|
|
142
|
-
|
143
|
-
|
164
|
+
# Removes obsolete sessions. This includes sessions that are older than one minute but
|
165
|
+
# have not been activated a double time. And sessions that have not
|
166
|
+
# received any packets for half a minute.
|
144
167
|
def do_gc
|
145
168
|
$logger.debug 'Remove outdated sessions.'
|
146
169
|
# rubocop:disable Style/HashEachMethods
|
147
|
-
# You cannot run through a map and edit it at the same time. Since this is
|
170
|
+
# You cannot run through a map and edit it at the same time. Since this is
|
171
|
+
# necessary due to the threading,
|
148
172
|
# a snapshot of the key variable is run through.
|
149
173
|
@sessions.keys.each do |session_id|
|
150
174
|
# rubocop:enable Style/HashEachMethods
|
151
175
|
|
152
176
|
session = @sessions[session_id]
|
153
|
-
# It is possible that sessions no longer exist here, as we may have
|
154
|
-
#
|
177
|
+
# It is possible that sessions no longer exist here, as we may have
|
178
|
+
# session IDs that have already been deleted.
|
179
|
+
# However, we can ignore this aspect here, as we are the only
|
180
|
+
# function that deletes sessions.
|
155
181
|
if !(session.tcp_handshake_complete && session.udp_handshake_complete)
|
156
182
|
# Handshake incomplete
|
157
183
|
if (Time.now.to_i - session.created.to_i) > 60
|
@@ -160,13 +186,16 @@ module SPing
|
|
160
186
|
del_session session_id
|
161
187
|
end
|
162
188
|
elsif (Time.now.to_i - session.last_rx.to_i) > 30
|
163
|
-
# 30 seconds have elapsed since the last ping from the peer was received.
|
189
|
+
# 30 seconds have elapsed since the last ping from the peer was received.
|
190
|
+
# The peer is probably dead.
|
164
191
|
$logger.debug "Session id #{session_id} without activity for over thirty seconds."
|
165
192
|
del_session session_id
|
166
193
|
end
|
167
194
|
end
|
168
195
|
end
|
169
196
|
|
197
|
+
# Receives a TCP handshake for a session and then has it
|
198
|
+
# managed by the Session Manager.
|
170
199
|
def request_session(session)
|
171
200
|
session.do_tcp_handshake2
|
172
201
|
|
@@ -180,6 +209,9 @@ module SPing
|
|
180
209
|
return nil
|
181
210
|
end
|
182
211
|
|
212
|
+
# Handler for a received packet. The function catches errors and outputs them.
|
213
|
+
# It also forwards the packet to the corresponding handler according to the
|
214
|
+
# session ID and type of packet.
|
183
215
|
def handle_packet(buf)
|
184
216
|
rxtime = Time.now
|
185
217
|
|
@@ -187,12 +219,20 @@ module SPing
|
|
187
219
|
peerport = buf[1][1]
|
188
220
|
$logger.debug "Packet received from #{peeraddr} port #{peerport}."
|
189
221
|
|
190
|
-
packet = MessagePack.unpack(buf[0])
|
222
|
+
packet = MessagePack.unpack(buf[0]).to_h
|
223
|
+
|
224
|
+
if !packet.key?('M') ||
|
225
|
+
!packet.key?('Y') ||
|
226
|
+
!packet.key?('S') ||
|
227
|
+
!packet['M'].is_a?(Integer) ||
|
228
|
+
!packet['Y'].is_a?(Integer) ||
|
229
|
+
!packet['S'].is_a?(Integer)
|
230
|
+
raise InvalidMessageError, 'The peer has sent an invalid message. The packet is incorrectly coded.'
|
231
|
+
end
|
191
232
|
|
192
233
|
# Check whether the Magic Number is the correct one.
|
193
234
|
if packet['M'] != 11_181
|
194
|
-
|
195
|
-
return
|
235
|
+
raise UnknownPacketError, 'Package contains incorrect magic number. Processing is canceled.'
|
196
236
|
end
|
197
237
|
|
198
238
|
type = packet['Y'].chr
|
@@ -202,54 +242,60 @@ module SPing
|
|
202
242
|
when 't'
|
203
243
|
handle_ping packet, rxtime, peeraddr, peerport
|
204
244
|
else
|
205
|
-
|
245
|
+
raise UnknownPacketError, "Package is of unknown type: #{type}"
|
206
246
|
end
|
247
|
+
rescue MessagePack::MalformedFormatError => e
|
248
|
+
$logger.error "Packet cannot be decoded: #{e.message}"
|
249
|
+
rescue PeerError => e
|
250
|
+
$logger.error "Perr error: #{e.message}"
|
207
251
|
end
|
208
252
|
|
253
|
+
# Handler for a receiving UDP handshake.
|
209
254
|
def handle_udp_handshake(packet, peeraddr, peerport)
|
255
|
+
if !(packet.keys - %w[M Y V S]).empty? ||
|
256
|
+
# M, Y are already checked in handle_packet from session manager
|
257
|
+
!packet['V'].is_a?(Integer)
|
258
|
+
raise InvalidMessageError, 'The peer has sent an invalid message. The handshake packet is incorrectly coded.'
|
259
|
+
end
|
260
|
+
|
210
261
|
session_id = packet['S']
|
211
262
|
session = @sessions[session_id]
|
212
|
-
|
213
|
-
|
214
|
-
$logger.warn "UDP handshake uses an unsupported version: #{packet['V']}"
|
215
|
-
return
|
216
|
-
end
|
263
|
+
raise UnknownSessionError, 'UDP handshake received for uninitiated session.' unless session
|
264
|
+
raise UnsupportedVersionError, "UDP handshake uses an unsupported version: #{packet['V']}" if packet['V'] != 3
|
217
265
|
|
218
|
-
|
266
|
+
session.set_endpoint peeraddr, peerport
|
219
267
|
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
if session.udp_handshake_complete
|
228
|
-
$logger.warn 'UDP handshake is received, although a previous one was already successful.'
|
229
|
-
return
|
230
|
-
end
|
231
|
-
|
232
|
-
session.udp_handshake_recived
|
233
|
-
$logger.info "UDP handshake for session ID #{session_id} was successful."
|
268
|
+
unless session.madebyme
|
269
|
+
# If the session was not started by me, the other peer expects a
|
270
|
+
# confirmation of the handshake in which the same is sent back.
|
271
|
+
$logger.debug "Send UDP Handshake back for session id #{session_id}."
|
272
|
+
session.send_udp_handshake
|
273
|
+
end
|
234
274
|
|
235
|
-
|
236
|
-
|
237
|
-
$logger.warn 'UDP handshake received for uninitiated session.'
|
275
|
+
if session.udp_handshake_complete
|
276
|
+
$logger.warn 'UDP handshake is received, although a previous one was already successful.'
|
238
277
|
return
|
239
278
|
end
|
279
|
+
|
280
|
+
session.udp_handshake_recived
|
281
|
+
$logger.info "UDP handshake for session ID #{session_id} was successful."
|
282
|
+
|
283
|
+
session.start_pinger
|
240
284
|
end
|
241
285
|
|
286
|
+
# Handler for a receiving ping or a receiving time message.
|
242
287
|
def handle_ping(packet, rxtime, peeraddr, peerport)
|
243
288
|
session_id = packet['S']
|
244
289
|
session = @sessions[session_id]
|
245
|
-
|
246
|
-
|
247
|
-
session.handle_ping packet, rxtime, peeraddr
|
248
|
-
else
|
249
|
-
$logger.warn "Ping packet received for non-initiated session id #{session_id}."
|
290
|
+
unless session&.tcp_handshake_complete && session&.udp_handshake_complete
|
291
|
+
raise UnknownSessionError, "Ping packet received for non-initiated session id #{session_id}."
|
250
292
|
end
|
293
|
+
|
294
|
+
session.set_endpoint peeraddr, peerport
|
295
|
+
session.handle_ping packet, rxtime, peeraddr
|
251
296
|
end
|
252
297
|
|
298
|
+
# Handler to establish a session for the request of a peer.
|
253
299
|
def handle_client(client)
|
254
300
|
host = client.peeraddr[3]
|
255
301
|
port = client.peeraddr[1]
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sping
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Marek Küthe
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-11-
|
11
|
+
date: 2023-11-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: msgpack
|
@@ -30,8 +30,9 @@ dependencies:
|
|
30
30
|
- - ">="
|
31
31
|
- !ruby/object:Gem::Version
|
32
32
|
version: 1.7.2
|
33
|
-
description: This is a reimplementation of
|
34
|
-
in
|
33
|
+
description: This is a reimplementation in Ruby of the reference implementation of
|
34
|
+
the sping protocol in Go. The program provides both the client and server part to
|
35
|
+
measure asymmetric latencies between two peers.
|
35
36
|
email: m.k@mk16.de
|
36
37
|
executables:
|
37
38
|
- sping
|
@@ -43,6 +44,7 @@ files:
|
|
43
44
|
- LICENSE
|
44
45
|
- README.md
|
45
46
|
- bin/sping
|
47
|
+
- lib/errors.rb
|
46
48
|
- lib/last_acks.rb
|
47
49
|
- lib/session.rb
|
48
50
|
- lib/session_manager.rb
|
@@ -68,8 +70,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
68
70
|
- !ruby/object:Gem::Version
|
69
71
|
version: '0'
|
70
72
|
requirements: []
|
71
|
-
rubygems_version: 3.4.
|
73
|
+
rubygems_version: 3.4.22
|
72
74
|
signing_key:
|
73
75
|
specification_version: 4
|
74
|
-
summary:
|
76
|
+
summary: Program for measuring asymmetric latencies.
|
75
77
|
test_files: []
|