Repository: fujiapple852/trippy (language: rust)

Issue:
Calculate Forward and Backward Loss heuristics
## Idea

Calculate "forward loss" and "backward loss" _heuristics_ and indicate these via new columns `Floss` and `Bloss`.

## Definiton

"Forward loss" for a given probe P in a given round R indicates that (i) probe P was lost in round R and (ii) all _subsequent_ probes within round R were also lost.  

"Backward loss" for a given probe P in a given round R indicates that (i) probe P was lost in round R and (ii) a _prior_ probe within round R has forward loss.

Informally, this is intended to indicate when a probe loss is the _cause_ of subsequent losses ("forward loss") and when a probe loss is the _victim_ of a previous loss ("backward loss").

## Example

In the following example, after initially discovering the target (10.0.0.105) at ttl=5 in round 1, packet loss occurs in all subsequent round at ttl=3.  No probes are able to get beyond ttl=3 and so we observe packet loss at hops ttl=4 and ttl=5.  We can determine that the loss at ttl=3 is "forward loss" in that it causes all subsequent probes (ttl=4, ttl=5) in the same round to be lost.  Likewise, we can determine that hops ttl=4 and ttl=5 have "backward loss" as they have loss in rounds where a prior hop (ttl=3) has "forward loss".

```
╭Hops──────────────────────────────────────────────────────╮
│#    Host         Loss%    Snd     Recv    Floss   Bloss  │
│1    10.0.0.101   0.0%     96      96      0       0      │
│2    10.0.0.102   0.0%     96      96      0       0      │
│3    No response  100.0%   96      0       95      0      │
│4    No response  100.0%   96      0       0       95     │
│5    10.0.0.105   99.0%    96      1       0       95     │
```

Note that in the above the `Loss%` column uses the traditional calculation (`(Snd - Recv) / Snd`).  Similar columns could be added for `Floss%` and `Bloss%`.

## Caveats

This technique is only a heuristic, it cannot conclusively determine forward and backward loss.

Every probe sent in every round is an independent trial, and so there is no guarantee that every probe within a given round will follow the same path (have the same "flow").  This invalidates the concept of forward and backward loss.  We can therefore only meaningfully measure this if we can be confident all probs follow a single path.  This is typically achieved (but not guaranteed) by tracing with the ICMP, UDP/Dublin or UDP/Paris strategy.

Even if a single path is followed by all probes, a given host on the path may drop packets for only a subset of probes sent within a given round, either due to rate limiting or genuine intermittent packet loss.  This could result in a false positive "forward loss" for a given hop if all subsequent hops in the round showing loss which is not genuine (i.e rate limited).  For example, hop with ttl=3 could be incorrectly be deemed to have "forward loss" for a given round due to observed loss from hops ttl=4 and ttl=5 which is not genuine (i.e. rate limited).

Similarly, a false positive "backward loss" could be recorded for a hop with genuine loss, if a previous hop shows loss which is not genuine (i.e. rate limited).  For example, even if hop with ttl=4 has genuine loss, it will still be shown to have "backward loss" due to ttl=3.


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7ccf7654..cb53bf64 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 - Highlight lost probes in sample history ([#1247](https://github.com/fujiapple852/trippy/issues/1247))
 - Added `quit-preserve-screen` (default: `shift+q`) key binding to quit Tui without clearing the
   screen ([#1382](https://github.com/fujiapple852/trippy/issues/1382))
+- Added forward add backward loss heuristics ([#860](https://github.com/fujiapple852/trippy/issues/860))
 
 ### Changed
 
diff --git a/README.md b/README.md
index 1795f7be..4388431f 100644
--- a/README.md
+++ b/README.md
@@ -911,30 +911,33 @@ The below table lists the columns that are available for display in the Tui. The
 `--tui-custom-columns` command line option or in the `tui-custom-columns` attribute in the `tui` section of the
 configuration file.
 
-| Column  | Code | Description                                                                                                                                                                                                                                                                                                                                           |
-|---------|------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `#`     | `h`  | The time-to-live (TTL) for the hop                                                                                                                                                                                                                                                                                                                    |
-| `Host`  | `o`  | The hostname(s) and IP address(s) for the host(s) for the hop<br/>May include AS info, GeoIp and ICMP extensions<br/>Shows full hop details in hop detail navigation mode                                                                                                                                                                             |
-| `Loss%` | `l`  | The packet loss % for the hop                                                                                                                                                                                                                                                                                                                         |
-| `Snd`   | `s`  | The number of probes sent for the hop                                                                                                                                                                                                                                                                                                                 |
-| `Recv`  | `r`  | The number of probe responses received for the hop                                                                                                                                                                                                                                                                                                    |
-| `Last`  | `a`  | The round-trip-time (RTT) of the last probe for the hop                                                                                                                                                                                                                                                                                               |
-| `Avg`   | `v`  | The average RTT of all probes for the hop                                                                                                                                                                                                                                                                                                             |
-| `Best`  | `b`  | The best RTT of all probes for the hop                                                                                                                                                                                                                                                                                                                |
-| `Wrst`  | `w`  | The worst RTT of all probes for the hop                                                                                                                                                                                                                                                                                                               |
-| `StDev` | `d`  | The standard deviation of all probes for the hop                                                                                                                                                                                                                                                                                                      |
-| `Sts`   | `t`  | The status for the hop:<br/>- 🟢 Healthy hop<br/>- 🔵 Non-target hop with packet loss (does not necessarily indicate a problem)<br/>- 🟤 Non-target hop is unresponsive (does not necessarily indicate a problem) <br/>- 🟡 Target hop with packet loss (likely indicates a problem)<br/>- 🔴 Target hop is unresponsive (likely indicates a problem) |
-| `Jttr`  | `j`  | The round-trip-time (RTT) difference between consecutive rounds for the hop                                                                                                                                                                                                                                                                           |
-| `Javg`  | `g`  | The average jitter of all probes for the hop                                                                                                                                                                                                                                                                                                          |
-| `Jmax`  | `x`  | The maximum jitter of all probes for the hop                                                                                                                                                                                                                                                                                                          |
-| `Jint`  | `i`  | The smoothed jitter value of all probes for the hop                                                                                                                                                                                                                                                                                                   |
-| `Seq`   | `Q`  | The sequence number for the last probe for the hop                                                                                                                                                                                                                                                                                                    |
-| `Sprt`  | `S`  | The source port for the last probe for the hop                                                                                                                                                                                                                                                                                                        |
-| `Dprt`  | `P`  | The destination port for the last probe for the hop                                                                                                                                                                                                                                                                                                   |
-| `Type`  | `T`  | The icmp packet type for the last probe for the hop:<br/>- TE: TimeExceeded<br/>- ER: EchoReply<br/>- DU: DestinationUnreachable<br/>- NA: NotApplicable                                                                                                                                                                                              |
-| `Code`  | `C`  | The icmp packet code for the last probe for the hop                                                                                                                                                                                                                                                                                                   |
-| `Nat`   | `N`  | The NAT detection status for the hop                                                                                                                                                                                                                                                                                                                  |
-| `Fail`  | `f`  | The number of probes which failed to send for the hop                                                                                                                                                                                                                                                                                                 |
+| Column   | Code | Description                                                                                                                                                                                                                                                                                                                                           |
+|----------|------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `#`      | `h`  | The time-to-live (TTL) for the hop                                                                                                                                                                                                                                                                                                                    |
+| `Host`   | `o`  | The hostname(s) and IP address(s) for the host(s) for the hop<br/>May include AS info, GeoIp and ICMP extensions<br/>Shows full hop details in hop detail navigation mode                                                                                                                                                                             |
+| `Loss%`  | `l`  | The packet loss % for the hop                                                                                                                                                                                                                                                                                                                         |
+| `Snd`    | `s`  | The number of probes sent for the hop                                                                                                                                                                                                                                                                                                                 |
+| `Recv`   | `r`  | The number of probe responses received for the hop                                                                                                                                                                                                                                                                                                    |
+| `Last`   | `a`  | The round-trip-time (RTT) of the last probe for the hop                                                                                                                                                                                                                                                                                               |
+| `Avg`    | `v`  | The average RTT of all probes for the hop                                                                                                                                                                                                                                                                                                             |
+| `Best`   | `b`  | The best RTT of all probes for the hop                                                                                                                                                                                                                                                                                                                |
+| `Wrst`   | `w`  | The worst RTT of all probes for the hop                                                                                                                                                                                                                                                                                                               |
+| `StDev`  | `d`  | The standard deviation of all probes for the hop                                                                                                                                                                                                                                                                                                      |
+| `Sts`    | `t`  | The status for the hop:<br/>- 🟢 Healthy hop<br/>- 🔵 Non-target hop with packet loss (does not necessarily indicate a problem)<br/>- 🟤 Non-target hop is unresponsive (does not necessarily indicate a problem) <br/>- 🟡 Target hop with packet loss (likely indicates a problem)<br/>- 🔴 Target hop is unresponsive (likely indicates a problem) |
+| `Jttr`   | `j`  | The round-trip-time (RTT) difference between consecutive rounds for the hop                                                                                                                                                                                                                                                                           |
+| `Javg`   | `g`  | The average jitter of all probes for the hop                                                                                                                                                                                                                                                                                                          |
+| `Jmax`   | `x`  | The maximum jitter of all probes for the hop                                                                                                                                                                                                                                                                                                          |
+| `Jint`   | `i`  | The smoothed jitter value of all probes for the hop                                                                                                                                                                                                                                                                                                   |
+| `Seq`    | `Q`  | The sequence number for the last probe for the hop                                                                                                                                                                                                                                                                                                    |
+| `Sprt`   | `S`  | The source port for the last probe for the hop                                                                                                                                                                                                                                                                                                        |
+| `Dprt`   | `P`  | The destination port for the last probe for the hop                                                                                                                                                                                                                                                                                                   |
+| `Type`   | `T`  | The icmp packet type for the last probe for the hop:<br/>- TE: TimeExceeded<br/>- ER: EchoReply<br/>- DU: DestinationUnreachable<br/>- NA: NotApplicable                                                                                                                                                                                              |
+| `Code`   | `C`  | The icmp packet code for the last probe for the hop                                                                                                                                                                                                                                                                                                   |
+| `Nat`    | `N`  | The NAT detection status for the hop                                                                                                                                                                                                                                                                                                                  |
+| `Fail`   | `f`  | The number of probes which failed to send for the hop                                                                                                                                                                                                                                                                                                 |
+| `Floss`  | `F`  | A _heuristic_ for the number of probes with _forward loss_ for the hop                                                                                                                                                                                                                                                                                |
+| `Bloss`  | `B`  | A _heuristic_ for the number of probes with _backward loss_ for the hop                                                                                                                                                                                                                                                                               |
+| `Floss%` | `D`  | The _forward loss_ % for the hop                                                                                                                                                                                                                                                                                                                      |
 
 The default columns are `holsravbwdt`.
 
diff --git a/crates/trippy-core/src/state.rs b/crates/trippy-core/src/state.rs
index 95817704..4421ef3d 100644
--- a/crates/trippy-core/src/state.rs
+++ b/crates/trippy-core/src/state.rs
@@ -170,6 +170,10 @@ pub struct Hop {
     total_recv: usize,
     /// The total probes that failed for this hop.
     total_failed: usize,
+    /// The total forward loss for this hop.
+    total_forward_lost: usize,
+    /// The total backward loss for this hop.
+    total_backward_lost: usize,
     /// The total round trip time for this hop across all rounds.
     total_time: Duration,
     /// The round trip time for this hop in the current round.
@@ -238,6 +242,18 @@ impl Hop {
         self.total_recv
     }
 
+    /// The total number of probes with forward loss.
+    #[must_use]
+    pub const fn total_forward_loss(&self) -> usize {
+        self.total_forward_lost
+    }
+
+    /// The total number of probes with backward loss.
+    #[must_use]
+    pub const fn total_backward_loss(&self) -> usize {
+        self.total_backward_lost
+    }
+
     /// The total number of probes that failed.
     #[must_use]
     pub const fn total_failed(&self) -> usize {
@@ -249,7 +265,29 @@ impl Hop {
     pub fn loss_pct(&self) -> f64 {
         if self.total_sent > 0 {
             let lost = self.total_sent - self.total_recv;
-            lost as f64 / self.total_sent as f64 * 100f64
+            lost as f64 / self.total_sent as f64 * 100_f64
+        } else {
+            0_f64
+        }
+    }
+
+    /// The % of packets that are lost forward.
+    #[must_use]
+    pub fn forward_loss_pct(&self) -> f64 {
+        if self.total_sent > 0 {
+            let lost = self.total_forward_lost;
+            lost as f64 / self.total_sent as f64 * 100_f64
+        } else {
+            0_f64
+        }
+    }
+
+    /// The % of packets that are lost backward.
+    #[must_use]
+    pub fn backward_loss_pct(&self) -> f64 {
+        if self.total_sent > 0 {
+            let lost = self.total_backward_lost;
+            lost as f64 / self.total_sent as f64 * 100_f64
         } else {
             0_f64
         }
@@ -366,6 +404,8 @@ impl Default for Hop {
             addrs: IndexMap::default(),
             total_sent: 0,
             total_recv: 0,
+            total_forward_lost: 0,
+            total_backward_lost: 0,
             total_failed: 0,
             total_time: Duration::default(),
             last: None,
@@ -491,7 +531,7 @@ mod state_updater {
     use crate::{NatStatus, ProbeStatus, Round};
     use std::time::Duration;
 
-    /// Update the `FlowState` from a `Round`.
+    /// Update the state of a `FlowState` from a `Round`.
     pub(super) struct StateUpdater<'a> {
         /// The state to update.
         state: &'a mut FlowState,
@@ -499,6 +539,8 @@ mod state_updater {
         round: &'a Round<'a>,
         /// The checksum of the previous hop, if any.
         prev_hop_checksum: Option<u16>,
+        /// Whether any previous hop in the round had forward loss.
+        forward_loss: bool,
     }
     impl<'a> StateUpdater<'a> {
         pub(super) fn new(state: &'a mut FlowState, round: &'a Round<'_>) -> Self {
@@ -506,6 +548,7 @@ mod state_updater {
                 state,
                 round,
                 prev_hop_checksum: None,
+                forward_loss: false,
             }
         }
 
@@ -577,30 +620,45 @@ mod state_updater {
                     state.update_lowest_ttl(awaited.ttl);
                     state.update_round(awaited.round);
                     let index = usize::from(awaited.ttl.0) - 1;
-                    state.hops[index].total_sent += 1;
-                    state.hops[index].ttl = awaited.ttl.0;
-                    state.hops[index].samples.insert(0, Duration::default());
-                    if state.hops[index].samples.len() > state.max_samples {
-                        state.hops[index].samples.pop();
+                    let hop = &mut state.hops[index];
+                    hop.total_sent += 1;
+                    hop.ttl = awaited.ttl.0;
+                    hop.samples.insert(0, Duration::default());
+                    if hop.samples.len() > state.max_samples {
+                        hop.samples.pop();
+                    }
+                    hop.last_src_port = awaited.src_port.0;
+                    hop.last_dest_port = awaited.dest_port.0;
+                    hop.last_sequence = awaited.sequence.0;
+                    if self.forward_loss {
+                        hop.total_backward_lost += 1;
+                    } else {
+                        let remaining = &self.round.probes[index..];
+                        let all_awaited = remaining
+                            .iter()
+                            .skip(1)
+                            .all(|p| matches!(p, ProbeStatus::Awaited(_) | ProbeStatus::Skipped));
+                        if all_awaited {
+                            hop.total_forward_lost += 1;
+                            self.forward_loss = true;
+                        }
                     }
-                    state.hops[index].last_src_port = awaited.src_port.0;
-                    state.hops[index].last_dest_port = awaited.dest_port.0;
-                    state.hops[index].last_sequence = awaited.sequence.0;
                 }
                 ProbeStatus::Failed(failed) => {
                     state.update_lowest_ttl(failed.ttl);
                     state.update_round(failed.round);
                     let index = usize::from(failed.ttl.0) - 1;
-                    state.hops[index].total_sent += 1;
-                    state.hops[index].total_failed += 1;
-                    state.hops[index].ttl = failed.ttl.0;
-                    state.hops[index].samples.insert(0, Duration::default());
-                    if state.hops[index].samples.len() > state.max_samples {
-                        state.hops[index].samples.pop();
+                    let hop = &mut state.hops[index];
+                    hop.total_sent += 1;
+                    hop.total_failed += 1;
+                    hop.ttl = failed.ttl.0;
+                    hop.samples.insert(0, Duration::default());
+                    if hop.samples.len() > state.max_samples {
+                        hop.samples.pop();
                     }
-                    state.hops[index].last_src_port = failed.src_port.0;
-                    state.hops[index].last_dest_port = failed.dest_port.0;
-                    state.hops[index].last_sequence = failed.sequence.0;
+                    hop.last_src_port = failed.src_port.0;
+                    hop.last_dest_port = failed.dest_port.0;
+                    hop.last_sequence = failed.sequence.0;
                 }
                 ProbeStatus::NotSent | ProbeStatus::Skipped => {}
             }
@@ -794,6 +852,8 @@ mod tests {
         ttl: Option<u8>,
         total_sent: Option<usize>,
         total_recv: Option<usize>,
+        total_forward_loss: Option<usize>,
+        total_backward_loss: Option<usize>,
         loss_pct: Option<f64>,
         last_ms: Option<f64>,
         best_ms: Option<f64>,
@@ -842,6 +902,7 @@ mod tests {
     #[test_case(file!("no_latency.yaml"))]
     #[test_case(file!("nat.yaml"))]
     #[test_case(file!("minimal.yaml"))]
+    #[test_case(file!("floss_bloss.yaml"))]
     fn test_scenario(scenario: Scenario) {
         let mut trace = State::new(StateConfig {
             max_flows: 1,
@@ -876,6 +937,14 @@ mod tests {
             assert_eq_opt(&Some(actual.total_sent()), &expected.total_sent);
             assert_eq_opt(&Some(actual.total_recv()), &expected.total_recv);
             assert_eq_opt_f64(&Some(actual.loss_pct()), &expected.loss_pct);
+            assert_eq_opt(
+                &Some(actual.total_forward_loss()),
+                &expected.total_forward_loss,
+            );
+            assert_eq_opt(
+                &Some(actual.total_backward_loss()),
+                &expected.total_backward_loss,
+            );
             assert_eq_opt_f64(&actual.last_ms(), &expected.last_ms);
             assert_eq_opt_f64(&actual.best_ms(), &expected.best_ms);
             assert_eq_opt_f64(&actual.worst_ms(), &expected.worst_ms);
diff --git a/crates/trippy-tui/locales/app.yml b/crates/trippy-tui/locales/app.yml
index 0bbc3d18..714b4fcb 100644
--- a/crates/trippy-tui/locales/app.yml
+++ b/crates/trippy-tui/locales/app.yml
@@ -1163,4 +1163,37 @@ column_fail:
   sv: "Misslyckades"
   ru: "Неуд"
   es: "Falló"
-  de: "Fehlgeschlagen"
\ No newline at end of file
+  de: "Fehlgeschlagen"
+column_floss:
+  en: "Floss"
+  fr: "Floss"
+  tr: "Floss"
+  it: "Floss"
+  pt: "Floss"
+  zh: "Floss"
+  sv: "Floss"
+  ru: "Floss"
+  es: "Floss"
+  de: "Floss"
+column_bloss:
+  en: "Bloss"
+  fr: "Bloss"
+  tr: "Bloss"
+  it: "Bloss"
+  pt: "Bloss"
+  zh: "Bloss"
+  sv: "Bloss"
+  ru: "Bloss"
+  es: "Bloss"
+  de: "Bloss"
+column_floss_pct:
+  en: "Floss%"
+  fr: "Floss%"
+  tr: "Floss%"
+  it: "Floss%"
+  pt: "Floss%"
+  zh: "Floss%"
+  sv: "Floss%"
+  ru: "Floss%"
+  es: "Floss%"
+  de: "Floss%"
\ No newline at end of file
diff --git a/crates/trippy-tui/src/config/columns.rs b/crates/trippy-tui/src/config/columns.rs
index 684e0cd6..9780a119 100644
--- a/crates/trippy-tui/src/config/columns.rs
+++ b/crates/trippy-tui/src/config/columns.rs
@@ -93,6 +93,12 @@ pub enum TuiColumn {
     LastNatStatus,
     /// The number of probes that failed for a hop.
     Failed,
+    /// The number of probes with forward loss for a hop.
+    Floss,
+    /// The number of probes with backward loss for a hop.
+    Bloss,
+    /// The forward loss % for a hop.
+    FlossPct,
 }
 
 impl TryFrom<char> for TuiColumn {
@@ -122,6 +128,9 @@ impl TryFrom<char> for TuiColumn {
             'C' => Ok(Self::LastIcmpPacketCode),
             'N' => Ok(Self::LastNatStatus),
             'f' => Ok(Self::Failed),
+            'F' => Ok(Self::Floss),
+            'B' => Ok(Self::Bloss),
+            'D' => Ok(Self::FlossPct),
             c => Err(anyhow!(format!("unknown column code: {c}"))),
         }
     }
@@ -152,6 +161,9 @@ impl Display for TuiColumn {
             Self::LastIcmpPacketCode => write!(f, "C"),
             Self::LastNatStatus => write!(f, "N"),
             Self::Failed => write!(f, "f"),
+            Self::Floss => write!(f, "F"),
+            Self::Bloss => write!(f, "B"),
+            Self::FlossPct => write!(f, "D"),
         }
     }
 }
diff --git a/crates/trippy-tui/src/frontend/columns.rs b/crates/trippy-tui/src/frontend/columns.rs
index ffa48daf..630e2b8b 100644
--- a/crates/trippy-tui/src/frontend/columns.rs
+++ b/crates/trippy-tui/src/frontend/columns.rs
@@ -189,6 +189,12 @@ pub enum ColumnType {
     LastNatStatus,
     /// The number of probes that failed for a hop.
     Failed,
+    /// The number of probes with forward loss for a hop.
+    Floss,
+    /// The number of probes with backward loss for a hop.
+    Bloss,
+    /// The forward loss % for a hop.
+    FlossPct,
 }
 
 impl From<ColumnType> for char {
@@ -216,6 +222,9 @@ impl From<ColumnType> for char {
             ColumnType::LastIcmpPacketCode => 'C',
             ColumnType::LastNatStatus => 'N',
             ColumnType::Failed => 'f',
+            ColumnType::Floss => 'F',
+            ColumnType::Bloss => 'B',
+            ColumnType::FlossPct => 'D',
         }
     }
 }
@@ -245,6 +254,9 @@ impl From<TuiColumn> for Column {
             TuiColumn::LastIcmpPacketCode => Self::new_shown(ColumnType::LastIcmpPacketCode),
             TuiColumn::LastNatStatus => Self::new_shown(ColumnType::LastNatStatus),
             TuiColumn::Failed => Self::new_shown(ColumnType::Failed),
+            TuiColumn::Floss => Self::new_shown(ColumnType::Floss),
+            TuiColumn::Bloss => Self::new_shown(ColumnType::Bloss),
+            TuiColumn::FlossPct => Self::new_shown(ColumnType::FlossPct),
         }
     }
 }
@@ -257,6 +269,7 @@ impl Display for ColumnType {
 
 impl ColumnType {
     /// The name of the column in the current locale.
+    #[allow(clippy::cognitive_complexity)]
     pub(self) fn name(&self) -> Cow<'_, str> {
         match self {
             Self::Ttl => Cow::Borrowed("#"),
@@ -281,6 +294,9 @@ impl ColumnType {
             Self::LastIcmpPacketCode => t!("column_code"),
             Self::LastNatStatus => t!("column_nat"),
             Self::Failed => t!("column_fail"),
+            Self::Floss => t!("column_floss"),
+            Self::Bloss => t!("column_bloss"),
+            Self::FlossPct => t!("column_floss_pct"),
         }
     }
 
@@ -319,6 +335,9 @@ impl ColumnType {
             Self::LastIcmpPacketCode => ColumnWidth::Fixed(width.max(7)),
             Self::LastNatStatus => ColumnWidth::Fixed(width.max(7)),
             Self::Failed => ColumnWidth::Fixed(width.max(7)),
+            Self::Floss => ColumnWidth::Fixed(width.max(7)),
+            Self::Bloss => ColumnWidth::Fixed(width.max(7)),
+            Self::FlossPct => ColumnWidth::Fixed(width.max(8)),
         }
     }
 }
@@ -379,6 +398,9 @@ mod tests {
                 Column::new_hidden(ColumnType::LastIcmpPacketCode),
                 Column::new_hidden(ColumnType::LastNatStatus),
                 Column::new_hidden(ColumnType::Failed),
+                Column::new_hidden(ColumnType::Floss),
+                Column::new_hidden(ColumnType::Bloss),
+                Column::new_hidden(ColumnType::FlossPct),
             ])
         );
     }
diff --git a/crates/trippy-tui/src/frontend/render/table.rs b/crates/trippy-tui/src/frontend/render/table.rs
index 22ccbf8b..c0c69538 100644
--- a/crates/trippy-tui/src/frontend/render/table.rs
+++ b/crates/trippy-tui/src/frontend/render/table.rs
@@ -151,7 +151,7 @@ fn new_cell(
             };
             host_cell
         }
-        ColumnType::LossPct => render_loss_pct_cell(hop),
+        ColumnType::LossPct => render_pct_cell(hop.loss_pct()),
         ColumnType::Sent => render_usize_cell(hop.total_sent()),
         ColumnType::Received => render_usize_cell(hop.total_recv()),
         ColumnType::Failed => render_usize_cell(hop.total_failed()),
@@ -171,6 +171,9 @@ fn new_cell(
         ColumnType::LastIcmpPacketType => render_icmp_packet_type_cell(hop.last_icmp_packet_type()),
         ColumnType::LastIcmpPacketCode => render_icmp_packet_code_cell(hop.last_icmp_packet_type()),
         ColumnType::LastNatStatus => render_nat_cell(hop.last_nat_status()),
+        ColumnType::Floss => render_usize_cell(hop.total_forward_loss()),
+        ColumnType::Bloss => render_usize_cell(hop.total_backward_loss()),
+        ColumnType::FlossPct => render_pct_cell(hop.forward_loss_pct()),
     }
 }
 
@@ -186,8 +189,8 @@ fn render_nat_cell(value: NatStatus) -> Cell<'static> {
     })
 }
 
-fn render_loss_pct_cell(hop: &Hop) -> Cell<'static> {
-    Cell::from(format!("{:.1}%", hop.loss_pct()))
+fn render_pct_cell(value: f64) -> Cell<'static> {
+    Cell::from(format!("{value:.1}%"))
 }
 
 fn render_avg_cell(hop: &Hop) -> Cell<'static> {
diff --git a/trippy-config-sample.toml b/trippy-config-sample.toml
index 64305c9d..a8074b67 100644
--- a/trippy-config-sample.toml
+++ b/trippy-config-sample.toml
@@ -282,6 +282,9 @@ tui-as-mode = "asn"
 #   C - Last icmp packet code
 #   N - Last NAT status
 #   f - Probes failed
+#   F = Forward loss
+#   B = Backward loss
+#   D = Forward loss %
 #
 # The columns will be shown in the order specified.
 tui-custom-columns = "holsravbwdt"


Test patch:
diff --git a/crates/trippy-core/tests/resources/state/floss_bloss.yaml b/crates/trippy-core/tests/resources/state/floss_bloss.yaml
new file mode 100644
index 00000000..91dbf87e
--- /dev/null
+++ b/crates/trippy-core/tests/resources/state/floss_bloss.yaml
@@ -0,0 +1,32 @@
+# Test forward and backward loss.
+largest_ttl: 3
+rounds:
+  - probes:
+      - 1 C 333 10.1.0.1 0 12340 80 0 0
+      - 2 C 777 10.1.0.2 1 12340 80 0 0
+      - 3 C 778 10.1.0.3 2 12340 80 0 0
+  - probes:
+      - 1 C 333 10.1.0.1 3 12340 80 0 0
+      - 2 A 777 10.1.0.2 4 12340 80 0 0
+      - 3 A 778 10.1.0.3 5 12340 80 0 0
+  - probes:
+      - 1 C 333 10.1.0.1 6 12340 80 0 0
+      - 2 C 777 10.1.0.2 7 12340 80 0 0
+      - 3 C 778 10.1.0.3 8 12340 80 0 0
+expected:
+  hops:
+    - ttl: 1
+      total_sent: 3
+      total_recv: 3
+      total_forward_loss: 0
+      total_backward_loss: 0
+    - ttl: 2
+      total_sent: 3
+      total_recv: 2
+      total_forward_loss: 1
+      total_backward_loss: 0
+    - ttl: 3
+      total_sent: 3
+      total_recv: 2
+      total_forward_loss: 0
+      total_backward_loss: 1