mirror of
https://github.com/cirruslabs/softnet.git
synced 2026-09-30 20:11:16 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a13c5922b | ||
|
|
a92f4e0c99 | ||
|
|
0a92c290be | ||
|
|
f5a1b1cdbd | ||
|
|
817dbb6e32 | ||
|
|
5f3b371e93 | ||
|
|
cd5f1d2f4f | ||
|
|
a775a92772 | ||
|
|
f38d65f98f | ||
|
|
4ba480ff4f | ||
|
|
d7699e95a9 | ||
|
|
535e03c97f | ||
|
|
d635751948 | ||
|
|
e71b32a8dd | ||
|
|
59cd9098e0 | ||
|
|
4ab3cd7e5c | ||
|
|
cd78047d79 | ||
|
|
a1108b1b7f | ||
|
|
11910d8540 |
+24
-6
@@ -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
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
/.idea
|
||||
/dist
|
||||
/target
|
||||
|
||||
+7
-9
@@ -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/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
+1507
-182
File diff suppressed because it is too large
Load Diff
+13
-4
@@ -7,17 +7,26 @@ 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"] }
|
||||
clap = { version = "4.5.2", 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"
|
||||
prefix-trie = "0.3.0"
|
||||
ipnet = "2.9.0"
|
||||
|
||||
@@ -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`.
|
||||
|
||||
+40
-20
@@ -1,4 +1,5 @@
|
||||
use crate::{Error, Result};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use clap::ValueEnum;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
use std::os::unix::net::UnixDatagram;
|
||||
@@ -8,6 +9,18 @@ use vmnet::mode::Mode;
|
||||
use vmnet::parameters::{Parameter, ParameterKind};
|
||||
use vmnet::{Events, Options};
|
||||
|
||||
#[derive(ValueEnum, 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,36 +31,43 @@ 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()
|
||||
},
|
||||
)
|
||||
.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 +84,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 +124,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
|
||||
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;
|
||||
|
||||
|
||||
+1
-25
@@ -1,30 +1,6 @@
|
||||
mod dhcp_snooper;
|
||||
mod host;
|
||||
pub use host::NetType;
|
||||
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
@@ -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 {
|
||||
|
||||
+19
-5
@@ -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 {
|
||||
@@ -16,10 +16,24 @@ impl Proxy {
|
||||
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<()> {
|
||||
|
||||
+23
-7
@@ -4,11 +4,13 @@ mod vm;
|
||||
|
||||
use crate::dhcp_snooper::DhcpSnooper;
|
||||
use crate::host::Host;
|
||||
use crate::host::NetType;
|
||||
use crate::poller::Poller;
|
||||
use crate::vm::VM;
|
||||
use crate::Error;
|
||||
use crate::Result;
|
||||
use anyhow::Result;
|
||||
use ipnet::Ipv4Net;
|
||||
use mac_address::MacAddress;
|
||||
use prefix_trie::PrefixSet;
|
||||
use smoltcp::wire::EthernetFrame;
|
||||
use std::io::ErrorKind;
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
@@ -19,12 +21,19 @@ pub struct Proxy {
|
||||
poller: Poller,
|
||||
vm_mac_address: smoltcp::wire::EthernetAddress,
|
||||
dhcp_snooper: DhcpSnooper,
|
||||
allow: PrefixSet<Ipv4Net>,
|
||||
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,
|
||||
allow: PrefixSet<Ipv4Net>,
|
||||
) -> 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 {
|
||||
@@ -33,6 +42,8 @@ impl Proxy {
|
||||
poller,
|
||||
vm_mac_address: smoltcp::wire::EthernetAddress(vm_mac_address.bytes()),
|
||||
dhcp_snooper: Default::default(),
|
||||
allow,
|
||||
enobufs_encountered: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -42,7 +53,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 +63,11 @@ impl Proxy {
|
||||
self.read_from_host(buf.as_mut_slice())?;
|
||||
}
|
||||
|
||||
// Graceful termination
|
||||
if interrupt {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.poller.rearm()?;
|
||||
}
|
||||
}
|
||||
@@ -69,7 +85,7 @@ impl Proxy {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
return Err(Error::VMIOFailed { source: err });
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,7 +104,7 @@ impl Proxy {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
return Err(Error::HostIOFailed { source: err });
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
-6
@@ -1,6 +1,8 @@
|
||||
use crate::proxy::udp_packet_helper::UdpPacketHelper;
|
||||
use crate::proxy::Proxy;
|
||||
use crate::{Error, Result};
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use ipnet::Ipv4Net;
|
||||
use smoltcp::wire::{
|
||||
ArpPacket, EthernetFrame, EthernetProtocol, IpProtocol, Ipv4Packet, UdpPacket,
|
||||
};
|
||||
@@ -16,7 +18,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<()> {
|
||||
@@ -57,15 +59,22 @@ impl Proxy {
|
||||
}
|
||||
|
||||
fn allowed_from_vm_ipv4(&self, ipv4_pkt: Ipv4Packet<&[u8]>) -> Option<()> {
|
||||
// Once we've learned the VM's IP from the DHCP snooping,
|
||||
// allow all global traffic for that VM's IP
|
||||
// Have we learned the VM's IP from the DHCP snooping?
|
||||
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();
|
||||
// If so, allow all global traffic
|
||||
let dst_addr = Ipv4Addr::from(ipv4_pkt.dst_addr().0);
|
||||
let dst_is_global = ip_network::IpNetwork::from(dst_addr).is_global();
|
||||
|
||||
if lease.valid_ip_source(ipv4_pkt.src_addr()) && dst_is_global {
|
||||
return Some(());
|
||||
}
|
||||
|
||||
// Also allow all traffic to the user-specified CIDRs
|
||||
let dst_net = Ipv4Net::from(dst_addr);
|
||||
|
||||
if self.allow.get_spm(&dst_net).is_some() {
|
||||
return Some(());
|
||||
}
|
||||
}
|
||||
|
||||
// Allow communication with host
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
nightly
|
||||
+81
-18
@@ -1,17 +1,25 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use clap::Parser;
|
||||
use ipnet::Ipv4Net;
|
||||
use nix::sys::signal::{signal, SigHandler, Signal};
|
||||
use prefix_trie::PrefixSet;
|
||||
use privdrop::PrivDrop;
|
||||
use softnet::proxy::Proxy;
|
||||
use softnet::NetType;
|
||||
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 {
|
||||
@@ -24,6 +32,9 @@ struct Args {
|
||||
#[clap(long, help = "MAC address to enforce for the VM")]
|
||||
vm_mac_address: mac_address::MacAddress,
|
||||
|
||||
#[clap(long, value_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",
|
||||
@@ -37,24 +48,67 @@ struct Args {
|
||||
#[clap(long, help = "group name to drop privileges to")]
|
||||
group: Option<String>,
|
||||
|
||||
#[clap(long, hide=true)]
|
||||
#[clap(
|
||||
long,
|
||||
help = "comma-separated list of CIDRs to allow the traffic to (e.g. --allow=192.168.0.0/24)",
|
||||
value_name = "comma-separated CIDRs",
|
||||
use_value_delimiter = true,
|
||||
action = clap::ArgAction::Set
|
||||
)]
|
||||
allow: Vec<Ipv4Net>,
|
||||
|
||||
#[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 +120,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 +135,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 +147,22 @@ 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,
|
||||
args.vm_net_type,
|
||||
PrefixSet::from_iter(args.allow),
|
||||
)
|
||||
.context("failed to initialize proxy")?;
|
||||
|
||||
// Drop effective privileges to the user
|
||||
// and group which have had invoked us
|
||||
@@ -107,10 +170,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 {
|
||||
|
||||
Reference in New Issue
Block a user