Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 32 additions & 13 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ and [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.4.0] - 2026-09-05

### Added

- **Fast allow (opt-in, `[ebpf] fast_allow = true`).** A process a lasting
Expand Down Expand Up @@ -75,20 +77,20 @@ and [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
already takes the sendmsg hooks that 5.10 refuses, and neither has
`group_dead`.
- **The startup report says what the fast path's kernel side is capable
of** (`fast_path=ready|sendmsg-unavailable|basic-connect` on the log
line, `none` where no connect hook attached), and the matrix test asserts it per kernel along with `group_dead`
where a run has already shown the answer: 5.10 takes the connect hooks and
refuses the sendmsg ones, 5.15 and 6.12 take both and still have no
`group_dead`, 6.18 and 7.1 have everything. A kernel that changes its
answer fails in CI rather than degrading quietly on a host; one without a
recorded answer is
printed, and the matrix summary carries the line.
of** (`fast_path=ready|sendmsg-unavailable|basic-connect` on the log line,
`none` where no connect hook attached), and the matrix test asserts it per
kernel, along with `group_dead`, wherever a run has already shown the
answer: 5.10 takes the connect hooks and refuses the sendmsg ones, 5.15 and
6.12 take both and still have no `group_dead`, 6.18 and 7.1 have
everything. A kernel that changes its answer fails in CI rather than
degrading quietly on a host; one without a recorded answer is printed, and
the matrix summary carries the line.

### Changed

- Three costs removed from paths every process on the machine takes, none of
them measured on a live kernel - this machine cannot run the daemon - and
each argued from what the code does rather than from a number. The exec and
them measured on a live kernel yet, each argued from what the code does
rather than from a number. The exec and
exit programs deleted a fast-allow grant on every `execve` and every exit,
unconditionally, on hosts where the feature is off (which is every host by
default); the delete is now behind one array read of the mark, which is
Expand All @@ -109,14 +111,31 @@ and [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
and written by nobody for two releases, is gone.
- The daemon now runs `nft` to put its fast-allow value into one set and
take it out again - the first time it touches nftables; the SELinux policy
grants exactly that. The set is flushed unconditionally at every start, so
a daemon that crashed while armed and came back with the path off does not
leave its predecessor's mark accepted; and once armed the daemon re-checks
grants exactly that. The set is flushed at every start of the daemon - with
the layer on, switched off in the config, or absent from the build - so a
daemon that crashed while armed and came back in any of those states does
not leave its predecessor's mark accepted; and once armed the daemon re-checks
every minute that the element is still there, so an `nft -f` that reloads
the ruleset is noticed and re-armed rather than reported as live. The
nftables snippet gains the set and the accept rule; an older snippet leaves
fast-allow off with the reason spelled out.

### Fixed

- The RPM spec still said 0.2.3 in the 0.3.0 tree, and nothing ran to say
so: `scripts/check-versions.sh` now holds the spec, the PKGBUILD and the
Colony manifest to `Cargo.toml` on every push, not only when a packaging
path changes.

### Internals

- Two integration tests that each copied a binary and spawned it raced each
other's fork (ETXTBSY on CI, the window between a fork and its exec, where
the child still holds the other test's write descriptor); copy and spawn
are now serialised behind one lock instead of retried past the race.
- Dependency bumps: rusqlite 0.40.2, libc 0.2.189, flate2 1.1.10,
owo-colors 4.4.0, thiserror 2.0.20; `action-gh-release` 3.0.3.

## [0.3.0] - 2026-09-02

### Added
Expand Down
18 changes: 9 additions & 9 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ members = [
exclude = ["crates/cfc-ebpf"]

[workspace.package]
version = "0.3.0"
version = "0.4.0"
edition = "2021"
rust-version = "1.88"
license = "GPL-3.0-or-later"
Expand Down
18 changes: 18 additions & 0 deletions crates/cfc-daemon/src/ebpf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,21 @@ pub struct Runtime {
_attached: Option<loader::Attached>,
}

/// Flushes a previous daemon's fast-allow mark out of the nftables set, for
/// the starts where [`start`] never reaches the loader's own flush: the layer
/// switched off in the config, or a build without it. The set outlives
/// daemons and the accept rule reads it whether or not anything still marks,
/// so a daemon that crashed while armed and came back with the layer off
/// would otherwise leave a standing bypass token behind it. A table that is
/// not loaded yet is not an error here - the nft unit is ordered after the
/// daemon - and `--dry-run` must not call this at all: it touches nothing, and
/// `main` is the one that knows it is running.
pub fn flush_stale_fast_allow() {
if let Err(e) = nft_set::disarm_for_start() {
tracing::warn!("could not flush a previous fast-allow mark from nftables: {e:#}");
}
}

/// Brings the eBPF layer up, as far as it will come up on this host.
///
/// Never returns an error: every failure mode is a note in the [`Report`].
Expand Down Expand Up @@ -797,6 +812,9 @@ pub fn start(
// `dns` and `table` are the loader's inputs; without it they are
// simply never wired to anything.
let _ = (dns, table);
// And the loader's flush of a predecessor's mark is never reached in
// this build, so it happens here.
flush_stale_fast_allow();
Runtime {
report: Report::inert_because(
cfg.enabled,
Expand Down
8 changes: 8 additions & 0 deletions crates/cfc-daemon/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,14 @@ async fn run() -> anyhow::Result<()> {
// sock_diag + /proc alone, which is exactly what the daemon does when the
// layer is unavailable anyway.
//
// The loader flushes a predecessor's fast-allow mark at the top of every
// load. With the layer switched off in the config that flush is never
// reached, and the nftables set outlives daemons - so it is done here for
// exactly that case. Not under --dry-run, which touches nothing.
if !args.dry_run && !cfg.ebpf.enabled.wants_load() {
ebpf::flush_stale_fast_allow();
}

// Held for the daemon's lifetime: dropping it detaches the programs.
let _ebpf = ebpf::start(
// `--dry-run` means "tell me what you would do without touching the
Expand Down
2 changes: 1 addition & 1 deletion crates/cfc-ebpf/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -362,8 +362,8 @@ with a single exception: under `[ebpf] fast_allow`, the sockets of a process
the daemon has already ruled allowed process-wide are marked in the connect
hook, and the snippet's `meta mark @fast_allow accept` rule takes them ahead
of the queue. That set is the one thing the daemon ever writes to nftables -
one element added when the path is armed, flushed unconditionally at every
start and at shutdown - and it ships empty, so a default install carries no
one element added when the path is armed, flushed at every daemon start (whether
or not the layer loads) and at shutdown - and it ships empty, so a default install carries no
bypass value.

**Where revocation reaches.** A grant is re-decided at every hook that opens a
Expand Down
8 changes: 7 additions & 1 deletion packaging/rpm/colony-firewall-control.spec
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
%global debug_package %{nil}

Name: colony-firewall-control
Version: 0.3.0
Version: 0.4.0
Release: 1%{?dist}
Summary: Application-aware outbound firewall for Linux

Expand Down Expand Up @@ -239,6 +239,12 @@ fi
%{_datadir}/selinux/devel/include/distributed/%{modulename}.if

%changelog
* Sat Sep 05 2026 MotherSphere <linhajahad@gmail.com> - 0.4.0-1
- Fast-allow path, opt-in: lastingly allowed processes skip the NFQUEUE round trip
- eBPF ABI v4
- The daemon arms one nftables set and the SELinux policy grants exactly that
- Kernel matrix brackets RHEL 9 with 5.10 and 5.15; veth latency bench

* Wed Sep 02 2026 MotherSphere <linhajahad@gmail.com> - 0.3.0-1
- Hash-bound prompt allows
- Tray icon fallback
Expand Down
2 changes: 1 addition & 1 deletion pkg/PKGBUILD
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# For building from a git checkout during development, see PKGBUILD-git.

pkgname=colony-firewall-control
pkgver=0.3.0
pkgver=0.4.0
pkgrel=1
pkgdesc="Application-aware outbound firewall for Linux, written in Rust"
arch=('x86_64')
Expand Down
2 changes: 1 addition & 1 deletion pkg/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ To reproduce the tarball locally, from the repo root:
cargo build --workspace --release --locked
cargo xtask build-ebpf # cfc-ebpf.o; postInstall fails outright without it

V=0.3.0
V=0.4.0
NAME="colony-firewall-control-${V}-linux-x86_64"
STAGE="$(mktemp -d)/${NAME}"
mkdir -p "${STAGE}"
Expand Down
4 changes: 2 additions & 2 deletions pkg/colony.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "colony-firewall-control",
"name": "Colony Firewall Control",
"version": "0.3.0",
"version": "0.4.0",
"description": "Application-aware outbound firewall for Linux. Per-app prompts, persistent rules, live connection feed.",
"license": "GPL-3.0-or-later",
"homepage": "https://github.com/Project-Colony/Colony-Firewall-Control",
Expand All @@ -11,7 +11,7 @@
"tags": ["firewall", "security", "network", "outbound", "opensnitch"],
"platforms": {
"linux-x86_64": {
"asset": "colony-firewall-control-0.3.0-linux-x86_64.tar.zst",
"asset": "colony-firewall-control-0.4.0-linux-x86_64.tar.zst",
"binaries": ["colony-firewalld", "colony-firewall", "colony-firewall-tray", "cfc"],
"installPath": "/usr/bin",
"postInstall": [
Expand Down
12 changes: 12 additions & 0 deletions scripts/check-versions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@
# - pkg/colony.json every "version" field and every version embedded
# in an "asset" filename
# - packaging/rpm/colony-firewall-control.spec Version:
# - crates/cfc-ebpf/Cargo.lock the `cfc-ebpf-common` entry. The kernel
# crate is its own workspace with its own
# committed lock, and nothing builds it with
# --locked, so a bump that regenerated only
# the root lock shipped 0.3.0 with this one
# still saying 0.2.3, and 0.4.0 nearly did
# the same. `cargo update -p cfc-ebpf-common`
# from crates/cfc-ebpf moves it.
#
# The spec is here because the job that compared it lived in rhel.yml, which
# only runs when packaging paths change - so a version bump that touched none
Expand Down Expand Up @@ -70,6 +78,10 @@ fi
spec_ver="$(sed -n 's/^Version:[[:space:]]*//p' "${ROOT}/packaging/rpm/colony-firewall-control.spec" | head -n1)"
check "packaging/rpm/colony-firewall-control.spec Version" "${spec_ver}"

# crates/cfc-ebpf/Cargo.lock -> the version recorded for cfc-ebpf-common
ebpf_lock_ver="$(awk '/^name = "cfc-ebpf-common"$/ { getline; sub(/^version = "/, ""); sub(/"$/, ""); print; exit }' "${ROOT}/crates/cfc-ebpf/Cargo.lock")"
check "crates/cfc-ebpf/Cargo.lock cfc-ebpf-common" "${ebpf_lock_ver}"

if [[ "${fail}" -ne 0 ]]; then
echo "version mismatch (canonical: Cargo.toml [workspace.package] = ${cargo_ver}):" >&2
for m in "${mismatches[@]}"; do
Expand Down
Loading