This repository has no description
README.md

Reroute #

A small, auditable browser extension for client-side URL rewrites in Firefox and Chrome. One codebase, no dependencies, no build step, no analytics, and no network access except an optional per-rule health probe against the instances you configure (see Health-checked pools).

Ships with editable presets:

What Example
Twitter / X → nitter pool (rule) https://x.com/user/status/1 → a live instance from an 8-front-end pool (xcancel.com, nitter.net, …)
Bluesky → impro.social (rule) https://bsky.app/profile/foo → https://impro.social/profile/foo
Reddit image links load directly (rule) opening https://i.redd.it/abc.jpeg shows the image, not Reddit's JS viewer
Reddit /media viewer skipped (rule) https://www.reddit.com/media?url=… → the decoded image URL
Reddit preview → original image (rule, off by default) https://preview.redd.it/abc.jpeg?width=… → https://i.redd.it/abc.jpeg

You can disable, edit, delete or reorder any rule, and add your own rules of any of the four types below.

The Reddit behaviors (now just ordinary rules) #

Reroute 1.2.0 turned the two Reddit-specific hacks of earlier versions into user-configurable rule types, so the exact same behavior works for any website. The Reddit rules are simply seeded defaults you can edit or delete.

Header fix (rule type accept). Reddit's image hosts (i.redd.it, preview.redd.it, cf.preview.redd.it, external-preview.redd.it) return an HTML page with a JavaScript viewer when the browser navigates to them, because the navigation's Accept header prefers text/html. The same server returns the raw image when asked for an image. The seeded "Reddit image links load directly" rule is a declarative modifyHeaders DNR rule that sets the Accept header of top-level navigations to those four hosts to image/avif,image/webp,*/*;q=0.8 — the URL never changes, Reddit's own server just serves the file. This is the same approach as load-reddit-images-directly, reimplemented on Manifest V3 / declarativeNetRequest so it works identically in Firefox and Chrome. Being a normal rule, you can point it at any host.

?url= redirect (rule type param-redirect). Reddit links images through www.reddit.com/media?url=<percent-encoded URL>. Declarative rules can't URL-decode, so this is implemented by a content script. The seeded "Skip the reddit.com/media viewer" rule matches *://*.reddit.com/media*, decodes the url parameter, and location.replace()s to it before the page renders. For safety it only redirects to hosts on the rule's allowlist (*.redd.it, *.redditmedia.com). As a normal rule, you can point it at any site that uses ?url=-style links (e.g. a non-Reddit image viewer).

How it works (and why you can trust it) #

The extension uses the browsers' declarativeNetRequest API: DNR rules are compiled once into declarative redirect rules that the browser's own network layer applies before a request is sent. param-redirect rules (which need to decode a query parameter, which DNR can't) use a single dynamically-registered content script. Consequences:

  • For URL rewrites, the original site is never contacted — the redirect happens pre-flight.
  • Extension code never sees, touches, or logs your traffic. There are no webRequest listeners.
  • The one content script (param-redirect.js) is registered only for the union of your enabled param-redirect rules' match patterns — it never runs on every page. It decodes the parameter, checks the destination against the rule's allowlist, and only then redirects. If anything fails the check it does nothing and the page renders normally.
  • Health probes (see below) only ever hit the root of an instance you configured, only when a health-checked pool rule matches, and only from the local go.html page. Nothing about the source or target URL is sent with a probe, and nothing is logged.
  • The only JavaScript that runs is: (a) background.js, which recompiles rules whenever you edit them, (b) param-redirect.js on URLs you configured, (c) go.js/probe.js on the local rotor page for pool rules, and (d) the options/popup pages while open.
  • Rules are stored in storage.local on your machine. Nothing syncs or phones home. The whole thing is a handful of small files you can read in ten minutes.

host_permissions: <all_urls> is required by Chrome for redirect actions — it grants the ability to redirect any URL your rules match, not the ability to read pages. It also lets the local go.html page probe instances you configured in a health-checked pool.

Auditability. Health checking is the one place the extension makes a network request, and it is fully opt-in: it is off by default, only applies to rules with 2+ destinations, only fires when one of those rules matches, is bounded to at most 2 probes per redirect, and only contacts the instance root (never the source or target URL). Results are cached locally for a few minutes so a healthy pool isn't re-probed on every visit. The extension collects nothing itself — the instances see ordinary requests, exactly what a normal visit to them would produce.

Install #

Firefox (temporary, for testing) #

  1. Open about:debugging#/runtime/this-firefox
  2. Click Load Temporary Add-on… and select manifest.json (or the .zip).

Temporary add-ons are removed when Firefox closes. For a permanent unsigned install use Firefox Developer Edition or ESR: set xpinstall.signatures.required to false in about:config, then install the zip via about:addons → gear icon → Install Add-On From File…. (Regular release Firefox requires the add-on to be signed; you can self-sign for free by uploading the zip to addons.mozilla.org as an unlisted add-on.)

Note for reinstalls: the add-on ID changed in 1.2.0 to reroute@7778777.online (the localbuild ID only applied to temporary installs). Rules are tied to the add-on ID, so export your rules before reinstalling and import them after.

Chrome / Chromium / Edge / Brave #

  1. Unzip the folder somewhere permanent.
  2. Open chrome://extensions, enable Developer mode (top right).
  3. Click Load unpacked and select the folder.

Rule types #

Domain swap — replace the host, keep everything else. List one or more source domains (subdomains are matched automatically) and one or more destination hosts. The scheme is forced to https.

Giving a domain swap several destinations turns it into a round-robin pool: each visit uses the next host in the list, e.g. twitter.com, x.com → xcancel.com, nitter.net. Because a declarativeNetRequest rule is static (one fixed destination), pool rules redirect through the extension's own local go.html page, which picks the next instance (per-rule cursor in storage.local) and immediately location.replace()s — an imperceptible local hop, no extra permissions, and still nothing observing your traffic. Single-destination rules keep the direct zero-hop transform. Pool bookkeeping details: the cursor lives under a separate storage key so rotating never triggers a rule recompile; on any failure go.html shows the original URL instead of auto-navigating, which is what makes redirect loops impossible; and pools apply to page navigations only (an extension page can't stand in for an embedded image or script). Editing the pool is just editing the comma-separated list — remove a dead instance and the rotation instantly adjusts.

Health-checked pools #

A host rule with 2+ destinations can turn on healthCheck (the "Health-check instances before redirecting" toggle in the rule editor). With it on, the rotor probes the next candidate before redirecting:

  • A probe is a plain GET to the instance root (https://<host>/) with a 4-second timeout. 2xx/3xx means up; a network error, timeout, 4xx or 5xx means down. The source/target URL is never part of a probe — only the rewritten URL is passed to the subsequent location.replace(), exactly like clicking a normal link.
  • Results are cached in storage.local: an up result is trusted for ~10 minutes; a down result gets a ~30-minute cooldown before it can be probed again, so a dead instance isn't hammered on every visit.
  • Probing is bounded: at most 2 probes per redirect. The rotor tries the cursor's candidate, then the next one; once the budget is spent it stops.
  • If every candidate is down (or the budget is spent with no healthy host), go.html shows the original URL and does not auto-navigate — the same loop-free fallback as any other failure.

healthCheck is off by default and only applies to pools; a single destination ignores the flag. The seeded Twitter preset ships with it on, because nitter instances come and go. Because probes are opt-in, per-rule, and bounded, the default "no network access" story is unchanged for rules that don't opt in.

Why direct probing instead of a status service. We evaluated the public nitter status API at https://status.d420.de/api/v1/instances (it is alive and current — it returns a {hosts:[…]} list with per-instance healthy flags and 15-minute check history). It is a useful snapshot, but we deliberately chose direct probing as the only source: a central status service is a third party that would see every instance query the extension makes, its view can be stale or from a different network than yours, and a healthy global status doesn't guarantee the instance is reachable from your machine. Direct probing is more accurate (you test what you'll actually load) and more private (the only observers are the instances you configured, seeing requests like any normal visit). A status service was not wired in, and is not a dependency.

Regex rewrite — a regular expression matched against the full URL (case-insensitive), plus a substitution template where \1–\9 insert capture groups and \0 inserts the whole match. Patterns must be RE2-compatible (no lookaheads or backreferences); the browser validates each pattern and the options page shows exactly why a rule was rejected, without silently disabling the others.

Header fix (accept) — set a request header on navigations to a list of hosts (subdomains matched automatically). The default, an image-friendly Accept header, makes servers that otherwise serve a JS viewer return the raw file. The URL is unchanged; the server just answers with what you asked for. Defaults to page navigations only.

?url= redirect (param-redirect) — for sites that route links through a viewer page carrying a percent-encoded destination in a query parameter. Give a content-script match pattern (e.g. *://*.reddit.com/media*), the parameter name (default url), and an allowlist of destination hosts. The script decodes the parameter and, only if the destination is an http/https URL on an allowed host, goes straight to it. Anything else is left alone. The allowed-host check matches on label boundaries, so *.redd.it matches i.redd.it but not evilredd.it — this stays airtight because it's the redirect primitive.

By default DNR rules apply to page navigations only (main_frame), so a rewrite of an image host can't break sites that embed content from it. Each rule has a checkbox to also cover embedded requests (iframes, images, scripts, XHR) if you want that. (?url= redirects follow the page's own links, so the checkbox doesn't apply to them.)

First matching rule wins, top to bottom — use ↑/↓ to reorder.

Extras #

  • Try a URL box in options: paste any URL and see which rule fires and what it becomes (including, for ?url= rules, whether the decoded destination passes the allowed-host check), before you rely on it.
  • Export / Import rules as JSON (do this before uninstalling — the browser deletes extension storage on uninstall).
  • Toolbar popup with a global kill switch and per-rule toggles.

Notes on the presets #

  • The Reddit preview → original rule (off by default) additionally maps preview.redd.it/<file> to i.redd.it/<file>, which serves the un-resized original. A small number of previews (converted GIFs, external previews) have no original on i.redd.it and may 404 — that's why it's opt-in; the Accept-header rule already makes preview URLs display fine as-is.
  • Nitter instances come and go, so the Twitter preset is a health-checked round-robin pool of 8 front-ends. The rotor probes each candidate before redirecting and skips dead ones, so you don't land on a broken instance. You can still edit the list or turn health checking off per rule.

File map #

manifest.json    permissions + cross-browser wiring (MV3)
background.js    storage → DNR rules + registers the param-redirect script
common.js        shared data + migration + pure security/test helpers
probe.js         pure health-check pool logic + the fetch wrapper (testable)
param-redirect.js  the one content script (?url= rules), registered on demand
go.html/.js      local round-robin rotor for pool rules (health-checked)
options.html/.css/.js   rule editor, import/export, URL tester, health-check UI
popup.html/.js   toolbar quick toggles
tests/           node --test suite for the security validator + probe logic
icons/           generated PNGs

Tests #

The security-critical param-redirect validation (allowlist suffix matching, scheme checks, decode/parse edge cases), the v1→v2 migration, and the health-check pool logic (candidate selection with a cursor, cache TTL + down-cooldown, bounded probe count, all-down fallback) are covered by a zero-dependency node:test suite. The fetch itself is a thin wrapper around the pure logic and is not exercised in the test environment:

node --test tests/*.test.js

License: MIT. Do whatever you like with it.