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
113 changes: 73 additions & 40 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ concurrency:
permissions:
contents: read

env:
# cargo-generate-rpm 0.21.0 has prebuilts for both x86_64 and aarch64.
PACKAGING_TOOLS: cargo-deb@3.8.0,cargo-generate-rpm@0.21.0

jobs:
clippy_check:
name: Run clippy check
Expand All @@ -34,15 +38,10 @@ jobs:
components: clippy

- name: Cache cargo + target
uses: actions/cache@v4
uses: Swatinem/rust-cache@v2
with:
key: clippy-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
clippy-${{ runner.os }}-
path: |
~/.cargo/registry
~/.cargo/git
./target
workspaces: . -> target
key: clippy

# Frontend dist/ must exist for the server's include_dir! to compile.
# Reuses the artifact produced by the `build_frontend` job.
Expand All @@ -58,6 +57,7 @@ jobs:

rustfmt:
name: Check style
needs: [image_meta]
runs-on: ubuntu-latest
steps:
- name: Checkout
Expand All @@ -69,17 +69,37 @@ jobs:
components: rustfmt

- name: Run cargo fmt
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
run: cargo fmt --all -- --check
image_meta:
name: Compute image tag
name: Validate version and compute image tag
runs-on: ubuntu-latest
outputs:
image_tag: ${{ steps.vars.outputs.image_tag }}
image_name: ${{ steps.vars.outputs.image_name }}
package_version: ${{ steps.version.outputs.package_version }}
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Validate release version
id: version
run: |
python3 - <<'PY'
import os
import tomllib

with open("privaxy/Cargo.toml", "rb") as manifest:
version = tomllib.load(manifest)["package"]["version"]
ref = os.environ["GITHUB_REF"]
if ref.startswith("refs/tags/v"):
tag_version = ref.removeprefix("refs/tags/v")
if tag_version != version:
print(f"::error::Release tag v{tag_version} does not match privaxy/Cargo.toml version {version}")
raise SystemExit(1)
with open(os.environ["GITHUB_OUTPUT"], "a") as output:
print(f"package_version={version}", file=output)
PY

- name: Get image tag
id: vars
run: |
Expand Down Expand Up @@ -135,6 +155,7 @@ jobs:
uses: docker/setup-buildx-action@v3

- name: Log in to GitHub Container Registry
if: github.event_name == 'push'
uses: docker/login-action@v3
with:
registry: ghcr.io
Expand All @@ -147,7 +168,7 @@ jobs:
with:
images: ${{ needs.image_meta.outputs.image_name }}

- name: Build and push by digest
- name: Build image
id: build
uses: docker/build-push-action@v6
with:
Expand All @@ -161,15 +182,18 @@ jobs:
annotations: ${{ steps.meta.outputs.annotations }}
cache-from: type=gha,scope=${{ matrix.cache_scope }}
cache-to: type=gha,mode=max,scope=${{ matrix.cache_scope }}
outputs: type=image,name=${{ needs.image_meta.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true
# PRs validate the image build without publishing to the registry.
outputs: ${{ github.event_name == 'push' && format('type=image,name={0},push-by-digest=true,name-canonical=true,push=true', needs.image_meta.outputs.image_name) || 'type=cacheonly' }}

- name: Export digest
if: github.event_name == 'push'
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"

- name: Upload digest
if: github.event_name == 'push'
uses: actions/upload-artifact@v4
with:
name: image-digest-${{ matrix.cache_scope }}
Expand All @@ -180,6 +204,7 @@ jobs:
image_merge:
name: Merge multi-arch manifest
needs: [image_meta, image_build]
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down Expand Up @@ -237,7 +262,7 @@ jobs:
- name: Cache cargo + wasm target
uses: Swatinem/rust-cache@v2
with:
workspaces: web_frontend -> target
workspaces: . -> target
shared-key: build-frontend

- name: Install node
Expand Down Expand Up @@ -315,19 +340,17 @@ jobs:
- name: Checkout
uses: actions/checkout@v6

- name: Cache build artifacts
uses: actions/cache@v3
with:
key: ${{ matrix.os }}-${{ matrix.target }}-artifacts
path: |
./target
~/.cargo

- name: Install rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}

- name: Cache build artifacts
uses: Swatinem/rust-cache@v2
with:
workspaces: . -> target
key: native-${{ matrix.target }}

- name: Install cross build dependencies
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: sudo apt-get update && sudo apt-get install -y gcc-aarch64-linux-gnu libc6-dev-arm64-cross
Expand All @@ -352,14 +375,21 @@ jobs:
CC_x86_64_unknown_linux_musl: musl-gcc
CC_aarch64_unknown_linux_musl: musl-gcc
run: cargo build --release --target ${{ matrix.target }} --bin privaxy --target-dir target

- name: Install packaging tools
if: ${{ !endsWith(matrix.target, '-musl') }}
uses: taiki-e/install-action@v2
with:
tool: ${{ env.PACKAGING_TOOLS }}

- name: Build packages
if: ${{ !endsWith(matrix.target, '-musl') }}
run: |
# target/ is cached; discard packages from previous versions.
rm -f target/${{ matrix.target }}/release/privaxy_*.deb \
target/${{ matrix.target }}/release/privaxy-*.rpm
cargo install cargo-deb && cargo deb -p privaxy --target ${{ matrix.target }} -o target/${{ matrix.target }}/release
cargo install cargo-generate-rpm && cargo generate-rpm -p privaxy --target ${{ matrix.target }} -o target/${{ matrix.target }}/release
cargo deb --no-build -p privaxy --target ${{ matrix.target }} -o target/${{ matrix.target }}/release
cargo generate-rpm -p privaxy --target ${{ matrix.target }} -o target/${{ matrix.target }}/release
- uses: actions/upload-artifact@v4
if: ${{ !endsWith(matrix.target, '-musl') }}
with:
Expand Down Expand Up @@ -401,17 +431,15 @@ jobs:
- name: Checkout
uses: actions/checkout@v6

- name: Cache build artifacts
uses: actions/cache@v3
with:
key: ${{ matrix.os }}-${{ matrix.target }}-artifacts
path: |
./target
~/.cargo

- name: Install rust
uses: dtolnay/rust-toolchain@nightly

- name: Cache build artifacts
uses: Swatinem/rust-cache@v2
with:
workspaces: . -> target
key: mips-${{ matrix.target }}-${{ hashFiles('Cross.toml') }}

- name: Install cross
uses: taiki-e/install-action@v2
with:
Expand All @@ -432,15 +460,21 @@ jobs:
- name: Build server
run: cross build --release --target ${{ matrix.target }} --bin privaxy --target-dir target

- name: Install packaging tools
if: ${{ endsWith(matrix.target, '-linux-gnu') }}
uses: taiki-e/install-action@v2
with:
tool: ${{ env.PACKAGING_TOOLS }}

- name: Build packages
if: ${{ endsWith(matrix.target, '-linux-gnu') }}
run: |
# target/ is cached; discard packages from previous versions.
rm -f target/${{ matrix.target }}/release/privaxy_*.deb \
target/${{ matrix.target }}/release/privaxy-*.rpm
variant=$(echo "${{ matrix.target }}" | cut -d- -f1)
cargo install cargo-deb && cargo deb --no-build --no-strip --variant "$variant" -p privaxy --target ${{ matrix.target }} -o target/${{ matrix.target }}/release
cargo install cargo-generate-rpm && cargo generate-rpm -p privaxy --target ${{ matrix.target }} -o target/${{ matrix.target }}/release
cargo deb --no-build --no-strip --variant "$variant" -p privaxy --target ${{ matrix.target }} -o target/${{ matrix.target }}/release
cargo generate-rpm -p privaxy --target ${{ matrix.target }} -o target/${{ matrix.target }}/release

- uses: actions/upload-artifact@v4
if: ${{ endsWith(matrix.target, '-linux-gnu') }}
Expand All @@ -462,7 +496,7 @@ jobs:

release:
name: Create GitHub Release
needs: [ci, ci_mips, image_merge]
needs: [image_meta, ci, ci_mips, image_merge]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
permissions:
Expand All @@ -477,10 +511,9 @@ jobs:
path: ./artifacts
pattern: privaxy-*
merge-multiple: false
- name: Set package version from tag
run: |
echo "PKG_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Flatten artifacts for release
env:
PKG_VERSION: ${{ needs.image_meta.outputs.package_version }}
run: |
mkdir ./release-assets
for dir in ./artifacts/privaxy-*/; do
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ filters/__pycache__
node_modules
.vscode
**/target/*
testing/
testing/

# Local opt-in request history can contain browsing metadata.
statistics.redb
80 changes: 80 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,86 @@

## Unreleased

- Add a shared footer with the running server version linked to the project.
Check for stable releases automatically when opened and hourly thereafter,
showing an "Update available" release link only when a newer version is found.
Update checks are authenticated and time-limited, with successful checks cached
for one hour and failures for one minute.
- Added opt-in persistent statistics and individual request history using redb.
History includes time ranges, trends, top domains/clients, and pagination, with
privacy levels that remove identifiers before collection. URL paths, query
strings, headers, and bodies are never saved in statistics.
- Added Statistics settings for retention, a request-count limit, clearing saved
history, and excluding domain patterns from both statistics and the live
Requests log. Exclusions do not change filtering behavior and work even when
persistence is disabled.
- Bounded statistics buffering, batched transactional writes, visible storage
errors, and shutdown flushing keep storage failures off the proxy request path.
Existing installations retain memory-only statistics until explicitly enabled.
- Show saved statistics directly on the Dashboard when persistence is enabled.
With persistence off or Anonymous mode selected, keep the original live,
per-process dashboard. Remove the separate History tab and redirect old
`/history` bookmarks to `/`. Both layouts retain the blocking toggle and CA
certificate download.
- Live dashboard top blocked entries now use domains and select the top 50 after
sorting; client tracking is bounded.
- Count each modified HTML response once when both head scripts and cosmetic
styles are injected, instead of counting the two injection stages separately.
- Use TLS ClientHello SNI for IP-address CONNECT requests when choosing
interception certificates, applying hostname inclusions and exclusions, and
verifying upstream TLS. HTTP requests and WebSocket upgrades still connect to
the original CONNECT IP and port. Named CONNECT requests retain their existing
identity; DoH redirects and userscript fetches keep their own routing.
- Tunnel CONNECT traffic to port 853 without interception, preserving end-to-end
DNS-over-TLS. The DoH policy continues to apply only to HTTP traffic. Explicit
CONNECT-host/IP exclusions bypass protocol probing.
- Bound CONNECT protocol detection, ClientHello reading, and intercepted TLS
handshakes to 10 seconds each, with a 64 KiB ClientHello limit. Reject malformed,
truncated, oversized, or stalled handshakes while preserving fragmented TLS
records and buffered bytes, including when SNI selects a bypass tunnel.
- Increase the upstream HTTP/2 response header limit from 16 KiB to 64 KiB and
allow 64 KiB of headers on intercepted HTTP/2 requests. This fixes 502 stream
protocol errors from responses with large CSP or Set-Cookie headers.
- Advertise HTTP/2 and HTTP/1.1 through ALPN on intercepted TLS connections so
browsers can multiplex page resources. Enable adaptive HTTP/2 flow control on
those connections and TCP_NODELAY on accepted proxy sockets, tunnel sockets,
and upstream upgrade connections to reduce delays for small writes.
- Preserve known-empty request bodies so the upstream client can retry HTTP/2
REFUSED_STREAM and graceful GOAWAY failures. Nonempty or unknown-length bodies
continue to stream without buffering or replay.
- Include underlying DNS, TLS, socket, and HTTP/2 failure causes in proxy 502
pages and logs instead of only reporting "error sending request".
- Improve live statistics, Requests, and log streams with visible connection
status, automatic reconnection, and handling for malformed or unexpected
frames. Leaving a live view cancels its connection and reconnect attempts.
- Show retryable loading errors in settings, filter catalogs, API-key loading,
and userscript management. A failed sign-in status check now offers a retry
instead of incorrectly showing the login form.
- Fix settings save feedback: acknowledge only successfully submitted values,
preserve text drafts after failures and edits made during a save, and disable
unchanged Save buttons and duplicate submissions. Ignore stale responses from
previously visited text-settings pages, and preserve pending exclusion edits
without restoring previously removed hosts.
- Keep blocking controls at their confirmed state after failed updates, and
restore the confirmed Debug settings after failed automatic saves. Lock
Network and PAC fields while saving so later edits cannot be marked saved by
an earlier response.
- Refresh installed filter lists after additions and removals while preserving
pending selections. Reset catalog pagination when searching, bound Next to
matching results, and keep failed removals visible. Built-in lists cannot be
removed through the catalog.
- Make navigation, settings forms, and dialogs fit phone screens. Improve form
labels and error/status announcements, and wrap notification text instead of
truncating it.
- Include [RELEASE.md](RELEASE.md) in generated GitHub release notes and discard cached
Debian/RPM packages before packaging to avoid publishing stale versions.
- Keep Docker builds on pull requests while restricting image and manifest
publishing to pushes. Validate release tags against the server package version
before building, and use that version in release artifact names.
- Correct the frontend cache path and key Rust caches by toolchain, dependencies,
and target. Use pinned prebuilt packaging tools and reuse compiled binaries
when creating Debian packages.

## v0.8.0 - 2026-10-02

Release highlights and upgrade notes are in [RELEASE.md](RELEASE.md).
Expand Down
Loading
Loading