Compare commits

..
Author SHA1 Message Date
Fedor Korotkov c9c9671170 Remove Sentry release steps 2026-06-03 14:43:41 -07:00
Fedor Korotkov eed811747c Migrate CI to GitHub Actions 2026-06-03 13:53:54 -07:00
9 changed files with 120 additions and 271 deletions
-68
View File
@@ -1,68 +0,0 @@
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
@@ -0,0 +1,102 @@
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
-48
View File
@@ -1333,15 +1333,6 @@ version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
[[package]]
name = "ipnetwork"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e"
dependencies = [
"serde",
]
[[package]]
name = "iri-string"
version = "0.7.10"
@@ -1612,12 +1603,6 @@ dependencies = [
"memoffset",
]
[[package]]
name = "no-std-net"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65"
[[package]]
name = "num-conv"
version = "0.2.0"
@@ -1822,38 +1807,6 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "pnet_base"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7"
dependencies = [
"no-std-net",
]
[[package]]
name = "pnet_datalink"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e79e70ec0be163102a332e1d2d5586d362ad76b01cec86f830241f2b6452a7b7"
dependencies = [
"ipnetwork",
"libc",
"pnet_base",
"pnet_sys",
"winapi",
]
[[package]]
name = "pnet_sys"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d4643d3d4db6b08741050c2f3afa9a892c4244c085a72fcda93c9c2c9a00f4b"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "polling"
version = "3.11.0"
@@ -2621,7 +2574,6 @@ dependencies = [
"nix 0.31.2",
"num_enum 0.7.6",
"oslog",
"pnet_datalink",
"polling",
"prefix-trie",
"privdrop",
-1
View File
@@ -34,7 +34,6 @@ oslog = "0.2.0"
log = "0.4.29"
serial_test = "3"
coarsetime = "0.1.37"
pnet_datalink = "0.35.0"
[profile.release]
debug = true
+3 -60
View File
@@ -1,8 +1,6 @@
use anyhow::{Context, Result, anyhow};
use clap::ValueEnum;
use log::info;
use smoltcp::wire::EthernetAddress;
use std::net::IpAddr;
use std::net::Ipv4Addr;
use std::os::unix::io::{AsRawFd, RawFd};
use std::os::unix::net::UnixDatagram;
@@ -30,7 +28,6 @@ pub struct Host {
new_packets_rx: UnixDatagram,
callback_can_continue_tx: SyncSender<()>,
pub gateway_ip: smoltcp::wire::Ipv4Address,
pub gateway_mac: EthernetAddress,
pub max_packet_size: u64,
pub read_max_packets: u64,
finalized: bool,
@@ -52,43 +49,16 @@ impl Host {
.context("failed to initialize vmnet interface")?;
// Retrieve first IP (gateway) used for this interface
let Some(Parameter::StartAddress(start_address)) =
let Some(Parameter::StartAddress(gateway_ip)) =
interface.parameters().get(ParameterKind::StartAddress)
else {
return Err(anyhow!(
"failed to retrieve vmnet's interface start address"
));
};
let start_address = Ipv4Addr::from_str(&start_address)
let gateway_ip = Ipv4Addr::from_str(&gateway_ip)
.context("failed to parse vmnet's interface start address")?;
// Retrieve last IP used for this interface and calculate the prefix
let Some(Parameter::EndAddress(end_address)) =
interface.parameters().get(ParameterKind::EndAddress)
else {
return Err(anyhow!("failed to retrieve vmnet's interface end address"));
};
let end_address = Ipv4Addr::from_str(&end_address)
.context("failed to parse vmnet's interface end address")?;
let Some(prefix) = Self::ipv4_range_prefix(start_address, end_address) else {
return Err(anyhow!(
"failed to resolve vmnet's interface: prefix ambiguity for {}–{}",
start_address,
end_address
));
};
// Figure out the gateway's interface MAC address
let Some(gateway_mac) = Self::interface_mac_for_ip(start_address, prefix) else {
return Err(anyhow!(
"failed to resolve vmnet's interface: no interface found with {}/{} CIDR",
start_address,
prefix
));
};
let gateway_mac = EthernetAddress(gateway_mac.octets());
// Retrieve max packet size for this interface
let Some(Parameter::MaxPacketSize(max_packet_size)) =
interface.parameters().get(ParameterKind::MaxPacketSize)
@@ -134,39 +104,12 @@ impl Host {
interface,
new_packets_rx,
callback_can_continue_tx,
gateway_ip: start_address,
gateway_mac,
gateway_ip,
max_packet_size,
read_max_packets,
finalized: false,
})
}
fn interface_mac_for_ip(ip: Ipv4Addr, prefix: u8) -> Option<pnet_datalink::MacAddr> {
for iface in pnet_datalink::interfaces() {
if iface
.ips
.iter()
.any(|network| network.ip() == IpAddr::V4(ip) && network.prefix() == prefix)
&& let Some(mac) = iface.mac
{
return Some(mac);
}
}
None
}
fn ipv4_range_prefix(start_address: Ipv4Addr, end_address: Ipv4Addr) -> Option<u8> {
let start_address = start_address.to_bits();
let end_address = end_address.to_bits();
if start_address > end_address {
return None;
}
Some((start_address ^ end_address).leading_zeros() as u8)
}
}
impl Host {
+5 -42
View File
@@ -1,9 +1,7 @@
use crate::proxy::Proxy;
use crate::proxy::udp_packet_helper::UdpPacketHelper;
use crate::proxy::{Action, Proxy};
use anyhow::{Context, Result};
use ipnet::Ipv4Net;
use smoltcp::wire::{ArpPacket, EthernetFrame, EthernetProtocol, Ipv4Packet, UdpPacket};
use std::net::Ipv4Addr;
use smoltcp::wire::{EthernetFrame, EthernetProtocol, Ipv4Packet, UdpPacket};
impl Proxy<'_> {
pub(crate) fn process_frame_from_host(&mut self, frame: &EthernetFrame<&[u8]>) -> Result<()> {
@@ -38,42 +36,14 @@ impl Proxy<'_> {
}
}
pub(crate) fn allowed_from_host(&self, frame: &EthernetFrame<&[u8]>) -> Option<()> {
if frame.src_addr() == self.host.gateway_mac {
return allowed_host_ethertype(frame);
}
match self.rules_mac.get(frame.src_addr().as_bytes()) {
Some(Action::Block) => return None,
Some(Action::Allow) => return allowed_host_ethertype(frame),
None => {}
}
fn allowed_from_host(&mut self, frame: &EthernetFrame<&[u8]>) -> Option<()> {
match frame.ethertype() {
EthernetProtocol::Arp => {
let arp_pkt = ArpPacket::new_checked(frame.payload()).ok()?;
let source_protocol_addr: [u8; 4] =
arp_pkt.source_protocol_addr().try_into().ok()?;
self.allowed_peer_ip_from_host(Ipv4Addr::from(source_protocol_addr))
}
EthernetProtocol::Ipv4 => {
let ipv4_pkt = Ipv4Packet::new_checked(frame.payload()).ok()?;
self.allowed_peer_ip_from_host(ipv4_pkt.src_addr())
}
EthernetProtocol::Arp => Some(()),
EthernetProtocol::Ipv4 => Some(()),
_ => None,
}
}
fn allowed_peer_ip_from_host(&self, peer_ip: Ipv4Addr) -> Option<()> {
let peer_net = Ipv4Net::from(peer_ip);
match self.rules.get_lpm(&peer_net).map(|(_, action)| action) {
Some(Action::Allow) => Some(()),
Some(Action::Block) | None => None,
}
}
fn snoop(&mut self, frame: &EthernetFrame<&[u8]>) {
if frame.ethertype() != EthernetProtocol::Ipv4 {
return;
@@ -104,10 +74,3 @@ impl Proxy<'_> {
self.dhcp_snooper.register_dhcp_reply(udp_pkt.payload());
}
}
fn allowed_host_ethertype(frame: &EthernetFrame<&[u8]>) -> Option<()> {
match frame.ethertype() {
EthernetProtocol::Arp | EthernetProtocol::Ipv4 => Some(()),
_ => None,
}
}
+5 -33
View File
@@ -16,7 +16,6 @@ use mac_address::MacAddress;
use port_forwarder::PortForwarder;
use prefix_trie::{Prefix, PrefixMap};
use smoltcp::wire::EthernetFrame;
use std::collections::HashMap;
use std::io::ErrorKind;
use std::os::unix::io::{AsRawFd, RawFd};
use std::str::FromStr;
@@ -30,7 +29,6 @@ pub struct Proxy<'proxy> {
vm_mac_address: smoltcp::wire::EthernetAddress,
dhcp_snooper: DhcpSnooper,
rules: PrefixMap<Ipv4Net, Action>,
rules_mac: HashMap<[u8; 6], Action>,
enobufs_encountered: bool,
port_forwarder: PortForwarder,
}
@@ -38,20 +36,9 @@ pub struct Proxy<'proxy> {
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Target {
Prefix(Ipv4Net),
MacAddress(MacAddress),
Host,
}
impl Target {
pub fn is_ipv4_default_route(&self) -> bool {
matches!(self, Target::Prefix(prefix) if *prefix == Ipv4Net::zero())
}
pub fn is_mac_address(&self) -> bool {
matches!(self, Target::MacAddress(_))
}
}
impl FromStr for Target {
type Err = ipnet::AddrParseError;
@@ -60,10 +47,6 @@ impl FromStr for Target {
return Ok(Target::Host);
}
if let Ok(mac_address) = MacAddress::from_str(s) {
return Ok(Target::MacAddress(mac_address));
}
Ipv4Net::from_str(s).map(Target::Prefix)
}
}
@@ -84,9 +67,10 @@ impl Proxy<'_> {
exposed_ports: Vec<ExposedPort>,
) -> Result<Proxy<'proxy>> {
let vm = VM::new(vm_fd)?;
let enable_isolation = !allow.iter().any(Target::is_ipv4_default_route)
&& !allow.iter().chain(block.iter()).any(Target::is_mac_address);
let host = Host::new(vm_net_type, enable_isolation)?;
let host = Host::new(
vm_net_type,
!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)?;
@@ -95,17 +79,11 @@ impl Proxy<'_> {
// SECURITY: blocking rules must always take precedence
// over allowing rules when prefixes are identical.
let mut rules = PrefixMap::new();
let mut rules_mac = HashMap::new();
for allow_target in allow {
let allow_prefix = match allow_target {
Target::Prefix(prefix) => prefix,
Target::Host => host.gateway_ip.into(),
Target::MacAddress(mac_address) => {
rules_mac.insert(mac_address.bytes(), Action::Allow);
continue;
}
};
rules.insert(allow_prefix, Action::Allow);
@@ -115,11 +93,6 @@ impl Proxy<'_> {
let block_prefix = match block_target {
Target::Prefix(prefix) => prefix,
Target::Host => host.gateway_ip.into(),
Target::MacAddress(mac_address) => {
rules_mac.insert(mac_address.bytes(), Action::Block);
continue;
}
};
rules.insert(block_prefix, Action::Block);
@@ -132,7 +105,6 @@ impl Proxy<'_> {
vm_mac_address: smoltcp::wire::EthernetAddress(vm_mac_address.bytes()),
dhcp_snooper: DhcpSnooper::new(poller_timeout),
rules,
rules_mac,
enobufs_encountered: false,
port_forwarder: PortForwarder::new(exposed_ports),
})
@@ -344,6 +316,6 @@ mod tests {
let ipv4_pkt = Ipv4Packet::new_unchecked(buf.as_slice());
proxy.allowed_from_vm_ipv4(ipv4_pkt, false)
proxy.allowed_from_vm_ipv4(ipv4_pkt)
}
}
+3 -17
View File
@@ -21,17 +21,11 @@ impl Proxy<'_> {
.context("failed to write to the host")
}
pub(crate) fn allowed_from_vm(&self, frame: &EthernetFrame<&[u8]>) -> Option<()> {
fn allowed_from_vm(&self, frame: &EthernetFrame<&[u8]>) -> Option<()> {
if frame.src_addr() != self.vm_mac_address {
return None;
}
let dst_mac_allowed = match self.rules_mac.get(frame.dst_addr().as_bytes()) {
Some(Action::Block) => return None,
Some(Action::Allow) => true,
None => false,
};
match frame.ethertype() {
EthernetProtocol::Arp => {
let arp_pkt = ArpPacket::new_checked(frame.payload()).ok()?;
@@ -39,7 +33,7 @@ impl Proxy<'_> {
}
EthernetProtocol::Ipv4 => {
let ipv4_pkt = Ipv4Packet::new_checked(frame.payload()).ok()?;
self.allowed_from_vm_ipv4(ipv4_pkt, dst_mac_allowed)
self.allowed_from_vm_ipv4(ipv4_pkt)
}
_ => None,
}
@@ -64,21 +58,13 @@ impl Proxy<'_> {
None
}
pub(crate) fn allowed_from_vm_ipv4(
&self,
ipv4_pkt: Ipv4Packet<&[u8]>,
dst_mac_allowed: bool,
) -> Option<()> {
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();
if dst_mac_allowed {
return Some(());
}
// Filter traffic based on user-specified rules first
if !self.rules.is_empty() {
let dst_net = Ipv4Net::from(dst_addr);
+2 -2
View File
@@ -59,7 +59,7 @@ struct Args {
In case an identical prefix is both --allow'ed and --block'ed, \
blocking will take precedence. --allow=0.0.0.0/0 is a special case, \
it additionally disables bridge isolation (even when --block=0.0.0.0/0 is specified).",
value_name = "comma-separated CIDRs, MAC addresses or @-aliases",
value_name = "comma-separated CIDRs or @-alias",
use_value_delimiter = true,
action = clap::ArgAction::Set
)]
@@ -74,7 +74,7 @@ struct Args {
When used with --allow, the longest prefix match always wins. \
In case an identical prefix is both --allow'ed and --block'ed, \
blocking will take precedence.",
value_name = "comma-separated CIDRs, MAC addresses or @-aliases",
value_name = "comma-separated CIDRs or @-alias",
use_value_delimiter = true,
action = clap::ArgAction::Set
)]