Compare commits

...
14 Commits
Author SHA1 Message Date
dependabot[bot] cf97e3878d Bump anyhow from 1.0.90 to 1.0.91 (#52)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.90 to 1.0.91.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.90...1.0.91)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-28 12:13:17 +04:00
dependabot[bot] 7f5293dd5e Bump libc from 0.2.159 to 0.2.161 (#50)
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.159 to 0.2.161.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.161/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.159...0.2.161)

---
updated-dependencies:
- dependency-name: libc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-21 11:53:12 +04:00
dependabot[bot] 9a2e59b844 Bump anyhow from 1.0.89 to 1.0.90 (#51)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.89 to 1.0.90.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.89...1.0.90)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-21 11:42:26 +04:00
dependabot[bot] 603c8b4889 Bump clap from 4.5.19 to 4.5.20 (#49) 2024-10-14 10:48:35 -04:00
Nikolay Edigaryev 24641d5325 $ cargo update (#48) 2024-10-03 23:54:02 +04:00
Nikolay Edigaryev ed64c139cf dependabot.yml: enable version updates for Cargo (#42) 2024-10-03 23:45:45 +04:00
Nikolay Edigaryev 8359992a08 $ cargo update (#40)
Also convert the "rust-toolchain" to "rust-toolchain.toml".
2024-07-23 14:33:31 +04:00
Nikolay Edigaryev 147c051b0e Disable isolation when --allow=0.0.0.0/0 is specified (#39)
* Disable isolation when --allow=0.0.0.0/0 is specified

* Upgrade & upgrade the dependencies
2024-07-02 15:36:07 +04:00
Nikolay Edigaryev 6456ed7228 README.md: proper nested bullet point syntax 2024-06-13 14:57:44 +04:00
Nikolay Edigaryev 56808c591f README.md: remove extraneous space 2024-06-13 14:57:02 +04:00
Nikolay Edigaryev eba21ed33e Link to Wikipedia's "ARP spoofing" article
And include some examples of the tools.
2024-06-13 14:56:01 +04:00
Nikolay Edigaryev 867679446e Ignore socketpair(2) errors when sending (#36) 2024-03-25 12:37:53 -04:00
Nikolay Edigaryev 4a13c5922b Prevent multiple --allow flags (#35)
* Prevent multiple --allow flags

* --allow: better value name instead of just <ALLOW>
2024-03-11 23:02:59 +04:00
Nikolay Edigaryev a92f4e0c99 Introduce --allow command-line argument to allow traffic to CIDRs (#34) 2024-03-11 22:07:39 +04:00
12 changed files with 1080 additions and 624 deletions
+6
View File
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
Generated
+960 -563
View File
File diff suppressed because it is too large Load Diff
+18 -16
View File
@@ -12,19 +12,21 @@ inherits = "release"
debug = true
[dependencies]
smoltcp = "0.8.1"
libc = "0.2.126"
polling = { git = "https://github.com/smol-rs/polling.git" }
dhcproto = "0.7.0"
vmnet = "0.1.1"
clap = { version = "3.1.18", features = ["derive"] }
mac_address = "1.1.3"
privdrop = "0.5.2"
anyhow = { version = "1.0.66", features = ["backtrace"] }
ip_network = "0.4.1"
uzers = "0.11.3"
system-configuration = "0.5.0"
num_enum = "0.5.7"
sentry = { version = "0.29.1", features = ["debug-images"] }
sentry-anyhow = { version = "0.29.1", features = ["backtrace"] }
nix = "0.26.2"
smoltcp = "0"
libc = "0"
polling = "3"
dhcproto = "0"
vmnet = "0"
clap = { version = "4", features = ["derive"] }
mac_address = "1"
privdrop = "0"
anyhow = { version = "1", features = ["backtrace"] }
ip_network = "0"
uzers = "0"
system-configuration = "0"
num_enum = "0"
sentry = { version = "0", features = ["debug-images"] }
sentry-anyhow = { version = "0", features = ["backtrace"] }
nix = { version = "0", features = ["signal"] }
prefix-trie = "0"
ipnet = "2"
+2 -2
View File
@@ -8,9 +8,9 @@ Please check out [this blog post](https://cirrus-ci.org/blog/2022/07/07/isolatin
Softnet solves two problems:
1. VM network isolation
* [`VZNATNetworkDeviceAttachment`](https://developer.apple.com/documentation/virtualization/vznatnetworkdeviceattachment) (the default networking in Tart) enables [vmnet's bridge isolation](https://developer.apple.com/documentation/vmnet/vmnet_enable_isolation_key) by default and prevents cross-VM traffic, however it's still possible for any VM to spoof the host's ARP-table and capture other VMs traffic
* [`VZNATNetworkDeviceAttachment`](https://developer.apple.com/documentation/virtualization/vznatnetworkdeviceattachment) (the default networking in Tart) enables [vmnet's bridge isolation](https://developer.apple.com/documentation/vmnet/vmnet_enable_isolation_key) by default and prevents cross-VM traffic, however it's still possible for any VM to spoof the host's ARP-table and capture other VMs traffic by using tools that enable conducting the [ARP spoofing attacks](https://en.wikipedia.org/wiki/ARP_spoofing) (e.g. [arpspoof](https://www.monkey.org/~dugsong/dsniff/), [arpoison](http://www.arpoison.net/) and so on)
2. DHCP exhaustion
* macOS built-in DHCP-server allocates a `/24` subnet with 86400 seconds lease time by default, which only allows for ~253 VMs a day (or 1 VM every ~6 minutes) to be spawned without causing a denial-of-service, which is pretty limiting for CI services like Cirrus CI
* macOS built-in DHCP-server allocates a `/24` subnet with 86400 seconds lease time by default, which only allows for ~253 VMs a day (or 1 VM every ~6 minutes) to be spawned without causing a denial-of-service, which is pretty limiting for CI services like Cirrus CI
And assumes that:
+19 -9
View File
@@ -1,5 +1,5 @@
use clap::ArgEnum;
use anyhow::{anyhow, Context, Result};
use clap::ValueEnum;
use std::net::Ipv4Addr;
use std::os::unix::io::{AsRawFd, RawFd};
use std::os::unix::net::UnixDatagram;
@@ -9,7 +9,7 @@ use vmnet::mode::Mode;
use vmnet::parameters::{Parameter, ParameterKind};
use vmnet::{Events, Options};
#[derive(ArgEnum, Clone, Debug)]
#[derive(ValueEnum, Clone, Debug)]
pub enum NetType {
/// Shared network
///
@@ -31,7 +31,7 @@ pub struct Host {
}
impl Host {
pub fn new(vm_net_type: NetType) -> Result<Host> {
pub fn new(vm_net_type: NetType, enable_isolation: bool) -> Result<Host> {
// Initialize a vmnet.framework NAT or Host interface with isolation enabled
let mut interface = vmnet::Interface::new(
match vm_net_type {
@@ -39,22 +39,30 @@ impl Host {
NetType::Host => Mode::Host(Default::default()),
},
Options {
enable_isolation: Some(true),
enable_isolation: Some(enable_isolation),
..Default::default()
},
)
.context("failed to initialize vmnet interface")?;
// Retrieve first IP (gateway) used for this interface
let Some(Parameter::StartAddress(gateway_ip)) = interface.parameters().get(ParameterKind::StartAddress) else {
return Err(anyhow!("failed to retrieve vmnet's interface start address"));
let Some(Parameter::StartAddress(gateway_ip)) =
interface.parameters().get(ParameterKind::StartAddress)
else {
return Err(anyhow!(
"failed to retrieve vmnet's interface start address"
));
};
let gateway_ip = Ipv4Addr::from_str(&gateway_ip)
.context("failed to parse vmnet's interface start address")?;
// Retrieve max packet size for this interface
let Some(Parameter::MaxPacketSize(max_packet_size)) = interface.parameters().get(ParameterKind::MaxPacketSize) else {
return Err(anyhow!("failed to retrieve vmnet's interface max packet size"));
let Some(Parameter::MaxPacketSize(max_packet_size)) =
interface.parameters().get(ParameterKind::MaxPacketSize)
else {
return Err(anyhow!(
"failed to retrieve vmnet's interface max packet size"
));
};
// Set up a socketpair() to emulate polling of the vmnet interface
@@ -66,7 +74,9 @@ impl Host {
interface
.set_event_callback(Events::PACKETS_AVAILABLE, move |_mask, _params| {
// Send a dummy datagram to make the other end of socketpair() readable
new_packets_tx.send(&[0; 1]).unwrap();
// and ignore the error as this merely a signalling channel to wake up
// the poller
new_packets_tx.send(&[0; 1]).ok();
// Wait for the permission to continue to avoid
// wasting CPU cycles or in case of termination,
+18 -16
View File
@@ -2,14 +2,15 @@ use anyhow::Result;
use num_enum::IntoPrimitive;
use polling::os::kqueue::PollerKqueueExt;
use polling::PollMode;
use std::os::fd::{AsRawFd, BorrowedFd};
use std::os::unix::io::RawFd;
use std::time::Duration;
pub struct Poller {
pub struct Poller<'poller> {
poller: polling::Poller,
events: Vec<polling::Event>,
vm_fd: RawFd,
host_fd: RawFd,
events: polling::Events,
vm_fd: BorrowedFd<'poller>,
host_fd: BorrowedFd<'poller>,
}
#[derive(IntoPrimitive)]
@@ -20,22 +21,25 @@ enum EventKey {
Interrupt,
}
impl Poller {
pub fn new(vm_fd: RawFd, host_fd: RawFd) -> Result<Poller> {
impl Poller<'_> {
pub fn new<'poller>(vm_fd: RawFd, host_fd: RawFd) -> Result<Poller<'poller>> {
let poller = polling::Poller::new()?;
Ok(Poller {
poller,
events: Vec::new(),
vm_fd,
host_fd,
events: polling::Events::new(),
vm_fd: unsafe { BorrowedFd::borrow_raw(vm_fd) },
host_fd: unsafe { BorrowedFd::borrow_raw(host_fd) },
})
}
pub fn arm(&self) -> Result<()> {
self.poller.add(self.vm_fd as RawFd, self.vm_interest())?;
self.poller
.add(self.host_fd as RawFd, self.host_interest())?;
unsafe {
self.poller
.add(self.vm_fd.as_raw_fd(), self.vm_interest())?;
self.poller
.add(self.host_fd.as_raw_fd(), self.host_interest())?;
}
let interrupt_signal = polling::os::kqueue::Signal(libc::SIGINT);
self.poller
@@ -52,10 +56,8 @@ impl Poller {
pub fn rearm(&mut self) -> Result<()> {
self.events.clear();
self.poller
.modify(self.vm_fd as RawFd, self.vm_interest())?;
self.poller
.modify(self.host_fd as RawFd, self.host_interest())?;
self.poller.modify(self.vm_fd, self.vm_interest())?;
self.poller.modify(self.host_fd, self.host_interest())?;
let interrupt_signal = polling::os::kqueue::Signal(libc::SIGINT);
self.poller.modify_filter(
+2 -2
View File
@@ -3,7 +3,7 @@ use crate::proxy::Proxy;
use anyhow::{Context, Result};
use smoltcp::wire::{EthernetFrame, EthernetProtocol, Ipv4Packet, UdpPacket};
impl Proxy {
impl Proxy<'_> {
pub(crate) fn process_frame_from_host(&mut self, frame: &EthernetFrame<&[u8]>) -> Result<()> {
if self.allowed_from_host(frame).is_none() {
// Block packet by not forwarding it to the VM
@@ -58,7 +58,7 @@ impl Proxy {
return;
}
if ipv4_pkt.protocol() != smoltcp::wire::IpProtocol::Udp {
if ipv4_pkt.next_header() != smoltcp::wire::IpProtocol::Udp {
return;
}
+14 -5
View File
@@ -8,24 +8,32 @@ use crate::host::NetType;
use crate::poller::Poller;
use crate::vm::VM;
use anyhow::Result;
use ipnet::Ipv4Net;
use mac_address::MacAddress;
use prefix_trie::{Prefix, PrefixSet};
use smoltcp::wire::EthernetFrame;
use std::io::ErrorKind;
use std::os::unix::io::{AsRawFd, RawFd};
pub struct Proxy {
pub struct Proxy<'proxy> {
vm: VM,
host: Host,
poller: Poller,
poller: Poller<'proxy>,
vm_mac_address: smoltcp::wire::EthernetAddress,
dhcp_snooper: DhcpSnooper,
allow: PrefixSet<Ipv4Net>,
enobufs_encountered: bool,
}
impl Proxy {
pub fn new(vm_fd: RawFd, vm_mac_address: MacAddress, vm_net_type: NetType) -> Result<Proxy> {
impl Proxy<'_> {
pub fn new<'proxy>(
vm_fd: RawFd,
vm_mac_address: MacAddress,
vm_net_type: NetType,
allow: PrefixSet<Ipv4Net>,
) -> Result<Proxy<'proxy>> {
let vm = VM::new(vm_fd)?;
let host = Host::new(vm_net_type)?;
let host = Host::new(vm_net_type, !allow.contains(&Ipv4Net::zero()))?;
let poller = Poller::new(vm.as_raw_fd(), host.as_raw_fd())?;
Ok(Proxy {
@@ -34,6 +42,7 @@ impl Proxy {
poller,
vm_mac_address: smoltcp::wire::EthernetAddress(vm_mac_address.bytes()),
dhcp_snooper: Default::default(),
allow,
enobufs_encountered: false,
})
}
+18 -6
View File
@@ -2,12 +2,13 @@ use crate::proxy::udp_packet_helper::UdpPacketHelper;
use crate::proxy::Proxy;
use anyhow::Context;
use anyhow::Result;
use ipnet::Ipv4Net;
use smoltcp::wire::{
ArpPacket, EthernetFrame, EthernetProtocol, IpProtocol, Ipv4Packet, UdpPacket,
};
use std::net::Ipv4Addr;
impl Proxy {
impl Proxy<'_> {
pub(crate) fn process_frame_from_vm(&mut self, frame: EthernetFrame<&[u8]>) -> Result<()> {
if self.allowed_from_vm(&frame).is_none() {
// Block packet by not forwarding it to the host
@@ -58,15 +59,26 @@ impl Proxy {
}
fn allowed_from_vm_ipv4(&self, ipv4_pkt: Ipv4Packet<&[u8]>) -> Option<()> {
// Once we've learned the VM's IP from the DHCP snooping,
// allow all global traffic for that VM's IP
// Have we learned the VM's IP from the DHCP snooping?
if let Some(lease) = &self.dhcp_snooper.lease() {
let dst_is_global =
ip_network::IpNetwork::from(Ipv4Addr::from(ipv4_pkt.dst_addr().0)).is_global();
// If so, allow all global traffic
let dst_addr = Ipv4Addr::from(ipv4_pkt.dst_addr().0);
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
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() {
return Some(());
}
}
// Allow communication with host
@@ -74,7 +86,7 @@ impl Proxy {
return Some(());
}
if ipv4_pkt.protocol() == IpProtocol::Udp {
if ipv4_pkt.next_header() == IpProtocol::Udp {
let udp_pkt = UdpPacket::new_checked(ipv4_pkt.payload()).ok()?;
// Allow DNS communication with the DNS-servers provided by DHCP
-1
View File
@@ -1 +0,0 @@
nightly
+2
View File
@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"
+21 -4
View File
@@ -1,11 +1,14 @@
use anyhow::{anyhow, Context};
use clap::Parser;
use ipnet::Ipv4Net;
use nix::sys::signal::{signal, SigHandler, Signal};
use prefix_trie::PrefixSet;
use privdrop::PrivDrop;
use softnet::NetType;
use softnet::proxy::Proxy;
use softnet::NetType;
use std::borrow::Cow;
use std::env;
use std::os::raw::c_int;
use std::os::unix::io::RawFd;
use std::os::unix::process::CommandExt;
@@ -29,7 +32,7 @@ struct Args {
#[clap(long, help = "MAC address to enforce for the VM")]
vm_mac_address: mac_address::MacAddress,
#[clap(long, arg_enum, help = "type of network to use for the VM", default_value_t=NetType::Nat)]
#[clap(long, value_enum, help = "type of network to use for the VM", default_value_t=NetType::Nat)]
vm_net_type: NetType,
#[clap(
@@ -45,6 +48,15 @@ struct Args {
#[clap(long, help = "group name to drop privileges to")]
group: Option<String>,
#[clap(
long,
help = "comma-separated list of CIDRs to allow the traffic to (e.g. --allow=192.168.0.0/24)",
value_name = "comma-separated CIDRs",
use_value_delimiter = true,
action = clap::ArgAction::Set
)]
allow: Vec<Ipv4Net>,
#[clap(long, hide = true)]
sudo_escalation_probing: bool,
@@ -144,8 +156,13 @@ fn try_main() -> anyhow::Result<()> {
set_bootpd_lease_time(args.bootpd_lease_time);
// 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)
.context("failed to initialize proxy")?;
let mut proxy = Proxy::new(
args.vm_fd as RawFd,
args.vm_mac_address,
args.vm_net_type,
PrefixSet::from_iter(args.allow),
)
.context("failed to initialize proxy")?;
// Drop effective privileges to the user
// and group which have had invoked us