diff --git a/lib/proxy/rule.pest b/lib/proxy/rule.pest index d1de94e..a4a737c 100644 --- a/lib/proxy/rule.pest +++ b/lib/proxy/rule.pest @@ -1,5 +1,6 @@ rule = { SOI ~ (stateful | target) ~ EOI } -stateful = { direction ~ " "+ ~ target } -direction = { "from" | "to" } +stateful = { direction ~ " "+ ~ side ~ " "+ ~ target } +direction = { "in" | "out" } +side = { "from" | "to" } target = @{ ANY+ } diff --git a/lib/proxy/rule.rs b/lib/proxy/rule.rs index c586929..020a4cb 100644 --- a/lib/proxy/rule.rs +++ b/lib/proxy/rule.rs @@ -20,7 +20,8 @@ pub enum Rule { Stateless(Target), Stateful { direction: Direction, - target: Target, + source: Option, + destination: Option, }, } @@ -32,8 +33,8 @@ pub enum Target { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Direction { - From, - To, + In, + Out, } #[derive(Debug)] @@ -66,14 +67,27 @@ impl Rule { .expect("a stateful rule always has a direction") .as_str() { - "from" => Direction::From, - "to" => Direction::To, + "in" => Direction::In, + "out" => Direction::Out, direction => unreachable!("unexpected direction: {direction}"), }; + let side = fields + .next() + .expect("a stateful rule always has a side") + .as_str(); let target = Self::parse_target(&fields.next().expect("a stateful rule always has a target"))?; + let (source, destination) = match side { + "from" => (Some(target), None), + "to" => (None, Some(target)), + side => unreachable!("unexpected side: {side}"), + }; - Ok(Rule::Stateful { direction, target }) + Ok(Rule::Stateful { + direction, + source, + destination, + }) } fn parse_target(pair: &Pair<'_, SyntaxRule>) -> Result { @@ -139,18 +153,22 @@ mod tests { #[test] fn parses_stateful_directions() { + let private_network = Target::Prefix(Ipv4Net::from_str("10.0.0.0/8").unwrap()); + assert_eq!( - "from @host".parse::().unwrap(), + "in from @host".parse::().unwrap(), Rule::Stateful { - direction: Direction::From, - target: Target::Host, + direction: Direction::In, + source: Some(Target::Host), + destination: None, } ); assert_eq!( - "to 10.0.0.0/8".parse::().unwrap(), + "out to 10.0.0.0/8".parse::().unwrap(), Rule::Stateful { - direction: Direction::To, - target: Target::Prefix(Ipv4Net::from_str("10.0.0.0/8").unwrap()), + direction: Direction::Out, + source: None, + destination: Some(private_network), } ); } @@ -159,18 +177,15 @@ mod tests { fn rejects_invalid_rules() { for input in [ "", - "from", - "around @host", - "from @host tcp", - "from @host port 8080", - "from @host proto sctp", - "from @host proto tcp port 8080", - "from@host", - "from=@host", - " from @host", - "from @host ", - "from\t@host", - "from\n@host", + "from @host", + "in", + "out", + "in @host", + "out @host", + "infrom @host", + " in from @host", + "in from @host ", + "in\tfrom @host", ] { assert!(input.parse::().is_err(), "{input:?} should fail"); } diff --git a/src/main.rs b/src/main.rs index e33820b..b4f0604 100644 --- a/src/main.rs +++ b/src/main.rs @@ -63,8 +63,9 @@ struct Args { help = "Comma-separated list of rules for allowing traffic.\n\n\ Rule forms:\n\n\ * TARGET: stateless rule\n\ - * from TARGET: stateful flows initiated from TARGET (not supported yet)\n\ - * to TARGET: stateful flows initiated toward TARGET (not supported yet)\n\n\ + * in|out from TARGET: stateful flows initiated from TARGET (not supported yet)\n\ + * in|out to TARGET: stateful flows initiated toward TARGET (not supported yet)\n\n\ + Stateful rules specify the initiating packet's direction: in enters the VM; out leaves it.\n\n\ Targets are:\n\n\ * IPv4 CIDRs\n\ * @host, which matches the vmnet bridge gateway IP\n\n\ @@ -74,9 +75,9 @@ struct Args { --block=0.0.0.0/0 is specified.\n\n\ Examples:\n\n\ * --allow=192.168.0.0/24 — allow stateless traffic with this LAN\n\ - * --allow=\"from @host\" — allow stateful flows initiated from @host\n\ - * --allow=\"to 192.168.0.0/24\" — allow stateful flows initiated toward this LAN\n\ - * --allow=\"from @host,to 192.168.0.0/24\" — multiple rules may be comma-separated", + * --allow=\"in from @host\" — allow stateful flows initiated from @host\n\ + * --allow=\"out to 192.168.0.0/24\" — allow stateful flows initiated toward this LAN\n\ + * --allow=\"in from @host,out to 192.168.0.0/24\" — multiple rules may be comma-separated", value_name = "comma-separated rules", use_value_delimiter = true, action = clap::ArgAction::Set @@ -89,8 +90,9 @@ struct Args { help = "Comma-separated list of rules for blocking traffic.\n\n\ Rule forms:\n\n\ * TARGET: stateless rule\n\ - * from TARGET: stateful flows initiated from TARGET (not supported yet)\n\ - * to TARGET: stateful flows initiated toward TARGET (not supported yet)\n\n\ + * in|out from TARGET: stateful flows initiated from TARGET (not supported yet)\n\ + * in|out to TARGET: stateful flows initiated toward TARGET (not supported yet)\n\n\ + Stateful rules specify the initiating packet's direction: in enters the VM; out leaves it.\n\n\ Targets are:\n\n\ * IPv4 CIDRs\n\ * @host, which matches the vmnet bridge gateway IP\n\n\ @@ -98,9 +100,9 @@ struct Args { allowed and blocked, blocking takes precedence.\n\n\ Examples:\n\n\ * --block=0.0.0.0/0 — establish a stateless default-deny policy\n\ - * --block=\"from @host\" — block stateful flows initiated from @host\n\ - * --block=\"to 66.66.66.0/24\" — block stateful flows initiated toward this CIDR\n\ - * --block=\"from @host,to 66.66.66.0/24\" — multiple rules may be comma-separated", + * --block=\"in from @host\" — block stateful flows initiated from @host\n\ + * --block=\"out to 66.66.66.0/24\" — block stateful flows initiated toward this CIDR\n\ + * --block=\"in from @host,out to 66.66.66.0/24\" — multiple rules may be comma-separated", value_name = "comma-separated rules", use_value_delimiter = true, action = clap::ArgAction::Set