From 3cd14500f882f1547f2ec579701f2b2363581feb Mon Sep 17 00:00:00 2001 From: Elliot Kroo Date: Thu, 16 Oct 2025 17:23:17 -0700 Subject: [PATCH] allow-only flag: --allow as a strict whitelist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change controls the allowed outgoing traffic CIDRs. Instead of * send traffic from its own MAC-address * send traffic from the IP-address assigned to it by the DHCP * send traffic to globally routable IPv4 addresses * send traffic to gateway IP of the vmnet bridge * receive any incoming traffic by passing --allow-only, the VM now only: * sends traffic from its own MAC-address (unchanged) * sends traffic from the IP-address assigned to it by the DHCP (unchanged) * sends traffic to the IPv4 CIDRs provided via `--allow` * sends/receives DHCP and DNS packets (unchanged) * communicates with the vmnet gateway (unchanged) * receives any inbound traffic (unchanged) Everything else that previously counted as “globally routable” is now blocked. --- README.md | 4 ++++ lib/proxy/mod.rs | 11 ++++++++++- lib/proxy/vm.rs | 18 ++++++++++-------- src/main.rs | 13 +++++++++++++ 4 files changed, 37 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 529ff96..3cdfbea 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,10 @@ It is essentially a userspace packet filter which restricts the VM networking an * send traffic to gateway IP of the vmnet bridge (this would normally be \"bridge100\" interface) * receive any incoming traffic +You can widen access to additional private ranges by passing a comma-separated list of CIDRs via `--allow`. + +If you'd like to treat `--allow` as a strict whitelist, add `--allow-only`. With that flag, the VM can only reach the CIDRs you specified (plus the inevitable DHCP/DNS traffic and the host gateway needed for connectivity). + In addition, Softnet tunes macOS built-in DHCP server to decrease its lease time from the default 86,400 seconds (one day) to 600 seconds (10 minutes). This is especially important when you use Tart to clone and run a lot of ephemeral VMs over a period of one day. Please check out [this blog post](https://cirrus-ci.org/blog/2022/07/07/isolating-network-between-tarts-macos-virtual-machines/) for backstory. diff --git a/lib/proxy/mod.rs b/lib/proxy/mod.rs index 0542422..c35100d 100644 --- a/lib/proxy/mod.rs +++ b/lib/proxy/mod.rs @@ -26,6 +26,7 @@ pub struct Proxy<'proxy> { vm_mac_address: smoltcp::wire::EthernetAddress, dhcp_snooper: DhcpSnooper, allow: PrefixSet, + allow_only: bool, enobufs_encountered: bool, port_forwarder: PortForwarder, } @@ -36,10 +37,17 @@ impl Proxy<'_> { vm_mac_address: MacAddress, vm_net_type: NetType, allow: PrefixSet, + allow_only: bool, exposed_ports: Vec, ) -> Result> { let vm = VM::new(vm_fd)?; - let host = Host::new(vm_net_type, !allow.contains(&Ipv4Net::zero()))?; + let enable_isolation = if allow_only { + true + } else { + !allow.contains(&Ipv4Net::zero()) + }; + + let host = Host::new(vm_net_type, enable_isolation)?; let poller = Poller::new(vm.as_raw_fd(), host.as_raw_fd())?; Ok(Proxy { @@ -49,6 +57,7 @@ impl Proxy<'_> { vm_mac_address: smoltcp::wire::EthernetAddress(vm_mac_address.bytes()), dhcp_snooper: Default::default(), allow, + allow_only, enobufs_encountered: false, port_forwarder: PortForwarder::new(exposed_ports), }) diff --git a/lib/proxy/vm.rs b/lib/proxy/vm.rs index 53c7530..745aafd 100644 --- a/lib/proxy/vm.rs +++ b/lib/proxy/vm.rs @@ -61,22 +61,24 @@ impl Proxy<'_> { fn allowed_from_vm_ipv4(&self, ipv4_pkt: Ipv4Packet<&[u8]>) -> Option<()> { // Have we learned the VM's IP from the DHCP snooping? if let Some(lease) = &self.dhcp_snooper.lease() { - // If so, allow all global traffic let dst_addr = ipv4_pkt.dst_addr(); - let dst_is_global = ip_network::IpNetwork::from(dst_addr).is_global(); - if lease.valid_ip_source(ipv4_pkt.src_addr()) && dst_is_global { - return Some(()); - } - - // Also allow all traffic to the user-specified CIDRs + // Allow traffic explicitly permitted by the user-specified CIDRs let dst_net = Ipv4Net::from(dst_addr); // Use get_lpm() instead of get_spm() to work around prefix-trie // not handling prefixes like 0.0.0.0/0 correctly[1] // // [1]: https://github.com/tiborschneider/prefix-trie/issues/8 - if self.allow.get_lpm(&dst_net).is_some() { + if lease.valid_ip_source(ipv4_pkt.src_addr()) && self.allow.get_lpm(&dst_net).is_some() + { + return Some(()); + } + + if !self.allow_only + && lease.valid_ip_source(ipv4_pkt.src_addr()) + && ip_network::IpNetwork::from(dst_addr).is_global() + { return Some(()); } } diff --git a/src/main.rs b/src/main.rs index 492f793..695ed6b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -59,6 +59,12 @@ struct Args { )] allow: Vec, + #[clap( + long, + help = "if specified, only allow traffic to the CIDRs passed via --allow (besides host/DHCP/DNS)" + )] + allow_only: bool, + #[clap( long, help = "comma-separated list of TCP ports to expose (e.g. --expose 2222:22,8080:80)", @@ -171,12 +177,19 @@ fn try_main() -> anyhow::Result<()> { // Set bootpd(8) min/max lease time while still having the root privileges set_bootpd_lease_time(args.bootpd_lease_time); + if args.allow_only && args.allow.is_empty() { + return Err(anyhow!( + "--allow-only requires at least one CIDR supplied via --allow" + )); + } + // Initialize the proxy while still having the root privileges let mut proxy = Proxy::new( args.vm_fd as RawFd, args.vm_mac_address, args.vm_net_type, PrefixSet::from_iter(args.allow), + args.allow_only, args.expose, ) .context("failed to initialize proxy")?;