Install broadcast route and firewall allowance on the host while broadcast is on
LAN discovery (CS 1.6 and similar) only worked after two manual steps on every host: route 255.255.255.255 through the TUN, and allow inbound traffic on it. Without the route the game's limited broadcast leaves via the physical adapter and never reaches the overlay; without the firewall allowance the server's unicast reply (from a different address than the one the request went to, so never matched by conntrack) is dropped before the game's socket. Verified by hand on Linux: with both in place the server answered A2S_INFO over the overlay. The system level (not a plugin) now installs exactly those two things for the interface it owns, and removes them when broadcast is turned off, the network is left or stopped, or the agent exits. Design: - New boundary overlay::hostrules (BroadcastHostRules: idempotent apply/clear, with Linux, Windows, Mock and Unsupported implementations). TunFactory gains host_rules(); ManagedTunFactory::with_host_rules wires it in the CLI. - Interface::sync_broadcast_rules reconciles the host against the routing table (RoutingTable::broadcast_host_target) and is called after address sync, on live `network broadcast on/off`, on network deactivation and by remove(). Failure is contained: the outcome is kept and logged, never fatal. - One interface carries several networks but the host has one 255.255.255.255 route, so the source is the lowest NetworkId among broadcast-enabled networks with an address (documented in docs/broadcast.md). - Linux: the route is added on the existing netlink thread (preferred source = overlay address, replace semantics). The firewall rule is `iptables -I INPUT -i <if> -p udp -s <overlay range>` tagged with a comment `tsunagi:<if>`; it is UDP-only and range-limited on purpose so TCP services stay unexposed. iptables is located in fixed system directories, never via PATH; stale tagged rules (e.g. from a crash) are replaced. A missing iptables leaves the route in place and is reported. rp_filter is not touched. - Windows: PowerShell (absolute path) New-NetRoute metric 1 and a tagged inbound UDP Defender rule scoped to the interface alias and overlay range. - status shows a `broadcast rules` row (ok, or which half failed and why). IPC control protocol bumped 14 -> 15 because OverlayReport gained a field. - AGENTS.md records the narrow exception to "never touch routing/firewall". Compatibility: restart the agent after upgrading (control protocol 15). No wire or schema change. Checks run (Linux only): cargo fmt --check, cargo clippy --locked --workspace --all-targets -D warnings, cargo test --locked --workspace --all-targets, all passing. Tests use a mock host and a scripted iptables; no OS setting is changed. NOT done: the Windows code was never compiled for or run on Windows (cross-compiling failed for lack of a C toolchain), only its script building is unit-tested; the real netlink route and iptables calls were not exercised against a live kernel in the suite; CS 1.6 was not launched from this change.
This commit is contained in:
1 parent
d2778f0b5a
commit
533921059a
18 files changed
+1834
-23
No files matched your search
@@ -2722,6 +2722,25 @@ fn network_section(
|
||||
"broadcast",
|
||||
if network.broadcast { "on" } else { "off" },
|
||||
));
|
||||
if network.broadcast
|
||||
&& let Some(rules) = network
|
||||
.overlay
|
||||
.as_ref()
|
||||
.and_then(|o| o.broadcast_rules.as_ref())
|
||||
{
|
||||
section.push(Row::new(
|
||||
if rules.ok {
|
||||
Health::Good
|
||||
} else {
|
||||
Health::Degraded
|
||||
},
|
||||
"broadcast rules",
|
||||
broadcast_rules_line(
|
||||
rules,
|
||||
network.overlay.as_ref().and_then(|o| o.address.as_deref()),
|
||||
),
|
||||
));
|
||||
}
|
||||
let relayed = network.relay_forwarded + network.relay_sent_via + network.relay_received_via;
|
||||
if relayed > 0 {
|
||||
section.push(Row::new(
|
||||
@@ -4087,6 +4106,14 @@ async fn build_report(
|
||||
unroutable_sample: overlay
|
||||
.as_ref()
|
||||
.and_then(|o| o.counters.unroutable_sample.map(|a| a.to_string())),
|
||||
broadcast_rules: overlay
|
||||
.as_ref()
|
||||
.and_then(|o| o.broadcast_rules.as_ref())
|
||||
.map(|rules| tsunagi::ipc::HostRulesReport {
|
||||
ok: rules.is_ok(),
|
||||
detail: rules.summary(),
|
||||
source: rules.plan.source.to_string(),
|
||||
}),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -4155,6 +4182,21 @@ async fn build_report(
|
||||
}
|
||||
}
|
||||
|
||||
/// The status line for the host rules, saying so when they are bound to
|
||||
/// another network's address.
|
||||
fn broadcast_rules_line(
|
||||
rules: &tsunagi::ipc::HostRulesReport,
|
||||
own_address: Option<&str>,
|
||||
) -> String {
|
||||
match own_address {
|
||||
Some(own) if own != rules.source => format!(
|
||||
"{} · route source is {}, another network's address",
|
||||
rules.detail, rules.source
|
||||
),
|
||||
_ => rules.detail.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves when the process is asked to stop.
|
||||
///
|
||||
/// Both Ctrl-C and `SIGTERM` are handled, so a service manager stopping the
|
||||
@@ -4193,16 +4235,22 @@ async fn stop_signal() -> &'static str {
|
||||
fn system_tun_factory() -> Result<Arc<dyn TunFactory>, Box<dyn std::error::Error>> {
|
||||
use tsunagi::overlay::{ManagedTunFactory, NetlinkProvisioner};
|
||||
let provisioner = NetlinkProvisioner::new()?;
|
||||
Ok(Arc::new(ManagedTunFactory::new(Arc::new(provisioner))))
|
||||
let rules = Arc::new(provisioner.host_rules());
|
||||
Ok(Arc::new(
|
||||
ManagedTunFactory::new(Arc::new(provisioner)).with_host_rules(rules),
|
||||
))
|
||||
}
|
||||
|
||||
/// The Wintun adapter, created and configured by the agent and removed when it
|
||||
/// exits, the same as the Linux one.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn system_tun_factory() -> Result<Arc<dyn TunFactory>, Box<dyn std::error::Error>> {
|
||||
use tsunagi::overlay::{ManagedTunFactory, WintunProvisioner};
|
||||
use tsunagi::overlay::{ManagedTunFactory, WindowsHostRules, WintunProvisioner};
|
||||
let provisioner = WintunProvisioner::new()?;
|
||||
Ok(Arc::new(ManagedTunFactory::new(Arc::new(provisioner))))
|
||||
Ok(Arc::new(
|
||||
ManagedTunFactory::new(Arc::new(provisioner))
|
||||
.with_host_rules(Arc::new(WindowsHostRules::new())),
|
||||
))
|
||||
}
|
||||
|
||||
/// There is no provisioner for this platform yet.
|
||||
@@ -4452,6 +4500,55 @@ mod status_tests {
|
||||
assert!(text.contains("mistyped secret"), "{text}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn broadcast_rules_are_reported_with_the_reason_when_incomplete() {
|
||||
let mut network = network_after_a_peer_returned();
|
||||
network.broadcast = true;
|
||||
let render = |network: &tsunagi::ipc::NetworkReport| {
|
||||
let mut out = report::Report::new();
|
||||
out.push(network_section(network, OWN, false));
|
||||
(out.render(false), out.worst())
|
||||
};
|
||||
|
||||
// Nothing installed (in-memory interface, or broadcast just off): no row.
|
||||
let (text, _) = render(&network);
|
||||
assert!(!text.contains("broadcast rules"), "{text}");
|
||||
|
||||
let overlay = network.overlay.as_mut().unwrap();
|
||||
overlay.broadcast_rules = Some(tsunagi::ipc::HostRulesReport {
|
||||
ok: true,
|
||||
detail: "ok (route + firewall)".into(),
|
||||
source: "10.13.37.69".into(),
|
||||
});
|
||||
let (text, worst) = render(&network);
|
||||
assert!(text.contains("broadcast rules"), "{text}");
|
||||
assert!(text.contains("ok (route + firewall)"), "{text}");
|
||||
assert_ne!(worst, Health::Degraded, "{text}");
|
||||
|
||||
let overlay = network.overlay.as_mut().unwrap();
|
||||
overlay.broadcast_rules = Some(tsunagi::ipc::HostRulesReport {
|
||||
ok: false,
|
||||
detail: "incomplete; route ok; firewall failed: iptables not found".into(),
|
||||
source: "10.13.37.69".into(),
|
||||
});
|
||||
let (text, worst) = render(&network);
|
||||
assert!(
|
||||
text.contains("firewall failed: iptables not found"),
|
||||
"{text}"
|
||||
);
|
||||
assert_eq!(worst, Health::Degraded, "{text}");
|
||||
|
||||
// Bound to another network's address: said, not hidden.
|
||||
network.overlay.as_mut().unwrap().address = Some("10.99.0.5".into());
|
||||
let (text, _) = render(&network);
|
||||
assert!(text.contains("another network's address"), "{text}");
|
||||
|
||||
// Broadcast off for this network: the line is not shown.
|
||||
network.broadcast = false;
|
||||
let (text, _) = render(&network);
|
||||
assert!(!text.contains("broadcast rules"), "{text}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_agent_with_nobody_to_contact_says_so_instead_of_waiting_quietly() {
|
||||
// The wiped-and-restarted case: both devices join the same network
|
||||
|
||||
Reference in new issue
Block a user