iporigin: Tell a Datacenter IP from a Home Connection, Offline and With No API Key

Datacenter servers on one side, homes on the other, with a magnifying glass sorting the traffic between them

"Is this visitor on a server or on a home connection?" comes up all the time. We ask it when scoring sign-ups, when filtering scrapers out of analytics, when deciding whether an abuse report is worth acting on, and when flagging logins that come from hosting ranges.

The usual answers are a paid API, which adds a network call, a key and a bill to every request, or a hand-maintained list of CIDRs that quietly goes stale within a month. We wanted a third option, so we built iporigin.

>>> import iporigin
>>> iporigin.classify("52.95.110.1")
Origin(ip='52.95.110.1', kind='hosting', provider='Amazon AWS', source='local')
>>> iporigin.is_datacenter("8.8.8.8")
True
>>> iporigin.is_datacenter("127.0.0.1")
False

No network calls, no signup, no runtime dependencies. The answer comes from a table bundled with the package, and a lookup takes about two microseconds.

What it tells you

Every address gets a kind, and a provider name when one is known:

kind meaning examples
hosting a machine in a cloud or hosting provider AWS, Hetzner, OVHcloud
cdn edge infrastructure in front of other people's sites Cloudflare, Akamai
vpn a consumer VPN or private relay exit Mullvad, ProtonVPN, Apple Private Relay
tor a Tor exit node
bot a declared crawler or an AI company's outbound IP Googlebot, GPTBot, Meta
reserved private, loopback, link-local, documentation 127.0.0.1, 10.0.0.0/8
unknown in none of the lists, most often a residential ISP

Two sets cover the common decisions, and they are deliberately different questions:

origin.kind in iporigin.DATACENTER_KINDS   # a machine, not a home line
origin.kind in iporigin.ANONYMIZER_KINDS   # vpn or tor

Someone browsing through Mullvad is a person at home, but the address they arrive from is a server, so it is in both sets. You may well want to rate-limit one and refuse the other.

unknown means absence of evidence. It is not a claim that the address is residential, and that difference matters if you are about to block someone over it.

From the shell

The package installs an iporigin command, which reads addresses from its arguments or from standard input:

$ iporigin 8.8.8.8 140.82.121.4 185.220.101.1 66.249.66.1 192.168.1.1
8.8.8.8                                  hosting   Google
140.82.121.4                             hosting   GitHub
185.220.101.1                            tor       Tor
66.249.66.1                              bot       Googlebot
192.168.1.1                              reserved

$ cut -d' ' -f1 access.log | iporigin --datacenter-only --json
{"ip": "34.82.1.5", "kind": "hosting", "provider": "Google Cloud", ...}

That second line is the one we reach for most: pipe a web server log through it and see which of your "visitors" are really machines. In Python, iporigin.classify_many() does the same for a batch and loads the table only once.

Where the data comes from

Most cloud providers publish their own address ranges, so the list can be compiled from source instead of maintained by hand. iporigin's dataset has three tiers, and every source is recorded with its licence in the repository's NOTICE file:

  • Published by the provider. AWS, Google and Google Cloud, Azure, DigitalOcean, Linode, Vultr, Oracle Cloud, GitHub, Cloudflare and Fastly, each from the company's own feed.
  • Community-maintained lists. Hetzner and OVH publish nothing machine-readable, and a large share of abusive traffic comes from exactly those two. Consumer VPN exits, Tor and crawler ranges have the same problem. These come from two public-domain (CC0) community repositories.
  • Used with the maintainer's permission. Two further repositories have no licence file, which would normally rule them out. We asked their maintainers, and they said yes.

We only take what adds coverage. The best-known list of this kind was left out on purpose: every one of 211,616 sampled addresses from it was already covered, and a source that adds nothing is still one more endpoint that can break the rebuild.

Around 454,000 published prefixes collapse into roughly 40,000 disjoint IPv4 and IPv6 ranges across 48 providers. A GitHub Action rebuilds the table every week, and a monthly release ships it to PyPI when it has changed, so pip install --upgrade iporigin is never more than about a month behind the feeds. The build also refuses to replace the dataset if it shrinks by more than 20%: a feed that suddenly answers with an empty body looks exactly like a provider giving up its address space, and nothing else would catch it.

You don't have to trust our copy, either. python tools/build_dataset.py rebuilds it from the same feeds.

Why the lookup is one bisect

The ranges are stored as sorted integer start/end pairs, so a lookup is one bisect and one comparison. The part that matters is making them disjoint.

The feeds overlap: GitHub runs on Azure and AWS, so its prefixes sit inside theirs. A bisect looks at exactly one candidate, and with overlapping ranges that candidate can be a narrow range that ends just before the address while a wider one still contains it. The result would be unknown for an address that is plainly in the table. So the build sweeps all ranges into a disjoint partition, and where they overlap the narrowest one wins. An address in GitHub's range inside Azure answers "GitHub", which is the more specific truth.

The table is loaded lazily on the first lookup, so importing the library and never calling it costs nothing.

Honest about the gaps

  • Most consumer VPNs are not in the table. Only providers whose exits are publicly tracked are. For broader coverage, the optional iporigin.online module asks our free Unblock Master IP API, which runs its own VPN detection. It falls back to the offline answer if the request fails, so it is safe in a request path, and it is the only part of the library that ever touches the network.
  • Some provider-owned addresses are outside the published ranges. 1.1.1.1 is Cloudflare's resolver, but it is not in Cloudflare's edge list, so it comes back unknown.
  • The data is as accurate as the feeds. A range reassigned yesterday is wrong until the next rebuild.

Install

pip install iporigin

Python 3.8 or later, no dependencies.

Give it a try

iporigin is MIT-licensed and the code is on GitHub: github.com/Yuix-Networks/iporigin. Issues and pull requests are welcome, especially pointers to providers that publish their ranges and are not in the table yet. And if it saves you an API bill, a star helps other people find it.