mirror of
https://github.com/cirruslabs/softnet.git
synced 2026-09-30 20:11:16 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fa4fb1bda | ||
|
|
11213d0f46 | ||
|
|
ba114b3c86 | ||
|
|
e37f52971d | ||
|
|
7ba7849a80 | ||
|
|
6d0702f884 | ||
|
|
54e02845ac | ||
|
|
50044e9cef | ||
|
|
36a7e994c0 | ||
|
|
4e70021b2b | ||
|
|
37f3c2f8ea | ||
|
|
c41a37a40f | ||
|
|
570f02e8d5 | ||
|
|
179ef6c282 | ||
|
|
8692eb9de6 | ||
|
|
86cc71e711 | ||
|
|
4e648fade1 | ||
|
|
47faa8f577 | ||
|
|
224ae136f6 | ||
|
|
6253053799 | ||
|
|
3b8ddff4d3 | ||
|
|
0eac5c49a9 | ||
|
|
2cfe616b1e | ||
|
|
06c1b809b6 | ||
|
|
4ef98589c4 | ||
|
|
82cdc24556 | ||
|
|
267466d572 | ||
|
|
c05bd23f4a | ||
|
|
3b83823a09 | ||
|
|
cf24be0992 | ||
|
|
14fe582a4d | ||
|
|
539cff564d | ||
|
|
8519fa2f86 | ||
|
|
f3acef87a7 | ||
|
|
a35e5ae92a | ||
|
|
1f5aeb29f3 | ||
|
|
9a62801cd6 | ||
|
|
694f2e138a | ||
|
|
c2ff5761cb | ||
|
|
762868a8eb | ||
|
|
95a3358f59 | ||
|
|
ebc7cf8973 | ||
|
|
86f082ac77 | ||
|
|
b3df49889a | ||
|
|
82be9577b3 | ||
|
|
04c6019437 | ||
|
|
7374ceb239 | ||
|
|
cc7cc0e740 | ||
|
|
e53beeeb79 | ||
|
|
05cba5d771 | ||
|
|
cf97e3878d | ||
|
|
7f5293dd5e | ||
|
|
9a2e59b844 | ||
|
|
603c8b4889 | ||
|
|
24641d5325 | ||
|
|
ed64c139cf | ||
|
|
8359992a08 | ||
|
|
147c051b0e | ||
|
|
6456ed7228 | ||
|
|
56808c591f | ||
|
|
eba21ed33e | ||
|
|
867679446e | ||
|
|
4a13c5922b | ||
|
|
a92f4e0c99 | ||
|
|
0a92c290be | ||
|
|
f5a1b1cdbd | ||
|
|
817dbb6e32 | ||
|
|
5f3b371e93 | ||
|
|
cd5f1d2f4f | ||
|
|
a775a92772 | ||
|
|
f38d65f98f | ||
|
|
4ba480ff4f | ||
|
|
d7699e95a9 | ||
|
|
535e03c97f | ||
|
|
d635751948 | ||
|
|
e71b32a8dd | ||
|
|
59cd9098e0 | ||
|
|
4ab3cd7e5c | ||
|
|
cd78047d79 | ||
|
|
a1108b1b7f | ||
|
|
11910d8540 | ||
|
|
22c92688e5 | ||
|
|
e2403f0ea9 |
+26
-6
@@ -4,24 +4,44 @@ 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 toolchain install nightly-aarch64-apple-darwin
|
||||
- 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 toolchain install nightly-aarch64-apple-darwin
|
||||
- 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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
* @edigaryev @fkorotkov
|
||||
@@ -0,0 +1,10 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
groups:
|
||||
all-updates:
|
||||
patterns:
|
||||
- "*"
|
||||
@@ -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
+2120
-215
File diff suppressed because it is too large
Load Diff
+24
-13
@@ -7,17 +7,28 @@ 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"
|
||||
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"
|
||||
ip_network = "0.4.1"
|
||||
users = "0.11.0"
|
||||
system-configuration = "0.5.0"
|
||||
num_enum = "0.5.7"
|
||||
smoltcp = "0"
|
||||
libc = "0"
|
||||
polling = "3"
|
||||
dhcproto = { git = "https://github.com/bluecatengineering/dhcproto.git", branch = "master" }
|
||||
vmnet = "0"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
mac_address = "1"
|
||||
privdrop = "0"
|
||||
anyhow = { version = "1", features = ["backtrace"] }
|
||||
ip_network = "0"
|
||||
uzers = "0"
|
||||
system-configuration = "0"
|
||||
num_enum = "0"
|
||||
sentry = { version = "0", features = ["debug-images"] }
|
||||
sentry-anyhow = { version = "0", features = ["backtrace"] }
|
||||
nix = { version = "0", features = ["signal"] }
|
||||
prefix-trie = "0"
|
||||
ipnet = "2"
|
||||
oslog = "0.2.0"
|
||||
log = "0.4.27"
|
||||
|
||||
@@ -2,14 +2,26 @@
|
||||
|
||||
Softnet is a software networking for [Tart](https://github.com/cirruslabs/tart) which provides better network isolation and alleviates DHCP shortage on production systems.
|
||||
|
||||
It is essentially a userspace packet filter which restricts the VM networking and prevents a class of security issues, such as ARP spoofing. By default, the VM will only be able to:
|
||||
|
||||
* send traffic from its own MAC-address
|
||||
* send traffic from the IP-address assigned to it by the DHCP
|
||||
* send traffic to globally routable IPv4 addresses
|
||||
* send traffic to gateway IP of the vmnet bridge (this would normally be \"bridge100\" interface)
|
||||
* receive any incoming traffic
|
||||
|
||||
In addition, Softnet tunes macOS built-in DHCP server to decrease its lease time from the default 86,400 seconds (one day) to 600 seconds (10 minutes). This is especially important when you use Tart to clone and run a lot of ephemeral VMs over a period of one day.
|
||||
|
||||
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 by using tools that enable conducting the [ARP spoofing attacks](https://en.wikipedia.org/wiki/ARP_spoofing) (e.g. [arpspoof](https://www.monkey.org/~dugsong/dsniff/), [arpoison](http://www.arpoison.net/) and so on)
|
||||
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
|
||||
|
||||
And assumes that:
|
||||
|
||||
@@ -30,4 +42,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`.
|
||||
|
||||
+13
-5
@@ -26,14 +26,14 @@ impl DhcpSnooper {
|
||||
};
|
||||
|
||||
let dns_ips = match message.opts().get(OptionCode::DomainNameServer) {
|
||||
Some(DhcpOption::DomainNameServer(dns_ips)) => HashSet::from_iter(
|
||||
dns_ips.iter().map(|dns_ip| Ipv4Address(dns_ip.octets())),
|
||||
),
|
||||
Some(DhcpOption::DomainNameServer(dns_ips)) => {
|
||||
HashSet::from_iter(dns_ips.iter().cloned())
|
||||
}
|
||||
_ => HashSet::new(),
|
||||
};
|
||||
|
||||
self.vm_lease = Some(Lease::new(
|
||||
message.yiaddr().into(),
|
||||
message.yiaddr(),
|
||||
Duration::from_secs(*lease_time as u64),
|
||||
dns_ips,
|
||||
))
|
||||
@@ -73,7 +73,15 @@ impl Lease {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn address(&self) -> Ipv4Address {
|
||||
self.address
|
||||
}
|
||||
|
||||
pub fn valid(&self) -> bool {
|
||||
Instant::now() < self.valid_until
|
||||
}
|
||||
|
||||
pub fn valid_ip_source(&self, address: Ipv4Address) -> bool {
|
||||
self.address == address && Instant::now() < self.valid_until
|
||||
self.address == address && self.valid()
|
||||
}
|
||||
}
|
||||
|
||||
+77
-24
@@ -1,4 +1,6 @@
|
||||
use crate::{Error, Result};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use clap::ValueEnum;
|
||||
use log::info;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
use std::os::unix::net::UnixDatagram;
|
||||
@@ -6,8 +8,21 @@ use std::str::FromStr;
|
||||
use std::sync::mpsc::{sync_channel, SyncSender};
|
||||
use vmnet::mode::Mode;
|
||||
use vmnet::parameters::{Parameter, ParameterKind};
|
||||
use vmnet::port_forwarding::{AddressFamily, Protocol};
|
||||
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,43 +33,52 @@ 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, enable_isolation: bool) -> 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),
|
||||
enable_isolation: Some(enable_isolation),
|
||||
..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);
|
||||
|
||||
interface
|
||||
.set_event_callback(Events::PACKETS_AVAILABLE, move |_mask, _params| {
|
||||
// Send a dummy datagram to make the other end of socketpair() readable
|
||||
new_packets_tx.send(&[0; 1]).unwrap();
|
||||
// and ignore the error as this merely a signalling channel to wake up
|
||||
// the poller
|
||||
new_packets_tx.send(&[0; 1]).ok();
|
||||
|
||||
// Wait for the permission to continue to avoid
|
||||
// wasting CPU cycles or in case of termination,
|
||||
@@ -64,13 +88,13 @@ 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,
|
||||
new_packets_rx,
|
||||
callback_can_continue_tx,
|
||||
gateway_ip: gateway_ip.into(),
|
||||
gateway_ip,
|
||||
max_packet_size,
|
||||
finalized: false,
|
||||
})
|
||||
@@ -78,6 +102,35 @@ impl Host {
|
||||
}
|
||||
|
||||
impl Host {
|
||||
pub fn port_forwarding_add_rule(
|
||||
&mut self,
|
||||
external_port: u16,
|
||||
internal_addr: Ipv4Addr,
|
||||
internal_port: u16,
|
||||
) -> Result<()> {
|
||||
let details = format!("external_port={external_port}, internal_addr={internal_addr}, internal_port={internal_port}");
|
||||
|
||||
self.interface
|
||||
.port_forwarding_rule_add(
|
||||
AddressFamily::Ipv4,
|
||||
Protocol::Tcp,
|
||||
external_port,
|
||||
internal_addr.into(),
|
||||
internal_port,
|
||||
)
|
||||
.map(|_| info!("added port forwarding rule {details}"))
|
||||
.map_err(|err| anyhow!("failed to add port forwarding rule {details}: {err}"))
|
||||
}
|
||||
|
||||
pub fn port_forwarding_remove_rule(&mut self, external_port: u16) -> Result<()> {
|
||||
let details = format!("external_port={external_port}");
|
||||
|
||||
self.interface
|
||||
.port_forwarding_rule_remove(AddressFamily::Ipv4, Protocol::Tcp, external_port)
|
||||
.map(|_| info!("removed port forwarding rule {details}"))
|
||||
.map_err(|err| anyhow!("failed to remove port forwarding rule {details}: {err}"))
|
||||
}
|
||||
|
||||
pub fn read(&mut self, buf: &mut [u8]) -> vmnet::Result<usize> {
|
||||
// Dequeue dummy datagram from the socket (if any)
|
||||
// to free up buffer space and reduce false-positives
|
||||
@@ -104,14 +157,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;
|
||||
|
||||
|
||||
+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>;
|
||||
|
||||
+45
-33
@@ -1,13 +1,16 @@
|
||||
use crate::{Error, Result};
|
||||
use anyhow::Result;
|
||||
use num_enum::IntoPrimitive;
|
||||
use polling::os::kqueue::PollerKqueueExt;
|
||||
use polling::PollMode;
|
||||
use std::os::fd::{AsRawFd, BorrowedFd};
|
||||
use std::os::unix::io::RawFd;
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct Poller {
|
||||
pub struct Poller<'poller> {
|
||||
poller: polling::Poller,
|
||||
events: Vec<polling::Event>,
|
||||
vm_fd: RawFd,
|
||||
host_fd: RawFd,
|
||||
events: polling::Events,
|
||||
vm_fd: BorrowedFd<'poller>,
|
||||
host_fd: BorrowedFd<'poller>,
|
||||
}
|
||||
|
||||
#[derive(IntoPrimitive)]
|
||||
@@ -15,55 +18,64 @@ 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() })?;
|
||||
impl Poller<'_> {
|
||||
pub fn new<'poller>(vm_fd: RawFd, host_fd: RawFd) -> Result<Poller<'poller>> {
|
||||
let poller = polling::Poller::new()?;
|
||||
|
||||
Ok(Poller {
|
||||
poller,
|
||||
events: Vec::new(),
|
||||
vm_fd,
|
||||
host_fd,
|
||||
events: polling::Events::new(),
|
||||
vm_fd: unsafe { BorrowedFd::borrow_raw(vm_fd) },
|
||||
host_fd: unsafe { BorrowedFd::borrow_raw(host_fd) },
|
||||
})
|
||||
}
|
||||
|
||||
pub fn arm(&self) -> Result<()> {
|
||||
self.poller
|
||||
.add(self.vm_fd as RawFd, self.vm_interest())
|
||||
.map_err(|err| Error::PollFailed { source: err })?;
|
||||
unsafe {
|
||||
self.poller.add_with_mode(
|
||||
self.vm_fd.as_raw_fd(),
|
||||
self.vm_interest(),
|
||||
PollMode::Edge,
|
||||
)?;
|
||||
self.poller.add_with_mode(
|
||||
self.host_fd.as_raw_fd(),
|
||||
self.host_interest(),
|
||||
PollMode::Edge,
|
||||
)?;
|
||||
}
|
||||
|
||||
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::Edge)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn rearm(&mut self) -> Result<()> {
|
||||
pub fn rearm(&mut self) {
|
||||
self.events.clear();
|
||||
|
||||
self.poller
|
||||
.modify(self.vm_fd as RawFd, self.vm_interest())
|
||||
.map_err(|err| Error::PollFailed { source: err })?;
|
||||
self.poller
|
||||
.modify(self.host_fd as RawFd, self.host_interest())
|
||||
.map_err(|err| Error::PollFailed { source: err })?;
|
||||
|
||||
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 {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
use anyhow::{anyhow, Context, Error};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||
pub struct ExposedPort {
|
||||
pub external_port: u16,
|
||||
pub internal_port: u16,
|
||||
}
|
||||
|
||||
impl FromStr for ExposedPort {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let splits: Vec<&str> = s.split(':').collect();
|
||||
|
||||
match splits.len() {
|
||||
2 => Ok(ExposedPort {
|
||||
external_port: splits[0]
|
||||
.parse()
|
||||
.context(format!("invalid external port {:?}", splits[0]))?,
|
||||
internal_port: splits[1]
|
||||
.parse()
|
||||
.context(format!("invalid internal port {:?}", splits[1]))?,
|
||||
}),
|
||||
_ => Err(anyhow!(
|
||||
"invalid exposed port specification {:?}, the format should be EXTERNAL:INTERNAL",
|
||||
s
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::proxy::exposed_port::ExposedPort;
|
||||
|
||||
#[test]
|
||||
fn exposed_port() {
|
||||
assert_eq!(
|
||||
ExposedPort {
|
||||
external_port: 2222,
|
||||
internal_port: 22
|
||||
},
|
||||
"2222:22".parse().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
+24
-8
@@ -1,9 +1,9 @@
|
||||
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 {
|
||||
impl Proxy<'_> {
|
||||
pub(crate) fn process_frame_from_host(&mut self, frame: &EthernetFrame<&[u8]>) -> Result<()> {
|
||||
if self.allowed_from_host(frame).is_none() {
|
||||
// Block packet by not forwarding it to the VM
|
||||
@@ -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<()> {
|
||||
@@ -42,7 +58,7 @@ impl Proxy {
|
||||
return;
|
||||
}
|
||||
|
||||
if ipv4_pkt.protocol() != smoltcp::wire::IpProtocol::Udp {
|
||||
if ipv4_pkt.next_header() != smoltcp::wire::IpProtocol::Udp {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+40
-11
@@ -1,30 +1,45 @@
|
||||
mod exposed_port;
|
||||
mod host;
|
||||
mod port_forwarder;
|
||||
mod udp_packet_helper;
|
||||
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;
|
||||
pub use exposed_port::ExposedPort;
|
||||
use ipnet::Ipv4Net;
|
||||
use mac_address::MacAddress;
|
||||
use port_forwarder::PortForwarder;
|
||||
use prefix_trie::{Prefix, PrefixSet};
|
||||
use smoltcp::wire::EthernetFrame;
|
||||
use std::io::ErrorKind;
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
|
||||
pub struct Proxy {
|
||||
pub struct Proxy<'proxy> {
|
||||
vm: VM,
|
||||
host: Host,
|
||||
poller: Poller,
|
||||
poller: Poller<'proxy>,
|
||||
vm_mac_address: smoltcp::wire::EthernetAddress,
|
||||
dhcp_snooper: DhcpSnooper,
|
||||
allow: PrefixSet<Ipv4Net>,
|
||||
enobufs_encountered: bool,
|
||||
port_forwarder: PortForwarder,
|
||||
}
|
||||
|
||||
impl Proxy {
|
||||
pub fn new(vm_fd: RawFd, vm_mac_address: MacAddress) -> Result<Proxy> {
|
||||
impl Proxy<'_> {
|
||||
pub fn new<'proxy>(
|
||||
vm_fd: RawFd,
|
||||
vm_mac_address: MacAddress,
|
||||
vm_net_type: NetType,
|
||||
allow: PrefixSet<Ipv4Net>,
|
||||
exposed_ports: Vec<ExposedPort>,
|
||||
) -> Result<Proxy<'proxy>> {
|
||||
let vm = VM::new(vm_fd)?;
|
||||
let host = Host::new()?;
|
||||
let host = Host::new(vm_net_type, !allow.contains(&Ipv4Net::zero()))?;
|
||||
let poller = Poller::new(vm.as_raw_fd(), host.as_raw_fd())?;
|
||||
|
||||
Ok(Proxy {
|
||||
@@ -33,6 +48,9 @@ impl Proxy {
|
||||
poller,
|
||||
vm_mac_address: smoltcp::wire::EthernetAddress(vm_mac_address.bytes()),
|
||||
dhcp_snooper: Default::default(),
|
||||
allow,
|
||||
enobufs_encountered: false,
|
||||
port_forwarder: PortForwarder::new(exposed_ports),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -42,7 +60,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,7 +70,18 @@ impl Proxy {
|
||||
self.read_from_host(buf.as_mut_slice())?;
|
||||
}
|
||||
|
||||
self.poller.rearm()?;
|
||||
// Graceful termination
|
||||
if interrupt {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Timeout
|
||||
if !vm_readable && !host_readable && !interrupt {
|
||||
self.port_forwarder
|
||||
.tick(&mut self.host, self.dhcp_snooper.lease());
|
||||
}
|
||||
|
||||
self.poller.rearm();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +98,7 @@ impl Proxy {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
return Err(Error::VMIOFailed { source: err });
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,7 +117,7 @@ impl Proxy {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
return Err(Error::HostIOFailed { source: err });
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
use crate::dhcp_snooper::Lease;
|
||||
use crate::host::Host;
|
||||
use crate::proxy::exposed_port::ExposedPort;
|
||||
use anyhow::Result;
|
||||
use log::error;
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PortForwarder {
|
||||
port_forwardings: Vec<PortForwarding>,
|
||||
failed: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
struct PortForwarding {
|
||||
exposed_port: ExposedPort,
|
||||
forwarding_to_addr: Option<Ipv4Addr>,
|
||||
}
|
||||
|
||||
impl PortForwarder {
|
||||
pub fn new(exposed_ports: Vec<ExposedPort>) -> PortForwarder {
|
||||
let port_forwardings = exposed_ports
|
||||
.into_iter()
|
||||
.map(|exposed_port| PortForwarding {
|
||||
exposed_port,
|
||||
..Default::default()
|
||||
})
|
||||
.collect();
|
||||
|
||||
PortForwarder {
|
||||
port_forwardings,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tick(&mut self, host: &mut Host, lease: &Option<Lease>) {
|
||||
if self.failed {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(err) = self.tick_inner(host, lease) {
|
||||
error!("port-forwarding failed: {}", err);
|
||||
|
||||
self.failed = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn tick_inner(&mut self, host: &mut Host, lease: &Option<Lease>) -> Result<()> {
|
||||
if let Some(lease) = lease {
|
||||
// Lease exists, but is not valid, remove all port forwardings
|
||||
if !lease.valid() {
|
||||
self.remove_all_port_forwardings(host)?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Lease exists and is valid, install/re-install port forwardings
|
||||
for port_forwarding in &mut self.port_forwardings {
|
||||
if let Some(installed_addr) = port_forwarding.forwarding_to_addr {
|
||||
// Port forwarding already installed, perhaps it's outdated?
|
||||
if installed_addr == lease.address() {
|
||||
// Nope, the port forwarding is up to date
|
||||
continue;
|
||||
}
|
||||
|
||||
// Remove port forwarding since the lease address had changed
|
||||
host.port_forwarding_remove_rule(port_forwarding.exposed_port.external_port)?;
|
||||
port_forwarding.forwarding_to_addr = None;
|
||||
}
|
||||
|
||||
// Install new port forwarding
|
||||
host.port_forwarding_add_rule(
|
||||
port_forwarding.exposed_port.external_port,
|
||||
lease.address(),
|
||||
port_forwarding.exposed_port.internal_port,
|
||||
)?;
|
||||
port_forwarding.forwarding_to_addr = Some(lease.address());
|
||||
}
|
||||
} else {
|
||||
// Lease does not exist, remove all port forwardings
|
||||
self.remove_all_port_forwardings(host)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_all_port_forwardings(&mut self, host: &mut Host) -> Result<()> {
|
||||
for port_forwarding in &mut self.port_forwardings {
|
||||
if port_forwarding.forwarding_to_addr.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
host.port_forwarding_remove_rule(port_forwarding.exposed_port.external_port)?;
|
||||
port_forwarding.forwarding_to_addr = None;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+22
-9
@@ -1,12 +1,14 @@
|
||||
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,
|
||||
};
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
impl Proxy {
|
||||
impl Proxy<'_> {
|
||||
pub(crate) fn process_frame_from_vm(&mut self, frame: EthernetFrame<&[u8]>) -> Result<()> {
|
||||
if self.allowed_from_vm(&frame).is_none() {
|
||||
// Block packet by not forwarding it to the host
|
||||
@@ -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<()> {
|
||||
@@ -46,7 +48,7 @@ impl Proxy {
|
||||
let source_protocol_addr = Ipv4Addr::from(source_protocol_addr);
|
||||
|
||||
if let Some(lease) = self.dhcp_snooper.lease() {
|
||||
if lease.valid_ip_source(source_protocol_addr.into()) {
|
||||
if lease.valid_ip_source(source_protocol_addr) {
|
||||
return Some(());
|
||||
}
|
||||
} else if source_protocol_addr.is_unspecified() {
|
||||
@@ -57,15 +59,26 @@ 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 = ipv4_pkt.dst_addr();
|
||||
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);
|
||||
|
||||
// Use get_lpm() instead of get_spm() to work around prefix-trie
|
||||
// not handling prefixes like 0.0.0.0/0 correctly[1]
|
||||
//
|
||||
// [1]: https://github.com/tiborschneider/prefix-trie/issues/8
|
||||
if self.allow.get_lpm(&dst_net).is_some() {
|
||||
return Some(());
|
||||
}
|
||||
}
|
||||
|
||||
// Allow communication with host
|
||||
@@ -73,7 +86,7 @@ impl Proxy {
|
||||
return Some(());
|
||||
}
|
||||
|
||||
if ipv4_pkt.protocol() == IpProtocol::Udp {
|
||||
if ipv4_pkt.next_header() == IpProtocol::Udp {
|
||||
let udp_pkt = UdpPacket::new_checked(ipv4_pkt.payload()).ok()?;
|
||||
|
||||
// Allow DNS communication with the DNS-servers provided by DHCP
|
||||
|
||||
@@ -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,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
+98
-18
@@ -1,17 +1,27 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use clap::Parser;
|
||||
use ipnet::Ipv4Net;
|
||||
use log::LevelFilter;
|
||||
use nix::sys::signal::{signal, SigHandler, Signal};
|
||||
use oslog::OsLogger;
|
||||
use prefix_trie::PrefixSet;
|
||||
use privdrop::PrivDrop;
|
||||
use softnet::proxy::ExposedPort;
|
||||
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 +34,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 +50,81 @@ 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,
|
||||
help = "comma-separated list of TCP ports to expose (e.g. --expose 2222:22,8080:80)",
|
||||
value_name = "comma-separated port specifications",
|
||||
use_value_delimiter = true,
|
||||
action = clap::ArgAction::Set
|
||||
)]
|
||||
expose: Vec<ExposedPort>,
|
||||
|
||||
#[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<()> {
|
||||
// Initialize logger
|
||||
OsLogger::new("org.cirruslabs.softnet")
|
||||
.level_filter(LevelFilter::Info)
|
||||
.init()?;
|
||||
|
||||
// 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 +136,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 +151,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 +163,23 @@ 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),
|
||||
args.expose,
|
||||
)
|
||||
.context("failed to initialize proxy")?;
|
||||
|
||||
// Drop effective privileges to the user
|
||||
// and group which have had invoked us
|
||||
@@ -107,10 +187,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