mirror of
https://github.com/cirruslabs/softnet.git
synced 2026-10-19 07:52:33 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd5f1d2f4f | ||
|
|
a775a92772 | ||
|
|
f38d65f98f | ||
|
|
4ba480ff4f | ||
|
|
d7699e95a9 | ||
|
|
535e03c97f | ||
|
|
d635751948 | ||
|
|
e71b32a8dd | ||
|
|
59cd9098e0 | ||
|
|
4ab3cd7e5c |
+8
-5
@@ -23,12 +23,11 @@ task:
|
|||||||
SENTRY_AUTH_TOKEN: ENCRYPTED[!c16a5cf7da5f856b4bc2f21fe8cb7aa2a6c981f851c094ed4d3025fd02ea59a58a86cee8b193a69a1fc20fa217e56ac3!]
|
SENTRY_AUTH_TOKEN: ENCRYPTED[!c16a5cf7da5f856b4bc2f21fe8cb7aa2a6c981f851c094ed4d3025fd02ea59a58a86cee8b193a69a1fc20fa217e56ac3!]
|
||||||
install_rust_script:
|
install_rust_script:
|
||||||
- curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
- curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||||
install_goreleaser_script:
|
install_script:
|
||||||
- brew install go goreleaser/tap/goreleaser-pro
|
- brew install go goreleaser/tap/goreleaser-pro getsentry/tools/sentry-cli
|
||||||
build_script:
|
build_script:
|
||||||
- cargo build --profile release-with-debug
|
- cargo build --profile release-with-debug
|
||||||
install_sentry_cli_script:
|
release_script: goreleaser
|
||||||
- brew install getsentry/tools/sentry-cli
|
|
||||||
upload_sentry_debug_files_script:
|
upload_sentry_debug_files_script:
|
||||||
- cd target/release-with-debug/
|
- cd target/release-with-debug/
|
||||||
# Generate and upload symbols
|
# Generate and upload symbols
|
||||||
@@ -37,4 +36,8 @@ task:
|
|||||||
# Bundle and upload sources
|
# Bundle and upload sources
|
||||||
- sentry-cli debug-files bundle-sources softnet.dSYM/
|
- sentry-cli debug-files bundle-sources softnet.dSYM/
|
||||||
- sentry-cli debug-files upload -o $SENTRY_ORG -p $SENTRY_PROJECT softnet.src.zip
|
- sentry-cli debug-files upload -o $SENTRY_ORG -p $SENTRY_PROJECT softnet.src.zip
|
||||||
release_script: goreleaser
|
create_sentry_release_script:
|
||||||
|
- export SENTRY_RELEASE="softnet@$CIRRUS_TAG"
|
||||||
|
- sentry-cli releases new $SENTRY_RELEASE
|
||||||
|
- sentry-cli releases set-commits $SENTRY_RELEASE --auto
|
||||||
|
- sentry-cli releases finalize $SENTRY_RELEASE
|
||||||
|
|||||||
Generated
+375
-317
File diff suppressed because it is too large
Load Diff
+3
-2
@@ -14,7 +14,7 @@ debug = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
smoltcp = "0.8.1"
|
smoltcp = "0.8.1"
|
||||||
libc = "0.2.126"
|
libc = "0.2.126"
|
||||||
polling = "2.2.0"
|
polling = { git = "https://github.com/smol-rs/polling.git" }
|
||||||
dhcproto = "0.7.0"
|
dhcproto = "0.7.0"
|
||||||
vmnet = "0.1.1"
|
vmnet = "0.1.1"
|
||||||
clap = { version = "3.1.18", features = ["derive"] }
|
clap = { version = "3.1.18", features = ["derive"] }
|
||||||
@@ -22,8 +22,9 @@ mac_address = "1.1.3"
|
|||||||
privdrop = "0.5.2"
|
privdrop = "0.5.2"
|
||||||
anyhow = { version = "1.0.66", features = ["backtrace"] }
|
anyhow = { version = "1.0.66", features = ["backtrace"] }
|
||||||
ip_network = "0.4.1"
|
ip_network = "0.4.1"
|
||||||
users = "0.11.0"
|
uzers = "0.11.3"
|
||||||
system-configuration = "0.5.0"
|
system-configuration = "0.5.0"
|
||||||
num_enum = "0.5.7"
|
num_enum = "0.5.7"
|
||||||
sentry = { version = "0.29.1", features = ["debug-images"] }
|
sentry = { version = "0.29.1", features = ["debug-images"] }
|
||||||
sentry-anyhow = { version = "0.29.1", features = ["backtrace"] }
|
sentry-anyhow = { version = "0.29.1", features = ["backtrace"] }
|
||||||
|
nix = "0.26.2"
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
# Softnet
|
# Softnet
|
||||||
|
|
||||||
Softnet is a software networking for [Tart](https://github.com/cirruslabs/tart) which provides better network isolation and alleviates DHCP shortage on production systems.
|
Softnet is a software networking for [Tart](https://github.com/cirruslabs/tart) which provides better network isolation and alleviates DHCP shortage on production systems.
|
||||||
|
Please check out [this blog post](https://cirrus-ci.org/blog/2022/07/07/isolating-network-between-tarts-macos-virtual-machines/) for backstory.
|
||||||
|
|
||||||
## Working model
|
## Working model
|
||||||
|
|
||||||
Softnet solves two problems:
|
Softnet solves two problems:
|
||||||
|
|
||||||
1. VM network isolation
|
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, for example
|
* [`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
|
||||||
2. DHCP exhaustion
|
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
|
||||||
|
|
||||||
@@ -30,4 +31,4 @@ For proper functioning, Softnet binary requires two things:
|
|||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
Softnet is started and managed automatically by Tart if `--with-softnet` flag is present when calling `tart run`.
|
Softnet is started and managed automatically by Tart if `--net-softnet` flag is provided when calling `tart run`.
|
||||||
|
|||||||
+33
-4
@@ -1,5 +1,7 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use num_enum::IntoPrimitive;
|
use num_enum::IntoPrimitive;
|
||||||
|
use polling::os::kqueue::PollerKqueueExt;
|
||||||
|
use polling::PollMode;
|
||||||
use std::os::unix::io::RawFd;
|
use std::os::unix::io::RawFd;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -15,6 +17,7 @@ pub struct Poller {
|
|||||||
enum EventKey {
|
enum EventKey {
|
||||||
VM,
|
VM,
|
||||||
Host,
|
Host,
|
||||||
|
Interrupt,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Poller {
|
impl Poller {
|
||||||
@@ -34,6 +37,15 @@ impl Poller {
|
|||||||
self.poller
|
self.poller
|
||||||
.add(self.host_fd as RawFd, self.host_interest())?;
|
.add(self.host_fd as RawFd, self.host_interest())?;
|
||||||
|
|
||||||
|
let interrupt_signal = polling::os::kqueue::Signal(libc::SIGINT);
|
||||||
|
self.poller
|
||||||
|
.add_filter(
|
||||||
|
interrupt_signal,
|
||||||
|
EventKey::Interrupt.into(),
|
||||||
|
PollMode::Oneshot,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,17 +57,34 @@ impl Poller {
|
|||||||
self.poller
|
self.poller
|
||||||
.modify(self.host_fd as RawFd, self.host_interest())?;
|
.modify(self.host_fd as RawFd, self.host_interest())?;
|
||||||
|
|
||||||
|
let interrupt_signal = polling::os::kqueue::Signal(libc::SIGINT);
|
||||||
|
self.poller.modify_filter(
|
||||||
|
interrupt_signal,
|
||||||
|
EventKey::Interrupt.into(),
|
||||||
|
PollMode::Oneshot,
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn wait(&mut self) -> Result<(bool, bool)> {
|
pub fn wait(&mut self) -> Result<(bool, bool, bool)> {
|
||||||
self.poller
|
self.poller
|
||||||
.wait(&mut self.events, Some(Duration::from_millis(100)))?;
|
.wait(&mut self.events, Some(Duration::from_millis(100)))?;
|
||||||
|
|
||||||
let vm_readable = self.events.iter().any(|ev| ev.key == EventKey::VM.into());
|
let vm_readable = self
|
||||||
let host_readable = self.events.iter().any(|ev| ev.key == EventKey::Host.into());
|
.events
|
||||||
|
.iter()
|
||||||
|
.any(|ev| ev.key == Into::<usize>::into(EventKey::VM));
|
||||||
|
let host_readable = self
|
||||||
|
.events
|
||||||
|
.iter()
|
||||||
|
.any(|ev| ev.key == Into::<usize>::into(EventKey::Host));
|
||||||
|
let interrupt = self
|
||||||
|
.events
|
||||||
|
.iter()
|
||||||
|
.any(|ev| ev.key == Into::<usize>::into(EventKey::Interrupt));
|
||||||
|
|
||||||
Ok((vm_readable, host_readable))
|
Ok((vm_readable, host_readable, interrupt))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn vm_interest(&self) -> polling::Event {
|
fn vm_interest(&self) -> polling::Event {
|
||||||
|
|||||||
+18
-4
@@ -16,10 +16,24 @@ impl Proxy {
|
|||||||
self.snoop(frame);
|
self.snoop(frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.vm
|
match self.vm.write(frame.as_ref()) {
|
||||||
.write(frame.as_ref())
|
Ok(_) => Ok(()),
|
||||||
.map(|_| ())
|
Err(err) => {
|
||||||
.context("failed to write to the VM")
|
if let Some(libc::ENOBUFS) = err.raw_os_error() {
|
||||||
|
if !self.enobufs_encountered {
|
||||||
|
sentry::capture_message(
|
||||||
|
"No buffer space available in VM's socket",
|
||||||
|
sentry::Level::Warning,
|
||||||
|
);
|
||||||
|
self.enobufs_encountered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(err).context("failed to write to the VM")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn allowed_from_host(&mut self, frame: &EthernetFrame<&[u8]>) -> Option<()> {
|
fn allowed_from_host(&mut self, frame: &EthernetFrame<&[u8]>) -> Option<()> {
|
||||||
|
|||||||
+8
-1
@@ -18,6 +18,7 @@ pub struct Proxy {
|
|||||||
poller: Poller,
|
poller: Poller,
|
||||||
vm_mac_address: smoltcp::wire::EthernetAddress,
|
vm_mac_address: smoltcp::wire::EthernetAddress,
|
||||||
dhcp_snooper: DhcpSnooper,
|
dhcp_snooper: DhcpSnooper,
|
||||||
|
enobufs_encountered: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Proxy {
|
impl Proxy {
|
||||||
@@ -32,6 +33,7 @@ impl Proxy {
|
|||||||
poller,
|
poller,
|
||||||
vm_mac_address: smoltcp::wire::EthernetAddress(vm_mac_address.bytes()),
|
vm_mac_address: smoltcp::wire::EthernetAddress(vm_mac_address.bytes()),
|
||||||
dhcp_snooper: Default::default(),
|
dhcp_snooper: Default::default(),
|
||||||
|
enobufs_encountered: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +43,7 @@ impl Proxy {
|
|||||||
self.poller.arm()?;
|
self.poller.arm()?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let (vm_readable, host_readable) = self.poller.wait()?;
|
let (vm_readable, host_readable, interrupt) = self.poller.wait()?;
|
||||||
|
|
||||||
if vm_readable {
|
if vm_readable {
|
||||||
self.read_from_vm(buf.as_mut_slice())?;
|
self.read_from_vm(buf.as_mut_slice())?;
|
||||||
@@ -51,6 +53,11 @@ impl Proxy {
|
|||||||
self.read_from_host(buf.as_mut_slice())?;
|
self.read_from_host(buf.as_mut_slice())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Graceful termination
|
||||||
|
if interrupt {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
self.poller.rearm()?;
|
self.poller.rearm()?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
nightly
|
||||||
+13
-2
@@ -1,7 +1,9 @@
|
|||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use nix::sys::signal::{signal, SigHandler, Signal};
|
||||||
use privdrop::PrivDrop;
|
use privdrop::PrivDrop;
|
||||||
use softnet::proxy::Proxy;
|
use softnet::proxy::Proxy;
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::os::raw::c_int;
|
use std::os::raw::c_int;
|
||||||
use std::os::unix::io::RawFd;
|
use std::os::unix::io::RawFd;
|
||||||
@@ -13,7 +15,7 @@ use system_configuration::core_foundation::number::CFNumber;
|
|||||||
use system_configuration::core_foundation::string::CFString;
|
use system_configuration::core_foundation::string::CFString;
|
||||||
use system_configuration::preferences::SCPreferences;
|
use system_configuration::preferences::SCPreferences;
|
||||||
use system_configuration::sys::preferences::{SCPreferencesCommitChanges, SCPreferencesSetValue};
|
use system_configuration::sys::preferences::{SCPreferencesCommitChanges, SCPreferencesSetValue};
|
||||||
use users::{get_current_groupname, get_current_username, get_effective_uid};
|
use uzers::{get_current_groupname, get_current_username, get_effective_uid};
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
struct Args {
|
struct Args {
|
||||||
@@ -54,13 +56,14 @@ fn main() -> ExitCode {
|
|||||||
|
|
||||||
// Initialize Sentry
|
// Initialize Sentry
|
||||||
let _sentry = sentry::init(sentry::ClientOptions {
|
let _sentry = sentry::init(sentry::ClientOptions {
|
||||||
|
release: option_env!("CIRRUS_TAG").map(|tag| Cow::from(format!("softnet@{tag}"))),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Enrich future events with Cirrus CI-specific tags
|
// Enrich future events with Cirrus CI-specific tags
|
||||||
if let Ok(tags) = env::var("CIRRUS_SENTRY_TAGS") {
|
if let Ok(tags) = env::var("CIRRUS_SENTRY_TAGS") {
|
||||||
sentry::configure_scope(|scope| {
|
sentry::configure_scope(|scope| {
|
||||||
for (key, value) in tags.split(",").map(|tag| tag.split_once("=")).flatten() {
|
for (key, value) in tags.split(',').filter_map(|tag| tag.split_once('=')) {
|
||||||
scope.set_tag(key, value);
|
scope.set_tag(key, value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -82,6 +85,14 @@ fn main() -> ExitCode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn try_main() -> anyhow::Result<()> {
|
fn try_main() -> anyhow::Result<()> {
|
||||||
|
// The default signal(3)[1] action for SIGINT is to interrupt program,
|
||||||
|
// but we want to handle SIGINT ourselves, so we ignore it. The kqueue(2)'s[2]
|
||||||
|
// EVFILT_SIGNAL will receive it anyways, because it has lower precedence.
|
||||||
|
//
|
||||||
|
// [1]: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/signal.3.html
|
||||||
|
// [2]: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/kqueue.2.html
|
||||||
|
unsafe { signal(Signal::SIGINT, SigHandler::SigIgn) }?;
|
||||||
|
|
||||||
let args: Args = Args::parse();
|
let args: Args = Args::parse();
|
||||||
|
|
||||||
// No need to run anything, just return
|
// No need to run anything, just return
|
||||||
|
|||||||
Reference in New Issue
Block a user