Compare commits

...
18 Commits
Author SHA1 Message Date
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
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
Nikolay Edigaryev cd78047d79 Upload symbols and sources to Sentry (#15)
* Upload symbols and sources to Sentry

* .cirrus.yml: add SENTRY_AUTH_TOKEN

* .cirrus.yml: install Sentry CLI
2022-12-14 19:34:26 +04:00
Fedor Korotkov a1108b1b7f Full backtrace (#14)
To include line numbers
2022-12-14 18:59:24 +04:00
Nikolay Edigaryev 11910d8540 Sentry integration (#13)
* Sentry integration

* Introduce a more generic CIRRUS_SENTRY_TAGS

* Revert switching to nightly toolchain
2022-12-12 21:33:45 +04:00
Nikolay Edigaryev 22c92688e5 Don't panic in Drop (#12) 2022-11-19 20:53:30 +04:00
Nikolay Edigaryev e2403f0ea9 Only perform DHCP snooping for frames destined to the VM (#10) 2022-11-10 20:31:18 +04:00
Nikolay Edigaryev 717e6b0f89 Increase the default bootpd(8) lease time from 1 to 10 minutes (#8) 2022-10-21 19:09:18 +04:00
Nikolay Edigaryev 71465f8bff README.md: clarify installation instructions a bit 2022-10-13 18:30:30 +04:00
14 changed files with 1554 additions and 222 deletions
+24 -6
View File
@@ -4,24 +4,42 @@ 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!]
SENTRY_ORG: cirrus-labs
SENTRY_PROJECT: persistent-workers
SENTRY_AUTH_TOKEN: ENCRYPTED[!c16a5cf7da5f856b4bc2f21fe8cb7aa2a6c981f851c094ed4d3025fd02ea59a58a86cee8b193a69a1fc20fa217e56ac3!]
install_rust_script:
- curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
install_goreleaser_script:
- brew install go goreleaser/tap/goreleaser-pro
- rustup target add x86_64-apple-darwin
install_script:
- brew install go goreleaser/tap/goreleaser-pro getsentry/tools/sentry-cli
build_script:
- cargo build --release
- 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/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/
# Bundle and upload sources
- sentry-cli debug-files bundle-sources softnet.dSYM/
- sentry-cli debug-files upload -o $SENTRY_ORG -p $SENTRY_PROJECT softnet.src.zip
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
+13 -4
View File
@@ -1,20 +1,29 @@
project_name: softnet
builds:
- builder: prebuilt
- id: softnet-arm64
builder: prebuilt
goos:
- darwin
goarch:
- arm64
prebuilt:
path: target/release/softnet
path: "target/aarch64-apple-darwin/release-with-debug/softnet"
- id: softnet-x86
builder: prebuilt
goos:
- darwin
goarch:
- amd64
prebuilt:
path: "target/x86_64-apple-darwin/release-with-debug/softnet"
archives:
- id: binary
format: binary
name_template: "{{ .ProjectName }}"
name_template: "{{ .ProjectName }}-{{ .Arch }}"
- id: regular
name_template: "{{ .ProjectName }}"
name_template: "{{ .ProjectName }}-{{ .Arch }}"
release:
prerelease: auto
Generated
+1348 -108
View File
File diff suppressed because it is too large Load Diff
+10 -3
View File
@@ -7,17 +7,24 @@ edition = "2021"
[lib]
path = "lib/mod.rs"
[profile.release-with-debug]
inherits = "release"
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"] }
mac_address = "1.1.3"
privdrop = "0.5.2"
thiserror = "1.0.31"
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"
+7 -6
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
@@ -20,14 +21,14 @@ And assumes that:
## Installing
For proper functioning Softnet binary requires two things:
For proper functioning, Softnet binary requires two things:
* a [SUID-bit](https://en.wikipedia.org/wiki/Setuid#SUID) is set on the binary or [passwordless sudo](https://serverfault.com/questions/160581/how-to-setup-passwordless-sudo-on-linux) is enabled, which effectively gives the binary `root` privileges
* this is needed in order to create [`vmnet.framework`](https://developer.apple.com/documentation/vmnet) interface and perform DHCP-related tweaks
* a [SUID-bit](https://en.wikipedia.org/wiki/Setuid#SUID) to be set on the binary or a [passwordless sudo](https://serverfault.com/questions/160581/how-to-setup-passwordless-sudo-on-linux) to be configured, which effectively gives the binary `root` privileges
* these privileges are needed to create [`vmnet.framework`](https://developer.apple.com/documentation/vmnet) interface and perform DHCP-related system tweaks
* the privileges will be dropped automatically to that of the calling user (or those represented by the `--user` and `--group` command-line arguments) once all of the initialization is completed
* the binary is available in `PATH`
* the binary to be available in `PATH`
* so that the Tart will be able to find it
## 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`.
+14 -18
View File
@@ -1,4 +1,4 @@
use crate::{Error, Result};
use anyhow::{anyhow, Context, Result};
use std::net::Ipv4Addr;
use std::os::unix::io::{AsRawFd, RawFd};
use std::os::unix::net::UnixDatagram;
@@ -27,27 +27,23 @@ impl Host {
..Default::default()
},
)
.map_err(|err| Error::VmnetFailed { source: err })?;
.context("failed to initialize vmnet interface")?;
// Retrieve first IP (gateway) used for this interface
let gateway_ip = match interface.parameters().get(ParameterKind::StartAddress) {
Some(Parameter::StartAddress(gateway_ip)) => gateway_ip,
_ => return Err(Error::VmnetUnexpected),
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).map_err(|_| Error::VmnetUnexpected)?;
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 max_packet_size = match interface.parameters().get(ParameterKind::MaxPacketSize) {
Some(Parameter::MaxPacketSize(max_packet_size)) => max_packet_size,
_ => return Err(Error::VmnetUnexpected),
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
let (new_packets_tx, new_packets_rx) =
UnixDatagram::pair().map_err(|err| Error::InitFailed { source: err.into() })?;
new_packets_rx
.set_nonblocking(true)
.map_err(|err| Error::InitFailed { source: err.into() })?;
let (new_packets_tx, new_packets_rx) = UnixDatagram::pair()?;
new_packets_rx.set_nonblocking(true)?;
let (callback_can_continue_tx, callback_can_continue_rx) = sync_channel(0);
@@ -64,7 +60,7 @@ impl Host {
// [1]: https://en.wikipedia.org/wiki/Blocks_(C_language_extension)
callback_can_continue_rx.recv().unwrap();
})
.map_err(|err| Error::VmnetFailed { source: err })?;
.context("failed to set vmnet interface's event callback")?;
Ok(Host {
interface,
@@ -104,14 +100,14 @@ impl Host {
// First make sure our callback won't be scheduled again after it finishes
self.interface
.clear_event_callback()
.map_err(|err| Error::VmnetFailed { source: err })?;
.context("failed to clear vmnet interface's event callback")?;
// Now let the callback finish
self.callback_can_continue_tx.send(()).unwrap();
let _ = self.callback_can_continue_tx.send(());
self.interface
.finalize()
.map_err(|err| Error::VmnetFailed { source: err })?;
.context("failed to finalize vmnet's interface")?;
self.finalized = true;
-25
View File
@@ -3,28 +3,3 @@ mod host;
mod poller;
pub mod proxy;
mod vm;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("initialization failed")]
InitFailed { source: Box<dyn std::error::Error> },
#[error("failed to poll")]
PollFailed { source: std::io::Error },
#[error("vmnet failed")]
VmnetFailed { source: vmnet::Error },
#[error("vmnet returned unexpected data")]
VmnetUnexpected,
#[error("failed to do I/O on VM socket")]
VMIOFailed { source: std::io::Error },
#[error("failed to do I/O on host socket")]
HostIOFailed { source: vmnet::Error },
}
pub type Result<T> = std::result::Result<T, Error>;
+38 -17
View File
@@ -1,5 +1,7 @@
use crate::{Error, Result};
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,12 +17,12 @@ pub struct Poller {
enum EventKey {
VM,
Host,
Interrupt,
}
impl Poller {
pub fn new(vm_fd: RawFd, host_fd: RawFd) -> Result<Poller> {
let poller =
polling::Poller::new().map_err(|err| Error::InitFailed { source: err.into() })?;
let poller = polling::Poller::new()?;
Ok(Poller {
poller,
@@ -31,13 +33,18 @@ impl Poller {
}
pub fn arm(&self) -> Result<()> {
self.poller.add(self.vm_fd as RawFd, self.vm_interest())?;
self.poller
.add(self.vm_fd as RawFd, self.vm_interest())
.map_err(|err| Error::PollFailed { source: err })?;
.add(self.host_fd as RawFd, self.host_interest())?;
let interrupt_signal = polling::os::kqueue::Signal(libc::SIGINT);
self.poller
.add(self.host_fd as RawFd, self.host_interest())
.map_err(|err| Error::PollFailed { source: err })?;
.add_filter(
interrupt_signal,
EventKey::Interrupt.into(),
PollMode::Oneshot,
)
.unwrap();
Ok(())
}
@@ -46,24 +53,38 @@ impl Poller {
self.events.clear();
self.poller
.modify(self.vm_fd as RawFd, self.vm_interest())
.map_err(|err| Error::PollFailed { source: err })?;
.modify(self.vm_fd as RawFd, self.vm_interest())?;
self.poller
.modify(self.host_fd as RawFd, self.host_interest())
.map_err(|err| Error::PollFailed { source: err })?;
.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)))
.map_err(|err| Error::PollFailed { source: err })?;
.wait(&mut self.events, Some(Duration::from_millis(100)))?;
let vm_readable = self.events.iter().any(|ev| ev.key == EventKey::VM.into());
let host_readable = self.events.iter().any(|ev| ev.key == EventKey::Host.into());
let vm_readable = self
.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 {
+22 -6
View File
@@ -1,6 +1,6 @@
use crate::proxy::udp_packet_helper::UdpPacketHelper;
use crate::proxy::Proxy;
use crate::{Error, Result};
use anyhow::{Context, Result};
use smoltcp::wire::{EthernetFrame, EthernetProtocol, Ipv4Packet, UdpPacket};
impl Proxy {
@@ -12,12 +12,28 @@ impl Proxy {
// Snoop bootpd(8) replies from the host to
// figure out the IP assigned to the VM
self.snoop(frame);
if frame.dst_addr() == self.vm_mac_address {
self.snoop(frame);
}
self.vm
.write(frame.as_ref())
.map(|_| ())
.map_err(|err| Error::VMIOFailed { source: err })
match self.vm.write(frame.as_ref()) {
Ok(_) => Ok(()),
Err(err) => {
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<()> {
+11 -5
View File
@@ -6,8 +6,7 @@ use crate::dhcp_snooper::DhcpSnooper;
use crate::host::Host;
use crate::poller::Poller;
use crate::vm::VM;
use crate::Error;
use crate::Result;
use anyhow::Result;
use mac_address::MacAddress;
use smoltcp::wire::EthernetFrame;
use std::io::ErrorKind;
@@ -19,6 +18,7 @@ pub struct Proxy {
poller: Poller,
vm_mac_address: smoltcp::wire::EthernetAddress,
dhcp_snooper: DhcpSnooper,
enobufs_encountered: bool,
}
impl Proxy {
@@ -33,6 +33,7 @@ impl Proxy {
poller,
vm_mac_address: smoltcp::wire::EthernetAddress(vm_mac_address.bytes()),
dhcp_snooper: Default::default(),
enobufs_encountered: false,
})
}
@@ -42,7 +43,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())?;
@@ -52,6 +53,11 @@ impl Proxy {
self.read_from_host(buf.as_mut_slice())?;
}
// Graceful termination
if interrupt {
return Ok(());
}
self.poller.rearm()?;
}
}
@@ -69,7 +75,7 @@ impl Proxy {
return Ok(());
}
return Err(Error::VMIOFailed { source: err });
return Err(err.into());
}
}
}
@@ -88,7 +94,7 @@ impl Proxy {
return Ok(());
}
return Err(Error::HostIOFailed { source: err });
return Err(err.into());
}
}
}
+3 -2
View File
@@ -1,6 +1,7 @@
use crate::proxy::udp_packet_helper::UdpPacketHelper;
use crate::proxy::Proxy;
use crate::{Error, Result};
use anyhow::Context;
use anyhow::Result;
use smoltcp::wire::{
ArpPacket, EthernetFrame, EthernetProtocol, IpProtocol, Ipv4Packet, UdpPacket,
};
@@ -16,7 +17,7 @@ impl Proxy {
self.host
.write(frame.as_ref())
.map(|_| ())
.map_err(|err| Error::HostIOFailed { source: err })
.context("failed to write to the host")
}
fn allowed_from_vm(&self, frame: &EthernetFrame<&[u8]>) -> Option<()> {
+2 -3
View File
@@ -1,4 +1,4 @@
use crate::{Error, Result};
use anyhow::Result;
use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
use std::os::unix::net::UnixDatagram;
@@ -9,8 +9,7 @@ pub struct VM {
impl VM {
pub fn new(vm_fd: RawFd) -> Result<VM> {
let sock = unsafe { UnixDatagram::from_raw_fd(vm_fd) };
sock.set_nonblocking(true)
.map_err(|err| Error::InitFailed { source: err.into() })?;
sock.set_nonblocking(true)?;
Ok(VM { sock })
}
+1
View File
@@ -0,0 +1 @@
nightly
+61 -19
View File
@@ -1,17 +1,21 @@
use anyhow::{anyhow, Context};
use clap::Parser;
use nix::sys::signal::{signal, SigHandler, Signal};
use privdrop::PrivDrop;
use softnet::proxy::Proxy;
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;
use std::process::Command;
use std::process::{Command, ExitCode};
use system_configuration::core_foundation::base::TCFType;
use system_configuration::core_foundation::dictionary::CFDictionary;
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,7 +31,7 @@ struct Args {
#[clap(
long,
help = "set bootpd(8) lease time to this value (in seconds) before starting the VM",
default_value_t = 60
default_value_t = 600
)]
bootpd_lease_time: u32,
@@ -37,24 +41,58 @@ struct Args {
#[clap(long, help = "group name to drop privileges to")]
group: Option<String>,
#[clap(long, hide=true)]
#[clap(long, hide = true)]
sudo_escalation_probing: bool,
#[clap(long, hide=true)]
#[clap(long, hide = true)]
sudo_escalation_done: bool,
}
fn main() {
if let Err(err) = try_main() {
match err.source() {
Some(source) => eprintln!("{}: {}", err, source),
None => eprintln!("{}", err),
fn main() -> ExitCode {
// Enable backtraces by default
if env::var("RUST_BACKTRACE").is_err() {
env::set_var("RUST_BACKTRACE", "full");
}
// Initialize Sentry
let _sentry = sentry::init(sentry::ClientOptions {
release: option_env!("CIRRUS_TAG").map(|tag| Cow::from(format!("softnet@{tag}"))),
..Default::default()
});
// Enrich future events with Cirrus CI-specific tags
if let Ok(tags) = env::var("CIRRUS_SENTRY_TAGS") {
sentry::configure_scope(|scope| {
for (key, value) in tags.split(',').filter_map(|tag| tag.split_once('=')) {
scope.set_tag(key, value);
}
});
}
match try_main() {
Ok(_) => ExitCode::SUCCESS,
Err(err) => {
// Print the error into stderr
let causes: Vec<String> = err.chain().map(|x| x.to_string()).collect();
eprintln!("{}", causes.join(": "));
// Capture the error into Sentry
sentry_anyhow::capture_anyhow(&err);
ExitCode::FAILURE
}
std::process::exit(1);
}
}
fn try_main() -> Result<(), Box<dyn std::error::Error>> {
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
@@ -66,11 +104,11 @@ fn try_main() -> Result<(), Box<dyn std::error::Error>> {
// Retrieve real (not effective) user and group names
let current_user_name = get_current_username()
.ok_or("failed to resolve real user name")?
.ok_or(anyhow!("failed to resolve real user name"))?
.to_string_lossy()
.to_string();
let current_group_name = get_current_groupname()
.ok_or("failed to resolve real group name")?
.ok_or(anyhow!("failed to resolve real group name"))?
.to_string_lossy()
.to_string();
@@ -81,7 +119,8 @@ fn try_main() -> Result<(), Box<dyn std::error::Error>> {
let args = std::env::args().skip(1);
let _ = Command::new("sudo")
.arg("-n")
.arg("--non-interactive")
.arg("--preserve-env=SENTRY_DSN,CIRRUS_SENTRY_TAGS")
.arg(&exe)
.args(args)
.arg("--sudo-escalation-done")
@@ -92,14 +131,17 @@ fn try_main() -> Result<(), Box<dyn std::error::Error>> {
.exec();
}
return Err("root privileges are required to run and passwordless sudo was not available".into());
return Err(anyhow!(
"root privileges are required to run and passwordless sudo was not available"
));
}
// Set bootpd(8) min/max lease time while still having the root privileges
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)
.context("failed to initialize proxy")?;
// Drop effective privileges to the user
// and group which have had invoked us
@@ -107,10 +149,10 @@ fn try_main() -> Result<(), Box<dyn std::error::Error>> {
.user(args.user.unwrap_or(current_user_name))
.group(args.group.unwrap_or(current_group_name))
.apply()
.map_err(|err| format!("failed to drop privileges: {}", err))?;
.context("failed to drop privileges")?;
// Run proxy
proxy.run().map_err(|err| err.into())
proxy.run()
}
fn sudo_escalation_works() -> bool {