Background

Notes on what each kind of lookup actually measures, and how much to trust it.

Geolocating an IP

There is no direct way to discern the physical location of an IP address. Locations are inferred from network delay and topology measurements combined with private and public databases. One family of algorithms starts from a set of landmarks at known locations, bounds the distance to the last router before the target using observed latency, intersects those bounds, and takes the centroid.

Accuracy is uneven and generally unquantified by the providers. Independent measurement studies consistently find that country-level assignment is reliable — error rates under about 1% across the major providers — while city-level accuracy varies by an order of magnitude, and is dramatically worse for mobile networks than for fixed ones. Treat country as solid, city as indicative, and latitude/longitude as a centroid rather than a location.

maxmind_geocode_ip reads the GeoLite2 City database locally. Anonymous downloads ended in 2019: a free MaxMind account and license key are now required.

Timezone

Timezone can be derived from coordinates, which means it inherits all the uncertainty above. Two paths are available:

  • maxmind_geocode_ip already returns location.time_zone directly from the City database. This is free and requires no extra lookup.

  • timezone_at resolves coordinates against timezone polygons offline via timezonefinder (the optional timezone extra). Useful as an independent cross-check.

  • geonames_timezone queries GeoNames over the network.

Ping and traceroute

ping sends ICMP echo requests and reports round-trip time (min, max, mean) and packet loss. traceroute maps the routers along the path and the time to each hop. Both shell out to the system commands and need no special privileges. Many hosts drop ICMP entirely, so silence is not evidence of absence.

Exposed services

  • Censys scans the IPv4 address space and reports open ports, protocols, certificates, and ASN. Requires registration; the free tier is credit-limited.

  • Shodan indexes internet-connected devices, services, and known vulnerabilities. IP lookups require a paid membership — free API keys cannot call the host endpoint.

Both report what a scanner observed at some past moment, not what is true now.

Reputation and abuse

Many organizations maintain blocklists, and they cite each other, so counting “detections” across services overstates independence.

  • VirusTotal aggregates verdicts from roughly ninety engines and reports ASN, network, RIR, JARM, and a reputation score derived from community votes. Note that VirusTotal returns categories only for domains and URLs, never for IP addresses.

  • AbuseIPDB collects user-submitted abuse reports with categories such as SSH brute force, port scanning, and web spam. days controls the lookback window, up to the API maximum of 365.

  • APIVoid reports proxy, VPN, Tor, and hosting flags alongside blocklist detections.

An IP appearing on a blocklist says something about the address, which may be shared, reassigned, or NATed behind thousands of users. It is weak evidence about any individual.

Query limits

Service

Free tier

Notes

GeoNames

10,000/day, 1,000/hour

Must enable the free web service

AbuseIPDB

1,000 checks/day

Higher on paid plans

VirusTotal

500/day, 4/minute

An unpublished monthly cap also applies

Censys

100 credits/month

Platform API

Shodan

None

IP lookup requires paid membership

APIVoid

None

30-day trial only

API reference

Know Your IP.

Collect data about IP addresses from multiple services:

  • Geolocation (latitude/longitude, country, city, timezone)

  • Reputation and abuse reports (AbuseIPDB, VirusTotal, APIVoid)

  • Exposed services and open ports (Shodan, Censys)

  • Network diagnostics (ping, traceroute)

exception know_your_ip.ConfigurationError[source]

Bases: Exception

Raised when configuration cannot be loaded or fails validation.

class know_your_ip.EnrichResult(records=<factory>, manifest=<factory>)[source]

Bases: object

The records from an enrichment run, plus how they were produced.

Returned rather than a bare DataFrame for two reasons: pandas is an optional extra, so the core path must not require it; and the manifest needs somewhere to live that a plain list of dicts does not provide.

Parameters:
records: list[dict[str, Any]]
manifest: dict[str, Any]
property columns: list[str]

Every field name present across the records, in first-seen order.

Providers return different fields for different addresses, so the union is what a table needs.

Returns:

Ordered field names.

property errors: dict[str, int]

How many records carry an error, per provider.

Returns:

Provider name to affected record count.

property canonical: list[dict[str, Any]]

one column per variable rather than per vendor.

Providers spell the same thing differently - country arrives about fifteen ways across five providers. This reduces them to canonical columns, each carrying which sources reported and whether they agreed.

Returns:

One canonical record per address.

Type:

The joined view

tidy()[source]

Long form: one row per address, field, and source.

This is the shape for comparing sources - “which disagreed, and how” becomes a groupby rather than reading forty columns by eye.

Returns:

Rows of {ip, field, source, value}.

Return type:

list[dict[str, Any]]

property disagreements: list[dict[str, Any]]

Every field where sources reported different values.

Returns:

Rows of {ip, field, chosen, values, sources}.

to_dataframe(shape='canonical')[source]

Return the records as a pandas DataFrame.

Parameters:

shape (str) – "canonical" for the joined table (default), "raw" for every vendor-shaped field, or "tidy" for long form.

Returns:

A DataFrame.

Raises:
  • ImportError – If the optional pandas extra is not installed.

  • ValueError – If shape is not one of the three known shapes.

Return type:

Any

to_csv(path, columns=None, shape='canonical')[source]

Write the records to a CSV file.

Parameters:
  • path (str | Path) – Destination file.

  • columns (list[str] | None) – Explicit column order. Defaults to the columns present.

  • shape (str) – "canonical" for the joined table (default), "raw" for every vendor-shaped field, or "tidy" for long form.

Returns:

The path written.

Raises:

ValueError – If shape is not one of the three known shapes.

Return type:

Path

exception know_your_ip.InvalidIPError[source]

Bases: ValueError

Raised when a value is not a valid IP address.

class know_your_ip.KnowYourIPConfig(*, maxmind=<factory>, geonames=<factory>, abuseipdb=<factory>, ping=<factory>, traceroute=<factory>, timezone=<factory>, network=<factory>, rdap=<factory>, ranges=<factory>, apivoid=<factory>, censys=<factory>, shodan=<factory>, virustotal=<factory>, output=<factory>)[source]

Bases: BaseModel

Top-level configuration.

Parameters:
  • maxmind (MaxMindConfig)

  • geonames (GeoNamesConfig)

  • abuseipdb (AbuseIPDBConfig)

  • ping (PingConfig)

  • traceroute (TracerouteConfig)

  • timezone (TimezoneConfig)

  • network (NetworkConfig)

  • rdap (RDAPConfig)

  • ranges (RangesConfig)

  • apivoid (APIVoidConfig)

  • censys (CensysConfig)

  • shodan (ShodanConfig)

  • virustotal (VirusTotalConfig)

  • output (OutputConfig)

model_config = {'extra': 'forbid'}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

maxmind: MaxMindConfig
geonames: GeoNamesConfig
abuseipdb: AbuseIPDBConfig
ping: PingConfig
traceroute: TracerouteConfig
timezone: TimezoneConfig
network: NetworkConfig
rdap: RDAPConfig
ranges: RangesConfig
apivoid: APIVoidConfig
censys: CensysConfig
shodan: ShodanConfig
virustotal: VirusTotalConfig
output: OutputConfig
know_your_ip.abuseipdb_api(config, ip)[source]

Check an IP address against AbuseIPDB.

Parameters:
  • config (KnowYourIPConfig) – Configuration object holding the AbuseIPDB API key and lookback window.

  • ip (str) – An IP address.

Returns:

Abuse fields prefixed with abuseipdb..

Return type:

dict[str, Any]

References

https://docs.abuseipdb.com/

Example

>>> abuseipdb_api(config, "222.186.30.49")
know_your_ip.apivoid_api(config, ip)[source]

Get IP reputation data from APIVoid.

Parameters:
  • config (KnowYourIPConfig) – Configuration object holding the APIVoid API key.

  • ip (str) – An IP address.

Returns:

Reputation fields prefixed with apivoid..

Return type:

dict[str, Any]

Note

Uses APIVoid API v2. The v1 endpoint reached its announced end of life in February 2026. v2 is a POST with an X-API-Key header and returns the report at the top level rather than under data.report.

There is no permanent free tier - only a 30-day trial.

References

https://www.apivoid.com/api/ip-reputation/

know_your_ip.canonicalize(record)[source]

Reduce a record’s vendor-shaped fields to canonical columns.

Parameters:

record (dict[str, Any]) – A record as returned by query_ip.

Returns:

Canonical fields, each accompanied by <field>.sources and, where more than one source reported, <field>.agree. <field>.values appears only when sources disagreed.

Return type:

dict[str, Any]

Example

>>> canonicalize({"ip": "8.8.8.8", "censys.asn": 15169})["asn"]
15169
know_your_ip.censys_api(config, ip)[source]

Get host data from the Censys Platform API.

Parameters:
  • config (KnowYourIPConfig) – Configuration object holding the Censys base URL and API key.

  • ip (str) – An IP address.

Returns:

Host fields prefixed with censys..

Return type:

dict[str, Any]

Note

Legacy Search (search.censys.io) was disabled for free accounts in March 2025 and is deprecated entirely in September 2026. Authentication is a Personal Access Token. organization_id is optional and should be omitted on the free tier, which allows 100 credits/month.

References

https://docs.censys.com/docs/platform-api-transition-guide

know_your_ip.create_default_config(output_file)[source]

Write a default configuration file.

Parameters:

output_file (Path) – Destination path. Parent directories are created.

Return type:

None

know_your_ip.enrich(ips, *, config=None, providers=None, as_of=None, cache=None, max_age=None, max_workers=5)[source]

Enrich many IP addresses concurrently.

Requests are paced to each provider’s published rate limit, so raising max_workers does not produce rate-limit errors. Invalid addresses are skipped and recorded in the manifest rather than aborting the run, and a provider failing on one address does not affect the others.

Parameters:
  • ips (Iterable[str]) – The addresses to enrich.

  • config (KnowYourIPConfig | None) – Configuration. Loaded from the standard locations if omitted.

  • providers (list[str] | None) – Provider names to run. Defaults to those enabled in config.

  • as_of (date | None) – Ask what was true on this date. Providers without historical support are skipped rather than answering with current data.

  • cache (Cache | str | Path | None) – A Cache, or a path to open one at. Re-running over cached addresses costs no API quota.

  • max_age (timedelta | None) – How stale a cached observation may be before it is refetched.

  • max_workers (int) – Concurrent lookups.

Returns:

An EnrichResult holding the records and a run manifest.

Raises:

ValueError – If max_workers is less than one.

Return type:

EnrichResult

Example

>>> result = enrich(["8.8.8.8", "1.1.1.1"], providers=["network"])
>>> len(result)
2
know_your_ip.enrich_csv(input_path, output_path=None, *, column=None, columns=None, **kwargs)[source]

Enrich the addresses in a file and optionally write a CSV.

Parameters:
  • input_path (str | Path) – A text file of addresses, one per line, or a CSV.

  • output_path (str | Path | None) – Where to write results. No file is written if omitted.

  • column (str | None) – Column holding addresses when the input is a CSV with a header. If omitted, the file is read as one address per line.

  • columns (list[str] | None) – Column order for the output. Defaults to every field collected.

  • **kwargs (Any) – Passed to enrich().

Returns:

An EnrichResult.

Return type:

EnrichResult

Example

>>> enrich_csv("ips.csv", "out.csv", column="ip")
know_your_ip.geonames_timezone(config, lat, lng)[source]

Get timezone information for a coordinate from GeoNames.

Parameters:
Returns:

GeoNames fields prefixed with geonames..

Return type:

dict[str, Any]

Note

Free tier is 10,000 credits/day and 1,000/hour per username.

Example

>>> geonames_timezone(config, 32.0617, 118.7778)
know_your_ip.load_config(config_file=None)[source]

Load configuration from a TOML file and environment variables.

Defaults are overridden by the file, which is overridden by environment variables.

Parameters:

config_file (Path | None) – Path to a configuration file. If None, standard locations are searched.

Returns:

A validated configuration object.

Raises:

ConfigurationError – If the file cannot be read or validation fails.

Return type:

KnowYourIPConfig

know_your_ip.maxmind_geocode_ip(config, ip)[source]

Look up an IP address in the MaxMind GeoLite2 City database.

Parameters:
Returns:

Geolocation fields prefixed with maxmind.. Empty if the address is absent from the database.

Raises:

FileNotFoundError – If GeoLite2-City.mmdb is not at the configured db_path.

Return type:

dict[str, Any]

Note

Reads the database directly with maxminddb. The geoip2 wrapper removed its raw attribute in 5.0 and pulls in aiohttp; maxminddb.Reader.get() returns the same underlying record with no additional dependencies.

A MaxMind account and license key are required to download GeoLite2; anonymous downloads ended in 2019.

know_your_ip.ping(config, ip)[source]

Measure round-trip time to an IP address.

Parameters:
  • config (KnowYourIPConfig) – Configuration object holding ping count and timeout.

  • ip (str) – An IP address.

Returns:

Fields prefixed with ping.. Timing keys are absent if the host did not respond.

Return type:

dict[str, Any]

know_your_ip.query_ip(config, ip, *, providers=None, as_of=None, cache=None, max_age=None)[source]

Collect data on an IP address from every selected provider.

Each provider is isolated: a failure in one is recorded under <provider>.error without preventing the others from running.

Parameters:
  • config (KnowYourIPConfig) – Configuration object.

  • ip (str) – An IP address.

  • providers (list[str] | None) – Provider names to run. Defaults to those enabled in config.

  • as_of (date | None) – Ask what was true on this date. Providers without historical support are skipped rather than answering with present-day data.

  • cache (Cache | None) – Cache to read from and append to, if any.

  • max_age (timedelta | None) – How stale a cached observation may be before it is refetched.

Returns:

Every field collected, keyed by <provider>.<field>. Use select_columns() to restrict the result to a chosen subset.

Return type:

dict[str, Any]

Example

>>> query_ip(config, "8.8.8.8")
{'ip': '8.8.8.8', 'maxmind.country.names.en': 'United States', ...}
know_your_ip.range_lookup(config, ip)[source]

Report which published networks an address belongs to.

Parameters:
  • config (Any) – Configuration object holding the ranges section.

  • ip (str) – An IP address.

Returns:

Fields prefixed with ranges..

Return type:

dict[str, Any]

know_your_ip.select_columns(record, columns)[source]

Restrict a record to a set of columns.

query_ip returns every field it collected. Use this when you want only the subset named in config.output.columns.

Parameters:
Returns:

A new dict containing only keys present in both record and columns.

Return type:

dict[str, Any]

know_your_ip.shodan_api(config, ip)[source]

Get host data from Shodan.

Parameters:
  • config (KnowYourIPConfig) – Configuration object holding the Shodan API key.

  • ip (str) – An IP address.

Returns:

Host fields prefixed with shodan..

Raises:

ImportError – If the optional shodan extra is not installed.

Return type:

dict[str, Any]

Note

IP lookups require a paid membership; free API keys cannot call the host endpoint.

know_your_ip.tidy(records)[source]

Reshape records into one row per (address, field, source).

The long form is what comparing sources actually wants, and what belongs in a paper’s appendix: it makes “which sources disagreed, and how” a groupby rather than a manual reading of forty columns.

Parameters:

records (list[dict[str, Any]]) – Records as returned by query_ip.

Returns:

Rows of {ip, field, source, value}.

Return type:

list[dict[str, Any]]

know_your_ip.timezone_at(config, lat, lng)[source]

Get the timezone name for a coordinate, offline.

Parameters:
Returns:

An IANA timezone name, or None if the coordinate maps to no timezone.

Raises:

ImportError – If the optional timezone extra is not installed.

Return type:

str | None

Note

Backed by timezonefinder. The finder loads a polygon dataset on first use and is cached for the lifetime of the process.

MaxMind’s City database already reports location.time_zone, so this is useful mainly as an independent cross-check.

Example

>>> timezone_at(config, 32.0617, 118.7778)
'Asia/Shanghai'
know_your_ip.traceroute(config, ip)[source]

Trace the network path to an IP address.

Parameters:
  • config (KnowYourIPConfig) – Configuration object holding the hop limit.

  • ip (str) – An IP address.

Returns:

Fields prefixed with traceroute..

Return type:

dict[str, Any]

know_your_ip.validate_ip(ip)[source]

Validate and normalize an IP address.

Parameters:

ip (str) – Candidate IPv4 or IPv6 address.

Returns:

The normalized string form of the address.

Raises:

InvalidIPError – If ip is not a valid IP address.

Return type:

str

Example

>>> validate_ip(" 8.8.8.8 ")
'8.8.8.8'
know_your_ip.virustotal_api(config, ip)[source]

Get an IP address report from VirusTotal API v3.

Parameters:
  • config (KnowYourIPConfig) – Configuration object holding the VirusTotal API key.

  • ip (str) – An IP address.

Returns:

Fields prefixed with virustotal., including analysis counts, reputation, network metadata, and vote totals.

Return type:

dict[str, Any]

Note

Public API limits are 500 requests/day and 4 requests/minute.

VirusTotal does not return a categories attribute for IP address objects; categories exists only on domain and URL objects.

References

https://docs.virustotal.com/reference/ip-info

Example

>>> virustotal_api(config, "8.8.8.8")

The join

The join: one column per variable, not one per vendor.

Providers describe the same things in different words. For a single address, MaxMind reports country.iso_code plus country names in ten languages, Censys reports country_code, AbuseIPDB country_code, VirusTotal country, and RDAP country - about fifteen columns naming three different concepts. Reconciling that is work every user would otherwise repeat.

This module maps provider fields onto canonical names, and records whether the sources agreed. Disagreement is reported rather than resolved away: geolocation sources genuinely differ, and a table that hides that is worse than one that shows it.

The mapping is deliberately conservative. Two fields merge only when they mean the same thing, and several near-misses are kept apart on purpose - see SEPARATE_BY_DESIGN.

know_your_ip.schema.canonicalize(record)[source]

Reduce a record’s vendor-shaped fields to canonical columns.

Parameters:

record (dict[str, Any]) – A record as returned by query_ip.

Returns:

Canonical fields, each accompanied by <field>.sources and, where more than one source reported, <field>.agree. <field>.values appears only when sources disagreed.

Return type:

dict[str, Any]

Example

>>> canonicalize({"ip": "8.8.8.8", "censys.asn": 15169})["asn"]
15169
know_your_ip.schema.canonical_columns()[source]

Every column canonicalize() can produce, in registry order.

Returns:

Column names, suitable as a default CSV header.

Return type:

list[str]

know_your_ip.schema.tidy(records)[source]

Reshape records into one row per (address, field, source).

The long form is what comparing sources actually wants, and what belongs in a paper’s appendix: it makes “which sources disagreed, and how” a groupby rather than a manual reading of forty columns.

Parameters:

records (list[dict[str, Any]]) – Records as returned by query_ip.

Returns:

Rows of {ip, field, source, value}.

Return type:

list[dict[str, Any]]

Published network ranges

Membership in published network ranges: clouds, CDNs, Tor exits, crawlers.

Tor’s exit list, AWS’s ip-ranges.json, GCP’s cloud.json, Cloudflare’s address list, Fastly’s, and Google’s and Bing’s crawler ranges are all the same operation - fetch a published list of CIDRs and test membership. That is one implementation, not six.

Every source here is the operator’s own published file: free, keyless, and authoritative in a way a third-party guess about “is this a datacenter” is not. For research the question these answer is often the real one - whether an address is a person or a machine.

Lists are cached on disk, so a batch run costs one fetch per source rather than one per address.

class know_your_ip.ranges.RangeSource(name, url, kind, parser)[source]

Bases: object

A published list of network ranges.

Parameters:
  • name (str) – Identifier used in output, e.g. aws.

  • url (str) – Where the list is published.

  • kind (str) – What membership implies - cloud, cdn, tor, or bot.

  • parser (str) – Name of the function that turns the payload into CIDRs.

class know_your_ip.ranges.RangeIndex[source]

Bases: object

Membership lookup over many networks.

AWS alone publishes around seven thousand prefixes, so scanning every network for every address does not survive a real batch: ten thousand addresses would be seventy million comparisons. Networks are bucketed by their first octet, which reduces a lookup to a few dozen comparisons.

add(cidrs, source, kind)[source]

Index a source’s networks.

Parameters:
  • cidrs (list[str]) – Networks or bare addresses.

  • source (str) – Source name.

  • kind (str) – What membership implies.

Returns:

How many entries were indexed.

Return type:

int

lookup(ip)[source]

Find every source whose ranges contain an address.

Parameters:

ip (str) – The address.

Returns:

(source, kind) pairs, possibly empty.

Return type:

list[tuple[str, str]]

property sources: set[str]

Names of the sources indexed.

know_your_ip.ranges.build_index(ttl=86400, sources=None)[source]

Fetch and index every configured source.

Parameters:
  • ttl (int) – Seconds a cached list stays usable.

  • sources (tuple[RangeSource, ...] | None) – Sources to index. Defaults to SOURCES, resolved at call time rather than bound as a default argument so the module-level list stays overridable.

Returns:

The populated index. Sources that could not be fetched are skipped with a warning rather than failing the run.

Return type:

RangeIndex

know_your_ip.ranges.get_index(ttl=86400)[source]

Return the process-wide index, building it on first use.

Parameters:

ttl (int) – Seconds a cached list stays usable. A different value than the index was built with rebuilds it, so the setting is not silently ignored after the first call.

Returns:

The shared index.

Return type:

RangeIndex

know_your_ip.ranges.reset_index()[source]

Discard the cached index. Intended for tests.

Return type:

None

know_your_ip.ranges.range_lookup(config, ip)[source]

Report which published networks an address belongs to.

Parameters:
  • config (Any) – Configuration object holding the ranges section.

  • ip (str) – An IP address.

Returns:

Fields prefixed with ranges..

Return type:

dict[str, Any]