lanet 0.4.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a1e22dfda5b58811a99f1d763c3ff32d84b2e8e291ab07b8f1ee90e3c1849446
4
- data.tar.gz: 70a12c6ea35bd5981acc401f0a7dc482c2f2161c2a67a8b4ffcefc00ac1777a4
3
+ metadata.gz: 8c54bf6a4ec7bdeafe5e225e2cbbe34a181c1990254cd2fb1d3b975fbe670451
4
+ data.tar.gz: a3c802ffc1435b5797b32c2083f1512de1ca74309c25387bdf5fa87110c1fa41
5
5
  SHA512:
6
- metadata.gz: e45f31e4dacefa4d0622f1fdfe8d8cb48dca0c4ad256481a06f8bf7d1dea2ef231be657db2b8c92c1e129faa8375bdc36221152fb7967cca15ae7bc3c5fce89b
7
- data.tar.gz: 4cceff6bc76a51aeebb0bc0585ec9228d53151421927e8e8461e39a79f0208ed3a793455f8b9e0714364ce9cb169e987ab757a29e7ad4ab3acc908ab72f8a11f
6
+ metadata.gz: 54aa773fe7dc3a6699e7d04de688174f656151dd507bfe439c9780a5efa6521dddbc26d7a97e6e4889f5e17d505cbc2e8926567ef9dc0b30b04f171fcdfcb1d9
7
+ data.tar.gz: c564091430f87f22ef93046ceb2c5a28c210610e9fee78e6d87e082a7380440c20547e1b449b34321bfed8c80e1a83d1707f9daa822fd70cbf053384e8827c00
data/CHANGELOG.md CHANGED
@@ -5,7 +5,17 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [0.4.0] - 2023-11-15
8
+ ## [0.5.0] - 2025-03-12
9
+
10
+ ### Added
11
+ - Advanced traceroute functionality for network path analysis
12
+ - Multiple protocol support for traceroute: ICMP, UDP, and TCP
13
+ - Load balancing detection in traceroute results
14
+ - CLI command: `traceroute` with protocol selection and customizable parameters
15
+ - Ruby API for programmatic traceroute operations
16
+ - Comprehensive documentation and examples for traceroute feature
17
+
18
+ ## [0.4.0] - 2025-03-10
9
19
 
10
20
  ### Added
11
21
  - Mesh networking functionality for decentralized communication
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- lanet (0.4.0)
4
+ lanet (0.5.0)
5
5
  thor (~> 1.2)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -20,6 +20,7 @@ A lightweight, powerful LAN communication tool that enables secure message excha
20
20
  - **Digital Signatures**: Ensure message authenticity and integrity
21
21
  - **File Transfers**: Securely send encrypted files over the LAN with progress tracking and integrity verification
22
22
  - **Mesh Networking**: Create resilient mesh networks for decentralized communication, enabling messages to be routed through multiple hops without central infrastructure
23
+ - **Advanced Traceroute**: Analyze network paths using multiple protocols (ICMP, UDP, and TCP)
23
24
 
24
25
  ## Security Features
25
26
 
@@ -334,6 +335,36 @@ View information about your mesh network:
334
335
  lanet mesh info
335
336
  ```
336
337
 
338
+ #### Tracing the route to a target host
339
+
340
+ Basic traceroute using UDP (default protocol):
341
+
342
+ ```bash
343
+ # Simple format
344
+ lanet traceroute google.com
345
+
346
+ # Option format
347
+ lanet traceroute --host google.com
348
+ ```
349
+
350
+ Trace using ICMP protocol:
351
+
352
+ ```bash
353
+ lanet traceroute --host google.com --protocol icmp
354
+ ```
355
+
356
+ Trace using TCP protocol:
357
+
358
+ ```bash
359
+ lanet traceroute --host google.com --protocol tcp --max-hops 15
360
+ ```
361
+
362
+ Customize traceroute parameters:
363
+
364
+ ```bash
365
+ lanet traceroute --host github.com --protocol tcp --max-hops 20 --timeout 2 --queries 4
366
+ ```
367
+
337
368
  ### Ruby API
338
369
 
339
370
  You can also use Lanet programmatically in your Ruby applications:
@@ -443,6 +474,17 @@ end
443
474
 
444
475
  # Properly stop the mesh node
445
476
  mesh.stop
477
+
478
+ # Trace route to a host with different protocols
479
+ tracer = Lanet.traceroute(protocol: :udp)
480
+ hops = tracer.trace('github.com')
481
+ hops.each do |hop|
482
+ puts "Hop #{hop[:ttl]}: #{hop[:ip]} - Response: #{hop[:avg_time]}ms"
483
+ end
484
+
485
+ # Use TCP protocol with custom parameters
486
+ tcp_tracer = Lanet.traceroute(protocol: :tcp, max_hops: 15, timeout: 2)
487
+ tcp_tracer.trace('google.com')
446
488
  ```
447
489
 
448
490
  ## Mesh Network
data/index.html CHANGED
@@ -243,6 +243,22 @@
243
243
  </div>
244
244
  </div>
245
245
 
246
+ <div class="feature">
247
+ <h3>Advanced Traceroute <span class="badge badge-new">New in v0.5.0</span></h3>
248
+ <p>Analyze network paths with multi-protocol traceroute capabilities to understand connectivity and troubleshoot network issues.</p>
249
+
250
+ <div class="security-feature">
251
+ <h4>Features of Advanced Traceroute:</h4>
252
+ <ul>
253
+ <li><strong>Multi-protocol support</strong>: Use ICMP, UDP, or TCP protocols for different network environments</li>
254
+ <li><strong>Load balancing detection</strong>: Identify multi-path routing and load balancers in the network</li>
255
+ <li><strong>Response time analysis</strong>: Measure latency at each hop in the network path</li>
256
+ <li><strong>Customizable parameters</strong>: Adjust max hops, timeouts, and query count for different scenarios</li>
257
+ <li><strong>Hostname resolution</strong>: Automatically resolve IP addresses to hostnames for easier identification</li>
258
+ </ul>
259
+ </div>
260
+ </div>
261
+
246
262
  <h2>Command Line Interface</h2>
247
263
 
248
264
  <div class="note">
@@ -437,6 +453,48 @@ Message cache: 24 messages
437
453
  </div>
438
454
  </div>
439
455
 
456
+ <div class="cli-section">
457
+ <h3>Traceroute Commands <span class="badge badge-new">New in v0.5.0</span></h3>
458
+
459
+ <h4>Basic Traceroute (UDP protocol)</h4>
460
+ <div class="cli-example">
461
+ lanet traceroute --host google.com
462
+ </div>
463
+
464
+ <h4>Traceroute with ICMP Protocol</h4>
465
+ <div class="cli-example">
466
+ lanet traceroute --host google.com --protocol icmp
467
+ </div>
468
+
469
+ <h4>Traceroute with TCP Protocol</h4>
470
+ <div class="cli-example">
471
+ lanet traceroute --host github.com --protocol tcp
472
+ </div>
473
+
474
+ <h4>Customize Traceroute Parameters</h4>
475
+ <div class="cli-example">
476
+ lanet traceroute --host cloudflare.com --protocol tcp --max-hops 20 --timeout 2 --queries 4
477
+ </div>
478
+
479
+ <p>Example output of a traceroute:</p>
480
+ <div class="output-example">
481
+ Tracing route to github.com using UDP protocol
482
+ Maximum hops: 30, Timeout: 1s, Queries: 3
483
+ ======================================================================
484
+ TTL IP Address Hostname Response Time
485
+ ----------------------------------------------------------------------
486
+ 1 192.168.1.1 router.home 2.34ms
487
+ 2 172.16.42.1 isp-gateway.net 8.72ms
488
+ 3 216.58.223.14 15.35ms
489
+ 4 172.217.170.78 edge-router.google.com 22.89ms
490
+ 5 * * Request timed out
491
+ 6 140.82.121.4 github.com 45.23ms
492
+ Destination unreachable
493
+ ======================================================================
494
+ Trace complete.
495
+ </div>
496
+ </div>
497
+
440
498
  <h2>Ruby Code Examples</h2>
441
499
 
442
500
  <div class="tab-container">
@@ -446,6 +504,7 @@ Message cache: 24 messages
446
504
  <button class="tab-button" onclick="openTab(event, 'tab-advanced')">Advanced Usage</button>
447
505
  <button class="tab-button" onclick="openTab(event, 'tab-filetransfer')">File Transfer</button>
448
506
  <button class="tab-button" onclick="openTab(event, 'tab-mesh')">Mesh Network</button>
507
+ <button class="tab-button" onclick="openTab(event, 'tab-traceroute')">Traceroute</button>
449
508
  </div>
450
509
 
451
510
  <div id="tab-basic" class="tab-content active">
@@ -626,6 +685,51 @@ end
626
685
  # Always stop the mesh node when done
627
686
  mesh.stop</code></pre>
628
687
  </div>
688
+
689
+ <div id="tab-traceroute" class="tab-content">
690
+ <pre><code>require 'lanet'
691
+
692
+ # Create a traceroute instance with UDP protocol (default)
693
+ tracer = Lanet.traceroute
694
+ results = tracer.trace('github.com')
695
+
696
+ # Display the results
697
+ puts "Path to github.com:"
698
+ results.each do |hop|
699
+ if hop[:ip].nil?
700
+ puts "Hop #{hop[:ttl]}: * * * Request timed out"
701
+ else
702
+ hostname = hop[:hostname] ? hop[:hostname] : ""
703
+ time = hop[:avg_time] ? "#{hop[:avg_time]}ms" : "*"
704
+ puts "Hop #{hop[:ttl]}: #{hop[:ip]} (#{hostname}) #{time}"
705
+
706
+ # Check for load balancing (multiple IPs at the same hop)
707
+ if hop[:all_ips] && hop[:all_ips].size > 1
708
+ puts " Multiple IPs detected (load balancing):"
709
+ hop[:all_ips].each { |ip| puts " - #{ip}" }
710
+ end
711
+ end
712
+ end
713
+
714
+ # Use ICMP protocol (may require root/admin privileges)
715
+ begin
716
+ icmp_tracer = Lanet.traceroute(protocol: :icmp, max_hops: 10)
717
+ icmp_tracer.trace('google.com')
718
+ rescue StandardError => e
719
+ puts "Error with ICMP traceroute: #{e.message}"
720
+ end
721
+
722
+ # Use TCP protocol with custom parameters
723
+ tcp_tracer = Lanet.traceroute(protocol: :tcp, max_hops: 15,
724
+ timeout: 2, queries: 4)
725
+ tcp_results = tcp_tracer.trace('cloudflare.com')
726
+
727
+ # Analyze a specific hop
728
+ interesting_hop = tcp_results[5] # Sixth hop
729
+ if interesting_hop && interesting_hop[:unreachable]
730
+ puts "Destination unreachable at hop #{interesting_hop[:ttl]}"
731
+ end</code></pre>
732
+ </div>
629
733
  </div>
630
734
 
631
735
  <h2>Installation</h2>
@@ -640,7 +744,7 @@ mesh.stop</code></pre>
640
744
  <p>For complete documentation, please visit the <a href="https://github.com/davidesantangelo/lanet">GitHub repository</a>.</p>
641
745
 
642
746
  <footer style="margin-top: 40px; text-align: center; color: #7f8c8d;">
643
- <p>Lanet v0.4.0 - Secure Network Communications Library</p>
747
+ <p>Lanet v0.5.0 - Secure Network Communications Library</p>
644
748
  </footer>
645
749
  </div>
646
750
 
data/lib/lanet/cli.rb CHANGED
@@ -6,6 +6,7 @@ require "lanet/receiver"
6
6
  require "lanet/scanner"
7
7
  require "lanet/ping"
8
8
  require "lanet/encryptor"
9
+ require "lanet/traceroute"
9
10
 
10
11
  module Lanet
11
12
  class CLI < Thor
@@ -366,6 +367,62 @@ module Lanet
366
367
  mesh.stop
367
368
  end
368
369
 
370
+ desc "traceroute [HOST]", "Trace the route to a target host using different protocols"
371
+ method_option :host, type: :string, desc: "Target host to trace route"
372
+ method_option :protocol, type: :string, default: "udp", desc: "Protocol to use (icmp, udp, tcp)"
373
+ method_option :max_hops, type: :numeric, default: 30, desc: "Maximum number of hops"
374
+ method_option :timeout, type: :numeric, default: 1, desc: "Timeout in seconds for each probe"
375
+ method_option :queries, type: :numeric, default: 3, desc: "Number of queries per hop"
376
+ def traceroute(single_host = nil)
377
+ # Use the positional parameter if provided, otherwise use the --host option
378
+ target_host = single_host || options[:host]
379
+
380
+ # Ensure we have a host to trace
381
+ unless target_host
382
+ puts "Error: No host specified. Please provide a host as an argument or use --host option."
383
+ return
384
+ end
385
+
386
+ tracer = Lanet::Traceroute.new(
387
+ protocol: options[:protocol].to_sym,
388
+ max_hops: options[:max_hops],
389
+ timeout: options[:timeout],
390
+ queries: options[:queries]
391
+ )
392
+
393
+ puts "Tracing route to #{target_host} using #{options[:protocol].upcase} protocol"
394
+ puts "Maximum hops: #{options[:max_hops]}, Timeout: #{options[:timeout]}s, Queries: #{options[:queries]}"
395
+ puts "=" * 70
396
+ puts format("%3s %-15s %-30s %-10s", "TTL", "IP Address", "Hostname", "Response Time")
397
+ puts "-" * 70
398
+
399
+ tracer.trace(target_host).each do |hop|
400
+ if hop[:ip].nil?
401
+ puts format("%3d %-15s %-30s %-10s", hop[:ttl], "*", "*", "Request timed out")
402
+ else
403
+ hostname = hop[:hostname] || ""
404
+ time_str = hop[:avg_time] ? "#{hop[:avg_time]}ms" : "*"
405
+ puts format("%3d %-15s %-30s %-10s", hop[:ttl], hop[:ip], hostname, time_str)
406
+
407
+ # Show all IPs if there are multiple (for load balancing detection)
408
+ if hop[:all_ips] && hop[:all_ips].size > 1
409
+ puts " Multiple IPs detected (possible load balancing):"
410
+ hop[:all_ips].each do |ip|
411
+ puts " - #{ip}"
412
+ end
413
+ end
414
+
415
+ # Show unreachable marker
416
+ puts " Destination unreachable" if hop[:unreachable]
417
+ end
418
+ end
419
+ puts "=" * 70
420
+ puts "Trace complete."
421
+ rescue StandardError => e
422
+ puts "Error performing traceroute: #{e.message}"
423
+ puts e.backtrace if options[:verbose]
424
+ end
425
+
369
426
  private
370
427
 
371
428
  def display_ping_details(host, result)
@@ -0,0 +1,426 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "timeout"
5
+ require "resolv"
6
+
7
+ module Lanet
8
+ class Traceroute
9
+ # Supported protocols
10
+ PROTOCOLS = %i[icmp udp tcp].freeze
11
+
12
+ # Default settings
13
+ DEFAULT_MAX_HOPS = 30
14
+ DEFAULT_TIMEOUT = 1
15
+ DEFAULT_QUERIES = 3
16
+ DEFAULT_PORT = 33_434 # Starting port for UDP traceroute
17
+
18
+ attr_reader :results, :protocol, :max_hops, :timeout, :queries
19
+
20
+ def initialize(protocol: :udp, max_hops: DEFAULT_MAX_HOPS, timeout: DEFAULT_TIMEOUT, queries: DEFAULT_QUERIES)
21
+ @protocol = protocol.to_sym
22
+ @max_hops = max_hops
23
+ @timeout = timeout
24
+ @queries = queries
25
+ @results = []
26
+
27
+ return if PROTOCOLS.include?(@protocol)
28
+
29
+ raise ArgumentError, "Protocol must be one of #{PROTOCOLS.join(", ")}"
30
+ end
31
+
32
+ def trace(destination)
33
+ @results = []
34
+ destination_ip = resolve_destination(destination)
35
+
36
+ begin
37
+ case @protocol
38
+ when :icmp
39
+ trace_icmp(destination_ip)
40
+ when :udp
41
+ trace_udp(destination_ip)
42
+ when :tcp
43
+ trace_tcp(destination_ip)
44
+ end
45
+ rescue StandardError => e
46
+ raise e unless e.message.include?("Must run as root/administrator")
47
+
48
+ # Fall back to system traceroute command if we don't have root privileges
49
+ trace_using_system_command(destination)
50
+ end
51
+
52
+ @results
53
+ end
54
+
55
+ private
56
+
57
+ def trace_using_system_command(destination)
58
+ # Build the appropriate system traceroute command
59
+ system_cmd = case @protocol
60
+ when :icmp
61
+ "traceroute -I"
62
+ when :tcp
63
+ "traceroute -T"
64
+ else
65
+ "traceroute" # UDP is the default for most traceroute commands
66
+ end
67
+
68
+ # Add options for max hops, timeout, and queries/retries
69
+ system_cmd += " -m #{@max_hops} -w #{@timeout} -q #{@queries} #{destination}"
70
+
71
+ # Execute the command and capture output
72
+ output = `#{system_cmd}`
73
+
74
+ # Parse the output to build our results
75
+ parse_system_traceroute_output(output)
76
+ end
77
+
78
+ def parse_system_traceroute_output(output)
79
+ lines = output.split("\n")
80
+
81
+ # Skip only the header line which typically starts with "traceroute to..."
82
+ lines.shift if lines.any? && lines.first.start_with?("traceroute to")
83
+
84
+ # Process each line of output
85
+ lines.each do |line|
86
+ # Extract hop number and details
87
+ next unless line =~ /^\s*(\d+)\s+(.+)$/
88
+
89
+ hop_num = Regexp.last_match(1).to_i
90
+ hop_details = Regexp.last_match(2)
91
+
92
+ # Parse the hop details
93
+ hostname = nil
94
+ avg_time = nil
95
+
96
+ # Check for timeout indicated by asterisks
97
+ if ["* * *", "*"].include?(hop_details.strip)
98
+ # All timeouts at this hop
99
+ @results << { ttl: hop_num, ip: nil, hostname: nil, avg_time: nil, timeouts: @queries }
100
+ next
101
+ end
102
+
103
+ # Extract all IPs from the hop details to support load-balancing detection
104
+ all_ips = extract_multiple_ips(hop_details)
105
+
106
+ # Extract the first IP (primary IP for this hop)
107
+ ip = all_ips&.first
108
+
109
+ # Try to extract hostname if present
110
+ # Format: "hostname (ip)"
111
+ if hop_details =~ /([^\s(]+)\s+\(([0-9.]+)\)/
112
+ hostname = Regexp.last_match(1)
113
+ # We already have the IP from all_ips, so no need to set it again
114
+ end
115
+
116
+ # Extract response times - typically format is "X.XXX ms Y.YYY ms Z.ZZZ ms"
117
+ times = hop_details.scan(/(\d+\.\d+)\s*ms/).flatten.map(&:to_f)
118
+ avg_time = (times.sum / times.size).round(2) if times.any?
119
+
120
+ # Add to results
121
+ @results << {
122
+ ttl: hop_num,
123
+ ip: ip,
124
+ hostname: hostname,
125
+ avg_time: avg_time,
126
+ timeouts: @queries - times.size,
127
+ # Include all IPs if there are multiple
128
+ all_ips: all_ips&.size && all_ips.size > 1 ? all_ips : nil
129
+ }
130
+ end
131
+ end
132
+
133
+ def extract_multiple_ips(hop_details)
134
+ # Match all IP addresses in the hop details
135
+ ips = hop_details.scan(/\b(?:\d{1,3}\.){3}\d{1,3}\b/).uniq
136
+
137
+ # If no IPs were found in a non-timeout line, there might be a special format
138
+ if ips.empty? && !hop_details.include?("*")
139
+ # Try to find any IP-like patterns (some traceroute outputs format differently)
140
+ potential_ips = hop_details.split(/\s+/).select do |part|
141
+ part =~ /\b(?:\d{1,3}\.){3}\d{1,3}\b/
142
+ end
143
+ ips = potential_ips unless potential_ips.empty?
144
+ end
145
+
146
+ ips
147
+ end
148
+
149
+ def resolve_destination(destination)
150
+ # If destination is already an IPv4 address, return it
151
+ return destination if destination =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
152
+
153
+ # Otherwise, resolve the hostname to an IPv4 address
154
+ begin
155
+ addresses = Resolv.getaddresses(destination)
156
+
157
+ # If no addresses are returned, the hostname is unresolvable
158
+ raise Resolv::ResolvError, "no address for #{destination}" if addresses.empty?
159
+
160
+ # Find the first IPv4 address
161
+ ipv4_address = addresses.find { |addr| addr =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ }
162
+
163
+ # If no IPv4 address is found, raise an error
164
+ raise "No IPv4 address found for hostname: #{destination}" if ipv4_address.nil?
165
+
166
+ ipv4_address
167
+ rescue Resolv::ResolvError => e
168
+ raise "Unable to resolve hostname: #{e.message}"
169
+ end
170
+ end
171
+
172
+ def get_hostname(ip)
173
+ Timeout.timeout(1) { Resolv.getname(ip) }
174
+ rescue StandardError
175
+ nil
176
+ end
177
+
178
+ def trace_icmp(destination_ip)
179
+ # ICMP traceroute implementation
180
+ 1.upto(@max_hops) do |ttl|
181
+ hop_info = trace_hop_icmp(destination_ip, ttl)
182
+ @results << hop_info
183
+
184
+ # Stop if we've reached the destination
185
+ break if hop_info[:ip] == destination_ip
186
+ # Stop if we've hit an unreachable marker
187
+ break if hop_info[:unreachable]
188
+ end
189
+ end
190
+
191
+ def trace_hop_icmp(destination_ip, ttl)
192
+ hop_info = { ttl: ttl, responses: [] }
193
+
194
+ # Use ping with increasing TTL values
195
+ @queries.times do
196
+ Time.now
197
+ cmd = ping_command_with_ttl(destination_ip, ttl)
198
+
199
+ ip = nil
200
+ response_time = nil
201
+
202
+ # Execute the ping command and parse the output
203
+ begin
204
+ output = `#{cmd}`
205
+
206
+ # Parse the response to get the responding IP
207
+ if output =~ /from (\d+\.\d+\.\d+\.\d+).*time=(\d+\.?\d*)/
208
+ ip = ::Regexp.last_match(1)
209
+ response_time = ::Regexp.last_match(2).to_f
210
+ end
211
+ rescue StandardError
212
+ # Handle errors
213
+ end
214
+
215
+ hop_info[:responses] << {
216
+ ip: ip,
217
+ response_time: response_time,
218
+ timeout: ip.nil?
219
+ }
220
+ end
221
+
222
+ # Process the responses
223
+ process_hop_responses(hop_info)
224
+ end
225
+
226
+ def ping_command_with_ttl(ip, ttl)
227
+ case RbConfig::CONFIG["host_os"]
228
+ when /mswin|mingw|cygwin/
229
+ "ping -n 1 -i #{ttl} -w #{@timeout * 1000} #{ip}"
230
+ when /darwin/
231
+ "ping -c 1 -m #{ttl} -t #{@timeout} #{ip}"
232
+ else
233
+ "ping -c 1 -t #{ttl} -W #{@timeout} #{ip}"
234
+ end
235
+ end
236
+
237
+ def trace_udp(destination_ip)
238
+ 1.upto(@max_hops) do |ttl|
239
+ hop_info = trace_hop_udp(destination_ip, ttl)
240
+ @results << hop_info
241
+
242
+ # Stop if we've reached the destination or hit a destination unreachable
243
+ break if hop_info[:ip] == destination_ip || hop_info[:unreachable]
244
+ end
245
+ end
246
+
247
+ def trace_hop_udp(destination_ip, ttl)
248
+ hop_info = { ttl: ttl, responses: [] }
249
+
250
+ # Create a listener socket for ICMP responses
251
+ icmp_socket = create_icmp_socket
252
+
253
+ @queries.times do |i|
254
+ start_time = Time.now
255
+ port = DEFAULT_PORT + i + (ttl * @queries)
256
+
257
+ begin
258
+ # Create and configure the sending socket
259
+ sender = UDPSocket.new
260
+ sender.setsockopt(Socket::IPPROTO_IP, Socket::IP_TTL, ttl)
261
+
262
+ # Send the UDP packet
263
+ Timeout.timeout(@timeout) do
264
+ sender.send("TRACE", 0, destination_ip, port)
265
+
266
+ # Wait for ICMP response
267
+ data, addr = icmp_socket.recvfrom(512)
268
+ response_time = ((Time.now - start_time) * 1000).round(2)
269
+
270
+ # The responding IP is in addr[2]
271
+ ip = addr[2]
272
+
273
+ # Check if we've received an ICMP destination unreachable message
274
+ unreachable = data.bytes[20] == 3 # ICMP Type 3 is Destination Unreachable
275
+
276
+ hop_info[:responses] << {
277
+ ip: ip,
278
+ response_time: response_time,
279
+ timeout: false,
280
+ unreachable: unreachable
281
+ }
282
+ end
283
+ rescue Timeout::Error
284
+ hop_info[:responses] << { ip: nil, response_time: nil, timeout: true }
285
+ rescue StandardError => e
286
+ hop_info[:responses] << { ip: nil, response_time: nil, timeout: true, error: e.message }
287
+ ensure
288
+ sender&.close
289
+ end
290
+ end
291
+
292
+ icmp_socket.close
293
+ process_hop_responses(hop_info)
294
+ end
295
+
296
+ def create_icmp_socket
297
+ socket = Socket.new(Socket::AF_INET, Socket::SOCK_RAW, Socket::IPPROTO_ICMP)
298
+ if RbConfig::CONFIG["host_os"] =~ /mswin|mingw|cygwin/
299
+ # Windows requires different socket setup
300
+ else
301
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
302
+ end
303
+ socket
304
+ rescue Errno::EPERM, Errno::EACCES
305
+ raise "Must run as root/administrator to create raw sockets for traceroute"
306
+ end
307
+
308
+ def trace_tcp(destination_ip)
309
+ 1.upto(@max_hops) do |ttl|
310
+ hop_info = trace_hop_tcp(destination_ip, ttl)
311
+ @results << hop_info
312
+
313
+ # Stop if we've reached the destination
314
+ break if hop_info[:ip] == destination_ip
315
+ # Stop if we've hit an unreachable marker
316
+ break if hop_info[:unreachable]
317
+ end
318
+ end
319
+
320
+ def trace_hop_tcp(destination_ip, ttl)
321
+ hop_info = { ttl: ttl, responses: [] }
322
+
323
+ @queries.times do |i|
324
+ # Use different ports for each query
325
+ port = 80 + i
326
+ start_time = Time.now
327
+
328
+ begin
329
+ # Create TCP socket with specific TTL
330
+ socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
331
+ socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_TTL, ttl)
332
+
333
+ # Attempt to connect with timeout
334
+ Timeout.timeout(@timeout) do
335
+ sockaddr = Socket.sockaddr_in(port, destination_ip)
336
+ socket.connect_nonblock(sockaddr)
337
+ end
338
+
339
+ # If we get here, we successfully connected (likely at the final hop)
340
+ response_time = ((Time.now - start_time) * 1000).round(2)
341
+ hop_info[:responses] << {
342
+ ip: destination_ip,
343
+ response_time: response_time,
344
+ timeout: false
345
+ }
346
+ rescue IO::WaitWritable
347
+ # Connection in progress - need to use select for non-blocking socket
348
+ response_time = nil
349
+ ip = nil
350
+
351
+ begin
352
+ Timeout.timeout(@timeout) do
353
+ _, writable, = IO.select(nil, [socket], nil, @timeout)
354
+ if writable&.any?
355
+ # Socket is writable, check for errors
356
+ begin
357
+ socket.connect_nonblock(sockaddr) # Will raise Errno::EISCONN if connected
358
+ rescue Errno::EISCONN
359
+ # Successfully connected
360
+ response_time = ((Time.now - start_time) * 1000).round(2)
361
+ ip = destination_ip
362
+ rescue SystemCallError
363
+ # Get the intermediary IP from the error
364
+ # This is a simplification - in reality, we'd need to use raw sockets
365
+ # and analyze TCP packets with specific TTL values
366
+ ip = nil
367
+ end
368
+ end
369
+ end
370
+ rescue Timeout::Error
371
+ hop_info[:responses] << { ip: nil, response_time: nil, timeout: true }
372
+ end
373
+
374
+ if ip
375
+ hop_info[:responses] << {
376
+ ip: ip,
377
+ response_time: response_time,
378
+ timeout: false
379
+ }
380
+ end
381
+ rescue SystemCallError, Timeout::Error
382
+ hop_info[:responses] << { ip: nil, response_time: nil, timeout: true }
383
+ ensure
384
+ socket&.close
385
+ end
386
+ end
387
+
388
+ process_hop_responses(hop_info)
389
+ end
390
+
391
+ def process_hop_responses(hop_info)
392
+ # Count timeouts
393
+ timeouts = hop_info[:responses].count { |r| r[:timeout] }
394
+
395
+ # If all queries timed out
396
+ return { ttl: hop_info[:ttl], ip: nil, hostname: nil, avg_time: nil, timeouts: timeouts } if timeouts == @queries
397
+
398
+ # Get all responding IPs (could be different if load balancing is in effect)
399
+ ips = hop_info[:responses].map { |r| r[:ip] }.compact.uniq
400
+
401
+ # Most common responding IP
402
+ ip = ips.max_by { |i| hop_info[:responses].count { |r| r[:ip] == i } }
403
+
404
+ # Calculate average response time for responses from the most common IP
405
+ valid_times = hop_info[:responses].select { |r| r[:ip] == ip && r[:response_time] }.map { |r| r[:response_time] }
406
+ avg_time = valid_times.empty? ? nil : (valid_times.sum / valid_times.size).round(2)
407
+
408
+ # Check if any responses indicated "unreachable"
409
+ unreachable = hop_info[:responses].any? { |r| r[:unreachable] }
410
+
411
+ # Get hostname for the IP
412
+ hostname = get_hostname(ip)
413
+
414
+ {
415
+ ttl: hop_info[:ttl],
416
+ ip: ip,
417
+ hostname: hostname,
418
+ avg_time: avg_time,
419
+ timeouts: timeouts,
420
+ unreachable: unreachable,
421
+ # Include all IPs if there are different ones (for load balancing detection)
422
+ all_ips: ips.size > 1 ? ips : nil
423
+ }
424
+ end
425
+ end
426
+ 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.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/lanet.rb CHANGED
@@ -9,6 +9,7 @@ require "lanet/cli"
9
9
  require "lanet/ping"
10
10
  require "lanet/file_transfer"
11
11
  require "lanet/mesh"
12
+ require "lanet/traceroute"
12
13
 
13
14
  module Lanet
14
15
  class Error < StandardError; end
@@ -56,5 +57,10 @@ module Lanet
56
57
  def mesh_network(port = 5050, max_hops = 10)
57
58
  Mesh.new(port, max_hops)
58
59
  end
60
+
61
+ # Create a new traceroute instance
62
+ def traceroute(protocol: :udp, max_hops: 30, timeout: 1, queries: 3)
63
+ Traceroute.new(protocol: protocol, max_hops: max_hops, timeout: timeout, queries: queries)
64
+ end
59
65
  end
60
66
  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.4.0
4
+ version: 0.5.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-10 00:00:00.000000000 Z
11
+ date: 2025-03-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -100,6 +100,7 @@ files:
100
100
  - lib/lanet/scanner.rb
101
101
  - lib/lanet/sender.rb
102
102
  - lib/lanet/signer.rb
103
+ - lib/lanet/traceroute.rb
103
104
  - lib/lanet/version.rb
104
105
  - sig/lanet.rbs
105
106
  homepage: https://github.com/davidesantangelo/lanet