From d608ffeeda1a479c9dc0e5f6ad269ba07a510d7f Mon Sep 17 00:00:00 2001 From: Andrew Guschin Date: Fri, 30 Oct 2026 03:10:41 +0400 Subject: Implement Linux proxy controller, subscription state, and CLI/TUI --- src/linux/routes.rs | 186 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 src/linux/routes.rs (limited to 'src/linux/routes.rs') diff --git a/src/linux/routes.rs b/src/linux/routes.rs new file mode 100644 index 0000000..07083bb --- /dev/null +++ b/src/linux/routes.rs @@ -0,0 +1,186 @@ +use super::firewall::CAPTURE_MARK; +use anyhow::{Result, ensure}; +use futures_util::TryStreamExt; +use rtnetlink::{ + Handle, IpVersion, RouteMessageBuilder, + packet_route::{ + route::{RouteAttribute, RouteProtocol}, + rule::{RuleAction, RuleAttribute}, + }, +}; +use std::net::{Ipv4Addr, Ipv6Addr}; + +const TABLE_ID: u32 = 20260; +const PRIORITY: u32 = 10260; +const PROTOCOL: u8 = 186; + +fn connection() -> Result { + let (connection, handle, _) = rtnetlink::new_connection()?; + tokio::spawn(connection); + Ok(handle) +} + +pub async fn preflight() -> Result<()> { + let handle = connection()?; + for version in [IpVersion::V4, IpVersion::V6] { + let mut rules = handle.rule().get(version).execute(); + while let Some(rule) = rules.try_next().await? { + let priority = rule + .attributes + .iter() + .find_map(|a| { + if let RuleAttribute::Priority(v) = a { + Some(*v) + } else { + None + } + }) + .unwrap_or(0); + ensure!( + matches!(priority, 0 | 32766 | 32767) || owned_rule(&rule), + "Unsupported policy-routing rule at priority {}; remove conflicting VPN or policy routing before enabling", + priority + ); + } + } + let mut routes = handle + .route() + .get(rtnetlink::RouteMessageBuilder::::new().build()) + .execute(); + while let Some(route) = routes.try_next().await? { + if table(&route) == TABLE_ID { + ensure!( + route.header.protocol == RouteProtocol::Other(PROTOCOL), + "Roentgen route table is already in use" + ); + } + } + Ok(()) +} + +fn table(route: &rtnetlink::packet_route::route::RouteMessage) -> u32 { + route + .attributes + .iter() + .find_map(|a| { + if let RouteAttribute::Table(v) = a { + Some(*v) + } else { + None + } + }) + .unwrap_or(u32::from(route.header.table)) +} +fn owned_rule(rule: &rtnetlink::packet_route::rule::RuleMessage) -> bool { + rule.attributes.contains(&RuleAttribute::Priority(PRIORITY)) + && rule + .attributes + .contains(&RuleAttribute::FwMark(CAPTURE_MARK)) + && rule + .attributes + .contains(&RuleAttribute::FwMask(CAPTURE_MARK)) + && rule.attributes.contains(&RuleAttribute::Table(TABLE_ID)) + && rule.header.action == RuleAction::ToTable +} + +pub async fn capture(interface: u32) -> Result<()> { + let handle = connection()?; + for route in [ + RouteMessageBuilder::::new() + .table_id(TABLE_ID) + .output_interface(interface) + .protocol(RouteProtocol::Other(PROTOCOL)) + .build(), + RouteMessageBuilder::::new() + .table_id(TABLE_ID) + .output_interface(interface) + .protocol(RouteProtocol::Other(PROTOCOL)) + .build(), + ] { + handle.route().add(route).replace().execute().await?; + } + for version in [IpVersion::V4, IpVersion::V6] { + let existing: Vec<_> = handle + .rule() + .get(version.clone()) + .execute() + .try_collect() + .await?; + if existing.iter().any(owned_rule) { + continue; + } + match version { + IpVersion::V4 => { + let mut request = handle + .rule() + .add() + .v4() + .priority(PRIORITY) + .table_id(TABLE_ID) + .fw_mark(CAPTURE_MARK) + .action(RuleAction::ToTable); + request + .message_mut() + .attributes + .push(RuleAttribute::FwMask(CAPTURE_MARK)); + request.execute().await?; + } + IpVersion::V6 => { + let mut request = handle + .rule() + .add() + .v6() + .priority(PRIORITY) + .table_id(TABLE_ID) + .fw_mark(CAPTURE_MARK) + .action(RuleAction::ToTable); + request + .message_mut() + .attributes + .push(RuleAttribute::FwMask(CAPTURE_MARK)); + request.execute().await?; + } + } + } + Ok(()) +} + +pub async fn remove() -> Result<()> { + let handle = connection()?; + for version in [IpVersion::V4, IpVersion::V6] { + let rules: Vec<_> = handle + .rule() + .get(version.clone()) + .execute() + .try_collect() + .await?; + for rule in rules.into_iter().filter(owned_rule) { + handle.rule().del(rule).execute().await?; + } + } + let routes: Vec<_> = handle + .route() + .get(RouteMessageBuilder::::new().build()) + .execute() + .try_collect() + .await?; + for route in routes + .into_iter() + .filter(|r| table(r) == TABLE_ID && r.header.protocol == RouteProtocol::Other(PROTOCOL)) + { + handle.route().del(route).execute().await?; + } + Ok(()) +} + +pub async fn tun_index() -> Result> { + let handle = connection()?; + Ok(handle + .link() + .get() + .match_name("roentgen0") + .execute() + .try_next() + .await? + .map(|link| link.header.index)) +} -- cgit v1.2.3