Merge branch 'next/major' of github.com:Start9Labs/start-os into feat/restart

This commit is contained in:
Aiden McClelland
2026-03-29 02:13:31 -06:00
2 changed files with 142 additions and 68 deletions

View File

@@ -103,7 +103,7 @@ impl OsPartitionInfo {
} }
} }
const BIOS_BOOT_TYPE_GUID: &str = "21686148-6449-6e6f-744e-656564726548"; const BIOS_BOOT_TYPE_GUID: &str = "21686148-6449-6E6F-744E-656564454649";
/// Find the BIOS boot partition on the same disk as `known_part`. /// Find the BIOS boot partition on the same disk as `known_part`.
async fn find_bios_boot_partition(known_part: &Path) -> Result<Option<PathBuf>, Error> { async fn find_bios_boot_partition(known_part: &Path) -> Result<Option<PathBuf>, Error> {

View File

@@ -765,6 +765,7 @@ async fn watcher(
} }
changed changed
}); });
gc_policy_routing(&ifaces).await;
for result in futures::future::join_all(jobs).await { for result in futures::future::join_all(jobs).await {
result.log_err(); result.log_err();
} }
@@ -806,15 +807,43 @@ async fn get_wan_ipv4(iface: &str, base_url: &Url) -> Result<Option<Ipv4Addr>, E
Ok(Some(trimmed.parse()?)) Ok(Some(trimmed.parse()?))
} }
struct PolicyRoutingCleanup { struct PolicyRoutingGuard {
table_id: u32, table_id: u32,
iface: String,
} }
impl Drop for PolicyRoutingCleanup {
fn drop(&mut self) { /// Remove stale per-interface policy-routing state (fwmark rules, routing
let table_str = self.table_id.to_string(); /// tables, iptables CONNMARK rules) for interfaces that no longer exist.
let iface = std::mem::take(&mut self.iface); async fn gc_policy_routing(active_ifaces: &BTreeSet<GatewayId>) {
tokio::spawn(async move { let active_tables: BTreeSet<u32> = active_ifaces
.iter()
.filter_map(|iface| if_nametoindex(iface.as_str()).ok().map(|idx| 1000 + idx))
.collect();
// GC fwmark ip rules at priority 50 and their routing tables.
if let Ok(rules) = Command::new("ip")
.arg("rule")
.arg("show")
.invoke(ErrorKind::Network)
.await
.and_then(|b| String::from_utf8(b).with_kind(ErrorKind::Utf8))
{
for line in rules.lines() {
let line = line.trim();
if !line.starts_with("50:") {
continue;
}
let Some(pos) = line.find("lookup ") else {
continue;
};
let token = line[pos + 7..].split_whitespace().next().unwrap_or("");
let Ok(table_id) = token.parse::<u32>() else {
continue;
};
if table_id < 1000 || active_tables.contains(&table_id) {
continue;
}
let table_str = table_id.to_string();
tracing::debug!("gc_policy_routing: removing stale table {table_id}");
Command::new("ip") Command::new("ip")
.arg("rule") .arg("rule")
.arg("del") .arg("del")
@@ -835,25 +864,46 @@ impl Drop for PolicyRoutingCleanup {
.invoke(ErrorKind::Network) .invoke(ErrorKind::Network)
.await .await
.ok(); .ok();
Command::new("iptables") }
.arg("-t") }
.arg("mangle")
.arg("-D") // GC iptables CONNMARK set-mark rules for defunct interfaces.
.arg("PREROUTING") if let Ok(rules) = Command::new("iptables")
.arg("-i") .arg("-t")
.arg(&iface) .arg("mangle")
.arg("-m") .arg("-S")
.arg("conntrack") .arg("PREROUTING")
.arg("--ctstate") .invoke(ErrorKind::Network)
.arg("NEW") .await
.arg("-j") .and_then(|b| String::from_utf8(b).with_kind(ErrorKind::Utf8))
.arg("CONNMARK") {
.arg("--set-mark") // Rules look like:
.arg(&table_str) // -A PREROUTING -i wg0 -m conntrack --ctstate NEW -j CONNMARK --set-mark 1005
.invoke(ErrorKind::Network) for line in rules.lines() {
.await let parts: Vec<&str> = line.split_whitespace().collect();
.ok(); if parts.first() != Some(&"-A") {
}); continue;
}
if !parts.contains(&"--set-mark") {
continue;
}
let Some(iface_idx) = parts.iter().position(|&p| p == "-i") else {
continue;
};
let Some(&iface) = parts.get(iface_idx + 1) else {
continue;
};
if active_ifaces.contains(&GatewayId::from(InternedString::intern(iface))) {
continue;
}
tracing::debug!("gc_policy_routing: removing stale iptables rule for {iface}");
let mut cmd = Command::new("iptables");
cmd.arg("-t").arg("mangle").arg("-D");
for &arg in &parts[1..] {
cmd.arg(arg);
}
cmd.invoke(ErrorKind::Network).await.ok();
}
} }
} }
@@ -985,11 +1035,8 @@ async fn watch_ip(
} else { } else {
None None
}; };
let policy_guard: Option<PolicyRoutingCleanup> = let policy_guard: Option<PolicyRoutingGuard> =
policy_table_id.map(|t| PolicyRoutingCleanup { policy_table_id.map(|t| PolicyRoutingGuard { table_id: t });
table_id: t,
iface: iface.as_str().to_owned(),
});
loop { loop {
until until
@@ -1016,7 +1063,7 @@ async fn watch_ip(
} }
async fn apply_policy_routing( async fn apply_policy_routing(
guard: &PolicyRoutingCleanup, guard: &PolicyRoutingGuard,
iface: &GatewayId, iface: &GatewayId,
lan_ip: &OrdSet<IpAddr>, lan_ip: &OrdSet<IpAddr>,
) -> Result<(), Error> { ) -> Result<(), Error> {
@@ -1250,7 +1297,7 @@ async fn poll_ip_info(
ip4_proxy: &Ip4ConfigProxy<'_>, ip4_proxy: &Ip4ConfigProxy<'_>,
ip6_proxy: &Ip6ConfigProxy<'_>, ip6_proxy: &Ip6ConfigProxy<'_>,
dhcp4_proxy: &Option<Dhcp4ConfigProxy<'_>>, dhcp4_proxy: &Option<Dhcp4ConfigProxy<'_>>,
policy_guard: &Option<PolicyRoutingCleanup>, policy_guard: &Option<PolicyRoutingGuard>,
iface: &GatewayId, iface: &GatewayId,
echoip_ratelimit_state: &mut BTreeMap<Url, Instant>, echoip_ratelimit_state: &mut BTreeMap<Url, Instant>,
db: Option<&TypedPatchDb<Database>>, db: Option<&TypedPatchDb<Database>>,
@@ -1299,6 +1346,49 @@ async fn poll_ip_info(
apply_policy_routing(guard, iface, &lan_ip).await?; apply_policy_routing(guard, iface, &lan_ip).await?;
} }
// Write IP info to the watch immediately so the gateway appears in the
// DB without waiting for the (slow) WAN IP fetch. The echoip HTTP
// request has a 5-second timeout per URL and is easily cancelled by
// D-Bus signals via the Until mechanism, which would prevent the
// gateway from ever appearing if we waited.
let mut ip_info = IpInfo {
name: name.clone(),
scope_id,
device_type,
subnets: subnets.clone(),
lan_ip,
wan_ip: None,
ntp_servers,
dns_servers,
};
write_to.send_if_modified(|m: &mut OrdMap<GatewayId, NetworkInterfaceInfo>| {
let (name, secure, gateway_type, prev_wan_ip) =
m.get(iface).map_or((None, None, None, None), |i| {
(
i.name.clone(),
i.secure,
i.gateway_type,
i.ip_info.as_ref().and_then(|i| i.wan_ip),
)
});
ip_info.wan_ip = prev_wan_ip;
let ip_info = Arc::new(ip_info);
m.insert(
iface.clone(),
NetworkInterfaceInfo {
name,
secure,
ip_info: Some(ip_info.clone()),
gateway_type,
},
)
.filter(|old| &old.ip_info == &Some(ip_info))
.is_none()
});
// Now fetch the WAN IP in a second pass. Even if this is slow or
// gets cancelled, the gateway already has valid ip_info above.
let echoip_urls = if let Some(db) = db { let echoip_urls = if let Some(db) = db {
db.peek() db.peek()
.await .await
@@ -1349,41 +1439,25 @@ async fn poll_ip_info(
); );
tracing::debug!("{e:?}"); tracing::debug!("{e:?}");
} }
let mut ip_info = IpInfo {
name: name.clone(),
scope_id,
device_type,
subnets,
lan_ip,
wan_ip,
ntp_servers,
dns_servers,
};
write_to.send_if_modified(|m: &mut OrdMap<GatewayId, NetworkInterfaceInfo>| { // Update with WAN IP if we obtained one
let (name, secure, gateway_type, prev_wan_ip) = if wan_ip.is_some() {
m.get(iface).map_or((None, None, None, None), |i| { write_to.send_if_modified(|m: &mut OrdMap<GatewayId, NetworkInterfaceInfo>| {
( let Some(entry) = m.get_mut(iface) else {
i.name.clone(), return false;
i.secure, };
i.gateway_type, let Some(ref existing_ip) = entry.ip_info else {
i.ip_info.as_ref().and_then(|i| i.wan_ip), return false;
) };
}); if existing_ip.wan_ip == wan_ip {
ip_info.wan_ip = ip_info.wan_ip.or(prev_wan_ip); return false;
let ip_info = Arc::new(ip_info); }
m.insert( let mut updated = (**existing_ip).clone();
iface.clone(), updated.wan_ip = wan_ip;
NetworkInterfaceInfo { entry.ip_info = Some(Arc::new(updated));
name, true
secure, });
ip_info: Some(ip_info.clone()), }
gateway_type,
},
)
.filter(|old| &old.ip_info == &Some(ip_info))
.is_none()
});
Ok(()) Ok(())
} }