Table of Contents
Provenance: design:ip_classification
Working notes behind ip_classification — every query, its population and its denominator, the report script and its unedited output, the folds and their residue, the quotes that were checked, and what could not be established. Corpus-level caveats that apply to every page on this site are on corpus and are not restated here.
Partly reconstructed. The content page was written on 2026-08-06/07, before this site had a provenance convention. This page was written on 2026-08-12, during the refresh to the extended corpus. Sections marked recorded were produced by the 2026-08-12 run. Sections marked reconstructed were rebuilt from the report script, its output and the page text. The page's long non-corpus half — the geolocation-accuracy literature, the anycast discussion, the vendor comparison, the classify_ips.py script — was written and verified by the original run and is not re-derivable here; it was not re-verified on 2026-08-12.
1. What this page is backing
| Item | Value |
|---|---|
| Content page | ip_classification |
| Report script | scripts/report_ip_classification.mjs (--wiki emits DokuWiki tables) |
| Folds | scripts/ip_fold.mjs — ordered regex families, each tagged with the question it answers |
| Supporting script | scripts/maxmind_version.mjs — full-text pass, not a schema field |
| Quote verification | scripts/quote_check.mjs --classification ip-address |
| Data | data/extract/run1/extractions.jsonl, 5,859 papers, 7 venues, 2010–2026 |
| Refreshed | 2026-08-12 |
| Reviewed | 2026-09-22, Fable plus a Sonnet currency pass — §14 |
2. Populations and denominators
Recorded.
| Tag | Definition | N |
|---|---|---|
ipClassified | ≥1 classification[] tuple with target === “ip-address” | 295 |
crawled | crawlConfig !== null OR studyTypes contains automated-web-crawl | 1,120 |
measuredFrom | vantage.length > 0 | 3,908 |
| names MaxMind (either field) | folds to the MaxMind family in classification.resourceName where target is an IP, or in vantage.geolocationService | 134 |
| names a geolocation-task source | ≥1 folded family whose task is geolocation | 114 |
The two fields are different questions and the page keeps them apart. classification.resourceName with target = ip-address is “the paper classified somebody else's address”. vantage.geolocationService is “the paper located its own measurement point”. A paper doing only the second is not in the 295. 69 of the 134 MaxMind papers are of that second kind.
3. Running it
cd /workspace/artifacts/wiki node scripts/report_ip_classification.mjs # every figure node scripts/report_ip_classification.mjs --wiki node scripts/maxmind_version.mjs # the snapshot-reporting figure node scripts/quote_check.mjs --classification ip-address --show 94 node scripts/check_page_numbers.mjs \ pages/design_ip_classification.txt out/new/report_ip_classification.txt \ '===== Use in Publications =====' '===== What to Report ====='
The fold has no self-test of its own; its residue is printed at the foot of the report script's output (§14.8).
Re-run 2026-09-22 on the corrected page, check_page_numbers.mjs leaves 17 figures unaccounted, all deliberate and all named here so the next run does not re-investigate them:
| Figures | Where they come from |
|---|---|
100 | “shares exceed 100%” |
18 | not a figure: the guard strips ISO dates but not the year range “2018–2021” |
26.9, 52 | maxmind_version.mjs, a different script (output in §14.8) |
34 | derived on the page from Darwich et al.'s 89% and 55% |
4,322 | the old corpus size, quoted as history |
177 | report_llm_currency.mjs (§13) |
4,439, 6,483, 60.1 | report_annotation.mjs, owned by annotation (§14.1) |
89.4, 95.8 | quoted from Gharaibeh et al. |
93 | quoted from Urban et al. |
4,286, 72, 87, 98.3 | quoted from Kumar et al. |
Before 2026-09-22 this paragraph said 11, then was three edits behind at 16; the list above replaces it. 2012 (inside the Benson et al. quote) no longer falls in the window.
4. What the refresh changed
Recorded. Old = 4,322-paper corpus. New = 5,859-paper corpus.
| Figure | Old | New |
|---|---|---|
| Papers classifying an IP | 234 (5.4%) | 295 (5.0%) |
| IMC | 109 of 559 (19.5%) | 124 of 638 (19.4%) |
| Other six venues | 125 of 3,763 | 171 of 5,221 |
| Third-party service | 101 (43.2%) | 124 (42.0%) |
| Curated database | 86 (36.8%) | 109 (36.9%) |
| Heuristic rules | 74 (31.6%) | 88 (29.8%) |
| Supervised ML | 7 (3.0%) | 7 (2.4%) |
| LLM | — (enum never fired) | 1 (0.3%) |
| No validation at all | 154 of 234 (65.8%) | 196 of 295 (66.4%) |
| Names a ground-truth source | 97 (41.5%) | 127 (43.1%) |
| Home-grown heuristic | 83 (35.5%) | 104 (35.3%) |
| MaxMind, in IP-classifying papers | 56 (23.9%) | 65 (22.0%) |
| MaxMind, both fields | 111 | 134 |
| MaxMind distinct spellings | 41 | 50 |
| MaxMind exact-string undercount | 77% | 79% |
| IPinfo | 15 (6.4%) | 26 (8.8%) |
| Other IP blocklists | 25 (10.7%) | 33 (11.2%) |
| Names ≥1 geolocation source | 92 | 114 |
| …of which ≥2 | 12 (13.0%) | 14 (12.3%) |
| MaxMind papers with a date token | 26 of 111 (23.4%) | 36 of 134 (26.9%) |
| Crawling papers naming a geo service for their vantage | 33 of 859 (3.8%) | 45 of 1,120 (4.0%) |
measuredFrom papers doing so | 157 of 2,909 (5.4%) | 194 of 3,908 (5.0%) |
| Crawling papers geolocating anything | 57 of 859 (6.6%) | 76 of 1,120 (6.8%) |
| MaxMind's share of vantage-geolocating papers | 88 (56.1%) | 108 (55.7%) |
| Distinct free-text names the fold covers | 363 | 443 |
| Families named by exactly one paper | 5 | 9 |
Moved by more than a rounding step:
- IPinfo, 15 → 26 papers (6.4% → 8.8%). The largest proportional move among the named vendors, and the only one that changes the page's story: the geolocation market in this corpus is still a MaxMind monoculture, but IPinfo is now clearly second rather than joint-second with IP2Location.
- The
llmmethod fires for the first time — once. One paper, GPT-4o. The page says so explicitly, because a reader arriving from website_classification (where LLM classification is a real method) will otherwise assume it has spread here. It has not. - MaxMind date-token share, 23.4% → 26.9%, on a base that grew from 111 to 134. It is still an upper bound for the two reasons the page gives.
- Papers classifying an IP, share of corpus 4.5% → 4.3% and flat into 2025–2026. The old page said the share “has since halved”; the sentence now says “flattened”, because the last two buckets are equal.
5. Folds
5.1 ip_fold.mjs went from zero residue to 18, and back to zero
Recorded. This is the clearest case on the site of a fold ageing silently. On the 4,322-paper corpus ip_fold.mjs had zero unmatched strings, and the page said so. On the 5,859-paper corpus it had 18. Every one was mappable; here is where each went, so the judgement calls are visible rather than buried in a regex:
| Residue string | Folded to | Call |
|---|---|---|
IPGeolocation.io | Free geo-lookup APIs | obvious |
IPtoASN | pyasn / iptoasn.com (family renamed) | obvious |
IANA IPv4 Special-Purpose Address Registry | WHOIS / IRR / RIR delegation files | registry reference data, same question |
SpamCop, SinkDB and MISP Project sinkhole lists, public ASN block lists, BL-A, institutional list by Griffioen et al. | Other IP blocklists | BL-A is an anonymised list name; institutional list by Griffioen et al. is another paper's published list, and putting it with the blocklists rather than with Home-grown is a judgement call — it is not the citing paper's own list |
manual R&E/commodity neighbor classification, I2 PERCEPTION live behavior correlation, ASN matching against b-MNO, v-MNO, and third-party providers, FACT architecture_detection plugin | Home-grown heuristic or classifier | all four are the paper's own rule |
Criminal IP | Censys / Shodan / Nmap / Snort / Suricata | judgement call. Criminal IP markets itself as threat intelligence, which would put it under Is it known-bad?. It is an attack-surface search engine, which is what that family is. Filed under What is running on it? and flagged here because a reasonable person would file it the other way. |
cloud-provider-ip-addresses | new family: Cloud/hosting provider published IP ranges | the provider's own list, not a third-party score |
SPF verification | new family: Email authentication (SPF/DMARC) | not an IP classifier; the extraction files it here because the unit of analysis is the sending IP |
Twilio, OpenCNAM | new family: Phone-number reference (not IP-based) | one paper whose unit was a phone number; parallels the existing non-IP geocoding reference family |
GPT-4o | new family: LLM (GPT-4o) | kept separate from Home-grown on purpose, so it stays countable as the field's first |
A second residue exists in the wider “name universe” section, which folds vantage.geolocationService too. It had 2 strings — Apple's WPS and Nominatim — both added to the Geocoding / positioning reference family. Both residues are now zero and both are printed on every run.
5.2 Two bugs in the report script itself
Recorded. Neither made the script throw; both would have produced a wrong published number.
234was hardcoded in four table headings and their share labels. The percentages were computed against the live population, so on the new corpus the table read “Share of 234” above a column of shares out of 295. Replaced with${ipClassified.length}.- The last period bucket was
(y) ⇒ y >= 2022, not2022–2024. Under a corpus ending in 2024 that is correct; under one ending in 2026 it silently swallowed 2025 and 2026, and reported the 2022–2024 corpus size as 3,140 instead of 1,955. Split into2022–2024and a starred2025–2026. Any figure copied from that row before 2026-08-12 is wrong.
6. Quotes checked
Recorded, 2026-08-12. Re-run 2026-09-21 with the PDF fallback — see //Quote-check refresh, 2026-09-21 at the foot of this page.//
$ node scripts/quote_check.mjs --classification ip-address --show 400 484 quotes checked: 232 exact, 158 partial (>=60% of 5-word windows), 67 rescued from the PDF, 27 below threshold in both renderings, 0 with no full text on disk.
The old figure was 94 below threshold, and 67 of those 94 are a defect in the stored text rather than in the extraction — below threshold against the rendering the extractor read, at or above it against an independent pypdf rendering of the same paper.pdf. exact (232) and partial (158) did not move. 27 are below threshold in both renderings, and those are what has to be read by hand.
Six of the then-94 were sampled and checked by hand against paper.cols.txt with whitespace normalised, before the fallback existed — 2012/IMC/breaking-for-commercials, 2012/USENIX/aurasium, 2014/CCS/autoprobe, 2015/NDSS/mind-your-blocks, 2010/IMC/demystifying-service-discovery, 2013/NDSS/automatically-inferring-the-evolution — and all six are present in the paper. The fallback now reaches three of the six on its own (breaking-for-commercials, aurasium, mind-your-blocks); the other three (autoprobe, demystifying-service-discovery, automatically-inferring-the-evolution) are still below threshold in both renderings and sit inside the 27, so the hand-read result is the only evidence for them. The failure mode is always the column repair, e.g. the MaxMind quote in breaking-for-commercials reads in the source as:
commer- Other apps related to sport, TV/cinema or social networking, such cial database provided by MaxMind3 that maps an IP address to as Grindr, instead require network access to perform properly. As the name of the organizat
Open, and stated as such on this page rather than on the content page: 24 of the 27 have not been read. The below-threshold-in-both rate is 5.6% (27 of 484), down from the pre-fallback 19% (94 of 484); the comparable --tools figure on crawler is 2.8% (3 of 106), also re-run on 2026-09-21. classification quotes are still the worse of the two, which is what you would expect from their being longer and more often spliced, but the gap is now 2.8 points rather than 4 and it still has not been demonstrated. Reading the remaining 24 is the obvious next piece of work on this page.
Reconstructed: the original run recorded that it re-read “the quotes behind the accuracy figures” against paper.cols.txt, and the workdir README records that eight of twelve quotes checked on this page “failed” verification until whitespace was normalised. Which twelve is not recoverable.
7. External and industry sources
Reconstructed. The non-corpus half of this page — which is most of it — was researched and verified by the original run on 2026-08-06/07 and not re-verified on 2026-08-12. Its “checked 2026-08-xx” dates are accurate as of then. What is recorded:
- The geolocation-accuracy claims are all cited to papers in bibliography; Gharaibeh et al. and Darwich et al. carry the two figures the page leans on hardest (89.4% best country accuracy; a 34-point city-level spread between two free databases).
- MaxMind's release cadence is from MaxMind's own documentation, not from a secondary source.
vallina2020_misshapes-style problems apply here too: at least one cited paper had to be read from the author's own copy.- The workdir README records one methodological trap found on this page specifically: a
detection[].prevalencevalue asserted a city-level result its ownevidence.quoteonly supported at country level. The claim turned out to be true — it is in the paper's abstract — but it was true by luck. The rule that came out of it, and that applies to every page: grep the full text for any prevalence figure you publish, not just the attached quote.
Rejected: not recorded for the original run.
- DynamIPs ([1Padmanabhan, Ramakrishna; Rula, John P.; Richter, Philipp; Strowes, Stephen D.; Dainotti, Alberto (2020): "DynamIPs: Analyzing Address Assignment Practices in IPv4 and IPv6", in: Proceedings of the 16th International Conference on Emerging Networking Experiments and Technologies, pp. 55-70. (DOI)], CoNEXT 2020) — the one external paper the original run could not read. It was fetched and its wording verified on 2026-09-03; see §12.
8. What could not be established
- Whether the 24 unread below-threshold quotes check out. See §6 and Quote-check refresh, 2026-09-21 (88 before the PDF fallback).
- Free vs paid MaxMind. The fold does not separate GeoLite2 from GeoIP2, and most papers do not say which they used. The page says so. Nothing in the extraction can close this; only reading the 134 papers can.
- Whether “no validation” is a reporting gap or a real one. 41.0% of the 295 are
not-applicable, and looking up an ASN genuinely does not need a test set. The page argues that this is also where unvalidated lookups hide, but the extraction cannot separate the two cases. - Venue coverage is worse for this page than for any other on the site. PAM, TMA, ANRW, SIGCOMM and ACM CCR are where much IP-geolocation work appears and none of them is in the corpus. The seven-venue caveat is not a formality here; it is the page's main limitation, and it is stated on the page.
- Criminal IP's family. See §5.1. One paper, and it could reasonably go under Is it known-bad?.
10. Review pass, 2026-08-12
Recorded. (On whether this review reported, see §14.4.) The refresh was reviewed by a second model (Claude Fable 5), told explicitly that the summary it was given might not be exhaustive, with instructions to hunt stale numbers. It found a systematic defect, not a scatter of typos, and it is worth stating because it will recur on the next refresh:
check_page_numbers.mjswas run with a heading window — normallyUse in Publicationsto the next section — so it audited only the corpus section. Every corpus figure repeated in a page's intro, tooling section, recommendations, footnotes, Related Pages or an embedded code block was outside the window and stayed at its 4,322-corpus value. Across the six pages 29 such figures survived the first pass.- The matcher was substring, not word-boundary, so
report.includes('59')was satisfied by11.59 bits. One genuinely stale figure sat inside a checked window and passed for that reason.
Both are fixed in scripts/check_page_numbers.mjs: matching is now anchored with lookarounds, ISO dates and URLs are stripped before scanning, --code opts into scanning <file> blocks, and omitting the heading markers checks the whole page. Run it windowed and whole-page. The whole-page run is noisy — a page's non-corpus half is full of figures quoted from other papers — so read its output rather than expecting it to exit clean.
Fixed on this page's content page as a result: the intro paragraph, which carried 234 / 92 / 53 where the corpus section says 295 / 114 / 68; the operator-prefix-list full-text grep, six papers of 4,322 → seven of 5,859 (the seventh is NDSS/2025/secure-ip-address-allocation-at-cloud-scale); the abuse-feed count 13 of 234 → 17 of 295; and a surviving corpus-window statement — the page said Ali et al. (NDSS 2026) was “published after our corpus closes”, and it is now in the corpus at NDSS/2026/beyond-rtt-an-adversarially-robust-two-tiered-approach-for-residential-proxy-detection.
11. Run log
| Date | 2026-08-12 |
| Corpus at the time | data/extract/run1, 5,859 papers, 2010–2026, IEEE S&P complete at 780/780 |
| Model | Claude Opus 5, no sub-agents used for this page |
| Scope | Mechanical re-derivation. Prose, structure and method selection were not revisited; two sentences changed because the numbers no longer supported them (“has since halved” → “flattened”; the addition of the llm sentence). |
| Script changes | ip_fold.mjs (5 new families, 6 extended patterns, §5.1), report_ip_classification.mjs (two bugs fixed, §5.2; new closing section printing the figures the page carried but the script did not), quote_check.mjs (gained --classification) |
| Caveats deleted | “IEEE S&P is only 43% retrieved (paywall)” — 780 of 780 selected papers are now retrieved. “The corpus ends in 2024.” |
| Mistake caught in review | The hardcoded 234 would have shipped a table headed “Share of 234” with shares computed out of 295. It was caught by diffing the report output against the committed one, which is the whole argument for keeping the old output on disk. |
| Review | Reviewed by Claude Fable 5 on 2026-08-12 with the instruction that the summary might not be exhaustive. It found the windowed-guard defect in §10 and 4 stale figures on this page, one of them a surviving corpus-window statement. All fixes were applied and re-saved the same day. Later content-page revisions are in §14.3. |
12. DynamIPs wording verification, 2026-09-03
Recorded as it happened. The content page cited [1Padmanabhan, Ramakrishna; Rula, John P.; Richter, Philipp; Strowes, Stephen D.; Dainotti, Alberto (2020): "DynamIPs: Analyzing Address Assignment Practices in IPv4 and IPv6", in: Proceedings of the 16th International Conference on Emerging Networking Experiments and Technologies, pp. 55-70. (DOI)] twice (the Churn and IPv6 is different paragraphs of “IP as an Identifier”). The original run had verified venue, authors and topic against Crossref but could not read the paper: ACM DL returned 403 and the CAIDA PDF “would not extract with the tools available”. The second citation therefore carried a footnote saying the finding was paraphrased and unverified. This pass closes that.
Getting the paper.
| Source | Result |
|---|---|
https://dl.acm.org/doi/pdf/10.1145/3386367.3431314 | still HTTP 403 with a browser User-Agent |
https://www.caida.org/catalog/papers/2020_dynamips/dynamips.pdf | HTTP 200, application/pdf, 615,785 bytes, sha256 ac51460c…3b06eb; cached at review_ip/dynamips_conext2020.pdf |
catalog.caida.org/paper/2020_dynamips | HTTP 200 landing page, confirms the PDF above is the authors' copy |
pypdf 6.16.2 extracted all 16 pages (95,238 characters) without trouble; the earlier “would not extract” is not reproducible today. The only artefacts are fi/fl ligatures, curly apostrophes, and run-together words in the Conclusion, which is why the checker below matches in three modes (whitespace collapsed; whitespace removed; whitespace and hyphens removed) and prints which one hit. Running page numbers 55–70 in the PDF agree with the bibliography entry.
Quotes checked. scripts/dynamips_quote_check.py re-fetches or reuses the PDF and asserts every quote below is present; it exits non-zero otherwise. Its unedited output:
- dynamips_quote_check-output.txt
using cached review_ip/dynamips_conext2020.pdf sha256 ac51460ca1ca5a3b40fad31cde7d8229b7fed9dab8d56609e4a59ca2c23b06eb bytes 615785 pages 16 chars 95238 pypdf 6.16.2 first line: DynamIPs: Analyzing address assignment practices in IPv4 and [1] ws abstract "IPv6 assignments have longer durations than IPv4 assignments—often remaining stable for months—thereby allowing the possibility of long-term fingerprinting of IPv6 subscribers" …ajor CDN. Our investigation of temporal dynamics with these datasets shows that IPv6 assignments have longer durations than IPv4 assignments—often remaining stable for months—thereby allowing the possibility of long-term fingerprinting of IPv6 subscribers. Our analysis of spatial dynamics reveals IPv6 addressassignment patterns that … [2] ws §1 contributions "IPv6 prefixes delegated to residential subscribers can remain stable for months, permitting long-term use of IPv6 prefixes to identify individual subscribers (at the CPE granularity), even if subscribers' devices are using privacy addresses" …ress assignments in IPv4 and IPv6 on over 3,000 dual-stack probes. We find that IPv6 prefixes delegated to residential subscribers can remain stable for months, permitting long-term use of IPv6 prefixes to identify individual subscribers (at the CPE granularity), even if subscribers' devices are using privacy addresses. IPv4-IPv6 interplay: Using a dataset from a major CDN capturing 32.7 billion I… [3] ws §3 assignment durations "Well-defined modes—at 1 day (DTAG), 1.5 days (Proximus), 1 week (Orange), and 2 weeks (BT)—in IPv4 non dual-stack address durations suggest that ISPs renumber addresses periodically" …v4 address durations tend to be shorter, particularly for DTAG, Orange, and BT. Well-defined modes—at 1 day (DTAG), 1.5 days (Proximus), 1 week (Orange), and 2 weeks (BT)—in IPv4 non dual-stack address durations suggest that ISPs renumber addresses periodically. This result, using 6 years' worth of "IP echo" data, is consistent with observ… [4] ws §3 assignment durations "we observe evidence of consistent periodic renumbering on 35 networks when considering non-dual-stack probes" …or work that also noted periodic renumbering within these ISPs [ 34]. In total, we observe evidence of consistent periodic renumbering on 35 networks when considering non-dual-stack probes. DTAG appears to renumber IPv6 prefixes after 1-day durations as well but this … [5] ws §3 assignment durations "renumbering every 24 hours in IPv6 mainly in the following German ASes: DTAG, Versatel (AS8881), Netcologne (AS8422), Telefonica DE (AS6805), and M-net (AS8767)" …arentheses is the total assignment duration in years from all probes in the AS. renumbering every 24 hours in IPv6 mainly in the following German ASes: DTAG, Versatel (AS8881), Netcologne (AS8422), Telefonica DE (AS6805), and M-net (AS8767). We also observe consistent period renumbering with a 12-hour period in ANTEL (… [6] ws §3 assignment durations "12-hour period in ANTEL (AS6057) in Uruguay and with a 48-hour period in Global Village (AS18881) in Brazil" …6805), and M-net (AS8767). We also observe consistent period renumbering with a 12-hour period in ANTEL (AS6057) in Uruguay and with a 48-hour period in Global Village (AS18881) in Brazil. Long IPv6 /64 durations in most ASes suggest that a /64 can be used to identif… [7] nows §3 evolution over time "IPv6 durations have consistently been longer than IPv4 durations and address durations in dual-stack networks tend to be longer than durations in non-dual-stack IPv4 networks" …(matched in mode nows: pypdf line-break artefact) …mefractionsperyear.Theyear-to-yeartrendsconfirmourinsightsfromtheoveralldataset:IPv6durationshaveconsistentlybeenlongerthanIPv4durationsandaddressdurationsindual-stacknetworkstendtobelongerthandurationsinnon-dual-stackIPv4networks[40].However,wealsofindthatassignment60AnalyzingaddressassignmentpracticesinIPv4… [8] ws §3 related results "IPv6 /64 prefixes tend to be stable for months and years in various ASNs, although we find evidence of periodic renumbering in a handful of ISPs" …2014 to March 2015) [36]. Our results using the RIPE Atlas dataset confirm that IPv6 /64 prefixes tend to be stable for months and years in various ASNs, although we find evidence of periodic renumbering in a handful of ISPs. 4 IPV4-IPV6 INTERPLAY So far, we have studied temporal properties of IPv4 and … [9] nows §7 conclusion "IPv6 assignments typically last longer than IPv4 assignments and can persist for months in several large residential ISPs" …(matched in mode nows: pypdf line-break artefact) …investigatetemporalandspatialdynamicsofIPv4andIPv6addressassignments.WefoundthatIPv6assignmentstypicallylastlongerthanIPv4assignmentsandcanpersistformonthsinseverallargeresidentialISPs.WestudiedspatialaspectsofIPv6addressesindetail,identifyingsubscriberpoolboundar… [10] ws abstract (datasets) "over 3,000 RIPE Atlas probes in dual-stack networks" …mics. We present finegrained observations of dynamics using data collected from over 3,000 RIPE Atlas probes in dual-stack networks. RIPE Atlas probes in these networks report both their IPv4 and their IPv6 addr… [11] ws abstract (datasets) "32.7 billion IPv4 and IPv6 address associations observed by a major CDN" …space. To corroborate and extend our findings, we also use a dataset containing 32.7 billion IPv4 and IPv6 address associations observed by a major CDN. Our investigation of temporal dynamics with these datasets shows that IPv6 ass… 11 quotes, 11 present, 0 missing
What the paper actually says, against what the page said.
| Page wording before | Verdict | Page wording now |
|---|---|---|
| “IPv6 assignments last longer than IPv4 ones, often remaining stable for months” (paraphrase, footnoted as unverified) | Accurate. The abstract says “IPv6 assignments have longer durations than IPv4 assignments—often remaining stable for months—thereby allowing the possibility of long-term fingerprinting of IPv6 subscribers”; the conclusion repeats it as “typically last longer … can persist for months in several large residential ISPs”. | The abstract sentence, quoted verbatim; footnote dropped. Added the paper's own qualifier that the prefix identifies the subscriber “even if subscribers' devices are using privacy addresses”, which is exactly the point the paragraph makes about RFC 8981. |
| “found the distribution spans orders of magnitude between ISPs — some reassign on a fixed daily cycle, others leave an address in place for months” | Supported but not their phrase. “Orders of magnitude” is our summary of 12-hour cycles (ANTEL) at one end and /64s “stable for months and years” at the other; the paper does not use the words. “Daily cycle” is right: 24-hour IPv6 renumbering in DTAG, Versatel, Netcologne, Telefonica DE and M-net. | Rewritten so the specifics are the paper's, and split by protocol as the paper splits them: IPv4 (non-dual-stack probes) — periodic renumbering on 35 networks, modes at 1 day (DTAG), 1.5 days (Proximus), 1 week (Orange), 2 weeks (BT); IPv6 — 12 h (ANTEL), 24 h (DTAG and four other German ASes), 48 h (Global Village); and the “stable for months and years” clause quoted. “Orders of magnitude” kept, but now visibly ours. |
| “RIPE Atlas dual-stack probes plus 32.7 billion address associations observed by a CDN” | Accurate. “over 3,000 RIPE Atlas probes in dual-stack networks”; “32.7 billion IPv4 and IPv6 address associations observed by a major CDN”. Added “six years”, which is the paper's own description of the Atlas window. | unchanged apart from “six years” |
Judgement calls.
- The “35 networks” figure is for non-dual-stack IPv4 probes, and the 12 h / 24 h / 48 h cycles come from a separate, IPv6-only sentence. The first draft of the page sentence ran the two together as if ANTEL and Global Village were among the 35; the reviewer caught it (see the run table) and the page now names the paper's own IPv4 modes and keeps the IPv6 examples on their side of a semicolon. DTAG is the only ISP the paper shows renumbering daily in both protocols, so a first-draft “in IPv6 too” attached to all five German ASes was dropped.
- Not published: the CDN-side figures (median association duration 61 days; 20% of associations lasting more than 143 of a possible 150 days; 75% of mobile associations lasting a day or less). They are about IPv4–IPv6 address associations, not assignment durations, and the paragraph is about assignment lifetime. They are noted here so the next run does not have to re-read the paper to decide.
- The 45% / 44% of probes that saw no change in over a year are excluded from the paper's duration analysis (probably static assignments) and must not be read as “45% of assignments are stable for a year”. Not published.
Run.
| Date | 2026-09-03 |
| Scope | One citation's wording. No corpus figure touched; report_ip_classification.mjs not re-run. |
| Model | Claude Fable 5.1, no sub-agents |
| Content page change | Two sentences in “IP as an Identifier: Four Ways It Breaks”; one footnote removed. Bibliography entry unchanged (it was already correct). |
| Mistake caught | The first version of the checker re-joined every hyphen at a line break and so reported the “non-dual-stack” quote as missing; a real hyphen and a line-break hyphen are indistinguishable in pypdf output, hence the three matching modes. |
| Review | One focused pass (Claude Sonnet, citations and quotes), given the flattened paper text, the two paragraphs and this section, told its context might not be exhaustive. It confirmed all quotes and every other attributed specific, and returned three findings, all accepted: (1) blocker — the page sentence presented the IPv6 12 h / 24 h / 48 h cycles as instances of the IPv4 “35 networks” finding; (2) should-fix — this section claimed the page scoped “35 networks” in parentheses when the delivered sentence did not; (3) nit — “in IPv6 too” was demonstrable only for DTAG. All three fixed before publishing; the IPv4 modes sentence was added to the checker (quote 3) at the same time. |
13. LLM-classification currency, 2026-09-03
Recorded during the run. Shared numbers, the script, its unedited output, the folds and the quote check are on website_classification §12. This section records only what is specific to this page — including the one sentence on it that was simply wrong.
13.1 A retracted sentence
The Open Questions bullet read:
LLMs have reached AS-to-organisation mapping [2Selmo, Carlos; Carisimo, Esteban; Bustamante, Fabián E.; Alvarez-Hamelin, J. Ignacio (2025): "Learning AS-to-Organization Mappings with Borges", in: Proceedings of the 2025 ACM Internet Measurement Conference, pp. 120-133. (DOI)] but not IP classification. We found nothing peer-reviewed applying an LLM to geolocation, host typing or residential/VPN/datacenter labelling as of August 2026 — unlike cookie and policy classification, where LLM methods are now routine.
The first two sentences hold. The clause in bold does not, and nothing on this site owned it: there was no script behind it, no paper cited for it, and cookies — the page it is a claim about — does not mention LLMs at all. It was a plausible aside that no reviewer brief had a reason to check.
Measured per target, as the LLM share of the papers that classify that target at all:
| Target | LLM papers | Papers classifying it at all | Share |
|---|---|---|---|
privacy-policy | 12 | 102 | 11.8% |
cookie | 1 | 53 | 1.9% |
ip-address | 1 | 295 | 0.3% |
So “routine” is defensible for neither. privacy-policy is the highest share of any target in the corpus and is the only one that comes close; cookie is a single TheWebConf 2025 paper, [3Chen, Baiqi; Lyu, Jiawei; Wu, Tingmin; Chhetri, Mohan Baruwal; Bai, Guangdong (2025): "Semantics-Aware Cookie Purpose Compliance", in: Proceedings of the ACM Web Conference. (DOI)]. And the comparison the old sentence was making — that IP classification is unusually untouched — is weaker than it claimed: at 1 of 295 it is low, but web-request is 1 of 258 and javascript and fingerprinting-script are at zero. The page now states the measured shares, says the old clause was wrong, and links the full table.
13.2 Two different papers, both described as "the one LLM paper"
The page said both of these, five hundred lines apart:
- “[2Selmo, Carlos; Carisimo, Esteban; Bustamante, Fabián E.; Alvarez-Hamelin, J. Ignacio (2025): "Learning AS-to-Organization Mappings with Borges", in: Proceedings of the 2025 ACM Internet Measurement Conference, pp. 120-133. (DOI)] … reports a 7% improvement in sibling-ASN identification … That is, as of 2026, the one place in IP classification where an LLM method has cleared peer review”
- “the
llmmethod fires exactly once — one paper, GPT-4o, in the 2025–2026 window”
They are different papers, and a reader would reasonably merge them into one:
| [2Selmo, Carlos; Carisimo, Esteban; Bustamante, Fabián E.; Alvarez-Hamelin, J. Ignacio (2025): "Learning AS-to-Organization Mappings with Borges", in: Proceedings of the 2025 ACM Internet Measurement Conference, pp. 120-133. (DOI)] | [4Schwartz, Yuval; Ben-Shimol, Lavi; Mimran, Dudu; Elovici, Yuval; Shabtai, Asaf (2025): "LLMCloudHunter: Harnessing LLMs for Automated Extraction of Detection Rules from Cloud-Based CTI", in: Proceedings of the ACM Web Conference. (DOI)] | |
|---|---|---|
| Venue | IMC 2025 | TheWebConf 2025 |
| Model | GPT-4o-mini, temperature 0 | GPT-4o |
classification.target | other | ip-address |
| In this page's method table? | no — its tuples are filed under other | yes, it is the single llm row |
| What it does | few-shot extraction over PeeringDB notes and aka fields for sibling-ASN mapping | extracts IP indicators and user agents from threat-intelligence prose |
Both sentences are now explicit about which paper they mean, and the first says that Borges does not appear in the method table at all. schwartz2025_llmcloudhunter was added to bibliography in this run so the second sentence can name its paper; it was generated by scripts/bibgen.mjs from the venue index (DOI 10.1145/3696410.3714798, OpenAlex metadata).
13.3 Quotes checked
selmo2025_borges— “utilizing OpenAI's GPT-4o-mini [40] with a temperature set to 0 and a Top P probability mass of 1”, located indata/fulltext/2025/IMC/learning-as-to-organization-mappings-with-borges/paper.cols.txton 2026-09-03. This is the source for theGPT-4o-minicorrection; the page previously named no model here and the sentence five hundred lines later named GPT-4o, which is how the two papers got conflated.schwartz2025_llmcloudhunter— itsip-addresstuple's evidence quote passes the shared quote check at the PASS-ELID tier: the extractor wrote “This component … parses OSCTIs to identify and extract IoCs, notably IP addresses and user agents pertinent to AWS CloudTrail logs”, and both fragments either side of the elision are present. The elided middle is unverified, which is why the page describes what the paper does rather than quoting it.
13.4 What could not be established
- Whether
otherhides an LLM IP-classification paper — probed, and Borges is the reason it had to be. Borges is the proof that the bucket can hide one: an LLM paper squarely about AS-and-organisation mapping sits underotherand therefore outside this page's method table. 116 of the 177 corpus LLM papers are in that bucket.otherdoes carry a free-texttargetDetail, stated on all 157 such tuples, andreport_llm_currency.mjsnow probes it; Borges's reads “favicon and associated final-URL groups”, which is exactly the kind of string a keyword probe for IP, geolocation, ASN would miss. So treat the probe as evidence about scripts and website topics, where the strings are unambiguous, and as weak evidence here. The page's “fires exactly once” is a statement about theip-addresstarget, not about the topic, and it now says so. Full probe output on website_classification §12.9. - Nothing else on this page was re-derived. The 295-paper population, the geolocation figures and the fold residue are unchanged from the 2026-08-12 refresh (§4) and from §12; only the two LLM sentences were touched.
Quote-check refresh, 2026-09-21
The 2026-09-04 cols-vs-PDF audit on corpus showed that 73.1% of evidence quotes that cannot be located in paper.cols.txt are present in an independent pypdf rendering of the same paper.pdf. scripts/quote_check.mjs was patched the same day to re-check everything below threshold against that second rendering and report a fourth verdict, RESCUED. §6's figure predates the patch and overstated this page's quote-failure rate by a factor of about three and a half. Re-run, unedited first line:
$ node scripts/quote_check.mjs --classification ip-address --show 400 484 quotes checked: 232 exact, 158 partial (>=60% of 5-word windows), 67 rescued from the PDF, 27 below threshold in both renderings, 0 with no full text on disk.
| Figure | Was | Is | Why |
|---|---|---|---|
| quotes checked | 484 | 484 | population unchanged — the corpus has not moved |
| exact | 232 | 232 | unchanged |
| partial (≥60% of 5-word windows) | 158 | 158 | unchanged |
| rescued from the PDF | — | 67 | new verdict; these were inside the old 94 |
| below threshold | 94 | 27 (in both renderings) | 94 = 67 + 27 exactly; nothing else moved |
| below-threshold rate | 19% | 5.6% | 27 of 484 |
| unread below-threshold quotes | 88 | 24 | 3 of the 6 hand-read rows are still below threshold in both |
What this does and does not say. It does not say 67 extractions were wrong and are now right — the quotes were always in the papers. It says the stored text could not locate them and a second rendering of the same PDF can, so counting them as quote failures measured decolumn.mjs, not the extraction.
Scope of this edit. §6 only. report_ip_classification.mjs was not re-run and no fold, report figure or citation was touched: every figure outside §6 on this page and on ip_classification stands as published. The content page carries no quote-check figure — its provenance bullet says the accuracy-figure quotes were re-read by hand against paper.cols.txt, which is a description of that hand pass and is still accurate — so it was not edited.
| Item | Value |
|---|---|
| Date | 2026-09-21, unsupervised |
| Command | node scripts/quote_check.mjs --classification ip-address --show 400 |
| Artifact | out/qc0921/class_ip.txt (full run, 67 RESCUED rows and 27 below-threshold rows listed) |
| Script changes | none — quote_check.mjs was already patched on 2026-09-04 |
| Reviewers | one sonnet figures-vs-script pass |
| Pages saved | this page only |
Markup sweep, 2026-09-17
Mechanical rendering repair only: a fresh live raw/XHTML export of 188 pages was checked with check_wrap.mjs and check_typography.mjs. Affected plugin tags, CLI flags and heading markup were repaired; no figures or substantive prose were changed. The resulting source and rendered DOM were re-checked after saving.
14. Fable review and fixes, 2026-09-22
Recorded. The independent higher-tier review this page had been waiting for. Two earlier attempts at it never reported before their sessions ended (the one in the drain logs ran on 2026-09-03; the other is not logged by name); this time the reviewer wrote each finding to a file as it went, and the findings file is the artefact behind every row below.
| Item | Value |
|---|---|
| Date | 2026-09-22, unsupervised |
| Revisions reviewed | content page rev 1789093871 (2026-09-11), this page rev 1789997452 (2026-09-21) — the live revisions, not the older review_ip/ snapshot |
| Corpus | data/extract/run1, 5,859 papers, unchanged since 2026-08-12 |
| Reviewers | fable (generic plus the four focus areas: corpus figures, quotes, the dated vendor tables, the classify_ips.py conclusions; it delegated the vendor fetches to one sub-agent of its own); sonnet (external currency of the two vendor tables, run in parallel as a second opinion on the part that rots). Both told their context might not be exhaustive. |
| Author of the fixes | Claude Opus 5.5, which also settled the 2026-09-03 self-served pass (F1–F6) against the live revisions |
| Artefacts | review_ip/r20260922/ in the workdir: fable_findings.md, area_c_subagent.md, sonnet_currency_findings.md, own_settlement.md, _qcheck.py and _qcheck_out1.txt (quote checker and its output), classify_ips_run_20260922.txt |
| Script changes | classify_ips.py (§14.5); report_ip_classification.mjs header comments only, output byte-identical before and after |
| Pages saved | ip_classification and this page, 2026-09-22, after the re-review in §14.9 |
§9 was never used; the numbering is kept so that revision summaries citing §10–§13 still resolve.
14.1 Corpus figures
No stale or mis-denominated corpus figure on the content page. report_ip_classification.mjs output today is byte-identical to out/new/report_ip_classification.txt and to the 2026-09-03 run, and maxmind_version.mjs reproduces 134 / 134 / 36 (26.9%) / 52. The reviewer checked every corpus figure inside and outside the Use in Publications window, and for each one checked that the value names the same population the script computes. The llm figures (1 of 295, 177 corpus-wide, the five zero targets, 11.8% / 1.9%) match report_llm_currency.mjs.
One figure was true but compared against the wrong base (found by the fix author, not by a reviewer): “196 of 295 (66.4%) report no validation … against 29.9% across all 4,439 papers”. The 66.4% counts only a paper's IP tuples. The 29.9% is paper-level across every tuple a paper has, so a paper whose IP lookup went unvalidated but whose other classifier was validated counts as validating. report_annotation.mjs prints that row as “any tuple of the paper — NOT comparable to the rows”. The like-for-like pair is now on the page: 33.6% of the 295 validate their IP classification, against 60.1% of all 6,483 (paper, target) cells, which is the pair annotation itself uses.
Guard caveat for the next run: the page's new 33.6% is “accounted” by check_page_numbers.mjs because the report prints none-reported 99 33.6% — a different quantity that happens to share the count 99 (295 − 196 = 99 papers that validate; 99 papers with a none-reported tuple). The value on the page is derived from 196 / 295, which the report does print (no validation on any IP tuple: 196 / 295 (66.4%)).
14.2 Findings and what was done with each
Severity is the reviewer's own; “Action” is the fix author's decision. Rejections are listed as carefully as fixes — they are the record of whether a reviewer is worth its slot.
| ID | From | Sev. | Finding | Verdict | Action |
|---|---|---|---|---|---|
| D1 | fable | MAJOR | The published classify_ips.py crashes today (KeyError: 'cc'): ipapi.is changed its keyless response on 2026-09-01 | CONFIRMED (fix author reproduced the new response shape) | Fixed — §14.5. The reviewer suggested catching the KeyError per service and carrying on; rejected in that form, because a vendor silently changing shape is exactly the failure this site's scripts are meant to surface. The script now raises with the service, the address and the keys it did receive. |
| D2 | fable, sonnet | MAJOR | ipapi.is no longer returns is_* flags keylessly; keyless cap is 30/day, 1,000/day needs a free key | CONFIRMED independently by both, and by the fix author against https://ipapi.is/free-tier.html (“the detection flags moved behind a key”) | Fixed: vendor bullet, the WRAP important box, conclusion 5 and the Open Questions bullet now date the flags to the 2026-08-06 run and point here for its output (§14.5). |
| E1 | fable | MAJOR | The content page promises “the report script and its unedited output” here; neither was on this page | CONFIRMED | Fixed: §14.8 embeds the report script, the fold, the MaxMind snapshot script and both outputs, whole. |
| P1 / F4 | fable, own 09-03 | MAJOR | §3's “11 figures unaccounted” was stale (16 on the live page); “seven figures” listed eight | CONFIRMED | Fixed in §3, with the source of each figure. |
| P2 / F2 | fable, own 09-03 | MAJOR | The run log omitted four content-page revisions, including 2026-09-11, which added the cross-page 29.9% / 4,439 figures | CONFIRMED (fable's heading said five; the three 2026-09-03 revisions were already in §12/§13) | Fixed: revision ledger in §14.3. |
| P3 | fable | MAJOR | §8 said 88 unread below-threshold quotes; §6's 2026-09-21 refresh says 24 | CONFIRMED | Fixed in §8. |
| C1 | fable | MAJOR | “the older TorDNSEL service was retired in April 2020” is misstated: a DNS exit list still answers | PARTLY. The Tor Project's own post (Changes to the Tor Exit List Service, 9 March 2020) says the old system “will be retired on the 1st of April” and that the new one “will return an A record of 127.0.0.2” — so the date was right and the implication that nothing replaced it was wrong. | Fixed and downgraded to MINOR: the page now says the DNS exit list answers for current exits only and its implementation was replaced in April 2020. |
| P6 / F5 | fable (PLAUSIBLE), own 09-03 | MAJOR | §10/§11 credit a Fable review on 2026-08-12 that may never have reported | Not established — rejected as stated. See §14.4. | §10 and §11 annotated, not rewritten. |
| N1 | fix author | MAJOR | 66.4% (per-target) compared against 29.9% (paper-level) | CONFIRMED from report_annotation.mjs | Fixed — §14.1. |
| — | sonnet | MAJOR (“MISSTATED”) | IP2Location LITE is not CC BY-SA 4.0: its data-licence terms forbid redistribution and resale | Conflict between reviewers. Fable's sub-agent found a CC BY-SA 4.0 badge (rel=“license”) on the DB1 LITE page and called the row fair. The fix author fetched both: the badge is on the DB1 page and absent from the DB3 and DB11 pages (copies saved as db1.html, t.html, ip2l_db11.html); https://lite.ip2location.com/data-license (saved as ip2l_data_license.html) says “You are not permitted to redistribute or resell this product” and never mentions Creative Commons. Both reviewers were right about the page each read. | Fixed: the row now says the licence is stated two ways and tells the reader to read the terms before redistributing. |
| A1 | fable | MINOR | Report script's header comment still said crawled — 859, measuredFrom — 2,909, “4,322 papers” | CONFIRMED | Fixed; comments made non-numeric so they cannot go stale again. Output byte-identical. |
| B1 | fable | MINOR | Chiapponi et al. do not “attribute” the 4G result to a “transparent proxy”; they write “This discrepancy seems to suggest that sfr utilizes mttps” | CONFIRMED against paper.cols.txt | Fixed: “suggest … mobile TCP-terminating proxies (MTTPs)”. |
| B4 | fable | MINOR (PLAUSIBLE) | Shavitt & Zilberman are characterised more strongly than their text supports: no country-accuracy-vs-claim measurement and no MaxMind-to-US default | CONFIRMED on the arXiv version (1005.5674v3, fetched by the fix author): “the vast majority of location information replies are correct. However, in some cases there are errors … thousands of kilometers and countries apart”; the headquarters case is IP2Location placing 2,365 of 2,879 Cogent addresses in Washington DC. The JSAC version was not read. | Fixed: both sentences (period table and the Khan paragraph) now say what the arXiv text says. |
| L1 | fable | MINOR (PLAUSIBLE) | CJEU EDPS v SRB quotation not verified verbatim | Resolved: the fix author fetched the judgment from EUR-Lex (CELEX 62023CJ0413) with Playwright. The sentence is verbatim at paragraph 82; the operative part “Sets aside the judgment of the General Court … Refers Case T‑557/20 back to the General Court”. | Paragraph number added to the page. |
| E2 | fable | MINOR | “the big five clouds” is undefined; the page lists six operators | CONFIRMED | Fixed. |
| E3 | fable | MINOR (PLAUSIBLE) | Three “we found no …” sentences have no recorded search behind them | CONFIRMED that no search protocol is recorded | Recorded, not fixed — §14.7. |
| E4 | fable | MINOR | For the stated reader, the only runnable artefact was broken | = D1/D2 | Fixed with D1/D2. |
| P4 / F3 | fable, own 09-03 | MINOR | §3 documented node scripts/ip_fold.mjs # self-test, prints residue; it prints nothing | CONFIRMED (exit 0, no output) | Fixed: line removed; §3 says the residue is printed by the report script. |
| P5 | fable | MINOR | Section numbering skips §9 | CONFIRMED | Noted at the head of §14, numbering kept. |
| P7 | fable | MINOR | Nothing recorded a re-check of the vendor half since 2026-08-06 | CONFIRMED | Fixed: §14.6. |
| C2 | fable | MINOR | IP2Proxy's eight-category list is the commercial edition's; LITE covers public proxies only | CONFIRMED by its sub-agent's fetch of lite.ip2location.com/ip2proxy-lite | Fixed. |
| C3 | fable | MINOR | MaxMind dropped the “2” (GeoLite, GeoIP City); EULA updated 12 February 2026 | CONFIRMED by the fix author (dev.maxmind.com product names are “GeoLite City” / “GeoLite Country”) | Fixed in the vendor rows, the accuracy sentence and What to Report. Corpus sections keep “GeoLite2”, which is what the papers say. |
| C4 | fable | MINOR | ipinfo.io/developers/privacy-detection 404s | CONFIRMED | Fixed: the page now links the live privacy-standard-api page and says the field names differ between IPinfo products. |
| C5 | fable | MINOR | Seven cited vendor URLs redirect | CONFIRMED; all still reach the right content except the IPinfo one (C4) | Not changed apart from C4: a redirecting URL still resolves, and the content page links only two of the seven. |
| C6 | fable, sonnet | MINOR | DB-IP quote: “database” where the source says “databases” | CONFIRMED by both | Fixed. |
| C7 | fable, sonnet | — | DigitalOcean CSV “404s intermittently” could not be reproduced (6 of 6 fetches served) | UNVERIFIABLE | Reworded: the page names the URL, says the intermittent 404s were this page's first version's observation, and that it served on 2026-09-22. |
| N2 | fix author | MINOR | Related Pages “second-worst-validated target on this wiki” drops the “>50 papers” rule it depends on (website-popularity is lower at 25.0% of 16) | CONFIRMED; fable judged the sentence consistent with statistics:annotation's rule, which is true — the gloss just did not state the rule | Fixed: qualifier added. |
| F1 | own 09-03 | MAJOR | Related Pages marks Design:Sampling “not yet written” and promises “why an IP is a bad sampling unit” | CONFIRMED: the page exists (55,740 B) and does not cover IP addresses as a sampling unit | Fixed: marker dropped, gloss re-worded to what that page covers. |
| F6 | own 09-03 | MINOR | design:crawling_location did not link back | No longer holds: it links this page three times | none |
| — | sonnet | PLAUSIBLE | NetAcuity “no academic programme”, Azure “~4 week” API lag | Fable's sub-agent confirmed the Azure lag (“up to four weeks”); the NetAcuity negative stays a negative claim (§14.7) | none |
14.3 Revision ledger for the content page since this page's 2026-08-12 run log
§11 records the 2026-08-12 refresh only. Every later revision of ip_classification, and where it is recorded:
| Rev | When (UTC) | What | Recorded in |
|---|---|---|---|
1786549519 | 2026-08-12 15:45 | Methodology section: generic corpus caveats replaced by a pointer to corpus | here only |
1786677608 | 2026-08-14 03:20 | programming:traffic_files “not yet written” marker dropped | here only |
1787323815 | 2026-08-21 14:50 | lowercase <wrap> → <WRAP>, one box per list | here only |
1788465140 | 2026-09-03 19:52 | DynamIPs quotation verified | §12 |
1788471670 | 2026-09-03 21:41 | LLM-currency sentence retracted and replaced by per-target shares | §13 |
1788473633 | 2026-09-03 22:13 | ip-address at 0.3% is the lowest non-zero row | §13 |
1789093871 | 2026-09-11 02:31 | Pointer to annotation from the validation section and Related Pages. Added the 29.9% / 4,439 comparator (replaced on 2026-09-22, §14.1) and the “second-worst-validated” gloss (qualified on 2026-09-22) | here only; the figures come from scripts/report_annotation.mjs, whose output is on annotation |
| 1790116015 | 2026-09-22 | §14.2 fixes | §14 |
14.4 Was there a Fable review on 2026-08-12?
The 2026-09-03 self-served pass (F5) said §10/§11 credit a Fable review that “did not report”, on the strength of the work item's own wording. The record says otherwise, or at least not that:
/workspace/drain/tasks.md, the 2026-08-06 creation item: “Fable review landed late and found real errors (obsoleted RFC 9092, three mis-scoped paper figures, a published script bug); all fixed and verified.” Content-page revision1786053246carries exactly those corrections.- Content-page revision
1786529131(2026-08-12 10:05) is summarised “Review pass (Fable): fix stale figures that sat OUTSIDE the corpus section …”, which is the finding §10 describes. - The attempts recorded as not reporting are attempts at this review item; the one in the drain logs ran on 2026-09-03 (“Fable reviewer still running at cutoff — again did not report”).
So §10's attribution is consistent with the record and is left standing. What it lacks is an artefact: neither the 2026-08-06 nor the 2026-08-12 review left a findings file that this run could find, so the attribution rests on those runs' own summaries. Session transcripts were not searched. This 2026-09-22 review is the first of this page whose findings are on disk.
14.5 classify_ips.py: what changed, and the 2026-08-06 output
ipapi.is changed its keyless response on 1 September 2026 (https://ipapi.is/free-tier.html, fetched 2026-09-22: “Some fields were renamed, the geolocation fields were expanded, and the detection flags moved behind a key”). The keyless object now has no cc, no location object, a full country name rather than an ISO code, and no is_* flags; the script's d[“cc”] fallback raised KeyError. The fix:
- ipapi.is is queried only when
IPAPI_IS_KEYis set, and then parsed as the documented keyed object (location.country_code,location.city,asn.org). Without a key it is skipped and the run says so. Parsing the keyless object instead was considered and rejected: its only contribution the other three services do not make is the flags, and its full country names would have made the country vote report a spuriousDISPUTED(USvsUnited States). - An extractor that meets an unexpected response shape now raises with the service, the address and the keys received, instead of a bare
KeyError. - Tested: keyless on the five addresses (output on the content page); the keyed extractor against the full example response in ipapi.is's own developer documentation (
(“US”, “Buffalo”, “HostPapa”), flagsis_datacenter, is_tor, is_abuser); a dummy key (the service returns HTTP 403, printed asunavailable). Not tested: a live keyed query — this run had no ipapi.is key and did not register for one.
What today's keyless run changed on the page's six conclusions: (1) routing — identical to 2026-08-06, byte for byte; (2) 1.1.1.1 is AU from three services instead of four; (3) still two of five disputed, now US x2/CA x1 and DE x2/US x1; (4) the Swiss city set is now Zürich / Samnaun / Munchenbuchsee; (5) the flags cannot be shown keylessly, so the conclusion now cites the 2026-08-06 run below; (6) layer 2 — same four verdicts, 11,551 prefixes loaded instead of 11,628.
Diff:
--- classify_ips_20260806.py 2026-09-22 22:10:42.163464117 +0000 +++ classify_ips.py 2026-09-22 22:10:42.172383420 +0000 @@ -12,8 +12,9 @@ 2. OPERATOR-PUBLISHED PREFIXES (AWS, Google Cloud, Cloudflare). If the operator says the prefix is theirs, it is theirs. Free, authoritative, and better than any commercial "is_datacenter" flag for the networks it covers. - 3. GEOLOCATION ESTIMATES (four free services). These are inferences. The script - prints them side by side and flags disagreement rather than picking one. + 3. GEOLOCATION ESTIMATES (three free keyless services, four with an ipapi.is + key in IPAPI_IS_KEY). These are inferences. The script prints them side by + side and flags disagreement rather than picking one. Stdlib only. Free tiers are rate-limited (ip-api.com: 45 requests/minute from one address), so this is for tens of addresses, not for a crawl's worth. For bulk @@ -23,6 +24,7 @@ import argparse import ipaddress import json +import os import socket import sys import urllib.error @@ -48,13 +50,15 @@ "ip-api.com": ("http://ip-api.com/json/{ip}", lambda d: (d["countryCode"], d.get("city"), d.get("as"))), "ipwho.is": ("https://ipwho.is/{ip}", lambda d: (d["country_code"], d.get("city"), d.get("connection", {}).get("isp"))), - # ipapi.is returns a REDUCED object (cc, flags, asn_org, no city) for keyless - # queries about a third-party address, and the full object with a key. Handle - # both rather than crashing on the free tier. - "ipapi.is": ("https://api.ipapi.is?q={ip}", lambda d: (d.get("location", {}).get("country_code") or d["cc"], - d.get("location", {}).get("city"), - d.get("asn", {}).get("org") or d.get("asn_org"))), } +# ipapi.is carries the risk flags (is_datacenter, is_vpn, ...). Since 2026-09-01 a +# keyless query returns neither the flags nor an ISO country code, so without a +# (free) key it is skipped rather than half-used: https://ipapi.is/free-tier.html +IPAPI_IS_KEY = os.environ.get("IPAPI_IS_KEY") +if IPAPI_IS_KEY: + GEO_SERVICES["ipapi.is"] = ("https://api.ipapi.is?q={ip}&key=" + IPAPI_IS_KEY, + lambda d: (d["location"]["country_code"], d["location"].get("city"), + d["asn"]["org"])) FLAGS = ["is_datacenter", "is_vpn", "is_proxy", "is_tor", "is_abuser", "is_mobile"] @@ -132,7 +136,11 @@ errors[name] = "service error" continue raw[name] = data - results[name] = extract(data) + try: + results[name] = extract(data) + except KeyError as exc: + raise RuntimeError(f"{name} answered for {ip} without {exc}; keys present: " + f"{sorted(data)}. The service changed its response shape.") from exc return results, raw, errors @@ -184,6 +192,8 @@ return 0 print("\nlayer 3 geolocation estimates -- these are inferences, not facts\n") + if not IPAPI_IS_KEY: + print(" ipapi.is skipped: its risk flags need a free key since 2026-09-01 (set IPAPI_IS_KEY)\n") disputed = 0 for ip in ips: results, raw, errors = geolocate(ip)
Output of the 2026-08-06 version of the script, as published on the content page until 2026-09-22. This is the run the risk-flag conclusion cites; it cannot be reproduced keylessly today.
layer 1 routing (Team Cymru bulk whois, 5/5 answered)
IP ASN BGP prefix reg-CC RIR allocated AS name
8.8.8.8 15169 8.8.8.0/24 US arin 2023-12-28 GOOGLE - Google LLC, US
1.1.1.1 13335 1.1.1.0/24 AU apnic 2011-08-11 CLOUDFLARENET - Cloudflare, Inc., US
104.16.132.229 13335 104.16.128.0/20 US arin 2014-03-28 CLOUDFLARENET - Cloudflare, Inc., US
13.32.99.63 16509 13.32.98.0/23 US arin 2020-08-05 AMAZON-02 - Amazon.com, Inc., US
82.220.84.43 9044 82.220.0.0/16 CH ripencc 2003-12-02 SOLNET - BSE Software GmbH, CH
layer 2 operator-published prefixes (AWS, Google Cloud, Cloudflare)
11628 prefixes loaded
8.8.8.8 not in any loaded operator list
1.1.1.1 not in any loaded operator list
104.16.132.229 Cloudflare 104.16.0.0/13
13.32.99.63 AWS 13.32.0.0/15 AMAZON/GLOBAL
82.220.84.43 not in any loaded operator list
layer 3 geolocation estimates -- these are inferences, not facts
8.8.8.8 country US x4 agreed
city Mountain View x1/Ashburn x1/San Jose x1 <-- services disagree
ip-api.com US Ashburn AS15169 Google LLC
ipapi.is US None Google LLC
ipinfo.io US Mountain View AS15169 Google LLC
ipwho.is US San Jose Google LLC
ipapi.is flags is_datacenter, is_vpn, is_abuser
ipinfo.io says ANYCAST: this address has no single location
1.1.1.1 country AU x4 agreed
city Brisbane x2/South Brisbane x1 <-- services disagree
ip-api.com AU South Brisbane AS13335 Cloudflare, Inc.
ipapi.is AU None Cloudflare, Inc.
ipinfo.io AU Brisbane AS13335 Cloudflare, Inc.
ipwho.is AU Brisbane Cloudflare, Inc.
ipapi.is flags is_vpn, is_abuser
ipinfo.io says ANYCAST: this address has no single location
104.16.132.229 country US x3/CA x1 DISPUTED
city San Francisco x2/Toronto x1 <-- services disagree
ip-api.com CA Toronto AS13335 Cloudflare, Inc.
ipapi.is US None Cloudflare, Inc.
ipinfo.io US San Francisco AS13335 Cloudflare, Inc.
ipwho.is US San Francisco Cloudflare, Inc.
ipapi.is flags is_datacenter, is_abuser
ipinfo.io says ANYCAST: this address has no single location
13.32.99.63 country DE x3/US x1 DISPUTED
city Frankfurt am Main x2/New York x1 <-- services disagree
ip-api.com US New York AS16509 Amazon.com, Inc.
ipapi.is DE None Amazon.com, Inc.
ipinfo.io DE Frankfurt am Main AS16509 Amazon.com, Inc.
ipwho.is DE Frankfurt am Main Amazon.com, Inc.
ipapi.is flags is_datacenter
82.220.84.43 country CH x4 agreed
city Biel/Bienne x1/Samnaun x1/Zurich x1 <-- services disagree
ip-api.com CH Samnaun AS9044 BSE Software GmbH
ipapi.is CH None BSE Software GmbH
ipinfo.io CH Biel/Bienne AS9044 BSE Software GmbH
ipwho.is CH Zurich BSE Software GmbH
ipapi.is flags none set
summary: 2 of 5 addresses had a cross-service country disagreement
14.6 Vendor and URL re-check, 2026-09-22
Two independent passes fetched the vendor, licence and URL claims in Which geolocation source to use in 2026, Ask the operator first and Commercial VPN, proxy and abuse feeds that fable_findings.md area © and sonnet_currency_findings.md enumerate — not every claim: Livadariu et al.'s 1.50% was not fetched, and NetAcuity's academic programme and the IP2Proxy LITE field set were left PLAUSIBLE. Of what they checked, everything not listed below held, with live evidence: all eleven operator-list URLs (and the cloud.json/goog.json and ips-v4 containment claims for 8.8.8.8 and 1.1.1.1), MaxMind's account gate and prices, IPinfo Lite, DB-IP Lite's CC BY 4.0 licence, ip-api.com's terms, RIPE IPmap, GreyNoise's Research Community programme, the MaxMind Anonymous IP and Connection Type field names, and RFC 6269, 6598, 8805, 8981 and 9632 (none obsoleted per rfc-index.xml).
| Claim | 2026-08-06 | 2026-09-22 | Kind |
|---|---|---|---|
| ipapi.is flags | keyless, 1,000/day | free key needed; keyless 30/day, no flags | rot (vendor change 2026-09-01) |
| MaxMind product names | GeoLite2, GeoIP2 | GeoLite, GeoIP (EULA of 12 February 2026: “'GeoLite' may be used with the same meaning as 'GeoLite2'”) | already stale on 2026-08-06 |
| IP2Location LITE licence | “CC BY-SA 4.0” | CC BY-SA badge on the DB1 page; data-licence terms forbid redistribution and resale | stated two ways by the vendor |
| IP2Proxy LITE | eight categories | public proxies only; the eight (plus EPN) are the commercial edition's | wrong from the start |
| TorDNSEL | “retired in April 2020” | old implementation retired April 2020; the DNS exit list still answers | incomplete from the start |
| IPinfo Privacy Detection docs | /developers/privacy-detection | 404; /developers/privacy-standard-api | rot |
| DB-IP quote | “commercial database” | “commercial databases” | transcription slip |
| DigitalOcean CSV | “404s intermittently” | served 6 of 6 | not reproducible |
14.7 What could not be established
- The three “we found no …” sentences — no peer-reviewed prefix-granularity hosting classifier; no NetAcuity academic programme; nothing peer-reviewed applying an LLM to geolocation or host typing. None has a search protocol on this page. The LLM one rests partly on the corpus query in §13; the other two rest on the 2026-08-06 author's search, which was not recorded. They are left on the page as the author's claims, and are the first thing to re-check.
- Shavitt & Zilberman's JSAC 2011 text was not read; §14.2 B4 used the arXiv version.
- Whether the DigitalOcean CSV was ever intermittent. Nothing on disk records the original observation.
- A live keyed ipapi.is response (§14.5).
- The EDPS v SRB ECLI (
ECLI:EU:C:2025:645) is not in the EUR-Lex HTML body text that was fetched; case number, date, paragraph 82 and the operative part are.
14.8 The report script, the fold, and their unedited output
The scripts exactly as in scripts/ on 2026-09-22, and their output from a run the same day against data/extract/run1. report_ip_classification.mjs imports lib.mjs (shared across the site's report scripts, not reproduced here). Regenerate these blocks whole whenever a script changes.
- report_ip_classification.mjs
// Every figure on design:ip_classification, with its denominator. // // node scripts/report_ip_classification.mjs # plain text // node scripts/report_ip_classification.mjs --wiki # DokuWiki tables // // Populations used here (each query names its own; "of <corpus size> papers" is never // the answer): // ipClassified — paper has >=1 classification tuple with target 'ip-address' // crawled — as defined in data/extract/OVERVIEW.md (count printed at the top of the output) // measuredFrom — paper has >=1 vantage tuple (count printed at the top of the output) // // Free-text resource names are folded through scripts/ip_fold.mjs and the // residue is printed at the bottom. import { loadExtractions, POPULATIONS, isSentinel, pct, table, wikiTable } from './lib.mjs'; import { foldIpResource, TASK_LABEL } from './ip_fold.mjs'; const WIKI = process.argv.includes('--wiki'); const T = (h, r) => (WIKI ? wikiTable(h, r) : table(h, r)); const key = (p) => `${p.venue}/${p.year}/${p.slug}`; const head = (s) => console.log(`\n${WIKI ? '==== ' + s + ' ====' : '### ' + s}\n`); const rows = loadExtractions(); const crawled = rows.filter(POPULATIONS.crawled); const measuredFrom = rows.filter(POPULATIONS.measuredFrom); const ipTuples = (p) => p.classification.filter((t) => t.target === 'ip-address'); const ipClassified = rows.filter((p) => ipTuples(p).length > 0); console.log(`corpus ${rows.length}`); console.log(`crawled ${crawled.length}`); console.log(`measuredFrom ${measuredFrom.length}`); console.log(`ipClassified ${ipClassified.length} (${pct(ipClassified.length, rows.length)} of corpus)`); // ---------------------------------------------------------------- reach ---- head('Papers classifying an IP address, by venue and period'); { const byVenue = new Map(); for (const p of ipClassified) byVenue.set(p.venue, (byVenue.get(p.venue) ?? 0) + 1); const venueTotal = new Map(); for (const p of rows) venueTotal.set(p.venue, (venueTotal.get(p.venue) ?? 0) + 1); console.log( T( ['Venue', 'Papers classifying an IP', 'Papers in corpus', 'Share of venue'], [...byVenue.entries()] .sort((a, b) => b[1] - a[1]) .map(([v, n]) => [v, n, venueTotal.get(v), pct(n, venueTotal.get(v))]) ) ); const buckets = [ ['2010–2013', (y) => y <= 2013], ['2014–2017', (y) => y >= 2014 && y <= 2017], ['2018–2021', (y) => y >= 2018 && y <= 2021], ['2022–2024', (y) => y >= 2022 && y <= 2024], // 2025–2026 is provisional: CCS/IMC 2026 not held, IEEE S&P/WWW 2026 // incompletely selected. Labelled, not dropped. ['2025–2026*', (y) => y >= 2025], ]; console.log(); console.log( T( ['Period', 'Papers classifying an IP', 'Papers in corpus', 'Share'], buckets.map(([label, f]) => { const a = ipClassified.filter((p) => f(p.year)).length; const b = rows.filter((p) => f(p.year)).length; return [label, a, b, pct(a, b)]; }) ) ); } // -------------------------------------------------- what method, enum'd ---- head(`How the IP was classified (enum, papers of ${ipClassified.length})`); { const m = new Map(); for (const p of ipClassified) for (const t of ipTuples(p)) { if (isSentinel(t.method)) continue; if (!m.has(t.method)) m.set(t.method, new Set()); m.get(t.method).add(key(p)); } console.log( T( ['Method', 'Papers', `Share of ${ipClassified.length}`], [...m.entries()] .sort((a, b) => b[1].size - a[1].size) .map(([k, s]) => [k, s.size, pct(s.size, ipClassified.length)]) ) ); } // ------------------------------------------------------------ validation ---- head(`Whether the IP classification was validated (papers of ${ipClassified.length})`); { const m = new Map(); for (const p of ipClassified) for (const t of ipTuples(p)) { const v = t.validation ?? 'not-stated'; if (!m.has(v)) m.set(v, new Set()); m.get(v).add(key(p)); } console.log( T( ['Validation', 'Papers', `Share of ${ipClassified.length}`], [...m.entries()] .sort((a, b) => b[1].size - a[1].size) .map(([k, s]) => [k, s.size, pct(s.size, ipClassified.length)]) ) ); const gt = new Set(); for (const p of ipClassified) for (const t of ipTuples(p)) if (!isSentinel(t.groundTruthSource) && t.groundTruthSource) gt.add(key(p)); console.log(`\nnames a ground-truth source: ${gt.size} / ${ipClassified.length} (${pct(gt.size, ipClassified.length)})`); // Papers whose *only* validation value is none-reported or not-applicable. const weak = ipClassified.filter((p) => ipTuples(p).every((t) => ['none-reported', 'not-applicable', null, 'not-stated'].includes(t.validation)) ); console.log(`no validation on any IP tuple: ${weak.length} / ${ipClassified.length} (${pct(weak.length, ipClassified.length)})`); } // ------------------------------------------------- the named resources ----- head(`Which resources, folded (papers of ${ipClassified.length})`); { const fam = new Map(); // family -> {task, set} const residue = new Map(); // raw -> Set(paper) for (const p of ipClassified) for (const t of ipTuples(p)) { if (isSentinel(t.resourceName) || !t.resourceName) continue; const f = foldIpResource(t.resourceName); if (!f) { if (!residue.has(t.resourceName)) residue.set(t.resourceName, new Set()); residue.get(t.resourceName).add(key(p)); continue; } const id = f.family; if (!fam.has(id)) fam.set(id, { task: f.task, set: new Set() }); fam.get(id).set.add(key(p)); } console.log( T( ['Resource family', 'Question it answers', 'Papers', `Share of ${ipClassified.length}`], [...fam.entries()] .sort((a, b) => b[1].set.size - a[1].set.size) .filter(([, v]) => v.set.size >= 2) .map(([k, v]) => [k, TASK_LABEL[v.task], v.set.size, pct(v.set.size, ipClassified.length)]) ) ); const singles = [...fam.entries()].filter(([, v]) => v.set.size === 1); console.log(`\nfamilies named by exactly one paper: ${singles.length} (${singles.map(([k]) => k).join('; ')})`); console.log(`unfolded residue: ${residue.size} distinct strings`); for (const [k, v] of residue) console.log(` ${k} (${v.size})`); } // ---------------------------------- MaxMind spelling count, the headline ---- head('How badly exact-string counting undercounts (MaxMind)'); { const spellings = new Set(); const folded = new Set(); const exact = new Map(); const scope = []; for (const p of rows) { for (const t of p.classification) if (t.target === 'ip-address' && t.resourceName && !isSentinel(t.resourceName)) scope.push([p, t.resourceName]); for (const t of p.vantage) if (t.geolocationService && !isSentinel(t.geolocationService)) scope.push([p, t.geolocationService]); } for (const [p, name] of scope) { const f = foldIpResource(name); if (f?.family !== 'MaxMind') continue; spellings.add(name); folded.add(key(p)); if (!exact.has(name)) exact.set(name, new Set()); exact.get(name).add(key(p)); } const best = [...exact.entries()].sort((a, b) => b[1].size - a[1].size)[0]; console.log(`distinct spellings of MaxMind: ${spellings.size}`); console.log(`papers, folded: ${folded.size}`); console.log(`papers under the commonest spelling ("${best[0]}"): ${best[1].size}`); console.log(`undercount if you count exact strings: ${(100 * (1 - best[1].size / folded.size)).toFixed(0)}%`); } // -------------------------------------- who says which service they used ---- head('Papers that geolocate their own vantage point'); { const named = (pop) => { const s = new Set(); for (const p of pop) for (const t of p.vantage) if (t.geolocationService && !isSentinel(t.geolocationService)) s.add(key(p)); return s; }; console.log( T( ['Population', 'N', 'Names a geolocation service', 'Share'], [ ['crawled', crawled.length, named(crawled).size, pct(named(crawled).size, crawled.length)], [ 'measuredFrom', measuredFrom.length, named(measuredFrom).size, pct(named(measuredFrom).size, measuredFrom.length), ], ] ) ); const fam = new Map(); for (const p of rows) for (const t of p.vantage) { if (!t.geolocationService || isSentinel(t.geolocationService)) continue; const f = foldIpResource(t.geolocationService); const id = f ? f.family : `UNFOLDED: ${t.geolocationService}`; if (!fam.has(id)) fam.set(id, new Set()); fam.get(id).add(key(p)); } const total = new Set([...fam.values()].flatMap((s) => [...s])).size; console.log(`\nof the ${total} papers naming one:`); console.log( T( ['Service family', 'Papers', 'Share of ' + total], [...fam.entries()] .sort((a, b) => b[1].size - a[1].size) .filter(([, s]) => s.size >= 2) .map(([k, s]) => [k, s.size, pct(s.size, total)]) ) ); console.log( `named by one paper each: ${[...fam.entries()].filter(([, s]) => s.size === 1).length} families` ); } // ------------------------------------------------------- version stated ---- head('Do the papers say which snapshot of the database they used?'); { // A geolocation database is versioned by date. The extraction does not carry a // version field for classification resources, so this is a text proxy: does // the evidence quote or the resource name mention a date, month or version? const dated = /\b(20\d\d|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|version|v\d|snapshot|as of)\b/i; const users = ipClassified.filter((p) => ipTuples(p).some((t) => { const f = t.resourceName ? foldIpResource(t.resourceName) : null; return f && (f.task === 'geolocation' || f.task === 'routing') && t.usedOrMentioned === 'used'; }) ); const withDate = users.filter((p) => ipTuples(p).some((t) => dated.test(`${t.resourceName ?? ''} ${t.evidence?.quote ?? ''}`)) ); console.log( `papers using a third-party geo or routing dataset: ${users.length}` ); console.log( ` ...whose evidence quote carries any date/version token: ${withDate.length} (${pct(withDate.length, users.length)})` ); console.log(' (text proxy, not a schema field — treat as an upper bound)'); } // ------------------------------------------- measured results, verbatim ---- head('Measured figures on geolocation-database accuracy (detection[].prevalence)'); { const re = /geoloc|maxmind|ip2location|ipinfo|netacuity|geolite|cgn|carrier.?grade|residential prox|\basn?\b|autonomous system|anycast/i; const seen = []; for (const p of rows) for (const t of p.detection) { if (!t.prevalence) continue; if (!re.test(`${t.phenomenon} ${t.technique} ${t.metric}`)) continue; seen.push([p, t]); } console.log(`${seen.length} prevalence-bearing detection tuples match the IP-classification regex`); console.log('(full dump written to out/ip_detection_sweep.txt by the ad-hoc sweep)'); } console.log('\ndone.'); // ------------------------------ operator-published prefix lists, full text ---- // Not a schema field: does anyone in this literature cite the cloud operators' // own IP-range files? Full-text grep over every paper in the corpus. { const fs = await import('node:fs'); const path = await import('node:path'); const { dataRoot } = await import('./lib.mjs'); const re = /ip-ranges\.amazonaws|gstatic\.com\/ipranges|cloudflare\.com\/ips|public_ip_ranges\.json|ServiceTags_Public/i; const hits = []; for (const p of rows) { const f = path.join(dataRoot(), 'fulltext', String(p.year), p.venue, p.slug, 'paper.cols.txt'); if (!fs.existsSync(f)) continue; if (re.test(fs.readFileSync(f, 'utf8'))) hits.push(`${p.year} ${p.venue} ${p.slug}`); } head('Papers citing an operator-published cloud prefix-list URL (full-text grep)'); console.log(`${hits.length} of ${rows.length} papers`); for (const h of hits) console.log(` ${h}`); console.log('(a floor: undercounts papers that used a list without citing its URL)'); } // ---------------------------------------------- cross-checking behaviour ---- head('Papers naming more than one geolocation source'); { const famsFor = (p, task) => new Set( ipTuples(p) .map((t) => t.resourceName) .filter((n) => n && !isSentinel(n)) .map(foldIpResource) .filter((f) => f && f.task === task) .map((f) => f.family) ); const geoUsers = ipClassified.filter((p) => famsFor(p, 'geolocation').size > 0); const multi = geoUsers.filter((p) => famsFor(p, 'geolocation').size >= 2); console.log(`name >=1 geolocation source: ${geoUsers.length} of ${ipClassified.length} (${pct(geoUsers.length, ipClassified.length)})`); console.log(` ...of which name >=2: ${multi.length} (${pct(multi.length, geoUsers.length)} of ${geoUsers.length})`); for (const p of multi.sort((a, b) => a.year - b.year)) console.log(` ${p.year} ${p.venue} ${p.title.slice(0, 78)}`); for (const task of ['routing', 'network-type']) { const n = ipClassified.filter((p) => famsFor(p, task).size > 0); console.log(`name >=1 '${task}' source: ${n.length} of ${ipClassified.length}`); if (task === 'network-type') console.log(` earliest ${Math.min(...n.map((p) => p.year))}, ${n.filter((p) => p.year >= 2019).length} from 2019 or later`); } } // --------------------------------------- size of the folded name universe ---- head('Free-text name universe that ip_fold.mjs covers'); { const names = new Set(); for (const p of rows) { for (const t of p.classification) if (t.target === 'ip-address' && t.resourceName && !isSentinel(t.resourceName)) names.add(t.resourceName); for (const t of p.vantage) if (t.geolocationService && !isSentinel(t.geolocationService)) names.add(t.geolocationService); } const unmapped = [...names].filter((n) => !foldIpResource(n)); console.log(`distinct strings: ${names.size}`); console.log(`unmapped residue: ${unmapped.length}${unmapped.length ? ' -> ' + unmapped.join('; ') : ''}`); } // ----------------------------------------------- figures the page carried ---- // Added 2026-08-12. Each of these was on design:ip_classification and in no // script, so none of them could be re-derived when the corpus grew. If a figure // is on the page it belongs here. head('Figures the page carries that were not previously printed'); { const imc = rows.filter((p) => p.venue === 'IMC').length; const imcIp = ipClassified.filter((p) => p.venue === 'IMC').length; console.log(`IMC: ${imcIp} of ${imc}. Other six venues: ${ipClassified.length - imcIp} of ${rows.length - imc}.`); // MaxMind, across BOTH fields the fold covers, vs within the IP-classifying set. const mmAll = new Set(); const mmIp = new Set(); const mmSpellings = new Set(); for (const p of rows) { let hitIp = false; let hit = false; for (const t of p.classification) { if (t.target !== 'ip-address' || isSentinel(t.resourceName)) continue; const f = foldIpResource(t.resourceName); if (f && f.family === 'MaxMind') { hit = true; hitIp = true; mmSpellings.add(t.resourceName); } } for (const t of p.vantage) { if (isSentinel(t.geolocationService)) continue; const f = foldIpResource(t.geolocationService); if (f && f.family === 'MaxMind') { hit = true; mmSpellings.add(t.geolocationService); } } if (hit) mmAll.add(key(p)); if (hitIp) mmIp.add(key(p)); } console.log(`MaxMind: ${mmAll.size} papers across both fields, ${mmIp.size} of them inside the ${ipClassified.length} IP-classifying papers, ` + `so ${mmAll.size - mmIp.size} name it only for their own vantage point. ${mmSpellings.size} distinct spellings.`); // Crawling papers: classify an observed address vs geolocate their own vantage. const crawlIp = crawled.filter((p) => ipTuples(p).length > 0).length; const crawlGeo = crawled.filter((p) => p.vantage.some((t) => !isSentinel(t.geolocationService))).length; const crawlEither = crawled.filter( (p) => ipTuples(p).length > 0 || p.vantage.some((t) => !isSentinel(t.geolocationService)) ).length; console.log(`Crawling papers: ${crawlIp} classify an observed address, ${crawlGeo} geolocate their own vantage point, ` + `union ${crawlEither} of ${crawled.length} (${pct(crawlEither, crawled.length)}).`); }
Output (node scripts/report_ip_classification.mjs, 2026-09-22):
- report_ip_classification-output.txt
corpus 5859 crawled 1120 measuredFrom 3908 ipClassified 295 (5.0% of corpus) ### Papers classifying an IP address, by venue and period Venue Papers classifying an IP Papers in corpus Share of venue ------- ------------------------ ---------------- -------------- IMC 124 638 19.4% USENIX 55 1410 3.9% NDSS 38 701 5.4% CCS 24 990 2.4% WWW 24 843 2.8% IEEE-SP 21 767 2.7% PETS 9 510 1.8% Period Papers classifying an IP Papers in corpus Share ---------- ------------------------ ---------------- ----- 2010–2013 31 511 6.1% 2014–2017 61 769 7.9% 2018–2021 68 1439 4.7% 2022–2024 84 1955 4.3% 2025–2026* 51 1185 4.3% ### How the IP was classified (enum, papers of 295) Method Papers Share of 295 ------------------- ------ ------------ third-party-service 124 42.0% curated-database 109 36.9% heuristic-rules 88 29.8% blocklist 16 5.4% regex-or-signature 12 4.1% other 9 3.1% manual-labelling 8 2.7% supervised-ml 7 2.4% graph-analysis 5 1.7% dynamic-analysis 4 1.4% static-analysis 2 0.7% llm 1 0.3% ### Whether the IP classification was validated (papers of 295) Validation Papers Share of 295 -------------------------- ------ ------------ not-applicable 121 41.0% none-reported 99 33.6% manual-validation 54 18.3% comparison-to-other-method 49 16.6% held-out-test-set 3 1.0% cross-validation 1 0.3% names a ground-truth source: 127 / 295 (43.1%) no validation on any IP tuple: 196 / 295 (66.4%) ### Which resources, folded (papers of 295) Resource family Question it answers Papers Share of 295 -------------------------------------------------------------------- -------------------------------------- ------ ------------ Home-grown heuristic or classifier Home-grown 104 35.3% MaxMind Where is it? 65 22.0% Other IP blocklists (DShield, FireHOL, CBL, AbuseIPDB, Honey Pot, …) Is it known-bad? 33 11.2% IPinfo Where is it? 26 8.8% Router alias / router-to-AS (bdrmapIT, MAP-IT, MIDAR, Hoiho) Is it a router, and which one? 16 5.4% Team Cymru IP-to-ASN Whose network is it? 16 5.4% Censys / Shodan / Nmap / Snort / Suricata What is running on it? 15 5.1% IP2Location Where is it? 14 4.7% VirusTotal / Google Safe Browsing Is it known-bad? 14 4.7% RouteViews Whose network is it? 14 4.7% WHOIS / IRR / RIR delegation files Whose network is it? 13 4.4% Spamhaus Is it known-bad? 12 4.1% CAIDA datasets (prefix2as, AS2Org, ITDK) Whose network is it? 11 3.7% Raw BGP feeds and IX data Whose network is it? 7 2.4% RIPE RIS / RIPEstat / RIPE Atlas Whose network is it? 7 2.4% Free geo-lookup APIs (freegeoip, ipstack, HostIP, IPInfoDB, …) Where is it? 6 2.0% PeeringDB Whose network is it? 6 2.0% NetAcuity (Digital Element) Where is it? 5 1.7% RIPE IPmap Where is it? 5 1.7% Fraud-score APIs (IPQualityScore, IPHub, Scamalytics, VPNAPI, Udger) What kind of host is it? 5 1.7% ASdb Whose network is it? 5 1.7% GreyNoise What kind of host is it? 5 1.7% Unnamed commercial geo database Where is it? 4 1.4% Geocoding / positioning reference (GeoNames, Google, Skyhook, WiGLE) Location reference data (not IP-based) 4 1.4% Chinese geo databases (Chunzhen/QQWry, AIWEN, QAX, 360, Tencent) Where is it? 3 1.0% MaxMind Anonymous IP / minFraud What kind of host is it? 3 1.0% Quova / Neustar Where is it? 2 0.7% Chainalysis Is it known-bad? 2 0.7% IP2Proxy What kind of host is it? 2 0.7% pyasn / iptoasn.com Whose network is it? 2 0.7% Spur What kind of host is it? 2 0.7% families named by exactly one paper: 9 (ip-api.com; Akamai EdgeScape; CDN / platform internal geo; bgp.tools; Phone-number reference (Twilio, OpenCNAM); Cloud/hosting provider published IP ranges; Email authentication (SPF/DMARC); DB-IP; LLM (GPT-4o)) unfolded residue: 0 distinct strings ### How badly exact-string counting undercounts (MaxMind) distinct spellings of MaxMind: 50 papers, folded: 134 papers under the commonest spelling ("MaxMind"): 28 undercount if you count exact strings: 79% ### Papers that geolocate their own vantage point Population N Names a geolocation service Share ------------ ---- --------------------------- ----- crawled 1120 45 4.0% measuredFrom 3908 194 5.0% of the 194 papers naming one: Service family Papers Share of 194 -------------------------------------------------------------------- ------ ------------ MaxMind 108 55.7% IPinfo 17 8.8% Geocoding / positioning reference (GeoNames, Google, Skyhook, WiGLE) 10 5.2% IP2Location 8 4.1% Free geo-lookup APIs (freegeoip, ipstack, HostIP, IPInfoDB, …) 7 3.6% ip-api.com 7 3.6% RIPE IPmap 7 3.6% Unnamed commercial geo database 5 2.6% NetAcuity (Digital Element) 5 2.6% CDN / platform internal geo 5 2.6% Chinese geo databases (Chunzhen/QQWry, AIWEN, QAX, 360, Tencent) 2 1.0% Akamai EdgeScape 2 1.0% Team Cymru IP-to-ASN 2 1.0% Quova / Neustar 2 1.0% Home-grown heuristic or classifier 2 1.0% WHOIS / IRR / RIR delegation files 2 1.0% named by one paper each: 4 families ### Do the papers say which snapshot of the database they used? papers using a third-party geo or routing dataset: 162 ...whose evidence quote carries any date/version token: 9 (5.6%) (text proxy, not a schema field — treat as an upper bound) ### Measured figures on geolocation-database accuracy (detection[].prevalence) 630 prevalence-bearing detection tuples match the IP-classification regex (full dump written to out/ip_detection_sweep.txt by the ad-hoc sweep) done. ### Papers citing an operator-published cloud prefix-list URL (full-text grep) 7 of 5859 papers 2017 IMC large-scale-scanning-of-tcps-initial-window 2019 IEEE-SP resident-evil-understanding-residential-ip-proxy-as-a-dark-service 2020 CCS censored-planet-an-internet-wide-longitudinal-censorship-observatory 2020 NDSS cdn-judo-breaking-the-cdn-dos-protection-with-itself 2021 CCS warmonger-inflicting-denial-of-service-via-serverless-functions-in-the-cloud 2023 USENIX dscope-a-cloud-native-internet-telescope 2025 NDSS secure-ip-address-allocation-at-cloud-scale (a floor: undercounts papers that used a list without citing its URL) ### Papers naming more than one geolocation source name >=1 geolocation source: 114 of 295 (38.6%) ...of which name >=2: 14 (12.3% of 114) 2010 IMC Eyeball ASes: from geography to connectivity. 2017 IMC A look at router geolocation in public and commercial databases. 2018 IMC An Empirical Analysis of the Commercial VPN Ecosystem. 2018 IMC Tracing Cross Border Web Tracking. 2022 CCS An Extensive Study of Residential Proxies in China. 2022 IMC Are we ready for metaverse?: a measurement study of social virtual reality pla 2022 USENIX Gossamer: Securely Measuring Password-based Logins 2023 IEEE-SP IPvSeeYou: Exploiting Leaked Identifiers in IPv6 for Street-Level Geolocation. 2023 IMC How to Operate a Meta-Telescope in your Spare Time. 2023 IMC Replication: Towards a Publicly Available Internet Scale IP Geolocation Datase 2024 IMC A First Look at Immersive Telepresence on Apple Vision Pro. 2024 IMC Watching TV with the Second-Party: A First Look at Automatic Content Recogniti 2025 NDSS Wallbleed: A Memory Disclosure Vulnerability in the Great Firewall of China 2025 USENIX eSIMplicity or eSIMplification? Privacy and Security Risks in the eSIM Ecosyst name >=1 'routing' source: 68 of 295 name >=1 'network-type' source: 17 of 295 earliest 2018, 16 from 2019 or later ### Free-text name universe that ip_fold.mjs covers distinct strings: 443 unmapped residue: 0 ### Figures the page carries that were not previously printed IMC: 124 of 638. Other six venues: 171 of 5221. MaxMind: 134 papers across both fields, 65 of them inside the 295 IP-classifying papers, so 69 name it only for their own vantage point. 50 distinct spellings. Crawling papers: 54 classify an observed address, 45 geolocate their own vantage point, union 76 of 1120 (6.8%).
- ip_fold.mjs
// Fold the free-text names that appear when a paper classifies an IP address. // // Scope: the union of // classification[].resourceName where target === 'ip-address' // vantage[].geolocationService // 363 distinct strings in run1. MaxMind alone appears under 24 spellings // ("MaxMind", "Maxmind geoip", "Max-Mind GeoIP2 Precision Insights", "GeoIP: // MaxMind", "MAXMIND GeoIP2", …), so counting exact strings understates the // single most-used resource in the field by a factor of about four. // // Rules, same as scripts/tool_fold.mjs: // * explicit ordered regex families, first match wins // * specific products before the generic term they contain // (IP2Proxy before IP2Location; GeoLite before the bare "GeoIP") // * everything unmatched is returned as residue and PRINTED, never dropped // // Each family also carries the *question* it answers, because "classify an IP" // is five different measurements with five different error budgets. /** @typedef {'geolocation'|'routing'|'network-type'|'reputation'|'topology'|'scanning'|'reference'|'custom'} Task */ /** @type {{family: string, task: Task, re: RegExp}[]} */ export const FAMILIES = [ // ---- proxy / VPN / hosting detection: match before their parent vendors ---- { family: 'IP2Proxy', task: 'network-type', re: /\bip\s*-?\s*2\s*-?\s*proxy\b/i }, { family: 'MaxMind Anonymous IP / minFraud', task: 'network-type', re: /anonymous[\s-]*ip|minfraud/i, }, { family: 'Spur', task: 'network-type', re: /\bspur\b/i }, { family: 'GreyNoise', task: 'network-type', re: /grey\s*-?\s*noise/i }, { family: 'Fraud-score APIs (IPQualityScore, IPHub, Scamalytics, VPNAPI, Udger)', task: 'network-type', re: /ipqualityscore|iphub|scamalytics|vpnapi|udger|access\.watch|vpnchecker/i, }, // ---- geolocation databases and services ---- { family: 'MaxMind', task: 'geolocation', re: /max\s*-?\s*mind|geolite|geo\s*-?ip\s*2?\b|geoip/i }, { family: 'IP2Location', task: 'geolocation', re: /ip\s*-?\s*2\s*-?\s*location/i }, { family: 'IPinfo', task: 'geolocation', re: /\bip\s*-?\s*info\b|ipinfo/i }, { family: 'NetAcuity (Digital Element)', task: 'geolocation', re: /net\s*-?\s*acuity|digital\s+(element|envoy)/i, }, { family: 'DB-IP', task: 'geolocation', re: /\bdb\s*-?\s*ip\b/i }, { family: 'Quova / Neustar', task: 'geolocation', re: /quova|neustar/i }, { family: 'Akamai EdgeScape', task: 'geolocation', re: /edge\s*-?\s*scape/i }, { family: 'ip-api.com', task: 'geolocation', re: /\bip\s*-?\s*api\b/i }, { family: 'RIPE IPmap', task: 'geolocation', re: /ip\s*-?\s*map/i }, { family: 'Free geo-lookup APIs (freegeoip, ipstack, HostIP, IPInfoDB, …)', task: 'geolocation', re: /freegeoip|ipstack|hostip|ipinfodb|ipvigilante|findip|ipregistry|iplocation|keycdn|ifconfig\.co|ipaddressapi|blackbox api|ipgeolocation\.io/i, }, { family: 'Chinese geo databases (Chunzhen/QQWry, AIWEN, QAX, 360, Tencent)', task: 'geolocation', re: /chunzhen|qqwry|aiwen|\bqax\b|360 security|tencent/i, }, { family: 'CDN / platform internal geo', task: 'geolocation', re: /cloudflare|cdn'?s? (proprietary )?geoloc|instagram ip geolocation|starlink location/i, }, { family: 'Unnamed commercial geo database', task: 'geolocation', re: /^(commercial (ip )?geolocation (services|databases)|third[- ]party (commercial service|mapping database)|ip geolocation (database|lookup)|free geoip databases|majority vote across 10( geolocation)? databases)$/i, }, // ---- routing / ASN ---- { family: 'Team Cymru IP-to-ASN', task: 'routing', re: /team\s*-?\s*cymru/i }, { family: 'RouteViews', task: 'routing', re: /route\s*-?\s*views/i }, { family: 'RIPE RIS / RIPEstat / RIPE Atlas', task: 'routing', re: /\bripe(?!n)|riswhois/i, // "RIPEstat" has no word boundary after RIPE }, { family: 'CAIDA datasets (prefix2as, AS2Org, ITDK)', task: 'routing', re: /caida/i }, { family: 'PeeringDB', task: 'routing', re: /peering\s*-?\s*db/i }, { family: 'bgp.tools', task: 'routing', re: /bgp\.tools/i }, { family: 'ASdb', task: 'routing', re: /\basdb\b|as names dataset|as-name string/i }, { family: 'pyasn / iptoasn.com', task: 'routing', re: /py\s*-?\s*asn|ip\s*-?\s*to\s*-?\s*asn/i }, { family: 'WHOIS / IRR / RIR delegation files', task: 'routing', re: /whois|internet routing registry|\birr\b|regional internet registry|routinator|iana [a-z0-9 ]*registry|special-purpose address registry/i, }, { family: 'Raw BGP feeds and IX data', task: 'routing', re: /\bbgp\b|euroix|packet clearing house|longest prefix match|majority-as ownership|telegeography/i, }, // ---- reputation / abuse blocklists ---- { family: 'Spamhaus', task: 'reputation', re: /spamhaus/i, }, { family: 'Other IP blocklists (DShield, FireHOL, CBL, AbuseIPDB, Honey Pot, …)', task: 'reputation', re: /dshield|firehol|composite blocking list|\bcbl\b|abuseipdb|project honey ?pot|uceprotect|\bblag\b|emergingthreats|urlhaus|dnsbl|all\.s5h|manitu|blacklist|blocklist|malwareworld|barracuda|brightmail|botscout|bothunter|cymon|\bnotos\b|blacklisting services|spamcop|sinkdb|\bmisp\b|block lists|^bl-[a-z]$|institutional list by/i, }, { family: 'VirusTotal / Google Safe Browsing', task: 'reputation', re: /virustotal|safe\s*-?\s*browsing/i, }, { family: 'Chainalysis', task: 'reputation', re: /chainalysis/i }, // Criminal IP is an attack-surface search engine in the Censys/Shodan mould, // marketed as threat intelligence. Filed under scanning, which is what it does. { family: 'Censys / Shodan / Nmap / Snort / Suricata', task: 'scanning', re: /criminal\s*-?\s*ip/i }, // Published hosting/cloud address ranges (AWS ip-ranges.json and friends). // Distinct from a fraud-score API: this is the provider's own list. { family: 'Cloud/hosting provider published IP ranges', task: 'network-type', re: /cloud[- ]provider[- ]ip[- ]addresses|cloud provider (published )?(ip )?ranges|ip-ranges\.json/i }, // 2026-08-12: the first LLM in this field. One paper, GPT-4o. Kept as its own // family rather than folded into 'Home-grown' so it stays countable. { family: 'LLM (GPT-4o)', task: 'custom', re: /\bgpt-?\s?[0-9]|\bllm\b|claude|gemini/i }, // Email-authentication records: not an IP classifier, but the extraction files // them here because the unit of analysis is the sending IP. { family: 'Email authentication (SPF/DMARC)', task: 'reputation', re: /\bspf\b|\bdmarc\b|\bdkim\b/i }, // Phone-number reference services, from one paper whose unit was a phone // number rather than an IP. Non-IP reference data, like the geocoding row. { family: 'Phone-number reference (Twilio, OpenCNAM)', task: 'reference', re: /twilio|opencnam/i }, // ---- active scanning / host fingerprinting ---- { family: 'Censys / Shodan / Nmap / Snort / Suricata', task: 'scanning', re: /censys|shodan|\bnmap\b|\bsnort\b|suricata|whatweb|\bhershel\b|snacktime|\bfpdns\b/i, }, // ---- router topology: alias resolution and router-to-AS ---- { family: 'Router alias / router-to-AS (bdrmapIT, MAP-IT, MIDAR, Hoiho)', task: 'topology', re: /bdrmap|map\s*-?\s*it\b|midar|hoiho|alias|addr6|\bmbt\b|monotonic bounds|manycast/i, }, // ---- non-IP location reference data ---- { family: 'Geocoding / positioning reference (GeoNames, Google, Skyhook, WiGLE)', task: 'reference', re: /geonames|google (geocoding|maps|location|geolocation)|skyhook|wigle|\bgps\b|html5 geolocation|ieee oui|iata|nominatim|apple'?s? wps|wi-?fi positioning/i, }, // ---- home-grown ---- { family: 'Home-grown heuristic or classifier', task: 'custom', re: /\(custom\)|^custom\b|heuristic|hand-crafted|regular expressions|regexes|random forest|xgboost|\blstm\b|support vector|k-nearest|belief propagation|threshold random walk|network[- ]aware clusters|reverse[- ]?dns|rdns|ptr and soa|manual dns|reverse ip address lookup|network fingerprinting|tcp fingerprint|http fingerprinting|ttl-based|mpls labeling|eui-64|snmp|ssh identifier|tor (tls|consensus)|collector network status|recursion-available|too big trick|passport|trackiptree|traffic distribution system|cyberprobe|great firewall|prediction functions|apple server naming|client-side ip address map|30-day unresponsive|gasser et al|ipv6 hitlist|cbg|shortest-ping|port-variation|packet-pattern|multi-stage scoring|multi-level|trained classifier|scam address list|forrester|compiled|who is my isp|mobile carrier database|back-to-the-future|bttf|dnsdb|\blfp\b|\bhoiho\b|dns suffix|r&e\/commodity|neighbor classification|perception live behavior|asn matching against|architecture_detection/i, }, ]; /** * @param {string} name * @returns {{family: string, task: Task} | null} */ export function foldIpResource(name) { const s = String(name).trim(); for (const f of FAMILIES) { if (f.re.test(s)) return { family: f.family, task: f.task }; } return null; } export const TASK_LABEL = { geolocation: 'Where is it?', routing: 'Whose network is it?', 'network-type': 'What kind of host is it?', reputation: 'Is it known-bad?', topology: 'Is it a router, and which one?', scanning: 'What is running on it?', reference: 'Location reference data (not IP-based)', custom: 'Home-grown', };
- maxmind_version.mjs
// Do papers that use MaxMind say WHICH snapshot of MaxMind they used? // // A geolocation database is a moving target: MaxMind reissues GeoLite2 twice a // week. "We used MaxMind" is therefore not a reproducible statement. The // extraction schema has no version field for a classification resource, so this // measures it directly against the full text: for every paper whose extraction // names MaxMind, find every sentence in paper.cols.txt that mentions MaxMind and // ask whether any of them carries a date, a month, or a version/edition token. // // node scripts/maxmind_version.mjs [--dump] // // This is a generous test. A sentence saying "we crawled in March 2019 using // MaxMind" counts as dated even though the crawl date is not the database date. // Read the figure as an UPPER BOUND on how often the snapshot is identifiable. import fs from 'node:fs'; import path from 'node:path'; import { loadExtractions, isSentinel, pct, dataRoot } from './lib.mjs'; import { foldIpResource } from './ip_fold.mjs'; const DUMP = process.argv.includes('--dump'); const root = dataRoot(); const rows = loadExtractions(); const isMaxMind = (s) => s && !isSentinel(s) && foldIpResource(s)?.family === 'MaxMind'; const users = rows.filter( (p) => p.classification.some((t) => t.target === 'ip-address' && isMaxMind(t.resourceName)) || p.vantage.some((t) => isMaxMind(t.geolocationService)) ); // A month name, a year, or an explicit version/edition word within the sentence. const DATED = /\b(19|20)\d\d\b|\b(jan(uary)?|feb(ruary)?|mar(ch)?|apr(il)?|may|jun(e)?|jul(y)?|aug(ust)?|sep(t|tember)?|oct(ober)?|nov(ember)?|dec(ember)?)\b|\bversion\b|\bv\d|\brelease[sd]?\b|\bsnapshot\b|\bedition\b|\bas of\b|\bdated\b|\bvintage\b/i; // A bibliography entry ("MaxMind GeoIP. http://maxmind.com/, 2014.") carries a // year but tells the reader nothing about which snapshot was queried. Drop // sentences that look like reference-list entries before the strict count. const BIBLIKE = /https?:\/\//i; let read = 0, missing = 0, mentioned = 0, dated = 0, datedStrict = 0; const examples = []; for (const p of users) { const file = path.join(root, 'fulltext', String(p.year), p.venue, p.slug, 'paper.cols.txt'); if (!fs.existsSync(file)) { missing += 1; continue; } read += 1; const text = fs.readFileSync(file, 'utf8').replace(/\s+/g, ' '); // Split on sentence-ish boundaries; keep it crude, the unit is "nearby text". const sentences = text.split(/(?<=[.;:])\s+/); const hits = sentences.filter((s) => /max\s*-?\s*mind|geolite|geoip/i.test(s)); if (hits.length === 0) continue; // extraction says MaxMind, text does not — see note mentioned += 1; const withDate = hits.filter((s) => DATED.test(s)); const strict = withDate.filter((s) => !BIBLIKE.test(s)); if (strict.length > 0) datedStrict += 1; if (withDate.length > 0) { dated += 1; if (strict.length > 0 && examples.length < 8) examples.push([`${p.year}/${p.venue}/${p.slug}`, strict[0].slice(0, 240)]); } else if (DUMP) { console.log(`UNDATED ${p.year}/${p.venue}/${p.slug}\n ${hits[0].slice(0, 200)}`); } } console.log(`papers whose extraction names MaxMind: ${users.length}`); console.log(` full text available: ${read} (missing ${missing})`); console.log(` MaxMind/GeoIP/GeoLite found in the text: ${mentioned} (${pct(mentioned, read)})`); console.log( ` ...with a date/version token in the same sentence: ${dated} (${pct(dated, mentioned)} of ${mentioned})` ); console.log( ` ...excluding sentences that are bibliography entries: ${datedStrict} (${pct(datedStrict, mentioned)} of ${mentioned})` ); console.log('\nStill an upper bound: any year token in the sentence counts, including crawl dates.\n'); for (const [k, s] of examples) console.log(` ${k}\n ${s}\n`);
Output (node scripts/maxmind_version.mjs, 2026-09-22):
- maxmind_version-output.txt
papers whose extraction names MaxMind: 134 full text available: 134 (missing 0) MaxMind/GeoIP/GeoLite found in the text: 134 (100.0%) ...with a date/version token in the same sentence: 52 (38.8% of 134) ...excluding sentences that are bibliography entries: 36 (26.9% of 134) Still an upper bound: any year token in the sentence counts, including crawl dates. 2011/IMC/analysis-of-country-wide-internet-outages-caused-by-censorship IPv4 address space delegated to Egypt (as of January 24, 2011) and Libya (as of February 15, 2011) by AfriNIC (top half) as well as additional IPv4 address ranges associated with the two countries based on MaxMind GeoLite database (as of Ja 2013/WWW/the-anatomy-of-ldns-clusters-findings-and-implications-for-web-content-delivery Using the database dated from February 1, 2011 (so number of HTTP requests for all LDNS across all their TTL that our analysis would reflect the GeoIP map at the time intervals. 2014/IMC/censorship-in-the-wild-analyzing-internet-filtering-in-syria We geolocalize each IP address in DIP v4 using the Maxmind GeoIP database.9 We then introduce, for each identified country, the cor- 6. 2015/WWW/cookies-that-give-you-away-the-surveillance-implications-of-web-tracking All IPs returned in each hop of the traceroute were We use a simplified version of this check when examining geo-located with the MaxMind GeoLite27 country databases. 2014/USENIX/an-internet-wide-view-of-internet-wide-scanning GeoIP, 2013. 2015/IMC/leveraging-internet-background-radiation-for-opportunistic-network-analysis Since MaxMind updates the database regularly (to reflect changes in the address space), we use the databases produced on August 1, 2012 and August 16, 2013 for the 2012 census and 2013 census periods, respectively. 2015/PETS/defending-tor-from-network-adversaries-a-case-study-of-network-path-prediction However, 49% reach the AS of the destination, as determined by the Max-Mind GeoIP database [2]. 2017/IMC/a-look-at-router-geolocation-in-public-and-commercial-databases First, our recommendations 467 IMC '17, November 1-3, 2017, London, United Kingdom 0.0 0.2 0.4 0.6 0.8 1.0 ARIN (4761) APNIC (468) AFRINIC (58) LACNIC (38) RIPENCC (1523) CDF AFRINIC APNIC −4 −3 −2 −1 0 10 10 10 10 10 (a) MaxMind-Paid (41.2
14.9 Re-review of the fixes
Both reviewers whose findings were acted on re-read the drafted fixes before they were saved, told again that their context might not be exhaustive, and wrote to fable_rereview.md and sonnet_rereview.md.
| ID | From | Sev. | Finding | Action |
|---|---|---|---|---|
| R1 | fable | MAJOR | The vendor-table date line said MaxMind dropped the “2” between the two checks; the rename predates 6 August (EULA of 12 February 2026), as §14.6 itself says | Fixed: the line now says the rename had already happened and the first check missed it. |
| R2 | fable | MINOR | ''KeyError: 'cc''' in §14.2 closes the monospace one apostrophe early | Fixed with …. |
| R3 | fable | MINOR | §14.6 said the two passes “fetched every vendor, licence and URL claim”; neither pass claimed that (Livadariu's 1.50% was not fetched, NetAcuity and the IP2Proxy LITE field set were left PLAUSIBLE) | Fixed: §14.6 now scopes itself to what the two findings files enumerate. |
| R4 | fable | MINOR (PLAUSIBLE) | The IP2Location data-licence quote had no fetched copy in the run directory | Fixed: ip2l_data_license.html and ip2l_db11.html saved (2026-09-22); the first contains “redistribute or resell”, neither contains a Creative Commons link. Sonnet's re-check found DB5 also carries no badge. |
| R5 | fable | MINOR | The flags box said “this page's 2026-08-06 run of the script below”, but the script below is now the fixed version, which cannot print those lines keylessly | Fixed: “the previous version of the script below”. |
| R6 | fable | note | §14.2 says four unrecorded revisions where fable's P2 heading said five | Correct as written: the three 2026-09-03 revisions were already in §12/§13. |
| — | sonnet | — | 11 changed external claims re-fetched and CONFIRMED (MaxMind EULA date and prices, IP2Location badge only on DB1, IP2Proxy LITE = PUB, ipapi.is limits, a live dnsel.torproject.org lookup of a current exit returning 127.0.0.2, DigitalOcean 4 of 4, CJEU para. 82); IPinfo's service field PLAUSIBLE | none |
The fixes to R1–R5 were not re-reviewed a third time.
