Exit nodes: block IPv6 while one is in use; exit icon on the network tile

IPv6 has no way through an exit node, so the same policy rules get an
unreachable default for IPv6 instead of leaking it. The tray shows a green
icon in the tile corner while an exit node is in use, with a one-line
confirmation to stop it; confirmations are shortened.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Aleksandr BogomiakovandClaude committed 2026-10-03 02:09:14 +01:00
1 parent b2431d7eb1
commit bb38562b0a
5 files changed
+228 -67

No files matched your search

+49 -44
View File
@@ -48,6 +48,21 @@ pub(crate) struct ExitConfirm {
name: String,
/// Stop using it, rather than start.
stop: bool,
/// Asked in the main window, not in the devices window of the network.
main: bool,
}
impl ExitConfirm {
/// A question about stopping the exit node a network's tile shows.
pub(crate) fn stop_from_tile(network: &NetworkReport, peer_id: &str) -> Self {
Self {
network_id: network.network_id.clone(),
peer_id: peer_id.to_string(),
name: exit::name_of(network, peer_id),
stop: true,
main: true,
}
}
}
impl Device<'_> {
@@ -126,18 +141,24 @@ pub(crate) fn show(
device_table(ui, unit, network, &devices, traffic, confirm);
});
});
confirm_exit(ctx, agent, confirm, &network.network_id);
confirm_exit(ctx, agent, confirm, Some(&network.network_id));
}
/// The confirmation for an exit-node choice, drawn in the window it was
/// made in. Everything behind it is blocked until it is answered.
fn confirm_exit(
///
/// `network_id` is the devices window the question is drawn in, or `None` for
/// the main window; a question appears in the window it was asked in only.
pub(crate) fn confirm_exit(
ctx: &egui::Context,
agent: &AgentClient,
confirm: &mut Option<ExitConfirm>,
network_id: &str,
network_id: Option<&str>,
) {
let Some(pending) = confirm.as_ref().filter(|c| c.network_id == network_id) else {
let Some(pending) = confirm.as_ref().filter(|c| match network_id {
Some(id) => !c.main && c.network_id == id,
None => c.main,
}) else {
return;
};
egui::Area::new(egui::Id::new("exit confirm backdrop"))
@@ -151,48 +172,31 @@ fn confirm_exit(
});
let (mut accepted, mut cancelled) = (false, false);
egui::Window::new(if pending.stop {
"Stop using exit node"
} else {
"Use as exit node"
})
.order(egui::Order::Foreground)
.collapsible(false)
.resizable(false)
.anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
.show(ctx, |ui| {
ui.set_max_width(380.0);
if pending.stop {
ui.label(format!("Stop using {} as exit node?", pending.name));
ui.label("Your internet traffic will leave this device the ordinary way again.");
} else {
ui.label(
egui::RichText::new(format!(
"Send ALL your internet traffic through {}?",
pending.name
))
.strong(),
);
ui.label(
"Everything not on this network will leave through that device, which can \
see it.",
);
}
ui.add_space(8.0);
ui.horizontal(|ui| {
let label = if pending.stop {
"Stop"
egui::Window::new("Exit node")
.order(egui::Order::Foreground)
.collapsible(false)
.resizable(false)
.anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
.show(ctx, |ui| {
ui.set_max_width(380.0);
ui.label(if pending.stop {
format!("Stop using {}?", pending.name)
} else {
"Use as exit node"
};
if ui.button(label).clicked() {
accepted = true;
}
if ui.button("Cancel").clicked() {
cancelled = true;
}
format!("Use {} as exit node?", pending.name)
});
ui.add_space(8.0);
ui.horizontal(|ui| {
if ui
.button(if pending.stop { "Stop" } else { "Use" })
.clicked()
{
accepted = true;
}
if ui.button("Cancel").clicked() {
cancelled = true;
}
});
});
});
if accepted {
agent.send(Command::SetExitNode {
network_id: pending.network_id.clone(),
@@ -431,6 +435,7 @@ fn exit_cell(
peer_id: device.id.to_string(),
name: device.name(),
stop: device.in_use,
main: false,
});
}
}
+27 -14
View File
@@ -128,6 +128,8 @@ pub(crate) fn draw(
});
}
});
devices::confirm_exit(ctx, agent, &mut state.exit_confirm, None);
}
/// The "agent cannot be reached" state.
@@ -281,21 +283,32 @@ fn draw_network(
}
});
ui.horizontal(|ui| {
if let Some(via) = &network.exit.via {
ui.weak("exit");
ui.label(exit::name_of(network, via));
if ui
.small_button("off")
.on_hover_text("send internet traffic the ordinary way again")
.clicked()
{
agent.send(Command::SetExitNode {
network_id: network.network_id.clone(),
peer: None,
});
}
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
// In the corner while this device sends its traffic through
// somebody: green while that works, red while it does not.
if let Some(via) = &network.exit.via {
let color = if network.exit.via_online {
exit::GOOD
} else {
exit::LOUD
};
let response = ui
.add(
egui::Label::new(
egui::RichText::new(icon::SIGN_OUT).size(18.0).color(color),
)
.sense(egui::Sense::click()),
)
.on_hover_cursor(egui::CursorIcon::PointingHand)
.on_hover_text(format!(
"exit node: {} — click to stop",
exit::name_of(network, via)
));
if response.clicked() {
state.exit_confirm =
Some(devices::ExitConfirm::stop_from_tile(network, via));
}
}
if ui.button("Show devices").clicked() {
state.open_devices.insert(network.network_id.clone());
}