Compare commits

..
Author SHA1 Message Date
Fedor Korotkov c377206064 chore: remove unused skill file for PR fixes 2026-01-08 15:27:44 +01:00
Fedor Korotkov 7623d698ce revert: restore args parsing style
- switch back to let mut args: Args = Args::parse()
2026-01-08 15:08:47 +01:00
Fedor KorotkovandClaude Opus 4.5 1ccb0ee4b4 fix: address PR review feedback
- simplify allow/block resolution without mem::take (refs #2661210452)
- use dns.google test domain with stable A records (refs #2661236654)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 12:34:46 +01:00
Fedor Korotkov 50b5e2af2d Add domain resolution for allow/block entries
- Updated `--allow` and `--block` flags to accept domains, alongside CIDRs and IPs.
- Implemented A-record resolution for domain entries during startup.
- Added tests for domain resolution functionality.
2025-12-20 06:44:51 -05:00
8 changed files with 499 additions and 1071 deletions
+68
View File
@@ -0,0 +1,68 @@
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 == ''
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 != ''
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
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/
# Generate and upload symbols
- dsymutil softnet
- sentry-cli debug-files upload -o $SENTRY_ORG -p $SENTRY_PROJECT softnet.dSYM/
# Bundle and upload sources
- sentry-cli debug-files bundle-sources softnet.dSYM/
- sentry-cli debug-files upload -o $SENTRY_ORG -p $SENTRY_PROJECT softnet.src.zip
create_sentry_release_script:
- export SENTRY_RELEASE="softnet@$CIRRUS_TAG"
- sentry-cli releases new $SENTRY_RELEASE
- sentry-cli releases set-commits $SENTRY_RELEASE --auto
- sentry-cli releases finalize $SENTRY_RELEASE
-102
View File
@@ -1,102 +0,0 @@
name: CI
on:
pull_request:
push:
branches:
- main
tags:
- '*'
workflow_dispatch:
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
jobs:
lint:
name: Lint
runs-on: macos-15
timeout-minutes: 30
steps:
- uses: actions/checkout@v5
- name: Install Rust
run: rustup toolchain install nightly --profile minimal --component rustfmt --component clippy
- name: Check formatting
run: cargo fmt --check
- name: Clippy
run: cargo clippy --all-targets --all-features -- -D warnings
test:
name: Test on ${{ matrix.name }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- name: macOS Sequoia
runner: macos-15
- name: macOS Tahoe
runner: macos-26
steps:
- uses: actions/checkout@v5
- name: Install Rust
run: rustup toolchain install nightly --profile minimal
- name: Test
run: cargo test
release_dry_run:
name: Release (Dry Run)
if: github.event_name != 'pull_request' && github.ref_type != 'tag'
needs:
- lint
- test
runs-on: macos-15
timeout-minutes: 45
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Install Rust
run: rustup toolchain install nightly --profile minimal
- name: Install tools
run: |
brew install go
brew install --cask goreleaser/tap/goreleaser-pro
- name: Build snapshot
run: goreleaser build --snapshot
- name: Upload dist
uses: actions/upload-artifact@v4
with:
name: dist-dry-run
path: dist/**
if-no-files-found: ignore
release:
name: Release
if: github.ref_type == 'tag'
needs:
- lint
- test
runs-on: macos-15
timeout-minutes: 45
permissions:
contents: write
env:
GITHUB_TOKEN: ${{ github.token }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Install Rust
run: rustup toolchain install nightly --profile minimal
- name: Install tools
run: |
brew install go
brew install --cask goreleaser/tap/goreleaser-pro
- name: Release
run: goreleaser
Generated
+292 -839
View File
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -16,7 +16,7 @@ smoltcp = "0"
libc = "0" libc = "0"
polling = "3" polling = "3"
dhcproto = { git = "https://github.com/bluecatengineering/dhcproto.git", branch = "master" } dhcproto = { git = "https://github.com/bluecatengineering/dhcproto.git", branch = "master" }
vmnet = "0.5.1" vmnet = "0.5.0"
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
mac_address = "1" mac_address = "1"
privdrop = "0" privdrop = "0"
@@ -33,7 +33,6 @@ ipnet = "2"
oslog = "0.2.0" oslog = "0.2.0"
log = "0.4.29" log = "0.4.29"
serial_test = "3" serial_test = "3"
coarsetime = "0.1.37"
[profile.release] [profile.release]
debug = true debug = true
+9 -18
View File
@@ -2,22 +2,14 @@ use dhcproto::Decodable;
use dhcproto::v4::{DhcpOption, MessageType, OptionCode}; 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; use std::time::{Duration, Instant};
#[derive(Default)] #[derive(Default)]
pub struct DhcpSnooper { pub struct DhcpSnooper {
vm_lease: Option<Lease>, vm_lease: Option<Lease>,
uncertainty_duration: Duration,
} }
impl DhcpSnooper { impl DhcpSnooper {
pub fn new(uncertainty_duration: Duration) -> Self {
DhcpSnooper {
uncertainty_duration,
..Default::default()
}
}
pub fn register_dhcp_reply(&mut self, dhcp_packet: &[u8]) { pub fn register_dhcp_reply(&mut self, dhcp_packet: &[u8]) {
let mut decoder = dhcproto::v4::Decoder::new(dhcp_packet); let mut decoder = dhcproto::v4::Decoder::new(dhcp_packet);
@@ -40,12 +32,11 @@ impl DhcpSnooper {
_ => HashSet::new(), _ => HashSet::new(),
}; };
let mut lease_duration = Duration::from_secs(*lease_time as u64); self.vm_lease = Some(Lease::new(
message.yiaddr(),
// Adjust for uncertainty caused by using a coarse clock Duration::from_secs(*lease_time as u64),
lease_duration = lease_duration.saturating_sub(self.uncertainty_duration); dns_ips,
))
self.vm_lease = Some(Lease::new(message.yiaddr(), lease_duration, dns_ips))
} }
Some(MessageType::Nak) => { Some(MessageType::Nak) => {
self.vm_lease = None; self.vm_lease = None;
@@ -75,7 +66,7 @@ impl DhcpSnooper {
#[derive(Debug)] #[derive(Debug)]
pub struct Lease { pub struct Lease {
address: Ipv4Address, address: Ipv4Address,
valid_until: coarsetime::Instant, valid_until: Instant,
dns_ips: HashSet<Ipv4Address>, dns_ips: HashSet<Ipv4Address>,
} }
@@ -83,7 +74,7 @@ impl Lease {
pub 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: coarsetime::Instant::recent() + lease_time.into(), valid_until: Instant::now() + lease_time,
dns_ips, dns_ips,
} }
} }
@@ -93,7 +84,7 @@ impl Lease {
} }
pub fn valid(&self) -> bool { pub fn valid(&self) -> bool {
coarsetime::Instant::recent() < self.valid_until Instant::now() < self.valid_until
} }
pub fn valid_ip_source(&self, address: Ipv4Address) -> bool { pub fn valid_ip_source(&self, address: Ipv4Address) -> bool {
+3 -8
View File
@@ -9,7 +9,6 @@ use std::time::Duration;
pub struct Poller<'poller> { pub struct Poller<'poller> {
poller: polling::Poller, poller: polling::Poller,
events: polling::Events, events: polling::Events,
timeout: Duration,
vm_fd: BorrowedFd<'poller>, vm_fd: BorrowedFd<'poller>,
host_fd: BorrowedFd<'poller>, host_fd: BorrowedFd<'poller>,
} }
@@ -23,17 +22,12 @@ enum EventKey {
} }
impl Poller<'_> { impl Poller<'_> {
pub fn new<'poller>( pub fn new<'poller>(vm_fd: RawFd, host_fd: RawFd) -> Result<Poller<'poller>> {
vm_fd: RawFd,
host_fd: RawFd,
timeout: Duration,
) -> Result<Poller<'poller>> {
let poller = polling::Poller::new()?; let poller = polling::Poller::new()?;
Ok(Poller { Ok(Poller {
poller, poller,
events: polling::Events::new(), events: polling::Events::new(),
timeout,
vm_fd: unsafe { BorrowedFd::borrow_raw(vm_fd) }, vm_fd: unsafe { BorrowedFd::borrow_raw(vm_fd) },
host_fd: unsafe { BorrowedFd::borrow_raw(host_fd) }, host_fd: unsafe { BorrowedFd::borrow_raw(host_fd) },
}) })
@@ -65,7 +59,8 @@ impl Poller<'_> {
} }
pub fn wait(&mut self) -> Result<(bool, bool, bool)> { pub fn wait(&mut self) -> Result<(bool, bool, bool)> {
self.poller.wait(&mut self.events, Some(self.timeout))?; self.poller
.wait(&mut self.events, Some(Duration::from_millis(100)))?;
let vm_readable = self let vm_readable = self
.events .events
+21 -83
View File
@@ -14,12 +14,10 @@ pub use exposed_port::ExposedPort;
use ipnet::Ipv4Net; use ipnet::Ipv4Net;
use mac_address::MacAddress; use mac_address::MacAddress;
use port_forwarder::PortForwarder; use port_forwarder::PortForwarder;
use prefix_trie::{Prefix, PrefixMap}; 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 std::str::FromStr;
use std::time::Duration;
use vmnet::Batch; use vmnet::Batch;
pub struct Proxy<'proxy> { pub struct Proxy<'proxy> {
@@ -33,24 +31,6 @@ pub struct Proxy<'proxy> {
port_forwarder: PortForwarder, port_forwarder: PortForwarder,
} }
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Target {
Prefix(Ipv4Net),
Host,
}
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);
}
Ipv4Net::from_str(s).map(Target::Prefix)
}
}
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub(crate) enum Action { pub(crate) enum Action {
Block, Block,
@@ -62,17 +42,13 @@ impl Proxy<'_> {
vm_fd: RawFd, vm_fd: RawFd,
vm_mac_address: MacAddress, vm_mac_address: MacAddress,
vm_net_type: NetType, vm_net_type: NetType,
allow: Vec<Target>, allow: PrefixSet<Ipv4Net>,
block: Vec<Target>, block: PrefixSet<Ipv4Net>,
exposed_ports: Vec<ExposedPort>, exposed_ports: Vec<ExposedPort>,
) -> Result<Proxy<'proxy>> { ) -> 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()))?;
vm_net_type, let poller = Poller::new(vm.as_raw_fd(), host.as_raw_fd())?;
!allow.contains(&Target::Prefix(Ipv4Net::zero())),
)?;
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 // Craft packet filter rules
// //
@@ -80,22 +56,12 @@ impl Proxy<'_> {
// over allowing rules when prefixes are identical. // over allowing rules when prefixes are identical.
let mut rules = PrefixMap::new(); let mut rules = PrefixMap::new();
for allow_target in allow { for allow_net in allow {
let allow_prefix = match allow_target { rules.insert(allow_net, Action::Allow);
Target::Prefix(prefix) => prefix,
Target::Host => host.gateway_ip.into(),
};
rules.insert(allow_prefix, Action::Allow);
} }
for block_target in block { for block_net in block {
let block_prefix = match block_target { rules.insert(block_net, Action::Block);
Target::Prefix(prefix) => prefix,
Target::Host => host.gateway_ip.into(),
};
rules.insert(block_prefix, Action::Block);
} }
Ok(Proxy { Ok(Proxy {
@@ -103,7 +69,7 @@ impl Proxy<'_> {
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: DhcpSnooper::new(poller_timeout), dhcp_snooper: Default::default(),
rules, rules,
enobufs_encountered: false, enobufs_encountered: false,
port_forwarder: PortForwarder::new(exposed_ports), port_forwarder: PortForwarder::new(exposed_ports),
@@ -126,9 +92,6 @@ impl Proxy<'_> {
loop { loop {
let (vm_readable, host_readable, interrupt) = self.poller.wait()?; let (vm_readable, host_readable, interrupt) = self.poller.wait()?;
// Update coarse time for the DHCP snooper
coarsetime::Instant::update();
if vm_readable { if vm_readable {
self.read_from_vm(buf.as_mut_slice())?; self.read_from_vm(buf.as_mut_slice())?;
} }
@@ -156,9 +119,6 @@ impl Proxy<'_> {
loop { loop {
match self.vm.read(buf) { match self.vm.read(buf) {
Ok(n) => { Ok(n) => {
// Update coarse time for the DHCP snooper
coarsetime::Instant::update();
if let Ok(frame) = EthernetFrame::new_checked(&buf[..n]) { if let Ok(frame) = EthernetFrame::new_checked(&buf[..n]) {
self.process_frame_from_vm(frame)?; self.process_frame_from_vm(frame)?;
} }
@@ -178,9 +138,6 @@ impl Proxy<'_> {
loop { loop {
match self.host.read(batch, bufs) { match self.host.read(batch, bufs) {
Ok(pktcnt) => { Ok(pktcnt) => {
// Update coarse time for the DHCP snooper
coarsetime::Instant::update();
for buf in batch.packet_sized_bufs(bufs).take(pktcnt) { for buf in batch.packet_sized_bufs(bufs).take(pktcnt) {
if let Ok(pkt) = EthernetFrame::new_checked(buf) { if let Ok(pkt) = EthernetFrame::new_checked(buf) {
self.process_frame_from_host(&pkt)?; self.process_frame_from_host(&pkt)?;
@@ -207,7 +164,7 @@ mod tests {
use ipnet::Ipv4Net; use ipnet::Ipv4Net;
use mac_address::MacAddress; use mac_address::MacAddress;
use nix::sys::socket::{AddressFamily, SockFlag, SockType, socketpair}; use nix::sys::socket::{AddressFamily, SockFlag, SockType, socketpair};
use prefix_trie::PrefixMap; use prefix_trie::{PrefixMap, PrefixSet};
use serial_test::serial; use serial_test::serial;
use smoltcp::wire::{Ipv4Address, Ipv4Packet}; use smoltcp::wire::{Ipv4Address, Ipv4Packet};
use std::collections::HashSet; use std::collections::HashSet;
@@ -251,27 +208,6 @@ mod tests {
assert!(allowed_from_vm_ipv4(&proxy, vm_ip, "33.33.33.34").is_none()); 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());
}
fn create_proxy<'test>(vm_ip: Ipv4Address, allow: Vec<&str>, block: Vec<&str>) -> Proxy<'test> { fn create_proxy<'test>(vm_ip: Ipv4Address, allow: Vec<&str>, block: Vec<&str>) -> Proxy<'test> {
let (vm_fd, _) = socketpair( let (vm_fd, _) = socketpair(
AddressFamily::Unix, AddressFamily::Unix,
@@ -286,14 +222,16 @@ mod tests {
vm_fd.as_raw_fd(), vm_fd.as_raw_fd(),
MacAddress::from_str("02:00:00:00:00:01").unwrap(), MacAddress::from_str("02:00:00:00:00:01").unwrap(),
NetType::Nat, NetType::Nat,
allow PrefixSet::from_iter(
.into_iter() allow
.map(|cidr| cidr.parse().unwrap()) .into_iter()
.collect(), .map(|cidr| Ipv4Net::from_str(cidr).unwrap()),
block ),
.into_iter() PrefixSet::from_iter(
.map(|cidr| cidr.parse().unwrap()) block
.collect(), .into_iter()
.map(|cidr| Ipv4Net::from_str(cidr).unwrap()),
),
Vec::default(), Vec::default(),
) )
.unwrap(); .unwrap();
+105 -19
View File
@@ -1,19 +1,22 @@
use anyhow::{Context, anyhow}; use anyhow::{Context, Error, anyhow};
use clap::Parser; use clap::Parser;
use ipnet::Ipv4Net;
use log::LevelFilter; use log::LevelFilter;
use nix::sys::signal::{SigHandler, Signal, signal}; use nix::sys::signal::{SigHandler, Signal, signal};
use oslog::OsLogger; use oslog::OsLogger;
use prefix_trie::PrefixSet;
use privdrop::PrivDrop; use privdrop::PrivDrop;
use softnet::NetType; use softnet::NetType;
use softnet::proxy::ExposedPort; use softnet::proxy::ExposedPort;
use softnet::proxy::Proxy; use softnet::proxy::Proxy;
use softnet::proxy::Target;
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;
@@ -22,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(
@@ -51,34 +115,32 @@ struct Args {
#[clap( #[clap(
long, long,
help = "Comma-separated list of CIDRs to allow the traffic to \ help = "Comma-separated list of CIDRs, IPs, or domains to allow the traffic to \
(e.g. --allow=192.168.0.0/24 may be used to allow a LAN access for a VM), \ (e.g. --allow=192.168.0.0/24 or --allow=example.com). Domains are resolved to A records \
plus supported @-aliases. Currently the only supported @-alias is @host, \ at startup. \
which matches the vmnet bridge gateway IP. \
When used with --block, the longest prefix match always wins. \ When used with --block, the longest prefix match always wins. \
In case an identical prefix is both --allow'ed and --block'ed, \ 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, \ 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).", it additionally disables bridge isolation (even when --block=0.0.0.0/0 is specified).",
value_name = "comma-separated CIDRs or @-alias", value_name = "comma-separated CIDRs/IPs/domains",
use_value_delimiter = true, use_value_delimiter = true,
action = clap::ArgAction::Set action = clap::ArgAction::Set
)] )]
allow: Vec<Target>, allow: Vec<AllowBlockEntry>,
#[clap( #[clap(
long, long,
help = "Comma-separated list of CIDRs to block the traffic to \ 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 \ (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. \ that is further relaxed with --allow). Domains are resolved to A records at startup. \
Currently the only supported @-alias is @host, which matches the vmnet bridge gateway IP. \ When used with --allow, \
When used with --allow, the longest prefix match always wins. \ the longest prefix match always wins. In case the same prefix is both \
In case an identical prefix is both --allow'ed and --block'ed, \ --allow'ed and --block'ed, blocking takes precedence.",
blocking will take precedence.", value_name = "comma-separated CIDRs/IPs/domains",
value_name = "comma-separated CIDRs or @-alias",
use_value_delimiter = true, use_value_delimiter = true,
action = clap::ArgAction::Set action = clap::ArgAction::Set
)] )]
block: Vec<Target>, block: Vec<AllowBlockEntry>,
#[clap( #[clap(
long, long,
@@ -148,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
@@ -191,6 +253,9 @@ 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);
@@ -199,8 +264,8 @@ fn try_main() -> anyhow::Result<()> {
args.vm_fd as RawFd, args.vm_fd as RawFd,
args.vm_mac_address, args.vm_mac_address,
args.vm_net_type, args.vm_net_type,
args.allow, PrefixSet::from_iter(allow),
args.block, PrefixSet::from_iter(block),
args.expose, args.expose,
) )
.context("failed to initialize proxy")?; .context("failed to initialize proxy")?;
@@ -252,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)))
);
}
}