lanet 0.2.1 → 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/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.2.1"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/lanet.rb CHANGED
@@ -7,6 +7,8 @@ require "lanet/scanner"
7
7
  require "lanet/encryptor"
8
8
  require "lanet/cli"
9
9
  require "lanet/ping"
10
+ require "lanet/file_transfer"
11
+ require "lanet/mesh"
10
12
 
11
13
  module Lanet
12
14
  class Error < StandardError; end
@@ -14,32 +16,45 @@ module Lanet
14
16
  # Default port used for communication
15
17
  DEFAULT_PORT = 5000
16
18
 
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)
19
+ class << self
20
+ # Creates a new sender instance
21
+ def sender(port = DEFAULT_PORT)
22
+ Sender.new(port)
23
+ end
24
+
25
+ # Creates a new receiver instance
26
+ def receiver(port = DEFAULT_PORT)
27
+ Receiver.new(port)
28
+ end
29
+
30
+ # Creates a new scanner instance
31
+ def scanner
32
+ Scanner.new
33
+ end
34
+
35
+ # Helper to encrypt a message
36
+ def encrypt(message, key)
37
+ Encryptor.prepare_message(message, key)
38
+ end
39
+
40
+ # Helper to decrypt a message
41
+ def decrypt(data, key)
42
+ result = Encryptor.process_message(data, key)
43
+ result[:content]
44
+ end
45
+
46
+ def pinger(timeout: 1, count: 3)
47
+ Ping.new(timeout: timeout, count: count)
48
+ end
49
+
50
+ # Add file transfer functionality
51
+ def file_transfer(port = 5001)
52
+ FileTransfer.new(port)
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
44
59
  end
45
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.2.1
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-07 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
@@ -93,6 +93,8 @@ files:
93
93
  - lib/lanet.rb
94
94
  - lib/lanet/cli.rb
95
95
  - lib/lanet/encryptor.rb
96
+ - lib/lanet/file_transfer.rb
97
+ - lib/lanet/mesh.rb
96
98
  - lib/lanet/ping.rb
97
99
  - lib/lanet/receiver.rb
98
100
  - lib/lanet/scanner.rb