Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
781e359775 | ||
|
|
d5e02ec256 |
No files matched your search
@@ -1 +1,2 @@
|
|||||||
/target
|
/target
|
||||||
|
/.claude
|
||||||
@@ -42,7 +42,8 @@ struct Device<'a> {
|
|||||||
copy: Option<String>,
|
copy: Option<String>,
|
||||||
/// The full endpoint id, which clicking the short form copies.
|
/// The full endpoint id, which clicking the short form copies.
|
||||||
id: &'a str,
|
id: &'a str,
|
||||||
proto: &'a str,
|
/// End-to-end tunnel protocol, independent of the path carrying it.
|
||||||
|
tunnel_protocol: &'a str,
|
||||||
address: Option<String>,
|
address: Option<String>,
|
||||||
handshake_secs: Option<u64>,
|
handshake_secs: Option<u64>,
|
||||||
overlay: Option<&'a OverlayPeerReport>,
|
overlay: Option<&'a OverlayPeerReport>,
|
||||||
@@ -360,7 +361,7 @@ fn devices<'a>(network: &'a NetworkReport, own_id: &str) -> Vec<Device<'a>> {
|
|||||||
hostname,
|
hostname,
|
||||||
copy: hostname.map(|h| format!("{h}.{}", network.name)),
|
copy: hostname.map(|h| format!("{h}.{}", network.name)),
|
||||||
id,
|
id,
|
||||||
proto: overlay.map_or("—", |p| p.protocol.as_str()),
|
tunnel_protocol: overlay.map_or("—", |p| p.protocol.as_str()),
|
||||||
address: overlay
|
address: overlay
|
||||||
.and_then(|p| p.address.clone())
|
.and_then(|p| p.address.clone())
|
||||||
.or_else(|| member.and_then(|m| m.overlay_address_v4.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] = [
|
const COLUMN_SAMPLES: [&str; 9] = [
|
||||||
"",
|
"",
|
||||||
"exit",
|
"exit",
|
||||||
"tcp-tls",
|
"wg-quic · waiting for handshake",
|
||||||
"255.255.255.255",
|
"255.255.255.255",
|
||||||
"23h 59m",
|
"direct over tcp-tls",
|
||||||
"99999/99999",
|
"99999/99999",
|
||||||
"1023.9 MB / 1023.9 MB",
|
"1023.9 MB / 1023.9 MB",
|
||||||
"1023.9 kB/s",
|
"1023.9 kB/s",
|
||||||
@@ -506,13 +507,36 @@ fn device_table(
|
|||||||
.spacing([10.0, 4.0])
|
.spacing([10.0, 4.0])
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
for (column, header) in [
|
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()
|
.into_iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
{
|
{
|
||||||
cell(ui, column, &mut widths, |ui| {
|
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();
|
ui.end_row();
|
||||||
@@ -523,7 +547,7 @@ fn device_table(
|
|||||||
exit_cell(ui, device, network_id, confirm);
|
exit_cell(ui, device, network_id, confirm);
|
||||||
});
|
});
|
||||||
cell(ui, 2, &mut widths, |ui| {
|
cell(ui, 2, &mut widths, |ui| {
|
||||||
ui.label(device.proto);
|
tunnel_cell(ui, device);
|
||||||
});
|
});
|
||||||
cell(ui, 3, &mut widths, |ui| match &device.address {
|
cell(ui, 3, &mut widths, |ui| match &device.address {
|
||||||
Some(address) => format::copy_field(ui, address, address),
|
Some(address) => format::copy_field(ui, address, address),
|
||||||
@@ -531,7 +555,7 @@ fn device_table(
|
|||||||
ui.weak("—");
|
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) {
|
match device.overlay.filter(|_| device.online) {
|
||||||
Some(peer) => {
|
Some(peer) => {
|
||||||
@@ -609,45 +633,95 @@ fn device_table(
|
|||||||
ui.memory_mut(|m| m.data.insert_temp(widths_id, widths));
|
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
|
/// 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.
|
/// so: the colour is the only difference in how it is treated.
|
||||||
fn link_cell(ui: &mut egui::Ui, device: &Device<'_>) {
|
fn data_path_cell(ui: &mut egui::Ui, device: &Device<'_>) {
|
||||||
if !device.online {
|
let (text, good) = data_path_text(device.path, device.via_name.as_deref(), 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,
|
|
||||||
};
|
|
||||||
let response = if good {
|
let response = if good {
|
||||||
ui.label(text)
|
ui.label(text)
|
||||||
} else {
|
} else {
|
||||||
ui.label(egui::RichText::new(text).color(AMBER))
|
ui.label(egui::RichText::new(text).color(AMBER))
|
||||||
};
|
};
|
||||||
if !good {
|
response.on_hover_text(match device.path {
|
||||||
response.on_hover_text(match device.path {
|
Some(DataPathReport::Direct { .. }) => {
|
||||||
Some(DataPathReport::Relayed { .. }) => {
|
"One Tsunagi hop with no mesh relay. The end-to-end tunnel may use a different \
|
||||||
"no direct link yet: traffic goes through another device, and moves to a \
|
protocol from this transport."
|
||||||
direct link by itself as soon as one comes up"
|
}
|
||||||
}
|
Some(DataPathReport::Relayed { .. }) => {
|
||||||
_ => "connected, but no path carries packets yet",
|
"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 {
|
fn yes_no(value: bool) -> &'static str {
|
||||||
@@ -726,4 +800,44 @@ mod tests {
|
|||||||
.is_none()
|
.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");
|
bytes[28..].copy_from_slice(b"LAN GAME");
|
||||||
// UDP checksum zero is valid for IPv4.
|
// UDP checksum zero is valid for IPv4.
|
||||||
let sum: u32 = bytes[..20]
|
let sum: u32 = bytes[..20]
|
||||||
.chunks_exact(2)
|
.as_chunks::<2>()
|
||||||
.map(|w| u16::from_be_bytes([w[0], w[1]]) as u32)
|
.0
|
||||||
|
.iter()
|
||||||
|
.map(|w| u16::from_be_bytes(*w) as u32)
|
||||||
.sum();
|
.sum();
|
||||||
let checksum = !((sum & 0xffff) + (sum >> 16)) as u16;
|
let checksum = !((sum & 0xffff) + (sum >> 16)) as u16;
|
||||||
bytes[10..12].copy_from_slice(&checksum.to_be_bytes());
|
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
|
// Online is online: a member reached through others is as much here as
|
||||||
// one reached directly, and is said to be reached that way.
|
// one reached directly, and is said to be reached that way.
|
||||||
let (path, data_path_good) = match row.path {
|
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, .. }) => (
|
Some(DataPathReport::Relayed { hops, .. }) => (
|
||||||
format!(
|
format!(
|
||||||
"relayed via {} ({hops} hops)",
|
"data path via {} · {hops} hops",
|
||||||
row.via_label.as_deref().unwrap_or("another member")
|
row.via_label.as_deref().unwrap_or("another member")
|
||||||
),
|
),
|
||||||
false,
|
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();
|
let mut detail = String::new();
|
||||||
@@ -3643,7 +3645,7 @@ fn member_row(row: &MemberRow<'_>) -> report::Row {
|
|||||||
}
|
}
|
||||||
detail.push_str(&path);
|
detail.push_str(&path);
|
||||||
if let Some(transport) = row.transport {
|
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 {
|
if let Some(rtt) = row.rtt_ms {
|
||||||
detail.push_str(&format!(" rtt {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 {
|
out = out.with_note(match tunnel.handshake_secs_ago {
|
||||||
Some(secs) => {
|
Some(secs) => {
|
||||||
let stats = format!(
|
let health = if proto == "tcp-tls" {
|
||||||
"tunnel up ({proto}), {} tx {} rx {}{}",
|
format!("uptime {secs}s")
|
||||||
if proto == "tcp-tls" {
|
} else {
|
||||||
format!("uptime {secs}s,")
|
format!("handshake {secs}s ago")
|
||||||
} else {
|
};
|
||||||
format!("handshake {secs}s ago,")
|
format!(
|
||||||
},
|
"end-to-end tunnel {proto} · {health} · tx {} rx {}{} · transport detail: {}",
|
||||||
tunnel.tx_packets,
|
tunnel.tx_packets,
|
||||||
tunnel.rx_packets,
|
tunnel.rx_packets,
|
||||||
if tunnel.dropped > 0 {
|
if tunnel.dropped > 0 {
|
||||||
@@ -3684,14 +3686,20 @@ fn member_row(row: &MemberRow<'_>) -> report::Row {
|
|||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
},
|
},
|
||||||
);
|
tunnel.path
|
||||||
format!("{stats:<48} · {}", tunnel.path)
|
)
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
if proto == "wg-quic" || proto == "wg" {
|
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 {
|
} 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);
|
let text = out.render(false);
|
||||||
assert!(text.contains("2 of 2 online"), "{text}");
|
assert!(text.contains("2 of 2 online"), "{text}");
|
||||||
assert!(
|
assert!(
|
||||||
text.contains("relayed via music (2 hops)"),
|
text.contains("data path via music · 2 hops"),
|
||||||
"names the member it goes through: {text}"
|
"names the member it goes through: {text}"
|
||||||
);
|
);
|
||||||
assert!(!text.contains("offline"), "{text}");
|
assert!(!text.contains("offline"), "{text}");
|
||||||
@@ -5907,10 +5915,40 @@ mod status_tests {
|
|||||||
out.push(network_section(&network, OWN, false));
|
out.push(network_section(&network, OWN, false));
|
||||||
let text = out.render(false);
|
let text = out.render(false);
|
||||||
assert_eq!(out.worst(), Health::Good, "{text}");
|
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("uptime 42s"), "{text}");
|
||||||
assert!(text.contains("tx 100 rx 200"), "{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