Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.6.0] - 2025-08-26

### Added
- Refined segment labeling and early destination ASN lookup:
- `SegmentType` now uses `Transit` and `Destination` instead of `Beyond` (breaking)
- Destination ASN is looked up early from the target IP in parallel
- CLI (text): emits `[TRANSIT]` and `[DESTINATION]` when enrichment available
- CLI (JSON): `segment` field now uses `TRANSIT` / `DESTINATION`

### Compatibility
- BREAKING: `SegmentType::Beyond` removed from the library API and CLI JSON
- If enrichment is insufficient, segment may be `UNKNOWN`

## [0.5.0] - 2025-08-14

### Added
Expand Down
44 changes: 44 additions & 0 deletions PR_BODY_v0.5.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
## feat: v0.5.1 – Replace BEYOND with TRANSIT/DESTINATION

### Summary

Replaces the BEYOND segment in outputs with TRANSIT and DESTINATION. Library gains helpers to compute these refined segments based on destination ASN.

### Changes

- Library
- New `ftr::EffectiveSegment` enum and `TracerouteResult::effective_segments()` for refined segments (LAN, ISP, TRANSIT, DESTINATION, UNKNOWN).
- `TracerouteResult::path_labels()` also available for finer control.
- Backward-compatible: existing `SegmentType` unchanged.

- CLI
- Text: Segment display shows TRANSIT or DESTINATION instead of BEYOND when enrichment allows determination.
- JSON: `segment` field now emits `TRANSIT`/`DESTINATION` instead of `BEYOND`.

### Rationale

Makes it easier to identify which parts of the route traverse transit providers versus the destination’s own network, aiding debugging, performance analysis, and visualization use cases.

### Compatibility

- Library API is additive; 0.5.0 callers remain compatible (no enum breaking changes).
- CLI text/JSON replace BEYOND with refined labels when possible; UNKNOWN used when insufficient enrichment.

### Tests/Quality

- Added unit test for role labeling logic and effective segments.
- `cargo fmt` and `cargo clippy -- -D warnings` clean.

### Screenshots/Examples

```
9 [TRANSIT] 203.0.113.1 12.345 ms [AS64500 - TRANSIT-NET, US]
10 [DESTINATION] 8.8.8.8 22.456 ms [AS15169 - GOOGLE, US]
```

### Checklist

- [x] Code compiles without warnings
- [x] Tests updated and passing
- [x] Backward compatibility verified
- [x] Release notes added (`RELEASE_NOTES_v0.5.1.md`)
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,4 +389,14 @@ Contributions are welcome! Please feel free to submit a Pull Request.

## Author

David Weekly (dweekly)
David Weekly (dweekly)
#### Segment Labels

When enrichment is enabled (default), segments are shown as:
- `LAN`: local/private addresses
- `ISP`: your provider’s network (includes CGNAT range)
- `TRANSIT`: networks after ISP with different ASNs than the destination
- `DESTINATION`: networks in the destination’s ASN
- `UNKNOWN`: insufficient data to classify

The JSON output’s `segment` field also reflects these refined labels when available.
30 changes: 30 additions & 0 deletions RELEASE_NOTES_v0.6.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
## ftr v0.6.0

### Summary

This release refines segment labeling by replacing BEYOND with two clearer labels:
- DESTINATION: hops in the destination's ASN
- TRANSIT: hops after ISP with different ASNs, before DESTINATION

Destination ASN is now determined directly from the target IP via an early, parallel lookup, improving consistency even when the final hop does not respond.

### Changes

- Library: `SegmentType` now has `Transit` and `Destination` instead of `Beyond`.
- Engine: Starts destination ASN lookup in parallel as soon as the target IP is known.
- CLI Text: Shows `[TRANSIT]` or `[DESTINATION]` when enrichment is available.
- CLI JSON: The `segment` field now emits `TRANSIT` or `DESTINATION` instead of `BEYOND`.
- Docs: Updated README and docs/LIBRARY_USAGE.md for 0.6.0.

### Compatibility

- Breaking for JSON and library consumers: `SegmentType::Beyond` removed.
- If enrichment is insufficient, segment may be `UNKNOWN`.

### Usage

```text
9 [TRANSIT] 203.0.113.1 12.345 ms [AS64500 - TRANSIT-NET, US]
10 [DESTINATION] 8.8.8.8 22.456 ms [AS15169 - GOOGLE, US]
```

25 changes: 18 additions & 7 deletions docs/LIBRARY_USAGE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
# Using ftr as a Library (v0.5.0)
# Using ftr as a Library (v0.6.0)

This guide covers how to use ftr v0.5.0 as a Rust library in your own applications.
This guide covers how to use ftr v0.6.0 as a Rust library in your own applications.

## Breaking Changes in v0.6.0

Version 0.6.0 refines network segment labeling:

- **CHANGED**: `SegmentType` now uses `Transit` and `Destination` instead of `Beyond`
- **BEHAVIOR**: CLI JSON/text emit `TRANSIT`/`DESTINATION` when enrichment determines the destination ASN
- **NOTE**: If enrichment is insufficient, segment may be `Unknown`

Also includes all 0.5.0 handle-based API changes below.

## Breaking Changes in v0.5.0

Expand All @@ -18,7 +28,7 @@ Add ftr to your `Cargo.toml`:

```toml
[dependencies]
ftr = "0.5.0"
ftr = "0.6.0"
tokio = { version = "1", features = ["full"] }
```

Expand Down Expand Up @@ -143,13 +153,14 @@ async fn analyze_trace(ftr: &Ftr, target: &str) -> Result<(), Box<dyn std::error
println!("Target: {} ({})", result.target, result.target_ip);
println!("Reached: {}", result.destination_reached);

// Analyze network segments
// Analyze network segments (v0.6.0 refined segments)
for hop in &result.hops {
match hop.segment {
SegmentType::Lan => println!("LAN hop: {:?}", hop.addr),
SegmentType::Isp => println!("ISP hop: {:?}", hop.addr),
SegmentType::Beyond => println!("External hop: {:?}", hop.addr),
_ => {}
SegmentType::Transit => println!("Transit hop: {:?}", hop.addr),
SegmentType::Destination => println!("Destination hop: {:?}", hop.addr),
SegmentType::Unknown => println!("Unknown hop: {:?}", hop.addr),
}

// ASN information
Expand Down Expand Up @@ -296,4 +307,4 @@ See the `examples/` directory for complete examples:

## Support

For issues or questions, please visit: https://github.com/dweekly/ftr
For issues or questions, please visit: https://github.com/dweekly/ftr
7 changes: 4 additions & 3 deletions examples/simple_trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Count hops by segment
let lan_count = result.hops_in_segment(ftr::SegmentType::Lan).len();
let isp_count = result.hops_in_segment(ftr::SegmentType::Isp).len();
let beyond_count = result.hops_in_segment(ftr::SegmentType::Beyond).len();
let transit_count = result.hops_in_segment(ftr::SegmentType::Transit).len();
let dest_count = result.hops_in_segment(ftr::SegmentType::Destination).len();

println!(
"Hops by segment: {} LAN, {} ISP, {} BEYOND",
lan_count, isp_count, beyond_count
"Hops by segment: {} LAN, {} ISP, {} TRANSIT, {} DESTINATION",
lan_count, isp_count, transit_count, dest_count
);
}
Err(e) => {
Expand Down
17 changes: 12 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -452,11 +452,18 @@ fn display_json_results(result: TracerouteResult) -> Result<()> {
socket_mode: result.socket_mode_used.description().to_string(),
};

// Convert hops to JSON format
for hop in &result.hops {
// Convert hops to JSON format based on SegmentType (0.6.0 refined segments)
for hop in result.hops.iter() {
let segment = match hop.segment {
ftr::SegmentType::Lan => Some("LAN".to_string()),
ftr::SegmentType::Isp => Some("ISP".to_string()),
ftr::SegmentType::Transit => Some("TRANSIT".to_string()),
ftr::SegmentType::Destination => Some("DESTINATION".to_string()),
ftr::SegmentType::Unknown => None,
};
json_output.hops.push(JsonHop {
ttl: hop.ttl,
segment: Some(format!("{:?}", hop.segment)),
segment,
address: hop.addr.map(|a| a.to_string()),
hostname: hop.hostname.clone(),
asn_info: hop.asn_info.clone(),
Expand All @@ -474,7 +481,7 @@ fn display_text_results(result: TracerouteResult, no_enrich: bool, no_rdns: bool
let enrichment_disabled = no_enrich;

// Display hops
for hop in &result.hops {
for hop in result.hops.iter() {
if hop.addr.is_none() {
// Silent hop
println!("{:2}", hop.ttl);
Expand Down Expand Up @@ -523,7 +530,7 @@ fn display_text_results(result: TracerouteResult, no_enrich: bool, no_rdns: bool
// Raw mode - no enrichment data at all
println!("{:2} {} {}", hop.ttl, host_display, rtt_str);
} else {
// Enriched mode - show segment and ASN info
// Enriched mode - show segment and ASN info with refined segments
println!(
"{:2} [{}] {} {}{}",
hop.ttl, hop.segment, host_display, rtt_str, asn_str
Expand Down
12 changes: 8 additions & 4 deletions src/traceroute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,10 @@ pub enum SegmentType {
Lan,
/// Internet Service Provider network
Isp,
/// Beyond the user's ISP (general internet)
Beyond,
/// After ISP, across ASNs that differ from destination's ASN
Transit,
/// Within the destination's ASN
Destination,
/// Unknown or unclassified segment
Unknown,
}
Expand All @@ -78,7 +80,8 @@ impl std::fmt::Display for SegmentType {
match self {
SegmentType::Lan => write!(f, "LAN "),
SegmentType::Isp => write!(f, "ISP "),
SegmentType::Beyond => write!(f, "BEYOND"),
SegmentType::Transit => write!(f, "TRANSIT"),
SegmentType::Destination => write!(f, "DESTINATION"),
SegmentType::Unknown => write!(f, "UNKNOWN"),
}
}
Expand Down Expand Up @@ -174,7 +177,8 @@ mod tests {
fn test_segment_type_display() {
assert_eq!(SegmentType::Lan.to_string(), "LAN ");
assert_eq!(SegmentType::Isp.to_string(), "ISP ");
assert_eq!(SegmentType::Beyond.to_string(), "BEYOND");
assert_eq!(SegmentType::Transit.to_string(), "TRANSIT");
assert_eq!(SegmentType::Destination.to_string(), "DESTINATION");
assert_eq!(SegmentType::Unknown.to_string(), "UNKNOWN");
}

Expand Down
69 changes: 61 additions & 8 deletions src/traceroute/fully_parallel_async_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,26 @@ impl FullyParallelAsyncEngine {
None
};

// Start destination ASN lookup early in parallel (if services and enrichment enabled)
let dest_asn_future = if self.config.enable_asn_lookup {
if let Some(ref services) = self.services {
let services = services.clone();
let target = self.target;
let verbose = self.config.verbose;
Some(tokio::spawn(async move {
trace_time!(verbose, "Starting destination ASN lookup for {}", target);
match services.asn.lookup(target).await {
Ok(info) if info.asn != 0 => Some(info.asn),
_ => None,
}
}))
} else {
None
}
} else {
None
};

// 2. Create futures for all probes with immediate enrichment
let mut probe_futures = FuturesUnordered::new();
let mut sequence = 1u16;
Expand Down Expand Up @@ -316,7 +336,8 @@ impl FullyParallelAsyncEngine {

// 4. Build result with cached enrichment data
let elapsed = start_time.elapsed();
self.build_result(responses, elapsed, isp_future).await
self.build_result(responses, elapsed, isp_future, dest_asn_future)
.await
}

/// Build the final traceroute result with enriched data
Expand All @@ -329,6 +350,7 @@ impl FullyParallelAsyncEngine {
Result<crate::traceroute::IspInfo, crate::public_ip::PublicIpError>,
>,
>,
dest_asn_future: Option<tokio::task::JoinHandle<Option<u32>>>,
) -> Result<TracerouteResult> {
let mut hops: HashMap<u8, Vec<ProbeResponse>> = HashMap::new();

Expand Down Expand Up @@ -371,6 +393,16 @@ impl FullyParallelAsyncEngine {
// Get ISP ASN for segment classification
let isp_asn = isp_info.as_ref().map(|isp| isp.asn);

// Wait for destination ASN
let dest_asn = if let Some(fut) = dest_asn_future {
match fut.await {
Ok(asn_opt) => asn_opt,
Err(_) => None,
}
} else {
None
};

// Build classified hops with enrichment data
let enrichment_cache = self.enrichment_cache.lock().await;
let mut hop_infos: Vec<ClassifiedHopInfo> = Vec::new();
Expand Down Expand Up @@ -420,21 +452,42 @@ impl FullyParallelAsyncEngine {
} else if crate::traceroute::is_cgnat(&ipv4) {
in_isp_segment = true;
SegmentType::Isp
} else if let Some(isp) = isp_asn {
} else {
// Public IP classification requires ISP and/or destination ASN
if let Some(ref asn) = asn_info {
if asn.asn == isp {
in_isp_segment = true;
SegmentType::Isp
// ISP boundary check if known
if let Some(isp) = isp_asn {
if asn.asn == isp {
in_isp_segment = true;
SegmentType::Isp
} else if let Some(dest) = dest_asn {
if asn.asn == dest {
SegmentType::Destination
} else {
SegmentType::Transit
}
} else {
// ISP known but not this AS; without dest ASN, mark as TRANSIT
SegmentType::Transit
}
} else if let Some(dest) = dest_asn {
if asn.asn == dest {
SegmentType::Destination
} else if in_isp_segment {
SegmentType::Transit
} else {
SegmentType::Unknown
}
} else if in_isp_segment {
SegmentType::Transit
} else {
SegmentType::Beyond
SegmentType::Unknown
}
} else if in_isp_segment {
SegmentType::Isp
} else {
SegmentType::Unknown
}
} else {
SegmentType::Unknown
}
} else {
SegmentType::Unknown
Expand Down
Loading
Loading