ARMANOS

Feature

Tracker blocking and link cleaning

Every profile refuses requests to tracker hosts inside the network stack and cuts 40 tracking parameters out of the addresses you open. What was blocked is counted on your computer and stays there.
40
Tracking parameters stripped
4
Tracker lists shipped
3
Rule sets in the profile extension
This computer only
Where the counters live

Blocking happens in the network stack, not inside the page

Every profile does the same two jobs. It refuses requests to tracker hosts, and it cuts tracking parameters out of the addresses you navigate to. How that is carried out depends on which engine the profile runs.

On ARMANOS Browser the app writes a small extension into the profile folder at launch and loads it with --load-extension. Its rules are declarativeNetRequest rules, so a match is decided inside the network stack. Nothing appears in the page for a site to notice, and no request travels back to the app for a verdict.

The built-in Electron engine has no such road, so the app does the work in its own process. A matching request is cancelled in session.webRequest.onBeforeRequest, and an address that can be cleaned is redirected to its clean form.

Both roads compile from one function, buildBlocker in tracker-rules.js. That is deliberate. Protection was originally written against Electron alone, so when ARMANOS Browser became the default engine it delivered the fingerprint and nothing else, and the Protection screen sat at zero forever.

A profile that cannot be protected does not open

If the protection extension cannot be written to disk, the launch is refused and the app says why. A window that looks normal but carries no blocking, no cleaning and no scenario scripts is worse than no window at all.

Four lists ship with the app, and the use case decides which apply

The general list holds 31 entries and goes to every use case. It is analytics, product measurement, crash reporting and session replay: google-analytics.com, googletagmanager.com, doubleclick.net, app-measurement.com, sentry.io, hotjar.com, fullstory.com, appsflyer.com and their neighbours.

The other three are use case specific and merge into the general one. A Privacy profile ends up with 31 unique entries, a Social profile with 74, an Ad accounts profile with 56. Nothing is loaded that the scenario did not ask for: an unknown list name is refused and named in the log instead of silently loading nothing.

The ads list is the most restrained on purpose. Every host an ad console needs to sign in, load, upload creatives, bill or answer its own API is deliberately absent, and so is every host that answers a top level navigation. A rule that fires inside the console you are paid to run is a business risk, not protection.

One entry can be matched three ways. A bare host matches that host and its subdomains. An entry containing a slash matches as a substring of the whole address. An entry starting with a double slash is anchored at the start of the authority, which is how log.pinterest.com is blocked without touching the real blog.pinterest.com.

ListEntriesGoes toWhat it holds
general31every use caseanalytics, crash reporting, session replay, mobile attribution
linkedin23Work, SocialLinkedIn ad and analytics hosts, the insight tag, tr.lnkd.in
social33SocialMeta, TikTok, X, Pinterest, Snapchat and Reddit pixels
ads25Ad accountsconversion and audience endpoints, data brokers

A platform watching you with its own requests needs path rules

A domain list catches other people's counters. It cannot catch the platform itself, because those calls go to the very site you came to, from its own address. For that there are separate sets of rules matched against the path.

The LinkedIn set covers /li/track, /px/, /li/fta, /csp/fta and the profile view endpoint. Two of its rules are query scoped: a messaging conversation call is dropped only when it carries seen or typing, and a search call only when it carries track, analytics or impression.

The social set covers Meta, X and Pinterest. Two Meta entries earn their place above all the others: facebook.com/ig_xsite_user_ and instagram.com/accounts/xuserid are the cross app identity handshake that ties a Facebook account to an Instagram one, and they fire on a plain page load of either site.

Every set is locked to its own hosts, and that is not tidiness. The LinkedIn set contains the bare words beacon, telemetry and /px/. Applied everywhere, a LinkedIn profile had an unrelated shop's /assets/beacon.js and a news site's /px/ image blocked with no explanation, so a scenario now gets only the rules that were tested against it.

  • LinkedIn set

    /li/track, /px/, /li/fta, /csp/fta, /voyager/api/identity/profileView, viewedEntity. Fires only on linkedin.com and licdn.com.

  • Social set

    /ajax/bz, /logging_client_events, /cavalry_endpoint.php, instagram.com/logging/, /i/jot, /1.1/attribution, pinterest.com/_/_/trace/. Fires only on those platforms' own hosts.

  • Ad consoles

    No bare path rules at all. Only host plus needle pairs, such as ads.google.com together with /gen_204, /jserror or /csi_204.

Two kinds of tracking no request filter can see

Two things are structurally out of reach for any network rule. navigator.sendBeacon fires on unload and the payload is the tracking itself. WebSocket frames are opaque to a filter that only reads addresses.

Where the use case asks for it, a script running in the page's own world replaces both. sendBeacon reports the event and returns true, so the page believes it succeeded. WebSocket.send drops payloads shorter than 500 characters that name TYPING, READ_RECEIPT or SEEN, so typing indicators and read receipts stop leaving while the rest of the socket carries on.

A replacement has to look like the built-in it replaced. The name and the length are copied from the original, and one shared toString hides all of them, including itself. Measured against the shipped engine, sendBeacon.length is 1 and not the 2 the specification implies, which is why the number is copied rather than written down.

The two halves talk over a CustomEvent on a random name chosen per launch. It used to be window.postMessage, which handed a payload naming the product to every page with an ordinary message listener, and let a page forge counts of its own.

  • Beacons

    sendBeacon is suppressed in the stealth scenarios and counted separately from network blocks.

  • Read receipts and typing

    the message privacy scenarios drop those frames; nothing else on the socket is touched.

  • Nothing names the product

    the page half and the extension half share a per launch random event name that a site cannot guess.

The counters are collected on your computer and stay there

On ARMANOS Browser the extension's service worker listens for rule matches, maps the ruleset that fired (block, telemetry or params) to a category, batches for 500 milliseconds and posts the batch to a loopback address on 127.0.0.1 with a random per profile token. It never leaves the machine.

Keeping that worker alive is what makes counting work at all. An MV3 worker is torn down between events, and matches are only observed while it runs. Measured, every counter came back zero for an hour of browsing while the blocking itself worked perfectly. A port held open by the top frame keeps the worker resident while a page is open.

The app treats those reports as local input rather than truth. Each batch is clamped, and only five categories are accepted. The numbers go into stats.json in the app data folder, keyed per day and per profile, with 14 days of history, and the file is written at most once a second so a busy page cannot turn counting into a performance problem.

The Protection screen shows today, the last 7 days, the total, a breakdown by type and by profile, and a bar for each day. Reset counters wipes the file locally, and there is nothing to sync, because nothing was ever sent.

Trackers
requests to hosts on the merged list
LinkedIn telemetry
first party telemetry paths, whichever set matched
Links cleaned
navigations a parameter was stripped from
Beacons
suppressed sendBeacon calls
WebSocket tracking
dropped read receipt and typing frames

Counted on this computer only, and kept when you close ARMANOS.

The Protection screen showing blocked today, the last 7 days and the total, with a breakdown by type, a breakdown by profile and a bar for each day
The counters are read back from a local file, so they still mean something after a restart.

On the built-in engine an installed blocker does not block on its own

Electron does not execute declarativeNetRequest. Measured with the same extension on both engines: on the built-in engine the request an ad blocker should have stopped reached the server, while ARMANOS Browser blocked it. A blocker installed there loads, shows its icon and stops nothing by itself.

The app closes part of that gap. It reads the rule files named in the extension's manifest, from inside the extension folder only, and runs them through request interception with the context each rule needs: which page is asking, what resource type it is, and which method.

What it will not do is pretend. A rule with its own regular expression, tab conditions, response header conditions or request domain conditions is counted as not understood and is not applied. A rule applied without its condition blocks more than was asked and leaves you with a white page instead of a site, so the two numbers, executed and not understood, are kept apart and written to the log.

Because the gap is real, the Extensions section carries a warning whenever the engine that will actually start is the built-in one. Choosing ARMANOS Browser without having installed it counts as built-in, since that is where the launch lands.

The Extensions section of the app with the catalog, a per profile toggle list and a warning line about the built-in engine
The warning appears whenever the engine that will really start is the built-in one.

What you do with it

Nothing here has a switch of its own. The use case you pick for a profile selects the lists, the telemetry set and the page scripts, and the rest happens at launch.

  1. 1

    Pick the use case when you create the profile

    Privacy gets the general list alone. Social gets the general, LinkedIn and social lists plus both telemetry sets. Ad accounts gets the general and ads lists with the host scoped console rules.

  2. 2

    Turn on ARMANOS Browser under Engine

    Blocking and cleaning work on both engines, but an installed blocker extension's own rules only run in full there. The build shipped today is 153.0.7978.0.

  3. 3

    Work as usual, then open Protection

    Today, the last 7 days and the total, split by type and by profile. A profile at zero after real browsing is telling you its engine or its use case is not what you assumed.

  4. 4

    Reset the counters before a measurement

    The reset is local and immediate, so you can start a clean count on one machine without touching anything on the server.

What this does not do

  • This is not an ad blocker. The lists are correlation infrastructure, between 31 and 74 entries per use case, so banners and sponsored posts keep showing.
  • Sites you sign into still know you. A login is an identity you handed over yourself, and cutting third party callbacks does not take it back.
  • The ad list spares ad consoles on purpose. Everything those consoles need to sign in, load, bill or answer their API is excluded, so working inside a console stays visible to the platform.
  • The Protection screen still labels the telemetry row LinkedIn telemetry, even when the count came from the social or ad console rule sets.
  • The four lists ship inside the app and change with an app update. There is no subscription list that refreshes itself on a schedule.

How to check

Open the file, then the stand that guards it.

One function builds the rules for both engines, and each telemetry set is locked to its own hosts
apps/desktop/src/lib/tracker-rules.js · apps/desktop/test/слежка-по-хостам.js
On ARMANOS Browser the same rules become a per profile extension, measured live through a loopback proxy
apps/desktop/src/lib/forkExtension.js · apps/desktop/test/блокировка-живьём.js
The 40 stripped parameters live in one shared list used by both engines
packages/shared/src/index.js · apps/desktop/test/блокировка-живьём.js
An installed extension's rules are executed by the app on the built-in engine, and rules it cannot express are counted as skipped
apps/desktop/src/lib/dnrBlock.js · apps/desktop/test/builtin-blocking.js
The Extensions section warns whenever the built-in engine is the one that will start
apps/desktop/src/manager/store.js · apps/desktop/test/extension-limits-honest.js

See it in your own profiles

Blocking, cleaning and the counters are on from the first profile you create.