mirror of
https://github.com/cirruslabs/softnet.git
synced 2026-09-30 20:11:16 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfc04a49e4 | ||
|
|
7dc5992b55 | ||
|
|
58d4b32258 | ||
|
|
431ae9bbc9 | ||
|
|
98988e5c73 | ||
|
|
b122b49b3c | ||
|
|
2650e78eb0 | ||
|
|
0c59910018 | ||
|
|
7e622716f7 | ||
|
|
80baf28ead | ||
|
|
7c75e2294e | ||
|
|
78e373d992 | ||
|
|
e42ef5150a | ||
|
|
580d84f0ca | ||
|
|
a7da95bd5b | ||
|
|
1c52f32688 | ||
|
|
a8d5ff65d6 | ||
|
|
b1459e9188 | ||
|
|
80745a6720 | ||
|
|
0117fcbdb2 | ||
|
|
31a1296e64 | ||
|
|
d4f66bfb5c | ||
|
|
b54679d6e4 | ||
|
|
560e909b28 | ||
|
|
e6c9f98162 | ||
|
|
ddc699e076 | ||
|
|
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 |
@@ -0,0 +1,2 @@
|
||||
[target.aarch64-apple-darwin]
|
||||
runner = 'sudo -E'
|
||||
+41
-18
@@ -1,37 +1,60 @@
|
||||
use_compute_credits: true
|
||||
|
||||
macos_instance:
|
||||
image: ghcr.io/cirruslabs/macos-runner:tahoe
|
||||
|
||||
env:
|
||||
PATH: "$PATH:$HOME/.cargo/bin"
|
||||
|
||||
task:
|
||||
name: Build
|
||||
macos_instance:
|
||||
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 --target aarch64-apple-darwin --target x86_64-apple-darwin
|
||||
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 == ''
|
||||
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
|
||||
build_script: goreleaser build --snapshot
|
||||
goreleaser_artifacts:
|
||||
path: "dist/**"
|
||||
|
||||
task:
|
||||
name: Release
|
||||
only_if: $CIRRUS_TAG != ''
|
||||
macos_instance:
|
||||
image: ghcr.io/cirruslabs/macos-sonoma-xcode:latest
|
||||
depends_on:
|
||||
- Lint
|
||||
- Test
|
||||
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
|
||||
- rustup target add x86_64-apple-darwin
|
||||
install_script:
|
||||
- brew install go goreleaser/tap/goreleaser-pro getsentry/tools/sentry-cli
|
||||
build_script:
|
||||
- cargo build --target aarch64-apple-darwin --target x86_64-apple-darwin --profile release-with-debug
|
||||
install_rust_script: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
install_script: brew install go getsentry/tools/sentry-cli
|
||||
install_goreleaser_script: brew install --cask goreleaser/tap/goreleaser-pro
|
||||
release_script: goreleaser
|
||||
upload_sentry_debug_files_script:
|
||||
- cd target/aarch64-apple-darwin/release-with-debug/
|
||||
- cd target/aarch64-apple-darwin/release/
|
||||
# Generate and upload symbols
|
||||
- dsymutil softnet
|
||||
- sentry-cli debug-files upload -o $SENTRY_ORG -p $SENTRY_PROJECT softnet.dSYM/
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
* @edigaryev @fkorotkov
|
||||
@@ -4,3 +4,7 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
groups:
|
||||
all-updates:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
+16
-13
@@ -1,26 +1,27 @@
|
||||
---
|
||||
version: 2
|
||||
project_name: softnet
|
||||
|
||||
builds:
|
||||
- id: softnet
|
||||
builder: prebuilt
|
||||
goamd64: [v1]
|
||||
goos:
|
||||
- darwin
|
||||
goarch:
|
||||
- arm64
|
||||
- amd64
|
||||
prebuilt:
|
||||
path: 'target/{{- if eq .Arch "arm64" }}aarch64{{- else }}x86_64{{ end }}-apple-darwin/release-with-debug/softnet'
|
||||
- builder: rust
|
||||
command: build
|
||||
targets:
|
||||
- aarch64-apple-darwin
|
||||
- x86_64-apple-darwin
|
||||
|
||||
universal_binaries:
|
||||
- replace: true
|
||||
|
||||
archives:
|
||||
- id: regular
|
||||
name_template: "{{ .ProjectName }}-{{ .Arch }}"
|
||||
- name_template: "{{ .ProjectName }}"
|
||||
formats:
|
||||
- tar.gz
|
||||
|
||||
release:
|
||||
prerelease: auto
|
||||
|
||||
brews:
|
||||
- name: softnet
|
||||
- name: "{{ .ProjectName }}"
|
||||
repository:
|
||||
owner: cirruslabs
|
||||
name: homebrew-cli
|
||||
@@ -28,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
+777
-190
File diff suppressed because it is too large
Load Diff
+8
-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.0"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
mac_address = "1"
|
||||
privdrop = "0"
|
||||
@@ -27,8 +27,12 @@ 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.22"
|
||||
log = "0.4.28"
|
||||
serial_test = "3"
|
||||
|
||||
[profile.release]
|
||||
debug = true
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
# 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.
|
||||
|
||||
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
|
||||
|
||||
+8
-2
@@ -1,5 +1,5 @@
|
||||
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};
|
||||
@@ -45,6 +45,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,6 +63,7 @@ impl DhcpSnooper {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Lease {
|
||||
address: Ipv4Address,
|
||||
valid_until: Instant,
|
||||
@@ -65,7 +71,7 @@ pub struct Lease {
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
+19
-6
@@ -1,15 +1,15 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use clap::ValueEnum;
|
||||
use log::info;
|
||||
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 {
|
||||
@@ -29,6 +29,7 @@ pub struct Host {
|
||||
callback_can_continue_tx: SyncSender<()>,
|
||||
pub gateway_ip: smoltcp::wire::Ipv4Address,
|
||||
pub max_packet_size: u64,
|
||||
pub read_max_packets: u64,
|
||||
finalized: bool,
|
||||
}
|
||||
|
||||
@@ -67,6 +68,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)?;
|
||||
@@ -96,6 +106,7 @@ impl Host {
|
||||
callback_can_continue_tx,
|
||||
gateway_ip,
|
||||
max_packet_size,
|
||||
read_max_packets,
|
||||
finalized: false,
|
||||
})
|
||||
}
|
||||
@@ -108,7 +119,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 +144,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
|
||||
|
||||
+13
-24
@@ -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;
|
||||
@@ -35,38 +35,27 @@ impl Poller<'_> {
|
||||
|
||||
pub fn arm(&self) -> Result<()> {
|
||||
unsafe {
|
||||
self.poller
|
||||
.add(self.vm_fd.as_raw_fd(), self.vm_interest())?;
|
||||
self.poller
|
||||
.add(self.host_fd.as_raw_fd(), self.host_interest())?;
|
||||
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_filter(
|
||||
interrupt_signal,
|
||||
EventKey::Interrupt.into(),
|
||||
PollMode::Oneshot,
|
||||
)
|
||||
.unwrap();
|
||||
.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, self.vm_interest())?;
|
||||
self.poller.modify(self.host_fd, 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, bool)> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::{anyhow, Context, Error};
|
||||
use anyhow::{Context, Error, anyhow};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||
pub struct ExposedPort {
|
||||
pub external_port: u16,
|
||||
pub internal_port: u16,
|
||||
|
||||
+1
-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};
|
||||
|
||||
|
||||
+144
-10
@@ -14,10 +14,11 @@ 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, PrefixSet};
|
||||
use smoltcp::wire::EthernetFrame;
|
||||
use std::io::ErrorKind;
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
use vmnet::Batch;
|
||||
|
||||
pub struct Proxy<'proxy> {
|
||||
vm: VM,
|
||||
@@ -25,38 +26,67 @@ pub struct Proxy<'proxy> {
|
||||
poller: Poller<'proxy>,
|
||||
vm_mac_address: smoltcp::wire::EthernetAddress,
|
||||
dhcp_snooper: DhcpSnooper,
|
||||
allow: PrefixSet<Ipv4Net>,
|
||||
rules: PrefixMap<Ipv4Net, Action>,
|
||||
enobufs_encountered: bool,
|
||||
port_forwarder: PortForwarder,
|
||||
}
|
||||
|
||||
#[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>,
|
||||
block: PrefixSet<Ipv4Net>,
|
||||
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())?;
|
||||
|
||||
// Craft packet filter rules
|
||||
//
|
||||
// SECURITY: blocking rules must always take precedence
|
||||
// over allowing rules when prefixes are identical.
|
||||
let mut rules = PrefixMap::new();
|
||||
|
||||
for allow_net in allow {
|
||||
rules.insert(allow_net, Action::Allow);
|
||||
}
|
||||
|
||||
for block_net in block {
|
||||
rules.insert(block_net, Action::Block);
|
||||
}
|
||||
|
||||
Ok(Proxy {
|
||||
vm,
|
||||
host,
|
||||
poller,
|
||||
vm_mac_address: smoltcp::wire::EthernetAddress(vm_mac_address.bytes()),
|
||||
dhcp_snooper: Default::default(),
|
||||
allow,
|
||||
rules,
|
||||
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 {
|
||||
@@ -67,7 +97,7 @@ impl Proxy<'_> {
|
||||
}
|
||||
|
||||
if host_readable {
|
||||
self.read_from_host(buf.as_mut_slice())?;
|
||||
self.read_from_host(&mut batch, &mut bufs)?;
|
||||
}
|
||||
|
||||
// Graceful termination
|
||||
@@ -81,7 +111,7 @@ impl Proxy<'_> {
|
||||
.tick(&mut self.host, self.dhcp_snooper.lease());
|
||||
}
|
||||
|
||||
self.poller.rearm()?;
|
||||
self.poller.rearm();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,12 +134,14 @@ 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) => {
|
||||
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 +155,105 @@ impl Proxy<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::NetType;
|
||||
use crate::dhcp_snooper::Lease;
|
||||
use crate::proxy::{Action, Proxy};
|
||||
use ipnet::Ipv4Net;
|
||||
use mac_address::MacAddress;
|
||||
use nix::sys::socket::{AddressFamily, SockFlag, SockType, socketpair};
|
||||
use prefix_trie::{PrefixMap, PrefixSet};
|
||||
use serial_test::serial;
|
||||
use smoltcp::wire::{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());
|
||||
}
|
||||
|
||||
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 mut proxy = Proxy::new(
|
||||
vm_fd.as_raw_fd(),
|
||||
MacAddress::from_str("02:00:00:00:00:01").unwrap(),
|
||||
NetType::Nat,
|
||||
PrefixSet::from_iter(
|
||||
allow
|
||||
.into_iter()
|
||||
.map(|cidr| Ipv4Net::from_str(cidr).unwrap()),
|
||||
),
|
||||
PrefixSet::from_iter(
|
||||
block
|
||||
.into_iter()
|
||||
.map(|cidr| Ipv4Net::from_str(cidr).unwrap()),
|
||||
),
|
||||
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<()> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
+37
-26
@@ -1,5 +1,5 @@
|
||||
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;
|
||||
@@ -58,43 +58,54 @@ 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]>) -> 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 {
|
||||
// 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(());
|
||||
|
||||
+26
-5
@@ -1,14 +1,14 @@
|
||||
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 std::borrow::Cow;
|
||||
use std::env;
|
||||
use std::os::raw::c_int;
|
||||
@@ -52,13 +52,31 @@ struct Args {
|
||||
|
||||
#[clap(
|
||||
long,
|
||||
help = "comma-separated list of CIDRs to allow the traffic to (e.g. --allow=192.168.0.0/24)",
|
||||
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). \
|
||||
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",
|
||||
use_value_delimiter = true,
|
||||
action = clap::ArgAction::Set
|
||||
)]
|
||||
allow: Vec<Ipv4Net>,
|
||||
|
||||
#[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). When used with --allow, \
|
||||
the longest prefix match always wins. In case the same prefix is both \
|
||||
--allow'ed and --block'ed, blocking takes precedence.",
|
||||
value_name = "comma-separated CIDRs",
|
||||
use_value_delimiter = true,
|
||||
action = clap::ArgAction::Set
|
||||
)]
|
||||
block: Vec<Ipv4Net>,
|
||||
|
||||
#[clap(
|
||||
long,
|
||||
help = "comma-separated list of TCP ports to expose (e.g. --expose 2222:22,8080:80)",
|
||||
@@ -78,7 +96,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
|
||||
@@ -177,6 +197,7 @@ fn try_main() -> anyhow::Result<()> {
|
||||
args.vm_mac_address,
|
||||
args.vm_net_type,
|
||||
PrefixSet::from_iter(args.allow),
|
||||
PrefixSet::from_iter(args.block),
|
||||
args.expose,
|
||||
)
|
||||
.context("failed to initialize proxy")?;
|
||||
|
||||
Reference in New Issue
Block a user