The Accept-Language header goes out with every request a browser makes, and a site that knows your proxy's country can compare the two. One of our profiles left through a Russian exit and kept sending es-MX, Spanish as spoken in Mexico. Our exit matcher had moved the timezone and the geolocation to Russia, left the language where it was, and then reported the match as complete. Leaving the language alone was correct. Reporting a clean match was the bug.

How a Mexican Spanish profile ended up in Russia

We were measuring detectors with a provider, the kind of proxy account where Mosaic mints a fresh session for each profile from one gateway line. One of those sessions was region-random: the seller picks the country. It picked Russia, and the exit geolocated to Yoshkar-Ola.

Mosaic's exit matcher, the same code that runs before each launch, looked the exit up and brought the profile into line. Its response for this one read:

changed: true
fields:  ['timezone', 'geolocation']
skipped: []

Two fields moved and nothing was skipped, which reads as a profile that now fully agrees with its exit. Meanwhile es-MX went on riding every request out of Yoshkar-Ola. The response also carried a reason saying the profile already agreed with the proxy exit, in the same breath as the field that contradicted it.

Why the language was left alone

Mosaic picks a language for an exit from its locale table. That table is what the generator draws new profiles from, country by country, so a profile matched to an exit afterwards looks like one that was created there. The table covers about fifty countries, and Russia is not one of them.

So the lookup returned nothing, and the matcher kept the old language rather than guessing. We still think that is right. A language invented on the spot, from a matcher, for one country, would be a value no profile created through the normal path could ever have.

Adding a Russian row is the real fix for the mismatch, and it is not a small one. It changes what every new profile can be drawn as, so it is a decision about the whole population of profiles. We did not want it made quietly from inside the code that matches one profile to one exit.

Every profile is drawn from one real computer, not from separate listsIn Mosaic: Machines that exist. GPU, screen, CPU and fonts from one real device. Free holds 3 profiles. Pro is $49 a month for any number.

How it works: Machines that exist

What we changed

The matcher now says when there is no language to move to. On that profile the skipped list carries:

language (es-MX does not match RU; the locale table has no row
for RU, so there is no language to move it to)

And the reason no longer claims agreement while listing a field that disagrees.

The check is careful in the other direction too. A profile already speaking ru-RU on a Russian exit agrees with it, row or no row, so it is not flagged. A warning about a match that is fine is noise, and noise teaches people to stop reading warnings.

Why the Accept-Language header is checked against the proxy country

Chrome builds the header from the profile's language list: es-MX,es;q=0.9 says Mexican Spanish first, any Spanish second. Page script reads the same list from navigator.languages. Neither needs a fingerprinting script. The header arrives with the very first request, before any of your code has run.

That is why Mosaic treats the language differently from the timezone. A logged-in session reads the timezone fresh each time, so moving it to match a new exit costs nothing. The language is different, because the site has been receiving it all along. A profile that has spoken de-AT to a site for a month and suddenly speaks en-US has told that site something about itself.

So the rule at launch is:

  • timezone and geolocation follow the exit, on every launch;
  • the language follows the exit only on a profile that has never run;
  • on a profile with history, a language that disagrees with the exit is reported, and stays.

The operator can then decide whether the mismatch matters for that account. Many real people browse in a language that is not their country's. A scanner still counts it. Whoer, in our runs, deducts for a language that does not match the exit's country.

What a region-random session costs you

The bigger lesson was about the session, not the header. A seller that picks the country for you can hand you a country your profile was never drawn for, and some fields cannot follow it. We now pin the country on sessions we measure with, and let the address move inside it. The reasoning, with the numbers from the runs that taught us, is in pin the country, not the address.

A profile created against a checked proxy is drawn for that exit from the start: timezone, language and geolocation together, which is what real fingerprints means in Mosaic. A profile with no proxy is drawn for the computer's own address, and how that went wrong once is in a profile in Austria claimed New York. For profiles on a seller's pool, set the country when you mint the session in proxy providers, and when you attach a proxy to profiles that already exist, POST /profiles/:id/match-exit does the same matching without opening a browser. Its skipped list names anything the match could not move.