Sentry integration (#13)

* Sentry integration

* Introduce a more generic CIRRUS_SENTRY_TAGS

* Revert switching to nightly toolchain
This commit is contained in:
Nikolay Edigaryev 2022-12-12 21:33:45 +04:00 committed by GitHub
parent 22c92688e5
commit 11910d8540
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1247 additions and 90 deletions

View File

@ -23,5 +23,5 @@ task:
install_goreleaser_script: install_goreleaser_script:
- brew install go goreleaser/tap/goreleaser-pro - brew install go goreleaser/tap/goreleaser-pro
build_script: build_script:
- cargo build --release - cargo build --profile release-with-debug
release_script: goreleaser release_script: goreleaser

View File

@ -7,7 +7,7 @@ builds:
goarch: goarch:
- arm64 - arm64
prebuilt: prebuilt:
path: target/release/softnet path: "target/release-with-debug/softnet"
archives: archives:
- id: binary - id: binary

1162
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,10 @@ edition = "2021"
[lib] [lib]
path = "lib/mod.rs" path = "lib/mod.rs"
[profile.release-with-debug]
inherits = "release"
debug = true
[dependencies] [dependencies]
smoltcp = "0.8.1" smoltcp = "0.8.1"
libc = "0.2.126" libc = "0.2.126"
@ -16,8 +20,10 @@ vmnet = "0.1.1"
clap = { version = "3.1.18", features = ["derive"] } clap = { version = "3.1.18", features = ["derive"] }
mac_address = "1.1.3" mac_address = "1.1.3"
privdrop = "0.5.2" privdrop = "0.5.2"
thiserror = "1.0.31" anyhow = { version = "1.0.66", features = ["backtrace"] }
ip_network = "0.4.1" ip_network = "0.4.1"
users = "0.11.0" users = "0.11.0"
system-configuration = "0.5.0" system-configuration = "0.5.0"
num_enum = "0.5.7" num_enum = "0.5.7"
sentry = "0.29.1"
sentry-anyhow = { version = "0.29.1", features = ["backtrace"] }

View File

@ -1,4 +1,4 @@
use crate::{Error, Result}; use anyhow::{anyhow, Context, Result};
use std::net::Ipv4Addr; use std::net::Ipv4Addr;
use std::os::unix::io::{AsRawFd, RawFd}; use std::os::unix::io::{AsRawFd, RawFd};
use std::os::unix::net::UnixDatagram; use std::os::unix::net::UnixDatagram;
@ -27,27 +27,23 @@ impl Host {
..Default::default() ..Default::default()
}, },
) )
.map_err(|err| Error::VmnetFailed { source: err })?; .context("failed to initialize vmnet interface")?;
// Retrieve first IP (gateway) used for this interface // Retrieve first IP (gateway) used for this interface
let gateway_ip = match interface.parameters().get(ParameterKind::StartAddress) { let Some(Parameter::StartAddress(gateway_ip)) = interface.parameters().get(ParameterKind::StartAddress) else {
Some(Parameter::StartAddress(gateway_ip)) => gateway_ip, return Err(anyhow!("failed to retrieve vmnet's interface start address"));
_ => return Err(Error::VmnetUnexpected),
}; };
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 // Retrieve max packet size for this interface
let max_packet_size = match interface.parameters().get(ParameterKind::MaxPacketSize) { let Some(Parameter::MaxPacketSize(max_packet_size)) = interface.parameters().get(ParameterKind::MaxPacketSize) else {
Some(Parameter::MaxPacketSize(max_packet_size)) => max_packet_size, return Err(anyhow!("failed to retrieve vmnet's interface max packet size"));
_ => return Err(Error::VmnetUnexpected),
}; };
// Set up a socketpair() to emulate polling of the vmnet interface // Set up a socketpair() to emulate polling of the vmnet interface
let (new_packets_tx, new_packets_rx) = let (new_packets_tx, new_packets_rx) = UnixDatagram::pair()?;
UnixDatagram::pair().map_err(|err| Error::InitFailed { source: err.into() })?; new_packets_rx.set_nonblocking(true)?;
new_packets_rx
.set_nonblocking(true)
.map_err(|err| Error::InitFailed { source: err.into() })?;
let (callback_can_continue_tx, callback_can_continue_rx) = sync_channel(0); 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) // [1]: https://en.wikipedia.org/wiki/Blocks_(C_language_extension)
callback_can_continue_rx.recv().unwrap(); callback_can_continue_rx.recv().unwrap();
}) })
.map_err(|err| Error::VmnetFailed { source: err })?; .context("failed to set vmnet interface's event callback")?;
Ok(Host { Ok(Host {
interface, interface,
@ -104,14 +100,14 @@ impl Host {
// First make sure our callback won't be scheduled again after it finishes // First make sure our callback won't be scheduled again after it finishes
self.interface self.interface
.clear_event_callback() .clear_event_callback()
.map_err(|err| Error::VmnetFailed { source: err })?; .context("failed to clear vmnet interface's event callback")?;
// Now let the callback finish // Now let the callback finish
let _ = self.callback_can_continue_tx.send(()); let _ = self.callback_can_continue_tx.send(());
self.interface self.interface
.finalize() .finalize()
.map_err(|err| Error::VmnetFailed { source: err })?; .context("failed to finalize vmnet's interface")?;
self.finalized = true; self.finalized = true;

View File

@ -3,28 +3,3 @@ mod host;
mod poller; mod poller;
pub mod proxy; pub mod proxy;
mod vm; 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>;

View File

@ -1,4 +1,4 @@
use crate::{Error, Result}; use anyhow::Result;
use num_enum::IntoPrimitive; use num_enum::IntoPrimitive;
use std::os::unix::io::RawFd; use std::os::unix::io::RawFd;
use std::time::Duration; use std::time::Duration;
@ -19,8 +19,7 @@ enum EventKey {
impl Poller { impl Poller {
pub fn new(vm_fd: RawFd, host_fd: RawFd) -> Result<Poller> { pub fn new(vm_fd: RawFd, host_fd: RawFd) -> Result<Poller> {
let poller = let poller = polling::Poller::new()?;
polling::Poller::new().map_err(|err| Error::InitFailed { source: err.into() })?;
Ok(Poller { Ok(Poller {
poller, poller,
@ -31,13 +30,9 @@ impl Poller {
} }
pub fn arm(&self) -> Result<()> { pub fn arm(&self) -> Result<()> {
self.poller.add(self.vm_fd as RawFd, self.vm_interest())?;
self.poller self.poller
.add(self.vm_fd as RawFd, self.vm_interest()) .add(self.host_fd as RawFd, self.host_interest())?;
.map_err(|err| Error::PollFailed { source: err })?;
self.poller
.add(self.host_fd as RawFd, self.host_interest())
.map_err(|err| Error::PollFailed { source: err })?;
Ok(()) Ok(())
} }
@ -46,19 +41,16 @@ impl Poller {
self.events.clear(); self.events.clear();
self.poller self.poller
.modify(self.vm_fd as RawFd, self.vm_interest()) .modify(self.vm_fd as RawFd, self.vm_interest())?;
.map_err(|err| Error::PollFailed { source: err })?;
self.poller self.poller
.modify(self.host_fd as RawFd, self.host_interest()) .modify(self.host_fd as RawFd, self.host_interest())?;
.map_err(|err| Error::PollFailed { source: err })?;
Ok(()) Ok(())
} }
pub fn wait(&mut self) -> Result<(bool, bool)> { pub fn wait(&mut self) -> Result<(bool, bool)> {
self.poller self.poller
.wait(&mut self.events, Some(Duration::from_millis(100))) .wait(&mut self.events, Some(Duration::from_millis(100)))?;
.map_err(|err| Error::PollFailed { source: err })?;
let vm_readable = self.events.iter().any(|ev| ev.key == EventKey::VM.into()); 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 host_readable = self.events.iter().any(|ev| ev.key == EventKey::Host.into());

View File

@ -1,6 +1,6 @@
use crate::proxy::udp_packet_helper::UdpPacketHelper; use crate::proxy::udp_packet_helper::UdpPacketHelper;
use crate::proxy::Proxy; use crate::proxy::Proxy;
use crate::{Error, Result}; use anyhow::{Context, Result};
use smoltcp::wire::{EthernetFrame, EthernetProtocol, Ipv4Packet, UdpPacket}; use smoltcp::wire::{EthernetFrame, EthernetProtocol, Ipv4Packet, UdpPacket};
impl Proxy { impl Proxy {
@ -19,7 +19,7 @@ impl Proxy {
self.vm self.vm
.write(frame.as_ref()) .write(frame.as_ref())
.map(|_| ()) .map(|_| ())
.map_err(|err| Error::VMIOFailed { source: 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<()> {

View File

@ -6,8 +6,7 @@ use crate::dhcp_snooper::DhcpSnooper;
use crate::host::Host; use crate::host::Host;
use crate::poller::Poller; use crate::poller::Poller;
use crate::vm::VM; use crate::vm::VM;
use crate::Error; use anyhow::Result;
use crate::Result;
use mac_address::MacAddress; use mac_address::MacAddress;
use smoltcp::wire::EthernetFrame; use smoltcp::wire::EthernetFrame;
use std::io::ErrorKind; use std::io::ErrorKind;
@ -69,7 +68,7 @@ impl Proxy {
return Ok(()); return Ok(());
} }
return Err(Error::VMIOFailed { source: err }); return Err(err.into());
} }
} }
} }
@ -88,7 +87,7 @@ impl Proxy {
return Ok(()); return Ok(());
} }
return Err(Error::HostIOFailed { source: err }); return Err(err.into());
} }
} }
} }

View File

@ -1,6 +1,7 @@
use crate::proxy::udp_packet_helper::UdpPacketHelper; use crate::proxy::udp_packet_helper::UdpPacketHelper;
use crate::proxy::Proxy; use crate::proxy::Proxy;
use crate::{Error, Result}; use anyhow::Context;
use anyhow::Result;
use smoltcp::wire::{ use smoltcp::wire::{
ArpPacket, EthernetFrame, EthernetProtocol, IpProtocol, Ipv4Packet, UdpPacket, ArpPacket, EthernetFrame, EthernetProtocol, IpProtocol, Ipv4Packet, UdpPacket,
}; };
@ -16,7 +17,7 @@ impl Proxy {
self.host self.host
.write(frame.as_ref()) .write(frame.as_ref())
.map(|_| ()) .map(|_| ())
.map_err(|err| Error::HostIOFailed { source: err }) .context("failed to write to the host")
} }
fn allowed_from_vm(&self, frame: &EthernetFrame<&[u8]>) -> Option<()> { fn allowed_from_vm(&self, frame: &EthernetFrame<&[u8]>) -> Option<()> {

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::io::{AsRawFd, FromRawFd, RawFd};
use std::os::unix::net::UnixDatagram; use std::os::unix::net::UnixDatagram;
@ -9,8 +9,7 @@ pub struct VM {
impl VM { impl VM {
pub fn new(vm_fd: RawFd) -> Result<VM> { pub fn new(vm_fd: RawFd) -> Result<VM> {
let sock = unsafe { UnixDatagram::from_raw_fd(vm_fd) }; let sock = unsafe { UnixDatagram::from_raw_fd(vm_fd) };
sock.set_nonblocking(true) sock.set_nonblocking(true)?;
.map_err(|err| Error::InitFailed { source: err.into() })?;
Ok(VM { sock }) Ok(VM { sock })
} }

View File

@ -1,10 +1,12 @@
use anyhow::{anyhow, Context};
use clap::Parser; use clap::Parser;
use privdrop::PrivDrop; use privdrop::PrivDrop;
use softnet::proxy::Proxy; use softnet::proxy::Proxy;
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;
use std::os::unix::process::CommandExt; 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::base::TCFType;
use system_configuration::core_foundation::dictionary::CFDictionary; use system_configuration::core_foundation::dictionary::CFDictionary;
use system_configuration::core_foundation::number::CFNumber; use system_configuration::core_foundation::number::CFNumber;
@ -44,17 +46,42 @@ struct Args {
sudo_escalation_done: bool, sudo_escalation_done: bool,
} }
fn main() { fn main() -> ExitCode {
if let Err(err) = try_main() { // Enable backtraces by default
match err.source() { if env::var("RUST_BACKTRACE").is_err() {
Some(source) => eprintln!("{}: {}", err, source), env::set_var("RUST_BACKTRACE", "1");
None => eprintln!("{}", err), }
// Initialize Sentry
let _sentry = sentry::init(sentry::ClientOptions {
..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(",").map(|tag| tag.split_once("=")).flatten() {
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<()> {
let args: Args = Args::parse(); let args: Args = Args::parse();
// No need to run anything, just return // No need to run anything, just return
@ -66,11 +93,11 @@ fn try_main() -> Result<(), Box<dyn std::error::Error>> {
// Retrieve real (not effective) user and group names // Retrieve real (not effective) user and group names
let current_user_name = get_current_username() 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_lossy()
.to_string(); .to_string();
let current_group_name = get_current_groupname() 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_lossy()
.to_string(); .to_string();
@ -81,7 +108,8 @@ fn try_main() -> Result<(), Box<dyn std::error::Error>> {
let args = std::env::args().skip(1); let args = std::env::args().skip(1);
let _ = Command::new("sudo") let _ = Command::new("sudo")
.arg("-n") .arg("--non-interactive")
.arg("--preserve-env=SENTRY_DSN,CIRRUS_SENTRY_TAGS")
.arg(&exe) .arg(&exe)
.args(args) .args(args)
.arg("--sudo-escalation-done") .arg("--sudo-escalation-done")
@ -92,14 +120,17 @@ fn try_main() -> Result<(), Box<dyn std::error::Error>> {
.exec(); .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(8) min/max lease time while still having the root privileges
set_bootpd_lease_time(args.bootpd_lease_time); set_bootpd_lease_time(args.bootpd_lease_time);
// Initialize the proxy while still having the root privileges // 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 // Drop effective privileges to the user
// and group which have had invoked us // and group which have had invoked us
@ -107,10 +138,10 @@ fn try_main() -> Result<(), Box<dyn std::error::Error>> {
.user(args.user.unwrap_or(current_user_name)) .user(args.user.unwrap_or(current_user_name))
.group(args.group.unwrap_or(current_group_name)) .group(args.group.unwrap_or(current_group_name))
.apply() .apply()
.map_err(|err| format!("failed to drop privileges: {}", err))?; .context("failed to drop privileges")?;
// Run proxy // Run proxy
proxy.run().map_err(|err| err.into()) proxy.run()
} }
fn sudo_escalation_works() -> bool { fn sudo_escalation_works() -> bool {