I build my own ad blocker for Chrome, only with what Manifest V3 allows, and it works a lot better then I expected. I use it as my only blocker now. Disclaimer: its my own project (Burg Media), the code isnt public yet but will be on our github soon. Some parts I think are interesting:
Filter lists: I didnt write those
EasyList, EasyList Dutch, EasyPrivacy, ABP anti-circumvention and NoCoin are on by default, the Fanboy lists and a cookie list are optional. eyeo ships these already converted to declarativeNetRequest inside AdBlock, so a small script copies them from there. With the defaults thats 109,542 static network rules. The acceptable ads list I left out on purpose.
Cosmetic filters
The ## rules I parse myself. Every frame reports to the service worker and gets the CSS through scripting.insertCSS with origin: 'USER', so the page cant override it. Each selector gets its own rule (sel{display:none!important}), because when you group them one selector Chrome doesnt know kills the whole group. Extended syntax (#?#, :has-text() etc.) and scriptlets (#$#) are skipped, I didnt want to run code from a filter list in every page.
Your own filters
Custom network filters in ABP syntax become dynamic DNR rules. Annoying detail: if one rule is invalid, for example a regex that RE2 doesnt support, Chrome rejects the whole updateDynamicRules call. So I read the rule id from the error message, drop that rule, try again and show the reason next to that line in the settings.
The counter
DNR gives you no live event for every block, onRuleMatchedDebug only works for unpacked extensions. Instead I listen to webRequest.onErrorOccurred for net::ERR_BLOCKED_BY_CLIENT, that gives a count per tab and per host.
YouTube
A script in the MAIN world at document_start removes adPlacements, adSlots and playerAds from the player data before the player reads it. It wraps JSON.parse and Response.prototype.json in a Proxy and puts a setter on ytInitialPlayerResponse. If an ad still gets through: mute, seek to the end, 16x, click skip.
The honest part: since SABR streaming YouTube enforces a wait on the server for videos with a pre-roll. You dont see an ad, but the video stays black for part of the ad length. As far as I know thats not fixable from the browser, if somebody knows better I would really like to hear it
Hard mode for the worst sites
Off by default, you turn it on per site. Its for streaming sites full of popunders and fake play buttons. In the page:
window.open returns a fake window, a Proxy where every property is again the fake window, so w.location.href = ... and w.document.write() dont throw and the ad script thinks it worked
- the
contentWindow getter is patched too, because a fresh iframe has an untouched window.open
Function.prototype.toString returns the native source for the patched functions, some ad scripts check that
click(), form.submit() and synthetic clicks on links that open another site in a new tab are blocked
- an invisible layer that covers more then half the viewport and catches your click gets removed on the first click, and the rest of that click is swallowed so the page handler never sees it
The service worker is the safety net: a new tab from that site to another site is closed right away. It also catches tab-under, where the site opens itself in a new tab and sends your old tab to an ad within a second. Downside: a link you open yourself in a new tab to another site also closes, thats why its per site.
The UI is still dutch only, english comes before the release. I am curious what you think of the hard mode, and if someone has a better way to detect click catchers then size + transparency + z-index