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 +4 -4
- data/CHANGELOG.md +11 -1
- data/Gemfile.lock +1 -1
- data/README.md +42 -0
- data/index.html +105 -1
- data/lib/lanet/cli.rb +57 -0
- data/lib/lanet/traceroute.rb +426 -0
- data/lib/lanet/version.rb +1 -1
- data/lib/lanet.rb +6 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8c54bf6a4ec7bdeafe5e225e2cbbe34a181c1990254cd2fb1d3b975fbe670451
|
4
|
+
data.tar.gz: a3c802ffc1435b5797b32c2083f1512de1ca74309c25387bdf5fa87110c1fa41
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
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.
|
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
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
|
+
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-
|
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
|