lanet 0.2.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +22 -1
- data/Gemfile.lock +1 -1
- data/README.md +163 -18
- data/index.html +388 -16
- data/lib/lanet/cli.rb +95 -14
- data/lib/lanet/file_transfer.rb +308 -0
- data/lib/lanet/scanner.rb +101 -135
- data/lib/lanet/version.rb +1 -1
- data/lib/lanet.rb +36 -27
- metadata +3 -2
@@ -0,0 +1,308 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "digest"
|
4
|
+
require "fileutils"
|
5
|
+
require "tempfile"
|
6
|
+
require "zlib"
|
7
|
+
require "securerandom"
|
8
|
+
require "base64"
|
9
|
+
require "json"
|
10
|
+
require "socket"
|
11
|
+
require "timeout"
|
12
|
+
|
13
|
+
module Lanet
|
14
|
+
class FileTransfer
|
15
|
+
# Constants
|
16
|
+
# Use smaller chunks in tests to avoid "Message too long" errors
|
17
|
+
CHUNK_SIZE = if ENV["LANET_TEST_CHUNK_SIZE"]
|
18
|
+
ENV["LANET_TEST_CHUNK_SIZE"].to_i
|
19
|
+
elsif ENV["RACK_ENV"] == "test"
|
20
|
+
8192 # 8KB in test environment
|
21
|
+
else
|
22
|
+
65_536 # 64KB in production
|
23
|
+
end
|
24
|
+
|
25
|
+
MAX_RETRIES = 3
|
26
|
+
TIMEOUT = ENV["RACK_ENV"] == "test" ? 2 : 10 # Seconds
|
27
|
+
|
28
|
+
# Message types
|
29
|
+
FILE_HEADER = "FH" # File metadata
|
30
|
+
FILE_CHUNK = "FC" # File data chunk
|
31
|
+
FILE_END = "FE" # End of transfer
|
32
|
+
FILE_ACK = "FA" # Acknowledgment
|
33
|
+
FILE_ERROR = "FR" # Error message
|
34
|
+
|
35
|
+
# Custom error class
|
36
|
+
class Error < StandardError; end
|
37
|
+
|
38
|
+
# Attributes for tracking progress
|
39
|
+
attr_reader :progress, :file_size, :transferred_bytes
|
40
|
+
|
41
|
+
### Initialization
|
42
|
+
def initialize(port = nil)
|
43
|
+
@port = port || 5001 # Default port for file transfers
|
44
|
+
@progress = 0.0
|
45
|
+
@file_size = 0
|
46
|
+
@transferred_bytes = 0
|
47
|
+
@sender = Lanet::Sender.new(@port) # Assumes Lanet::Sender is defined elsewhere
|
48
|
+
@cancellation_requested = false
|
49
|
+
end
|
50
|
+
|
51
|
+
### Send File Method
|
52
|
+
def send_file(target_ip, file_path, encryption_key = nil, private_key = nil, progress_callback = nil)
|
53
|
+
# Validate file
|
54
|
+
unless File.exist?(file_path) && File.file?(file_path)
|
55
|
+
raise Error, "File not found or is not a regular file: #{file_path}"
|
56
|
+
end
|
57
|
+
|
58
|
+
# Initialize transfer state
|
59
|
+
@file_size = File.size(file_path)
|
60
|
+
@transferred_bytes = 0
|
61
|
+
@progress = 0.0
|
62
|
+
@cancellation_requested = false
|
63
|
+
transfer_id = SecureRandom.uuid
|
64
|
+
chunk_index = 0
|
65
|
+
|
66
|
+
receiver = nil
|
67
|
+
|
68
|
+
begin
|
69
|
+
# Send file header
|
70
|
+
file_name = File.basename(file_path)
|
71
|
+
file_checksum = calculate_file_checksum(file_path)
|
72
|
+
header_data = {
|
73
|
+
id: transfer_id,
|
74
|
+
name: file_name,
|
75
|
+
size: @file_size,
|
76
|
+
checksum: file_checksum,
|
77
|
+
timestamp: Time.now.to_i
|
78
|
+
}.to_json
|
79
|
+
header_message = Lanet::Encryptor.prepare_message("#{FILE_HEADER}#{header_data}", encryption_key, private_key)
|
80
|
+
@sender.send_to(target_ip, header_message)
|
81
|
+
|
82
|
+
# Wait for initial ACK
|
83
|
+
receiver = UDPSocket.new
|
84
|
+
receiver.bind("0.0.0.0", @port)
|
85
|
+
wait_for_ack(receiver, target_ip, transfer_id, encryption_key, "initial")
|
86
|
+
|
87
|
+
# Send file chunks
|
88
|
+
File.open(file_path, "rb") do |file|
|
89
|
+
until file.eof? || @cancellation_requested
|
90
|
+
chunk = file.read(CHUNK_SIZE)
|
91
|
+
chunk_data = {
|
92
|
+
id: transfer_id,
|
93
|
+
index: chunk_index,
|
94
|
+
data: Base64.strict_encode64(chunk)
|
95
|
+
}.to_json
|
96
|
+
chunk_message = Lanet::Encryptor.prepare_message("#{FILE_CHUNK}#{chunk_data}", encryption_key, private_key)
|
97
|
+
@sender.send_to(target_ip, chunk_message)
|
98
|
+
|
99
|
+
chunk_index += 1
|
100
|
+
@transferred_bytes += chunk.bytesize
|
101
|
+
@progress = (@transferred_bytes.to_f / @file_size * 100).round(2)
|
102
|
+
progress_callback&.call(@progress, @transferred_bytes, @file_size)
|
103
|
+
|
104
|
+
sleep(0.01) # Prevent overwhelming the receiver
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Send end marker and wait for final ACK
|
109
|
+
unless @cancellation_requested
|
110
|
+
end_data = { id: transfer_id, total_chunks: chunk_index }.to_json
|
111
|
+
end_message = Lanet::Encryptor.prepare_message("#{FILE_END}#{end_data}", encryption_key, private_key)
|
112
|
+
@sender.send_to(target_ip, end_message)
|
113
|
+
wait_for_ack(receiver, target_ip, transfer_id, encryption_key, "final")
|
114
|
+
true # Transfer successful
|
115
|
+
end
|
116
|
+
rescue StandardError => e
|
117
|
+
send_error(target_ip, transfer_id, e.message, encryption_key, private_key)
|
118
|
+
raise Error, "File transfer failed: #{e.message}"
|
119
|
+
ensure
|
120
|
+
receiver&.close
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
### Receive File Method
|
125
|
+
def receive_file(output_dir, encryption_key = nil, public_key = nil, progress_callback = nil)
|
126
|
+
FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)
|
127
|
+
receiver = UDPSocket.new
|
128
|
+
receiver.bind("0.0.0.0", @port)
|
129
|
+
active_transfers = {}
|
130
|
+
|
131
|
+
begin
|
132
|
+
loop do
|
133
|
+
data, addr = receiver.recvfrom(65_536) # Large buffer for chunks
|
134
|
+
sender_ip = addr[3]
|
135
|
+
result = Lanet::Encryptor.process_message(data, encryption_key, public_key)
|
136
|
+
next unless result[:content]&.length&.> 2
|
137
|
+
|
138
|
+
message_type = result[:content][0..1]
|
139
|
+
message_data = result[:content][2..]
|
140
|
+
|
141
|
+
case message_type
|
142
|
+
when FILE_HEADER
|
143
|
+
handle_file_header(sender_ip, message_data, active_transfers, encryption_key, progress_callback)
|
144
|
+
when FILE_CHUNK
|
145
|
+
handle_file_chunk(sender_ip, message_data, active_transfers, progress_callback, encryption_key)
|
146
|
+
when FILE_END
|
147
|
+
handle_file_end(sender_ip, message_data, active_transfers, output_dir, encryption_key, progress_callback)
|
148
|
+
when FILE_ERROR
|
149
|
+
handle_file_error(sender_ip, message_data, active_transfers, progress_callback)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
rescue Interrupt
|
153
|
+
puts "\nFile receiver stopped."
|
154
|
+
ensure
|
155
|
+
cleanup_transfers(active_transfers)
|
156
|
+
receiver.close
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
### Cancel Transfer
|
161
|
+
def cancel_transfer
|
162
|
+
@cancellation_requested = true
|
163
|
+
end
|
164
|
+
|
165
|
+
private
|
166
|
+
|
167
|
+
### Helper Methods
|
168
|
+
|
169
|
+
def calculate_file_checksum(file_path)
|
170
|
+
Digest::SHA256.file(file_path).hexdigest
|
171
|
+
end
|
172
|
+
|
173
|
+
def send_error(target_ip, transfer_id, message, encryption_key, private_key = nil)
|
174
|
+
error_data = { id: transfer_id, message: message, timestamp: Time.now.to_i }.to_json
|
175
|
+
error_message = Lanet::Encryptor.prepare_message("#{FILE_ERROR}#{error_data}", encryption_key, private_key)
|
176
|
+
@sender.send_to(target_ip, error_message)
|
177
|
+
end
|
178
|
+
|
179
|
+
def wait_for_ack(receiver, target_ip, transfer_id, encryption_key, context)
|
180
|
+
Timeout.timeout(TIMEOUT) do
|
181
|
+
data, addr = receiver.recvfrom(1024)
|
182
|
+
sender_ip = addr[3]
|
183
|
+
if sender_ip == target_ip
|
184
|
+
result = Lanet::Encryptor.process_message(data, encryption_key)
|
185
|
+
return if result[:content]&.start_with?(FILE_ACK) && result[:content][2..] == transfer_id
|
186
|
+
|
187
|
+
# Valid ACK received
|
188
|
+
|
189
|
+
raise Error, "Invalid #{context} ACK received: #{result[:content]}"
|
190
|
+
|
191
|
+
end
|
192
|
+
end
|
193
|
+
rescue Timeout::Error
|
194
|
+
raise Error, "Timeout waiting for #{context} transfer acknowledgment"
|
195
|
+
end
|
196
|
+
|
197
|
+
def handle_file_header(sender_ip, message_data, active_transfers, encryption_key, callback)
|
198
|
+
header = JSON.parse(message_data)
|
199
|
+
transfer_id = header["id"]
|
200
|
+
active_transfers[transfer_id] = {
|
201
|
+
sender_ip: sender_ip,
|
202
|
+
file_name: header["name"],
|
203
|
+
file_size: header["size"],
|
204
|
+
expected_checksum: header["checksum"],
|
205
|
+
temp_file: Tempfile.new([File.basename(header["name"], ".*"), File.extname(header["name"])]),
|
206
|
+
chunks_received: 0,
|
207
|
+
timestamp: Time.now
|
208
|
+
}
|
209
|
+
ack_message = Lanet::Encryptor.prepare_message("#{FILE_ACK}#{transfer_id}", encryption_key)
|
210
|
+
@sender.send_to(sender_ip, ack_message)
|
211
|
+
callback&.call(:start, {
|
212
|
+
transfer_id: transfer_id,
|
213
|
+
sender_ip: sender_ip,
|
214
|
+
file_name: header["name"],
|
215
|
+
file_size: header["size"]
|
216
|
+
})
|
217
|
+
rescue JSON::ParserError => e
|
218
|
+
send_error(sender_ip, "unknown", "Invalid header format: #{e.message}", encryption_key)
|
219
|
+
end
|
220
|
+
|
221
|
+
def handle_file_chunk(sender_ip, message_data, active_transfers, callback, encryption_key)
|
222
|
+
chunk = JSON.parse(message_data)
|
223
|
+
transfer_id = chunk["id"]
|
224
|
+
transfer = active_transfers[transfer_id]
|
225
|
+
if transfer && transfer[:sender_ip] == sender_ip
|
226
|
+
chunk_data = Base64.strict_decode64(chunk["data"])
|
227
|
+
transfer[:temp_file].write(chunk_data)
|
228
|
+
transfer[:chunks_received] += 1
|
229
|
+
bytes_received = transfer[:temp_file].size
|
230
|
+
progress = (bytes_received.to_f / transfer[:file_size] * 100).round(2)
|
231
|
+
callback&.call(:progress, {
|
232
|
+
transfer_id: transfer_id,
|
233
|
+
sender_ip: sender_ip,
|
234
|
+
file_name: transfer[:file_name],
|
235
|
+
progress: progress,
|
236
|
+
bytes_received: bytes_received,
|
237
|
+
total_bytes: transfer[:file_size]
|
238
|
+
})
|
239
|
+
end
|
240
|
+
rescue JSON::ParserError => e
|
241
|
+
send_error(sender_ip, "unknown", "Invalid chunk format: #{e.message}", encryption_key)
|
242
|
+
end
|
243
|
+
|
244
|
+
def handle_file_end(sender_ip, message_data, active_transfers, output_dir, encryption_key, callback)
|
245
|
+
end_data = JSON.parse(message_data)
|
246
|
+
transfer_id = end_data["id"]
|
247
|
+
transfer = active_transfers[transfer_id]
|
248
|
+
if transfer && transfer[:sender_ip] == sender_ip
|
249
|
+
transfer[:temp_file].close
|
250
|
+
calculated_checksum = calculate_file_checksum(transfer[:temp_file].path)
|
251
|
+
if calculated_checksum == transfer[:expected_checksum]
|
252
|
+
final_path = File.join(output_dir, transfer[:file_name])
|
253
|
+
FileUtils.mv(transfer[:temp_file].path, final_path)
|
254
|
+
ack_message = Lanet::Encryptor.prepare_message("#{FILE_ACK}#{transfer_id}", encryption_key)
|
255
|
+
@sender.send_to(sender_ip, ack_message)
|
256
|
+
callback&.call(:complete, {
|
257
|
+
transfer_id: transfer_id,
|
258
|
+
sender_ip: sender_ip,
|
259
|
+
file_name: transfer[:file_name],
|
260
|
+
file_path: final_path
|
261
|
+
})
|
262
|
+
else
|
263
|
+
error_msg = "Checksum verification failed"
|
264
|
+
send_error(sender_ip, transfer_id, error_msg, encryption_key)
|
265
|
+
callback&.call(:error, {
|
266
|
+
transfer_id: transfer_id,
|
267
|
+
sender_ip: sender_ip,
|
268
|
+
error: error_msg
|
269
|
+
})
|
270
|
+
end
|
271
|
+
transfer[:temp_file].unlink
|
272
|
+
active_transfers.delete(transfer_id)
|
273
|
+
end
|
274
|
+
rescue JSON::ParserError => e
|
275
|
+
send_error(sender_ip, "unknown", "Invalid end marker format: #{e.message}", encryption_key)
|
276
|
+
end
|
277
|
+
|
278
|
+
def handle_file_error(sender_ip, message_data, active_transfers, callback)
|
279
|
+
error_data = JSON.parse(message_data)
|
280
|
+
transfer_id = error_data["id"]
|
281
|
+
if callback && active_transfers[transfer_id]
|
282
|
+
callback.call(:error, {
|
283
|
+
transfer_id: transfer_id,
|
284
|
+
sender_ip: sender_ip,
|
285
|
+
error: error_data["message"]
|
286
|
+
})
|
287
|
+
if active_transfers[transfer_id]
|
288
|
+
active_transfers[transfer_id][:temp_file].close
|
289
|
+
active_transfers[transfer_id][:temp_file].unlink
|
290
|
+
active_transfers.delete(transfer_id)
|
291
|
+
end
|
292
|
+
end
|
293
|
+
rescue JSON::ParserError
|
294
|
+
# Ignore malformed error messages
|
295
|
+
end
|
296
|
+
|
297
|
+
def cleanup_transfers(active_transfers)
|
298
|
+
active_transfers.each_value do |transfer|
|
299
|
+
transfer[:temp_file].close
|
300
|
+
begin
|
301
|
+
transfer[:temp_file].unlink
|
302
|
+
rescue StandardError
|
303
|
+
nil
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
data/lib/lanet/scanner.rb
CHANGED
@@ -25,55 +25,54 @@ module Lanet
|
|
25
25
|
8443 => "HTTPS-ALT"
|
26
26
|
}.freeze
|
27
27
|
|
28
|
-
# Ports to check during scan
|
29
28
|
QUICK_CHECK_PORTS = [80, 443, 22, 445, 139, 8080].freeze
|
30
29
|
|
31
30
|
def initialize
|
32
31
|
@hosts = []
|
33
32
|
@mutex = Mutex.new
|
33
|
+
@arp_cache = {}
|
34
34
|
end
|
35
35
|
|
36
|
-
# Scan network and return active hosts
|
37
36
|
def scan(cidr, timeout = 1, max_threads = 32, verbose = false)
|
38
37
|
@verbose = verbose
|
39
38
|
@timeout = timeout
|
40
|
-
|
41
|
-
# Clear previous scan results
|
42
39
|
@hosts = []
|
43
|
-
|
44
|
-
# Get the range of IP addresses to scan
|
45
40
|
range = IPAddr.new(cidr).to_range
|
46
|
-
|
47
|
-
# Create a queue of IPs to scan
|
48
41
|
queue = Queue.new
|
49
42
|
range.each { |ip| queue << ip.to_s }
|
50
|
-
|
51
43
|
total_ips = queue.size
|
52
44
|
completed = 0
|
53
45
|
|
54
|
-
#
|
55
|
-
|
46
|
+
# Initial ARP cache population
|
47
|
+
@arp_cache = parse_arp_table
|
56
48
|
|
57
|
-
# Create worker threads to process the queue
|
58
49
|
threads = Array.new([max_threads, total_ips].min) do
|
59
50
|
Thread.new do
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
51
|
+
loop do
|
52
|
+
begin
|
53
|
+
ip = queue.pop(true)
|
54
|
+
rescue ThreadError
|
55
|
+
break
|
56
|
+
end
|
65
57
|
scan_host(ip)
|
66
58
|
@mutex.synchronize do
|
67
59
|
completed += 1
|
68
60
|
if total_ips < 100 || (completed % 10).zero? || completed == total_ips
|
69
|
-
print_progress(completed,
|
70
|
-
total_ips)
|
61
|
+
print_progress(completed, total_ips)
|
71
62
|
end
|
72
63
|
end
|
73
64
|
end
|
74
65
|
end
|
75
66
|
end
|
76
67
|
|
68
|
+
# Periodically update ARP cache
|
69
|
+
arp_updater = Thread.new do
|
70
|
+
while threads.any?(&:alive?)
|
71
|
+
sleep 5
|
72
|
+
@mutex.synchronize { @arp_cache = parse_arp_table }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
77
76
|
begin
|
78
77
|
threads.each(&:join)
|
79
78
|
print_progress(total_ips, total_ips)
|
@@ -82,6 +81,8 @@ module Lanet
|
|
82
81
|
rescue Interrupt
|
83
82
|
puts "\nScan interrupted. Returning partial results..."
|
84
83
|
@verbose ? @hosts : @hosts.map { |h| h[:ip] }
|
84
|
+
ensure
|
85
|
+
arp_updater.kill if arp_updater.alive?
|
85
86
|
end
|
86
87
|
end
|
87
88
|
|
@@ -92,47 +93,62 @@ module Lanet
|
|
92
93
|
print "\rScanning network: #{percent}% complete (#{completed}/#{total})"
|
93
94
|
end
|
94
95
|
|
95
|
-
def
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
range.each { |ip| queue << ip.to_s }
|
96
|
+
def parse_arp_table
|
97
|
+
cmd = RbConfig::CONFIG["host_os"] =~ /mswin|mingw|cygwin/ ? "arp -a" : "arp -a"
|
98
|
+
output = `#{cmd}`
|
99
|
+
arp_cache = {}
|
100
100
|
|
101
|
-
|
102
|
-
|
101
|
+
case RbConfig::CONFIG["host_os"]
|
102
|
+
when /darwin/
|
103
|
+
output.each_line do |line|
|
104
|
+
next unless line =~ /\((\d+\.\d+\.\d+\.\d+)\) at ([0-9a-f:]+) on/
|
103
105
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
queue.pop(true)
|
108
|
-
rescue ThreadError
|
109
|
-
nil
|
110
|
-
end)
|
111
|
-
# Use system ping to update ARP table
|
112
|
-
system("ping -c 1 -W 1 #{ip} > /dev/null 2>&1 &")
|
113
|
-
sleep 0.01 # Small delay to prevent overwhelming the system
|
114
|
-
processed += 1
|
115
|
-
end
|
106
|
+
ip = ::Regexp.last_match(1)
|
107
|
+
mac = ::Regexp.last_match(2).downcase
|
108
|
+
arp_cache[ip] = mac unless mac == "(incomplete)"
|
116
109
|
end
|
117
|
-
|
110
|
+
when /linux/
|
111
|
+
output.each_line do |line|
|
112
|
+
next unless line =~ /^(\d+\.\d+\.\d+\.\d+)\s+\w+\s+([0-9a-f:]+)\s+/
|
113
|
+
|
114
|
+
ip = ::Regexp.last_match(1)
|
115
|
+
mac = ::Regexp.last_match(2).downcase
|
116
|
+
arp_cache[ip] = mac unless mac == "00:00:00:00:00:00"
|
117
|
+
end
|
118
|
+
when /mswin|mingw|cygwin/
|
119
|
+
output.each_line do |line|
|
120
|
+
next unless line =~ /^\s*(\d+\.\d+\.\d+\.\d+)\s+([0-9a-f-]+)\s+/
|
118
121
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
+
ip = ::Regexp.last_match(1)
|
123
|
+
mac = ::Regexp.last_match(2).gsub("-", ":").downcase
|
124
|
+
arp_cache[ip] = mac
|
125
|
+
end
|
126
|
+
end
|
127
|
+
arp_cache
|
122
128
|
end
|
123
129
|
|
124
130
|
def scan_host(ip)
|
125
|
-
#
|
126
|
-
|
131
|
+
# Handle broadcast addresses immediately
|
132
|
+
if ip.end_with?(".255") || ip == "255.255.255.255"
|
133
|
+
host_info = { ip: ip, mac: "ff:ff:ff:ff:ff:ff", response_time: 0, detection_method: "Broadcast" }
|
134
|
+
if @verbose
|
135
|
+
host_info[:hostname] = "Broadcast"
|
136
|
+
host_info[:ports] = {}
|
137
|
+
end
|
138
|
+
@mutex.synchronize { @hosts << host_info }
|
139
|
+
return
|
140
|
+
end
|
141
|
+
|
142
|
+
# Skip network addresses
|
143
|
+
return if ip.end_with?(".0") && !ip.end_with?(".0.0")
|
127
144
|
|
128
|
-
# Use multiple methods to detect if a host is alive
|
129
145
|
is_active = false
|
130
146
|
detection_method = nil
|
131
147
|
response_time = nil
|
132
148
|
start_time = Time.now
|
133
149
|
open_ports = []
|
134
150
|
|
135
|
-
#
|
151
|
+
# TCP port scan
|
136
152
|
tcp_result = tcp_port_scan(ip, QUICK_CHECK_PORTS)
|
137
153
|
if tcp_result[:active]
|
138
154
|
is_active = true
|
@@ -140,116 +156,86 @@ module Lanet
|
|
140
156
|
open_ports = tcp_result[:open_ports]
|
141
157
|
end
|
142
158
|
|
143
|
-
#
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
is_active = true
|
148
|
-
detection_method = "ICMP"
|
149
|
-
end
|
159
|
+
# ICMP ping
|
160
|
+
if !is_active && ping_check(ip)
|
161
|
+
is_active = true
|
162
|
+
detection_method = "ICMP"
|
150
163
|
end
|
151
164
|
|
152
|
-
#
|
153
|
-
if !is_active && (ip.end_with?(".1") || ip.end_with?(".254")
|
154
|
-
|
155
|
-
|
156
|
-
is_active = true
|
157
|
-
detection_method = "UDP"
|
158
|
-
end
|
165
|
+
# UDP check for common network devices
|
166
|
+
if !is_active && (ip.end_with?(".1") || ip.end_with?(".254")) && udp_check(ip)
|
167
|
+
is_active = true
|
168
|
+
detection_method = "UDP"
|
159
169
|
end
|
160
170
|
|
161
|
-
#
|
171
|
+
# ARP check
|
162
172
|
unless is_active
|
163
|
-
|
164
|
-
if
|
173
|
+
mac = get_mac_address(ip)
|
174
|
+
if mac && mac != "(incomplete)"
|
165
175
|
is_active = true
|
166
176
|
detection_method = "ARP"
|
167
177
|
end
|
168
178
|
end
|
169
179
|
|
170
|
-
# For broadcast addresses, always consider them active
|
171
|
-
if ip.end_with?(".255") || ip == "255.255.255.255"
|
172
|
-
is_active = true
|
173
|
-
detection_method = "Broadcast"
|
174
|
-
end
|
175
|
-
|
176
|
-
# Calculate response time
|
177
180
|
response_time = ((Time.now - start_time) * 1000).round(2) if is_active
|
178
|
-
|
179
181
|
return unless is_active
|
180
182
|
|
181
|
-
|
182
|
-
host_info = {
|
183
|
-
ip: ip,
|
184
|
-
mac: get_mac_address(ip),
|
185
|
-
response_time: response_time,
|
186
|
-
detection_method: detection_method
|
187
|
-
}
|
183
|
+
host_info = { ip: ip, mac: get_mac_address(ip), response_time: response_time, detection_method: detection_method }
|
188
184
|
|
189
185
|
if @verbose
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
end
|
195
|
-
rescue Resolv::ResolvError, Timeout::Error
|
196
|
-
host_info[:hostname] = "Unknown"
|
186
|
+
host_info[:hostname] = begin
|
187
|
+
Timeout.timeout(1) { Resolv.getname(ip) }
|
188
|
+
rescue StandardError
|
189
|
+
"Unknown"
|
197
190
|
end
|
198
|
-
|
199
|
-
# For verbose mode, scan more ports if TCP detection method was successful
|
200
191
|
if detection_method == "TCP"
|
201
192
|
extra_ports = tcp_port_scan(ip, COMMON_PORTS.keys - QUICK_CHECK_PORTS)[:open_ports]
|
202
193
|
open_ports += extra_ports
|
203
194
|
end
|
204
|
-
|
205
195
|
host_info[:ports] = open_ports.map { |port| [port, COMMON_PORTS[port] || "Unknown"] }.to_h
|
206
196
|
end
|
207
197
|
|
208
198
|
@mutex.synchronize { @hosts << host_info }
|
209
|
-
rescue StandardError => e
|
210
|
-
puts "\nError scanning host #{ip}: #{e.message}" if $DEBUG
|
211
199
|
end
|
212
200
|
|
213
201
|
def tcp_port_scan(ip, ports)
|
214
202
|
open_ports = []
|
215
203
|
is_active = false
|
204
|
+
threads = ports.map do |port|
|
205
|
+
Thread.new(port) do |p|
|
206
|
+
Timeout.timeout(@timeout) do
|
207
|
+
socket = TCPSocket.new(ip, p)
|
208
|
+
Thread.current[:open] = p
|
209
|
+
socket.close
|
210
|
+
end
|
211
|
+
rescue Errno::ECONNREFUSED
|
212
|
+
Thread.current[:active] = true
|
213
|
+
rescue StandardError
|
214
|
+
# Port closed or filtered
|
215
|
+
end
|
216
|
+
end
|
216
217
|
|
217
|
-
|
218
|
-
|
219
|
-
|
218
|
+
threads.each do |thread|
|
219
|
+
thread.join
|
220
|
+
if thread[:open]
|
221
|
+
open_ports << thread[:open]
|
222
|
+
is_active = true
|
223
|
+
elsif thread[:active]
|
220
224
|
is_active = true
|
221
|
-
open_ports << port
|
222
|
-
socket.close
|
223
225
|
end
|
224
|
-
rescue Errno::ECONNREFUSED
|
225
|
-
# Connection refused means host is up but port is closed
|
226
|
-
is_active = true
|
227
|
-
rescue StandardError
|
228
|
-
# Other errors mean port is probably closed or filtered
|
229
226
|
end
|
230
227
|
|
231
228
|
{ active: is_active, open_ports: open_ports }
|
232
229
|
end
|
233
230
|
|
234
231
|
def ping_check(ip)
|
235
|
-
cmd =
|
236
|
-
when /darwin/
|
237
|
-
"ping -c 1 -W 1 #{ip}"
|
238
|
-
when /linux/
|
239
|
-
"ping -c 1 -W 1 #{ip}"
|
240
|
-
when /mswin|mingw|cygwin/
|
241
|
-
"ping -n 1 -w 1000 #{ip}"
|
242
|
-
else
|
243
|
-
"ping -c 1 -W 1 #{ip}"
|
244
|
-
end
|
245
|
-
|
232
|
+
cmd = RbConfig::CONFIG["host_os"] =~ /mswin|mingw|cygwin/ ? "ping -n 1 -w 1000 #{ip}" : "ping -c 1 -W 1 #{ip}"
|
246
233
|
system("#{cmd} > /dev/null 2>&1")
|
247
234
|
$CHILD_STATUS.exitstatus.zero?
|
248
235
|
end
|
249
236
|
|
250
237
|
def udp_check(ip)
|
251
238
|
common_udp_ports = [53, 67, 68, 123, 137, 138, 1900, 5353]
|
252
|
-
|
253
239
|
common_udp_ports.each do |port|
|
254
240
|
Timeout.timeout(0.5) do
|
255
241
|
socket = UDPSocket.new
|
@@ -259,35 +245,15 @@ module Lanet
|
|
259
245
|
return true
|
260
246
|
end
|
261
247
|
rescue Errno::ECONNREFUSED
|
262
|
-
return true
|
248
|
+
return true
|
263
249
|
rescue StandardError
|
264
|
-
|
250
|
+
next
|
265
251
|
end
|
266
252
|
false
|
267
253
|
end
|
268
254
|
|
269
255
|
def get_mac_address(ip)
|
270
|
-
|
271
|
-
|
272
|
-
# Get MAC from ARP table
|
273
|
-
cmd = case RbConfig::CONFIG["host_os"]
|
274
|
-
when /darwin/
|
275
|
-
"arp -n #{ip}"
|
276
|
-
when /linux/
|
277
|
-
"arp -n #{ip}"
|
278
|
-
when /mswin|mingw|cygwin/
|
279
|
-
"arp -a #{ip}"
|
280
|
-
else
|
281
|
-
"arp -n #{ip}"
|
282
|
-
end
|
283
|
-
|
284
|
-
output = `#{cmd}`
|
285
|
-
|
286
|
-
if output =~ /([0-9a-fA-F]{1,2}[:-][0-9a-fA-F]{1,2}[:-][0-9a-fA-F]{1,2}[:-][0-9a-fA-F]{1,2}[:-][0-9a-fA-F]{1,2}[:-][0-9a-fA-F]{1,2})/
|
287
|
-
::Regexp.last_match(1).downcase
|
288
|
-
else
|
289
|
-
"(incomplete)"
|
290
|
-
end
|
256
|
+
@mutex.synchronize { @arp_cache[ip] || "(incomplete)" }
|
291
257
|
end
|
292
258
|
end
|
293
259
|
end
|
data/lib/lanet/version.rb
CHANGED