Compare commits

..
Author SHA1 Message Date
fedor a540299219 fixed build 2024-03-07 07:25:01 -05:00
fedor 16864f38f0 Allow traffic to private networks 2024-03-07 05:57:53 -05:00
Sergei Parshev 0a92c290be Added a way to enable host-only networking through tart using --net-host (#32)
* Added a way to enable host-only networking through tart using SOFTNET_NET_TYPE=host

* Removed env variable and moved to Enum instead of str

* Fixed defaults & restricted publicity of host

* Fixed usage of NetType
2024-03-01 11:25:19 -05:00
Fedor Korotkov f5a1b1cdbd Goreleaser Fix (#30)
* Goreleaser Bug

Theoretically this config should work but it doesn't

* Specify `goamd64`
2024-01-24 19:09:59 +04:00
Fedor Korotkov 817dbb6e32 Fixed x86_64 Homebrew update (#29)
Seems we need to build separate archives too and pass it to `brew`.
2024-01-24 07:30:36 -05:00
Fedor Korotkov 5f3b371e93 Build for x86_64 (#28) 2024-01-24 15:23:04 +04:00
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
13 changed files with 483 additions and 353 deletions
+7 -5
View File
@@ -4,17 +4,18 @@ env:
task:
name: Build
macos_instance:
image: ghcr.io/cirruslabs/macos-monterey-xcode:14
image: ghcr.io/cirruslabs/macos-sonoma-xcode:latest
install_rust_script:
- curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
- rustup target add x86_64-apple-darwin
build_script:
- cargo build
- cargo build --target aarch64-apple-darwin --target x86_64-apple-darwin
task:
name: Release
only_if: $CIRRUS_TAG != ''
macos_instance:
image: ghcr.io/cirruslabs/macos-monterey-xcode:14
image: ghcr.io/cirruslabs/macos-sonoma-xcode:latest
env:
GITHUB_TOKEN: ENCRYPTED[!98ace8259c6024da912c14d5a3c5c6aac186890a8d4819fad78f3e0c41a4e0cd3a2537dd6e91493952fb056fa434be7c!]
GORELEASER_KEY: ENCRYPTED[!9b80b6ef684ceaf40edd4c7af93014ee156c8aba7e6e5795f41c482729887b5c31f36b651491d790f1f668670888d9fd!]
@@ -23,13 +24,14 @@ task:
SENTRY_AUTH_TOKEN: ENCRYPTED[!c16a5cf7da5f856b4bc2f21fe8cb7aa2a6c981f851c094ed4d3025fd02ea59a58a86cee8b193a69a1fc20fa217e56ac3!]
install_rust_script:
- curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
- rustup target add x86_64-apple-darwin
install_script:
- brew install go goreleaser/tap/goreleaser-pro getsentry/tools/sentry-cli
build_script:
- cargo build --profile release-with-debug
- cargo build --target aarch64-apple-darwin --target x86_64-apple-darwin --profile release-with-debug
release_script: goreleaser
upload_sentry_debug_files_script:
- cd target/release-with-debug/
- cd target/aarch64-apple-darwin/release-with-debug/
# Generate and upload symbols
- dsymutil softnet
- sentry-cli debug-files upload -o $SENTRY_ORG -p $SENTRY_PROJECT softnet.dSYM/
+2
View File
@@ -1 +1,3 @@
/.idea
/dist
/target
+7 -9
View File
@@ -1,29 +1,27 @@
project_name: softnet
builds:
- builder: prebuilt
- id: softnet
builder: prebuilt
goamd64: [v1]
goos:
- darwin
goarch:
- arm64
- amd64
prebuilt:
path: "target/release-with-debug/softnet"
path: 'target/{{- if eq .Arch "arm64" }}aarch64{{- else }}x86_64{{ end }}-apple-darwin/release-with-debug/softnet'
archives:
- id: binary
format: binary
name_template: "{{ .ProjectName }}"
- id: regular
name_template: "{{ .ProjectName }}"
name_template: "{{ .ProjectName }}-{{ .Arch }}"
release:
prerelease: auto
brews:
- name: softnet
ids:
- regular
tap:
repository:
owner: cirruslabs
name: homebrew-cli
caveats: See the Github repository for more information
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]
smoltcp = "0.8.1"
libc = "0.2.126"
polling = "2.2.0"
polling = { git = "https://github.com/smol-rs/polling.git" }
dhcproto = "0.7.0"
vmnet = "0.1.1"
clap = { version = "3.1.18", features = ["derive"] }
@@ -22,8 +22,9 @@ mac_address = "1.1.3"
privdrop = "0.5.2"
anyhow = { version = "1.0.66", features = ["backtrace"] }
ip_network = "0.4.1"
users = "0.11.0"
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"
+3 -2
View File
@@ -1,13 +1,14 @@
# 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.
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
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, 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
* 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
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`.
+19 -3
View File
@@ -1,3 +1,4 @@
use clap::ArgEnum;
use anyhow::{anyhow, Context, Result};
use std::net::Ipv4Addr;
use std::os::unix::io::{AsRawFd, RawFd};
@@ -8,6 +9,18 @@ use vmnet::mode::Mode;
use vmnet::parameters::{Parameter, ParameterKind};
use vmnet::{Events, Options};
#[derive(ArgEnum, Clone, Debug)]
pub enum NetType {
/// Shared network
///
/// Uses NAT-translation to give guests access to the global network
Nat,
/// Host network
///
/// Guests will be able to talk only to the host without access to global network
Host,
}
pub struct Host {
interface: vmnet::Interface,
new_packets_rx: UnixDatagram,
@@ -18,10 +31,13 @@ pub struct Host {
}
impl Host {
pub fn new() -> Result<Host> {
// Initialize a vmnet.framework NAT interface with isolation enabled
pub fn new(vm_net_type: NetType) -> Result<Host> {
// Initialize a vmnet.framework NAT or Host interface with isolation enabled
let mut interface = vmnet::Interface::new(
Mode::Shared(Default::default()),
match vm_net_type {
NetType::Nat => Mode::Shared(Default::default()),
NetType::Host => Mode::Host(Default::default()),
},
Options {
enable_isolation: Some(true),
..Default::default()
+1
View File
@@ -1,5 +1,6 @@
mod dhcp_snooper;
mod host;
pub use host::NetType;
mod poller;
pub mod proxy;
mod vm;
+25 -2
View File
@@ -1,5 +1,7 @@
use anyhow::Result;
use num_enum::IntoPrimitive;
use polling::os::kqueue::PollerKqueueExt;
use polling::PollMode;
use std::os::unix::io::RawFd;
use std::time::Duration;
@@ -15,6 +17,7 @@ pub struct Poller {
enum EventKey {
VM,
Host,
Interrupt,
}
impl Poller {
@@ -34,6 +37,15 @@ impl Poller {
self.poller
.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(())
}
@@ -45,10 +57,17 @@ impl Poller {
self.poller
.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(())
}
pub fn wait(&mut self) -> Result<(bool, bool)> {
pub fn wait(&mut self) -> Result<(bool, bool, bool)> {
self.poller
.wait(&mut self.events, Some(Duration::from_millis(100)))?;
@@ -60,8 +79,12 @@ impl Poller {
.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 {
+7 -4
View File
@@ -20,10 +20,13 @@ impl Proxy {
Ok(_) => Ok(()),
Err(err) => {
if let Some(libc::ENOBUFS) = err.raw_os_error() {
sentry::capture_message(
"No buffer space available in VM's socket",
sentry::Level::Warning,
);
if !self.enobufs_encountered {
sentry::capture_message(
"No buffer space available in VM's socket",
sentry::Level::Warning,
);
self.enobufs_encountered = true;
}
return Ok(());
}
+11 -3
View File
@@ -4,6 +4,7 @@ mod vm;
use crate::dhcp_snooper::DhcpSnooper;
use crate::host::Host;
use crate::host::NetType;
use crate::poller::Poller;
use crate::vm::VM;
use anyhow::Result;
@@ -18,12 +19,13 @@ pub struct Proxy {
poller: Poller,
vm_mac_address: smoltcp::wire::EthernetAddress,
dhcp_snooper: DhcpSnooper,
enobufs_encountered: bool,
}
impl Proxy {
pub fn new(vm_fd: RawFd, vm_mac_address: MacAddress) -> Result<Proxy> {
pub fn new(vm_fd: RawFd, vm_mac_address: MacAddress, vm_net_type: NetType) -> Result<Proxy> {
let vm = VM::new(vm_fd)?;
let host = Host::new()?;
let host = Host::new(vm_net_type)?;
let poller = Poller::new(vm.as_raw_fd(), host.as_raw_fd())?;
Ok(Proxy {
@@ -32,6 +34,7 @@ impl Proxy {
poller,
vm_mac_address: smoltcp::wire::EthernetAddress(vm_mac_address.bytes()),
dhcp_snooper: Default::default(),
enobufs_encountered: false,
})
}
@@ -41,7 +44,7 @@ impl Proxy {
self.poller.arm()?;
loop {
let (vm_readable, host_readable) = self.poller.wait()?;
let (vm_readable, host_readable, interrupt) = self.poller.wait()?;
if vm_readable {
self.read_from_vm(buf.as_mut_slice())?;
@@ -51,6 +54,11 @@ impl Proxy {
self.read_from_host(buf.as_mut_slice())?;
}
// Graceful termination
if interrupt {
return Ok(());
}
self.poller.rearm()?;
}
}
+7 -3
View File
@@ -61,10 +61,14 @@ impl Proxy {
// Once we've learned the VM's IP from the DHCP snooping,
// allow all global traffic for that VM's IP
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();
let ip_net = &ip_network::IpNetwork::from(Ipv4Addr::from(ipv4_pkt.dst_addr().0));
let dst_is_global_or_private =
match &ip_net {
ip_network::IpNetwork::V4(ip_net) => ip_net.is_global() || ip_net.is_private(),
ip_network::IpNetwork::V6(ip_net) => ip_net.is_global(),
};
if lease.valid_ip_source(ipv4_pkt.src_addr()) && dst_is_global {
if lease.valid_ip_source(ipv4_pkt.src_addr()) && dst_is_global_or_private {
return Some(());
}
}
+16 -3
View File
@@ -1,6 +1,8 @@
use anyhow::{anyhow, Context};
use clap::Parser;
use nix::sys::signal::{signal, SigHandler, Signal};
use privdrop::PrivDrop;
use softnet::NetType;
use softnet::proxy::Proxy;
use std::borrow::Cow;
use std::env;
@@ -14,7 +16,7 @@ use system_configuration::core_foundation::number::CFNumber;
use system_configuration::core_foundation::string::CFString;
use system_configuration::preferences::SCPreferences;
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)]
struct Args {
@@ -27,6 +29,9 @@ 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)]
vm_net_type: NetType,
#[clap(
long,
help = "set bootpd(8) lease time to this value (in seconds) before starting the VM",
@@ -55,7 +60,7 @@ fn main() -> ExitCode {
// Initialize Sentry
let _sentry = sentry::init(sentry::ClientOptions {
release: option_env!("CIRRUS_TAG").map(|tag| { Cow::from(format!("softnet@{tag}")) }),
release: option_env!("CIRRUS_TAG").map(|tag| Cow::from(format!("softnet@{tag}"))),
..Default::default()
});
@@ -84,6 +89,14 @@ fn main() -> ExitCode {
}
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();
// No need to run anything, just return
@@ -131,7 +144,7 @@ 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)
let mut proxy = Proxy::new(args.vm_fd as RawFd, args.vm_mac_address, args.vm_net_type)
.context("failed to initialize proxy")?;
// Drop effective privileges to the user