lanet 0.1.0 → 0.2.1

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.
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
- # Pre-populate ARP table to improve MAC address resolution
55
- update_arp_table(cidr, max_threads)
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
- while (ip = begin
61
- queue.pop(true)
62
- rescue ThreadError
63
- nil
64
- end)
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 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 }
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
- total = queue.size
102
- processed = 0
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
- 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
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
- end
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
- # Wait for ping operations to complete
120
- threads.each(&:join)
121
- sleep 1 # Give the system time to update ARP table
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
- # 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
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
- # Method 1: Try TCP port scan (most reliable)
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
- # 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
159
+ # ICMP ping
160
+ if !is_active && ping_check(ip)
161
+ is_active = true
162
+ detection_method = "ICMP"
150
163
  end
151
164
 
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
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
- # Method 4: ARP Check - if we have a MAC, the host is likely active
171
+ # ARP check
162
172
  unless is_active
163
- mac_address = get_mac_address(ip)
164
- if mac_address && mac_address != "(incomplete)"
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
- # 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
- }
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
- # 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"
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
- ports.each do |port|
218
- Timeout.timeout(@timeout) do
219
- socket = TCPSocket.new(ip, port)
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 = 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
-
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 # Connection refused means host is up
248
+ return true
263
249
  rescue StandardError
264
- # Try next port
250
+ next
265
251
  end
266
252
  false
267
253
  end
268
254
 
269
255
  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
256
+ @mutex.synchronize { @arp_cache[ip] || "(incomplete)" }
291
257
  end
292
258
  end
293
259
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "base64"
5
+
6
+ module Lanet
7
+ class Signer
8
+ # Error class for signature failures
9
+ class Error < StandardError; end
10
+
11
+ # Signs a message using the provided private key
12
+ # @param message [String] the message to sign
13
+ # @param private_key_pem [String] the PEM-encoded private key
14
+ # @return [String] Base64-encoded signature
15
+ def self.sign(message, private_key_pem)
16
+ private_key = OpenSSL::PKey::RSA.new(private_key_pem)
17
+ signature = private_key.sign(OpenSSL::Digest.new("SHA256"), message.to_s)
18
+ Base64.strict_encode64(signature)
19
+ rescue StandardError => e
20
+ raise Error, "Signing failed: #{e.message}"
21
+ end
22
+
23
+ # Verifies a signature using the provided public key
24
+ # @param message [String] the original message
25
+ # @param signature_base64 [String] the Base64-encoded signature
26
+ # @param public_key_pem [String] the PEM-encoded public key
27
+ # @return [Boolean] true if signature is valid
28
+ def self.verify(message, signature_base64, public_key_pem)
29
+ public_key = OpenSSL::PKey::RSA.new(public_key_pem)
30
+ signature = Base64.strict_decode64(signature_base64)
31
+ public_key.verify(OpenSSL::Digest.new("SHA256"), signature, message.to_s)
32
+ rescue StandardError => e
33
+ raise Error, "Verification failed: #{e.message}"
34
+ end
35
+
36
+ # Generates a new RSA key pair
37
+ # @param bits [Integer] key size in bits
38
+ # @return [Hash] containing :private_key and :public_key as PEM strings
39
+ def self.generate_key_pair(bits = 2048)
40
+ key = OpenSSL::PKey::RSA.new(bits)
41
+ {
42
+ private_key: key.to_pem,
43
+ public_key: key.public_key.to_pem
44
+ }
45
+ end
46
+ end
47
+ end
data/lib/lanet/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lanet
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lanet
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Davide Santangelo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-03-06 00:00:00.000000000 Z
11
+ date: 2025-03-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -97,6 +97,7 @@ files:
97
97
  - lib/lanet/receiver.rb
98
98
  - lib/lanet/scanner.rb
99
99
  - lib/lanet/sender.rb
100
+ - lib/lanet/signer.rb
100
101
  - lib/lanet/version.rb
101
102
  - sig/lanet.rbs
102
103
  homepage: https://github.com/davidesantangelo/lanet