Skip to content
Open
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
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,19 @@ updates:
directory: "/"
schedule:
interval: weekly
# Transitive crates too: a root daemon parsing untrusted packets is only
# as current as its whole dependency tree.
allow:
- dependency-type: all
groups:
# Minor and patch bumps arrive as one PR; majors stay separate.
cargo-minor:
update-types: ["minor", "patch"]

- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
groups:
actions:
patterns: ["*"]
37 changes: 37 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: e2e

on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read

env:
CARGO_TERM_COLOR: always

jobs:
armed-e2e:
name: armed e2e (real NFQUEUE verdicts in a netns)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de # master
with:
toolchain: stable
- name: Install build and test deps
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
protobuf-compiler libnfnetlink-dev libnetfilter-queue-dev nftables jq
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: armed-e2e
- name: Armed e2e (build, netns, shipped nft snippet, daemon, curl)
timeout-minutes: 25
run: ./scripts/armed-e2e.sh
13 changes: 0 additions & 13 deletions .github/workflows/ebpf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -513,19 +513,6 @@ jobs:
grep -o 'verified_insns: [^ ]* = [0-9]*' guest.log | while read -r _ p _ n; do
echo "| \`${p}\` verified insns | ${n} |"
done || true
# The fast path's facts as this guest observed them and the
# decision the eligibility ladder takes on them with the feature
# on. Not what `cfc status` would say on a real host of this
# kernel: the guest mounts no bpffs, so `lifecycle_pinned` is
# false here on every kernel and the deadline shown is the reduced
# one wherever a real host would pin. The other two facts -
# `exit_precise` and the capability - are the kernel's own.
# `tr -d '\r'`: the guest console writes CRLF, and a CR inside a
# table cell ends the markdown row early. `|| true` as above: a
# guest that died before printing this already failed on the marker.
grep -o 'fast path on this kernel: .*' guest.log | tr -d '\r' | tail -1 | while read -r line; do
echo "| fast path | ${line#fast path on this kernel: } |"
done || true
} >> "${GITHUB_STEP_SUMMARY}"

# Wall clock, not just instruction count. They are different
Expand Down
30 changes: 18 additions & 12 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ jobs:

- name: Resolve version and verify it matches the tag
id: version
env:
REF_TYPE: ${{ github.ref_type }}
REF_NAME: ${{ github.ref_name }}
run: |
VERSION="$(sed -n '/^\[workspace\.package\]/,/^\[/{s/^version *= *"\(.*\)"/\1/p}' Cargo.toml | head -n1)"
if [ -z "${VERSION}" ]; then
Expand All @@ -30,8 +33,8 @@ jobs:
# The release is named after the tag, but every asset filename is
# built from the Cargo version. A mismatch publishes "v0.3.0"
# containing colony-firewall-control-0.2.0-*.tar.zst. Refuse.
if [ "${{ github.ref_type }}" = "tag" ] && [ "${{ github.ref_name }}" != "v${VERSION}" ]; then
echo "::error::tag ${{ github.ref_name }} does not match Cargo.toml [workspace.package] version ${VERSION} (expected tag v${VERSION}). Bump Cargo.toml or retag."
if [ "${REF_TYPE}" = "tag" ] && [ "${REF_NAME}" != "v${VERSION}" ]; then
echo "::error::tag ${REF_NAME} does not match Cargo.toml [workspace.package] version ${VERSION} (expected tag v${VERSION}). Bump Cargo.toml or retag."
exit 1
fi
echo "version=${VERSION}" >> "${GITHUB_OUTPUT}"
Expand Down Expand Up @@ -189,7 +192,7 @@ jobs:
crates/cfc-ebpf/target/bpfel-unknown-none/release/cfc-ebpf.o \
"${STAGE}/"

# Docs, for parity with what the AUR package puts in
# Docs, for parity with what pkg/PKGBUILD puts in
# /usr/share/doc. `cfc status` points users at TROUBLESHOOTING.md
# by name, so it has to actually ship.
install -m644 \
Expand Down Expand Up @@ -236,8 +239,9 @@ jobs:
SHA256SUMS
RELEASE_BODY.md

# Build pkg/PKGBUILD for real against the freshly pushed tag, and produce
# the AUR-submittable artifacts (PKGBUILD with real checksums + .SRCINFO).
# Build pkg/PKGBUILD for real against the freshly pushed tag, and attach
# the Arch packaging recipe (PKGBUILD with real checksums + .SRCINFO) to
# the release. The project is not published on the AUR.
#
# Hard gate, no continue-on-error: at tag time the source= URL
# (.../archive/v$pkgver.tar.gz) resolves, because GitHub generates the
Expand Down Expand Up @@ -275,10 +279,12 @@ jobs:
chown -R builder: .

- name: Verify pkgver matches the tag
env:
REF_NAME: ${{ github.ref_name }}
run: |
PKGVER="$(sed -n 's/^pkgver=//p' pkg/PKGBUILD | head -n1)"
if [ "${{ github.ref_name }}" != "v${PKGVER}" ]; then
echo "::error::pkg/PKGBUILD pkgver=${PKGVER} does not match tag ${{ github.ref_name }}"
if [ "${REF_NAME}" != "v${PKGVER}" ]; then
echo "::error::pkg/PKGBUILD pkgver=${PKGVER} does not match tag ${REF_NAME}"
exit 1
fi

Expand All @@ -287,7 +293,7 @@ jobs:
run: |
runuser -u builder -- updpkgsums PKGBUILD
# A remote (non-VCS) source with sha256sums=('SKIP') is not
# acceptable AUR practice: it disables integrity checking of the
# acceptable Arch packaging practice: it disables integrity checking of the
# release tarball entirely.
if grep -qE "^sha256sums=\(.*'SKIP'" PKGBUILD; then
echo "::error::pkg/PKGBUILD still has a SKIP checksum after updpkgsums"
Expand All @@ -308,8 +314,8 @@ jobs:
# An undotted copy is what gets attached: GitHub renames dot-leading
# asset filenames (`.SRCINFO` became `default.SRCINFO`), so the
# published name never matched what the README told people to
# download. Ship a name GitHub keeps; the AUR checkout renames it
# back to `.SRCINFO` locally.
# download. Ship a name GitHub keeps; whoever builds from it renames
# it back to `.SRCINFO` locally.
cp .SRCINFO SRCINFO

- name: namcap the PKGBUILD
Expand Down Expand Up @@ -359,7 +365,7 @@ jobs:
exit 1
fi

- name: Upload AUR artifacts
- name: Upload Arch packaging artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: aur-assets
Expand Down Expand Up @@ -394,7 +400,7 @@ jobs:
files: |
release-assets/colony-firewall-control-*.tar.zst
release-assets/SHA256SUMS
- name: Attach AUR artifacts
- name: Attach Arch packaging artifacts
if: github.ref_type == 'tag'
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
with:
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ and [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Removed

- The Fast Allow userspace path, disabled since 0.7.0 because a socket mark
cannot prove which process sends and so opened bypasses. `cfc --json status`
no longer has a `fast_allow` key, `StatusResponse` field 16 is reserved, and
the `[ebpf] fast_allow` and `fast_allow_mark` keys are ignored with a
warning. For hosts upgrading from 0.4-0.6, startup still flushes the legacy
nftables set, disarms the legacy pinned maps and removes the old sendmsg
link pins.

## [0.7.0] - 2026-09-30

### Added
Expand Down
76 changes: 48 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,22 @@ NFQUEUE in the kernel, per-app pop-ups in iced, gRPC IPC over a Unix socket.
+--------------------------------------------------+
```

Seven workspace crates:

| Crate | Role |
|---------------|----------------------------------------------------------|
| `cfc-core` | Shared types: `Rule`, `Verdict`, `Connection`, `Process` |
| `cfc-proto` | gRPC schema (tonic + tonic-prost) |
| `cfc-client` | Shared UDS gRPC client wrapper |
| `cfc-daemon` | Privileged daemon |
| `cfc-ui` | iced GUI |
| `cfc-cli` | Terminal control tool |
| `cfc-tray` | System-tray companion (StatusNotifierItem) |
Ten crates: nine workspace members, plus the kernel-side `cfc-ebpf`, which
is its own workspace (pinned nightly + bpf-linker, built by `cargo xtask
build-ebpf`) so stable builds never see it:

| Crate | Role |
|-------------------|----------------------------------------------------------------------------|
| `cfc-core` | Shared types and rule matching: `Rule`, `Verdict`, `Connection`, `Process` |
| `cfc-proto` | gRPC schema (tonic + tonic-prost) |
| `cfc-client` | Shared UDS gRPC client wrapper |
| `cfc-daemon` | Privileged daemon |
| `cfc-ui` | iced GUI |
| `cfc-cli` | Terminal control tool |
| `cfc-tray` | System-tray companion (StatusNotifierItem) |
| `cfc-ebpf-common` | POD types and pure parsers shared by eBPF and userspace |
| `cfc-ebpf` | Kernel-side programs of the optional eBPF backend |
| `xtask` | Build automation (eBPF object build) |

More docs:

Expand Down Expand Up @@ -171,7 +176,7 @@ Enable the installed daemon and enforcement in First run below.
## First run

A fresh install has **zero rules**: once enforcement is on, every new
outbound connection prompts (or falls back to the profile default). Do
remote outbound connection prompts (or falls back to the profile default). Do
these three things, in order:

**1. Enable enforcement persistently.** A companion unit loads the
Expand Down Expand Up @@ -204,11 +209,10 @@ systemd-timesyncd and chronyd NTP (:123/udp), the DHCP clients (dhcpcd,
NetworkManager and systemd-networkd, :67 and :547/udp), pacman and paru
HTTPS mirrors (:443/tcp), and the SSH client (:22/tcp) - and is
idempotent (already-present rules are skipped by name; `--dry-run`
previews). **Do not skip this step.** No profile allows anything on its
own, so on a machine with no rules and no UI connected nothing outbound
gets through - including the DHCP lease. Filtering starts before the
network is configured (see below), and these rules are what let the
machine come up at all.
previews). **Do not skip this step.** No profile allows unmatched remote flows
on its own. With no rules and no UI connected, unmatched queued remote
connections are denied. Filtering starts before the network is configured
(see below), and these rules keep DHCP, DNS and NTP usable.

For everything else, there are bundles:

Expand Down Expand Up @@ -240,8 +244,8 @@ On a headless machine, answer them from the terminal instead:
cfc prompts
```

With no subscriber at all the daemon applies `no_ui_action` to every
unmatched flow without asking anyone. **That is a denial under every
With no subscriber at all the daemon applies `no_ui_action` to unmatched
remote flows without asking anyone. **That is a denial under every
profile.** "Nobody is connected" is a permanent condition on a headless
box, not a passing one, and answering it with an allow would mean those
hosts had no outbound firewall whatsoever. Stored rules are what such a
Expand All @@ -267,13 +271,25 @@ initramfs, interfaces already configured before these units, other network
managers, or a later external ruleset flush. Early unmatched flows use
`no_ui_action`; bootstrap DHCP/DNS/NTP rules keep strict configurations usable.

**Scope.** Rules decide new tracked flows; established and related traffic
retains its connection-wide authorization. Passed or inherited sockets and
local DNS/proxy relays are not confined to their original executable.
Loopback is exempt, and packet-layer traffic from applications with
`CAP_NET_RAW` is outside these IP hooks. Use OS containment for those cases.
Fast Allow is disabled even when `fast_allow = true` is configured; allowed
flows use the normal NFQUEUE path.
**Scope.** Normal mode decides new tracked IP flows from socket attribution;
established and related traffic retains its connection-wide authorization.
Passed or inherited sockets are not reauthorized for each sending executable.
A current descriptor holder does not prove which process sent a packet.
While the daemon runs, new direct loopback flows follow explicit rules;
unmatched local IPC is allowed without prompting. While no daemon listens on
the queue, new loopback flows are allowed (`queue ... bypass` on `lo` only), so
local services keep working; resolving names that are not cached still needs
the daemon. An allowed local resolver or proxy can still relay remote
traffic. CFC cannot establish the originating application's identity from
remote flows delegated through local brokers, including AF_UNIX and D-Bus.

Applications with `CAP_NET_RAW` can use AF_PACKET outside the `inet OUTPUT`
hook. Raw IP packets can also coincide with another socket's tuple; socket
attribution does not prove their origin. Use explicit application confinement
or OS containment for those cases.
Fast Allow was removed: a socket mark cannot prove which process sends, so it
opened bypasses. The old `[ebpf] fast_allow` and `fast_allow_mark` keys are
ignored with a warning, and allowed flows use the normal NFQUEUE path.

Then confirm it is really filtering:

Expand All @@ -282,15 +298,19 @@ cfc status # "enforcing yes", and it warns on stderr when it is not
```

> **WARNING - remote / SSH machines:** the shipped nftables snippet is
> fail-closed. If the daemon is down while the rule is loaded, **all new
> outbound connections drop**, and a mistake can lock you out of a box you
> fail-closed for everything except new loopback flows, which are allowed
> while no daemon listens. If the daemon is down while the rule is loaded,
> **all new non-loopback outbound connections drop**, and a mistake can lock you out of a box you
> only reach over SSH. Read
> [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) - specifically the
> SSH exemption and dead-man's-switch patterns - *before* enabling
> enforcement remotely.

### Explicit application confinement

**Experimental.** This mode is new in 0.7.0, has not been externally
audited, and its interface and platform requirements may change.

`cfc applications run` starts a separate, headless application tree with an
empty network permission list. Administrators may approve exact numeric peer
addresses with `--allow IP`. Permissions apply to the entire tree across
Expand Down
10 changes: 7 additions & 3 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
# Security Policy

Colony Firewall Control is **alpha software**. It runs a daemon as root with
Colony Firewall Control is **beta software**. It runs a daemon as root with
`CAP_NET_ADMIN` and makes allow/deny decisions about your network traffic, so
security reports are taken seriously -- but expectations should match the
project's maturity: there has been no external audit, and interfaces may
change without notice.
project's maturity: **there has been no external security audit yet**, and
interfaces may change without notice.

The explicit application confinement mode (`cfc applications run`, new in
0.7.0) is **experimental**, and its interface and platform requirements may
change.

## Supported Versions

Expand Down
Loading
Loading