mirror of
https://github.com/cyberarm/w3d_hub_linux_launcher.git
synced 2026-03-22 20:26:16 +00:00
167 lines
5.1 KiB
Ruby
167 lines
5.1 KiB
Ruby
require "async"
|
|
require "socket"
|
|
require "securerandom"
|
|
|
|
class W3DHub
|
|
class Ping
|
|
ICMPHeader = Data.define(:type, :code, :checksum, :_ping_id, :_sequence_id, :data)
|
|
EchoRequest = Struct.new(:ping_id, :sequence_id, :data, :time, :timed_out)
|
|
|
|
ICMP_ECHOREPLY = 0
|
|
ICMP_ECHO = 8
|
|
ICMP_SUBCODE = 0
|
|
|
|
BIT_PACKER = "C2 n3 A*".freeze
|
|
MINIMUM_INTERVAL = 250 # ms # intervals below 200ms are considered rude and may be dropped due to flooding.
|
|
ECHO_REQUEST_HISTORY = 30 # 100 # keep the last n requests
|
|
|
|
attr_reader :address
|
|
|
|
def initialize(address:, count: 10, ttl: 120, interval: 1_000, data: nil)
|
|
@address = address
|
|
@count = count
|
|
@ttl = ttl
|
|
@interval = interval.to_i < MINIMUM_INTERVAL ? MINIMUM_INTERVAL : interval # ms
|
|
@data = data
|
|
|
|
# circular buffer
|
|
@echo_requests = Array.new(ECHO_REQUEST_HISTORY) { EchoRequest.new(-1, -1, "", nil, false) }
|
|
@echo_requests_index = 0
|
|
|
|
# NOTE: The PING_ID _might_ be overruled by the kernel and should not be used
|
|
# to check that any received echo replies are ours.
|
|
#
|
|
# Sequence ID and Data appear to be unmodified.
|
|
@ping_id = SecureRandom.hex.to_i(16) & 0xffff
|
|
@sequence_id = SecureRandom.hex.to_i(16) & 0xffff
|
|
|
|
addresses = Addrinfo.getaddrinfo(@address, nil, Socket::AF_INET, :DGRAM)
|
|
raise "NO ADDRESSES!" if addresses.empty?
|
|
|
|
@socket_address = addresses.sample.to_sockaddr
|
|
|
|
@socket = Socket.new(Socket::AF_INET, Socket::SOCK_DGRAM, Socket::IPPROTO_ICMP)
|
|
@socket.setsockopt(Socket::SOL_SOCKET, Socket::IP_TTL, @ttl)
|
|
end
|
|
|
|
# Perform a checksum on the message. This is the sum of all the short
|
|
# words and it folds the high order bits into the low order bits.
|
|
def message_checksum(message)
|
|
length = message.length
|
|
num_short = length / 2
|
|
check = 0
|
|
|
|
message.unpack("n#{num_short}").each do |short|
|
|
check += short
|
|
end
|
|
|
|
check += message[length - 1, 1].unpack1("C") << 8 if (length % 2).positive?
|
|
|
|
check = (check >> 16) + (check & 0xffff)
|
|
~((check >> 16) + check) & 0xffff
|
|
end
|
|
|
|
def random_data
|
|
SecureRandom.hex
|
|
end
|
|
|
|
def monotonic_time
|
|
Process.clock_gettime(:CLOCK_MONOTONIC, :millisecond)
|
|
end
|
|
|
|
def verified?(message)
|
|
data = message.unpack(BIT_PACKER)
|
|
checksum = data[2]
|
|
|
|
# set checksum in message to 0
|
|
data[2] = 0
|
|
|
|
checksum == message_checksum(data.pack(BIT_PACKER))
|
|
end
|
|
|
|
def request_complete?(request)
|
|
request.timed_out || !request.time.nil?
|
|
end
|
|
|
|
def packet_loss
|
|
completed_requests = @echo_requests.select { |r| request_complete?(r) }
|
|
failed_requests = completed_requests.select(&:timed_out)
|
|
|
|
# 0% packet loss 😎
|
|
return 0.0 if failed_requests.empty?
|
|
|
|
# 100% packet loss
|
|
return 1.0 if failed_requests.size == completed_requests.size
|
|
|
|
failed_requests.size / completed_requests.size.to_f
|
|
end
|
|
|
|
def average_ping
|
|
times = @echo_requests.select { |r| request_complete?(r) && !r.timed_out }.map(&:time)
|
|
|
|
return -1 unless times.size.positive?
|
|
|
|
times.sum.to_f / times.size
|
|
end
|
|
|
|
# returns true if any echo requests have completed (reply received or timed out) and packet loss is less than 30%
|
|
def okay?
|
|
completed_requests = @echo_requests.select { |r| request_complete?(r) }.size
|
|
|
|
completed_requests.positive? && packet_loss < 0.3
|
|
end
|
|
|
|
def ping(count = @count)
|
|
return if count <= 0
|
|
|
|
Async do |task|
|
|
@count.times do
|
|
task.Async do |subtask|
|
|
@sequence_id = (@sequence_id + 1) % 0xffff
|
|
data = @data || random_data
|
|
|
|
checksum = 0
|
|
message = [ICMP_ECHO, ICMP_SUBCODE, checksum, @ping_id, @sequence_id, data].pack(BIT_PACKER)
|
|
checksum = message_checksum(message)
|
|
message = [ICMP_ECHO, ICMP_SUBCODE, checksum, @ping_id, @sequence_id, data].pack(BIT_PACKER)
|
|
|
|
@socket.send(message, 0, @socket_address)
|
|
|
|
s = monotonic_time
|
|
request = @echo_requests[@echo_requests_index]
|
|
request.ping_id = @ping_id
|
|
request.sequence_id = @sequence_id
|
|
request.data = data
|
|
request.time = nil
|
|
request.timed_out = false
|
|
@echo_requests_index = (@echo_requests_index + 1) % ECHO_REQUEST_HISTORY
|
|
|
|
subtask.with_timeout(2) do
|
|
loop do
|
|
data, _addrinfo = @socket.recvfrom(1500)
|
|
|
|
# ignore corruption
|
|
next unless verified?(data)
|
|
|
|
header = ICMPHeader.new(*data.unpack(BIT_PACKER))
|
|
|
|
if header.type == ICMP_ECHOREPLY && header._sequence_id == request.sequence_id && header.data == request.data
|
|
duration = monotonic_time - s
|
|
request.time = duration
|
|
|
|
break
|
|
end
|
|
end
|
|
rescue Async::TimeoutError
|
|
request.timed_out = true
|
|
end
|
|
end
|
|
|
|
# Don't send out pings in a flood, it's considered rude.
|
|
sleep @interval / 1000.0
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|