diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa966e37..0a8a6e51 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,9 +4,13 @@ on: push: branches: - main + - develop + tags: + - 'v*' pull_request: branches: - - main + - main + - develop concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -23,7 +27,7 @@ jobs: continue-on-error: true steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@stable with: @@ -57,7 +61,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v6 - name: Install rust uses: dtolnay/rust-toolchain@stable @@ -81,9 +85,11 @@ jobs: run: | rel="${GITHUB_REF#refs/*/}" if grep -qE '^v?\d+\.\d+\.\d+' <<< "$rel" ; then - tag="$rel" - elif [ "$rel" = "main" ]; then + tag="${rel#v}" + elif [ "$rel" = "develop" ]; then tag="dev" + elif [ "$rel" = "main" ]; then + tag="latest" else tag="$(echo "$GITHUB_SHA" | cut -c1-7)" fi @@ -112,7 +118,7 @@ jobs: cache_scope: docker-arm64 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Download prebuilt binary uses: actions/download-artifact@v4 @@ -221,7 +227,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install rust uses: dtolnay/rust-toolchain@stable @@ -307,7 +313,7 @@ jobs: target: aarch64-unknown-linux-musl steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v6 - name: Cache build artifacts uses: actions/cache@v3 @@ -342,14 +348,10 @@ jobs: path: privaxy/prebuilt - name: Build server - uses: actions-rs/cargo@v1 env: CC_x86_64_unknown_linux_musl: musl-gcc CC_aarch64_unknown_linux_musl: musl-gcc - with: - command: build - working-directory: . - args: --release --target ${{ matrix.target }} --bin privaxy --target-dir target + run: cargo build --release --target ${{ matrix.target }} --bin privaxy --target-dir target - name: Build packages if: ${{ !endsWith(matrix.target, '-musl') }} run: | @@ -386,9 +388,15 @@ jobs: - build: linux os: ubuntu-22.04 target: mipsel-unknown-linux-musl + - build: linux + os: ubuntu-22.04 + target: mips-unknown-linux-musl + - build: linux + os: ubuntu-22.04 + target: mips-unknown-linux-gnu steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Cache build artifacts uses: actions/cache@v3 @@ -422,19 +430,20 @@ jobs: run: cross build --release --target ${{ matrix.target }} --bin privaxy --target-dir target - name: Build packages - if: matrix.target == 'mipsel-unknown-linux-gnu' + if: ${{ endsWith(matrix.target, '-linux-gnu') }} run: | - cargo install cargo-deb && cargo deb --no-build --no-strip --variant mipsel -p privaxy --target ${{ matrix.target }} -o target/${{ matrix.target }}/release + 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 - uses: actions/upload-artifact@v4 - if: matrix.target == 'mipsel-unknown-linux-gnu' + if: ${{ endsWith(matrix.target, '-linux-gnu') }} with: name: privaxy-deb-${{ matrix.target }} path: target/${{ matrix.target }}/release/privaxy_*.deb - uses: actions/upload-artifact@v4 - if: matrix.target == 'mipsel-unknown-linux-gnu' + if: ${{ endsWith(matrix.target, '-linux-gnu') }} with: name: privaxy-rpm-${{ matrix.target }} path: target/${{ matrix.target }}/release/*.rpm @@ -444,3 +453,37 @@ jobs: name: privaxy-${{ matrix.target }} path: | target/${{ matrix.target }}/release/privaxy + + release: + name: Create GitHub Release + needs: [ci, ci_mips, image_merge] + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') + permissions: + contents: write + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + 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 + run: | + mkdir ./release-assets + for dir in ./artifacts/privaxy-*/; do + target=$(basename "$dir" | sed 's/^privaxy-//') + cp "$dir/privaxy" "./release-assets/privaxy-${PKG_VERSION}-${target}" + done + find ./artifacts -name '*.deb' -exec cp {} ./release-assets/ \; + find ./artifacts -name '*.rpm' -exec cp {} ./release-assets/ \; + + - name: Create release + uses: softprops/action-gh-release@v2 + with: + files: ./release-assets/* + generate_release_notes: true + fail_on_unmatched_files: true \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 309c0226..41320fa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,38 @@ # Changelog -## Unreleased +## v0.7.1 + +- Fix WebSocket / protocol-upgrade connections hanging + - Upgrade requests (`wss://`, and proprietary HTTP-upgrade transports like MMTLS long-link) could spin forever with `upgrade expected but not completed`. The proxy fabricated a `101 Switching Protocols` to the client regardless of what the upstream actually returned, so a failed upgrade left the client waiting on a tunnel that was never bridged. It now forwards the upstream's real response when it isn't a genuine `101`, and the dedicated upgrade HTTP client gained the same connection hardening (`connect_timeout`, `tcp_keepalive`, no idle pooling) as the main client. + - Excluded hosts performing a plain-HTTP protocol upgrade are now blind-tunneled at the TCP level instead of being run through the (HTTP-only) upgrade bridge, so MITM-excluded apps using non-HTTP upgrade protocols work. Previously the exclusion list was only consulted on the `CONNECT` path, so excluding such a host had no effect on its plain-HTTP upgrade traffic. + - When an opaque (non-WebSocket) upgrade is seen for a host that is *not* excluded, a warning is logged naming the host and suggesting it be added to the exclusions, instead of failing cryptically. +- Validate filter lists when added + - Adding a filter now rejects URLs that do not serve a `text/plain` filter list (e.g. an HTML error/landing page returned with a `200`) with a `422`, instead of silently saving a broken filter. The error is surfaced in the web UI, and filters whose URL stops serving a list are dropped from the engine with a warning on the next refresh. +- Fix proxied requests randomly hanging/timing out + - The outbound HTTP client had no connection timeouts, so a pooled keep-alive connection silently dropped by the remote would be reused and block until the OS TCP timeout (minutes). Added `connect_timeout`, `pool_idle_timeout`, and `tcp_keepalive`. +- DNS-over-HTTPS (DoH) interception + - Detects DoH requests passing through the MITM proxy (RFC 8484 `application/dns-message`, JSON DoH, and known resolver endpoints) + - `block` mode (default) refuses DoH so fallback-mode clients (e.g. default Firefox) revert to the system resolver, which Privaxy already sees — the HTTP-layer equivalent of the `use-application-dns.net` canary a non-DNS proxy cannot serve + - `redirect` mode transparently forwards queries to a configured `upstream` resolver + - Configured under `[network.doh]` (`mode`, `upstream`, `extra_hosts`) or from the web UI under Settings → General; MITM-excluded hosts are left untouched +- Fix cookie not invalidating upon logout/cred change +- All four engine-matching call sites now use match_url (canonical, default port stripped); the outbound request and stats still use the raw uri with its port, so nothing about proxying changes. This was silently breaking every hostname-anchored (||host/path) network rule on every HTTPS site +- Update ublock annoyances url +- Add support for MIPS, MIPSLE +- Injected uBlock scriptlets now actually run + - Even after the 0.7.0 scriptlet repair, every injected `##+js(...)` scriptlet was a silent no-op. adblock-rust emits scriptlet bodies that reference an ambient `scriptletGlobals` object (uBlock Origin supplies it in its own injector; adblock-rust leaves it to the embedder), so the first internal call threw `ReferenceError: scriptletGlobals is not defined`, which each scriptlet's own `try/catch` swallowed. Privaxy now defines `scriptletGlobals` at the top of the injected payload, so `abort-current-script`, `prevent-addEventListener`, `abort-on-property-read`, `set-cookie`, etc. take effect. +- Procedural cosmetic filtering + - Non-CSS procedural filters are no longer dropped (previously only filters reducible to plain CSS were applied). `:has-text`, `:matches-css`/`-before`/`-after`, `:matches-attr`, `:matches-path`, `:min-text-length`, `:upward`, `:xpath`, and the `:remove()`/`:style()`/`remove-attr`/`remove-class` actions are now evaluated in-page by an injected shim. + - The shim re-runs on DOM mutations and recurses into same-origin child frames (`about:blank`/`srcdoc`/`data:` with `allow-same-origin`), so ad content written into such frames after load is also matched. Cross-origin frames and closed shadow DOM remain out of reach. +- Scriptlet error logging (debugging) + - New opt-in `debug.scriptlet_console_logging` (off by default), toggleable from Settings → Debug, surfaces errors thrown by injected scriptlets in the page console as `[privaxy scriptlet]` entries instead of swallowing them. +- Live log streaming in the web UI + - Settings → Debug now shows the server's log output in real time + - The level can be changed in the webui +- Fix cosmetic "modified responses" statistic undercount + - Pages where only element-hiding (`display: none`) selectors were injected were not counted as modified; any injected cosmetic CSS now counts + +## v0.7.0 - Built-in authentication for the web UI and API - First-run setup page for choosing an admin username + password @@ -19,6 +51,7 @@ - Inject into CSP-protected websites - Add docker compose example + ## v0.6.0 - Remove gui app diff --git a/Cargo.lock b/Cargo.lock index ae6e34ae..14db423b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2037,7 +2037,7 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "privaxy" -version = "0.7.0" +version = "0.7.1" dependencies = [ "adblock", "argon2", diff --git a/Cross.toml b/Cross.toml index 034b5e75..e01eb019 100644 --- a/Cross.toml +++ b/Cross.toml @@ -1,5 +1,15 @@ [target.mipsel-unknown-linux-gnu] build-std = true +[target.mips-unknown-linux-gnu] +build-std = true + [target.mipsel-unknown-linux-musl] build-std = true +[target.mipsel-unknown-linux-musl.env] +RUSTFLAGS = "-C target-feature=+crt-static" + +[target.mips-unknown-linux-musl] +build-std = true +[target.mips-unknown-linux-musl.env] +RUSTFLAGS = "-C target-feature=+crt-static" diff --git a/Dockerfile b/Dockerfile index 0507d0cb..422bca3b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,12 +39,13 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \ && cargo build --release; rm src/main.rs COPY . . +ARG COMPILE_MODE="release" RUN --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/app/target \ --mount=type=cache,target=/root/.npm \ - cd web_frontend && trunk build --release \ - && cd .. && cargo build --release \ - && cp target/release/privaxy /privaxy-out \ + cd web_frontend && trunk build --${COMPILE_MODE} \ + && cd .. && cargo build --${COMPILE_MODE} \ + && cp target/${COMPILE_MODE}/privaxy /privaxy-out \ && chmod +x /privaxy-out # Prebuilt path: expect $PREBUILT_BINARY to exist in the build context. diff --git a/README.md b/README.md index 2584bf48..653987d0 100644 --- a/README.md +++ b/README.md @@ -8,35 +8,64 @@

-**Forked from the [app version](https://github.com/Barre/privaxy/tree/v0.5.2)** - -This reverts it back to [v0.3.1](https://github.com/Barre/privaxy/tree/v0.3.1), but with -newer updates, an improved UI, and server-friendly configuration. To skip to the differences, -[see here](#differences) - -See features [here](#features) - - -
-dashboard -requests -filters -filterlists -general -addfilter -
+**A fork of [privaxy](https://github.com/Barre/privaxy)** + +This reverts it back to [v0.3.1](https://github.com/Barre/privaxy/tree/v0.3.1), adding more +features, dependency updates, an improved UI, and server-friendly configuration options. + + +## Table of Contents + +- [About](#about) + - [Compared to DNS-based blockers (Pi-hole, AdGuard Home)](#compared-to-dns-based-blockers-pi-hole-adguard-home) +- [Features](#features) +- [Installation](#installation) + - [Debian/Ubuntu](#debianubuntu) + - [RHEL/Fedora/Rocky](#rhelfedorarocky) + - [MIPS](#mips) + - [Docker](#docker) + - [From source](#from-source) + - [Docker Compose](#docker-compose) +- [Setup](#setup) + - [First-run web UI](#1-first-run-web-ui) + - [Install the root CA on your client devices](#2-install-the-root-ca-on-your-client-devices) + - [Point clients at the proxy](#3-point-clients-at-the-proxy) + - [Cert-pinned hosts (exclusions)](#4-cert-pinned-hosts-exclusions) +- [Screenshots](#screenshots) +- [Acknowledgements](#acknowledgements) ## About Privaxy is a MITM HTTP(s) proxy that sits in between HTTP(s) talking applications, such as a web browser and HTTP servers, such as those serving websites. +**This app sees all your plaintext, run it on hardware you trust. DO NOT FACE THIS PUBLICLY** + By establishing a two-way tunnel between both ends, Privaxy is able to block network requests based on URL patterns and to inject scripts as well as styles into HTML documents. Operating at a lower level, Privaxy is both more efficient as well as more streamlined than browser add-on-based blockers. A single instance of Privaxy on a small virtual machine, server or even, on the same computer as the traffic is originating from, can filter thousands of requests per second while requiring a very small amount of memory. Privaxy is not limited by the browser’s APIs and can operate with any HTTP traffic, not only the traffic flowing from web browsers. -Privaxy is also way more capable than DNS-based blockers as it is able to operate directly on URLs and to inject resources into web pages. +### Compared to DNS-based blockers (Pi-hole, AdGuard Home) + +DNS sinkholes can only answer "is this whole domain allowed or not?". Because +Privaxy works at the HTTP layer instead of the DNS layer, it can do things a +DNS blocker fundamentally cannot: + +- **Block by full URL, not just domain** — individual paths and query strings + can be blocked, so first-party and same-domain ads/trackers (served off a + domain you otherwise need) are reachable targets. +- **Cosmetic filtering** — hide page elements and inject uBlock Origin-style + scriptlets, instead of leaving broken gaps where a blocked domain used to be. +- **Intercept DNS-over-HTTPS (DoH)** — DoH is the mechanism that routinely + bypasses DNS-level blockers; Privaxy sees it as HTTPS and can block or + redirect it. + +The trade-off is that Privaxy is a MITM proxy: clients must trust its root CA +and route traffic through it, and it sees plaintext. It **complements** a DNS +blocker more than it strictly replaces one. + +**Upon initial setup, a lot of your websites/apps will break due to cert pinning. This is a one time occurrence, add websites/endpoints to exlcusions as broken websites are encountered** ## Features @@ -48,6 +77,7 @@ Privaxy is also way more capable than DNS-based blockers as it is able to operat - Browser and HTTP client agnostic. - Support for custom filters. - Support for excluding hosts from the MITM pipeline. +- DNS-over-HTTPS (DoH) interception — `block` (default) clients' DoH so they fall back to the system resolver, or `redirect` queries to a resolver you configure. Closes the DoH bypass that defeats DNS-level blockers. - Support for protocol upgrades, such as with websockets. - Automatic filter lists updates. - Very low resource usage. @@ -75,7 +105,17 @@ Download and install the deb/rpm/binary with mips in the name ### Docker -`docker run -d --name privaxy --restart unless-stopped -p 8100:8100 -p 8200:8200 -v /path/to/conf:/conf privaxy:ghcr.io/joshrmcdaniel/privaxy:dev` +```sh +docker run -d --name privaxy --restart unless-stopped \ + -p 8100:8100 -p 8200:8200 \ + -v /path/to/conf:/conf \ + ghcr.io/joshrmcdaniel/privaxy: +``` + +- `dev` is mapped to the develop branch +- `latest` is mapped to the main branch +- `` maps to official releases +- `` maps to a specific commit ### From source @@ -104,7 +144,7 @@ Build requirements: ```yaml services: privaxy: - image: ghcr.io/joshrmcdaniel/privaxy:dev + image: ghcr.io/joshrmcdaniel/privaxy ports: - "8100:8100" - "8200:8200" @@ -113,6 +153,13 @@ services: restart: unless-stopped ``` +Tags: + +- `dev` is mapped to the develop branch +- `latest` is mapped to the main branch +- `` maps to official releases +- `` maps to a specific commit + ## Setup ### 1. First-run web UI @@ -192,5 +239,26 @@ to the recommended list. > `password_hash` value from the config file and restart. The web UI will > force the setup flow again. -### Future -- Add DNS resolutions; incoporate DNS level blocking? +## Screenshots + +
+dashboard +requests +filters +filterlists +general +addfilter +
+ +## Acknowledgements + +Privaxy was originally created by [Pierre Barre](https://github.com/Barre) +([Barre/privaxy](https://github.com/Barre/privaxy)). This fork stands on top of +that work, full credit for the original design and implementation goes to him. + +Thanks also to: + +- [uBlock Origin](https://github.com/gorhill/uBlock) and [Raymond Hill](https://github.com/gorhill). + Privaxy bundles uBlock Origin's scriptlets and web-accessible resources for filter compatibility. +- [filterlists.com](https://filterlists.com) — for the filter-list directory + that powers in-app filter discovery. diff --git a/filterlists-api/src/lib.rs b/filterlists-api/src/lib.rs index 9c068d7d..5d25379b 100644 --- a/filterlists-api/src/lib.rs +++ b/filterlists-api/src/lib.rs @@ -20,7 +20,7 @@ pub async fn get_filters() -> Result, FilterListError> { pub async fn get_filter_information(filter: FilterArgs) -> Result { let id = match filter { FilterArgs::U32(id) => id, - FilterArgs::Filter(filter) => filter.id.clone(), + FilterArgs::Filter(filter) => filter.id, }; _get::(&format!("{FILTERLISTS_API_URL}/lists/{id}")).await } diff --git a/privaxy/Cargo.toml b/privaxy/Cargo.toml index 1676b854..d183e25c 100644 --- a/privaxy/Cargo.toml +++ b/privaxy/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "privaxy" description = "Next generation tracker and advertisement blocker" -version = "0.7.0" +version = "0.7.1" edition = "2021" authors = [ "Pierre Barre ", @@ -22,6 +22,10 @@ assets = [ name = "privaxy" depends = "libc6, libgcc-s1, libatomic1" +[package.metadata.deb.variants.mips] +name = "privaxy" +depends = "libc6, libgcc-s1, libatomic1" + [package.metadata.generate-rpm] license = "AGPL-3.0-or-later" summary = "Next generation tracker and advertisement blocker" diff --git a/privaxy/src/resources/procedural_cosmetics.js b/privaxy/src/resources/procedural_cosmetics.js new file mode 100644 index 00000000..5be040f7 --- /dev/null +++ b/privaxy/src/resources/procedural_cosmetics.js @@ -0,0 +1,328 @@ +/* + * Privaxy in-page procedural cosmetic filtering shim. + * + * The proxy can apply plain-CSS cosmetic rules server-side by injecting a + *