trackdown 0.3.0 → 0.4.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 +4 -4
- data/CHANGELOG.md +70 -0
- data/README.md +478 -21
- data/lib/generators/trackdown/install_generator.rb +14 -5
- data/lib/generators/trackdown/templates/trackdown.rb +87 -4
- data/lib/trackdown/configuration.rb +112 -6
- data/lib/trackdown/database_fingerprint.rb +102 -0
- data/lib/trackdown/database_updater.rb +89 -28
- data/lib/trackdown/ip_locator.rb +4 -1
- data/lib/trackdown/location_result.rb +223 -18
- data/lib/trackdown/providers/auto_provider.rb +251 -20
- data/lib/trackdown/providers/base_provider.rb +71 -5
- data/lib/trackdown/providers/cloudflare_provider.rb +57 -20
- data/lib/trackdown/providers/cloudfront_provider.rb +197 -0
- data/lib/trackdown/providers/maxmind_provider.rb +113 -16
- data/lib/trackdown/version.rb +1 -1
- data/lib/trackdown.rb +3 -1
- metadata +12 -16
- data/.simplecov +0 -25
- data/AGENTS.md +0 -5
- data/CLAUDE.md +0 -5
- data/Rakefile +0 -12
- data/sig/trackdown.rbs +0 -4
data/README.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# 📍 `trackdown` - Ruby gem to geolocate IPs
|
|
2
2
|
|
|
3
|
+
[](https://badge.fury.io/rb/trackdown) [](https://github.com/rameerez/trackdown/actions)
|
|
4
|
+
|
|
3
5
|
> [!TIP]
|
|
4
6
|
> **🚀 Ship your next Rails app 10x faster!** I've built **[RailsFast](https://railsfast.com/?ref=trackdown)**, 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=trackdown)!
|
|
5
7
|
|
|
6
8
|
`trackdown` is a Ruby gem that allows you to geolocate IP addresses easily.
|
|
7
9
|
|
|
8
|
-
It
|
|
10
|
+
It reads geolocation headers from **Cloudflare** and **Amazon CloudFront** without API calls or additional runtime gems, and it is also a convenient wrapper around **MaxMind**. CDN-side header forwarding and origin protection still need to be configured correctly; the exact requirements are documented below.
|
|
9
11
|
|
|
10
12
|
`trackdown` offers a clean API for Rails applications to fetch country, city, region, continent, timezone, coordinates, and emoji flag information for any IP address.
|
|
11
13
|
|
|
@@ -20,37 +22,60 @@ Given an IP, it gives you the corresponding:
|
|
|
20
22
|
- 📺 Metro code (e.g. "807")
|
|
21
23
|
- 🇺🇸 Emoji flag of the country
|
|
22
24
|
|
|
25
|
+
And, because a location you can't explain isn't much of a location, every result also tells you [how it knows](#how-do-you-know): which provider answered, when, how precise that provider says it is, whether you vouched for the request it came from, and — when there's no answer — exactly why.
|
|
26
|
+
|
|
23
27
|
## First, choose your `trackdown` Geo IP provider
|
|
24
28
|
|
|
25
|
-
### Option 1: Cloudflare (recommended
|
|
29
|
+
### Option 1: Cloudflare (recommended for Cloudflare origins)
|
|
26
30
|
|
|
27
|
-
If your Rails app is behind Cloudflare,
|
|
31
|
+
If your Rails app is behind Cloudflare, `trackdown` reads the location information Cloudflare adds to origin requests:
|
|
28
32
|
- No API keys needed
|
|
29
33
|
- No database downloads
|
|
30
34
|
- No external dependencies
|
|
31
35
|
- Instant lookups from Cloudflare headers
|
|
32
36
|
|
|
33
|
-
|
|
37
|
+
Enable "IP Geolocation" in your Cloudflare dashboard. For the full set of location fields (city, region, coordinates, etc.), enable ["Add visitor location headers"](https://developers.cloudflare.com/rules/transform/managed-transforms/reference/) in Managed Transforms. `:auto` also verifies that the documented [`CF-Connecting-IP` edge-to-origin header](https://developers.cloudflare.com/fundamentals/reference/http-headers/#cf-connecting-ip) matches the IP passed to `Trackdown.locate` before trusting the location headers. If Cloudflare's "Remove visitor IP headers" transform suppresses that corroborator, use an explicitly configured provider only after securing the origin.
|
|
38
|
+
|
|
39
|
+
As with every header-based provider, direct-origin access must be blocked. Cloudflare recommends [blocking traffic that does not come from Cloudflare IPs](https://developers.cloudflare.com/fundamentals/concepts/cloudflare-ip-addresses/#block-other-ip-addresses-recommended) or using [Authenticated Origin Pulls](https://developers.cloudflare.com/ssl/origin-configuration/authenticated-origin-pull/) to ensure requests came through its network.
|
|
40
|
+
|
|
41
|
+
### Option 2: Amazon CloudFront (recommended for CloudFront origins)
|
|
42
|
+
|
|
43
|
+
If your Rails app is behind Amazon CloudFront, `trackdown` can read CloudFront's viewer-location headers:
|
|
44
|
+
- No API keys needed
|
|
45
|
+
- No database downloads
|
|
46
|
+
- No external dependencies
|
|
47
|
+
- Instant lookups from CloudFront `CloudFront-Viewer-*` headers
|
|
48
|
+
|
|
49
|
+
CloudFront requires explicit distribution and origin configuration:
|
|
50
|
+
|
|
51
|
+
1. Attach an [origin request policy](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/controlling-origin-requests.html) that adds the viewer-location headers. A custom least-privilege policy containing only the required `CloudFront-*` headers is preferred. AWS's managed [`AllViewerAndCloudFrontHeaders-2022-06` policy](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-origin-request-policies.html#managed-origin-request-policy-all-viewer-and-cloudfront) includes them, but also forwards **every viewer header, cookie, and query string**.
|
|
52
|
+
2. Prevent direct access to the origin. Header presence alone does not prove that a request passed through CloudFront. AWS documents how to [add an origin-only custom header](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/add-origin-custom-headers.html) and [configure a custom origin to accept only CloudFront requests](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-overview.html).
|
|
53
|
+
3. Pass the request and the intended viewer IP: `Trackdown.locate(request.remote_ip, request: request)`.
|
|
54
|
+
|
|
55
|
+
AWS documents the exact [viewer-location header names, availability rules, address format, and RFC 3986 encoding](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/adding-cloudfront-headers.html#cloudfront-headers-viewer-location). `trackdown` validates the country, decodes percent-encoded UTF-8 fields, validates coordinate bounds, and—under `:auto`—requires `CloudFront-Viewer-Address` to match the requested IP.
|
|
56
|
+
|
|
57
|
+
> [!IMPORTANT]
|
|
58
|
+
> If both Cloudflare and CloudFront header families match the target IP, `:auto` fails closed because viewer-forwarded headers make the situation ambiguous. It tries MaxMind and otherwise returns `'Unknown'`. Choose `config.provider = :cloudflare` or `:cloudfront` for an intentional stacked-CDN deployment after securing the origin.
|
|
34
59
|
|
|
35
|
-
### Option
|
|
60
|
+
### Option 3: MaxMind (BYOK - Bring Your Own Key)
|
|
36
61
|
|
|
37
|
-
For apps not behind
|
|
62
|
+
For apps not behind a supported CDN, offline apps, non-Rails apps, or as a fallback, use MaxMind:
|
|
38
63
|
- Requires MaxMind account and license key
|
|
39
64
|
- Requires downloading and maintaining a local database
|
|
40
65
|
- Works offline once database is downloaded
|
|
41
66
|
- Get started at [MaxMind](https://www.maxmind.com/)
|
|
42
67
|
|
|
43
|
-
### Option
|
|
68
|
+
### Option 4: Auto
|
|
44
69
|
|
|
45
|
-
By default, `trackdown` uses **`:auto` mode
|
|
70
|
+
By default, `trackdown` uses **`:auto` mode**. It uses an edge provider only when that provider's documented client-IP header matches the target IP. When no unique edge provider can be verified, it tries MaxMind and otherwise returns `'Unknown'`.
|
|
46
71
|
|
|
47
72
|
> [!NOTE]
|
|
48
|
-
> Trackdown fails gracefully. If no provider is available (no
|
|
73
|
+
> Trackdown fails gracefully. If no provider is available (no verified CDN headers and no MaxMind database), it returns `'Unknown'` instead of raising an error, so your app doesn't crash due to a missing geolocation provider.
|
|
49
74
|
|
|
50
75
|
|
|
51
76
|
## Installation
|
|
52
77
|
|
|
53
|
-
Add this line to your application's Gemfile:
|
|
78
|
+
`trackdown` needs Ruby 3.1 or newer. Add this line to your application's Gemfile:
|
|
54
79
|
|
|
55
80
|
```ruby
|
|
56
81
|
gem 'trackdown'
|
|
@@ -79,9 +104,40 @@ If your app is behind Cloudflare, setup is super simple:
|
|
|
79
104
|
```ruby
|
|
80
105
|
# In your controller
|
|
81
106
|
Trackdown.locate(request.remote_ip, request: request).country
|
|
82
|
-
# => 'United States'
|
|
107
|
+
# => 'United States of America'
|
|
83
108
|
```
|
|
84
109
|
|
|
110
|
+
### Setup with Amazon CloudFront
|
|
111
|
+
|
|
112
|
+
1. Create an origin request policy that adds these headers:
|
|
113
|
+
- `CloudFront-Viewer-Country`
|
|
114
|
+
- `CloudFront-Viewer-City`
|
|
115
|
+
- `CloudFront-Viewer-Country-Region-Name`
|
|
116
|
+
- `CloudFront-Viewer-Country-Region`
|
|
117
|
+
- `CloudFront-Viewer-Latitude`
|
|
118
|
+
- `CloudFront-Viewer-Longitude`
|
|
119
|
+
- `CloudFront-Viewer-Time-Zone`
|
|
120
|
+
- `CloudFront-Viewer-Postal-Code`
|
|
121
|
+
- `CloudFront-Viewer-Metro-Code`
|
|
122
|
+
- `CloudFront-Viewer-Address` (required for `:auto` IP corroboration)
|
|
123
|
+
|
|
124
|
+
AWS source for creating and attaching origin request policies:
|
|
125
|
+
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/controlling-origin-requests.html
|
|
126
|
+
|
|
127
|
+
2. Restrict the custom origin so viewers cannot bypass CloudFront and forge these headers. AWS's documented mechanism is an origin custom header that the origin requires and that CloudFront overwrites before forwarding:
|
|
128
|
+
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/add-origin-custom-headers.html
|
|
129
|
+
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-overview.html
|
|
130
|
+
|
|
131
|
+
3. Use the request-bound API:
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
Trackdown.locate(request.remote_ip, request: request).country
|
|
135
|
+
# => 'United States of America'
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
For a distribution dedicated to this application, use a custom policy containing only the required headers. If you instead use AWS's managed `AllViewerAndCloudFrontHeaders-2022-06` policy, remember that AWS documents it as forwarding all viewer headers, cookies, and query strings:
|
|
139
|
+
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-origin-request-policies.html#managed-origin-request-policy-all-viewer-and-cloudfront
|
|
140
|
+
|
|
85
141
|
### Setup with MaxMind
|
|
86
142
|
|
|
87
143
|
If you want to use `trackdown` with a MaxMind database as the geo IP data provider:
|
|
@@ -132,7 +188,7 @@ production:
|
|
|
132
188
|
refresh_trackdown_database:
|
|
133
189
|
class: TrackdownDatabaseRefreshJob
|
|
134
190
|
queue: default
|
|
135
|
-
schedule: every Saturday at 4am
|
|
191
|
+
schedule: every Saturday at 4am
|
|
136
192
|
```
|
|
137
193
|
|
|
138
194
|
> [!NOTE]
|
|
@@ -140,15 +196,17 @@ production:
|
|
|
140
196
|
|
|
141
197
|
## Usage
|
|
142
198
|
|
|
143
|
-
### With Cloudflare
|
|
199
|
+
### With Cloudflare or CloudFront
|
|
144
200
|
|
|
145
201
|
```ruby
|
|
146
202
|
# In your controller - pass the request object
|
|
147
203
|
result = Trackdown.locate(request.remote_ip, request: request)
|
|
148
204
|
result.country
|
|
149
|
-
# => 'United States'
|
|
205
|
+
# => 'United States of America'
|
|
150
206
|
```
|
|
151
207
|
|
|
208
|
+
In `:auto`, `request.remote_ip` must represent the same viewer that the CDN's corroborating IP header represents. If your Rails proxy configuration deliberately produces a different IP, use MaxMind for that target or explicitly select the correctly configured CDN provider.
|
|
209
|
+
|
|
152
210
|
### With MaxMind or without request object
|
|
153
211
|
|
|
154
212
|
To geolocate an IP address:
|
|
@@ -172,9 +230,9 @@ In fact, there are a few methods you can use:
|
|
|
172
230
|
|
|
173
231
|
```ruby
|
|
174
232
|
result.country_code # => 'US'
|
|
175
|
-
result.country_name # => 'United States'
|
|
233
|
+
result.country_name # => 'United States' (MaxMind's own name for it)
|
|
176
234
|
result.country # => 'United States' (alias for country_name)
|
|
177
|
-
result.city # => 'Mountain View' (from MaxMind or
|
|
235
|
+
result.city # => 'Mountain View' (from MaxMind or configured CDN headers)
|
|
178
236
|
result.region # => 'California'
|
|
179
237
|
result.region_code # => 'CA'
|
|
180
238
|
result.continent # => 'NA'
|
|
@@ -189,8 +247,21 @@ result.country_flag # => '🇺🇸' (alias for flag_emoji)
|
|
|
189
247
|
result.country_info # => # Rich country data from the `countries` gem
|
|
190
248
|
```
|
|
191
249
|
|
|
250
|
+
`country_name` comes from MaxMind's own record on the MaxMind path, and from the [`countries` gem](https://github.com/countries/countries) on the Cloudflare and CloudFront paths — so the same country can read `'United States'` or `'United States of America'` depending on who answered. `country_code` is the one to compare against.
|
|
251
|
+
|
|
252
|
+
And the same result will tell you where all of that came from:
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
result.available? # => true (did we actually resolve a location?)
|
|
256
|
+
result.provider_name # => :maxmind
|
|
257
|
+
result.provider # => :maxmind (alias for provider_name)
|
|
258
|
+
result.resolved_at # => 2026-08-15 04:22:47 UTC
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
There's [a whole section on that](#how-do-you-know) below.
|
|
262
|
+
|
|
192
263
|
> [!NOTE]
|
|
193
|
-
> The
|
|
264
|
+
> The optional fields require Cloudflare's ["Add visitor location headers"](https://developers.cloudflare.com/rules/transform/managed-transforms/reference/), an applicable CloudFront origin request policy, or a MaxMind GeoLite2-City database. AWS notes that city, metro, and postal data may be unavailable and that extended CloudFront location headers are omitted for viewers on AWS networks: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/adding-cloudfront-headers.html#cloudfront-headers-viewer-location. Unavailable fields return `nil` (`city` uses `'Unknown'`).
|
|
194
265
|
|
|
195
266
|
### Rich country information
|
|
196
267
|
|
|
@@ -206,7 +277,8 @@ result.country_info.iso_long_name # => 'The United States of America'
|
|
|
206
277
|
|
|
207
278
|
### Hash data
|
|
208
279
|
|
|
209
|
-
If you prefer
|
|
280
|
+
If you prefer a hash, the no-argument form keeps Trackdown's original 13-key
|
|
281
|
+
shape exactly:
|
|
210
282
|
|
|
211
283
|
```ruby
|
|
212
284
|
result.to_h
|
|
@@ -227,14 +299,277 @@ result.to_h
|
|
|
227
299
|
# }
|
|
228
300
|
```
|
|
229
301
|
|
|
302
|
+
Ask for provenance when you want the provenance-rich shape:
|
|
303
|
+
|
|
304
|
+
```ruby
|
|
305
|
+
result.to_h(include_provenance: true)
|
|
306
|
+
# => {
|
|
307
|
+
# country_code: 'US',
|
|
308
|
+
# country_name: 'United States',
|
|
309
|
+
# city: 'Mountain View',
|
|
310
|
+
# flag_emoji: '🇺🇸',
|
|
311
|
+
# region: 'California',
|
|
312
|
+
# region_code: 'CA',
|
|
313
|
+
# continent: 'NA',
|
|
314
|
+
# timezone: 'America/Los_Angeles',
|
|
315
|
+
# latitude: 37.7749,
|
|
316
|
+
# longitude: -122.4194,
|
|
317
|
+
# postal_code: '94107',
|
|
318
|
+
# metro_code: '807',
|
|
319
|
+
# provider_name: :maxmind,
|
|
320
|
+
# provider_source: :maxmind_local_database,
|
|
321
|
+
# source_trust: nil,
|
|
322
|
+
# resolved_at: 2026-08-15 04:22:47 UTC,
|
|
323
|
+
# available: true,
|
|
324
|
+
# estimated: true,
|
|
325
|
+
# unavailable_reason: nil,
|
|
326
|
+
# accuracy_radius_in_kilometers: 20,
|
|
327
|
+
# accuracy_radius_confidence_percentage: 67,
|
|
328
|
+
# database_build_epoch: 1735689600,
|
|
329
|
+
# database_built_at: 2025-01-01 00:00:00 UTC,
|
|
330
|
+
# country_info: { ... }
|
|
331
|
+
# }
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Or just the fields you actually want to keep, named in plain English, in the order you name them:
|
|
335
|
+
|
|
336
|
+
```ruby
|
|
337
|
+
result.to_h(only: %i[country_code city latitude longitude accuracy_radius_in_kilometers provider_name])
|
|
338
|
+
# => { country_code: 'US', city: 'Mountain View', latitude: 37.7749,
|
|
339
|
+
# longitude: -122.4194, accuracy_radius_in_kilometers: 20, provider_name: :maxmind }
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
`country_info` is a large derived payload, so you can also just leave it out:
|
|
343
|
+
|
|
344
|
+
```ruby
|
|
345
|
+
result.to_h(include_country_info: false)
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
What you name is what you get, in that order — naming a field that doesn't exist raises, and nothing you name is ever dropped, so a typo can't quietly cost you a column in something you're storing.
|
|
349
|
+
|
|
350
|
+
> [!NOTE]
|
|
351
|
+
> The no-argument `to_h` is deliberately backward compatible: same keys, same order, same values. `include_provenance: true` opts into every provenance field except `database_sha256`; a digest can require reading the whole database file, so Trackdown never hides that I/O inside ordinary serialization. Ask for the digest explicitly with `only:` — [see below](#which-database-said-so).
|
|
352
|
+
|
|
353
|
+
## How do you know?
|
|
354
|
+
|
|
355
|
+
Geolocating an IP is a guess. A good one, but a guess — and if you're using it for fraud checks, abuse reports, audit trails, or anything you might one day have to explain, the guess is only half of what you need. You also need to know *how it was made*.
|
|
356
|
+
|
|
357
|
+
So every result carries its own provenance. Nothing here is guessed: every field is either what the provider said, or something derived from it by a rule this README documents. A field the answering provider can't supply is `nil`, never a plausible-looking placeholder.
|
|
358
|
+
|
|
359
|
+
### Which provider answered, and when
|
|
360
|
+
|
|
361
|
+
```ruby
|
|
362
|
+
result.provider_name # => :cloudflare, :cloudfront, or :maxmind
|
|
363
|
+
result.provider_source # => :cloudflare_request_headers, :cloudfront_request_headers, or :maxmind_local_database
|
|
364
|
+
result.resolved_at # => 2026-08-15 04:22:47 UTC
|
|
365
|
+
result.estimated? # => true
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
`provider_name` uses the very same symbols you'd set as `config.provider`, so `result.provider_name == :cloudflare` reads exactly like the config that produced it. In `:auto` mode you get the provider that actually won, after every fallback — if Cloudflare was skipped and MaxMind answered, the result says `:maxmind`.
|
|
369
|
+
|
|
370
|
+
`estimated?` is `true` whenever the provider returned any location estimate. That
|
|
371
|
+
includes a partial result with a city or coordinates but no country, even though
|
|
372
|
+
that same result is `unavailable?`. It is `false` when nothing spatial was
|
|
373
|
+
resolved—for example, a bare Cloudflare `T1` Tor marker. `available?` answers
|
|
374
|
+
"could we name a country?"; `estimated?` answers "did the provider return any
|
|
375
|
+
inferred location?" Those are intentionally independent questions.
|
|
376
|
+
|
|
377
|
+
GeoIP never proves that a person or device was at a location. MaxMind explicitly
|
|
378
|
+
says its data cannot identify a specific household, individual, or street address
|
|
379
|
+
and may locate a VPN or server rather than its end user:
|
|
380
|
+
https://support.maxmind.com/knowledge-base/articles/maxmind-geolocation-accuracy
|
|
381
|
+
|
|
382
|
+
### Did we actually find anything?
|
|
383
|
+
|
|
384
|
+
```ruby
|
|
385
|
+
result.available? # => false
|
|
386
|
+
result.unavailable? # => true
|
|
387
|
+
result.unavailable_reason # => :address_not_found
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
The reasons are stable symbols, part of the public API, and never translated:
|
|
391
|
+
|
|
392
|
+
| Reason | What happened |
|
|
393
|
+
|---|---|
|
|
394
|
+
| `:no_provider_available` | No usable CDN header result and no MaxMind database. Nobody could answer. |
|
|
395
|
+
| `:address_not_found` | We searched a real database and this address simply isn't in it. |
|
|
396
|
+
| `:provider_returned_unknown_country` | The CDN answered, but with no country — Cloudflare's `XX`, Unicode's unknown/invalid `ZZ`, or `T1` for a visitor arriving over Tor. |
|
|
397
|
+
| `:provider_data_incomplete` | A provider returned a record, but not enough of one to name a country. |
|
|
398
|
+
|
|
399
|
+
`unavailable?` means precisely *"we could not name a country"*. Some of those results still carry something useful — a Tor result keeps `country_code == 'T1'`, and an incomplete database record can still have a city and coordinates. If those are worth having to you, read them; Trackdown hands back everything it got either way.
|
|
400
|
+
|
|
401
|
+
> [!NOTE]
|
|
402
|
+
> `country_name` and `city` still return the display string `'Unknown'` when a provider has no value, exactly as they always have — existing code keeps working. New code should branch on `available?` / `unavailable_reason` rather than comparing against a display string.
|
|
403
|
+
|
|
404
|
+
### How precise is it?
|
|
405
|
+
|
|
406
|
+
MaxMind's City records carry an accuracy radius, and Trackdown passes it straight through:
|
|
407
|
+
|
|
408
|
+
```ruby
|
|
409
|
+
result.accuracy_radius_in_kilometers # => 20
|
|
410
|
+
result.accuracy_radius_km # => 20 (alias)
|
|
411
|
+
result.accuracy_radius_confidence_percentage # => 67
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
That reads: *the address is within 20 km of these coordinates, with 67% confidence* — [MaxMind's own definition](https://support.maxmind.com/knowledge-base/articles/maxmind-geolocation-accuracy). Neither Cloudflare's [exact visitor-location field list](https://developers.cloudflare.com/rules/transform/managed-transforms/reference/#add-visitor-location-headers) nor CloudFront's [exact viewer-location header list](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/adding-cloudfront-headers.html#cloudfront-headers-viewer-location) publishes an accuracy figure, so those results return `nil` instead of an invented one.
|
|
415
|
+
|
|
416
|
+
### Which database said so?
|
|
417
|
+
|
|
418
|
+
```ruby
|
|
419
|
+
result.database_build_epoch # => 1735689600
|
|
420
|
+
result.database_built_at # => 2025-01-01 00:00:00 UTC
|
|
421
|
+
result.database_sha256 # => '4f8b42c22dd3729b519ba6f68d2da7cc…'
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
The build epoch comes from the exact answering reader's database metadata; the
|
|
425
|
+
MaxMind DB format defines it as the database build timestamp in Unix-epoch form:
|
|
426
|
+
https://maxmind.github.io/MaxMind-DB/#build_epoch. In a serialized hash it is
|
|
427
|
+
included only when you ask for provenance or name it with `only:`.
|
|
428
|
+
|
|
429
|
+
The digest is lazy because it costs a full read of the database file. Trackdown
|
|
430
|
+
computes it the first time someone asks and shares that lazy fingerprint among
|
|
431
|
+
pooled readers bound to the same path, file identity, and build epoch. Ordinary
|
|
432
|
+
lookups and ordinary `to_h` calls never compute it. When you want it, name it:
|
|
433
|
+
|
|
434
|
+
```ruby
|
|
435
|
+
result.database_sha256 # the reader
|
|
436
|
+
result.to_h(only: %i[database_sha256]) # or in a hash
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
Every result retains the fingerprint of the exact reader that answered it. That
|
|
440
|
+
matters because the MaxMind Ruby reader can either copy the file into memory when
|
|
441
|
+
opened or retain an open file handle, depending on mode:
|
|
442
|
+
https://github.com/maxmind/MaxMind-DB-Reader-ruby/blob/v1.2.0/lib/maxmind/db/memory_reader.rb#L7-L15
|
|
443
|
+
and
|
|
444
|
+
https://github.com/maxmind/MaxMind-DB-Reader-ruby/blob/v1.2.0/lib/maxmind/db/file_reader.rb#L36-L55.
|
|
445
|
+
If the configured path is replaced after an older reader opened it, the older
|
|
446
|
+
result keeps that reader's build epoch and its digest becomes `nil`; it never
|
|
447
|
+
borrows the replacement file's digest. A new reader receives a new fingerprint.
|
|
448
|
+
|
|
449
|
+
`Trackdown.update_database` writes the complete download to a temporary file in
|
|
450
|
+
the destination directory, flushes it, and replaces the configured path with one
|
|
451
|
+
`File.rename` only after the archive contains a `.mmdb` file. That prevents
|
|
452
|
+
Trackdown's updater from truncating a database underneath a `MODE_FILE` reader.
|
|
453
|
+
Ruby's rename contract is documented at
|
|
454
|
+
https://docs.ruby-lang.org/en/3.3/File.html#method-c-rename, and the reader's
|
|
455
|
+
open-file behavior is visible in the exact source linked above.
|
|
456
|
+
|
|
457
|
+
> [!NOTE]
|
|
458
|
+
> `Trackdown.update_database` drops the cached reader pool in the process that ran it, so that process's next lookup opens the new database. If you refresh from a separate process — a cron job or a `rails runner`, as the scheduling section recommends — your web workers keep serving the database they already have open until they restart. Call `Trackdown::Providers::MaxmindProvider.reset_database!` in a worker to make its next lookup pick up the new file without a restart.
|
|
459
|
+
|
|
460
|
+
### Did the request really come through your CDN?
|
|
461
|
+
|
|
462
|
+
Here's the uncomfortable part. `CF-IPCountry` is just a header. Anyone who can reach your origin directly can send you one, and it will look exactly like the real thing. Matching `CF-Connecting-IP` against the IP you're asking about — which `:auto` already does — is useful corroboration, but it is *not* proof that the request came through Cloudflare.
|
|
463
|
+
|
|
464
|
+
Only your own origin protection can vouch for the request path. Cloudflare and
|
|
465
|
+
CloudFront are configured independently: a verified path through one CDN must
|
|
466
|
+
never authenticate the other CDN's headers.
|
|
467
|
+
|
|
468
|
+
For Cloudflare, have the layer that actually validates Authenticated Origin Pulls
|
|
469
|
+
or the Cloudflare peer network place a non-viewer-controlled boolean in the Rack
|
|
470
|
+
environment, then read that boolean:
|
|
471
|
+
|
|
472
|
+
```ruby
|
|
473
|
+
Trackdown.configure do |config|
|
|
474
|
+
config.verify_request_came_through_trusted_cloudflare_path_with do |request|
|
|
475
|
+
request.env['my_app.cloudflare_origin_was_verified'] == true
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
For a CloudFront custom origin header, compare the CloudFront-only secret and
|
|
481
|
+
refuse to boot if the expected value is blank:
|
|
482
|
+
|
|
483
|
+
```ruby
|
|
484
|
+
expected_cloudfront_origin_secret =
|
|
485
|
+
Rails.application.credentials.dig(:cloudfront, :origin_secret).to_s
|
|
486
|
+
raise 'Missing CloudFront origin secret' if expected_cloudfront_origin_secret.empty?
|
|
487
|
+
|
|
488
|
+
Trackdown.configure do |config|
|
|
489
|
+
config.verify_request_came_through_trusted_cloudfront_path_with do |request|
|
|
490
|
+
supplied_cloudfront_origin_secret =
|
|
491
|
+
request.env['HTTP_X_CLOUDFRONT_ORIGIN_SECRET'].to_s
|
|
492
|
+
|
|
493
|
+
!supplied_cloudfront_origin_secret.empty? &&
|
|
494
|
+
ActiveSupport::SecurityUtils.secure_compare(
|
|
495
|
+
supplied_cloudfront_origin_secret,
|
|
496
|
+
expected_cloudfront_origin_secret
|
|
497
|
+
)
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
Both non-empty checks matter. Rails implements `secure_compare` as an equal-byte-
|
|
503
|
+
length check followed by a fixed-length comparison, so two empty strings compare
|
|
504
|
+
equal:
|
|
505
|
+
https://api.rubyonrails.org/classes/ActiveSupport/SecurityUtils.html#method-c-secure_compare
|
|
506
|
+
|
|
507
|
+
```ruby
|
|
508
|
+
result.source_trust # => :host_verified (or :unverified)
|
|
509
|
+
result.source_was_verified_by_host? # => true
|
|
510
|
+
result.host_verified? # => true (alias)
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
Without that callback, a request-backed result is always `:unverified` — no matter how complete or how corroborated its headers are. Header presence alone can never produce `:host_verified`. MaxMind results have no request path to verify at all, so their `source_trust` is `nil`.
|
|
514
|
+
|
|
515
|
+
What you put in each callback is whatever that deployment path actually proves:
|
|
516
|
+
|
|
517
|
+
- **Cloudflare:** [Authenticated Origin Pulls](https://developers.cloudflare.com/ssl/origin-configuration/authenticated-origin-pull/), or [blocking every IP that isn't Cloudflare's](https://developers.cloudflare.com/fundamentals/concepts/cloudflare-ip-addresses/#block-other-ip-addresses-recommended).
|
|
518
|
+
- **CloudFront:** [an origin custom header CloudFront adds and viewers can't](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/add-origin-custom-headers.html), plus [restricting the custom origin](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-overview.html).
|
|
519
|
+
|
|
520
|
+
The separation is security-relevant. Cloudflare documents that it passes ordinary
|
|
521
|
+
viewer request headers to the origin:
|
|
522
|
+
https://developers.cloudflare.com/fundamentals/reference/http-headers/#request-headers.
|
|
523
|
+
AWS documents that `AllViewerAndCloudFrontHeaders-2022-06` forwards all viewer
|
|
524
|
+
headers:
|
|
525
|
+
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-origin-request-policies.html#managed-origin-request-policy-all-viewer-and-cloudfront.
|
|
526
|
+
Therefore a trusted CloudFront path cannot vouch for a forwarded `CF-*` family,
|
|
527
|
+
and a trusted Cloudflare path cannot vouch for a forwarded `CloudFront-*` family.
|
|
528
|
+
|
|
529
|
+
> [!IMPORTANT]
|
|
530
|
+
> Trackdown **reports** this trust state. It doesn't act on it — an unverified location is still returned in full. Whether an unverified location is good enough to ban an account, or only good enough to show a flag in the UI, is your application's call, not a gem's.
|
|
531
|
+
|
|
532
|
+
### Putting it together
|
|
533
|
+
|
|
534
|
+
Act on the strength of the evidence, not just on the answer:
|
|
535
|
+
|
|
536
|
+
```ruby
|
|
537
|
+
location = Trackdown.locate(request.remote_ip, request: request)
|
|
538
|
+
|
|
539
|
+
if location.available? && location.host_verified?
|
|
540
|
+
enforce_geoblock!(location.country_code) # evidence you'd be willing to defend
|
|
541
|
+
elsif location.available?
|
|
542
|
+
flag_for_review(location.country_code) # good enough to look at, not to act on
|
|
543
|
+
end
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
And when you store it, store how you got it:
|
|
547
|
+
|
|
548
|
+
```ruby
|
|
549
|
+
AbuseReport.create!(
|
|
550
|
+
ip: request.remote_ip,
|
|
551
|
+
**location.to_h(only: %i[
|
|
552
|
+
country_code city latitude longitude accuracy_radius_in_kilometers
|
|
553
|
+
provider_name provider_source source_trust resolved_at database_built_at
|
|
554
|
+
])
|
|
555
|
+
)
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
The names you can pass to `only:` are `Trackdown::LocationResult::FIELDS`. Three
|
|
559
|
+
ready-made slices come with it: `LOCATION_FIELDS` (where the IP is),
|
|
560
|
+
`PROVENANCE_FIELDS` (how we know), and `DEFAULT_FIELDS` (exactly what a
|
|
561
|
+
no-argument `to_h` returns, `country_info` included).
|
|
562
|
+
|
|
230
563
|
## Configuration
|
|
231
564
|
|
|
232
565
|
### Provider Options
|
|
233
566
|
|
|
234
567
|
```ruby
|
|
235
568
|
Trackdown.configure do |config|
|
|
236
|
-
# :auto -
|
|
569
|
+
# :auto - Use one IP-corroborated edge provider; fall back to MaxMind when none
|
|
570
|
+
# or both are valid (default, recommended for unambiguous deployments)
|
|
237
571
|
# :cloudflare - Only use Cloudflare headers
|
|
572
|
+
# :cloudfront - Only use Amazon CloudFront headers
|
|
238
573
|
# :maxmind - Only use MaxMind database
|
|
239
574
|
config.provider = :auto
|
|
240
575
|
end
|
|
@@ -260,6 +595,9 @@ Trackdown.configure do |config|
|
|
|
260
595
|
|
|
261
596
|
# General
|
|
262
597
|
config.reject_private_ips = true # Reject 192.168.x.x, 127.0.0.1, etc.
|
|
598
|
+
|
|
599
|
+
# Optional provider-specific source-trust callbacks are documented, with
|
|
600
|
+
# fail-closed examples, in "Did the request really come through your CDN?"
|
|
263
601
|
end
|
|
264
602
|
```
|
|
265
603
|
|
|
@@ -290,12 +628,131 @@ When you enable "IP Geolocation" in Cloudflare, they add the `CF-IPCountry` head
|
|
|
290
628
|
| `cf-postal-code` | `postal_code` |
|
|
291
629
|
| `cf-timezone` | `timezone` |
|
|
292
630
|
|
|
293
|
-
Trackdown reads these headers directly from the request
|
|
631
|
+
Trackdown reads these headers directly from the request—no database lookup or
|
|
632
|
+
external API call. It rejects non-finite or out-of-range coordinates using the
|
|
633
|
+
same WGS-84 bounds as the CloudFront provider. Exact Cloudflare field source:
|
|
634
|
+
https://developers.cloudflare.com/rules/transform/managed-transforms/reference/#add-visitor-location-headers.
|
|
635
|
+
Exact coordinate bounds:
|
|
636
|
+
https://www.rfc-editor.org/rfc/rfc5870#section-3.4.2.
|
|
637
|
+
|
|
638
|
+
Cloudflare's `XX` and `T1` values are not countries. Trackdown preserves `T1`
|
|
639
|
+
as useful Tor provenance but reports the location unavailable and renders the
|
|
640
|
+
white unknown flag rather than a malformed regional-indicator glyph. Unicode's
|
|
641
|
+
`ZZ` unknown/invalid territory is unavailable too. Non-string or malformed
|
|
642
|
+
optional values, and non-string, invalidly encoded, or malformed country values,
|
|
643
|
+
are ignored rather than allowed to raise from a lookup. Exact Cloudflare code contract:
|
|
644
|
+
https://developers.cloudflare.com/fundamentals/reference/http-headers/#cf-ipcountry.
|
|
645
|
+
Exact Unicode `ZZ` semantics:
|
|
646
|
+
https://www.unicode.org/reports/tr35/tr35-78/tr35.html#unicode_region_subtag_validity.
|
|
647
|
+
|
|
648
|
+
### CloudFront Provider
|
|
649
|
+
|
|
650
|
+
When your app is behind Amazon CloudFront and an origin request policy adds the viewer-location headers, Trackdown maps the following values:
|
|
651
|
+
|
|
652
|
+
| CloudFront header | `trackdown` field |
|
|
653
|
+
|---|---|
|
|
654
|
+
| `CloudFront-Viewer-Country` | `country_code` |
|
|
655
|
+
| `CloudFront-Viewer-City` | `city` |
|
|
656
|
+
| `CloudFront-Viewer-Country-Region-Name` | `region` |
|
|
657
|
+
| `CloudFront-Viewer-Country-Region` | `region_code` |
|
|
658
|
+
| `CloudFront-Viewer-Latitude` | `latitude` |
|
|
659
|
+
| `CloudFront-Viewer-Longitude` | `longitude` |
|
|
660
|
+
| `CloudFront-Viewer-Metro-Code` | `metro_code` |
|
|
661
|
+
| `CloudFront-Viewer-Postal-Code` | `postal_code` |
|
|
662
|
+
| `CloudFront-Viewer-Time-Zone` | `timezone` |
|
|
663
|
+
|
|
664
|
+
Exact AWS source for every mapped header and its semantics:
|
|
665
|
+
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/adding-cloudfront-headers.html#cloudfront-headers-viewer-location
|
|
666
|
+
|
|
667
|
+
Important details from that contract:
|
|
668
|
+
|
|
669
|
+
- `CloudFront-Viewer-Country` is validated against the `countries` catalog before CloudFront is considered available, with one deliberate exception: `XK` is preserved for Kosovo. Unicode CLDR documents `XK` as established industry practice and `ZZ` as unknown/invalid territory: https://www.unicode.org/reports/tr35/tr35-78/tr35.html#unicode_region_subtag_validity.
|
|
670
|
+
- Non-ASCII viewer-location values are RFC 3986 percent-encoded by CloudFront. Trackdown decodes them as UTF-8 without applying HTML form `+`-as-space behavior. RFC source: https://www.rfc-editor.org/rfc/rfc3986#section-2.1.
|
|
671
|
+
- Latitude and longitude are accepted only when finite and inside the WGS-84 bounds of `-90..90` and `-180..180`. Bounds source: https://www.rfc-editor.org/rfc/rfc5870#section-3.4.2.
|
|
672
|
+
- City, metro code, and postal code may be unavailable. Extended headers are omitted for viewer IPs on the AWS network.
|
|
673
|
+
- CloudFront does not provide a continent header, so `continent` is derived from the validated country via the [`countries` gem](https://github.com/countries/countries) and normalized to the same two-letter code (`NA`, `EU`, …) returned by the other providers.
|
|
674
|
+
|
|
675
|
+
In `:auto`, Trackdown compares the target IP with `CloudFront-Viewer-Address`. A missing, malformed, or mismatching address causes that candidate to be skipped. If Cloudflare and CloudFront both appear valid, Trackdown refuses to guess, tries MaxMind, and otherwise returns `'Unknown'`. An explicitly configured `:cloudfront` provider reads valid CloudFront location headers without requiring the address comparison, which is useful only when the deployment's CloudFront trust boundary has already been secured.
|
|
676
|
+
|
|
677
|
+
The AWS managed policy includes every header in the table plus `CloudFront-Viewer-Address`, but it also forwards all viewer headers, cookies, and query strings:
|
|
678
|
+
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-origin-request-policies.html#managed-origin-request-policy-all-viewer-and-cloudfront
|
|
679
|
+
|
|
680
|
+
Header values are trustworthy only when the origin rejects direct requests. Exact AWS sources:
|
|
681
|
+
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/add-origin-custom-headers.html
|
|
682
|
+
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-overview.html
|
|
294
683
|
|
|
295
684
|
### MaxMind Provider
|
|
296
685
|
|
|
297
|
-
Downloads the [GeoLite2-City](https://dev.maxmind.com/geoip/docs/databases/city-and-country/) database to your server and performs local lookups using connection pooling for performance. All fields (`country`, `city`, `region`, `continent`, `timezone`, `latitude`, `longitude`, `postal_code`, `metro_code`) are extracted from the database record.
|
|
686
|
+
Downloads the [GeoLite2-City](https://dev.maxmind.com/geoip/docs/databases/city-and-country/) database to your server and performs local lookups using connection pooling for performance. All fields (`country`, `city`, `region`, `continent`, `timezone`, `latitude`, `longitude`, `postal_code`, `metro_code`) are extracted from the database record, along with the record's [`accuracy_radius`](https://dev.maxmind.com/geoip/docs/databases/city-and-country/city-binary/) and the identity of the database that answered — see [How do you know?](#how-do-you-know).
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
## Docker & Container Deployments
|
|
690
|
+
|
|
691
|
+
When deploying with Docker, Kubernetes, or similar container orchestration, the MaxMind database file needs special handling since container filesystems are ephemeral.
|
|
692
|
+
|
|
693
|
+
### Option 1: Persistent Volume (Recommended)
|
|
694
|
+
|
|
695
|
+
Mount a persistent volume for the database file so it survives container restarts and deployments.
|
|
298
696
|
|
|
697
|
+
**Kamal (`config/deploy.yml`):**
|
|
698
|
+
```yaml
|
|
699
|
+
volumes:
|
|
700
|
+
- "trackdown_data:/rails/db/geodata"
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
Then configure the database path:
|
|
704
|
+
```ruby
|
|
705
|
+
# config/initializers/trackdown.rb
|
|
706
|
+
config.database_path = Rails.root.join('db', 'geodata', 'GeoLite2-City.mmdb').to_s
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
**Docker Compose:**
|
|
710
|
+
```yaml
|
|
711
|
+
services:
|
|
712
|
+
app:
|
|
713
|
+
volumes:
|
|
714
|
+
- trackdown_data:/rails/db/geodata
|
|
715
|
+
|
|
716
|
+
volumes:
|
|
717
|
+
trackdown_data:
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
### Option 2: Download on Container Start
|
|
721
|
+
|
|
722
|
+
If you prefer not to use volumes, download the database when the container starts. Add to your entrypoint or a post-deploy hook:
|
|
723
|
+
|
|
724
|
+
```bash
|
|
725
|
+
# In your entrypoint.sh or deploy hook
|
|
726
|
+
bin/rails runner "Trackdown.update_database unless File.exist?(Trackdown.configuration.database_path)"
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
Or create a job that runs on boot:
|
|
730
|
+
|
|
731
|
+
```ruby
|
|
732
|
+
# config/initializers/trackdown_boot.rb
|
|
733
|
+
Rails.application.config.after_initialize do
|
|
734
|
+
if Rails.env.production? && !File.exist?(Trackdown.configuration.database_path)
|
|
735
|
+
Trackdown.update_database
|
|
736
|
+
end
|
|
737
|
+
end
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
> [!WARNING]
|
|
741
|
+
> Option 2 adds startup time (~10-30 seconds) on fresh deploys and requires network access during boot. A persistent volume is more reliable for production.
|
|
742
|
+
|
|
743
|
+
### Background Jobs Consideration
|
|
744
|
+
|
|
745
|
+
When using background job processors (Sidekiq, SolidQueue, GoodJob), geolocation lookups in jobs **cannot use Cloudflare or CloudFront headers** because there is no HTTP request. These jobs fall back to MaxMind automatically under `:auto`.
|
|
746
|
+
|
|
747
|
+
Make sure MaxMind is properly configured if you're doing geolocation in background jobs:
|
|
748
|
+
|
|
749
|
+
```ruby
|
|
750
|
+
# This works in controllers (has request)
|
|
751
|
+
Trackdown.locate(ip, request: request) # Uses one verified CDN provider if available
|
|
752
|
+
|
|
753
|
+
# This works in background jobs (no request)
|
|
754
|
+
Trackdown.locate(ip) # Falls back to MaxMind
|
|
755
|
+
```
|
|
299
756
|
|
|
300
757
|
## Development
|
|
301
758
|
|
|
@@ -22,17 +22,26 @@ module Trackdown
|
|
|
22
22
|
def display_post_install_message
|
|
23
23
|
say "\tThe `trackdown` gem has been successfully installed!", :green
|
|
24
24
|
say "\nChoose your setup path:"
|
|
25
|
-
say "\n Option 1: Cloudflare (
|
|
25
|
+
say "\n Option 1: Cloudflare (Header-Based)"
|
|
26
26
|
say " 1. Ensure your app is behind Cloudflare"
|
|
27
27
|
say " 2. Enable 'IP Geolocation' in Cloudflare dashboard (Network settings)"
|
|
28
28
|
say " 3. Use: Trackdown.locate(request.remote_ip, request: request)"
|
|
29
|
-
say "
|
|
30
|
-
say "
|
|
29
|
+
say " 4. Restrict direct-origin access before trusting CF-* headers"
|
|
30
|
+
say " Cloudflare origin security: https://developers.cloudflare.com/ssl/origin-configuration/authenticated-origin-pull/"
|
|
31
|
+
say " No API keys or database needed after CDN/origin setup."
|
|
32
|
+
say "\n Option 2: Amazon CloudFront (Header-Based)"
|
|
33
|
+
say " 1. Forward CloudFront viewer-location headers and CloudFront-Viewer-Address"
|
|
34
|
+
say " 2. Restrict direct-origin access before trusting CloudFront-* headers"
|
|
35
|
+
say " 3. Use: Trackdown.locate(request.remote_ip, request: request)"
|
|
36
|
+
say " AWS headers: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/adding-cloudfront-headers.html#cloudfront-headers-viewer-location"
|
|
37
|
+
say " AWS origin security: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-overview.html"
|
|
38
|
+
say "\n Option 3: MaxMind (BYOK)"
|
|
31
39
|
say " 1. Configure your MaxMind credentials in `config/initializers/trackdown.rb`"
|
|
32
40
|
say " 2. Run 'Trackdown.update_database' to download the database"
|
|
33
41
|
say " 3. Schedule TrackdownDatabaseRefreshJob to run weekly"
|
|
34
|
-
say "\n Option
|
|
35
|
-
say " The default :auto mode
|
|
42
|
+
say "\n Option 4: Auto (Verified Edge + MaxMind Fallback)"
|
|
43
|
+
say " The default :auto mode uses one IP-corroborated CDN provider"
|
|
44
|
+
say " and falls back safely when no unique edge provider can be verified"
|
|
36
45
|
say "\nEnjoy `trackdown`!", :green
|
|
37
46
|
end
|
|
38
47
|
|