mirror of
https://github.com/cirruslabs/softnet.git
synced 2026-10-03 13:37:46 +02:00
Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97e7da22fb | ||
|
|
2947383d1a | ||
|
|
df84a30016 | ||
|
|
bbff9996ab | ||
|
|
dd55a20de0 | ||
|
|
012a93834a | ||
|
|
d806af1f9d | ||
|
|
d992da7023 | ||
|
|
2e74102ee4 | ||
|
|
35ae0608db | ||
|
|
54f40f914b | ||
|
|
088011e714 | ||
|
|
3cd8732f1c | ||
|
|
ca15d50f8d | ||
|
|
928dc3c333 | ||
|
|
62d9326d26 | ||
|
|
173f7832b3 | ||
|
|
be2c706b00 | ||
|
|
b1f5678f18 | ||
|
|
c1ddb2afc6 | ||
|
|
401dea6612 | ||
|
|
1dcb0755df | ||
|
|
1706062004 | ||
|
|
dfc04a49e4 | ||
|
|
7dc5992b55 | ||
|
|
58d4b32258 | ||
|
|
431ae9bbc9 | ||
|
|
98988e5c73 | ||
|
|
b122b49b3c | ||
|
|
2650e78eb0 | ||
|
|
0c59910018 | ||
|
|
7e622716f7 |
@@ -0,0 +1,2 @@
|
||||
[target.aarch64-apple-darwin]
|
||||
runner = 'sudo -E'
|
||||
+29
-4
@@ -1,11 +1,35 @@
|
||||
use_compute_credits: true
|
||||
|
||||
macos_instance:
|
||||
image: ghcr.io/cirruslabs/macos-runner:tahoe
|
||||
|
||||
env:
|
||||
PATH: "$PATH:$HOME/.cargo/bin"
|
||||
|
||||
task:
|
||||
name: Lint
|
||||
install_rust_script: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
rustfmt_script: cargo fmt --check
|
||||
clippy_script: cargo clippy --all-targets --all-features -- -D warnings
|
||||
|
||||
task:
|
||||
alias: Test
|
||||
matrix:
|
||||
- name: Test on macOS Sequoia
|
||||
macos_instance:
|
||||
image: ghcr.io/cirruslabs/macos-runner:sequoia
|
||||
- name: Test on macOS Tahoe
|
||||
macos_instance:
|
||||
image: ghcr.io/cirruslabs/macos-runner:tahoe
|
||||
install_rust_script: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
test_script: cargo test
|
||||
|
||||
task:
|
||||
name: Release (Dry Run)
|
||||
only_if: $CIRRUS_TAG == ''
|
||||
macos_instance:
|
||||
image: ghcr.io/cirruslabs/macos-sequoia-xcode:latest
|
||||
depends_on:
|
||||
- Lint
|
||||
- Test
|
||||
install_rust_script: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
install_script: brew install go
|
||||
install_goreleaser_script: brew install --cask goreleaser/tap/goreleaser-pro
|
||||
@@ -16,8 +40,9 @@ task:
|
||||
task:
|
||||
name: Release
|
||||
only_if: $CIRRUS_TAG != ''
|
||||
macos_instance:
|
||||
image: ghcr.io/cirruslabs/macos-sequoia-xcode:latest
|
||||
depends_on:
|
||||
- Lint
|
||||
- Test
|
||||
env:
|
||||
GITHUB_TOKEN: ENCRYPTED[!98ace8259c6024da912c14d5a3c5c6aac186890a8d4819fad78f3e0c41a4e0cd3a2537dd6e91493952fb056fa434be7c!]
|
||||
GORELEASER_KEY: ENCRYPTED[!9b80b6ef684ceaf40edd4c7af93014ee156c8aba7e6e5795f41c482729887b5c31f36b651491d790f1f668670888d9fd!]
|
||||
|
||||
@@ -29,3 +29,5 @@ brews:
|
||||
homepage: https://github.com/cirruslabs/softnet
|
||||
description: Software networking with isolation for Tart
|
||||
skip_upload: auto
|
||||
custom_block: |
|
||||
depends_on :macos => :sequoia
|
||||
|
||||
Generated
+917
-313
File diff suppressed because it is too large
Load Diff
+7
-4
@@ -2,7 +2,7 @@
|
||||
name = "softnet"
|
||||
version = "0.1.0"
|
||||
publish = false
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
path = "lib/mod.rs"
|
||||
@@ -16,7 +16,7 @@ smoltcp = "0"
|
||||
libc = "0"
|
||||
polling = "3"
|
||||
dhcproto = { git = "https://github.com/bluecatengineering/dhcproto.git", branch = "master" }
|
||||
vmnet = "0"
|
||||
vmnet = "0.5.1"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
mac_address = "1"
|
||||
privdrop = "0"
|
||||
@@ -27,11 +27,14 @@ system-configuration = "0"
|
||||
num_enum = "0"
|
||||
sentry = { version = "0", features = ["debug-images"] }
|
||||
sentry-anyhow = { version = "0", features = ["backtrace"] }
|
||||
nix = { version = "0", features = ["signal"] }
|
||||
nix = { version = "0", features = ["signal", "socket"] }
|
||||
prefix-trie = "0"
|
||||
ipnet = "2"
|
||||
oslog = "0.2.0"
|
||||
log = "0.4.28"
|
||||
log = "0.4.29"
|
||||
serial_test = "3"
|
||||
coarsetime = "0.1.37"
|
||||
pnet_datalink = "0.35.0"
|
||||
|
||||
[profile.release]
|
||||
debug = true
|
||||
|
||||
+26
-11
@@ -1,15 +1,23 @@
|
||||
use dhcproto::v4::{DhcpOption, MessageType, OptionCode};
|
||||
use dhcproto::Decodable;
|
||||
use dhcproto::v4::{DhcpOption, MessageType, OptionCode};
|
||||
use smoltcp::wire::Ipv4Address;
|
||||
use std::collections::HashSet;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DhcpSnooper {
|
||||
vm_lease: Option<Lease>,
|
||||
uncertainty_duration: Duration,
|
||||
}
|
||||
|
||||
impl DhcpSnooper {
|
||||
pub fn new(uncertainty_duration: Duration) -> Self {
|
||||
DhcpSnooper {
|
||||
uncertainty_duration,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_dhcp_reply(&mut self, dhcp_packet: &[u8]) {
|
||||
let mut decoder = dhcproto::v4::Decoder::new(dhcp_packet);
|
||||
|
||||
@@ -32,11 +40,12 @@ impl DhcpSnooper {
|
||||
_ => HashSet::new(),
|
||||
};
|
||||
|
||||
self.vm_lease = Some(Lease::new(
|
||||
message.yiaddr(),
|
||||
Duration::from_secs(*lease_time as u64),
|
||||
dns_ips,
|
||||
))
|
||||
let mut lease_duration = Duration::from_secs(*lease_time as u64);
|
||||
|
||||
// Adjust for uncertainty caused by using a coarse clock
|
||||
lease_duration = lease_duration.saturating_sub(self.uncertainty_duration);
|
||||
|
||||
self.vm_lease = Some(Lease::new(message.yiaddr(), lease_duration, dns_ips))
|
||||
}
|
||||
Some(MessageType::Nak) => {
|
||||
self.vm_lease = None;
|
||||
@@ -45,6 +54,11 @@ impl DhcpSnooper {
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn set_lease(&mut self, vm_lease: Option<Lease>) {
|
||||
self.vm_lease = vm_lease
|
||||
}
|
||||
|
||||
pub fn lease(&self) -> &Option<Lease> {
|
||||
&self.vm_lease
|
||||
}
|
||||
@@ -58,17 +72,18 @@ impl DhcpSnooper {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Lease {
|
||||
address: Ipv4Address,
|
||||
valid_until: Instant,
|
||||
valid_until: coarsetime::Instant,
|
||||
dns_ips: HashSet<Ipv4Address>,
|
||||
}
|
||||
|
||||
impl Lease {
|
||||
fn new(address: Ipv4Address, lease_time: Duration, dns_ips: HashSet<Ipv4Address>) -> Lease {
|
||||
pub fn new(address: Ipv4Address, lease_time: Duration, dns_ips: HashSet<Ipv4Address>) -> Lease {
|
||||
Lease {
|
||||
address,
|
||||
valid_until: Instant::now() + lease_time,
|
||||
valid_until: coarsetime::Instant::recent() + lease_time.into(),
|
||||
dns_ips,
|
||||
}
|
||||
}
|
||||
@@ -78,7 +93,7 @@ impl Lease {
|
||||
}
|
||||
|
||||
pub fn valid(&self) -> bool {
|
||||
Instant::now() < self.valid_until
|
||||
coarsetime::Instant::recent() < self.valid_until
|
||||
}
|
||||
|
||||
pub fn valid_ip_source(&self, address: Ipv4Address) -> bool {
|
||||
|
||||
+134
-9
@@ -1,15 +1,17 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use clap::ValueEnum;
|
||||
use log::info;
|
||||
use smoltcp::wire::EthernetAddress;
|
||||
use std::net::IpAddr;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
use std::os::unix::net::UnixDatagram;
|
||||
use std::str::FromStr;
|
||||
use std::sync::mpsc::{sync_channel, SyncSender};
|
||||
use std::sync::mpsc::{SyncSender, sync_channel};
|
||||
use vmnet::mode::Mode;
|
||||
use vmnet::parameters::{Parameter, ParameterKind};
|
||||
use vmnet::port_forwarding::{AddressFamily, Protocol};
|
||||
use vmnet::{Events, Options};
|
||||
use vmnet::{Batch, Events, Options};
|
||||
|
||||
#[derive(ValueEnum, Clone, Debug)]
|
||||
pub enum NetType {
|
||||
@@ -28,7 +30,9 @@ pub struct Host {
|
||||
new_packets_rx: UnixDatagram,
|
||||
callback_can_continue_tx: SyncSender<()>,
|
||||
pub gateway_ip: smoltcp::wire::Ipv4Address,
|
||||
pub gateway_mac: EthernetAddress,
|
||||
pub max_packet_size: u64,
|
||||
pub read_max_packets: u64,
|
||||
finalized: bool,
|
||||
}
|
||||
|
||||
@@ -48,16 +52,43 @@ impl Host {
|
||||
.context("failed to initialize vmnet interface")?;
|
||||
|
||||
// Retrieve first IP (gateway) used for this interface
|
||||
let Some(Parameter::StartAddress(gateway_ip)) =
|
||||
let Some(Parameter::StartAddress(start_address)) =
|
||||
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)
|
||||
let start_address = Ipv4Addr::from_str(&start_address)
|
||||
.context("failed to parse vmnet's interface start address")?;
|
||||
|
||||
// Retrieve last IP used for this interface and calculate the prefix
|
||||
let Some(Parameter::EndAddress(end_address)) =
|
||||
interface.parameters().get(ParameterKind::EndAddress)
|
||||
else {
|
||||
return Err(anyhow!("failed to retrieve vmnet's interface end address"));
|
||||
};
|
||||
let end_address = Ipv4Addr::from_str(&end_address)
|
||||
.context("failed to parse vmnet's interface end address")?;
|
||||
|
||||
let Some(prefix) = Self::ipv4_range_prefix(start_address, end_address) else {
|
||||
return Err(anyhow!(
|
||||
"failed to resolve vmnet's interface: prefix ambiguity for {}–{}",
|
||||
start_address,
|
||||
end_address
|
||||
));
|
||||
};
|
||||
|
||||
// Figure out the gateway's interface MAC address
|
||||
let Some(gateway_mac) = Self::interface_mac_for_ip(start_address, prefix) else {
|
||||
return Err(anyhow!(
|
||||
"failed to resolve vmnet's interface: no interface found with {}/{} CIDR",
|
||||
start_address,
|
||||
prefix
|
||||
));
|
||||
};
|
||||
let gateway_mac = EthernetAddress(gateway_mac.octets());
|
||||
|
||||
// Retrieve max packet size for this interface
|
||||
let Some(Parameter::MaxPacketSize(max_packet_size)) =
|
||||
interface.parameters().get(ParameterKind::MaxPacketSize)
|
||||
@@ -67,6 +98,15 @@ impl Host {
|
||||
));
|
||||
};
|
||||
|
||||
// Retrieve read max packets for this interface
|
||||
let Some(Parameter::ReadMaxPackets(read_max_packets)) =
|
||||
interface.parameters().get(ParameterKind::ReadMaxPackets)
|
||||
else {
|
||||
return Err(anyhow!(
|
||||
"failed to retrieve vmnet's interface read max packets"
|
||||
));
|
||||
};
|
||||
|
||||
// Set up a socketpair() to emulate polling of the vmnet interface
|
||||
let (new_packets_tx, new_packets_rx) = UnixDatagram::pair()?;
|
||||
new_packets_rx.set_nonblocking(true)?;
|
||||
@@ -94,11 +134,39 @@ impl Host {
|
||||
interface,
|
||||
new_packets_rx,
|
||||
callback_can_continue_tx,
|
||||
gateway_ip,
|
||||
gateway_ip: start_address,
|
||||
gateway_mac,
|
||||
max_packet_size,
|
||||
read_max_packets,
|
||||
finalized: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn interface_mac_for_ip(ip: Ipv4Addr, prefix: u8) -> Option<pnet_datalink::MacAddr> {
|
||||
for iface in pnet_datalink::interfaces() {
|
||||
if iface
|
||||
.ips
|
||||
.iter()
|
||||
.any(|network| network.ip() == IpAddr::V4(ip) && network.prefix() == prefix)
|
||||
&& let Some(mac) = iface.mac
|
||||
{
|
||||
return Some(mac);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn ipv4_range_prefix(start_address: Ipv4Addr, end_address: Ipv4Addr) -> Option<u8> {
|
||||
let start_address = start_address.to_bits();
|
||||
let end_address = end_address.to_bits();
|
||||
|
||||
if start_address > end_address {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((start_address ^ end_address).leading_zeros() as u8)
|
||||
}
|
||||
}
|
||||
|
||||
impl Host {
|
||||
@@ -108,7 +176,9 @@ impl Host {
|
||||
internal_addr: Ipv4Addr,
|
||||
internal_port: u16,
|
||||
) -> Result<()> {
|
||||
let details = format!("external_port={external_port}, internal_addr={internal_addr}, internal_port={internal_port}");
|
||||
let details = format!(
|
||||
"external_port={external_port}, internal_addr={internal_addr}, internal_port={internal_port}"
|
||||
);
|
||||
|
||||
self.interface
|
||||
.port_forwarding_rule_add(
|
||||
@@ -131,14 +201,14 @@ impl Host {
|
||||
.map_err(|err| anyhow!("failed to remove port forwarding rule {details}: {err}"))
|
||||
}
|
||||
|
||||
pub fn read(&mut self, buf: &mut [u8]) -> vmnet::Result<usize> {
|
||||
pub fn read(&mut self, batch: &mut Batch, bufs: &mut [Vec<u8>]) -> vmnet::Result<usize> {
|
||||
// Dequeue dummy datagram from the socket (if any)
|
||||
// to free up buffer space and reduce false-positives
|
||||
// when polling
|
||||
let mut buf_to_be_discarded: [u8; 1] = [0; 1];
|
||||
let _ = self.new_packets_rx.recv(&mut buf_to_be_discarded);
|
||||
|
||||
let result = self.interface.read(buf);
|
||||
let result = self.interface.read_batch(batch, bufs);
|
||||
|
||||
if let Err(vmnet::Error::VmnetReadNothing) = result {
|
||||
// We've emptied everything, unlock the callback
|
||||
@@ -185,3 +255,58 @@ impl AsRawFd for Host {
|
||||
self.new_packets_rx.as_raw_fd()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Host;
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
#[test]
|
||||
fn ipv4_range_prefix_for_class_c_subnet() {
|
||||
assert_eq!(
|
||||
Host::ipv4_range_prefix(
|
||||
Ipv4Addr::new(192, 168, 64, 0),
|
||||
Ipv4Addr::new(192, 168, 64, 255),
|
||||
),
|
||||
Some(24)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ipv4_range_prefix_for_single_address() {
|
||||
assert_eq!(
|
||||
Host::ipv4_range_prefix(Ipv4Addr::new(10, 0, 0, 1), Ipv4Addr::new(10, 0, 0, 1)),
|
||||
Some(32)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ipv4_range_prefix_for_class_c_usable_range() {
|
||||
assert_eq!(
|
||||
Host::ipv4_range_prefix(
|
||||
Ipv4Addr::new(192, 168, 64, 1),
|
||||
Ipv4Addr::new(192, 168, 64, 254),
|
||||
),
|
||||
Some(24)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ipv4_range_prefix_for_containing_class_c_subnet() {
|
||||
assert_eq!(
|
||||
Host::ipv4_range_prefix(
|
||||
Ipv4Addr::new(192, 168, 64, 2),
|
||||
Ipv4Addr::new(192, 168, 64, 254),
|
||||
),
|
||||
Some(24)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ipv4_range_prefix_rejects_inverted_range() {
|
||||
assert_eq!(
|
||||
Host::ipv4_range_prefix(Ipv4Addr::new(10, 0, 0, 2), Ipv4Addr::new(10, 0, 0, 1)),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+9
-4
@@ -1,7 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use num_enum::IntoPrimitive;
|
||||
use polling::os::kqueue::PollerKqueueExt;
|
||||
use polling::PollMode;
|
||||
use polling::os::kqueue::PollerKqueueExt;
|
||||
use std::os::fd::{AsRawFd, BorrowedFd};
|
||||
use std::os::unix::io::RawFd;
|
||||
use std::time::Duration;
|
||||
@@ -9,6 +9,7 @@ use std::time::Duration;
|
||||
pub struct Poller<'poller> {
|
||||
poller: polling::Poller,
|
||||
events: polling::Events,
|
||||
timeout: Duration,
|
||||
vm_fd: BorrowedFd<'poller>,
|
||||
host_fd: BorrowedFd<'poller>,
|
||||
}
|
||||
@@ -22,12 +23,17 @@ enum EventKey {
|
||||
}
|
||||
|
||||
impl Poller<'_> {
|
||||
pub fn new<'poller>(vm_fd: RawFd, host_fd: RawFd) -> Result<Poller<'poller>> {
|
||||
pub fn new<'poller>(
|
||||
vm_fd: RawFd,
|
||||
host_fd: RawFd,
|
||||
timeout: Duration,
|
||||
) -> Result<Poller<'poller>> {
|
||||
let poller = polling::Poller::new()?;
|
||||
|
||||
Ok(Poller {
|
||||
poller,
|
||||
events: polling::Events::new(),
|
||||
timeout,
|
||||
vm_fd: unsafe { BorrowedFd::borrow_raw(vm_fd) },
|
||||
host_fd: unsafe { BorrowedFd::borrow_raw(host_fd) },
|
||||
})
|
||||
@@ -59,8 +65,7 @@ impl Poller<'_> {
|
||||
}
|
||||
|
||||
pub fn wait(&mut self) -> Result<(bool, bool, bool)> {
|
||||
self.poller
|
||||
.wait(&mut self.events, Some(Duration::from_millis(100)))?;
|
||||
self.poller.wait(&mut self.events, Some(self.timeout))?;
|
||||
|
||||
let vm_readable = self
|
||||
.events
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::{anyhow, Context, Error};
|
||||
use anyhow::{Context, Error, anyhow};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||
|
||||
+9
-1
@@ -1,5 +1,5 @@
|
||||
use crate::proxy::udp_packet_helper::UdpPacketHelper;
|
||||
use crate::proxy::Proxy;
|
||||
use crate::proxy::udp_packet_helper::UdpPacketHelper;
|
||||
use anyhow::{Context, Result};
|
||||
use smoltcp::wire::{EthernetFrame, EthernetProtocol, Ipv4Packet, UdpPacket};
|
||||
|
||||
@@ -37,6 +37,14 @@ impl Proxy<'_> {
|
||||
}
|
||||
|
||||
fn allowed_from_host(&mut self, frame: &EthernetFrame<&[u8]>) -> Option<()> {
|
||||
let from_gateway = frame.src_addr() == self.host.gateway_mac;
|
||||
let peer_action = self.rules_mac.get(frame.src_addr().as_bytes());
|
||||
let from_allowed_peer = peer_action == Some(&crate::proxy::Action::Allow);
|
||||
|
||||
if !from_gateway && !from_allowed_peer {
|
||||
return None;
|
||||
}
|
||||
|
||||
match frame.ethertype() {
|
||||
EthernetProtocol::Arp => Some(()),
|
||||
EthernetProtocol::Ipv4 => Some(()),
|
||||
|
||||
+275
-13
@@ -14,10 +14,14 @@ pub use exposed_port::ExposedPort;
|
||||
use ipnet::Ipv4Net;
|
||||
use mac_address::MacAddress;
|
||||
use port_forwarder::PortForwarder;
|
||||
use prefix_trie::{Prefix, PrefixSet};
|
||||
use prefix_trie::{Prefix, PrefixMap};
|
||||
use smoltcp::wire::EthernetFrame;
|
||||
use std::collections::HashMap;
|
||||
use std::io::ErrorKind;
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use vmnet::Batch;
|
||||
|
||||
pub struct Proxy<'proxy> {
|
||||
vm: VM,
|
||||
@@ -25,49 +29,139 @@ pub struct Proxy<'proxy> {
|
||||
poller: Poller<'proxy>,
|
||||
vm_mac_address: smoltcp::wire::EthernetAddress,
|
||||
dhcp_snooper: DhcpSnooper,
|
||||
allow: PrefixSet<Ipv4Net>,
|
||||
rules: PrefixMap<Ipv4Net, Action>,
|
||||
rules_mac: HashMap<[u8; 6], Action>,
|
||||
enobufs_encountered: bool,
|
||||
port_forwarder: PortForwarder,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Target {
|
||||
Prefix(Ipv4Net),
|
||||
MacAddress(MacAddress),
|
||||
Host,
|
||||
}
|
||||
|
||||
impl Target {
|
||||
pub fn is_ipv4_default_route(&self) -> bool {
|
||||
matches!(self, Target::Prefix(prefix) if *prefix == Ipv4Net::zero())
|
||||
}
|
||||
|
||||
pub fn is_mac_address(&self) -> bool {
|
||||
matches!(self, Target::MacAddress(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Target {
|
||||
type Err = ipnet::AddrParseError;
|
||||
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
if s == "@host" {
|
||||
return Ok(Target::Host);
|
||||
}
|
||||
|
||||
if let Ok(mac_address) = MacAddress::from_str(s) {
|
||||
return Ok(Target::MacAddress(mac_address));
|
||||
}
|
||||
|
||||
Ipv4Net::from_str(s).map(Target::Prefix)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) enum Action {
|
||||
Block,
|
||||
Allow,
|
||||
}
|
||||
|
||||
impl Proxy<'_> {
|
||||
pub fn new<'proxy>(
|
||||
vm_fd: RawFd,
|
||||
vm_mac_address: MacAddress,
|
||||
vm_net_type: NetType,
|
||||
allow: PrefixSet<Ipv4Net>,
|
||||
enable_isolation: bool,
|
||||
allow: Vec<Target>,
|
||||
block: Vec<Target>,
|
||||
exposed_ports: Vec<ExposedPort>,
|
||||
) -> Result<Proxy<'proxy>> {
|
||||
let vm = VM::new(vm_fd)?;
|
||||
let host = Host::new(vm_net_type, !allow.contains(&Ipv4Net::zero()))?;
|
||||
let poller = Poller::new(vm.as_raw_fd(), host.as_raw_fd())?;
|
||||
let host = Host::new(vm_net_type, enable_isolation)?;
|
||||
let poller_timeout = Duration::from_millis(100);
|
||||
let poller = Poller::new(vm.as_raw_fd(), host.as_raw_fd(), poller_timeout)?;
|
||||
|
||||
// Craft packet filter rules
|
||||
//
|
||||
// SECURITY: blocking rules must always take precedence
|
||||
// over allowing rules when prefixes are identical.
|
||||
let mut rules = PrefixMap::new();
|
||||
let mut rules_mac = HashMap::new();
|
||||
|
||||
for allow_target in allow {
|
||||
let allow_prefix = match allow_target {
|
||||
Target::Prefix(prefix) => prefix,
|
||||
Target::Host => host.gateway_ip.into(),
|
||||
Target::MacAddress(mac_address) => {
|
||||
rules_mac.insert(mac_address.bytes(), Action::Allow);
|
||||
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
rules.insert(allow_prefix, Action::Allow);
|
||||
}
|
||||
|
||||
for block_target in block {
|
||||
let block_prefix = match block_target {
|
||||
Target::Prefix(prefix) => prefix,
|
||||
Target::Host => host.gateway_ip.into(),
|
||||
Target::MacAddress(mac_address) => {
|
||||
rules_mac.insert(mac_address.bytes(), Action::Block);
|
||||
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
rules.insert(block_prefix, Action::Block);
|
||||
}
|
||||
|
||||
Ok(Proxy {
|
||||
vm,
|
||||
host,
|
||||
poller,
|
||||
vm_mac_address: smoltcp::wire::EthernetAddress(vm_mac_address.bytes()),
|
||||
dhcp_snooper: Default::default(),
|
||||
allow,
|
||||
dhcp_snooper: DhcpSnooper::new(poller_timeout),
|
||||
rules,
|
||||
rules_mac,
|
||||
enobufs_encountered: false,
|
||||
port_forwarder: PortForwarder::new(exposed_ports),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run(&mut self) -> Result<()> {
|
||||
// Create a single buffer from reading from the VM
|
||||
let mut buf: Vec<u8> = vec![0; self.host.max_packet_size as usize];
|
||||
|
||||
// Create multiple buffers and a batch for reading from the host
|
||||
let mut bufs = vec![
|
||||
vec![0u8; self.host.max_packet_size as usize];
|
||||
self.host.read_max_packets as usize
|
||||
];
|
||||
let mut batch = Batch::preallocate(bufs.len());
|
||||
|
||||
self.poller.arm()?;
|
||||
|
||||
loop {
|
||||
let (vm_readable, host_readable, interrupt) = self.poller.wait()?;
|
||||
|
||||
// Update coarse time for the DHCP snooper
|
||||
coarsetime::Instant::update();
|
||||
|
||||
if vm_readable {
|
||||
self.read_from_vm(buf.as_mut_slice())?;
|
||||
}
|
||||
|
||||
if host_readable {
|
||||
self.read_from_host(buf.as_mut_slice())?;
|
||||
self.read_from_host(&mut batch, &mut bufs)?;
|
||||
}
|
||||
|
||||
// Graceful termination
|
||||
@@ -89,6 +183,9 @@ impl Proxy<'_> {
|
||||
loop {
|
||||
match self.vm.read(buf) {
|
||||
Ok(n) => {
|
||||
// Update coarse time for the DHCP snooper
|
||||
coarsetime::Instant::update();
|
||||
|
||||
if let Ok(frame) = EthernetFrame::new_checked(&buf[..n]) {
|
||||
self.process_frame_from_vm(frame)?;
|
||||
}
|
||||
@@ -104,12 +201,17 @@ impl Proxy<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn read_from_host(&mut self, buf: &mut [u8]) -> Result<()> {
|
||||
fn read_from_host(&mut self, batch: &mut Batch, bufs: &mut [Vec<u8>]) -> Result<()> {
|
||||
loop {
|
||||
match self.host.read(buf) {
|
||||
Ok(n) => {
|
||||
if let Ok(pkt) = EthernetFrame::new_checked(&buf[..n]) {
|
||||
self.process_frame_from_host(&pkt)?;
|
||||
match self.host.read(batch, bufs) {
|
||||
Ok(pktcnt) => {
|
||||
// Update coarse time for the DHCP snooper
|
||||
coarsetime::Instant::update();
|
||||
|
||||
for buf in batch.packet_sized_bufs(bufs).take(pktcnt) {
|
||||
if let Ok(pkt) = EthernetFrame::new_checked(buf) {
|
||||
self.process_frame_from_host(&pkt)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -123,3 +225,163 @@ impl Proxy<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::NetType;
|
||||
use crate::dhcp_snooper::Lease;
|
||||
use crate::proxy::{Action, Proxy, Target};
|
||||
use ipnet::Ipv4Net;
|
||||
use mac_address::MacAddress;
|
||||
use nix::sys::socket::{AddressFamily, SockFlag, SockType, socketpair};
|
||||
use prefix_trie::PrefixMap;
|
||||
use serial_test::serial;
|
||||
use smoltcp::wire::{EthernetAddress, Ipv4Address, Ipv4Packet};
|
||||
use std::collections::HashSet;
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_blocking_takes_precedence() {
|
||||
let vm_ip = Ipv4Address::from_str("192.168.0.2").unwrap();
|
||||
let proxy = create_proxy(vm_ip, vec!["66.66.0.0/16"], vec!["66.66.0.0/16"]);
|
||||
|
||||
assert_eq!(
|
||||
proxy.rules,
|
||||
PrefixMap::<Ipv4Net, Action>::from_iter(vec![(
|
||||
Ipv4Net::from_str("66.66.0.0/16").unwrap(),
|
||||
Action::Block
|
||||
),])
|
||||
);
|
||||
|
||||
assert!(allowed_from_vm_ipv4(&proxy, vm_ip, "66.66.66.66").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_longest_prefix_match_wins() {
|
||||
let vm_ip = Ipv4Address::from_str("192.168.0.2").unwrap();
|
||||
let proxy = create_proxy(vm_ip, vec!["33.33.33.33/32"], vec!["33.33.33.0/24"]);
|
||||
|
||||
assert_eq!(
|
||||
proxy.rules,
|
||||
PrefixMap::<Ipv4Net, Action>::from_iter(vec![
|
||||
(Ipv4Net::from_str("33.33.33.33/32").unwrap(), Action::Allow),
|
||||
(Ipv4Net::from_str("33.33.33.0/24").unwrap(), Action::Block),
|
||||
])
|
||||
);
|
||||
|
||||
assert!(allowed_from_vm_ipv4(&proxy, vm_ip, "33.33.33.32").is_none());
|
||||
assert!(allowed_from_vm_ipv4(&proxy, vm_ip, "33.33.33.33").is_some());
|
||||
assert!(allowed_from_vm_ipv4(&proxy, vm_ip, "33.33.33.34").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_allow_host() {
|
||||
let vm_ip = Ipv4Address::from_str("192.168.0.2").unwrap();
|
||||
let proxy = create_proxy(vm_ip, vec!["@host"], vec!["0.0.0.0/0"]);
|
||||
|
||||
assert_eq!(
|
||||
proxy.rules,
|
||||
PrefixMap::from_iter(vec![
|
||||
(proxy.host.gateway_ip.into(), Action::Allow),
|
||||
(Ipv4Net::from_str("0.0.0.0/0").unwrap(), Action::Block),
|
||||
])
|
||||
);
|
||||
|
||||
// Access to global IPs should be disallowed because of --block=0.0.0.0/0
|
||||
assert!(allowed_from_vm_ipv4(&proxy, vm_ip, "8.8.8.8").is_none());
|
||||
|
||||
// Despite the above, access to host IP address should be possible because of --allow=@host
|
||||
assert!(allowed_from_vm_ipv4(&proxy, vm_ip, &proxy.host.gateway_ip.to_string()).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_allow_mac_takes_precedence_over_blocked_prefix() {
|
||||
let vm_ip = Ipv4Address::from_str("192.168.0.2").unwrap();
|
||||
let peer_mac = EthernetAddress([0x02, 0, 0, 0, 0, 0x02]);
|
||||
let proxy = create_proxy(vm_ip, vec!["02:00:00:00:00:02"], vec!["0.0.0.0/0"]);
|
||||
|
||||
assert!(allowed_from_vm_ipv4_to_mac(&proxy, vm_ip, "192.168.0.3", peer_mac).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_block_mac_takes_precedence_over_allowed_prefix() {
|
||||
let vm_ip = Ipv4Address::from_str("192.168.0.2").unwrap();
|
||||
let peer_mac = EthernetAddress([0x02, 0, 0, 0, 0, 0x02]);
|
||||
let proxy = create_proxy(
|
||||
vm_ip,
|
||||
vec!["192.168.0.0/24", "02:00:00:00:00:02"],
|
||||
vec!["02:00:00:00:00:02"],
|
||||
);
|
||||
|
||||
assert!(allowed_from_vm_ipv4_to_mac(&proxy, vm_ip, "192.168.0.3", peer_mac).is_none());
|
||||
}
|
||||
|
||||
fn create_proxy<'test>(vm_ip: Ipv4Address, allow: Vec<&str>, block: Vec<&str>) -> Proxy<'test> {
|
||||
let (vm_fd, _) = socketpair(
|
||||
AddressFamily::Unix,
|
||||
SockType::Datagram,
|
||||
None,
|
||||
SockFlag::empty(),
|
||||
)
|
||||
.unwrap();
|
||||
let vm_fd = Box::leak(Box::new(vm_fd));
|
||||
|
||||
let allow: Vec<Target> = allow
|
||||
.into_iter()
|
||||
.map(|cidr| cidr.parse().unwrap())
|
||||
.collect();
|
||||
let block: Vec<Target> = block
|
||||
.into_iter()
|
||||
.map(|cidr| cidr.parse().unwrap())
|
||||
.collect();
|
||||
let enable_isolation = !allow.iter().any(Target::is_ipv4_default_route)
|
||||
&& !allow.iter().chain(block.iter()).any(Target::is_mac_address);
|
||||
|
||||
let mut proxy = Proxy::new(
|
||||
vm_fd.as_raw_fd(),
|
||||
MacAddress::from_str("02:00:00:00:00:01").unwrap(),
|
||||
NetType::Nat,
|
||||
enable_isolation,
|
||||
allow,
|
||||
block,
|
||||
Vec::default(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
proxy.dhcp_snooper.set_lease(Some(Lease::new(
|
||||
vm_ip,
|
||||
Duration::from_secs(600),
|
||||
HashSet::new(),
|
||||
)));
|
||||
|
||||
proxy
|
||||
}
|
||||
|
||||
fn allowed_from_vm_ipv4(proxy: &Proxy, src: Ipv4Address, dst: &str) -> Option<()> {
|
||||
allowed_from_vm_ipv4_to_mac(proxy, src, dst, EthernetAddress([0x02, 0, 0, 0, 0, 0x03]))
|
||||
}
|
||||
|
||||
fn allowed_from_vm_ipv4_to_mac(
|
||||
proxy: &Proxy,
|
||||
src: Ipv4Address,
|
||||
dst: &str,
|
||||
dst_mac: EthernetAddress,
|
||||
) -> Option<()> {
|
||||
let mut buf = vec![0; 1500];
|
||||
|
||||
let mut ipv4_pkt_mut = Ipv4Packet::new_unchecked(&mut buf[..]);
|
||||
ipv4_pkt_mut.set_src_addr(src);
|
||||
ipv4_pkt_mut.set_dst_addr(Ipv4Address::from_str(dst).unwrap());
|
||||
|
||||
let ipv4_pkt = Ipv4Packet::new_unchecked(buf.as_slice());
|
||||
|
||||
proxy.allowed_from_vm_ipv4(ipv4_pkt, dst_mac)
|
||||
}
|
||||
}
|
||||
|
||||
+59
-30
@@ -1,10 +1,10 @@
|
||||
use crate::proxy::udp_packet_helper::UdpPacketHelper;
|
||||
use crate::proxy::Proxy;
|
||||
use crate::proxy::{Action, Proxy};
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use ipnet::Ipv4Net;
|
||||
use smoltcp::wire::{
|
||||
ArpPacket, EthernetFrame, EthernetProtocol, IpProtocol, Ipv4Packet, UdpPacket,
|
||||
ArpPacket, EthernetAddress, EthernetFrame, EthernetProtocol, IpProtocol, Ipv4Packet, UdpPacket,
|
||||
};
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
@@ -29,21 +29,29 @@ impl Proxy<'_> {
|
||||
match frame.ethertype() {
|
||||
EthernetProtocol::Arp => {
|
||||
let arp_pkt = ArpPacket::new_checked(frame.payload()).ok()?;
|
||||
self.allowed_from_vm_arp(arp_pkt)
|
||||
self.allowed_from_vm_arp(arp_pkt, frame.dst_addr())
|
||||
}
|
||||
EthernetProtocol::Ipv4 => {
|
||||
let ipv4_pkt = Ipv4Packet::new_checked(frame.payload()).ok()?;
|
||||
self.allowed_from_vm_ipv4(ipv4_pkt)
|
||||
self.allowed_from_vm_ipv4(ipv4_pkt, frame.dst_addr())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn allowed_from_vm_arp(&self, arp_pkt: ArpPacket<&[u8]>) -> Option<()> {
|
||||
fn allowed_from_vm_arp(
|
||||
&self,
|
||||
arp_pkt: ArpPacket<&[u8]>,
|
||||
dst_mac: EthernetAddress,
|
||||
) -> Option<()> {
|
||||
if arp_pkt.source_hardware_addr() != self.vm_mac_address.0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.rules_mac.get(dst_mac.as_bytes()) == Some(&Action::Block) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let source_protocol_addr: [u8; 4] = arp_pkt.source_protocol_addr().try_into().unwrap();
|
||||
let source_protocol_addr = Ipv4Addr::from(source_protocol_addr);
|
||||
|
||||
@@ -58,43 +66,64 @@ impl Proxy<'_> {
|
||||
None
|
||||
}
|
||||
|
||||
fn allowed_from_vm_ipv4(&self, ipv4_pkt: Ipv4Packet<&[u8]>) -> Option<()> {
|
||||
// Have we learned the VM's IP from the DHCP snooping?
|
||||
if let Some(lease) = &self.dhcp_snooper.lease() {
|
||||
// If so, allow all global traffic
|
||||
pub(crate) fn allowed_from_vm_ipv4(
|
||||
&self,
|
||||
ipv4_pkt: Ipv4Packet<&[u8]>,
|
||||
dst_mac: EthernetAddress,
|
||||
) -> Option<()> {
|
||||
// Is this packet coming from VM's IP address that we've learned from DHCP snooping?
|
||||
if let Some(lease) = &self.dhcp_snooper.lease()
|
||||
&& lease.valid_ip_source(ipv4_pkt.src_addr())
|
||||
{
|
||||
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 {
|
||||
match self.rules_mac.get(dst_mac.as_bytes()) {
|
||||
Some(Action::Block) => return None,
|
||||
Some(Action::Allow) => return Some(()),
|
||||
None => {}
|
||||
}
|
||||
|
||||
// Filter traffic based on user-specified rules first
|
||||
if !self.rules.is_empty() {
|
||||
let dst_net = Ipv4Net::from(dst_addr);
|
||||
|
||||
if let Some((_, action)) = self.rules.get_lpm(&dst_net) {
|
||||
return match action {
|
||||
Action::Allow => Some(()),
|
||||
Action::Block => None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// When no user-specified rules matched, simply allow all global traffic
|
||||
if ip_network::IpNetwork::from(dst_addr).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() {
|
||||
// Additionally, allow communication with the host,
|
||||
// otherwise things like SSH to a VM won't work
|
||||
if ipv4_pkt.dst_addr() == self.host.gateway_ip {
|
||||
return Some(());
|
||||
}
|
||||
|
||||
// Additionally, allow DNS requests to DNS-servers
|
||||
// provided to a VM by the host's DHCP server
|
||||
if ipv4_pkt.next_header() == IpProtocol::Udp {
|
||||
let udp_pkt = UdpPacket::new_checked(ipv4_pkt.payload()).ok()?;
|
||||
|
||||
if udp_pkt.is_dns_request()
|
||||
&& self.dhcp_snooper.valid_dns_target(&ipv4_pkt.dst_addr())
|
||||
{
|
||||
return Some(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Allow communication with host
|
||||
if ipv4_pkt.dst_addr() == self.host.gateway_ip {
|
||||
return Some(());
|
||||
}
|
||||
|
||||
// Allow outgoing DHCP requests to broadcast addresses,
|
||||
// otherwise DHCP snooper will never be populated
|
||||
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
|
||||
if udp_pkt.is_dns_request() && self.dhcp_snooper.valid_dns_target(&ipv4_pkt.dst_addr())
|
||||
{
|
||||
return Some(());
|
||||
}
|
||||
|
||||
// Allow DHCP communication with the bootpd(8) on host via broadcast address
|
||||
if udp_pkt.is_dhcp_request() && ipv4_pkt.dst_addr().is_broadcast() {
|
||||
return Some(());
|
||||
|
||||
+86
-10
@@ -1,14 +1,13 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{Context, anyhow};
|
||||
use clap::Parser;
|
||||
use ipnet::Ipv4Net;
|
||||
use log::LevelFilter;
|
||||
use nix::sys::signal::{signal, SigHandler, Signal};
|
||||
use nix::sys::signal::{SigHandler, Signal, signal};
|
||||
use oslog::OsLogger;
|
||||
use prefix_trie::PrefixSet;
|
||||
use privdrop::PrivDrop;
|
||||
use softnet::NetType;
|
||||
use softnet::proxy::ExposedPort;
|
||||
use softnet::proxy::Proxy;
|
||||
use softnet::NetType;
|
||||
use softnet::proxy::Target;
|
||||
use std::borrow::Cow;
|
||||
use std::env;
|
||||
use std::os::raw::c_int;
|
||||
@@ -52,12 +51,34 @@ struct Args {
|
||||
|
||||
#[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",
|
||||
help = "Comma-separated list of CIDRs to allow the traffic to \
|
||||
(e.g. --allow=192.168.0.0/24 may be used to allow a LAN access for a VM), \
|
||||
plus supported @-aliases. Currently the only supported @-alias is @host, \
|
||||
which matches the vmnet bridge gateway IP. \
|
||||
When used with --block, the longest prefix match always wins. \
|
||||
In case an identical prefix is both --allow'ed and --block'ed, \
|
||||
blocking will take precedence. --allow=0.0.0.0/0 is a special case, \
|
||||
it additionally disables bridge isolation (even when --block=0.0.0.0/0 is specified).",
|
||||
value_name = "comma-separated CIDRs, MAC addresses or @-aliases",
|
||||
use_value_delimiter = true,
|
||||
action = clap::ArgAction::Set
|
||||
)]
|
||||
allow: Vec<Ipv4Net>,
|
||||
allow: Vec<Target>,
|
||||
|
||||
#[clap(
|
||||
long,
|
||||
help = "Comma-separated list of CIDRs to block the traffic to \
|
||||
(e.g. --block=0.0.0.0/0 may be used to establish a default deny policy \
|
||||
that is further relaxed with --allow), plus supported @-aliases. \
|
||||
Currently the only supported @-alias is @host, which matches the vmnet bridge gateway IP. \
|
||||
When used with --allow, the longest prefix match always wins. \
|
||||
In case an identical prefix is both --allow'ed and --block'ed, \
|
||||
blocking will take precedence.",
|
||||
value_name = "comma-separated CIDRs, MAC addresses or @-aliases",
|
||||
use_value_delimiter = true,
|
||||
action = clap::ArgAction::Set
|
||||
)]
|
||||
block: Vec<Target>,
|
||||
|
||||
#[clap(
|
||||
long,
|
||||
@@ -78,7 +99,9 @@ struct Args {
|
||||
fn main() -> ExitCode {
|
||||
// Enable backtraces by default
|
||||
if env::var("RUST_BACKTRACE").is_err() {
|
||||
env::set_var("RUST_BACKTRACE", "full");
|
||||
unsafe {
|
||||
env::set_var("RUST_BACKTRACE", "full");
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize Sentry
|
||||
@@ -171,12 +194,16 @@ fn try_main() -> anyhow::Result<()> {
|
||||
// Set bootpd(8) min/max lease time while still having the root privileges
|
||||
set_bootpd_lease_time(args.bootpd_lease_time);
|
||||
|
||||
let enable_isolation = bridge_isolation_enabled(&args.allow, &args.block);
|
||||
|
||||
// Initialize the proxy while still having the root privileges
|
||||
let mut proxy = Proxy::new(
|
||||
args.vm_fd as RawFd,
|
||||
args.vm_mac_address,
|
||||
args.vm_net_type,
|
||||
PrefixSet::from_iter(args.allow),
|
||||
enable_isolation,
|
||||
args.allow,
|
||||
args.block,
|
||||
args.expose,
|
||||
)
|
||||
.context("failed to initialize proxy")?;
|
||||
@@ -207,6 +234,14 @@ fn sudo_escalation_works() -> bool {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn bridge_isolation_enabled(allow: &[Target], block: &[Target]) -> bool {
|
||||
let disables_isolation_for_compat = allow.iter().any(Target::is_ipv4_default_route);
|
||||
let disables_isolation_for_mac_filtering =
|
||||
allow.iter().chain(block.iter()).any(Target::is_mac_address);
|
||||
|
||||
!(disables_isolation_for_compat || disables_isolation_for_mac_filtering)
|
||||
}
|
||||
|
||||
fn set_bootpd_lease_time(lease_time: u32) {
|
||||
let prefs = SCPreferences::group(
|
||||
&CFString::new("softnet"),
|
||||
@@ -228,3 +263,44 @@ fn set_bootpd_lease_time(lease_time: u32) {
|
||||
SCPreferencesCommitChanges(prefs.as_concrete_TypeRef());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn targets(values: &[&str]) -> Vec<Target> {
|
||||
values.iter().map(|value| value.parse().unwrap()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridge_isolation_disabled_for_legacy_allow_default_route() {
|
||||
assert!(!bridge_isolation_enabled(
|
||||
&targets(&["0.0.0.0/0"]),
|
||||
&targets(&[])
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridge_isolation_disabled_for_legacy_allow_default_route_even_when_blocked() {
|
||||
assert!(!bridge_isolation_enabled(
|
||||
&targets(&["0.0.0.0/0"]),
|
||||
&targets(&["0.0.0.0/0"])
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridge_isolation_kept_for_block_default_route_only() {
|
||||
assert!(bridge_isolation_enabled(
|
||||
&targets(&[]),
|
||||
&targets(&["0.0.0.0/0"])
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bridge_isolation_disabled_for_mac_filtering() {
|
||||
assert!(!bridge_isolation_enabled(
|
||||
&targets(&[]),
|
||||
&targets(&["02:00:00:00:00:02"])
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user