Exit nodes: offer one per network, send all traffic through a member

A member can offer itself as an exit node per network (off by default); it is
announced over the control channel (ctrl/4) once its iptables masquerade and
forward rules are in place, with a warning when kernel forwarding is off. A
device picks one member as its exit node via policy routing, with the agent's
own uid exempt from the tunnel; if the exit node disappears the rules stay
and traffic is dropped, loudly reported. CLI: `network exit-node`,
`exit-node`; tray: tile checkbox, warnings and a per-device icon with
confirmation. Linux only.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Aleksandr BogomiakovandClaude committed 2026-10-03 01:14:51 +01:00
1 parent c62d29fa14
commit b2431d7eb1
34 files changed
+4148 -61

No files matched your search

+43
View File
@@ -418,3 +418,46 @@ fn ambiguous_and_unknown_names_leave_every_network_untouched() {
assert_eq!(remaining.len(), 1);
assert_eq!(remaining[0].network_id, before[1].network_id);
}
#[test]
fn an_exit_node_is_offered_per_network_only_when_asked_and_nobody_offers_by_default() {
let agent = start_bare(0);
assert!(agent.run(&["join", "-n", "exit-offer"]).status.success());
let offered = || {
read_networks(&agent.dir)
.iter()
.find(|n| n.name.as_str() == "exit-offer")
.unwrap()
.exit_node
};
assert!(!offered(), "off unless somebody turns it on");
let on = agent.run(&["network", "exit-node", "exit-offer", "on"]);
assert!(
on.status.success(),
"{}",
String::from_utf8_lossy(&on.stderr)
);
assert!(String::from_utf8_lossy(&on.stdout).contains("exit node on"));
assert!(offered());
let off = agent.run(&["network", "exit-node", "exit-offer", "off"]);
assert!(off.status.success());
assert!(!offered());
let listed = agent.run(&["exit-node"]);
assert!(listed.status.success());
assert!(
String::from_utf8_lossy(&listed.stdout).contains("no connected device offers"),
"{}",
String::from_utf8_lossy(&listed.stdout)
);
let unknown = agent.run(&["exit-node", "nobody"]);
assert!(!unknown.status.success());
assert!(
String::from_utf8_lossy(&unknown.stderr).contains("no connected device matches"),
"{}",
String::from_utf8_lossy(&unknown.stderr)
);
}