Edge-Layer Rules a Page Script Cannot Evaluate
A public ruleset splits 126 rules into 95 a page can check and 31 that need the network edge. Real handshakes show why the heaviest ones sit on the far side.
BotBrowser Team
Want the structured docs for Fingerprint?
This article lives in the editorial library. For step-by-step setup, reference material, and ongoing updates, jump into the docs section.
BotBrowser keeps a declared profile consistent across the surfaces a page can read and can keep a bounded record of what a page reported, and it does not expose the TLS handshake, the HTTP/2 settings frame or the address a proxy presents at the far end of a connection. This review reads one public detection ruleset as data, splits its rules by the layer each one needs, and then measures those connection-layer facts on real handshakes, so the split rests on observed bytes rather than on an assumption about what a network sees.
TL;DR
- The ruleset carries 126 rules. A page can evaluate 95 of them; the remaining 31 need a fact that never reaches the page.
- All 31 edge rules read at least one value outside the page, and 25 of them read nothing else at all.
- The edge subset is 23 coherence rules, 4 reputation rules, 3 automation rules and 1 artifact rule, spread over the network layer 28 times, the browser layer 14 times and the reputation layer 4 times.
- Only 6 of the 31 compare a page value with a wire value, and those 6 are the ones a page can half-answer: it supplies one side and has nothing for the other.
- Every edge rule weighs at least 0.5. Four sit at 0.85 or above, 8 between 0.7 and 0.84, and 19 between 0.5 and 0.69.
- The three heaviest rules share weight 0.9 and all three describe HTTP/2 frame behaviour, which no page script can read.
- Two handshakes that differ only in the protocol list produce
alpn=h2andalpn=http/1.1on the same TLS 1.3 version, the same cipher and the same key exchange group. - A client settings frame carrying an initial window of 1048576 and a maximum frame of 32768 arrives as an 18-byte record that the peer reads directly; the page that sent it can read neither number.
- The server side of a real connection reported an initial window of 65536, a maximum frame of 16777215 and 256 concurrent streams, all of which are visible to the client's network stack and to no script on the page.
- A page-side report can describe the environment it exposes, and it cannot falsify a rule whose inputs it never sees.
Contents
- Where the ruleset splits
- Which six rules a page can half-answer
- What a real handshake puts on the wire
- Why the heaviest rules sit on the far side
- How a reading of an edge finding differs
- Record a reading
Where the ruleset splits
A ruleset is a list of claims, and each claim names the values it reads. The split that matters here is not the category or the weight but where those values live. The probe reads the published ruleset as data and sorts every rule by whether a script running in the page could read the same inputs:
node=v26.9.0
ruleset=0.74.49 rules=126 digest=cf896731d6c894fb38266206bc121db1f235f536ffbf334aa2c0da7468cc0e59
split -> page_evaluable=95 edge_only=31
edge_categories -> coherence=23 reputation=4 automation=3 artifact=1
edge_layers -> network=28 browser=14 reputation=4
edge_rules_with_a_wire_only_read -> 31 of 31
every_edge_rule_needs_the_wire -> true
edge_rules_with_no_page_read_at_all -> 25
The first line of the split is the one that decides how much of a detector a page can reason about. Ninety-five rules read values that exist in the document: the user agent string, the language list, the screen geometry, the time zone, the shape of a native function. A page that wants to know what those rules will say can read the same values and compute the same answer.
The other 31 read something else. The layer counts add to more than 31 because a rule can span layers, and the category counts show where they cluster: coherence at 23 is by far the largest, which fits the name, since a coherence rule exists to compare two sources that should agree. Reputation at 4 asks whether the address or the network belongs to a hosting range or a known proxy exit. Automation at 3 and artifact at 1 complete the set.
The line that carries the argument is every_edge_rule_needs_the_wire -> true. Each of the 31 names at least one input that is not a page value, so none of them can be decided from inside the document. The next line narrows it further: 25 of the 31 read nothing but wire values. For those, a page is not a partial witness; it is not a witness at all. There is no page-side query that returns the same number, no property whose absence stands in for the missing value, and no arithmetic that recovers it.
That distinction separates two kinds of report that look similar on the surface. A page-side scan produces a list of findings about surfaces it could reach, and a network-side check produces findings about a connection. When the two are merged into one score, the merge hides which half produced which finding, and the reader cannot tell whether a clean result means the page was consistent or means the connection was never examined. Network-layer and browser consistency names the layers a session exposes for exactly this reason, and network information fingerprinting shows which of them a page can reach on its own. A public suite catalogue shows the same gap from the outside: what public detector suites actually test counts how many of their published tests name the connection.
Which six rules a page can half-answer
Six of the 31 pair one page value with one wire value. Those are the interesting cases, because a page can supply half of the comparison and has nothing for the other half:
| Weight | Page side | Wire side |
|---|---|---|
| 0.85 | browser.webrtc_public_ip | network.observed_ip |
| 0.7 | browser.ua_browser | network.ja4_browser_hint |
| 0.6 | browser.ua_browser | network.h2_browser_hint |
| 0.6 | browser.ua_platform | network.ch_platform_header |
| 0.6 | browser.ua_browser | network.ch_ua_browser |
| 0.55 | browser.nav_language_primary | network.accept_language_primary |
The probe prints the shape of the heaviest of them directly:
cross_layer -> weight=0.85 net.webrtc_ip_vs_observed page=browser.webrtc_public_ip wire=network.observed_ip
page_side_view -> net.webrtc_ip_vs_observed page_reads=1 wire_reads=1
page_side_view -> predicate=not_equal needs_both_sides=true
page_side_view -> a page can supply browser.webrtc_public_ip and nothing for network.observed_ip
The predicate is not_equal, and that is what makes the pair hard to answer from inside the page. A page can read the address its own ICE gathering reports, and a WebRTC implementation that returns a public address at all returns a value the page can compare against whatever else it knows. What the page cannot read is the address the receiving end observed for the same connection, because that value exists only at the far end. A page that compares its WebRTC address against its own idea of its address is comparing one local value with another local value, and that comparison is not the rule.
The same structure holds for the other five. A page reads its own user agent, and it cannot read the browser family a TLS handshake implied. A page reads navigator.language, and it cannot read the primary language on the Accept-Language header the network stack sent. A page reads the platform string, and it cannot read the client-hints platform header. In each case the page value is real and the wire value is real, and only one of them is reachable from a script.
There is a second consequence that is easy to miss. Because the rule needs both sides, a page cannot even tell whether the rule would fire. It can compute its own half and stop. A deployment that changes a page surface to match a target profile has changed one input of a two-input comparison, and whether the comparison now agrees depends on a fact the deployment cannot query from the page.
What a real handshake puts on the wire
The claim that these values exist outside the page is easy to assert and worth measuring. The probe opens two TLS connections to the same host that differ only in the protocol list the client offers, and reads back what the connection negotiated:
handshake:dual_alpn -> alpn=h2 protocol=TLSv1.3 cipher=TLS_AES_256_GCM_SHA384 group=X25519MLKEM768
handshake:http11_only -> alpn=http/1.1 protocol=TLSv1.3 cipher=TLS_AES_256_GCM_SHA384 group=X25519MLKEM768
wire_only_distinction -> alpn_differs=true
Both connections land on TLS 1.3, both agree on TLS_AES_256_GCM_SHA384, and both use the X25519MLKEM768 key exchange group. The only difference is the negotiated protocol, h2 against http/1.1, and that difference came from a client-side parameter the peer read during the handshake. Nothing in the document can observe it. A page loaded over either connection sees the same APIs, the same headers on its own requests once they are sent, and the same environment; the protocol that carried the request is a property of the connection, not of the document.
The same is true of the HTTP/2 settings frame. The probe builds two settings frames and parses them back from raw bytes at a local listener, so the numbers come from the frame rather than from a client object:
settings_frame:wide -> enable_push=0 initial_window=1048576 max_frame=32768 bytes=18
settings_frame:narrow -> enable_push=1 initial_window=65535 max_frame=16384 bytes=18
settings_frame_differs -> initial_window_differs=true max_frame_differs=true
Both frames are 18 bytes. They carry an initial window of 1048576 against 65535, a maximum frame of 32768 against 16384, and opposite push settings. A peer that wants to know how a client behaves reads these six-byte entries as they arrive, before any document exists. A page cannot read the frame it sent, because the frame is written by the network stack below the document and is not exposed upward.
The server side of a real connection is readable by the client's network stack as well:
peer_settings:botbrowser.io -> initial_window=65536 max_frame=16777215 max_streams=256 header_table=4096
An initial window of 65536, a maximum frame of 16777215, 256 concurrent streams and a header table of 4096 are all values the client received from the server. A page can send requests and observe responses, and it cannot read any of these four numbers, because they describe the transport the requests travel on.
Put together, the measurements establish the shape of the gap rather than a specific detection. The connection carries a protocol list, a cipher suite, a key exchange group, a settings frame and an observed address. The document carries a user agent, a language, a platform and a set of rendered surfaces. The two sets overlap in exactly the six places listed above, and in each of those the comparison needs a value from each side.
Why the heaviest rules sit on the far side
If the edge rules were a thin tail of weak checks, a page-side scan would cover the subject well enough. The weights say otherwise:
edge_weight_bands -> ge_0.85=4 0.7_to_0.84=8 0.5_to_0.69=19 lt_0.5=0
heaviest -> weight=0.9 net.h2_rapid_reset layers=network
heaviest -> weight=0.9 net.h2_continuation_flood layers=network
heaviest -> weight=0.9 net.h2_control_flood layers=network
heaviest -> weight=0.85 net.webrtc_ip_vs_observed layers=network+browser
heaviest -> weight=0.8 net.ja4_unstable_within_session layers=network
single_wire_fact_rules -> 21
No edge rule weighs less than 0.5. The distribution is concentrated in the middle and the top: 19 rules between 0.5 and 0.69, 8 between 0.7 and 0.84, and 4 at 0.85 or above. The heaviest three share weight 0.9 and all three describe frame behaviour on an HTTP/2 connection. The first is net.h2_rapid_reset, a stream reset that arrives faster than a client could have processed a response. The second counts continuation frames on one stream. The third counts control frames across the session. Each is a timing or volume property of the transport, and each is measured by counting frames at the receiving end.
The two remaining heavy rules describe stability rather than a single value. net.ja4_unstable_within_session at 0.8 asks whether the client's handshake parameters changed inside one session, and net.webrtc_ip_vs_observed at 0.85 asks whether two addresses for the same client disagree. Both need more than one observation across time or across layers, which is a second reason a page cannot answer them: even if a script could read one handshake parameter, it has no access to the previous one.
Twenty-one of the 31 are simpler than that. They are presence checks over a single wire fact, such as whether a header the client sent was missing, whether the accepted encoding omitted a compression method, or whether the handshake carried a parameter a current client normally includes. A single fact decides the rule, and the fact lives entirely outside the document. That combination, one input and a weight of at least 0.5, is what makes the edge subset a real part of the detector rather than a footnote.
The practical reading is about coverage, not about scores. A page-side review can describe 95 of 126 rules and can describe none of the 31 that read the connection. When a report lists findings and says nothing about which layer produced them, the missing 31 are invisible: the report is not wrong about what it saw, and it is silent about the layer where four rules weigh 0.85 or more.
How a reading of an edge finding differs
A page-side finding and an edge finding support different claims, and the difference decides what a record has to contain. A page-side finding is reproducible from the document alone, because anyone who loads the same page in the same environment reads the same values and reaches the same conclusion; a disagreement between two runs then points at a change in the environment. An edge finding is reproducible only with the connection, since the same document loaded over a different protocol list, a different cipher or a different route produces a different input, and two runs that disagree may differ in the transport rather than in the browser.
That makes the record's field list longer for an edge finding. It has to name the connection the observation came from, not only the page, which for the runs above means the negotiated alpn=h2 or alpn=http/1.1 rather than a page URL. It has to state which side of each comparison was measured, because a rule such as net.webrtc_ip_vs_observed at weight 0.85 is not answered by reading only browser.webrtc_public_ip. And it has to keep the rule identity rather than a count, because 31 rules spread across 23 coherence, 4 reputation, 3 automation and 1 artifact do not collapse into a number that means anything on its own.
There is a limit worth stating plainly. A profile that keeps every page surface consistent with a declared target does not thereby change the handshake, the settings frame or the address observed at the far end; those are separate facts produced by separate parts of the client. Reading a page-side report as evidence about the connection overstates what was measured, and reading an edge report as evidence about the document makes the opposite mistake. The two reports describe two layers, and a claim about the browser as a whole needs both.
Record a reading
An edge reading supports a narrow claim: this ruleset version, this connection, produced these inputs. Four fields keep it checkable:
- The ruleset version and digest, because a rule that gains or loses an input changes what the split means without any change to the client.
- The layer each rule needs, because 25 of the 31 read nothing from the page and a report that omits the layer implies the page covered them.
- The measured wire values for the connection that was actually used, because a protocol list, a cipher and a settings frame belong to that connection and not to the browser in general.
- The six comparisons that need both sides, marked as half-measured when only the page value was read.
Public sources
- RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3
- RFC 9113: HTTP/2
- RFC 7301: TLS Application-Layer Protocol Negotiation Extension
- MDN: Navigator.userAgentData
- MDN: Accept-Language
- BotBrowser: automation consistency reference
Practical conclusion
Read a report that lists findings together with the layer each one came from, because a page-side scan can describe 95 of 126 rules and cannot describe the 31 that read the connection. Treat the six rules that pair a page value with a wire value as half-answered whenever only the page was measured, since the comparison needs both inputs and a page can supply one. Keep the ruleset version and the measured connection with any edge finding, because a protocol list, a cipher suite and a settings frame belong to that connection rather than to the browser as a whole. For the scoring that decides whether a fired rule may convict, read why one odd signal never convicts a browser; for how one library groups its own findings before any verdict, read CreepJS signal groups and how it combines them.
This article is part of the guide: What Is a Browser Fingerprint?
Related Articles
Why One Odd Signal Never Convicts a Browser
FingerprintWhat Public Detector Suites Actually Test
FingerprintNetwork Information API Fingerprinting: A Policy Review
NetworkDNS over TLS: Resolver Privacy, Deployment, and Failure Boundaries
NetworkNetwork-Layer Browser Consistency for Privacy Protection
FingerprintNavigator Properties Fingerprinting: Browser Leaks
Take BotBrowser from research to production
The guides cover the model first, then move into cross-platform validation, isolated contexts, and scale-ready browser deployment.