lanet 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/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +25 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +67 -0
- data/LICENSE.txt +21 -0
- data/README.md +437 -0
- data/Rakefile +12 -0
- data/bin/console +11 -0
- data/bin/lanet +7 -0
- data/bin/setup +8 -0
- data/exe/lanet +8 -0
- data/index.html +233 -0
- data/lib/lanet/cli.rb +229 -0
- data/lib/lanet/encryptor.rb +84 -0
- data/lib/lanet/ping.rb +297 -0
- data/lib/lanet/receiver.rb +21 -0
- data/lib/lanet/scanner.rb +293 -0
- data/lib/lanet/sender.rb +21 -0
- data/lib/lanet/version.rb +5 -0
- data/lib/lanet.rb +45 -0
- data/sig/lanet.rbs +4 -0
- metadata +128 -0
data/lib/lanet/ping.rb
ADDED
@@ -0,0 +1,297 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "English"
|
4
|
+
require "open3"
|
5
|
+
require "timeout"
|
6
|
+
|
7
|
+
module Lanet
|
8
|
+
class Ping
|
9
|
+
attr_reader :timeout, :count
|
10
|
+
|
11
|
+
def initialize(timeout: 1, count: 3)
|
12
|
+
@timeout = timeout
|
13
|
+
@count = count
|
14
|
+
@continuous = false
|
15
|
+
end
|
16
|
+
|
17
|
+
# Ping a single host with real-time output
|
18
|
+
# @param host [String] The IP address or hostname to ping
|
19
|
+
# @param realtime [Boolean] Whether to print output in real-time
|
20
|
+
# @param continuous [Boolean] Whether to ping continuously until interrupted
|
21
|
+
# @return [Hash] Result with status, response time, and output
|
22
|
+
def ping_host(host, realtime = false, continuous = false)
|
23
|
+
@continuous = continuous
|
24
|
+
|
25
|
+
result = {
|
26
|
+
host: host,
|
27
|
+
status: false,
|
28
|
+
response_time: nil,
|
29
|
+
packet_loss: 100,
|
30
|
+
output: "",
|
31
|
+
responses: [] # Store individual ping responses
|
32
|
+
}
|
33
|
+
|
34
|
+
begin
|
35
|
+
# Command varies by OS
|
36
|
+
ping_cmd = ping_command(host)
|
37
|
+
|
38
|
+
# Use different approaches based on output mode
|
39
|
+
if realtime
|
40
|
+
process_realtime_ping(ping_cmd, result)
|
41
|
+
else
|
42
|
+
# Use backticks for quiet mode - much more reliable than Open3 for this case
|
43
|
+
process_quiet_ping(ping_cmd, result)
|
44
|
+
end
|
45
|
+
rescue Timeout::Error
|
46
|
+
result[:output] = "Ping timed out after #{@timeout * 2} seconds"
|
47
|
+
rescue Interrupt
|
48
|
+
# Handle Ctrl+C gracefully for continuous mode
|
49
|
+
print_ping_statistics(host, result) if realtime
|
50
|
+
exit(0) if realtime # Only exit if in realtime mode - otherwise let the caller handle it
|
51
|
+
rescue StandardError => e
|
52
|
+
result[:output] = "Error: #{e.message}"
|
53
|
+
end
|
54
|
+
|
55
|
+
# Only print statistics in realtime mode and not continuous
|
56
|
+
print_ping_statistics(host, result) if realtime && !@continuous
|
57
|
+
|
58
|
+
result
|
59
|
+
end
|
60
|
+
|
61
|
+
# Check if a host is reachable
|
62
|
+
# @param host [String] The IP address or hostname to check
|
63
|
+
# @return [Boolean] True if the host is reachable
|
64
|
+
def reachable?(host)
|
65
|
+
ping_host(host)[:status]
|
66
|
+
end
|
67
|
+
|
68
|
+
# Get the response time for a host
|
69
|
+
# @param host [String] The IP address or hostname to check
|
70
|
+
# @return [Float, nil] The response time in ms, or nil if unreachable
|
71
|
+
def response_time(host)
|
72
|
+
ping_host(host)[:response_time]
|
73
|
+
end
|
74
|
+
|
75
|
+
# Ping multiple hosts in parallel
|
76
|
+
# @param hosts [Array<String>] Array of IP addresses or hostnames
|
77
|
+
# @param realtime [Boolean] Whether to print output in real-time
|
78
|
+
# @param continuous [Boolean] Whether to ping continuously until interrupted
|
79
|
+
# @return [Hash] Results indexed by host
|
80
|
+
def ping_hosts(hosts, realtime = false, continuous = false)
|
81
|
+
results = {}
|
82
|
+
if realtime
|
83
|
+
# For real-time output, run pings sequentially
|
84
|
+
hosts.each do |host|
|
85
|
+
results[host] = ping_host(host, true, continuous)
|
86
|
+
puts "\n" unless host == hosts.last
|
87
|
+
end
|
88
|
+
else
|
89
|
+
# For non-realtime output, run pings in parallel
|
90
|
+
threads = []
|
91
|
+
|
92
|
+
hosts.each do |host|
|
93
|
+
threads << Thread.new do
|
94
|
+
results[host] = ping_host(host)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
threads.each(&:join)
|
99
|
+
end
|
100
|
+
results
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def process_quiet_ping(ping_cmd, result)
|
106
|
+
# Use backticks for simplest, most reliable execution in quiet mode
|
107
|
+
Timeout.timeout(@timeout * @count * 2) do
|
108
|
+
output = `#{ping_cmd}`
|
109
|
+
result[:output] = output
|
110
|
+
exit_status = $CHILD_STATUS.exitstatus
|
111
|
+
|
112
|
+
# Process the output
|
113
|
+
if exit_status.zero? || output.include?("bytes from")
|
114
|
+
result[:status] = true
|
115
|
+
|
116
|
+
# Extract individual ping responses
|
117
|
+
extract_ping_responses(output, result)
|
118
|
+
|
119
|
+
# Calculate average response time
|
120
|
+
if result[:responses].any?
|
121
|
+
result[:response_time] = result[:responses].map { |r| r[:time] }.sum / result[:responses].size
|
122
|
+
end
|
123
|
+
|
124
|
+
# Extract packet loss
|
125
|
+
result[:packet_loss] = ::Regexp.last_match(1).to_f if output =~ /(\d+(?:\.\d+)?)% packet loss/
|
126
|
+
else
|
127
|
+
# No responses
|
128
|
+
result[:status] = false
|
129
|
+
result[:packet_loss] = 100.0
|
130
|
+
end
|
131
|
+
end
|
132
|
+
rescue Timeout::Error
|
133
|
+
result[:output] = "Ping timed out after #{@timeout * @count * 2} seconds"
|
134
|
+
rescue StandardError => e
|
135
|
+
result[:output] = "Error: #{e.message}"
|
136
|
+
end
|
137
|
+
|
138
|
+
def process_realtime_ping(ping_cmd, result)
|
139
|
+
all_output = ""
|
140
|
+
thread = nil
|
141
|
+
|
142
|
+
begin
|
143
|
+
Open3.popen3(ping_cmd) do |_stdin, stdout, _stderr, process_thread|
|
144
|
+
thread = process_thread
|
145
|
+
stdout_thread = Thread.new do
|
146
|
+
# Read stdout in real time
|
147
|
+
while (line = stdout.gets)
|
148
|
+
all_output += line
|
149
|
+
print line # Print in real-time
|
150
|
+
|
151
|
+
# Parse and store responses as they come
|
152
|
+
case RbConfig::CONFIG["host_os"]
|
153
|
+
when /mswin|mingw|cygwin/
|
154
|
+
if line =~ /Reply from .* time=(\d+)ms TTL=(\d+)/
|
155
|
+
seq = result[:responses].size
|
156
|
+
ttl = ::Regexp.last_match(2).to_i
|
157
|
+
time = ::Regexp.last_match(1).to_f
|
158
|
+
result[:responses] << { seq: seq, ttl: ttl, time: time }
|
159
|
+
end
|
160
|
+
else
|
161
|
+
if line =~ /icmp_seq=(\d+) ttl=(\d+) time=([\d.]+) ms/
|
162
|
+
seq = ::Regexp.last_match(1).to_i
|
163
|
+
ttl = ::Regexp.last_match(2).to_i
|
164
|
+
time = ::Regexp.last_match(3).to_f
|
165
|
+
result[:responses] << { seq: seq, ttl: ttl, time: time }
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# For non-continuous mode, exit when we've collected enough responses
|
170
|
+
break if !@continuous && result[:responses].size >= @count
|
171
|
+
end
|
172
|
+
rescue IOError
|
173
|
+
# Stream may be closed - this is ok
|
174
|
+
end
|
175
|
+
|
176
|
+
# Wait for the stdout thread to complete or the process to exit
|
177
|
+
if @continuous
|
178
|
+
stdout_thread.join # Wait indefinitely in continuous mode
|
179
|
+
else
|
180
|
+
# For non-continuous mode, wait for completion with a reasonable timeout
|
181
|
+
begin
|
182
|
+
Timeout.timeout(@timeout * @count * 2) do
|
183
|
+
process_thread.join
|
184
|
+
end
|
185
|
+
rescue Timeout::Error
|
186
|
+
# If it takes too long, we'll terminate below in the ensure block
|
187
|
+
ensure
|
188
|
+
stdout_thread.kill if stdout_thread.alive?
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
# Set success status
|
193
|
+
result[:status] = !result[:responses].empty?
|
194
|
+
result[:output] = all_output
|
195
|
+
|
196
|
+
# Calculate response time
|
197
|
+
if result[:responses].any?
|
198
|
+
result[:response_time] = result[:responses].map { |r| r[:time] }.sum / result[:responses].size
|
199
|
+
|
200
|
+
# Calculate packet loss for non-continuous mode
|
201
|
+
unless @continuous
|
202
|
+
total_expected = @count
|
203
|
+
result[:packet_loss] = ((total_expected - result[:responses].size) / total_expected.to_f * 100).round(1)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
rescue IOError => e
|
208
|
+
# Handle IOError specifically
|
209
|
+
result[:output] += "\nWarning: IO operation failed: #{e.message}"
|
210
|
+
ensure
|
211
|
+
# Clean up any threads and processes
|
212
|
+
if thread&.alive?
|
213
|
+
|
214
|
+
begin
|
215
|
+
Process.kill("TERM", thread.pid)
|
216
|
+
rescue StandardError
|
217
|
+
nil
|
218
|
+
end
|
219
|
+
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def print_ping_statistics(host, result)
|
225
|
+
puts "\n--- #{host} ping statistics ---"
|
226
|
+
if result[:responses].any?
|
227
|
+
avg_time = result[:responses].map { |r| r[:time] }.sum / result[:responses].size
|
228
|
+
min_time = result[:responses].map { |r| r[:time] }.min
|
229
|
+
max_time = result[:responses].map { |r| r[:time] }.max
|
230
|
+
|
231
|
+
# Calculate proper packet loss
|
232
|
+
if @continuous
|
233
|
+
highest_seq = result[:responses].map { |r| r[:seq] }.max
|
234
|
+
unique_seqs = result[:responses].map { |r| r[:seq] }.uniq.size
|
235
|
+
transmitted = highest_seq + 1
|
236
|
+
packet_loss = ((transmitted - unique_seqs) / transmitted.to_f * 100).round(1)
|
237
|
+
|
238
|
+
puts "#{transmitted} packets transmitted, #{unique_seqs} packets received, #{packet_loss}% packet loss"
|
239
|
+
else
|
240
|
+
# Normal mode - compare against expected count
|
241
|
+
packet_loss = ((@count - result[:responses].size) / @count.to_f * 100).round(1)
|
242
|
+
puts "#{@count} packets transmitted, #{result[:responses].size} packets received, #{packet_loss}% packet loss"
|
243
|
+
end
|
244
|
+
|
245
|
+
puts "round-trip min/avg/max = #{min_time.round(3)}/#{avg_time.round(3)}/#{max_time.round(3)} ms"
|
246
|
+
else
|
247
|
+
puts "0 packets transmitted, 0 packets received, 100% packet loss"
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def ping_command(host)
|
252
|
+
if @continuous
|
253
|
+
# Continuous mode - don't specify count
|
254
|
+
case RbConfig::CONFIG["host_os"]
|
255
|
+
when /mswin|mingw|cygwin/
|
256
|
+
# Windows - use -t for continuous ping
|
257
|
+
"ping -t -w #{@timeout * 1000} #{host}"
|
258
|
+
when /darwin/
|
259
|
+
# macOS - for continuous ping, simply omit the count parameter
|
260
|
+
"ping #{host}"
|
261
|
+
else
|
262
|
+
# Linux/Unix - no count flag means continuous
|
263
|
+
"ping -W #{@timeout} #{host}"
|
264
|
+
end
|
265
|
+
else
|
266
|
+
# Normal mode with count
|
267
|
+
case RbConfig::CONFIG["host_os"]
|
268
|
+
when /mswin|mingw|cygwin/
|
269
|
+
# Windows
|
270
|
+
"ping -n #{@count} -w #{@timeout * 1000} #{host}"
|
271
|
+
when /darwin/
|
272
|
+
# macOS
|
273
|
+
"ping -c #{@count} #{host}"
|
274
|
+
else
|
275
|
+
# Linux/Unix
|
276
|
+
"ping -c #{@count} -W #{@timeout} #{host}"
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
def extract_ping_responses(output, result)
|
282
|
+
# Extract individual ping responses based on OS format
|
283
|
+
case RbConfig::CONFIG["host_os"]
|
284
|
+
when /mswin|mingw|cygwin/
|
285
|
+
# Windows format
|
286
|
+
output.scan(/Reply from .* time=(\d+)ms TTL=(\d+)/).each_with_index do |match, seq|
|
287
|
+
result[:responses] << { seq: seq, ttl: match[1].to_i, time: match[0].to_f }
|
288
|
+
end
|
289
|
+
else
|
290
|
+
# Unix-like format (Linux/macOS)
|
291
|
+
output.scan(/icmp_seq=(\d+) ttl=(\d+) time=([\d.]+) ms/).each do |match|
|
292
|
+
result[:responses] << { seq: match[0].to_i, ttl: match[1].to_i, time: match[2].to_f }
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "socket"
|
4
|
+
|
5
|
+
module Lanet
|
6
|
+
class Receiver
|
7
|
+
def initialize(port)
|
8
|
+
@port = port
|
9
|
+
@socket = UDPSocket.new
|
10
|
+
@socket.bind("0.0.0.0", @port)
|
11
|
+
end
|
12
|
+
|
13
|
+
def listen(&block)
|
14
|
+
loop do
|
15
|
+
data, addr = @socket.recvfrom(1024)
|
16
|
+
ip = addr[3]
|
17
|
+
block.call(data, ip)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,293 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "English"
|
4
|
+
require "ipaddr"
|
5
|
+
require "socket"
|
6
|
+
require "timeout"
|
7
|
+
require "resolv"
|
8
|
+
|
9
|
+
module Lanet
|
10
|
+
class Scanner
|
11
|
+
COMMON_PORTS = {
|
12
|
+
21 => "FTP",
|
13
|
+
22 => "SSH",
|
14
|
+
23 => "Telnet",
|
15
|
+
25 => "SMTP",
|
16
|
+
80 => "HTTP",
|
17
|
+
443 => "HTTPS",
|
18
|
+
3389 => "RDP",
|
19
|
+
5900 => "VNC",
|
20
|
+
8080 => "HTTP-ALT",
|
21
|
+
137 => "NetBIOS",
|
22
|
+
139 => "NetBIOS",
|
23
|
+
445 => "SMB",
|
24
|
+
1025 => "RPC",
|
25
|
+
8443 => "HTTPS-ALT"
|
26
|
+
}.freeze
|
27
|
+
|
28
|
+
# Ports to check during scan
|
29
|
+
QUICK_CHECK_PORTS = [80, 443, 22, 445, 139, 8080].freeze
|
30
|
+
|
31
|
+
def initialize
|
32
|
+
@hosts = []
|
33
|
+
@mutex = Mutex.new
|
34
|
+
end
|
35
|
+
|
36
|
+
# Scan network and return active hosts
|
37
|
+
def scan(cidr, timeout = 1, max_threads = 32, verbose = false)
|
38
|
+
@verbose = verbose
|
39
|
+
@timeout = timeout
|
40
|
+
|
41
|
+
# Clear previous scan results
|
42
|
+
@hosts = []
|
43
|
+
|
44
|
+
# Get the range of IP addresses to scan
|
45
|
+
range = IPAddr.new(cidr).to_range
|
46
|
+
|
47
|
+
# Create a queue of IPs to scan
|
48
|
+
queue = Queue.new
|
49
|
+
range.each { |ip| queue << ip.to_s }
|
50
|
+
|
51
|
+
total_ips = queue.size
|
52
|
+
completed = 0
|
53
|
+
|
54
|
+
# Pre-populate ARP table to improve MAC address resolution
|
55
|
+
update_arp_table(cidr, max_threads)
|
56
|
+
|
57
|
+
# Create worker threads to process the queue
|
58
|
+
threads = Array.new([max_threads, total_ips].min) do
|
59
|
+
Thread.new do
|
60
|
+
while (ip = begin
|
61
|
+
queue.pop(true)
|
62
|
+
rescue ThreadError
|
63
|
+
nil
|
64
|
+
end)
|
65
|
+
scan_host(ip)
|
66
|
+
@mutex.synchronize do
|
67
|
+
completed += 1
|
68
|
+
if total_ips < 100 || (completed % 10).zero? || completed == total_ips
|
69
|
+
print_progress(completed,
|
70
|
+
total_ips)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
begin
|
78
|
+
threads.each(&:join)
|
79
|
+
print_progress(total_ips, total_ips)
|
80
|
+
puts "\nScan complete. Found #{@hosts.size} active hosts."
|
81
|
+
@verbose ? @hosts : @hosts.map { |h| h[:ip] }
|
82
|
+
rescue Interrupt
|
83
|
+
puts "\nScan interrupted. Returning partial results..."
|
84
|
+
@verbose ? @hosts : @hosts.map { |h| h[:ip] }
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def print_progress(completed, total)
|
91
|
+
percent = (completed.to_f / total * 100).round(1)
|
92
|
+
print "\rScanning network: #{percent}% complete (#{completed}/#{total})"
|
93
|
+
end
|
94
|
+
|
95
|
+
def update_arp_table(cidr, max_threads = 10)
|
96
|
+
# Use fast ping method to update ARP table
|
97
|
+
range = IPAddr.new(cidr).to_range
|
98
|
+
queue = Queue.new
|
99
|
+
range.each { |ip| queue << ip.to_s }
|
100
|
+
|
101
|
+
total = queue.size
|
102
|
+
processed = 0
|
103
|
+
|
104
|
+
threads = Array.new([max_threads, total].min) do
|
105
|
+
Thread.new do
|
106
|
+
while (ip = begin
|
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
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Wait for ping operations to complete
|
120
|
+
threads.each(&:join)
|
121
|
+
sleep 1 # Give the system time to update ARP table
|
122
|
+
end
|
123
|
+
|
124
|
+
def scan_host(ip)
|
125
|
+
# Skip special addresses to save time
|
126
|
+
return if ip.end_with?(".0") && !ip.end_with?(".0.0") # Skip network addresses except 0.0.0.0
|
127
|
+
|
128
|
+
# Use multiple methods to detect if a host is alive
|
129
|
+
is_active = false
|
130
|
+
detection_method = nil
|
131
|
+
response_time = nil
|
132
|
+
start_time = Time.now
|
133
|
+
open_ports = []
|
134
|
+
|
135
|
+
# Method 1: Try TCP port scan (most reliable)
|
136
|
+
tcp_result = tcp_port_scan(ip, QUICK_CHECK_PORTS)
|
137
|
+
if tcp_result[:active]
|
138
|
+
is_active = true
|
139
|
+
detection_method = "TCP"
|
140
|
+
open_ports = tcp_result[:open_ports]
|
141
|
+
end
|
142
|
+
|
143
|
+
# Method 2: Try ICMP ping if TCP didn't work
|
144
|
+
unless is_active
|
145
|
+
ping_result = ping_check(ip)
|
146
|
+
if ping_result
|
147
|
+
is_active = true
|
148
|
+
detection_method = "ICMP"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Method 3: If host is a common network device (e.g., router), check with UDP
|
153
|
+
if !is_active && (ip.end_with?(".1") || ip.end_with?(".254") || ip.end_with?(".255"))
|
154
|
+
udp_result = udp_check(ip)
|
155
|
+
if udp_result
|
156
|
+
is_active = true
|
157
|
+
detection_method = "UDP"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Method 4: ARP Check - if we have a MAC, the host is likely active
|
162
|
+
unless is_active
|
163
|
+
mac_address = get_mac_address(ip)
|
164
|
+
if mac_address && mac_address != "(incomplete)"
|
165
|
+
is_active = true
|
166
|
+
detection_method = "ARP"
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
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
|
+
response_time = ((Time.now - start_time) * 1000).round(2) if is_active
|
178
|
+
|
179
|
+
return unless is_active
|
180
|
+
|
181
|
+
# For active hosts, collect more information if in verbose mode
|
182
|
+
host_info = {
|
183
|
+
ip: ip,
|
184
|
+
mac: get_mac_address(ip),
|
185
|
+
response_time: response_time,
|
186
|
+
detection_method: detection_method
|
187
|
+
}
|
188
|
+
|
189
|
+
if @verbose
|
190
|
+
# For verbose mode, try to resolve hostname
|
191
|
+
begin
|
192
|
+
Timeout.timeout(1) do
|
193
|
+
host_info[:hostname] = Resolv.getname(ip)
|
194
|
+
end
|
195
|
+
rescue Resolv::ResolvError, Timeout::Error
|
196
|
+
host_info[:hostname] = "Unknown"
|
197
|
+
end
|
198
|
+
|
199
|
+
# For verbose mode, scan more ports if TCP detection method was successful
|
200
|
+
if detection_method == "TCP"
|
201
|
+
extra_ports = tcp_port_scan(ip, COMMON_PORTS.keys - QUICK_CHECK_PORTS)[:open_ports]
|
202
|
+
open_ports += extra_ports
|
203
|
+
end
|
204
|
+
|
205
|
+
host_info[:ports] = open_ports.map { |port| [port, COMMON_PORTS[port] || "Unknown"] }.to_h
|
206
|
+
end
|
207
|
+
|
208
|
+
@mutex.synchronize { @hosts << host_info }
|
209
|
+
rescue StandardError => e
|
210
|
+
puts "\nError scanning host #{ip}: #{e.message}" if $DEBUG
|
211
|
+
end
|
212
|
+
|
213
|
+
def tcp_port_scan(ip, ports)
|
214
|
+
open_ports = []
|
215
|
+
is_active = false
|
216
|
+
|
217
|
+
ports.each do |port|
|
218
|
+
Timeout.timeout(@timeout) do
|
219
|
+
socket = TCPSocket.new(ip, port)
|
220
|
+
is_active = true
|
221
|
+
open_ports << port
|
222
|
+
socket.close
|
223
|
+
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
|
+
end
|
230
|
+
|
231
|
+
{ active: is_active, open_ports: open_ports }
|
232
|
+
end
|
233
|
+
|
234
|
+
def ping_check(ip)
|
235
|
+
cmd = case RbConfig::CONFIG["host_os"]
|
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
|
+
|
246
|
+
system("#{cmd} > /dev/null 2>&1")
|
247
|
+
$CHILD_STATUS.exitstatus.zero?
|
248
|
+
end
|
249
|
+
|
250
|
+
def udp_check(ip)
|
251
|
+
common_udp_ports = [53, 67, 68, 123, 137, 138, 1900, 5353]
|
252
|
+
|
253
|
+
common_udp_ports.each do |port|
|
254
|
+
Timeout.timeout(0.5) do
|
255
|
+
socket = UDPSocket.new
|
256
|
+
socket.connect(ip, port)
|
257
|
+
socket.send("PING", 0)
|
258
|
+
socket.close
|
259
|
+
return true
|
260
|
+
end
|
261
|
+
rescue Errno::ECONNREFUSED
|
262
|
+
return true # Connection refused means host is up
|
263
|
+
rescue StandardError
|
264
|
+
# Try next port
|
265
|
+
end
|
266
|
+
false
|
267
|
+
end
|
268
|
+
|
269
|
+
def get_mac_address(ip)
|
270
|
+
return "ff:ff:ff:ff:ff:ff" if ip.end_with?(".255") # Special case for broadcast
|
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
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
data/lib/lanet/sender.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "socket"
|
4
|
+
|
5
|
+
module Lanet
|
6
|
+
class Sender
|
7
|
+
def initialize(port)
|
8
|
+
@port = port
|
9
|
+
@socket = UDPSocket.new
|
10
|
+
@socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_BROADCAST, true)
|
11
|
+
end
|
12
|
+
|
13
|
+
def send_to(target_ip, message)
|
14
|
+
@socket.send(message, 0, target_ip, @port)
|
15
|
+
end
|
16
|
+
|
17
|
+
def broadcast(message)
|
18
|
+
@socket.send(message, 0, "255.255.255.255", @port)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/lanet.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "lanet/version"
|
4
|
+
require "lanet/sender"
|
5
|
+
require "lanet/receiver"
|
6
|
+
require "lanet/scanner"
|
7
|
+
require "lanet/encryptor"
|
8
|
+
require "lanet/cli"
|
9
|
+
require "lanet/ping"
|
10
|
+
|
11
|
+
module Lanet
|
12
|
+
class Error < StandardError; end
|
13
|
+
|
14
|
+
# Default port used for communication
|
15
|
+
DEFAULT_PORT = 5000
|
16
|
+
|
17
|
+
# Creates a new sender instance
|
18
|
+
def self.sender(port = DEFAULT_PORT)
|
19
|
+
Sender.new(port)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Creates a new receiver instance
|
23
|
+
def self.receiver(port = DEFAULT_PORT)
|
24
|
+
Receiver.new(port)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Creates a new scanner instance
|
28
|
+
def self.scanner
|
29
|
+
Scanner.new
|
30
|
+
end
|
31
|
+
|
32
|
+
# Helper to encrypt a message
|
33
|
+
def self.encrypt(message, key)
|
34
|
+
Encryptor.prepare_message(message, key)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Helper to decrypt a message
|
38
|
+
def self.decrypt(data, key)
|
39
|
+
Encryptor.process_message(data, key)
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.pinger(timeout: 1, count: 3)
|
43
|
+
Ping.new(timeout: timeout, count: count)
|
44
|
+
end
|
45
|
+
end
|
data/sig/lanet.rbs
ADDED