mirror of
https://github.com/cirruslabs/softnet.git
synced 2026-10-18 13:21:08 +02:00
Compare commits
102
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c377206064 | ||
|
|
7623d698ce | ||
|
|
1ccb0ee4b4 | ||
|
|
50b5e2af2d | ||
|
|
c1ddb2afc6 | ||
|
|
401dea6612 | ||
|
|
1dcb0755df | ||
|
|
1706062004 | ||
|
|
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 | ||
|
|
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 |
@@ -0,0 +1,2 @@
|
|||||||
|
[target.aarch64-apple-darwin]
|
||||||
|
runner = 'sudo -E'
|
||||||
+41
-16
@@ -1,35 +1,60 @@
|
|||||||
|
use_compute_credits: true
|
||||||
|
|
||||||
|
macos_instance:
|
||||||
|
image: ghcr.io/cirruslabs/macos-runner:tahoe
|
||||||
|
|
||||||
env:
|
env:
|
||||||
PATH: "$PATH:$HOME/.cargo/bin"
|
PATH: "$PATH:$HOME/.cargo/bin"
|
||||||
|
|
||||||
task:
|
task:
|
||||||
name: Build
|
name: Lint
|
||||||
macos_instance:
|
install_rust_script: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||||
image: ghcr.io/cirruslabs/macos-monterey-xcode:14
|
rustfmt_script: cargo fmt --check
|
||||||
install_rust_script:
|
clippy_script: cargo clippy --all-targets --all-features -- -D warnings
|
||||||
- curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
|
||||||
build_script:
|
task:
|
||||||
- cargo build
|
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:
|
task:
|
||||||
name: Release
|
name: Release
|
||||||
only_if: $CIRRUS_TAG != ''
|
only_if: $CIRRUS_TAG != ''
|
||||||
macos_instance:
|
depends_on:
|
||||||
image: ghcr.io/cirruslabs/macos-monterey-xcode:14
|
- Lint
|
||||||
|
- Test
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ENCRYPTED[!98ace8259c6024da912c14d5a3c5c6aac186890a8d4819fad78f3e0c41a4e0cd3a2537dd6e91493952fb056fa434be7c!]
|
GITHUB_TOKEN: ENCRYPTED[!98ace8259c6024da912c14d5a3c5c6aac186890a8d4819fad78f3e0c41a4e0cd3a2537dd6e91493952fb056fa434be7c!]
|
||||||
GORELEASER_KEY: ENCRYPTED[!9b80b6ef684ceaf40edd4c7af93014ee156c8aba7e6e5795f41c482729887b5c31f36b651491d790f1f668670888d9fd!]
|
GORELEASER_KEY: ENCRYPTED[!9b80b6ef684ceaf40edd4c7af93014ee156c8aba7e6e5795f41c482729887b5c31f36b651491d790f1f668670888d9fd!]
|
||||||
SENTRY_ORG: cirrus-labs
|
SENTRY_ORG: cirrus-labs
|
||||||
SENTRY_PROJECT: persistent-workers
|
SENTRY_PROJECT: persistent-workers
|
||||||
SENTRY_AUTH_TOKEN: ENCRYPTED[!c16a5cf7da5f856b4bc2f21fe8cb7aa2a6c981f851c094ed4d3025fd02ea59a58a86cee8b193a69a1fc20fa217e56ac3!]
|
SENTRY_AUTH_TOKEN: ENCRYPTED[!c16a5cf7da5f856b4bc2f21fe8cb7aa2a6c981f851c094ed4d3025fd02ea59a58a86cee8b193a69a1fc20fa217e56ac3!]
|
||||||
install_rust_script:
|
install_rust_script: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||||
- curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
install_script: brew install go getsentry/tools/sentry-cli
|
||||||
install_script:
|
install_goreleaser_script: brew install --cask goreleaser/tap/goreleaser-pro
|
||||||
- brew install go goreleaser/tap/goreleaser-pro getsentry/tools/sentry-cli
|
|
||||||
build_script:
|
|
||||||
- cargo build --profile release-with-debug
|
|
||||||
release_script: goreleaser
|
release_script: goreleaser
|
||||||
upload_sentry_debug_files_script:
|
upload_sentry_debug_files_script:
|
||||||
- cd target/release-with-debug/
|
- cd target/aarch64-apple-darwin/release/
|
||||||
# Generate and upload symbols
|
# Generate and upload symbols
|
||||||
- dsymutil softnet
|
- dsymutil softnet
|
||||||
- sentry-cli debug-files upload -o $SENTRY_ORG -p $SENTRY_PROJECT softnet.dSYM/
|
- sentry-cli debug-files upload -o $SENTRY_ORG -p $SENTRY_PROJECT softnet.dSYM/
|
||||||
|
|||||||
@@ -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
|
/target
|
||||||
|
|||||||
+17
-16
@@ -1,32 +1,33 @@
|
|||||||
|
---
|
||||||
|
version: 2
|
||||||
project_name: softnet
|
project_name: softnet
|
||||||
|
|
||||||
builds:
|
builds:
|
||||||
- builder: prebuilt
|
- builder: rust
|
||||||
goos:
|
command: build
|
||||||
- darwin
|
targets:
|
||||||
goarch:
|
- aarch64-apple-darwin
|
||||||
- arm64
|
- x86_64-apple-darwin
|
||||||
prebuilt:
|
|
||||||
path: "target/release-with-debug/softnet"
|
universal_binaries:
|
||||||
|
- replace: true
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
- id: binary
|
- name_template: "{{ .ProjectName }}"
|
||||||
format: binary
|
formats:
|
||||||
name_template: "{{ .ProjectName }}"
|
- tar.gz
|
||||||
- id: regular
|
|
||||||
name_template: "{{ .ProjectName }}"
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
prerelease: auto
|
prerelease: auto
|
||||||
|
|
||||||
brews:
|
brews:
|
||||||
- name: softnet
|
- name: "{{ .ProjectName }}"
|
||||||
ids:
|
repository:
|
||||||
- regular
|
|
||||||
tap:
|
|
||||||
owner: cirruslabs
|
owner: cirruslabs
|
||||||
name: homebrew-cli
|
name: homebrew-cli
|
||||||
caveats: See the Github repository for more information
|
caveats: See the Github repository for more information
|
||||||
homepage: https://github.com/cirruslabs/softnet
|
homepage: https://github.com/cirruslabs/softnet
|
||||||
description: Software networking with isolation for Tart
|
description: Software networking with isolation for Tart
|
||||||
skip_upload: auto
|
skip_upload: auto
|
||||||
|
custom_block: |
|
||||||
|
depends_on :macos => :sequoia
|
||||||
|
|||||||
Generated
+1857
-583
File diff suppressed because it is too large
Load Diff
+25
-17
@@ -2,7 +2,7 @@
|
|||||||
name = "softnet"
|
name = "softnet"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
publish = false
|
publish = false
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
path = "lib/mod.rs"
|
path = "lib/mod.rs"
|
||||||
@@ -12,19 +12,27 @@ inherits = "release"
|
|||||||
debug = true
|
debug = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
smoltcp = "0.8.1"
|
smoltcp = "0"
|
||||||
libc = "0.2.126"
|
libc = "0"
|
||||||
polling = { git = "https://github.com/smol-rs/polling.git" }
|
polling = "3"
|
||||||
dhcproto = "0.7.0"
|
dhcproto = { git = "https://github.com/bluecatengineering/dhcproto.git", branch = "master" }
|
||||||
vmnet = "0.1.1"
|
vmnet = "0.5.0"
|
||||||
clap = { version = "3.1.18", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
mac_address = "1.1.3"
|
mac_address = "1"
|
||||||
privdrop = "0.5.2"
|
privdrop = "0"
|
||||||
anyhow = { version = "1.0.66", features = ["backtrace"] }
|
anyhow = { version = "1", features = ["backtrace"] }
|
||||||
ip_network = "0.4.1"
|
ip_network = "0"
|
||||||
uzers = "0.11.3"
|
uzers = "0"
|
||||||
system-configuration = "0.5.0"
|
system-configuration = "0"
|
||||||
num_enum = "0.5.7"
|
num_enum = "0"
|
||||||
sentry = { version = "0.29.1", features = ["debug-images"] }
|
sentry = { version = "0", features = ["debug-images"] }
|
||||||
sentry-anyhow = { version = "0.29.1", features = ["backtrace"] }
|
sentry-anyhow = { version = "0", features = ["backtrace"] }
|
||||||
nix = "0.26.2"
|
nix = { version = "0", features = ["signal", "socket"] }
|
||||||
|
prefix-trie = "0"
|
||||||
|
ipnet = "2"
|
||||||
|
oslog = "0.2.0"
|
||||||
|
log = "0.4.29"
|
||||||
|
serial_test = "3"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
debug = true
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
# Softnet
|
# 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.
|
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.
|
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
|
## Working model
|
||||||
@@ -8,9 +19,9 @@ Please check out [this blog post](https://cirrus-ci.org/blog/2022/07/07/isolatin
|
|||||||
Softnet solves two problems:
|
Softnet solves two problems:
|
||||||
|
|
||||||
1. VM network isolation
|
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
|
* [`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
|
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:
|
And assumes that:
|
||||||
|
|
||||||
|
|||||||
+21
-7
@@ -1,5 +1,5 @@
|
|||||||
use dhcproto::v4::{DhcpOption, MessageType, OptionCode};
|
|
||||||
use dhcproto::Decodable;
|
use dhcproto::Decodable;
|
||||||
|
use dhcproto::v4::{DhcpOption, MessageType, OptionCode};
|
||||||
use smoltcp::wire::Ipv4Address;
|
use smoltcp::wire::Ipv4Address;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
@@ -26,14 +26,14 @@ impl DhcpSnooper {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let dns_ips = match message.opts().get(OptionCode::DomainNameServer) {
|
let dns_ips = match message.opts().get(OptionCode::DomainNameServer) {
|
||||||
Some(DhcpOption::DomainNameServer(dns_ips)) => HashSet::from_iter(
|
Some(DhcpOption::DomainNameServer(dns_ips)) => {
|
||||||
dns_ips.iter().map(|dns_ip| Ipv4Address(dns_ip.octets())),
|
HashSet::from_iter(dns_ips.iter().cloned())
|
||||||
),
|
}
|
||||||
_ => HashSet::new(),
|
_ => HashSet::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.vm_lease = Some(Lease::new(
|
self.vm_lease = Some(Lease::new(
|
||||||
message.yiaddr().into(),
|
message.yiaddr(),
|
||||||
Duration::from_secs(*lease_time as u64),
|
Duration::from_secs(*lease_time as u64),
|
||||||
dns_ips,
|
dns_ips,
|
||||||
))
|
))
|
||||||
@@ -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> {
|
pub fn lease(&self) -> &Option<Lease> {
|
||||||
&self.vm_lease
|
&self.vm_lease
|
||||||
}
|
}
|
||||||
@@ -58,6 +63,7 @@ impl DhcpSnooper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Lease {
|
pub struct Lease {
|
||||||
address: Ipv4Address,
|
address: Ipv4Address,
|
||||||
valid_until: Instant,
|
valid_until: Instant,
|
||||||
@@ -65,7 +71,7 @@ pub struct Lease {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
Lease {
|
||||||
address,
|
address,
|
||||||
valid_until: Instant::now() + lease_time,
|
valid_until: Instant::now() + lease_time,
|
||||||
@@ -73,7 +79,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 {
|
pub fn valid_ip_source(&self, address: Ipv4Address) -> bool {
|
||||||
self.address == address && Instant::now() < self.valid_until
|
self.address == address && self.valid()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+85
-15
@@ -1,12 +1,27 @@
|
|||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{Context, Result, anyhow};
|
||||||
|
use clap::ValueEnum;
|
||||||
|
use log::info;
|
||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
use std::os::unix::io::{AsRawFd, RawFd};
|
use std::os::unix::io::{AsRawFd, RawFd};
|
||||||
use std::os::unix::net::UnixDatagram;
|
use std::os::unix::net::UnixDatagram;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::mpsc::{sync_channel, SyncSender};
|
use std::sync::mpsc::{SyncSender, sync_channel};
|
||||||
use vmnet::mode::Mode;
|
use vmnet::mode::Mode;
|
||||||
use vmnet::parameters::{Parameter, ParameterKind};
|
use vmnet::parameters::{Parameter, ParameterKind};
|
||||||
use vmnet::{Events, Options};
|
use vmnet::port_forwarding::{AddressFamily, Protocol};
|
||||||
|
use vmnet::{Batch, 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 {
|
pub struct Host {
|
||||||
interface: vmnet::Interface,
|
interface: vmnet::Interface,
|
||||||
@@ -14,31 +29,52 @@ pub struct Host {
|
|||||||
callback_can_continue_tx: SyncSender<()>,
|
callback_can_continue_tx: SyncSender<()>,
|
||||||
pub gateway_ip: smoltcp::wire::Ipv4Address,
|
pub gateway_ip: smoltcp::wire::Ipv4Address,
|
||||||
pub max_packet_size: u64,
|
pub max_packet_size: u64,
|
||||||
|
pub read_max_packets: u64,
|
||||||
finalized: bool,
|
finalized: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Host {
|
impl Host {
|
||||||
pub fn new() -> Result<Host> {
|
pub fn new(vm_net_type: NetType, enable_isolation: bool) -> Result<Host> {
|
||||||
// Initialize a vmnet.framework NAT interface with isolation enabled
|
// Initialize a vmnet.framework NAT or Host interface with isolation enabled
|
||||||
let mut interface = vmnet::Interface::new(
|
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 {
|
Options {
|
||||||
enable_isolation: Some(true),
|
enable_isolation: Some(enable_isolation),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.context("failed to initialize vmnet interface")?;
|
.context("failed to initialize vmnet interface")?;
|
||||||
|
|
||||||
// Retrieve first IP (gateway) used for this interface
|
// Retrieve first IP (gateway) used for this interface
|
||||||
let Some(Parameter::StartAddress(gateway_ip)) = interface.parameters().get(ParameterKind::StartAddress) else {
|
let Some(Parameter::StartAddress(gateway_ip)) =
|
||||||
return Err(anyhow!("failed to retrieve vmnet's interface 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 gateway_ip = Ipv4Addr::from_str(&gateway_ip)
|
||||||
.context("failed to parse vmnet's interface start address")?;
|
.context("failed to parse vmnet's interface start address")?;
|
||||||
|
|
||||||
// Retrieve max packet size for this interface
|
// Retrieve max packet size for this interface
|
||||||
let Some(Parameter::MaxPacketSize(max_packet_size)) = interface.parameters().get(ParameterKind::MaxPacketSize) else {
|
let Some(Parameter::MaxPacketSize(max_packet_size)) =
|
||||||
return Err(anyhow!("failed to retrieve vmnet's interface max packet size"));
|
interface.parameters().get(ParameterKind::MaxPacketSize)
|
||||||
|
else {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"failed to retrieve vmnet's interface max packet size"
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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
|
// Set up a socketpair() to emulate polling of the vmnet interface
|
||||||
@@ -50,7 +86,9 @@ impl Host {
|
|||||||
interface
|
interface
|
||||||
.set_event_callback(Events::PACKETS_AVAILABLE, move |_mask, _params| {
|
.set_event_callback(Events::PACKETS_AVAILABLE, move |_mask, _params| {
|
||||||
// Send a dummy datagram to make the other end of socketpair() readable
|
// 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
|
// Wait for the permission to continue to avoid
|
||||||
// wasting CPU cycles or in case of termination,
|
// wasting CPU cycles or in case of termination,
|
||||||
@@ -66,22 +104,54 @@ impl Host {
|
|||||||
interface,
|
interface,
|
||||||
new_packets_rx,
|
new_packets_rx,
|
||||||
callback_can_continue_tx,
|
callback_can_continue_tx,
|
||||||
gateway_ip: gateway_ip.into(),
|
gateway_ip,
|
||||||
max_packet_size,
|
max_packet_size,
|
||||||
|
read_max_packets,
|
||||||
finalized: false,
|
finalized: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Host {
|
impl Host {
|
||||||
pub fn read(&mut self, buf: &mut [u8]) -> vmnet::Result<usize> {
|
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, batch: &mut Batch, bufs: &mut [Vec<u8>]) -> vmnet::Result<usize> {
|
||||||
// Dequeue dummy datagram from the socket (if any)
|
// Dequeue dummy datagram from the socket (if any)
|
||||||
// to free up buffer space and reduce false-positives
|
// to free up buffer space and reduce false-positives
|
||||||
// when polling
|
// when polling
|
||||||
let mut buf_to_be_discarded: [u8; 1] = [0; 1];
|
let mut buf_to_be_discarded: [u8; 1] = [0; 1];
|
||||||
let _ = self.new_packets_rx.recv(&mut buf_to_be_discarded);
|
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 {
|
if let Err(vmnet::Error::VmnetReadNothing) = result {
|
||||||
// We've emptied everything, unlock the callback
|
// We've emptied everything, unlock the callback
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
mod dhcp_snooper;
|
mod dhcp_snooper;
|
||||||
mod host;
|
mod host;
|
||||||
|
pub use host::NetType;
|
||||||
mod poller;
|
mod poller;
|
||||||
pub mod proxy;
|
pub mod proxy;
|
||||||
mod vm;
|
mod vm;
|
||||||
|
|||||||
+25
-34
@@ -1,15 +1,16 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use num_enum::IntoPrimitive;
|
use num_enum::IntoPrimitive;
|
||||||
use polling::os::kqueue::PollerKqueueExt;
|
|
||||||
use polling::PollMode;
|
use polling::PollMode;
|
||||||
|
use polling::os::kqueue::PollerKqueueExt;
|
||||||
|
use std::os::fd::{AsRawFd, BorrowedFd};
|
||||||
use std::os::unix::io::RawFd;
|
use std::os::unix::io::RawFd;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
pub struct Poller {
|
pub struct Poller<'poller> {
|
||||||
poller: polling::Poller,
|
poller: polling::Poller,
|
||||||
events: Vec<polling::Event>,
|
events: polling::Events,
|
||||||
vm_fd: RawFd,
|
vm_fd: BorrowedFd<'poller>,
|
||||||
host_fd: RawFd,
|
host_fd: BorrowedFd<'poller>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(IntoPrimitive)]
|
#[derive(IntoPrimitive)]
|
||||||
@@ -20,51 +21,41 @@ enum EventKey {
|
|||||||
Interrupt,
|
Interrupt,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Poller {
|
impl Poller<'_> {
|
||||||
pub fn new(vm_fd: RawFd, host_fd: RawFd) -> Result<Poller> {
|
pub fn new<'poller>(vm_fd: RawFd, host_fd: RawFd) -> Result<Poller<'poller>> {
|
||||||
let poller = polling::Poller::new()?;
|
let poller = polling::Poller::new()?;
|
||||||
|
|
||||||
Ok(Poller {
|
Ok(Poller {
|
||||||
poller,
|
poller,
|
||||||
events: Vec::new(),
|
events: polling::Events::new(),
|
||||||
vm_fd,
|
vm_fd: unsafe { BorrowedFd::borrow_raw(vm_fd) },
|
||||||
host_fd,
|
host_fd: unsafe { BorrowedFd::borrow_raw(host_fd) },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn arm(&self) -> Result<()> {
|
pub fn arm(&self) -> Result<()> {
|
||||||
self.poller.add(self.vm_fd as RawFd, self.vm_interest())?;
|
unsafe {
|
||||||
self.poller
|
self.poller.add_with_mode(
|
||||||
.add(self.host_fd as RawFd, self.host_interest())?;
|
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);
|
let interrupt_signal = polling::os::kqueue::Signal(libc::SIGINT);
|
||||||
self.poller
|
self.poller
|
||||||
.add_filter(
|
.add_filter(interrupt_signal, EventKey::Interrupt.into(), PollMode::Edge)?;
|
||||||
interrupt_signal,
|
|
||||||
EventKey::Interrupt.into(),
|
|
||||||
PollMode::Oneshot,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rearm(&mut self) -> Result<()> {
|
pub fn rearm(&mut self) {
|
||||||
self.events.clear();
|
self.events.clear();
|
||||||
|
|
||||||
self.poller
|
|
||||||
.modify(self.vm_fd as RawFd, self.vm_interest())?;
|
|
||||||
self.poller
|
|
||||||
.modify(self.host_fd as RawFd, self.host_interest())?;
|
|
||||||
|
|
||||||
let interrupt_signal = polling::os::kqueue::Signal(libc::SIGINT);
|
|
||||||
self.poller.modify_filter(
|
|
||||||
interrupt_signal,
|
|
||||||
EventKey::Interrupt.into(),
|
|
||||||
PollMode::Oneshot,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn wait(&mut self) -> Result<(bool, bool, bool)> {
|
pub fn wait(&mut self) -> Result<(bool, bool, bool)> {
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
use anyhow::{Context, Error, anyhow};
|
||||||
|
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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-3
@@ -1,9 +1,9 @@
|
|||||||
use crate::proxy::udp_packet_helper::UdpPacketHelper;
|
|
||||||
use crate::proxy::Proxy;
|
use crate::proxy::Proxy;
|
||||||
|
use crate::proxy::udp_packet_helper::UdpPacketHelper;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use smoltcp::wire::{EthernetFrame, EthernetProtocol, Ipv4Packet, UdpPacket};
|
use smoltcp::wire::{EthernetFrame, EthernetProtocol, Ipv4Packet, UdpPacket};
|
||||||
|
|
||||||
impl Proxy {
|
impl Proxy<'_> {
|
||||||
pub(crate) fn process_frame_from_host(&mut self, frame: &EthernetFrame<&[u8]>) -> Result<()> {
|
pub(crate) fn process_frame_from_host(&mut self, frame: &EthernetFrame<&[u8]>) -> Result<()> {
|
||||||
if self.allowed_from_host(frame).is_none() {
|
if self.allowed_from_host(frame).is_none() {
|
||||||
// Block packet by not forwarding it to the VM
|
// Block packet by not forwarding it to the VM
|
||||||
@@ -58,7 +58,7 @@ impl Proxy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ipv4_pkt.protocol() != smoltcp::wire::IpProtocol::Udp {
|
if ipv4_pkt.next_header() != smoltcp::wire::IpProtocol::Udp {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+169
-12
@@ -1,45 +1,92 @@
|
|||||||
|
mod exposed_port;
|
||||||
mod host;
|
mod host;
|
||||||
|
mod port_forwarder;
|
||||||
mod udp_packet_helper;
|
mod udp_packet_helper;
|
||||||
mod vm;
|
mod vm;
|
||||||
|
|
||||||
use crate::dhcp_snooper::DhcpSnooper;
|
use crate::dhcp_snooper::DhcpSnooper;
|
||||||
use crate::host::Host;
|
use crate::host::Host;
|
||||||
|
use crate::host::NetType;
|
||||||
use crate::poller::Poller;
|
use crate::poller::Poller;
|
||||||
use crate::vm::VM;
|
use crate::vm::VM;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
pub use exposed_port::ExposedPort;
|
||||||
|
use ipnet::Ipv4Net;
|
||||||
use mac_address::MacAddress;
|
use mac_address::MacAddress;
|
||||||
|
use port_forwarder::PortForwarder;
|
||||||
|
use prefix_trie::{Prefix, PrefixMap, PrefixSet};
|
||||||
use smoltcp::wire::EthernetFrame;
|
use smoltcp::wire::EthernetFrame;
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
use std::os::unix::io::{AsRawFd, RawFd};
|
use std::os::unix::io::{AsRawFd, RawFd};
|
||||||
|
use vmnet::Batch;
|
||||||
|
|
||||||
pub struct Proxy {
|
pub struct Proxy<'proxy> {
|
||||||
vm: VM,
|
vm: VM,
|
||||||
host: Host,
|
host: Host,
|
||||||
poller: Poller,
|
poller: Poller<'proxy>,
|
||||||
vm_mac_address: smoltcp::wire::EthernetAddress,
|
vm_mac_address: smoltcp::wire::EthernetAddress,
|
||||||
dhcp_snooper: DhcpSnooper,
|
dhcp_snooper: DhcpSnooper,
|
||||||
|
rules: PrefixMap<Ipv4Net, Action>,
|
||||||
enobufs_encountered: bool,
|
enobufs_encountered: bool,
|
||||||
|
port_forwarder: PortForwarder,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Proxy {
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub fn new(vm_fd: RawFd, vm_mac_address: MacAddress) -> Result<Proxy> {
|
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 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())?;
|
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 {
|
Ok(Proxy {
|
||||||
vm,
|
vm,
|
||||||
host,
|
host,
|
||||||
poller,
|
poller,
|
||||||
vm_mac_address: smoltcp::wire::EthernetAddress(vm_mac_address.bytes()),
|
vm_mac_address: smoltcp::wire::EthernetAddress(vm_mac_address.bytes()),
|
||||||
dhcp_snooper: Default::default(),
|
dhcp_snooper: Default::default(),
|
||||||
|
rules,
|
||||||
enobufs_encountered: false,
|
enobufs_encountered: false,
|
||||||
|
port_forwarder: PortForwarder::new(exposed_ports),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(&mut self) -> Result<()> {
|
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];
|
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()?;
|
self.poller.arm()?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
@@ -50,7 +97,7 @@ impl Proxy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if host_readable {
|
if host_readable {
|
||||||
self.read_from_host(buf.as_mut_slice())?;
|
self.read_from_host(&mut batch, &mut bufs)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Graceful termination
|
// Graceful termination
|
||||||
@@ -58,7 +105,13 @@ impl Proxy {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
self.poller.rearm()?;
|
// Timeout
|
||||||
|
if !vm_readable && !host_readable && !interrupt {
|
||||||
|
self.port_forwarder
|
||||||
|
.tick(&mut self.host, self.dhcp_snooper.lease());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.poller.rearm();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,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 {
|
loop {
|
||||||
match self.host.read(buf) {
|
match self.host.read(batch, bufs) {
|
||||||
Ok(n) => {
|
Ok(pktcnt) => {
|
||||||
if let Ok(pkt) = EthernetFrame::new_checked(&buf[..n]) {
|
for buf in batch.packet_sized_bufs(bufs).take(pktcnt) {
|
||||||
self.process_frame_from_host(&pkt)?;
|
if let Ok(pkt) = EthernetFrame::new_checked(buf) {
|
||||||
|
self.process_frame_from_host(&pkt)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -100,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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
+45
-22
@@ -1,13 +1,14 @@
|
|||||||
use crate::proxy::udp_packet_helper::UdpPacketHelper;
|
use crate::proxy::udp_packet_helper::UdpPacketHelper;
|
||||||
use crate::proxy::Proxy;
|
use crate::proxy::{Action, Proxy};
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use ipnet::Ipv4Net;
|
||||||
use smoltcp::wire::{
|
use smoltcp::wire::{
|
||||||
ArpPacket, EthernetFrame, EthernetProtocol, IpProtocol, Ipv4Packet, UdpPacket,
|
ArpPacket, EthernetFrame, EthernetProtocol, IpProtocol, Ipv4Packet, UdpPacket,
|
||||||
};
|
};
|
||||||
use std::net::Ipv4Addr;
|
use std::net::Ipv4Addr;
|
||||||
|
|
||||||
impl Proxy {
|
impl Proxy<'_> {
|
||||||
pub(crate) fn process_frame_from_vm(&mut self, frame: EthernetFrame<&[u8]>) -> Result<()> {
|
pub(crate) fn process_frame_from_vm(&mut self, frame: EthernetFrame<&[u8]>) -> Result<()> {
|
||||||
if self.allowed_from_vm(&frame).is_none() {
|
if self.allowed_from_vm(&frame).is_none() {
|
||||||
// Block packet by not forwarding it to the host
|
// Block packet by not forwarding it to the host
|
||||||
@@ -47,7 +48,7 @@ impl Proxy {
|
|||||||
let source_protocol_addr = Ipv4Addr::from(source_protocol_addr);
|
let source_protocol_addr = Ipv4Addr::from(source_protocol_addr);
|
||||||
|
|
||||||
if let Some(lease) = self.dhcp_snooper.lease() {
|
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(());
|
return Some(());
|
||||||
}
|
}
|
||||||
} else if source_protocol_addr.is_unspecified() {
|
} else if source_protocol_addr.is_unspecified() {
|
||||||
@@ -57,32 +58,54 @@ impl Proxy {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn allowed_from_vm_ipv4(&self, ipv4_pkt: Ipv4Packet<&[u8]>) -> Option<()> {
|
pub(crate) fn allowed_from_vm_ipv4(&self, ipv4_pkt: Ipv4Packet<&[u8]>) -> Option<()> {
|
||||||
// Once we've learned the VM's IP from the DHCP snooping,
|
// Is this packet coming from VM's IP address that we've learned from DHCP snooping?
|
||||||
// allow all global traffic for that VM's IP
|
if let Some(lease) = &self.dhcp_snooper.lease()
|
||||||
if let Some(lease) = &self.dhcp_snooper.lease() {
|
&& lease.valid_ip_source(ipv4_pkt.src_addr())
|
||||||
let dst_is_global =
|
{
|
||||||
ip_network::IpNetwork::from(Ipv4Addr::from(ipv4_pkt.dst_addr().0)).is_global();
|
let dst_addr = ipv4_pkt.dst_addr();
|
||||||
|
|
||||||
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(());
|
return 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
|
// Allow outgoing DHCP requests to broadcast addresses,
|
||||||
if ipv4_pkt.dst_addr() == self.host.gateway_ip {
|
// otherwise DHCP snooper will never be populated
|
||||||
return Some(());
|
if ipv4_pkt.next_header() == IpProtocol::Udp {
|
||||||
}
|
|
||||||
|
|
||||||
if ipv4_pkt.protocol() == IpProtocol::Udp {
|
|
||||||
let udp_pkt = UdpPacket::new_checked(ipv4_pkt.payload()).ok()?;
|
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
|
// Allow DHCP communication with the bootpd(8) on host via broadcast address
|
||||||
if udp_pkt.is_dhcp_request() && ipv4_pkt.dst_addr().is_broadcast() {
|
if udp_pkt.is_dhcp_request() && ipv4_pkt.dst_addr().is_broadcast() {
|
||||||
return Some(());
|
return Some(());
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
nightly
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "nightly"
|
||||||
+154
-6
@@ -1,14 +1,22 @@
|
|||||||
use anyhow::{anyhow, Context};
|
use anyhow::{Context, Error, anyhow};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use nix::sys::signal::{signal, SigHandler, Signal};
|
use ipnet::Ipv4Net;
|
||||||
|
use log::LevelFilter;
|
||||||
|
use nix::sys::signal::{SigHandler, Signal, signal};
|
||||||
|
use oslog::OsLogger;
|
||||||
|
use prefix_trie::PrefixSet;
|
||||||
use privdrop::PrivDrop;
|
use privdrop::PrivDrop;
|
||||||
|
use softnet::NetType;
|
||||||
|
use softnet::proxy::ExposedPort;
|
||||||
use softnet::proxy::Proxy;
|
use softnet::proxy::Proxy;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use std::net::{Ipv4Addr, SocketAddr, ToSocketAddrs};
|
||||||
use std::os::raw::c_int;
|
use std::os::raw::c_int;
|
||||||
use std::os::unix::io::RawFd;
|
use std::os::unix::io::RawFd;
|
||||||
use std::os::unix::process::CommandExt;
|
use std::os::unix::process::CommandExt;
|
||||||
use std::process::{Command, ExitCode};
|
use std::process::{Command, ExitCode};
|
||||||
|
use std::str::FromStr;
|
||||||
use system_configuration::core_foundation::base::TCFType;
|
use system_configuration::core_foundation::base::TCFType;
|
||||||
use system_configuration::core_foundation::dictionary::CFDictionary;
|
use system_configuration::core_foundation::dictionary::CFDictionary;
|
||||||
use system_configuration::core_foundation::number::CFNumber;
|
use system_configuration::core_foundation::number::CFNumber;
|
||||||
@@ -17,6 +25,67 @@ use system_configuration::preferences::SCPreferences;
|
|||||||
use system_configuration::sys::preferences::{SCPreferencesCommitChanges, SCPreferencesSetValue};
|
use system_configuration::sys::preferences::{SCPreferencesCommitChanges, SCPreferencesSetValue};
|
||||||
use uzers::{get_current_groupname, get_current_username, get_effective_uid};
|
use uzers::{get_current_groupname, get_current_username, get_effective_uid};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum AllowBlockEntry {
|
||||||
|
Net(Ipv4Net),
|
||||||
|
Domain(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for AllowBlockEntry {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let trimmed = s.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
return Err(anyhow!("empty allow/block entry"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(net) = trimmed.parse::<Ipv4Net>() {
|
||||||
|
return Ok(AllowBlockEntry::Net(net));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(addr) = trimmed.parse::<Ipv4Addr>() {
|
||||||
|
return Ok(AllowBlockEntry::Net(Ipv4Net::from(addr)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(AllowBlockEntry::Domain(trimmed.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_allow_block_entries(
|
||||||
|
kind: &str,
|
||||||
|
entries: Vec<AllowBlockEntry>,
|
||||||
|
) -> anyhow::Result<Vec<Ipv4Net>> {
|
||||||
|
let mut nets = Vec::new();
|
||||||
|
|
||||||
|
for entry in entries {
|
||||||
|
match entry {
|
||||||
|
AllowBlockEntry::Net(net) => nets.push(net),
|
||||||
|
AllowBlockEntry::Domain(domain) => {
|
||||||
|
// A-record resolution happens here via ToSocketAddrs, then we keep only IPv4s.
|
||||||
|
let resolved: Vec<Ipv4Net> = (domain.as_str(), 0)
|
||||||
|
.to_socket_addrs()
|
||||||
|
.with_context(|| format!("failed to resolve {kind} entry {domain}"))?
|
||||||
|
.filter_map(|addr| match addr {
|
||||||
|
SocketAddr::V4(v4) => Some(Ipv4Net::from(*v4.ip())),
|
||||||
|
SocketAddr::V6(_) => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if resolved.is_empty() {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"no IPv4 addresses found for {kind} entry {domain}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
nets.extend(resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(nets)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
struct Args {
|
struct Args {
|
||||||
#[clap(
|
#[clap(
|
||||||
@@ -28,6 +97,9 @@ struct Args {
|
|||||||
#[clap(long, help = "MAC address to enforce for the VM")]
|
#[clap(long, help = "MAC address to enforce for the VM")]
|
||||||
vm_mac_address: mac_address::MacAddress,
|
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(
|
#[clap(
|
||||||
long,
|
long,
|
||||||
help = "set bootpd(8) lease time to this value (in seconds) before starting the VM",
|
help = "set bootpd(8) lease time to this value (in seconds) before starting the VM",
|
||||||
@@ -41,6 +113,44 @@ struct Args {
|
|||||||
#[clap(long, help = "group name to drop privileges to")]
|
#[clap(long, help = "group name to drop privileges to")]
|
||||||
group: Option<String>,
|
group: Option<String>,
|
||||||
|
|
||||||
|
#[clap(
|
||||||
|
long,
|
||||||
|
help = "Comma-separated list of CIDRs, IPs, or domains to allow the traffic to \
|
||||||
|
(e.g. --allow=192.168.0.0/24 or --allow=example.com). Domains are resolved to A records \
|
||||||
|
at startup. \
|
||||||
|
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/IPs/domains",
|
||||||
|
use_value_delimiter = true,
|
||||||
|
action = clap::ArgAction::Set
|
||||||
|
)]
|
||||||
|
allow: Vec<AllowBlockEntry>,
|
||||||
|
|
||||||
|
#[clap(
|
||||||
|
long,
|
||||||
|
help = "Comma-separated list of CIDRs, IPs, or domains 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). Domains are resolved to A records at startup. \
|
||||||
|
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/IPs/domains",
|
||||||
|
use_value_delimiter = true,
|
||||||
|
action = clap::ArgAction::Set
|
||||||
|
)]
|
||||||
|
block: Vec<AllowBlockEntry>,
|
||||||
|
|
||||||
|
#[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)]
|
#[clap(long, hide = true)]
|
||||||
sudo_escalation_probing: bool,
|
sudo_escalation_probing: bool,
|
||||||
|
|
||||||
@@ -51,7 +161,9 @@ struct Args {
|
|||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
// Enable backtraces by default
|
// Enable backtraces by default
|
||||||
if env::var("RUST_BACKTRACE").is_err() {
|
if env::var("RUST_BACKTRACE").is_err() {
|
||||||
env::set_var("RUST_BACKTRACE", "full");
|
unsafe {
|
||||||
|
env::set_var("RUST_BACKTRACE", "full");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize Sentry
|
// Initialize Sentry
|
||||||
@@ -85,6 +197,11 @@ fn main() -> ExitCode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn try_main() -> anyhow::Result<()> {
|
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,
|
// 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]
|
// 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.
|
// EVFILT_SIGNAL will receive it anyways, because it has lower precedence.
|
||||||
@@ -93,7 +210,7 @@ fn try_main() -> anyhow::Result<()> {
|
|||||||
// [2]: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/kqueue.2.html
|
// [2]: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/kqueue.2.html
|
||||||
unsafe { signal(Signal::SIGINT, SigHandler::SigIgn) }?;
|
unsafe { signal(Signal::SIGINT, SigHandler::SigIgn) }?;
|
||||||
|
|
||||||
let args: Args = Args::parse();
|
let mut args: Args = Args::parse();
|
||||||
|
|
||||||
// No need to run anything, just return
|
// No need to run anything, just return
|
||||||
// so that the invoker process knows we
|
// so that the invoker process knows we
|
||||||
@@ -136,12 +253,22 @@ fn try_main() -> anyhow::Result<()> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let allow = resolve_allow_block_entries("allow", std::mem::take(&mut args.allow))?;
|
||||||
|
let block = resolve_allow_block_entries("block", std::mem::take(&mut args.block))?;
|
||||||
|
|
||||||
// Set bootpd(8) min/max lease time while still having the root privileges
|
// Set bootpd(8) min/max lease time while still having the root privileges
|
||||||
set_bootpd_lease_time(args.bootpd_lease_time);
|
set_bootpd_lease_time(args.bootpd_lease_time);
|
||||||
|
|
||||||
// Initialize the proxy while still having the root privileges
|
// Initialize the proxy while still having the root privileges
|
||||||
let mut proxy = Proxy::new(args.vm_fd as RawFd, args.vm_mac_address)
|
let mut proxy = Proxy::new(
|
||||||
.context("failed to initialize proxy")?;
|
args.vm_fd as RawFd,
|
||||||
|
args.vm_mac_address,
|
||||||
|
args.vm_net_type,
|
||||||
|
PrefixSet::from_iter(allow),
|
||||||
|
PrefixSet::from_iter(block),
|
||||||
|
args.expose,
|
||||||
|
)
|
||||||
|
.context("failed to initialize proxy")?;
|
||||||
|
|
||||||
// Drop effective privileges to the user
|
// Drop effective privileges to the user
|
||||||
// and group which have had invoked us
|
// and group which have had invoked us
|
||||||
@@ -190,3 +317,24 @@ fn set_bootpd_lease_time(lease_time: u32) {
|
|||||||
SCPreferencesCommitChanges(prefs.as_concrete_TypeRef());
|
SCPreferencesCommitChanges(prefs.as_concrete_TypeRef());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{AllowBlockEntry, resolve_allow_block_entries};
|
||||||
|
use ipnet::Ipv4Net;
|
||||||
|
use std::net::Ipv4Addr;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_domain_to_ipv4_nets_dns_google() {
|
||||||
|
let nets = resolve_allow_block_entries(
|
||||||
|
"allow",
|
||||||
|
vec![AllowBlockEntry::Domain("dns.google".to_string())],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
nets.contains(&Ipv4Net::from(Ipv4Addr::new(8, 8, 8, 8)))
|
||||||
|
|| nets.contains(&Ipv4Net::from(Ipv4Addr::new(8, 8, 4, 4)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user