Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
afa2fb3
chore(workspace): scaffold the crate graph — edition 2024, MSRV 1.90,…
lunarthegrey Aug 20, 2026
bbf8af5
build(packaging): ship a .deb and a systemd unit from slice one — pac…
lunarthegrey Aug 20, 2026
85b702c
feat(config): line-based grammar with line-cited refusals — a typo st…
lunarthegrey Aug 20, 2026
e6ff84f
feat(daemon): foreground loop, polled signals and an acknowledged rel…
lunarthegrey Aug 20, 2026
46418f5
feat(policy): read the mitigation set behind guards for every way the…
lunarthegrey Aug 20, 2026
2afc8d6
feat(reconcile): level-triggered convergence with a stale path that c…
lunarthegrey Aug 20, 2026
b5b1a89
feat(rtbh): the fast tier, with guards that refuse and damping that m…
lunarthegrey Aug 20, 2026
fb60c53
feat(scrub-divert): the divert sequence as a pure machine, with the s…
lunarthegrey Aug 20, 2026
d216a89
feat(observability): metrics, status and explain — and the rate enric…
lunarthegrey Aug 20, 2026
4300495
fix(policy): stop rate enrichment from consuming the pagination stub'…
lunarthegrey Aug 20, 2026
e003baf
feat(probe): gate diversion on the return path, and read the flag the…
lunarthegrey Aug 20, 2026
98fd197
feat(bgp): the GoBGP sidecar backend, honest about what a confirmatio…
lunarthegrey Aug 20, 2026
8ad6cba
docs(runbooks): the four things an operator needs at three in the mor…
lunarthegrey Aug 20, 2026
46c6115
fix(module): give refusals distinct variants and a sentence — "protec…
lunarthegrey Aug 20, 2026
414ff32
fix(ci): run per-triple clippy inside cross — linting off-target is h…
lunarthegrey Aug 21, 2026
965747c
fix(ci): bump cargo-deb to 3.7.0, and point the binary asset at the p…
lunarthegrey Aug 21, 2026
3259478
fix(packaging): declare procps, and install through apt so declared d…
lunarthegrey Aug 21, 2026
d09b955
fix(packaging): declare dependencies explicitly and write the .deb to…
lunarthegrey Aug 21, 2026
eafd4e3
fix(release): apply the fixed output path to release.yml too
lunarthegrey Aug 21, 2026
204ce2e
fix(rtbh): make never-blackhole a two-way overlap test — a covering p…
lunarthegrey Aug 21, 2026
20b6ec8
fix(policy): install ca-file, break empty streaks on failure, and sto…
lunarthegrey Aug 21, 2026
cd85cf4
fix(probe): gate sysfs behind Linux, and keep the reason a failing re…
lunarthegrey Aug 21, 2026
b936f4b
fix(config): refuse module edits on reload, and refuse directives thi…
lunarthegrey Aug 21, 2026
02ef232
fix(divert): drive the quorum from the RIB — nothing could confirm on…
lunarthegrey Aug 21, 2026
2034703
fix(bgp): send TYPE_EXACT for an exact lookup — a literal 0 was TYPE_…
lunarthegrey Aug 21, 2026
b95b326
docs: stop claiming a clippy.toml that does not exist, and describe t…
lunarthegrey Aug 21, 2026
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
196 changes: 196 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
name: CI

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

env:
# Pinned to match rust-toolchain.toml. Both move together, in one PR.
RUST_STABLE: "1.97.1"

jobs:
check:
name: fmt, clippy, test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

# Installed with rustup directly rather than via a third-party action, so
# the toolchain version is visible in this file and not in someone else's.
- name: Install Rust ${{ env.RUST_STABLE }}
run: |
rustup toolchain install "$RUST_STABLE" --profile minimal --component rustfmt,clippy
rustup default "$RUST_STABLE"

- uses: actions/cache@v6
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

# A proto edited by hand, or a partial re-vendor, would otherwise ship an
# encoder that silently disagrees with the daemon on the other end. A
# field number that moved is not a compile error — it is a runtime
# disagreement — so this is the only thing that catches it early.
- name: Verify the vendored GoBGP protos
run: |
./ci/refresh-proto-manifest.sh
git diff --exit-code crates/bgp/proto/SOURCE.json \
|| { echo "::error::vendored protos do not match SOURCE.json"; exit 1; }

- run: cargo fmt --all --check
- run: cargo clippy --workspace --all-targets --all-features -- -D warnings
- run: cargo test --workspace

cross-build:
name: cross-build ${{ matrix.target }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
target:
- aarch64-unknown-linux-musl
- x86_64-unknown-linux-musl
- aarch64-unknown-linux-gnu
- x86_64-unknown-linux-gnu
steps:
- uses: actions/checkout@v7

- name: Install Rust ${{ env.RUST_STABLE }}
run: |
rustup toolchain install "$RUST_STABLE" --profile minimal --component clippy
rustup default "$RUST_STABLE"
rustup target add "${{ matrix.target }}"

- uses: actions/cache@v6
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cross-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}

- run: cargo install cross --locked --version 0.2.5

# Per-triple clippy over --all-targets BEFORE the build. `cross build`
# covers lib and bins only, and the host clippy run above sees exactly one
# triple — so without this, a test that fails to compile on aarch64, or a
# cfg-gated path only reachable on musl, ships green.
#
# Run through `cross`, not bare cargo. `reqwest`'s rustls feature pulls
# aws-lc-sys, which needs a C compiler for the *target*, and the runner
# has no aarch64 or musl cross-toolchain. Linting in a different
# environment from the one you build in is how a lint goes green while
# the build goes red — which is exactly what happened the first time this
# ran.
- run: cross clippy --workspace --all-targets --all-features --target "${{ matrix.target }}" -- -D warnings

- run: cross build --workspace --release --target "${{ matrix.target }}"

package:
name: package ${{ matrix.target }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
arch: amd64
- target: aarch64-unknown-linux-gnu
arch: arm64
steps:
- uses: actions/checkout@v7

- name: Install Rust ${{ env.RUST_STABLE }}
run: |
rustup toolchain install "$RUST_STABLE" --profile minimal
rustup default "$RUST_STABLE"
rustup target add "${{ matrix.target }}"

- uses: actions/cache@v6
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-pkg-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}

- run: cargo install cross --locked --version 0.2.5
# cargo-deb is pinned exactly and bumped in a reviewed PR. 2.7.0 could not
# parse `resolver = "3"` at all — its cargo_toml only knew resolvers 1 and 2 —
# which failed the packaging step outright.
- run: cargo install cargo-deb --locked --version 3.7.0

# Reproducible: the timestamp comes from the commit, not from the clock.
- name: Build release
run: |
SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct HEAD) \
cross build --workspace --release --target "${{ matrix.target }}"

# Every pull request produces an installable artifact an operator can drop
# on a staging node. The version carries the short SHA so a test package is
# never mistaken for a release, and sorts below the release it precedes.
- name: Build .deb
id: deb
run: |
VERSION="$(cat VERSION)~git$(git rev-parse --short HEAD)"
# An explicit output path rather than globbing the debian directory.
# `target/` is restored from cache, so a .deb from an earlier commit —
# named with that commit's sha — survives there, and `ls *.deb` then
# returns two lines and corrupts $GITHUB_OUTPUT.
OUT="dist/filterframe_${{ matrix.arch }}.deb"
mkdir -p dist
SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct HEAD) \
cargo deb -p filterframe-cli \
--target "${{ matrix.target }}" \
--no-build --no-strip \
--deb-version "$VERSION" \
--output "$OUT"
echo "path=$OUT" >> "$GITHUB_OUTPUT"

# The point of building packages on every PR rather than only at release:
# packaging breakage is caught by the change that caused it. Installing in
# a clean container is what makes this a test and not just an artifact.
- name: Verify the package installs
if: matrix.arch == 'amd64'
run: |
docker run --rm -v "$PWD:/w" -w /w debian:trixie-slim bash -euxo pipefail -c '
apt-get update -qq
apt-get install -y -qq --no-install-recommends systemd >/dev/null

# Install with apt, not dpkg, so declared dependencies are actually
# resolved. `dpkg -i` leaves them unsatisfied and the check then
# passes on a package that would not install cleanly on a real host.
apt-get install -y -qq "$PWD/${{ steps.deb.outputs.path }}" >/dev/null

# The unit must be valid to systemd itself, not merely present.
# `systemd-analyze verify` exits non-zero on a malformed unit and on
# an ExecStart that does not exist.
systemd-analyze verify /lib/systemd/system/filterframe.service

# Installed disabled and stopped, deliberately.
! systemctl is-enabled filterframe 2>/dev/null

test -f /etc/filterframe/example.conf
test -x /usr/bin/filterframe

# Argument parsing works with no configuration present.
filterframe --version
filterframe version
filterframe --help >/dev/null

apt-get remove -y -qq filterframe >/dev/null
'

- uses: actions/upload-artifact@v7
with:
name: filterframe-${{ matrix.arch }}-deb
path: dist/*.deb
retention-days: 14
if-no-files-found: error
135 changes: 135 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
name: Release

# Releases are the same build path as CI's `package` job with publishing
# appended — never a separate, less-exercised one. `dry_run` runs the whole
# matrix and skips publishing, so a release can be rehearsed before the tag
# exists.
on:
push:
tags: ['v*.*.*']
workflow_dispatch:
inputs:
dry_run:
description: "Build every artifact but do not publish"
type: boolean
default: true

permissions:
contents: write

env:
RUST_STABLE: "1.97.1"

jobs:
build:
name: build ${{ matrix.target }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- { target: x86_64-unknown-linux-gnu, arch: amd64, deb: true }
- { target: aarch64-unknown-linux-gnu, arch: arm64, deb: true }
- { target: x86_64-unknown-linux-musl, arch: amd64, deb: false }
- { target: aarch64-unknown-linux-musl, arch: arm64, deb: false }
steps:
- uses: actions/checkout@v7

- name: Install Rust ${{ env.RUST_STABLE }}
run: |
rustup toolchain install "$RUST_STABLE" --profile minimal
rustup default "$RUST_STABLE"
rustup target add "${{ matrix.target }}"

- run: cargo install cross --locked --version 0.2.5
- name: Install cargo-deb
if: matrix.deb
# cargo-deb is pinned exactly and bumped in a reviewed PR. 2.7.0 could not
# parse `resolver = "3"` at all — its cargo_toml only knew resolvers 1 and 2 —
# which failed the packaging step outright.
run: cargo install cargo-deb --locked --version 3.7.0

- name: Build
run: |
SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct HEAD) \
cross build --workspace --release --target "${{ matrix.target }}"

# Tarball carries what an operator needs to run it off a non-Debian host:
# the binary, the annotated reference config, the licence, and the docs.
- name: Stage tarball
run: |
VERSION="$(cat VERSION)"
STAGE="filterframe-${VERSION}-${{ matrix.target }}"
mkdir -p "dist/$STAGE"
cp "target/${{ matrix.target }}/release/filterframe" "dist/$STAGE/"
cp conf/example.conf LICENSE README.md VERSION "dist/$STAGE/"
tar -C dist --sort=name --owner=0 --group=0 --numeric-owner \
--mtime="@$(git log -1 --pretty=%ct HEAD)" \
-czf "dist/$STAGE.tar.gz" "$STAGE"
rm -rf "dist/$STAGE"
( cd dist && sha256sum "$STAGE.tar.gz" > "$STAGE.tar.gz.sha256" )

- name: Build .deb
if: matrix.deb
run: |
# Explicit output path rather than globbing target/: a .deb from an
# earlier build can survive there, and copying both would put two
# packages claiming the same version into one release.
DEB="dist/filterframe_$(cat VERSION)_${{ matrix.arch }}.deb"
SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct HEAD) \
cargo deb -p filterframe-cli --target "${{ matrix.target }}" \
--no-build --no-strip \
--output "$DEB"
( cd dist && for f in *.deb; do sha256sum "$f" > "$f.sha256"; done )

- uses: actions/upload-artifact@v7
with:
name: dist-${{ matrix.target }}
path: dist/*
if-no-files-found: error

publish:
name: publish
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
with:
path: staging

- name: Collect artifacts
run: |
mkdir -p dist
find staging -type f -exec cp {} dist/ \;
# One SHA256SUMS covering everything, rather than making a verifier
# fetch a per-file .sha256 for each artifact they downloaded.
( cd dist && cat *.sha256 | sort -k2 > SHA256SUMS && rm -f *.sha256 )
ls -la dist

# Signing is optional so that a fork without the secret still produces a
# complete, verifiable release rather than failing at the last step.
- name: Sign SHA256SUMS
if: env.GPG_PRIVATE_KEY != ''
env:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
run: |
echo "$GPG_PRIVATE_KEY" | gpg --batch --import
gpg --batch --yes --detach-sign --armor dist/SHA256SUMS

- name: Publish
if: startsWith(github.ref, 'refs/tags/v') && inputs.dry_run != true
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="${GITHUB_REF#refs/tags/}"
# A tag carrying a hyphen is a pre-release by construction: v0.1.0-rc1.
PRERELEASE=""
case "$TAG" in *-*) PRERELEASE="--prerelease" ;; esac
gh release create "$TAG" dist/* --generate-notes $PRERELEASE

- name: Dry run summary
if: inputs.dry_run == true
run: |
echo "Dry run — the following would have been published:" >> "$GITHUB_STEP_SUMMARY"
( cd dist && ls -la ) >> "$GITHUB_STEP_SUMMARY"
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/target
/Cargo.lock.bak
**/*.rs.bk

# Working documents that are not part of the distributed source.
/SPEC.md
/plans/
.claude/

# Never commit a bearer token, however it got here.
*.token
/conf/*.token
Loading