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 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
+ [![Gem Version](https://badge.fury.io/rb/openasn.svg)](https://badge.fury.io/rb/openasn) [![Build Status](https://github.com/openasn/openasn-ruby/workflows/Tests/badge.svg)](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.