Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
781e359775 | ||
|
|
d5e02ec256 |
No files matched your search
@@ -1 +1,2 @@
|
||||
/target
|
||||
/.claude
|
||||
@@ -42,7 +42,8 @@ struct Device<'a> {
|
||||
copy: Option<String>,
|
||||
/// The full endpoint id, which clicking the short form copies.
|
||||
id: &'a str,
|
||||
proto: &'a str,
|
||||
/// End-to-end tunnel protocol, independent of the path carrying it.
|
||||
tunnel_protocol: &'a str,
|
||||
address: Option<String>,
|
||||
handshake_secs: Option<u64>,
|
||||
overlay: Option<&'a OverlayPeerReport>,
|
||||
@@ -360,7 +361,7 @@ fn devices<'a>(network: &'a NetworkReport, own_id: &str) -> Vec<Device<'a>> {
|
||||
hostname,
|
||||
copy: hostname.map(|h| format!("{h}.{}", network.name)),
|
||||
id,
|
||||
proto: overlay.map_or("—", |p| p.protocol.as_str()),
|
||||
tunnel_protocol: overlay.map_or("—", |p| p.protocol.as_str()),
|
||||
address: overlay
|
||||
.and_then(|p| p.address.clone())
|
||||
.or_else(|| member.and_then(|m| m.overlay_address_v4.clone())),
|
||||
@@ -389,9 +390,9 @@ fn devices<'a>(network: &'a NetworkReport, own_id: &str) -> Vec<Device<'a>> {
|
||||
const COLUMN_SAMPLES: [&str; 9] = [
|
||||
"",
|
||||
"exit",
|
||||
"tcp-tls",
|
||||
"wg-quic · waiting for handshake",
|
||||
"255.255.255.255",
|
||||
"23h 59m",
|
||||
"direct over tcp-tls",
|
||||
"99999/99999",
|
||||
"1023.9 MB / 1023.9 MB",
|
||||
"1023.9 kB/s",
|
||||
@@ -506,13 +507,36 @@ fn device_table(
|
||||
.spacing([10.0, 4.0])
|
||||
.show(ui, |ui| {
|
||||
for (column, header) in [
|
||||
"device", "exit", "proto", "address", "link", "pkts", "bytes", "rate", "plot",
|
||||
"device",
|
||||
"exit",
|
||||
"end-to-end",
|
||||
"address",
|
||||
"data path",
|
||||
"pkts",
|
||||
"bytes",
|
||||
"rate",
|
||||
"plot",
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
cell(ui, column, &mut widths, |ui| {
|
||||
ui.weak(header);
|
||||
let response = ui.weak(header);
|
||||
match column {
|
||||
2 => {
|
||||
response.on_hover_text(
|
||||
"Protocol that encrypts traffic between this device and the \
|
||||
destination. It is independent of the route carrying it.",
|
||||
);
|
||||
}
|
||||
4 => {
|
||||
response.on_hover_text(
|
||||
"Current route carrying the encrypted tunnel traffic. `direct \
|
||||
over tcp-tls` means one Tsunagi hop with no mesh relay.",
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
ui.end_row();
|
||||
@@ -523,7 +547,7 @@ fn device_table(
|
||||
exit_cell(ui, device, network_id, confirm);
|
||||
});
|
||||
cell(ui, 2, &mut widths, |ui| {
|
||||
ui.label(device.proto);
|
||||
tunnel_cell(ui, device);
|
||||
});
|
||||
cell(ui, 3, &mut widths, |ui| match &device.address {
|
||||
Some(address) => format::copy_field(ui, address, address),
|
||||
@@ -531,7 +555,7 @@ fn device_table(
|
||||
ui.weak("—");
|
||||
}
|
||||
});
|
||||
cell(ui, 4, &mut widths, |ui| link_cell(ui, device));
|
||||
cell(ui, 4, &mut widths, |ui| data_path_cell(ui, device));
|
||||
|
||||
match device.overlay.filter(|_| device.online) {
|
||||
Some(peer) => {
|
||||
@@ -609,45 +633,95 @@ fn device_table(
|
||||
ui.memory_mut(|m| m.data.insert_temp(widths_id, widths));
|
||||
}
|
||||
|
||||
/// How a device is reached, and how long its tunnel has been up.
|
||||
/// A protocol name as it should appear in the table.
|
||||
fn displayed_protocol(protocol: &str) -> &str {
|
||||
if protocol.is_empty() {
|
||||
"wg-quic"
|
||||
} else {
|
||||
protocol
|
||||
}
|
||||
}
|
||||
|
||||
/// The end-to-end tunnel and its own health, without transport details.
|
||||
fn tunnel_text(protocol: &str, handshake_secs: Option<u64>) -> String {
|
||||
let protocol = displayed_protocol(protocol);
|
||||
if protocol == "—" {
|
||||
return protocol.to_string();
|
||||
}
|
||||
match handshake_secs {
|
||||
Some(secs) if protocol == "tcp-tls" => {
|
||||
format!("{protocol} · up {}", format::age(secs))
|
||||
}
|
||||
Some(secs) => format!("{protocol} · handshake {} ago", format::age(secs)),
|
||||
None => format!("{protocol} · waiting for handshake"),
|
||||
}
|
||||
}
|
||||
|
||||
fn tunnel_cell(ui: &mut egui::Ui, device: &Device<'_>) {
|
||||
let protocol = displayed_protocol(device.tunnel_protocol);
|
||||
let text = tunnel_text(protocol, device.handshake_secs);
|
||||
if protocol == "—" {
|
||||
ui.weak(text);
|
||||
return;
|
||||
}
|
||||
let hint = match device.path {
|
||||
Some(DataPathReport::Direct { transport }) => format!(
|
||||
"The end-to-end {protocol} tunnel is currently carried over a direct {transport} \
|
||||
transport."
|
||||
),
|
||||
Some(DataPathReport::Relayed { hops, .. }) => format!(
|
||||
"The end-to-end {protocol} tunnel is currently carried through other devices over \
|
||||
{hops} transport hops."
|
||||
),
|
||||
Some(DataPathReport::None) | None => {
|
||||
format!("The end-to-end {protocol} tunnel does not have a data path carrying it yet.")
|
||||
}
|
||||
};
|
||||
ui.label(text).on_hover_text(hint);
|
||||
}
|
||||
|
||||
/// Current route text and whether that route is the preferred direct form.
|
||||
fn data_path_text(
|
||||
path: Option<&DataPathReport>,
|
||||
via_name: Option<&str>,
|
||||
online: bool,
|
||||
) -> (String, bool) {
|
||||
if !online {
|
||||
return ("offline".into(), false);
|
||||
}
|
||||
match path {
|
||||
Some(DataPathReport::Direct { transport }) => (format!("direct over {transport}"), true),
|
||||
Some(DataPathReport::Relayed { hops, .. }) => (
|
||||
format!("via {} · {hops} hops", via_name.unwrap_or("another device")),
|
||||
false,
|
||||
),
|
||||
Some(DataPathReport::None) | None => ("no data path yet".into(), false),
|
||||
}
|
||||
}
|
||||
|
||||
/// How a device is reached right now.
|
||||
///
|
||||
/// A device reached through others is online, over a longer path, and says
|
||||
/// so: the colour is the only difference in how it is treated.
|
||||
fn link_cell(ui: &mut egui::Ui, device: &Device<'_>) {
|
||||
if !device.online {
|
||||
ui.label("offline");
|
||||
return;
|
||||
}
|
||||
let (path, good) = match device.path {
|
||||
Some(DataPathReport::Direct { transport }) => (format!("direct · {transport}"), true),
|
||||
Some(DataPathReport::Relayed { hops, .. }) => (
|
||||
format!(
|
||||
"via {} · {hops} hops",
|
||||
device.via_name.as_deref().unwrap_or("another device")
|
||||
),
|
||||
false,
|
||||
),
|
||||
Some(DataPathReport::None) | None => ("no data path".to_string(), false),
|
||||
};
|
||||
let age = device.handshake_secs.map(format::age);
|
||||
let text = match age {
|
||||
Some(age) => format!("{path} · {age}"),
|
||||
None => path,
|
||||
};
|
||||
fn data_path_cell(ui: &mut egui::Ui, device: &Device<'_>) {
|
||||
let (text, good) = data_path_text(device.path, device.via_name.as_deref(), device.online);
|
||||
let response = if good {
|
||||
ui.label(text)
|
||||
} else {
|
||||
ui.label(egui::RichText::new(text).color(AMBER))
|
||||
};
|
||||
if !good {
|
||||
response.on_hover_text(match device.path {
|
||||
Some(DataPathReport::Relayed { .. }) => {
|
||||
"no direct link yet: traffic goes through another device, and moves to a \
|
||||
direct link by itself as soon as one comes up"
|
||||
}
|
||||
_ => "connected, but no path carries packets yet",
|
||||
});
|
||||
}
|
||||
response.on_hover_text(match device.path {
|
||||
Some(DataPathReport::Direct { .. }) => {
|
||||
"One Tsunagi hop with no mesh relay. The end-to-end tunnel may use a different \
|
||||
protocol from this transport."
|
||||
}
|
||||
Some(DataPathReport::Relayed { .. }) => {
|
||||
"No direct transport yet: traffic goes through another device, and moves to a \
|
||||
direct path by itself as soon as one comes up."
|
||||
}
|
||||
_ if device.online => "Connected, but no data path carries packets yet.",
|
||||
_ => "This device is not reachable now.",
|
||||
});
|
||||
}
|
||||
|
||||
fn yes_no(value: bool) -> &'static str {
|
||||
@@ -726,4 +800,44 @@ mod tests {
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tunnel_and_direct_transport_are_named_as_different_layers() {
|
||||
assert_eq!(tunnel_text("wg", Some(1)), "wg · handshake 1s ago");
|
||||
assert_eq!(
|
||||
data_path_text(
|
||||
Some(&DataPathReport::Direct {
|
||||
transport: "tcp-tls".into(),
|
||||
}),
|
||||
None,
|
||||
true,
|
||||
),
|
||||
("direct over tcp-tls".into(), true)
|
||||
);
|
||||
assert_eq!(tunnel_text("tcp-tls", Some(42)), "tcp-tls · up 42s");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incomplete_and_longer_paths_stay_explicit() {
|
||||
assert_eq!(
|
||||
tunnel_text("wg-quic", None),
|
||||
"wg-quic · waiting for handshake"
|
||||
);
|
||||
assert_eq!(
|
||||
data_path_text(
|
||||
Some(&DataPathReport::Relayed {
|
||||
hops: 2,
|
||||
via: "near".into(),
|
||||
}),
|
||||
Some("iris"),
|
||||
true,
|
||||
),
|
||||
("via iris · 2 hops".into(), false)
|
||||
);
|
||||
assert_eq!(
|
||||
data_path_text(Some(&DataPathReport::None), None, true),
|
||||
("no data path yet".into(), false)
|
||||
);
|
||||
assert_eq!(data_path_text(None, None, false), ("offline".into(), false));
|
||||
}
|
||||
}
|
||||
@@ -1661,8 +1661,10 @@ async fn lan_broadcast_fanout_is_scoped_opt_in_and_does_not_reflood() {
|
||||
bytes[28..].copy_from_slice(b"LAN GAME");
|
||||
// UDP checksum zero is valid for IPv4.
|
||||
let sum: u32 = bytes[..20]
|
||||
.chunks_exact(2)
|
||||
.map(|w| u16::from_be_bytes([w[0], w[1]]) as u32)
|
||||
.as_chunks::<2>()
|
||||
.0
|
||||
.iter()
|
||||
.map(|w| u16::from_be_bytes(*w) as u32)
|
||||
.sum();
|
||||
let checksum = !((sum & 0xffff) + (sum >> 16)) as u16;
|
||||
bytes[10..12].copy_from_slice(&checksum.to_be_bytes());
|
||||
|
||||
+56
-18
@@ -3626,15 +3626,17 @@ fn member_row(row: &MemberRow<'_>) -> report::Row {
|
||||
// Online is online: a member reached through others is as much here as
|
||||
// one reached directly, and is said to be reached that way.
|
||||
let (path, data_path_good) = match row.path {
|
||||
Some(DataPathReport::Direct { transport }) => (format!("direct {transport}"), true),
|
||||
Some(DataPathReport::Direct { transport }) => {
|
||||
(format!("data path direct over {transport}"), true)
|
||||
}
|
||||
Some(DataPathReport::Relayed { hops, .. }) => (
|
||||
format!(
|
||||
"relayed via {} ({hops} hops)",
|
||||
"data path via {} · {hops} hops",
|
||||
row.via_label.as_deref().unwrap_or("another member")
|
||||
),
|
||||
false,
|
||||
),
|
||||
Some(DataPathReport::None) | None => ("connected, no data path yet".to_string(), false),
|
||||
Some(DataPathReport::None) | None => ("connected · no data path yet".to_string(), false),
|
||||
};
|
||||
|
||||
let mut detail = String::new();
|
||||
@@ -3643,7 +3645,7 @@ fn member_row(row: &MemberRow<'_>) -> report::Row {
|
||||
}
|
||||
detail.push_str(&path);
|
||||
if let Some(transport) = row.transport {
|
||||
detail.push_str(&format!(" · control {}", transport.to_lowercase()));
|
||||
detail.push_str(&format!(" · control path {}", transport.to_lowercase()));
|
||||
}
|
||||
if let Some(rtt) = row.rtt_ms {
|
||||
detail.push_str(&format!(" rtt {rtt}ms"));
|
||||
@@ -3670,13 +3672,13 @@ fn member_row(row: &MemberRow<'_>) -> report::Row {
|
||||
};
|
||||
out = out.with_note(match tunnel.handshake_secs_ago {
|
||||
Some(secs) => {
|
||||
let stats = format!(
|
||||
"tunnel up ({proto}), {} tx {} rx {}{}",
|
||||
if proto == "tcp-tls" {
|
||||
format!("uptime {secs}s,")
|
||||
} else {
|
||||
format!("handshake {secs}s ago,")
|
||||
},
|
||||
let health = if proto == "tcp-tls" {
|
||||
format!("uptime {secs}s")
|
||||
} else {
|
||||
format!("handshake {secs}s ago")
|
||||
};
|
||||
format!(
|
||||
"end-to-end tunnel {proto} · {health} · tx {} rx {}{} · transport detail: {}",
|
||||
tunnel.tx_packets,
|
||||
tunnel.rx_packets,
|
||||
if tunnel.dropped > 0 {
|
||||
@@ -3684,14 +3686,20 @@ fn member_row(row: &MemberRow<'_>) -> report::Row {
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
);
|
||||
format!("{stats:<48} · {}", tunnel.path)
|
||||
tunnel.path
|
||||
)
|
||||
}
|
||||
None => {
|
||||
if proto == "wg-quic" || proto == "wg" {
|
||||
"no WireGuard handshake yet; the tunnel cannot carry traffic".to_string()
|
||||
format!(
|
||||
"end-to-end tunnel {proto} · no WireGuard handshake yet; the tunnel \
|
||||
cannot carry traffic"
|
||||
)
|
||||
} else {
|
||||
format!("no {proto} handshake yet; the tunnel cannot carry traffic")
|
||||
format!(
|
||||
"end-to-end tunnel {proto} · no {proto} handshake yet; the tunnel \
|
||||
cannot carry traffic"
|
||||
)
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -5814,7 +5822,7 @@ mod status_tests {
|
||||
let text = out.render(false);
|
||||
assert!(text.contains("2 of 2 online"), "{text}");
|
||||
assert!(
|
||||
text.contains("relayed via music (2 hops)"),
|
||||
text.contains("data path via music · 2 hops"),
|
||||
"names the member it goes through: {text}"
|
||||
);
|
||||
assert!(!text.contains("offline"), "{text}");
|
||||
@@ -5907,10 +5915,40 @@ mod status_tests {
|
||||
out.push(network_section(&network, OWN, false));
|
||||
let text = out.render(false);
|
||||
assert_eq!(out.worst(), Health::Good, "{text}");
|
||||
assert!(text.contains("tunnel up (tcp-tls)"), "{text}");
|
||||
assert!(text.contains("end-to-end tunnel tcp-tls"), "{text}");
|
||||
assert!(text.contains("uptime 42s"), "{text}");
|
||||
assert!(text.contains("tx 100 rx 200"), "{text}");
|
||||
assert!(text.contains("tcp-tls via 192.0.2.1:443"), "{text}");
|
||||
assert!(
|
||||
text.contains("transport detail: tcp-tls via 192.0.2.1:443"),
|
||||
"{text}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn end_to_end_tunnel_and_data_transport_are_named_separately() {
|
||||
let mut network = network_after_a_peer_returned();
|
||||
network.peers[0].path = tsunagi::ipc::DataPathReport::Direct {
|
||||
transport: "tcp-tls".into(),
|
||||
};
|
||||
network.members[1].path = network.peers[0].path.clone();
|
||||
let mut t = tunnel(ONLINE, Some(1));
|
||||
t.protocol = "wg".into();
|
||||
t.path = "tcp-tls via 192.0.2.1:443".into();
|
||||
network.overlay = Some(overlay(vec![t]));
|
||||
|
||||
let mut out = report::Report::new();
|
||||
out.push(network_section(&network, OWN, false));
|
||||
let text = out.render(false);
|
||||
assert!(text.contains("data path direct over tcp-tls"), "{text}");
|
||||
assert!(text.contains("control path direct"), "{text}");
|
||||
assert!(
|
||||
text.contains("end-to-end tunnel wg · handshake 1s ago"),
|
||||
"{text}"
|
||||
);
|
||||
assert!(
|
||||
text.contains("transport detail: tcp-tls via 192.0.2.1:443"),
|
||||
"{text}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user