aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHimbeer <himbeer@disroot.org>2025-03-18 22:02:07 +0100
committerHimbeer <himbeer@disroot.org>2025-03-20 15:21:30 +0100
commita425965131b868aa48b70cf96a945aa66e7ea3c5 (patch)
treee06a3ee9e58cee6132faf413da16fa7b2570e957
parenta16817b17ef485a45e3f686fc8ad2a5237d5fbfa (diff)
Add bulk route and policy rule parsers
-rw-r--r--Cargo.lock2
-rw-r--r--Cargo.toml2
-rw-r--r--src/main.rs560
3 files changed, 561 insertions, 3 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 2fa8117..89c777c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -324,7 +324,7 @@ dependencies = [
[[package]]
name = "rsdsl_netlinklib"
version = "0.6.0"
-source = "git+https://github.com/rsdsl/netlinklib.git#2567498ff9646c4ad6a11d4f2e0380b100cb9f1f"
+source = "git+https://github.com/rsdsl/netlinklib.git#9d55957b737cce1ace694376bdd157624286c020"
dependencies = [
"futures",
"libc",
diff --git a/Cargo.toml b/Cargo.toml
index 1b6db0a..2e436f3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,4 +6,4 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-rsdsl_netlinklib = { git = "https://github.com/rsdsl/netlinklib.git", version = "0.6.0", features = ["blocking"] }
+rsdsl_netlinklib = { git = "https://github.com/rsdsl/netlinklib.git", version = "0.6.0", features = ["blocking", "rule"] }
diff --git a/src/main.rs b/src/main.rs
index e7a11a9..c1d4583 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,561 @@
+use std::collections::HashMap;
+use std::fmt;
+use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
+use std::str::FromStr;
+
+use rsdsl_netlinklib::rule::RuleAction;
+
+const ROUTES_PATH: &str = "/data/static.rt";
+const RULES_PATH: &str = "/data/policies.rl";
+
+#[derive(Debug)]
+enum RouteParseError {
+ DstNotIpv4,
+ DstNotIpv6,
+ DuplicateAttr(String),
+ InvalidAttr(String),
+ InvalidCidr(String),
+ InvalidCmd(String),
+ InvalidVersion(String),
+ NoAttrValue(String),
+ NoCmd,
+ NoDst,
+ NoLink,
+ NoVersion,
+ ParseAddr(std::net::AddrParseError),
+ ParseBool(std::str::ParseBoolError),
+ ParseInt(std::num::ParseIntError),
+ RtrNotIpv4,
+ RtrNotIpv6,
+}
+
+impl fmt::Display for RouteParseError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::DstNotIpv4 => write!(f, "route4 with missing or non-IPv4 destination")?,
+ Self::DstNotIpv6 => write!(f, "route6 with missing or non-IPv6 destination")?,
+ Self::DuplicateAttr(a) => write!(f, "duplicate attribute {}", a)?,
+ Self::InvalidAttr(a) => write!(f, "invalid attribute {}", a)?,
+ Self::InvalidCidr(c) => write!(f, "invalid CIDR {} (want exactly 1 /)", c)?,
+ Self::InvalidCmd(c) => write!(f, "invalid command {} (want \"add\" or \"del\")", c)?,
+ Self::InvalidVersion(v) => {
+ write!(f, "invalid version: {} (want \"route4\" or \"route6\")", v)?
+ }
+ Self::NoAttrValue(a) => write!(f, "missing value for attribute {}", a)?,
+ Self::NoCmd => write!(f, "missing command (want \"add\" or \"del\")")?,
+ Self::NoDst => write!(f, "missing destination network (\"to\" attribute)")?,
+ Self::NoLink => write!(f, "missing network interface (\"dev\" attribute)")?,
+ Self::NoVersion => write!(f, "missing version (want \"route4\" or \"route6\")")?,
+ Self::ParseAddr(e) => write!(f, "parse network address: {}", e)?,
+ Self::ParseBool(e) => write!(f, "parse bool: {}", e)?,
+ Self::ParseInt(e) => write!(f, "parse integer: {}", e)?,
+ Self::RtrNotIpv4 => write!(f, "route4 with non-IPv4 gateway")?,
+ Self::RtrNotIpv6 => write!(f, "route6 with non-IPv6 gateway")?,
+ }
+
+ Ok(())
+ }
+}
+
+impl From<std::net::AddrParseError> for RouteParseError {
+ fn from(e: std::net::AddrParseError) -> RouteParseError {
+ RouteParseError::ParseAddr(e)
+ }
+}
+
+impl From<std::str::ParseBoolError> for RouteParseError {
+ fn from(e: std::str::ParseBoolError) -> RouteParseError {
+ RouteParseError::ParseBool(e)
+ }
+}
+
+impl From<std::num::ParseIntError> for RouteParseError {
+ fn from(e: std::num::ParseIntError) -> RouteParseError {
+ RouteParseError::ParseInt(e)
+ }
+}
+
+impl std::error::Error for RouteParseError {}
+
+#[derive(Debug)]
+enum RuleParseError {
+ DstIllegal,
+ DstNotIpv4,
+ DstNotIpv6,
+ DuplicateAttr(String),
+ InvalidAction(String),
+ InvalidAttr(String),
+ InvalidCidr(String),
+ InvalidCmd(String),
+ InvalidVersion(String),
+ NoAction,
+ NoAttrValue(String),
+ NoCmd,
+ NoVersion,
+ ParseAddr(std::net::AddrParseError),
+ ParseBool(std::str::ParseBoolError),
+ ParseInt(std::num::ParseIntError),
+ SrcIllegal,
+ SrcNotIpv4,
+ SrcNotIpv6,
+}
+
+impl fmt::Display for RuleParseError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::DstIllegal => write!(f, "protocol-agnostic rule with destination prefix")?,
+ Self::DstNotIpv4 => write!(f, "rule4 with non-IPv4 destination")?,
+ Self::DstNotIpv6 => write!(f, "rule6 with non-IPv6 destination")?,
+ Self::DuplicateAttr(a) => write!(f, "duplicate attribute {}", a)?,
+ Self::InvalidAction(a) => write!(f, "invalid action {}", a)?,
+ Self::InvalidAttr(a) => write!(f, "invalid attribute {}", a)?,
+ Self::InvalidCidr(c) => write!(f, "invalid CIDR {} (want exactly 1 /)", c)?,
+ Self::InvalidCmd(c) => write!(f, "invalid command {} (want \"add\" or \"del\")", c)?,
+ Self::InvalidVersion(v) => write!(
+ f,
+ "invalid version: {} (want \"rule\", \"rule4\" or \"rule6\")",
+ v
+ )?,
+ Self::NoAction => write!(f, "missing action (\"action\" attribute)")?,
+ Self::NoAttrValue(a) => write!(f, "missing value for attribute {}", a)?,
+ Self::NoCmd => write!(f, "missing command (want \"add\" or \"del\")")?,
+ Self::NoVersion => {
+ write!(f, "missing version (want \"rule\", \"rule4\" or \"rule6\")")?
+ }
+ Self::ParseAddr(e) => write!(f, "parse network address: {}", e)?,
+ Self::ParseBool(e) => write!(f, "parse bool: {}", e)?,
+ Self::ParseInt(e) => write!(f, "parse integer: {}", e)?,
+ Self::SrcIllegal => write!(f, "protocol-agnostic rule with source prefix")?,
+ Self::SrcNotIpv4 => write!(f, "rule4 with non-IPv4 source")?,
+ Self::SrcNotIpv6 => write!(f, "rule6 with non-IPv6 source")?,
+ }
+
+ Ok(())
+ }
+}
+
+impl From<std::net::AddrParseError> for RuleParseError {
+ fn from(e: std::net::AddrParseError) -> RuleParseError {
+ RuleParseError::ParseAddr(e)
+ }
+}
+
+impl From<std::str::ParseBoolError> for RuleParseError {
+ fn from(e: std::str::ParseBoolError) -> RuleParseError {
+ RuleParseError::ParseBool(e)
+ }
+}
+
+impl From<std::num::ParseIntError> for RuleParseError {
+ fn from(e: std::num::ParseIntError) -> RuleParseError {
+ RuleParseError::ParseInt(e)
+ }
+}
+
+impl std::error::Error for RuleParseError {}
+
+#[derive(Debug)]
+enum Error {
+ ParseRoutes(RouteParseError),
+ ParseRules(RuleParseError),
+ ReadRoutes(std::io::Error),
+ ReadRules(std::io::Error),
+}
+
+impl fmt::Display for Error {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::ParseRoutes(e) => write!(f, "parse routes: {}", e)?,
+ Self::ParseRules(e) => write!(f, "parse rules: {}", e)?,
+ Self::ReadRoutes(e) => write!(f, "read routes ({}): {}", ROUTES_PATH, e)?,
+ Self::ReadRules(e) => write!(f, "read rules ({}): {}", RULES_PATH, e)?,
+ }
+
+ Ok(())
+ }
+}
+
+impl From<RouteParseError> for Error {
+ fn from(e: RouteParseError) -> Error {
+ Error::ParseRoutes(e)
+ }
+}
+
+impl From<RuleParseError> for Error {
+ fn from(e: RuleParseError) -> Error {
+ Error::ParseRules(e)
+ }
+}
+
+impl std::error::Error for Error {}
+
+#[derive(Debug)]
+enum RouteVersion {
+ Ipv4,
+ Ipv6,
+}
+
+#[derive(Debug)]
+enum RouteDef {
+ V4(rsdsl_netlinklib::route::Route4),
+ V6(rsdsl_netlinklib::route::Route6),
+}
+
+#[derive(Debug)]
+struct Route {
+ delete: bool,
+ def: RouteDef,
+}
+
+impl FromStr for Route {
+ type Err = RouteParseError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let mut words = s.split_whitespace();
+
+ let version_str = words.next().ok_or(RouteParseError::NoVersion)?;
+ let version = match version_str {
+ "route4" => RouteVersion::Ipv4,
+ "route6" => RouteVersion::Ipv6,
+ _ => return Err(RouteParseError::InvalidVersion(version_str.to_string())),
+ };
+
+ let cmd = words.next().ok_or(RouteParseError::NoCmd)?;
+ let delete = match cmd {
+ "add" => false,
+ "del" => true,
+ _ => return Err(RouteParseError::InvalidCmd(cmd.to_string())),
+ };
+
+ let mut attrs = HashMap::<&str, &str>::new();
+ let mut current_attr = None;
+ for word in words {
+ if let Some(attr) = current_attr {
+ if attrs.insert(attr, word).is_some() {
+ return Err(RouteParseError::DuplicateAttr(attr.to_string()));
+ }
+ current_attr = None;
+ } else {
+ current_attr = Some(word);
+ }
+ }
+
+ if let Some(attr) = current_attr {
+ return Err(RouteParseError::NoAttrValue(attr.to_string()));
+ }
+
+ let mut dst = None;
+ let mut prefix_len = None;
+ let mut rtr = None;
+ let mut on_link = false;
+ let mut table = None;
+ let mut metric = None;
+ let mut link = None;
+
+ for (attr, value) in attrs {
+ match attr {
+ "to" => {
+ let mut prefix = value.split('/');
+
+ let addr = prefix
+ .next()
+ .ok_or(RouteParseError::InvalidCidr(value.to_string()))?;
+ let cidr = prefix
+ .next()
+ .ok_or(RouteParseError::InvalidCidr(value.to_string()))?;
+
+ if prefix.next().is_some() {
+ return Err(RouteParseError::InvalidCidr(value.to_string()));
+ }
+
+ dst = Some(addr.parse()?);
+ prefix_len = Some(cidr.parse()?);
+ }
+ "via" => rtr = Some(value.parse()?),
+ "onlink" => on_link = value.parse()?,
+ "table" => table = Some(value.parse()?),
+ "metric" => metric = Some(value.parse()?),
+ "dev" => link = Some(value.to_string()),
+ _ => return Err(RouteParseError::InvalidAttr(attr.to_string())),
+ }
+ }
+
+ match version {
+ RouteVersion::Ipv4 => Ok(Route {
+ delete,
+ def: RouteDef::V4(rsdsl_netlinklib::route::Route4 {
+ dst: if let Some(IpAddr::V4(dst)) = dst {
+ dst
+ } else {
+ return Err(RouteParseError::DstNotIpv4);
+ },
+ prefix_len: prefix_len.ok_or(RouteParseError::NoDst)?,
+ rtr: match rtr {
+ Some(IpAddr::V4(rtr)) => Some(rtr),
+ Some(_) => return Err(RouteParseError::RtrNotIpv4),
+ None => None,
+ },
+ on_link,
+ table,
+ metric,
+ link: link.ok_or(RouteParseError::NoLink)?,
+ }),
+ }),
+ RouteVersion::Ipv6 => Ok(Route {
+ delete,
+ def: RouteDef::V6(rsdsl_netlinklib::route::Route6 {
+ dst: if let Some(IpAddr::V6(dst)) = dst {
+ dst
+ } else {
+ return Err(RouteParseError::DstNotIpv6);
+ },
+ prefix_len: prefix_len.ok_or(RouteParseError::NoDst)?,
+ rtr: match rtr {
+ Some(IpAddr::V6(rtr)) => Some(rtr),
+ Some(_) => return Err(RouteParseError::RtrNotIpv6),
+ None => None,
+ },
+ on_link,
+ table,
+ metric,
+ link: link.ok_or(RouteParseError::NoLink)?,
+ }),
+ }),
+ }
+ }
+}
+
+#[derive(Debug)]
+struct Routes {
+ routes: Vec<Route>,
+}
+
+impl FromStr for Routes {
+ type Err = RouteParseError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let routes = s
+ .lines()
+ .map(|l| l.parse::<Route>())
+ .collect::<Result<Vec<Route>, Self::Err>>()?;
+
+ Ok(Self { routes })
+ }
+}
+
+#[derive(Debug, Default)]
+enum RuleVersion {
+ #[default]
+ Both,
+ Ipv4,
+ Ipv6,
+}
+
+#[derive(Debug)]
+struct Rule {
+ delete: bool,
+ version: RuleVersion,
+ invert: bool,
+ fwmark: Option<u32>,
+ dst: Option<(IpAddr, u8)>,
+ src: Option<(IpAddr, u8)>,
+ action: RuleAction,
+ table: u32,
+}
+
+impl FromStr for Rule {
+ type Err = RuleParseError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let mut words = s.split_whitespace();
+
+ let version_str = words.next().ok_or(RuleParseError::NoVersion)?;
+ let version = match version_str {
+ "rule" => RuleVersion::Both,
+ "rule4" => RuleVersion::Ipv4,
+ "rule6" => RuleVersion::Ipv6,
+ _ => return Err(RuleParseError::InvalidVersion(version_str.to_string())),
+ };
+
+ let cmd = words.next().ok_or(RuleParseError::NoCmd)?;
+ let delete = match cmd {
+ "add" => false,
+ "del" => true,
+ _ => return Err(RuleParseError::InvalidCmd(cmd.to_string())),
+ };
+
+ let mut attrs = HashMap::<&str, &str>::new();
+ let mut current_attr = None;
+ for word in words {
+ if let Some(attr) = current_attr {
+ if attrs.insert(attr, word).is_some() {
+ return Err(RuleParseError::DuplicateAttr(attr.to_string()));
+ }
+ current_attr = None;
+ } else {
+ current_attr = Some(word);
+ }
+ }
+
+ if let Some(attr) = current_attr {
+ return Err(RuleParseError::NoAttrValue(attr.to_string()));
+ }
+
+ let mut invert = false;
+ let mut fwmark = None;
+ let mut dst = None;
+ let mut src = None;
+ let mut action = None;
+ let mut table = None;
+
+ for (attr, value) in attrs {
+ match attr {
+ "invert" => invert = value.parse()?,
+ "fwmark" => fwmark = Some(value.parse()?),
+ "dst" => {
+ let mut prefix = value.split('/');
+
+ let addr = prefix
+ .next()
+ .ok_or(RuleParseError::InvalidCidr(value.to_string()))?;
+ let cidr = prefix
+ .next()
+ .ok_or(RuleParseError::InvalidCidr(value.to_string()))?;
+
+ if prefix.next().is_some() {
+ return Err(RuleParseError::InvalidCidr(value.to_string()));
+ }
+
+ dst = Some((addr.parse()?, cidr.parse()?));
+ }
+ "src" => {
+ let mut prefix = value.split('/');
+
+ let addr = prefix
+ .next()
+ .ok_or(RuleParseError::InvalidCidr(value.to_string()))?;
+ let cidr = prefix
+ .next()
+ .ok_or(RuleParseError::InvalidCidr(value.to_string()))?;
+
+ if prefix.next().is_some() {
+ return Err(RuleParseError::InvalidCidr(value.to_string()));
+ }
+
+ src = Some((addr.parse()?, cidr.parse()?));
+ }
+ "action" => match value {
+ "to_table" => action = Some(RuleAction::ToTable),
+ "blackhole" => action = Some(RuleAction::Blackhole),
+ "unreachable" => action = Some(RuleAction::Unreachable),
+ "prohibit" => action = Some(RuleAction::Prohibit),
+ a => return Err(RuleParseError::InvalidAction(a.to_string())),
+ },
+ "table" => table = Some(value.parse()?),
+ _ => return Err(RuleParseError::InvalidAttr(attr.to_string())),
+ }
+ }
+
+ match version {
+ RuleVersion::Both => Ok(Rule {
+ delete,
+ version,
+ invert,
+ fwmark,
+ dst: if dst.is_some() {
+ return Err(RuleParseError::DstIllegal);
+ } else {
+ None
+ },
+ src: if src.is_some() {
+ return Err(RuleParseError::SrcIllegal);
+ } else {
+ None
+ },
+ action: action.ok_or(RuleParseError::NoAction)?,
+ table: table.unwrap_or_default(),
+ }),
+ RuleVersion::Ipv4 => Ok(Rule {
+ delete,
+ version,
+ invert,
+ fwmark,
+ dst: match dst {
+ Some((IpAddr::V4(dst), cidr)) => Some((IpAddr::V4(dst), cidr)),
+ Some(_) => return Err(RuleParseError::DstNotIpv4),
+ None => None,
+ },
+ src: match src {
+ Some((IpAddr::V4(src), cidr)) => Some((IpAddr::V4(src), cidr)),
+ Some(_) => return Err(RuleParseError::SrcNotIpv4),
+ None => None,
+ },
+ action: action.ok_or(RuleParseError::NoAction)?,
+ table: table.unwrap_or_default(),
+ }),
+ RuleVersion::Ipv6 => Ok(Rule {
+ delete,
+ version,
+ invert,
+ fwmark,
+ dst: match dst {
+ Some((IpAddr::V6(dst), cidr)) => Some((IpAddr::V6(dst), cidr)),
+ Some(_) => return Err(RuleParseError::DstNotIpv6),
+ None => None,
+ },
+ src: match src {
+ Some((IpAddr::V6(src), cidr)) => Some((IpAddr::V6(src), cidr)),
+ Some(_) => return Err(RuleParseError::SrcNotIpv6),
+ None => None,
+ },
+ action: action.ok_or(RuleParseError::NoAction)?,
+ table: table.unwrap_or_default(),
+ }),
+ }
+ }
+}
+
+#[derive(Debug)]
+struct Rules {
+ rules: Vec<Rule>,
+}
+
+impl FromStr for Rules {
+ type Err = RuleParseError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let rules = s
+ .lines()
+ .map(|l| l.parse::<Rule>())
+ .collect::<Result<Vec<Rule>, Self::Err>>()?;
+
+ Ok(Self { rules })
+ }
+}
+
fn main() {
- println!("Hello, world!");
+ println!("[info] init");
+
+ match run() {
+ Ok(()) => loop {
+ std::thread::park()
+ },
+ Err(e) => eprintln!("[warn] {}", e),
+ }
+}
+
+fn run() -> Result<(), Error> {
+ let routes = match std::fs::read_to_string(ROUTES_PATH) {
+ Ok(s) => s,
+ Err(e) => return Err(Error::ReadRoutes(e)),
+ };
+ let routes: Routes = routes.parse()?;
+
+ let rules = match std::fs::read_to_string(RULES_PATH) {
+ Ok(s) => s,
+ Err(e) => return Err(Error::ReadRules(e)),
+ };
+ let rules: Rules = rules.parse()?;
+
+ todo!()
}