openasn 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.simplecov +25 -0
- data/AGENTS.md +23 -0
- data/CHANGELOG.md +52 -0
- data/LICENSE.txt +21 -0
- data/README.md +242 -0
- data/Rakefile +34 -0
- data/lib/generators/openasn/install/install_generator.rb +68 -0
- data/lib/generators/openasn/install/templates/openasn.rb +68 -0
- data/lib/openasn/binary_format.rb +305 -0
- data/lib/openasn/cidr_utils.rb +49 -0
- data/lib/openasn/classifier.rb +152 -0
- data/lib/openasn/configuration.rb +163 -0
- data/lib/openasn/data/seed/fetch-manifest.json +589 -0
- data/lib/openasn/data/seed/manifest.json +99 -0
- data/lib/openasn/data/seed/openasn-ipv4.bin +0 -0
- data/lib/openasn/data/seed/openasn-ipv6.bin +0 -0
- data/lib/openasn/dataset.rb +117 -0
- data/lib/openasn/errors.rb +22 -0
- data/lib/openasn/http_client.rb +86 -0
- data/lib/openasn/ip.rb +70 -0
- data/lib/openasn/middleware.rb +49 -0
- data/lib/openasn/overlay_store.rb +124 -0
- data/lib/openasn/parsers.rb +515 -0
- data/lib/openasn/railtie.rb +30 -0
- data/lib/openasn/result.rb +93 -0
- data/lib/openasn/snapshot.rb +171 -0
- data/lib/openasn/special_ranges.rb +44 -0
- data/lib/openasn/tier_b.rb +223 -0
- data/lib/openasn/update_job.rb +32 -0
- data/lib/openasn/updater.rb +173 -0
- data/lib/openasn/version.rb +5 -0
- data/lib/openasn.rb +115 -0
- metadata +85 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7f423b3d573d4d10e9062c210b0f1704520518dd0984beb6229092c36aa18c91
|
|
4
|
+
data.tar.gz: e9c15ca9d880ee0a2fd762c9b7253bfd5bbe92e50b990ac62191ab3cae59b5eb
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a269180e3fd9f820e2a24f1ae0f5747fd610c4cc81a2e0b09439970cecba931c4059e3ca21fac194b271709532d40a44cdfc9345ce2c19a48891b36b93401775
|
|
7
|
+
data.tar.gz: cc2a87918a1869b661fc5624c36779e0a33ee98c7ba3db9d35686469ab9a7622ebeae77e8d77a6f4e71dc9ef0801c4c00854f32beb9e211726f8b2957463e25f
|
data/.simplecov
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
SimpleCov.start do
|
|
4
|
+
formatter SimpleCov::Formatter::SimpleFormatter
|
|
5
|
+
|
|
6
|
+
add_filter "/test/"
|
|
7
|
+
|
|
8
|
+
track_files "{lib,app}/**/*.rb"
|
|
9
|
+
|
|
10
|
+
enable_coverage :branch
|
|
11
|
+
|
|
12
|
+
minimum_coverage line: 80, branch: 65
|
|
13
|
+
|
|
14
|
+
command_name "Job #{ENV['TEST_ENV_NUMBER']}" if ENV['TEST_ENV_NUMBER']
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
SimpleCov.at_exit do
|
|
18
|
+
SimpleCov.result.format!
|
|
19
|
+
puts "\n" + "=" * 60
|
|
20
|
+
puts "COVERAGE SUMMARY"
|
|
21
|
+
puts "=" * 60
|
|
22
|
+
puts "Line Coverage: #{SimpleCov.result.covered_percent.round(2)}%"
|
|
23
|
+
puts "Branch Coverage: #{SimpleCov.result.coverage_statistics[:branch]&.percent&.round(2) || 'N/A'}%"
|
|
24
|
+
puts "=" * 60
|
|
25
|
+
end
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to AI agents (and humans) working in this repository.
|
|
4
|
+
|
|
5
|
+
This is the Ruby client for the OpenASN data project. Read `README.md`
|
|
6
|
+
first; the data semantics, artifact format (FORMAT.md), precedence
|
|
7
|
+
rationale (DECISIONS.md) and legal design live in the data repo:
|
|
8
|
+
https://github.com/openasn/openasn
|
|
9
|
+
|
|
10
|
+
Hard rules:
|
|
11
|
+
|
|
12
|
+
1. **Zero runtime dependencies.** stdlib only. Rails/ActiveJob integrations
|
|
13
|
+
stay conditionally defined. Do not add gems.
|
|
14
|
+
2. **The precedence ladder in lib/openasn/classifier.rb is the product.**
|
|
15
|
+
Each line encodes a documented false-positive lesson; change only with
|
|
16
|
+
the data repo's DECISIONS.md in hand, and mirror spot-panel changes.
|
|
17
|
+
3. **Data never ships via gem releases** — the gem versions on code; data
|
|
18
|
+
flows through the nightly GitHub Releases + UpdateJob. (The bundled
|
|
19
|
+
seed is refreshed via `rake seed:refresh` only as part of a code release.)
|
|
20
|
+
4. **Readers never block.** Snapshot swaps are single ivar assignments;
|
|
21
|
+
keep locks away from the lookup path.
|
|
22
|
+
5. Tests are offline (WebMock). `bundle exec rake test` must stay green on
|
|
23
|
+
Ruby 3.1+.
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.2.0] - 2026-07-06
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- 34 new Tier B VPN provider sources (fetch-manifest entries + parsers),
|
|
8
|
+
organized into config groups:
|
|
9
|
+
- `vpn_providers` (enabled by default — provider-published exact-IP
|
|
10
|
+
endpoints): Mullvad, IVPN, Private Internet Access, AirVPN, Windscribe,
|
|
11
|
+
PrivadoVPN, RiseupVPN, WLVPN, WorldVPN, OVPN, and Anonine — joining
|
|
12
|
+
ProtonVPN from 0.1.0.
|
|
13
|
+
- `vpn_heavy` (opt-in): NordVPN (~35 MB API response; deliberate opt-in).
|
|
14
|
+
- `vpn_dns` (opt-in — provider-published hostnames resolved via local DNS):
|
|
15
|
+
Surfshark, IPVanish, PrivateVPN, PureVPN, TorGuard, FastestVPN, VPNSecure,
|
|
16
|
+
TunnelBear, StrongVPN, VyprVPN, Giganews, SlickVPN, AzireVPN, VPN.ac, and
|
|
17
|
+
Trust.Zone.
|
|
18
|
+
- `public_relays` (opt-in — volunteer-run, high-churn): VPN Gate, VPNBook,
|
|
19
|
+
and FreeVPN.us.
|
|
20
|
+
- New parser machinery, all stdlib and dependency-free: HTML table/status
|
|
21
|
+
parsers, a bounded ZIP reader for `.ovpn` archive sources (64 MB inflate
|
|
22
|
+
cap), threaded DNS hostname resolution with an injectable
|
|
23
|
+
`Configuration.dns_resolver` hook, and `HttpClient#post_form` for provider
|
|
24
|
+
endpoints that require form POSTs (redirects preserve method and headers).
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Overlay lookups are served from a per-family index precomputed once per
|
|
29
|
+
snapshot — measurably faster lookups with many overlays enabled (the old
|
|
30
|
+
per-lookup scan was allocation-heavy past ~8 overlays).
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- The default dataset URL is now the tag-addressed
|
|
35
|
+
`releases/download/latest/…` form, which stays pinned to the rolling
|
|
36
|
+
release no matter which release holds GitHub's "Latest" badge. The
|
|
37
|
+
superficially equivalent `releases/latest/download/…` form resolves via the
|
|
38
|
+
badge and briefly served a frozen weekly snapshot on 2026-07-05. Applied
|
|
39
|
+
everywhere the URL appears (default config, install generator template,
|
|
40
|
+
docs) and pinned by a regression test. See the data repo's DECISIONS.md
|
|
41
|
+
D-REL-1.
|
|
42
|
+
|
|
43
|
+
## [0.1.0] - 2026-07-05
|
|
44
|
+
|
|
45
|
+
- Initial release: offline IP origin classification (verdict-first API),
|
|
46
|
+
bundled data seed, nightly refresh from OpenASN releases, Tier B
|
|
47
|
+
fetch-manifest executor (Apple Private Relay, Tor exits, cloud provider
|
|
48
|
+
ranges, provider-attributed VPN overlays), Rack middleware, Rails install
|
|
49
|
+
generator.
|
|
50
|
+
|
|
51
|
+
[0.2.0]: https://github.com/openasn/openasn-ruby/compare/v0.1.0...v0.2.0
|
|
52
|
+
[0.1.0]: https://github.com/openasn/openasn-ruby/releases/tag/v0.1.0
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Javi R
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# 🛰️ `openasn` — offline IP origin intelligence for Ruby
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/rb/openasn) [](https://github.com/openasn/openasn-ruby/actions)
|
|
4
|
+
|
|
5
|
+
> [!TIP]
|
|
6
|
+
> **🚀 Ship your next Rails app 10x faster!** I've built **[RailsFast](https://railsfast.com/?ref=openasn)**, a production-ready Rails boilerplate template that comes with everything you need to launch a software business in days, not weeks. Go [check it out](https://railsfast.com/?ref=openasn)!
|
|
7
|
+
|
|
8
|
+
`openasn` tells you what kind of network an IP address is *really* coming from — **residential ISP, mobile carrier, hosting/datacenter, VPN, Tor exit, iCloud Private Relay, enterprise gateway, business, education, government, CGNAT** — entirely offline, in ~20 microseconds, with **zero API calls, zero per-lookup cost, and zero runtime dependencies**.
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
result = OpenASN.lookup(request.remote_ip)
|
|
12
|
+
|
|
13
|
+
result.verdict # => :residential_isp
|
|
14
|
+
result.infrastructure? # => false (true only for :hosting / :vpn / :tor_exit)
|
|
15
|
+
result.likely_human? # => true (:residential_isp, :mobile, :relay, :cgnat, :enterprise_gateway)
|
|
16
|
+
result.asn # => 3352
|
|
17
|
+
result.as_org # => "TELEFONICA DE ESPANA S.A.U."
|
|
18
|
+
result.sources # => [:asn_category] — every verdict is auditable
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
It's the missing first line of defense for signups, checkouts, and abuse-prone endpoints: the signal that today costs $79–$200/month from commercial IP-intelligence APIs, as free, open, auditable data — compiled nightly from legally clean sources by the [OpenASN data project](https://github.com/openasn/openasn), with a data seed bundled right in the gem so it works on first boot, offline.
|
|
22
|
+
|
|
23
|
+
> [!IMPORTANT]
|
|
24
|
+
> **What this is NOT.** A clean or `:residential_isp` verdict is **absence of evidence, not proof of innocence**. Residential proxies — malicious traffic exiting through real home IPs — are structurally hard to detect offline, and OpenASN does not claim to detect them. `:vpn`, `:hosting`, and `:tor_exit` are high-confidence; treat everything else as a signal, not a sentence. **Never hard-block `:relay`, `:cgnat`, or `:mobile` — those are real people.** OpenASN is a first line of defense, not a fraud engine.
|
|
25
|
+
|
|
26
|
+
## How it works
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
┌────────────────────────────┐ nightly ┌──────────────────────────────┐
|
|
30
|
+
│ openasn/openasn (data) │ ─────────────▶ │ your server │
|
|
31
|
+
│ 560k+ IP ranges, all ASNs, │ GitHub │ gem seed → daily UpdateJob │
|
|
32
|
+
│ VPN/DC overlays, curated │ Releases │ + Tier B overlays fetched │
|
|
33
|
+
│ corrections (CC0) │ (free CDN) │ from original authorities: │
|
|
34
|
+
└────────────────────────────┘ │ Apple relay list, Tor exits,│
|
|
35
|
+
│ AWS/GCP/Azure/OCI ranges… │
|
|
36
|
+
lookups: local binary search, ~20µs, └──────────────────────────────┘
|
|
37
|
+
no user IP ever leaves your server
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- **Bundled seed**: the gem ships with a full snapshot — works offline from the first boot, forever.
|
|
41
|
+
- **Nightly refresh**: `OpenASN::UpdateJob` pulls updated artifacts (SHA-256 verified, atomically swapped — readers never block, never see partial data).
|
|
42
|
+
- **Tier B overlays**: fast-moving lists (Tor exits, Apple Private Relay, cloud ranges, exact VPN provider server lists) are fetched by *your* server directly from the original authorities — never proxied through anyone.
|
|
43
|
+
- **Data never flows through gem releases.** The gem versions on code; the data has its own nightly release channel.
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
# Gemfile
|
|
49
|
+
gem "openasn"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Then:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
bundle install
|
|
56
|
+
rails generate openasn:install
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The generator creates `config/initializers/openasn.rb` (documented defaults) and, if you're on Solid Queue, schedules the daily `OpenASN::UpdateJob` in `config/recurring.yml`. **No migrations, no database** — data lives in `storage/openasn/` as memory-mapped-style packed files.
|
|
60
|
+
|
|
61
|
+
Not on Rails? `OpenASN.lookup` works anywhere Ruby ≥ 3.1 runs; schedule `OpenASN.update!` daily with cron/whenever.
|
|
62
|
+
|
|
63
|
+
## The API
|
|
64
|
+
|
|
65
|
+
### Lookup
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
r = OpenASN.lookup("146.70.107.100") # aliases: OpenASN.check, OpenASN.[]
|
|
69
|
+
# Never nil. Raises OpenASN::InvalidIPError (an ArgumentError) on garbage input.
|
|
70
|
+
|
|
71
|
+
r.verdict # one of: :residential_isp :mobile :business :hosting :vpn
|
|
72
|
+
# :tor_exit :relay :enterprise_gateway :education
|
|
73
|
+
# :government :cgnat :private :unknown
|
|
74
|
+
r.infrastructure? # :hosting | :vpn | :tor_exit — the honest, high-confidence boolean
|
|
75
|
+
r.likely_human? # :residential_isp | :mobile | :relay | :cgnat | :enterprise_gateway
|
|
76
|
+
r.vpn? r.hosting? r.tor? r.relay? r.mobile? r.private? r.cgnat?
|
|
77
|
+
|
|
78
|
+
r.asn # 9009
|
|
79
|
+
r.as_org # "M247 Europe SRL" (nil until the first data refresh downloads org names)
|
|
80
|
+
r.category # "hosting" (raw upstream category)
|
|
81
|
+
r.network_role # "major_transit" (raw upstream routing role)
|
|
82
|
+
r.provider # "aws" | "ProtonVPN" | "iCloud Private Relay" | nil (overlay attribution)
|
|
83
|
+
r.sources # [:x4b_vpn] — which data layer decided; every verdict is auditable
|
|
84
|
+
r.context_flags # [:cloudflare_range] — context that never decides verdicts
|
|
85
|
+
r.unrouted? # true when no ASN announces this IP
|
|
86
|
+
r.flags # raw u16 for power users (see the data project's FORMAT.md)
|
|
87
|
+
r.to_h # everything, stable keys — built for shadow-mode logging
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Note the deliberate asymmetry: `:business`, `:education`, `:government`, and `:unknown` are **neither** `infrastructure?` **nor** `likely_human?` — that's your call, not the gem's. There is intentionally no `suspicious?` — that's a policy word, and your app owns policy.
|
|
91
|
+
|
|
92
|
+
### The mental model: verdict, category, flags, sources
|
|
93
|
+
|
|
94
|
+
OpenASN returns one object, but its fields answer different questions:
|
|
95
|
+
|
|
96
|
+
| Field | What it answers | How to use it |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| `verdict` | "What should my app consider this IP?" | Primary policy input. Switch on this. |
|
|
99
|
+
| `infrastructure?` | "Is this high-confidence non-eyeball infrastructure?" | Safe shorthand for `:hosting`, `:vpn`, `:tor_exit`. |
|
|
100
|
+
| `likely_human?` | "Is blocking this IP likely to hit real people?" | True for residential/mobile/relay/CGNAT/enterprise gateways. |
|
|
101
|
+
| `category` | Raw ASN category from upstream data (`"isp"`, `"hosting"`, `"business"`, ...) | Context for logs and analyst UI. Do not treat it as the verdict. |
|
|
102
|
+
| `network_role` | Raw routing role (`"access_provider"`, `"midsize_transit"`, `"tier1_transit"`, ...) | Explains why some ISP ASNs stay human while pure backbones stay unknown. |
|
|
103
|
+
| `sources` | Which rule/data layer decided the verdict | Best debugging field. Log it. |
|
|
104
|
+
| `provider` | Provider attribution from an exact overlay hit (`"aws"`, `"azure"`, `"ProtonVPN"`, ...) | Display/log it when present; nil is normal. |
|
|
105
|
+
| `context_flags` | Extra context that never decides the verdict | Useful for policy experiments; do not block solely on it. |
|
|
106
|
+
| `flags` | Packed low-level artifact bits | Power-user/debug field; prefer `verdict` and `sources` in app code. |
|
|
107
|
+
|
|
108
|
+
Common examples:
|
|
109
|
+
|
|
110
|
+
- A DIGI Spain home IP is usually `category: "isp"` and `verdict: :residential_isp`. That is expected: `category` describes the ASN; `verdict` is OpenASN's safer application-level label.
|
|
111
|
+
- An Amazon IP may be `category: "hosting"`, `sources: [:x4b_dc]`, and with Tier B enabled `provider: "aws"`. The verdict remains `:hosting`; Tier B only improves attribution.
|
|
112
|
+
- `bad_asn` is not an accusation and not a verdict. It means the ASN appears in `brianhama/bad-asn-list`, a curated MIT-licensed hosting/cloud/colo ASN list. When that bit decides classification, `sources` includes `:asn_bad_asn`.
|
|
113
|
+
- `:residential_isp` means "known eyeball ISP, no stronger infrastructure signal found." It does **not** mean "safe user."
|
|
114
|
+
|
|
115
|
+
### Verdict cheat sheet
|
|
116
|
+
|
|
117
|
+
| Verdict | Meaning | Confidence | Advice |
|
|
118
|
+
|---|---|---|---|
|
|
119
|
+
| `:hosting` | datacenter/cloud/colo | high | fine to challenge on sensitive flows |
|
|
120
|
+
| `:vpn` | known VPN egress | high | challenge > block (privacy users are customers too) |
|
|
121
|
+
| `:tor_exit` | Tor exit node | high (with Tier B fresh) | your policy |
|
|
122
|
+
| `:relay` | iCloud Private Relay | high | **treat like CGNAT — real paying humans** (Apple says the same) |
|
|
123
|
+
| `:enterprise_gateway` | Zscaler-style corporate egress | high | humans at work; never block |
|
|
124
|
+
| `:residential_isp` | eyeball ISP | *absence of evidence* | trust but verify |
|
|
125
|
+
| `:mobile` | cellular carrier | high | one IP = hundreds of humans (CGNAT); no per-IP rate-limits |
|
|
126
|
+
| `:cgnat` / `:private` | RFC 6598 / RFC 1918 space | certain | check your proxy config if you see these on public traffic |
|
|
127
|
+
| `:business` / `:education` / `:government` | org category from ASN data | medium | your policy |
|
|
128
|
+
| `:unknown` | genuinely ambiguous (e.g. tier-1 backbone, uncategorized ASN) | honest | design for it — `unknown` is a feature, not a bug |
|
|
129
|
+
|
|
130
|
+
### API stability contract
|
|
131
|
+
|
|
132
|
+
Your `case result.verdict` statements and shadow-log parsers are API surface. The rules, from 0.1.0 onward:
|
|
133
|
+
|
|
134
|
+
- **The verdict enum is append-only.** Existing verdicts are never removed, renamed, or silently redefined (a meaning change would be a major version). New verdicts may be *added* in a minor version, announced loudly in the CHANGELOG — so give exhaustive `case` statements an `else` branch (treat unknown-to-you verdicts as you treat `:unknown`).
|
|
135
|
+
- **Verdicts are code, not data: a data refresh can never emit a verdict your gem version doesn't know.** The nightly artifacts carry ranges and flag bits; the mapping to verdicts is compiled into the client. Data updates are always safe to auto-apply.
|
|
136
|
+
- **`Result#to_h` keys are append-only** — shadow-mode logs you write today stay parseable tomorrow.
|
|
137
|
+
- **`sources` and `context_flags` symbols are informational**: new ones appear as data sources evolve. Log them, display them, never exhaustively match on them.
|
|
138
|
+
- **Config keys are additive**; artifact bytes are governed by the data project's [FORMAT.md](https://github.com/openasn/openasn/blob/main/FORMAT.md) (any layout change bumps `format_version`, and readers reject unknown versions rather than guess).
|
|
139
|
+
|
|
140
|
+
### Configuration (all optional)
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
OpenASN.configure do |config|
|
|
144
|
+
config.data_dir = Rails.root.join("storage", "openasn") # default; use a persistent volume in containers
|
|
145
|
+
config.memory_mode = :packed # :packed ~11MB data resident, ~15µs/lookup · :arrays faster, more RAM
|
|
146
|
+
config.auto_update = true
|
|
147
|
+
config.release_url = "https://github.com/openasn/openasn/releases/download/latest/" # self-hostable; tag-addressed on purpose (badge-immune, see data repo DECISIONS.md D-REL-1)
|
|
148
|
+
config.pin_version = nil # e.g. "2026-07-04" to pin a dated data release
|
|
149
|
+
config.tier_b = { apple_relay: true, tor: true, clouds: true,
|
|
150
|
+
vpn_providers: true, vpn_heavy: false,
|
|
151
|
+
vpn_dns: false, public_relays: false, zscaler: false,
|
|
152
|
+
nazgul_mixed: false }
|
|
153
|
+
config.logger = Rails.logger
|
|
154
|
+
end
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`vpn_providers: true` enables small/stable exact-IP provider lists such as ProtonVPN, Mullvad, IVPN, Private Internet Access, AirVPN, Windscribe, PrivadoVPN, RiseupVPN, WLVPN, WorldVPN, OVPN, and Anonine. WLVPN is backend infrastructure powered by IPVanish and used by white-label resellers such as FastVPN/Namecheap/Spaceship; OpenASN labels the exact source as `provider: "WLVPN"` rather than guessing the reseller. WorldVPN, OVPN, and Anonine publish exact IPs in first-party server/status tables or JSON endpoints, so they do not need DNS expansion. `vpn_heavy: true` opts into large or historically fragile provider APIs such as NordVPN. `vpn_dns: true` opts into provider-published hostnames resolved by your server's DNS at update time, covering sources such as Surfshark, IPVanish, PrivateVPN, PureVPN, TorGuard, FastestVPN, VPNSecure, TunnelBear, StrongVPN, VyprVPN, Giganews VyprVPN, SlickVPN, AzireVPN, VPN.AC, and Trust.Zone; this is useful but intentionally off by default because DNS answers can vary by resolver/vantage. `public_relays: true` opts into volunteer/free relay networks such as VPN Gate, VPNBook, and FreeVPN.us, which can label residential-looking IPs as `:vpn` while they are actively advertised as relays. FreeVPN.us intentionally includes only OpenVPN/WireGuard/PPTP rows; SSH Tunnel and V2Ray rows are excluded from the VPN overlay.
|
|
158
|
+
|
|
159
|
+
### Updates
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
OpenASN.update! # refresh now → :updated | :tier_b_only | :unchanged | :locked
|
|
163
|
+
OpenASN.dataset_info # build id, origin (:seed/:data_dir), record counts, per-source Tier B status
|
|
164
|
+
OpenASN.eager_load! # load at boot instead of first lookup (~50–200ms once per process)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Updates are atomic end to end: SHA-256 verified against the release manifest → written to temp files → `rename(2)` into place → in-memory snapshot swapped in a single assignment. Concurrent lookups never block and never see partial state; concurrent updaters (multi-worker Puma) coordinate via file lock; sibling processes pick up new data within ~5 minutes via a one-`stat()` freshness probe. Every Tier B source failure keeps last-good data and surfaces in `dataset_info` — a broken upstream can never crash your app or silently blank a signal.
|
|
168
|
+
|
|
169
|
+
### Rack middleware (optional)
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
# config/initializers/openasn.rb
|
|
173
|
+
Rails.application.config.middleware.use OpenASN::Middleware
|
|
174
|
+
|
|
175
|
+
# anywhere downstream:
|
|
176
|
+
request.env["openasn.result"]&.infrastructure?
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
> [!WARNING]
|
|
180
|
+
> **The classic integration bug:** behind a proxy/CDN without trusted-proxy configuration, you'll classify your own load balancer on every request (everything comes back `:private` or your host's datacenter). Inside Rails the middleware uses `remote_ip` semantics (honors `config.action_dispatch.trusted_proxies`); make sure the real client IP reaches your app — e.g. with the `cloudflare-rails` gem when behind Cloudflare.
|
|
181
|
+
|
|
182
|
+
## Recommended rollout: shadow mode first
|
|
183
|
+
|
|
184
|
+
Log for two weeks. Then decide. (This is how we dogfood it.)
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
# 1. SHADOW — measure, block nothing:
|
|
188
|
+
class RegistrationsController < ApplicationController
|
|
189
|
+
def create
|
|
190
|
+
Rails.logger.info(openasn: OpenASN.lookup(request.remote_ip).to_h.merge(flow: "signup"))
|
|
191
|
+
# … existing signup logic unchanged
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# 2. Weeks later, with YOUR false-positive data in hand, act — prefer
|
|
196
|
+
# step-up verification (email confirm, captcha, phone) over hard blocks:
|
|
197
|
+
Rack::Attack.blocklist("openasn: infrastructure on signup") do |req|
|
|
198
|
+
req.post? && req.path == "/signup" && OpenASN.lookup(req.ip).infrastructure?
|
|
199
|
+
end
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Who should NOT rely on this:** banks, crypto exchanges, KYC flows, high-chargeback marketplaces — you need paid behavioral intelligence (MaxMind Anonymous IP/Residential Proxy, IPQS…). OpenASN is your prefilter at most.
|
|
203
|
+
|
|
204
|
+
## What you can and cannot conclude (honesty section)
|
|
205
|
+
|
|
206
|
+
**Can:** recognize known infrastructure with high confidence; identify network type; explain every verdict (`sources`); do it all offline with zero latency budget and zero privacy leakage (user IPs never leave your server).
|
|
207
|
+
|
|
208
|
+
**Cannot:** detect residential proxies (structurally invisible offline — that's why the paid products exist); prove any IP is safe; outrun VPN infrastructure churn beyond nightly + Tier B cadence; promise IPv6 range-overlay parity (v6 VPN signal rides ASN-level data; documented lower confidence).
|
|
209
|
+
|
|
210
|
+
## Performance
|
|
211
|
+
|
|
212
|
+
Measured on the bundled real dataset (433k+ IPv4 ranges, 125k+ IPv6, full overlays), pure Ruby, no C extensions:
|
|
213
|
+
|
|
214
|
+
| mode | lookup | throughput | memory |
|
|
215
|
+
|---|---|---|---|
|
|
216
|
+
| `:packed` (default) | ~15µs | ~68k/sec/core | ~11MB data |
|
|
217
|
+
| `:arrays` | ~9µs | ~108k/sec/core | ~40MB data |
|
|
218
|
+
|
|
219
|
+
(Apple M-series, canonical-data only; GitHub's shared CI runners measure ~24µs packed / ~12µs arrays. CI asserts a generous 100µs ceiling as a regression tripwire.) Enabling many Tier B overlays adds a few µs per active overlay; with all defaults loaded (~11 overlays) a full lookup is ~35µs on Apple Silicon, still tens of thousands per second per core. Run `rake bench` to measure on your hardware.
|
|
220
|
+
|
|
221
|
+
## Data provenance & licensing
|
|
222
|
+
|
|
223
|
+
Every byte in the dataset is traceable: compiled nightly from PDDL/CC0/MIT-licensed sources ([sapics/ip-location-db](https://github.com/sapics/ip-location-db), [ipverse/as-metadata](https://github.com/ipverse/as-metadata), [X4BNet/lists_vpn](https://github.com/X4BNet/lists_vpn), [brianhama/bad-asn-list](https://github.com/brianhama/bad-asn-list)) plus OpenASN's own curated corrections — with upstream license texts SHA-256-pinned in CI and full per-build provenance in `manifest.json`. The dataset is CC0. The details, gates, and the "fetch ≠ redistribute" legal design live in the [data project README](https://github.com/openasn/openasn).
|
|
224
|
+
|
|
225
|
+
Sibling of [`nondisposable`](https://github.com/rameerez/nondisposable) (disposable-email blocking) and [`trackdown`](https://github.com/rameerez/trackdown) (IP geolocation) — same philosophy: boring, offline, production-grade primitives for Rails apps.
|
|
226
|
+
|
|
227
|
+
## Development
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
bundle install
|
|
231
|
+
bundle exec rake test # full suite, offline (WebMock)
|
|
232
|
+
rake bench # lookup latency on your machine
|
|
233
|
+
rake seed:refresh # pull the latest data release into lib/openasn/data/seed/
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Contributing
|
|
237
|
+
|
|
238
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/openasn/openasn-ruby. Wrong verdict for an IP? That's usually a *data* issue — check `result.sources` and open it against [openasn/openasn](https://github.com/openasn/openasn) (the override files are one sourced line per PR).
|
|
239
|
+
|
|
240
|
+
## License
|
|
241
|
+
|
|
242
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rake/testtask"
|
|
5
|
+
|
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
|
7
|
+
t.libs << "test"
|
|
8
|
+
t.libs << "lib"
|
|
9
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
desc "Refresh the bundled data seed from the latest OpenASN release (run before each gem release)"
|
|
13
|
+
task "seed:refresh" do
|
|
14
|
+
require "open-uri"
|
|
15
|
+
require "fileutils"
|
|
16
|
+
base = ENV.fetch("OPENASN_RELEASE_URL", "https://github.com/openasn/openasn/releases/download/latest/") # tag-addressed, badge-immune: see Configuration#release_url
|
|
17
|
+
seed = File.expand_path("lib/openasn/data/seed", __dir__)
|
|
18
|
+
FileUtils.mkdir_p(seed)
|
|
19
|
+
%w[openasn-ipv4.bin openasn-ipv6.bin manifest.json fetch-manifest.json].each do |f|
|
|
20
|
+
puts "downloading #{f}…"
|
|
21
|
+
URI.open("#{base}#{f}", "User-Agent" => "openasn-seed-refresh") do |io| # rubocop:disable Security/Open
|
|
22
|
+
File.binwrite(File.join(seed, f), io.read)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
puts "seed refreshed — remember: gem versions ship on CODE changes; data freshness flows through releases + UpdateJob, never through gem releases"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
desc "Benchmark lookup latency against the bundled seed (RUN_BENCH=1 rake bench)"
|
|
29
|
+
task :bench do
|
|
30
|
+
ENV["RUN_BENCH"] = "1"
|
|
31
|
+
ruby "-Ilib", "-Itest", "test/bench.rb"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
task default: :test
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
|
|
5
|
+
module Openasn
|
|
6
|
+
module Generators
|
|
7
|
+
# rails generate openasn:install
|
|
8
|
+
#
|
|
9
|
+
# Creates the initializer and wires the daily update job into Solid
|
|
10
|
+
# Queue's recurring.yml when present. No migrations — OpenASN is
|
|
11
|
+
# file-based by design (storage/openasn/), nothing touches your DB.
|
|
12
|
+
class InstallGenerator < Rails::Generators::Base
|
|
13
|
+
source_root File.expand_path("templates", __dir__)
|
|
14
|
+
|
|
15
|
+
def create_initializer
|
|
16
|
+
template "openasn.rb", "config/initializers/openasn.rb"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def wire_recurring_job
|
|
20
|
+
recurring = "config/recurring.yml"
|
|
21
|
+
unless File.exist?(File.expand_path(recurring, destination_root))
|
|
22
|
+
say_status :skipped, "#{recurring} not found — schedule OpenASN::UpdateJob with your own scheduler (see README)", :yellow
|
|
23
|
+
return
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if File.read(File.expand_path(recurring, destination_root)).include?("OpenASN::UpdateJob")
|
|
27
|
+
say_status :identical, "#{recurring} already schedules OpenASN::UpdateJob", :blue
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# 4:12am UTC: after the nightly data build (03:17 UTC) completes,
|
|
32
|
+
# off-hour to be kind to the Tier B upstreams.
|
|
33
|
+
append_to_file recurring, <<~YAML
|
|
34
|
+
|
|
35
|
+
# Added by `rails g openasn:install` — daily IP-origin data refresh
|
|
36
|
+
production:
|
|
37
|
+
openasn_update:
|
|
38
|
+
class: OpenASN::UpdateJob
|
|
39
|
+
queue: default
|
|
40
|
+
schedule: every day at 4:12am UTC
|
|
41
|
+
YAML
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def display_post_install_message
|
|
45
|
+
say ""
|
|
46
|
+
say "\tThe `openasn` gem has been successfully installed!", :green
|
|
47
|
+
say ""
|
|
48
|
+
say "OpenASN works out of the box: a data seed is bundled with the gem and"
|
|
49
|
+
say "refreshes itself daily via OpenASN::UpdateJob (data flows through GitHub"
|
|
50
|
+
say "Releases — never through gem updates)."
|
|
51
|
+
say ""
|
|
52
|
+
say "To complete the setup:"
|
|
53
|
+
say " 1. Review config/initializers/openasn.rb (defaults are production-ready)."
|
|
54
|
+
say " 2. Make sure a queue backend runs OpenASN::UpdateJob daily (done for you"
|
|
55
|
+
say " if you use Solid Queue's recurring.yml)."
|
|
56
|
+
say " 3. Start in SHADOW MODE — log verdicts, block nothing:"
|
|
57
|
+
say ""
|
|
58
|
+
say " # e.g. in your signups controller:"
|
|
59
|
+
say " Rails.logger.info(openasn: OpenASN.lookup(request.remote_ip).to_h)"
|
|
60
|
+
say ""
|
|
61
|
+
say " After a week or two of data, decide your own thresholds. Remember:"
|
|
62
|
+
say " a residential verdict is absence of evidence, not proof of innocence,"
|
|
63
|
+
say " and :relay/:cgnat/:mobile are real humans — never hard-block them.", :yellow
|
|
64
|
+
say ""
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# OpenASN — offline IP origin intelligence.
|
|
2
|
+
# Docs: https://github.com/openasn/openasn-ruby
|
|
3
|
+
#
|
|
4
|
+
# Everything below shows the DEFAULT — uncomment only what you change.
|
|
5
|
+
|
|
6
|
+
OpenASN.configure do |config|
|
|
7
|
+
# Where downloaded data lives. Default: Rails.root/storage/openasn.
|
|
8
|
+
# Containerized? Point this at a persistent volume, or each boot starts
|
|
9
|
+
# from the bundled seed and re-downloads (~15MB) on first refresh.
|
|
10
|
+
# config.data_dir = Rails.root.join("storage", "openasn")
|
|
11
|
+
|
|
12
|
+
# :packed → ~11MB data resident, ~15-25µs per lookup (right for almost everyone)
|
|
13
|
+
# :arrays → several× the memory, ~9µs per lookup (lookup-heavy batch pipelines)
|
|
14
|
+
# config.memory_mode = :packed
|
|
15
|
+
|
|
16
|
+
# Allow automatic refreshes (UpdateJob + boot staleness check).
|
|
17
|
+
# config.auto_update = true
|
|
18
|
+
|
|
19
|
+
# Self-host or pin the data source if you need to:
|
|
20
|
+
# config.release_url = "https://github.com/openasn/openasn/releases/download/latest/"
|
|
21
|
+
# config.pin_version = "2026-07-04" # a dated release tag, for reproducible builds
|
|
22
|
+
|
|
23
|
+
# Tier B overlays — fetched by YOUR server from the original authorities
|
|
24
|
+
# (Apple, Tor Project, AWS…), per the project's fetch-manifest. Defaults:
|
|
25
|
+
# config.tier_b = {
|
|
26
|
+
# apple_relay: true, # iCloud Private Relay → :relay (real users! never block)
|
|
27
|
+
# tor: true, # Tor Project exits → :tor_exit
|
|
28
|
+
# clouds: true, # AWS/GCP/Azure/OCI/DO/Linode/Vultr → :hosting (+provider)
|
|
29
|
+
# vpn_providers: true, # exact VPN provider/backend IP lists → :vpn (+provider)
|
|
30
|
+
# vpn_heavy: false, # large/fragile provider APIs (NordVPN) — opt-in
|
|
31
|
+
# vpn_dns: false, # provider hostnames resolved locally (Surfshark/IPVanish/FastestVPN/VPNSecure/TunnelBear/StrongVPN/...) - opt-in
|
|
32
|
+
# public_relays: false, # volunteer/free public VPN relays (VPN Gate/VPNBook/FreeVPN.us) — opt-in
|
|
33
|
+
# zscaler: false, # extra :enterprise_gateway ranges (ASN flags already cover most)
|
|
34
|
+
# nazgul_mixed: false # broad "high-risk hosting" flag — NOT a VPN signal; opt-in
|
|
35
|
+
# }
|
|
36
|
+
|
|
37
|
+
# config.logger = Rails.logger
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Optional: classify every request once and read it anywhere downstream as
|
|
41
|
+
# request.env["openasn.result"]. (Costs ~20µs/request.)
|
|
42
|
+
#
|
|
43
|
+
# ⚠️ Behind a proxy/CDN? Make sure Rails' trusted proxies are set up so
|
|
44
|
+
# remote_ip is the real client — otherwise you'll classify your own load
|
|
45
|
+
# balancer on every request. See "Trusted proxies" in the README.
|
|
46
|
+
#
|
|
47
|
+
# Rails.application.config.middleware.use OpenASN::Middleware
|
|
48
|
+
|
|
49
|
+
# Optional: eager-load the dataset at boot (first lookup otherwise pays
|
|
50
|
+
# ~50-200ms of lazy loading once per process):
|
|
51
|
+
#
|
|
52
|
+
# Rails.application.config.after_initialize { OpenASN.eager_load! }
|
|
53
|
+
|
|
54
|
+
# ── Shadow mode (START HERE — measure before you act) ──────────────────────
|
|
55
|
+
# Log verdicts on your critical flows for a couple of weeks:
|
|
56
|
+
#
|
|
57
|
+
# result = OpenASN.lookup(request.remote_ip)
|
|
58
|
+
# Rails.logger.info(openasn: result.to_h.merge(flow: "signup"))
|
|
59
|
+
#
|
|
60
|
+
# Then, and only then, act — prefer step-up verification over hard blocks:
|
|
61
|
+
#
|
|
62
|
+
# # Rack::Attack example (config/initializers/rack_attack.rb):
|
|
63
|
+
# # Rack::Attack.blocklist("openasn: infrastructure on signup") do |req|
|
|
64
|
+
# # req.post? && req.path == "/signup" && OpenASN.lookup(req.ip).infrastructure?
|
|
65
|
+
# # end
|
|
66
|
+
#
|
|
67
|
+
# infrastructure? is true only for :hosting/:vpn/:tor_exit (high-confidence
|
|
68
|
+
# classes). :relay, :cgnat and :mobile are REAL PEOPLE — never hard-block.
|