Compare commits

...
10 Commits
Author SHA1 Message Date
Nikolay Edigaryev cd5f1d2f4f Fix unaligned read by switching from unmaintained users crate (#26)
* Fix unaligned read by switching from unmaintained users crate

* Bump proc-macro2 to to fix "unknown feature `proc_macro_span_shrink`"
2023-09-12 06:59:02 -04:00
Fedor Korotkov a775a92772 Add link to the blog post (#25) 2023-05-05 03:19:40 +04:00
Nikolay Edigaryev f38d65f98f README.md: Tart now uses --net-softnet 2023-03-28 08:21:19 +04:00
dependabot[bot] 4ba480ff4f Bump openssl from 0.10.45 to 0.10.48 (#24)
Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.45 to 0.10.48.
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.45...openssl-v0.10.48)

---
updated-dependencies:
- dependency-name: openssl
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-25 16:20:34 +04:00
Nikolay Edigaryev d7699e95a9 Support graceful termination via SIGINT (#23)
* Support graceful termination via SIGINT

* $ cargo fmt

* Explain why we need to ignore the SIGINT
2023-03-16 18:38:03 +04:00
dependabot[bot] 535e03c97f Bump tokio from 1.23.0 to 1.25.0 (#21)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.23.0 to 1.25.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.23.0...tokio-1.25.0)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-05 19:46:48 +04:00
Nikolay Edigaryev d635751948 Capture ENOBUFS message into Sentry only once (#19) 2022-12-24 11:36:42 +04:00
Fedor KorotkovandNikolay Edigaryev e71b32a8dd Populate Sentry Release (#17)
* Populate Sentry Release

* Reverted new line

* Update src/main.rs

Co-authored-by: Nikolay Edigaryev <edigaryev@gmail.com>

Co-authored-by: Nikolay Edigaryev <edigaryev@gmail.com>
2022-12-14 17:17:34 -05:00
Nikolay Edigaryev 59cd9098e0 Ignore ENOBUFS when writing to VM's socket (#18)
* Ignore ENOBUFS when writing to VM's socket

* Hint the into() target type to the compiler to fix the build error

* Fix Clippy warnings
2022-12-15 00:15:19 +04:00
Nikolay Edigaryev 4ab3cd7e5c Switch back to nightly toolchain (#16) 2022-12-14 23:18:24 +04:00
9 changed files with 462 additions and 337 deletions
+8 -5
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -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"
+3 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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()?;
} }
} }
+1
View File
@@ -0,0 +1 @@
nightly
+13 -2
View File
@@ -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