Rule parser: require "in" or "out" direction specifier

This commit is contained in:
Nikolay Edigaryev 2026-07-23 12:07:59 +01:00
parent 0dfede19e6
commit 964014b98b
3 changed files with 54 additions and 36 deletions

View File

@ -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+ }

View File

@ -20,7 +20,8 @@ pub enum Rule {
Stateless(Target),
Stateful {
direction: Direction,
target: Target,
source: Option<Target>,
destination: Option<Target>,
},
}
@ -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<Target, ParseRuleError> {
@ -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::<Rule>().unwrap(),
"in from @host".parse::<Rule>().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::<Rule>().unwrap(),
"out to 10.0.0.0/8".parse::<Rule>().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::<Rule>().is_err(), "{input:?} should fail");
}

View File

@ -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