--allow: support "@host" syntax (#160)

* --allow: support "@host" syntax

To make communication with the guest VM possible
when using --block=0.0.0.0/0.

* Use "@-alias" wording instead of "@host"
This commit is contained in:
Nikolay Edigaryev 2026-05-15 16:06:03 +02:00 committed by GitHub
parent bbff9996ab
commit df84a30016
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 85 additions and 31 deletions

View File

@ -14,10 +14,11 @@ pub use exposed_port::ExposedPort;
use ipnet::Ipv4Net; use ipnet::Ipv4Net;
use mac_address::MacAddress; use mac_address::MacAddress;
use port_forwarder::PortForwarder; use port_forwarder::PortForwarder;
use prefix_trie::{Prefix, PrefixMap, PrefixSet}; use prefix_trie::{Prefix, PrefixMap};
use smoltcp::wire::EthernetFrame; use smoltcp::wire::EthernetFrame;
use std::io::ErrorKind; use std::io::ErrorKind;
use std::os::unix::io::{AsRawFd, RawFd}; use std::os::unix::io::{AsRawFd, RawFd};
use std::str::FromStr;
use std::time::Duration; use std::time::Duration;
use vmnet::Batch; use vmnet::Batch;
@ -32,6 +33,24 @@ pub struct Proxy<'proxy> {
port_forwarder: PortForwarder, port_forwarder: PortForwarder,
} }
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Target {
Prefix(Ipv4Net),
Host,
}
impl FromStr for Target {
type Err = ipnet::AddrParseError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
if s == "@host" {
return Ok(Target::Host);
}
Ipv4Net::from_str(s).map(Target::Prefix)
}
}
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub(crate) enum Action { pub(crate) enum Action {
Block, Block,
@ -43,12 +62,15 @@ impl Proxy<'_> {
vm_fd: RawFd, vm_fd: RawFd,
vm_mac_address: MacAddress, vm_mac_address: MacAddress,
vm_net_type: NetType, vm_net_type: NetType,
allow: PrefixSet<Ipv4Net>, allow: Vec<Target>,
block: PrefixSet<Ipv4Net>, block: Vec<Target>,
exposed_ports: Vec<ExposedPort>, exposed_ports: Vec<ExposedPort>,
) -> Result<Proxy<'proxy>> { ) -> Result<Proxy<'proxy>> {
let vm = VM::new(vm_fd)?; let vm = VM::new(vm_fd)?;
let host = Host::new(vm_net_type, !allow.contains(&Ipv4Net::zero()))?; let host = Host::new(
vm_net_type,
!allow.contains(&Target::Prefix(Ipv4Net::zero())),
)?;
let poller_timeout = Duration::from_millis(100); let poller_timeout = Duration::from_millis(100);
let poller = Poller::new(vm.as_raw_fd(), host.as_raw_fd(), poller_timeout)?; let poller = Poller::new(vm.as_raw_fd(), host.as_raw_fd(), poller_timeout)?;
@ -58,12 +80,22 @@ impl Proxy<'_> {
// over allowing rules when prefixes are identical. // over allowing rules when prefixes are identical.
let mut rules = PrefixMap::new(); let mut rules = PrefixMap::new();
for allow_net in allow { for allow_target in allow {
rules.insert(allow_net, Action::Allow); let allow_prefix = match allow_target {
Target::Prefix(prefix) => prefix,
Target::Host => host.gateway_ip.into(),
};
rules.insert(allow_prefix, Action::Allow);
} }
for block_net in block { for block_target in block {
rules.insert(block_net, Action::Block); let block_prefix = match block_target {
Target::Prefix(prefix) => prefix,
Target::Host => host.gateway_ip.into(),
};
rules.insert(block_prefix, Action::Block);
} }
Ok(Proxy { Ok(Proxy {
@ -175,7 +207,7 @@ mod tests {
use ipnet::Ipv4Net; use ipnet::Ipv4Net;
use mac_address::MacAddress; use mac_address::MacAddress;
use nix::sys::socket::{AddressFamily, SockFlag, SockType, socketpair}; use nix::sys::socket::{AddressFamily, SockFlag, SockType, socketpair};
use prefix_trie::{PrefixMap, PrefixSet}; use prefix_trie::PrefixMap;
use serial_test::serial; use serial_test::serial;
use smoltcp::wire::{Ipv4Address, Ipv4Packet}; use smoltcp::wire::{Ipv4Address, Ipv4Packet};
use std::collections::HashSet; use std::collections::HashSet;
@ -219,6 +251,27 @@ mod tests {
assert!(allowed_from_vm_ipv4(&proxy, vm_ip, "33.33.33.34").is_none()); assert!(allowed_from_vm_ipv4(&proxy, vm_ip, "33.33.33.34").is_none());
} }
#[test]
#[serial]
fn test_allow_host() {
let vm_ip = Ipv4Address::from_str("192.168.0.2").unwrap();
let proxy = create_proxy(vm_ip, vec!["@host"], vec!["0.0.0.0/0"]);
assert_eq!(
proxy.rules,
PrefixMap::from_iter(vec![
(proxy.host.gateway_ip.into(), Action::Allow),
(Ipv4Net::from_str("0.0.0.0/0").unwrap(), Action::Block),
])
);
// Access to global IPs should be disallowed because of --block=0.0.0.0/0
assert!(allowed_from_vm_ipv4(&proxy, vm_ip, "8.8.8.8").is_none());
// Despite the above, access to host IP address should be possible because of --allow=@host
assert!(allowed_from_vm_ipv4(&proxy, vm_ip, &proxy.host.gateway_ip.to_string()).is_some());
}
fn create_proxy<'test>(vm_ip: Ipv4Address, allow: Vec<&str>, block: Vec<&str>) -> Proxy<'test> { fn create_proxy<'test>(vm_ip: Ipv4Address, allow: Vec<&str>, block: Vec<&str>) -> Proxy<'test> {
let (vm_fd, _) = socketpair( let (vm_fd, _) = socketpair(
AddressFamily::Unix, AddressFamily::Unix,
@ -233,16 +286,14 @@ mod tests {
vm_fd.as_raw_fd(), vm_fd.as_raw_fd(),
MacAddress::from_str("02:00:00:00:00:01").unwrap(), MacAddress::from_str("02:00:00:00:00:01").unwrap(),
NetType::Nat, NetType::Nat,
PrefixSet::from_iter(
allow allow
.into_iter() .into_iter()
.map(|cidr| Ipv4Net::from_str(cidr).unwrap()), .map(|cidr| cidr.parse().unwrap())
), .collect(),
PrefixSet::from_iter(
block block
.into_iter() .into_iter()
.map(|cidr| Ipv4Net::from_str(cidr).unwrap()), .map(|cidr| cidr.parse().unwrap())
), .collect(),
Vec::default(), Vec::default(),
) )
.unwrap(); .unwrap();

View File

@ -1,14 +1,13 @@
use anyhow::{Context, anyhow}; use anyhow::{Context, anyhow};
use clap::Parser; use clap::Parser;
use ipnet::Ipv4Net;
use log::LevelFilter; use log::LevelFilter;
use nix::sys::signal::{SigHandler, Signal, signal}; use nix::sys::signal::{SigHandler, Signal, signal};
use oslog::OsLogger; use oslog::OsLogger;
use prefix_trie::PrefixSet;
use privdrop::PrivDrop; use privdrop::PrivDrop;
use softnet::NetType; use softnet::NetType;
use softnet::proxy::ExposedPort; use softnet::proxy::ExposedPort;
use softnet::proxy::Proxy; use softnet::proxy::Proxy;
use softnet::proxy::Target;
use std::borrow::Cow; use std::borrow::Cow;
use std::env; use std::env;
use std::os::raw::c_int; use std::os::raw::c_int;
@ -53,29 +52,33 @@ struct Args {
#[clap( #[clap(
long, long,
help = "Comma-separated list of CIDRs to allow the traffic to \ help = "Comma-separated list of CIDRs to allow the traffic to \
(e.g. --allow=192.168.0.0/24 may be used to allow a LAN access for a VM). \ (e.g. --allow=192.168.0.0/24 may be used to allow a LAN access for a VM), \
plus supported @-aliases. Currently the only supported @-alias is @host, \
which matches the vmnet bridge gateway IP. \
When used with --block, the longest prefix match always wins. \ When used with --block, the longest prefix match always wins. \
In case an identical prefix is both --allow'ed and --block'ed, \ In case an identical prefix is both --allow'ed and --block'ed, \
blocking will take precedence. --allow=0.0.0.0/0 is a special case, \ blocking will take precedence. --allow=0.0.0.0/0 is a special case, \
it additionally disables bridge isolation (even when --block=0.0.0.0/0 is specified).", it additionally disables bridge isolation (even when --block=0.0.0.0/0 is specified).",
value_name = "comma-separated CIDRs", value_name = "comma-separated CIDRs or @-alias",
use_value_delimiter = true, use_value_delimiter = true,
action = clap::ArgAction::Set action = clap::ArgAction::Set
)] )]
allow: Vec<Ipv4Net>, allow: Vec<Target>,
#[clap( #[clap(
long, long,
help = "Comma-separated list of CIDRs to block the traffic to \ help = "Comma-separated list of CIDRs to block the traffic to \
(e.g. --block=0.0.0.0/0 may be used to establish a default deny policy \ (e.g. --block=0.0.0.0/0 may be used to establish a default deny policy \
that is further relaxed with --allow). When used with --allow, \ that is further relaxed with --allow), plus supported @-aliases. \
the longest prefix match always wins. In case the same prefix is both \ Currently the only supported @-alias is @host, which matches the vmnet bridge gateway IP. \
--allow'ed and --block'ed, blocking takes precedence.", When used with --allow, the longest prefix match always wins. \
value_name = "comma-separated CIDRs", In case an identical prefix is both --allow'ed and --block'ed, \
blocking will take precedence.",
value_name = "comma-separated CIDRs or @-alias",
use_value_delimiter = true, use_value_delimiter = true,
action = clap::ArgAction::Set action = clap::ArgAction::Set
)] )]
block: Vec<Ipv4Net>, block: Vec<Target>,
#[clap( #[clap(
long, long,
@ -196,8 +199,8 @@ fn try_main() -> anyhow::Result<()> {
args.vm_fd as RawFd, args.vm_fd as RawFd,
args.vm_mac_address, args.vm_mac_address,
args.vm_net_type, args.vm_net_type,
PrefixSet::from_iter(args.allow), args.allow,
PrefixSet::from_iter(args.block), args.block,
args.expose, args.expose,
) )
.context("failed to initialize proxy")?; .context("failed to initialize proxy")?;