lanet 0.3.0 → 0.4.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.
data/lib/lanet/cli.rb CHANGED
@@ -278,6 +278,94 @@ module Lanet
278
278
  puts "Lanet version #{Lanet::VERSION}"
279
279
  end
280
280
 
281
+ desc "mesh start", "Start a mesh network node"
282
+ option :port, type: :numeric, default: 5050, desc: "Port for mesh communication"
283
+ option :max_hops, type: :numeric, default: 10, desc: "Maximum number of hops for message routing"
284
+ def mesh_start
285
+ mesh = Lanet::Mesh.new(options[:port], options[:max_hops])
286
+
287
+ puts "Starting mesh network node with ID: #{mesh.node_id}"
288
+ puts "Listening on port: #{options[:port]}"
289
+ puts "Press Ctrl+C to stop"
290
+
291
+ mesh.start
292
+
293
+ # Keep the process running
294
+ begin
295
+ loop do
296
+ sleep 1
297
+ end
298
+ rescue Interrupt
299
+ puts "\nStopping mesh network node..."
300
+ mesh.stop
301
+ end
302
+ end
303
+
304
+ desc "mesh send", "Send a message through the mesh network"
305
+ option :target, type: :string, required: true, desc: "Target node ID"
306
+ option :message, type: :string, required: true, desc: "Message to send"
307
+ option :key, type: :string, desc: "Encryption key (optional)"
308
+ option :private_key_file, type: :string, desc: "Path to private key file for signing (optional)"
309
+ option :port, type: :numeric, default: 5050, desc: "Port for mesh communication"
310
+ def mesh_send
311
+ mesh = Lanet::Mesh.new(options[:port])
312
+
313
+ private_key = nil
314
+ if options[:private_key_file]
315
+ begin
316
+ private_key = File.read(options[:private_key_file])
317
+ puts "Message will be digitally signed"
318
+ rescue StandardError => e
319
+ puts "Error reading private key file: #{e.message}"
320
+ return
321
+ end
322
+ end
323
+
324
+ # Initialize the mesh network
325
+ mesh.start
326
+
327
+ begin
328
+ message_id = mesh.send_message(
329
+ options[:target],
330
+ options[:message],
331
+ options[:key],
332
+ private_key
333
+ )
334
+
335
+ puts "Message sent through mesh network"
336
+ puts "Message ID: #{message_id}"
337
+ rescue Lanet::Mesh::Error => e
338
+ puts "Error sending mesh message: #{e.message}"
339
+ ensure
340
+ mesh.stop
341
+ end
342
+ end
343
+
344
+ desc "mesh info", "Display information about the mesh network"
345
+ option :port, type: :numeric, default: 5050, desc: "Port for mesh communication"
346
+ def mesh_info
347
+ mesh = Lanet::Mesh.new(options[:port])
348
+
349
+ # Load state if available
350
+ mesh.start
351
+
352
+ puts "Mesh Node ID: #{mesh.node_id}"
353
+ puts "\nConnected nodes:"
354
+
355
+ if mesh.connections.empty?
356
+ puts " No direct connections"
357
+ else
358
+ mesh.connections.each do |node_id, info|
359
+ last_seen = Time.now.to_i - info[:last_seen]
360
+ puts " #{node_id} (#{info[:ip]}, last seen #{last_seen}s ago)"
361
+ end
362
+ end
363
+
364
+ puts "\nMessage cache: #{mesh.message_cache.size} messages"
365
+
366
+ mesh.stop
367
+ end
368
+
281
369
  private
282
370
 
283
371
  def display_ping_details(host, result)
@@ -122,7 +122,10 @@ module Lanet
122
122
  end
123
123
 
124
124
  ### Receive File Method
125
- def receive_file(output_dir, encryption_key = nil, public_key = nil, progress_callback = nil)
125
+ def receive_file(output_dir, encryption_key = nil, public_key = nil, progress_callback = nil, &block)
126
+ # Use the block parameter if provided and progress_callback is nil
127
+ progress_callback = block if block_given? && progress_callback.nil?
128
+
126
129
  FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)
127
130
  receiver = UDPSocket.new
128
131
  receiver.bind("0.0.0.0", @port)
@@ -131,6 +134,10 @@ module Lanet
131
134
  begin
132
135
  loop do
133
136
  data, addr = receiver.recvfrom(65_536) # Large buffer for chunks
137
+
138
+ # Skip if we received nil data or address
139
+ next if addr.nil? || data.nil?
140
+
134
141
  sender_ip = addr[3]
135
142
  result = Lanet::Encryptor.process_message(data, encryption_key, public_key)
136
143
  next unless result[:content]&.length&.> 2
data/lib/lanet/mesh.rb ADDED
@@ -0,0 +1,493 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "set"
5
+ require "json"
6
+ require "fileutils"
7
+ require "logger"
8
+
9
+ module Lanet
10
+ class Mesh
11
+ class Error < StandardError; end
12
+
13
+ DEFAULT_TTL = 10
14
+ DEFAULT_MESH_PORT = 5050
15
+ DEFAULT_DISCOVERY_INTERVAL = 60 # seconds
16
+ DEFAULT_MESSAGE_EXPIRY = 600 # 10 minutes
17
+ DEFAULT_CONNECTION_TIMEOUT = 180 # 3x discovery interval
18
+
19
+ MESSAGE_TYPES = {
20
+ discovery: "DISCOVERY",
21
+ discovery_response: "DISCOVERY_RESPONSE",
22
+ message: "MESSAGE",
23
+ route: "ROUTE_INFO"
24
+ }.freeze
25
+
26
+ attr_reader :node_id, :connections, :message_cache, :logger
27
+
28
+ def initialize(port = DEFAULT_MESH_PORT, max_hops = DEFAULT_TTL,
29
+ discovery_interval = DEFAULT_DISCOVERY_INTERVAL,
30
+ message_expiry = DEFAULT_MESSAGE_EXPIRY,
31
+ logger = nil)
32
+ @port = port
33
+ @max_hops = max_hops
34
+ @discovery_interval = discovery_interval
35
+ @message_expiry = message_expiry
36
+ @connection_timeout = @discovery_interval * 3
37
+ @node_id = SecureRandom.uuid
38
+ @connections = {}
39
+ @routes = {}
40
+ @message_cache = Set.new
41
+ @message_timestamps = {}
42
+ @processed_message_count = 0
43
+ @mutex = Mutex.new
44
+ @logger = logger || Logger.new($stdout)
45
+ @logger.level = Logger::INFO
46
+
47
+ # Setup communication channels
48
+ @sender = Lanet::Sender.new(port)
49
+ @receiver = nil
50
+
51
+ # For handling data storage
52
+ @storage_path = File.join(Dir.home, ".lanet", "mesh")
53
+ FileUtils.mkdir_p(@storage_path) unless Dir.exist?(@storage_path)
54
+ end
55
+
56
+ def start
57
+ return if @running
58
+
59
+ @running = true
60
+ start_receiver
61
+ start_discovery_service
62
+ start_monitoring
63
+ start_cache_pruning
64
+
65
+ @logger.info("Mesh node #{@node_id} started on port #{@port}")
66
+ load_state
67
+ end
68
+
69
+ def stop
70
+ return unless @running
71
+
72
+ @logger.info("Stopping mesh node #{@node_id}")
73
+ @running = false
74
+
75
+ [@discovery_thread, @receiver_thread, @monitor_thread, @cache_pruning_thread].each do |thread|
76
+ thread&.exit
77
+ end
78
+
79
+ save_state
80
+ @logger.info("Mesh node stopped")
81
+ end
82
+
83
+ def send_message(target_id, message, encryption_key = nil, private_key = nil)
84
+ unless @connections.key?(target_id) || @routes.key?(target_id)
85
+ @logger.debug("No known route to #{target_id}, performing discovery")
86
+ perform_discovery
87
+
88
+ # Replace sleep with timeout-based approach
89
+ discovery_timeout = Time.now.to_i + 2
90
+ until @connections.key?(target_id) || @routes.key?(target_id) || Time.now.to_i > discovery_timeout
91
+ sleep 0.1 # Short sleep to avoid CPU spinning
92
+ end
93
+
94
+ unless @connections.key?(target_id) || @routes.key?(target_id)
95
+ @logger.error("No route to node #{target_id}")
96
+ raise Error, "No route to node #{target_id}"
97
+ end
98
+ end
99
+
100
+ message_id = SecureRandom.uuid
101
+
102
+ # Prevent message loops by adding to cache
103
+ @mutex.synchronize do
104
+ @message_cache.add(message_id)
105
+ @message_timestamps[message_id] = Time.now.to_i
106
+ end
107
+
108
+ # Prepare the mesh message container
109
+ encrypted_content = encryption_key ? Encryptor.prepare_message(message, encryption_key, private_key) : message
110
+ mesh_message = build_mesh_message(
111
+ MESSAGE_TYPES[:message],
112
+ id: message_id,
113
+ target: target_id,
114
+ content: encrypted_content,
115
+ hops: 0
116
+ )
117
+
118
+ # Direct connection
119
+ if @connections.key?(target_id)
120
+ @logger.debug("Sending direct message to #{target_id}")
121
+ @sender.send_to(@connections[target_id][:ip], mesh_message.to_json)
122
+ return message_id
123
+ end
124
+
125
+ # Route through intermediate node
126
+ if @routes.key?(target_id)
127
+ next_hop = @routes[target_id][:next_hop]
128
+ if @connections.key?(next_hop)
129
+ @logger.debug("Sending message to #{target_id} via #{next_hop}")
130
+ @sender.send_to(@connections[next_hop][:ip], mesh_message.to_json)
131
+ return message_id
132
+ end
133
+ end
134
+
135
+ # Broadcast as last resort
136
+ @logger.debug("Broadcasting message to find route to #{target_id}")
137
+ broadcast_mesh_message(mesh_message)
138
+ message_id
139
+ end
140
+
141
+ def broadcast_mesh_message(mesh_message)
142
+ @connections.each do |id, info|
143
+ next if id == mesh_message[:origin]
144
+
145
+ @sender.send_to(info[:ip], mesh_message.to_json)
146
+ end
147
+ end
148
+
149
+ def healthy?
150
+ @running &&
151
+ @receiver_thread&.alive? &&
152
+ @discovery_thread&.alive? &&
153
+ @monitor_thread&.alive? &&
154
+ @cache_pruning_thread&.alive?
155
+ end
156
+
157
+ def stats
158
+ {
159
+ node_id: @node_id,
160
+ connections: @connections.size,
161
+ routes: @routes.size,
162
+ message_cache_size: @message_cache.size,
163
+ processed_messages: @processed_message_count
164
+ }
165
+ end
166
+
167
+ private
168
+
169
+ def build_mesh_message(type, extra_fields = {})
170
+ {
171
+ type: type,
172
+ id: extra_fields[:id] || SecureRandom.uuid,
173
+ origin: @node_id,
174
+ timestamp: Time.now.to_i
175
+ }.merge(extra_fields)
176
+ end
177
+
178
+ def start_receiver
179
+ @receiver = Lanet::Receiver.new(@port)
180
+ @receiver_thread = Thread.new do
181
+ @logger.info("Starting receiver on port #{@port}")
182
+ @receiver.listen do |data, sender_ip|
183
+ handle_incoming_data(data, sender_ip)
184
+ rescue StandardError => e
185
+ @logger.error("Error handling mesh message: #{e.message}")
186
+ @logger.error(e.backtrace.join("\n")) if @logger.debug?
187
+ end
188
+ end
189
+ end
190
+
191
+ def start_monitoring
192
+ @monitor_thread = Thread.new do
193
+ @logger.info("Starting thread monitor")
194
+
195
+ while @running
196
+ unless @receiver_thread&.alive?
197
+ @logger.warn("Receiver thread died, restarting...")
198
+ start_receiver
199
+ end
200
+
201
+ unless @discovery_thread&.alive?
202
+ @logger.warn("Discovery thread died, restarting...")
203
+ start_discovery_service
204
+ end
205
+
206
+ unless @cache_pruning_thread&.alive?
207
+ @logger.warn("Cache pruning thread died, restarting...")
208
+ start_cache_pruning
209
+ end
210
+
211
+ sleep 30
212
+ end
213
+ end
214
+ end
215
+
216
+ def start_cache_pruning
217
+ @cache_pruning_thread = Thread.new do
218
+ @logger.info("Starting cache pruning service")
219
+
220
+ while @running
221
+ prune_message_cache
222
+ sleep @discovery_interval
223
+ end
224
+ end
225
+ end
226
+
227
+ def handle_incoming_data(data, sender_ip)
228
+ message = JSON.parse(data, symbolize_names: true)
229
+
230
+ # Track metrics
231
+ @mutex.synchronize { @processed_message_count += 1 }
232
+
233
+ # Discard messages older than configured expiry time
234
+ if message[:timestamp] < Time.now.to_i - @message_expiry
235
+ @logger.debug("Discarding expired message: #{message[:id]}")
236
+ return
237
+ end
238
+
239
+ # Skip messages we've already processed
240
+ if @message_cache.include?(message[:id])
241
+ @logger.debug("Skipping already processed message: #{message[:id]}")
242
+ return
243
+ end
244
+
245
+ # Add to cache to prevent loops
246
+ @mutex.synchronize do
247
+ @message_cache.add(message[:id])
248
+ @message_timestamps[message[:id]] = Time.now.to_i
249
+ end
250
+
251
+ # Dispatch to appropriate handler
252
+ case message[:type]
253
+ when MESSAGE_TYPES[:discovery]
254
+ handle_discovery(message, sender_ip)
255
+ when MESSAGE_TYPES[:discovery_response]
256
+ handle_discovery_response(message, sender_ip)
257
+ when MESSAGE_TYPES[:message]
258
+ handle_message(message, sender_ip)
259
+ when MESSAGE_TYPES[:route]
260
+ handle_route_info(message, sender_ip)
261
+ else
262
+ @logger.warn("Unknown message type: #{message[:type]}")
263
+ end
264
+ rescue JSON::ParserError => e
265
+ @logger.debug("Ignoring non-JSON message: #{e.message[0..100]}")
266
+ rescue StandardError => e
267
+ @logger.error("Error processing message: #{e.message}")
268
+ @logger.error(e.backtrace.join("\n")) if @logger.debug?
269
+ end
270
+
271
+ def handle_discovery(message, sender_ip)
272
+ # Add the sender to our connections
273
+ @mutex.synchronize do
274
+ @connections[message[:origin]] = {
275
+ ip: sender_ip,
276
+ last_seen: Time.now.to_i
277
+ }
278
+ end
279
+
280
+ @logger.debug("Added connection to #{message[:origin]} at #{sender_ip}")
281
+
282
+ # Send a discovery response
283
+ response = build_mesh_message(
284
+ MESSAGE_TYPES[:discovery_response],
285
+ target: message[:origin],
286
+ known_nodes: @connections.keys
287
+ )
288
+
289
+ @sender.send_to(sender_ip, response.to_json)
290
+
291
+ # Share route information
292
+ share_routes_with(message[:origin])
293
+ end
294
+
295
+ def handle_discovery_response(message, sender_ip)
296
+ @mutex.synchronize do
297
+ # Update our connection to the sender
298
+ @connections[message[:origin]] = {
299
+ ip: sender_ip,
300
+ last_seen: Time.now.to_i
301
+ }
302
+
303
+ # Add routes for known nodes with correct distance
304
+ message[:known_nodes].each do |node_id|
305
+ next if node_id == @node_id || @connections.key?(node_id)
306
+
307
+ @routes[node_id] = {
308
+ next_hop: message[:origin],
309
+ distance: 2, # Corrected to reflect hops through responder
310
+ last_updated: Time.now.to_i
311
+ }
312
+
313
+ @logger.debug("Added route to #{node_id} via #{message[:origin]} (distance: 2)")
314
+ end
315
+ end
316
+ end
317
+
318
+ def handle_message(message, _sender_ip)
319
+ # If message is for us, process it
320
+ if message[:target] == @node_id
321
+ @logger.info("Received mesh message from #{message[:origin]}: #{message[:content]}")
322
+ return
323
+ end
324
+
325
+ # Otherwise, forward if we haven't exceeded max hops
326
+ if message[:hops] >= @max_hops
327
+ @logger.debug("Message exceeded max hops (#{@max_hops}), dropping")
328
+ return
329
+ end
330
+
331
+ message[:hops] += 1
332
+ @logger.debug("Forwarding message from #{message[:origin]} to #{message[:target]} (hop #{message[:hops]})")
333
+
334
+ if @connections.key?(message[:target])
335
+ @sender.send_to(@connections[message[:target]][:ip], message.to_json)
336
+ elsif @routes.key?(message[:target])
337
+ next_hop = @routes[message[:target]][:next_hop]
338
+ if @connections.key?(next_hop)
339
+ @sender.send_to(@connections[next_hop][:ip], message.to_json)
340
+ else
341
+ @logger.warn("Lost connection to next hop #{next_hop}, broadcasting")
342
+ broadcast_mesh_message(message)
343
+ end
344
+ else
345
+ broadcast_mesh_message(message)
346
+ end
347
+ end
348
+
349
+ def handle_route_info(message, _sender_ip)
350
+ @mutex.synchronize do
351
+ message[:routes].each do |node_id, route_info|
352
+ next if node_id.to_s == @node_id || @connections.key?(node_id.to_s)
353
+
354
+ distance = route_info[:distance] + 1
355
+
356
+ # Only update if we don't have a route or the new route is better
357
+ next unless !@routes.key?(node_id.to_s) || @routes[node_id.to_s][:distance] > distance
358
+
359
+ @routes[node_id.to_s] = {
360
+ next_hop: message[:origin],
361
+ distance: distance,
362
+ last_updated: Time.now.to_i
363
+ }
364
+
365
+ @logger.debug("Updated route to #{node_id} via #{message[:origin]} (distance: #{distance})")
366
+ end
367
+ end
368
+ end
369
+
370
+ def start_discovery_service
371
+ @discovery_thread = Thread.new do
372
+ @logger.info("Starting discovery service")
373
+
374
+ while @running
375
+ perform_discovery
376
+ prune_old_connections
377
+ sleep @discovery_interval
378
+ end
379
+ end
380
+ end
381
+
382
+ def perform_discovery
383
+ @logger.debug("Performing network discovery")
384
+ discovery_message = build_mesh_message(MESSAGE_TYPES[:discovery])
385
+ @sender.broadcast(discovery_message.to_json)
386
+ end
387
+
388
+ def share_routes_with(target_node_id)
389
+ return unless @connections.key?(target_node_id)
390
+
391
+ @logger.debug("Sharing route information with #{target_node_id}")
392
+ route_message = build_mesh_message(MESSAGE_TYPES[:route], routes: @routes)
393
+ @sender.send_to(@connections[target_node_id][:ip], route_message.to_json)
394
+ end
395
+
396
+ def prune_old_connections
397
+ now = Time.now.to_i
398
+ pruned_connections = 0
399
+ pruned_routes = 0
400
+
401
+ @mutex.synchronize do
402
+ # Remove old connections
403
+ @connections.each do |id, info|
404
+ next unless now - info[:last_seen] > @connection_timeout
405
+
406
+ @connections.delete(id)
407
+ pruned_connections += 1
408
+ @logger.debug("Pruned stale connection to #{id}")
409
+ end
410
+
411
+ # Remove old routes
412
+ @routes.each do |id, info|
413
+ next unless now - info[:last_updated] > @connection_timeout
414
+
415
+ @routes.delete(id)
416
+ pruned_routes += 1
417
+ @logger.debug("Pruned stale route to #{id}")
418
+ end
419
+ end
420
+
421
+ return unless pruned_connections.positive? || pruned_routes.positive?
422
+
423
+ @logger.info("Pruned #{pruned_connections} connections and #{pruned_routes} routes")
424
+ end
425
+
426
+ def prune_message_cache
427
+ now = Time.now.to_i
428
+ pruned_count = 0
429
+
430
+ @mutex.synchronize do
431
+ @message_timestamps.each do |msg_id, timestamp|
432
+ next unless now - timestamp > @message_expiry
433
+
434
+ @message_cache.delete(msg_id)
435
+ @message_timestamps.delete(msg_id)
436
+ pruned_count += 1
437
+ end
438
+ end
439
+
440
+ @logger.info("Pruned #{pruned_count} messages from cache") if pruned_count.positive?
441
+ end
442
+
443
+ def save_state
444
+ state = {
445
+ node_id: @node_id,
446
+ connections: @connections,
447
+ routes: @routes,
448
+ timestamp: Time.now.to_i
449
+ }
450
+
451
+ begin
452
+ File.write(File.join(@storage_path, "state.json"), state.to_json)
453
+ @logger.info("Mesh state saved successfully")
454
+ rescue StandardError => e
455
+ @logger.error("Failed to save mesh state: #{e.message}")
456
+ end
457
+ end
458
+
459
+ def load_state
460
+ state_file = File.join(@storage_path, "state.json")
461
+ return unless File.exist?(state_file)
462
+
463
+ begin
464
+ @logger.info("Loading mesh state from #{state_file}")
465
+ state = JSON.parse(File.read(state_file), symbolize_names: true)
466
+ validate_state(state)
467
+
468
+ @node_id = state[:node_id]
469
+ @connections = state[:connections]
470
+ @routes = state[:routes]
471
+
472
+ @logger.info("Mesh state loaded successfully, node ID: #{@node_id}")
473
+ rescue JSON::ParserError => e
474
+ @logger.error("Error parsing mesh state file: #{e.message}")
475
+ rescue KeyError => e
476
+ @logger.error("Invalid mesh state structure: #{e.message}")
477
+ rescue StandardError => e
478
+ @logger.error("Error loading mesh state: #{e.message}")
479
+ end
480
+ end
481
+
482
+ def validate_state(state)
483
+ %i[node_id connections routes timestamp].each do |key|
484
+ raise KeyError, "Missing required key: #{key}" unless state.key?(key)
485
+ end
486
+
487
+ # Verify timestamp is reasonable
488
+ return unless state[:timestamp] < Time.now.to_i - 30 * 24 * 60 * 60
489
+
490
+ @logger.warn("State file is more than 30 days old")
491
+ end
492
+ end
493
+ 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.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/lanet.rb CHANGED
@@ -8,6 +8,7 @@ require "lanet/encryptor"
8
8
  require "lanet/cli"
9
9
  require "lanet/ping"
10
10
  require "lanet/file_transfer"
11
+ require "lanet/mesh"
11
12
 
12
13
  module Lanet
13
14
  class Error < StandardError; end
@@ -50,5 +51,10 @@ module Lanet
50
51
  def file_transfer(port = 5001)
51
52
  FileTransfer.new(port)
52
53
  end
54
+
55
+ # Create a new mesh network instance
56
+ def mesh_network(port = 5050, max_hops = 10)
57
+ Mesh.new(port, max_hops)
58
+ end
53
59
  end
54
60
  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.3.0
4
+ version: 0.4.0
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-08 00:00:00.000000000 Z
11
+ date: 2025-03-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -94,6 +94,7 @@ files:
94
94
  - lib/lanet/cli.rb
95
95
  - lib/lanet/encryptor.rb
96
96
  - lib/lanet/file_transfer.rb
97
+ - lib/lanet/mesh.rb
97
98
  - lib/lanet/ping.rb
98
99
  - lib/lanet/receiver.rb
99
100
  - lib/lanet/scanner.rb