Syllabus Lesson 126 of 156 · Build the defences
Build the defences

Leg 6 project: harden the engine

Why it matters This is where the module comes together. You assemble your own engine from the pieces you built, partitioning, Storage Access, link cleaning, farbling and a bounce purge, and run it against a tracker suite whose host names you cannot know in advance. Passing means you understand not just each defence, but why an engine needs all of them at once.

Think of it as securing a house. A lock on the front door is useless if the back window is open. Each tracker in the suite tries a different way in, and the hidden version moves the windows around. Only a house secured by how doors work, not by a list of which doors exist, keeps everyone out.

Defence in depth: several independent defences, so a tracker that slips past one is stopped by another.

Hidden suite: the same five tracking techniques as in the Breakage lab, but on other seeds. The three unlisted trackers get different host names each time, and the features move to different sites.

The brief

Write an engine that, on the 20 practice sites, leaves no tracking technique linked and keeps the breakage rate within a budget of 0.10 (at most two broken sites in 20), on seed 0 and on two hidden seeds.

Your engine is two functions the suite calls:

  • make_browser(world): returns a browser. Use AccessBrowser(world, request_fn=..., access_fn=..., blocker=..., trait_filter=...) from watched.leg6b_access. It is the practice browser with partitioning on and the Storage Access hooks.
  • after_navigation(browser, visit, interacted): called after every page load. visit.records lists every request; navigation records have kind "document" or "form", all but the last of those are redirect hops, and record.stored lists the cookies a response managed to save. interacted is True when Sam really used the page he landed on.

Run it with result = run_suite(make_browser, after_navigation) from watched.leg6b_harden. It returns linked, broken and breakage, just like the Breakage lab.

Checklist

  1. Storage Access rules: request_access and has_access, as in the Storage Access lab, so the comments widget works after a click.
  2. Blocker: navigations (kind "document" or "form") get cleaned with your clean_url, never blocked; subresources whose site is on TRACKER_LIST are blocked; everything else is allowed. Do not block every third party: that breaks the cdn.example script on every site.
  3. Link cleaning: clean_url(url) with the strip list and the allowlist from watched.leg6b_links, as in the link cleaning lab.
  4. Farbling: farble(values, top_site, session_key) as in the Farbling lab, used by a trait_filter(traits, page) that farbles traits["canvas_pixels"] and traits["audio_samples"] with page.top_site and a SESSION_KEY you choose.
  5. Bounce purge: in after_navigation, remember the sites Sam interacted with, and clear the cookies and storage of any redirect hop that stored a cookie and is not one of them.
  6. Measure: print result, then fix whatever is still linked or broken.

Two tips

Keep state on the browser, not in a global variable. The tests run your engine on three seeds, and a global set of "used sites" would carry over from one run to the next. In make_browser, write browser.used_sites = set() and read it back in after_navigation as browser.used_sites.

When something is still linked, ask which of your defences was meant to stop that technique, then print what that defence saw. The Breakage lab's table is a good map: each technique has one defence that stops it without a host list.

TechniqueStopped by
third-party cookiethe blocklist, or partitioning
frame storage (unlisted host)partitioning
fingerprint (unlisted host)farbling
link decorationcleaning every navigation
bounce (unlisted host)the bounce purge

The tests forbid watched.leg6b_presets and the link module's reference_clean: the point is that the engine is yours.

An honest note

Passing this suite does not make a real engine safe. Real trackers adapt, real sites are messier than twenty practice ones, and real engines spend years on exactly this loop: measure, defend, measure breakage, adjust. What you have now is the method, and that is what engine teams use every day.

In other languages

A real engine makes the blocker decision in fast native code. A sketch in Rust of the same rule your blocker follows:

use std::collections::HashSet;

enum Decision { Allow, Block, Rewrite(String) }

fn decide(kind: &str, url: &str, site: &str, tracker_list: &HashSet<String>) -> Decision {
    if kind == "document" || kind == "form" {
        let cleaned = clean_url(url);          // your clean_url, in Rust; never block a navigation
        return if cleaned != url { Decision::Rewrite(cleaned) } else { Decision::Allow };
    }
    if tracker_list.contains(site) { Decision::Block } else { Decision::Allow }
}

And the bounce purge in JavaScript, as a browser extension might write it:

function afterNavigation(hops, landed, interacted, usedSites) {
  if (interacted) usedSites.add(landed);
  for (const hop of hops) {
    if (hop.storedCookies.length && !usedSites.has(hop.site)) clearSiteData(hop.site);
  }
}

Recap

  • No single defence stops every technique; an engine needs them layered.
  • Defences that work by behaviour (partitioning, farbling, purging) beat trackers whose names you do not know.
  • Clean navigations instead of blocking them, and keep allowlists for what sites need.
  • Ship only what you have measured on both numbers: tracking left and breakage caused.
Your turn

Build your engine. Define request_access, has_access, clean_url, blocker, farble, trait_filter, make_browser(world) and after_navigation(browser, visit, interacted) as described in the checklist, then set result = run_suite(make_browser, after_navigation). The tests run your engine on seed 0 and two hidden seeds; each run must leave result["linked"] empty with result["breakage"] at most 0.10. Do not import watched.leg6b_presets or reference_clean.

Spotted a problem in this lesson? Report it

Code · runs in your browser
Output