trackdown 0.3.0 → 0.3.1
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 +7 -0
- data/README.md +71 -1
- data/lib/trackdown/providers/auto_provider.rb +67 -2
- data/lib/trackdown/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 78637df00207906a24e8553f69a16536a402d59af543c531613b9b3b881bad2c
|
|
4
|
+
data.tar.gz: 21c3fc9d6845db2322bfe85cd1a85d87539c16b211c12a3f3c9dc4de3bc4dd12
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '08cc2f24e4339a74690c74a9adfb687fb9db337c4215911f7673cf2c9583054e8325b8250c9ab219f7c878a80b8ba2287e238cf7776b61fe475830ef54203a07'
|
|
7
|
+
data.tar.gz: dfbff6a70b8d6ecbd8b18b191d0b6a819af2e72aad179e8ee9c007f8511af5a22a9531d80a698c87f33759f27ec48905aec23ffa6282b13b44ca1bb9cf021400
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [0.3.1] - 2026-02-24
|
|
2
|
+
|
|
3
|
+
- Fix incorrect Cloudflare geolocation when an upstream proxy sits before Cloudflare
|
|
4
|
+
- In `:auto` mode, compare `CF-Connecting-IP` with the target IP and fall back to MaxMind on mismatch
|
|
5
|
+
- Normalize compared IPs with `IPAddr` so equivalent IPv6 / IPv4-mapped forms are treated as matches
|
|
6
|
+
- Add tests covering matching and mismatching `CF-Connecting-IP` scenarios
|
|
7
|
+
|
|
1
8
|
## [0.3.0] - 2026-02-08
|
|
2
9
|
|
|
3
10
|
- Add 8 new geolocation fields: `region`, `region_code`, `continent`, `timezone`, `latitude`, `longitude`, `postal_code`, `metro_code`
|
data/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
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
|
|
|
@@ -132,7 +134,7 @@ production:
|
|
|
132
134
|
refresh_trackdown_database:
|
|
133
135
|
class: TrackdownDatabaseRefreshJob
|
|
134
136
|
queue: default
|
|
135
|
-
schedule: every Saturday at 4am
|
|
137
|
+
schedule: every Saturday at 4am
|
|
136
138
|
```
|
|
137
139
|
|
|
138
140
|
> [!NOTE]
|
|
@@ -297,6 +299,74 @@ Trackdown reads these headers directly from the request with zero overhead — n
|
|
|
297
299
|
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.
|
|
298
300
|
|
|
299
301
|
|
|
302
|
+
## Docker & Container Deployments
|
|
303
|
+
|
|
304
|
+
When deploying with Docker, Kubernetes, or similar container orchestration, the MaxMind database file needs special handling since container filesystems are ephemeral.
|
|
305
|
+
|
|
306
|
+
### Option 1: Persistent Volume (Recommended)
|
|
307
|
+
|
|
308
|
+
Mount a persistent volume for the database file so it survives container restarts and deployments.
|
|
309
|
+
|
|
310
|
+
**Kamal (`config/deploy.yml`):**
|
|
311
|
+
```yaml
|
|
312
|
+
volumes:
|
|
313
|
+
- "trackdown_data:/rails/db/geodata"
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Then configure the database path:
|
|
317
|
+
```ruby
|
|
318
|
+
# config/initializers/trackdown.rb
|
|
319
|
+
config.database_path = Rails.root.join('db', 'geodata', 'GeoLite2-City.mmdb').to_s
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**Docker Compose:**
|
|
323
|
+
```yaml
|
|
324
|
+
services:
|
|
325
|
+
app:
|
|
326
|
+
volumes:
|
|
327
|
+
- trackdown_data:/rails/db/geodata
|
|
328
|
+
|
|
329
|
+
volumes:
|
|
330
|
+
trackdown_data:
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Option 2: Download on Container Start
|
|
334
|
+
|
|
335
|
+
If you prefer not to use volumes, download the database when the container starts. Add to your entrypoint or a post-deploy hook:
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
# In your entrypoint.sh or deploy hook
|
|
339
|
+
bin/rails runner "Trackdown.update_database unless File.exist?(Trackdown.configuration.database_path)"
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Or create a job that runs on boot:
|
|
343
|
+
|
|
344
|
+
```ruby
|
|
345
|
+
# config/initializers/trackdown_boot.rb
|
|
346
|
+
Rails.application.config.after_initialize do
|
|
347
|
+
if Rails.env.production? && !File.exist?(Trackdown.configuration.database_path)
|
|
348
|
+
Trackdown.update_database
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
> [!WARNING]
|
|
354
|
+
> 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.
|
|
355
|
+
|
|
356
|
+
### Background Jobs Consideration
|
|
357
|
+
|
|
358
|
+
When using background job processors (Sidekiq, SolidQueue, GoodJob), geolocation lookups in jobs **cannot use Cloudflare headers** since there's no HTTP request. These jobs will fall back to MaxMind automatically when using `:auto` provider.
|
|
359
|
+
|
|
360
|
+
Make sure MaxMind is properly configured if you're doing geolocation in background jobs:
|
|
361
|
+
|
|
362
|
+
```ruby
|
|
363
|
+
# This works in controllers (has request)
|
|
364
|
+
Trackdown.locate(ip, request: request) # Uses Cloudflare if available
|
|
365
|
+
|
|
366
|
+
# This works in background jobs (no request)
|
|
367
|
+
Trackdown.locate(ip) # Falls back to MaxMind
|
|
368
|
+
```
|
|
369
|
+
|
|
300
370
|
## Development
|
|
301
371
|
|
|
302
372
|
After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec rake test` to run the Minitest tests.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'ipaddr'
|
|
4
|
+
|
|
3
5
|
require_relative 'base_provider'
|
|
4
6
|
require_relative 'cloudflare_provider'
|
|
5
7
|
require_relative 'maxmind_provider'
|
|
@@ -9,11 +11,19 @@ module Trackdown
|
|
|
9
11
|
# Intelligent provider that automatically selects the best available provider
|
|
10
12
|
# Priority order:
|
|
11
13
|
# 1. Cloudflare (fastest, zero overhead, no external dependencies)
|
|
12
|
-
# 2. MaxMind (fallback when Cloudflare not available)
|
|
14
|
+
# 2. MaxMind (fallback when Cloudflare not available or IP mismatch)
|
|
13
15
|
#
|
|
14
16
|
# This is the recommended default for most applications
|
|
17
|
+
#
|
|
18
|
+
# IMPORTANT: When there's an upstream proxy before Cloudflare (e.g., a legacy
|
|
19
|
+
# API gateway), Cloudflare's geo headers will reflect the proxy's location,
|
|
20
|
+
# not the real client. AutoProvider detects this by comparing CF-Connecting-IP
|
|
21
|
+
# with the passed IP and falls back to MaxMind when they don't match.
|
|
15
22
|
class AutoProvider < BaseProvider
|
|
23
|
+
CF_CONNECTING_IP_HEADER = 'HTTP_CF_CONNECTING_IP'
|
|
24
|
+
|
|
16
25
|
@@warned_no_providers = false
|
|
26
|
+
@@warned_ip_mismatch = false
|
|
17
27
|
@@warn_mutex = Mutex.new
|
|
18
28
|
|
|
19
29
|
class << self
|
|
@@ -29,8 +39,16 @@ module Trackdown
|
|
|
29
39
|
# @return [LocationResult] The location information
|
|
30
40
|
def locate(ip, request: nil)
|
|
31
41
|
# Try Cloudflare first - it's instant and free!
|
|
42
|
+
# But only if the IP matches what Cloudflare geolocated
|
|
32
43
|
if CloudflareProvider.available?(request: request)
|
|
33
|
-
|
|
44
|
+
if cloudflare_ip_matches?(ip, request)
|
|
45
|
+
return CloudflareProvider.locate(ip, request: request)
|
|
46
|
+
else
|
|
47
|
+
# IP mismatch: there's likely an upstream proxy before Cloudflare
|
|
48
|
+
# Cloudflare's geo headers are based on the proxy IP, not the real client
|
|
49
|
+
# Fall back to MaxMind with the correct IP
|
|
50
|
+
warn_ip_mismatch(ip, request)
|
|
51
|
+
end
|
|
34
52
|
end
|
|
35
53
|
|
|
36
54
|
# Fall back to MaxMind if available
|
|
@@ -45,6 +63,53 @@ module Trackdown
|
|
|
45
63
|
|
|
46
64
|
private
|
|
47
65
|
|
|
66
|
+
# Check if the IP we want to geolocate matches what Cloudflare saw as the client
|
|
67
|
+
# If they don't match, there's an upstream proxy and Cloudflare's geo headers are wrong
|
|
68
|
+
def cloudflare_ip_matches?(ip, request)
|
|
69
|
+
return true unless request # No request means we can't check
|
|
70
|
+
|
|
71
|
+
cf_connecting_ip = request.env[CF_CONNECTING_IP_HEADER]
|
|
72
|
+
return true if cf_connecting_ip.nil? || cf_connecting_ip.empty?
|
|
73
|
+
|
|
74
|
+
# Normalize IPs for comparison (handle IPv6 formatting differences)
|
|
75
|
+
normalize_ip(ip) == normalize_ip(cf_connecting_ip)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def normalize_ip(ip)
|
|
79
|
+
return nil if ip.nil?
|
|
80
|
+
|
|
81
|
+
value = ip.to_s.strip
|
|
82
|
+
return nil if value.empty?
|
|
83
|
+
|
|
84
|
+
parsed_ip = IPAddr.new(value)
|
|
85
|
+
parsed_ip = parsed_ip.native if parsed_ip.ipv4_mapped?
|
|
86
|
+
parsed_ip.to_s.downcase
|
|
87
|
+
rescue IPAddr::InvalidAddressError
|
|
88
|
+
# If parsing fails, fall back to string comparison so we still have
|
|
89
|
+
# deterministic behavior and can trigger MaxMind fallback on mismatch.
|
|
90
|
+
value.downcase
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def warn_ip_mismatch(ip, request)
|
|
94
|
+
return if @@warned_ip_mismatch
|
|
95
|
+
|
|
96
|
+
@@warn_mutex.synchronize do
|
|
97
|
+
return if @@warned_ip_mismatch
|
|
98
|
+
@@warned_ip_mismatch = true
|
|
99
|
+
|
|
100
|
+
cf_ip = request&.env&.dig(CF_CONNECTING_IP_HEADER)
|
|
101
|
+
message = "[Trackdown] IP mismatch detected: request IP (#{ip}) differs from " \
|
|
102
|
+
"CF-Connecting-IP (#{cf_ip}). This usually means there's an upstream " \
|
|
103
|
+
"proxy before Cloudflare. Falling back to MaxMind for accurate geolocation."
|
|
104
|
+
|
|
105
|
+
if defined?(Rails)
|
|
106
|
+
Rails.logger.info(message)
|
|
107
|
+
else
|
|
108
|
+
warn(message)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
48
113
|
def warn_no_providers
|
|
49
114
|
# Only warn once per process to avoid log spam
|
|
50
115
|
return if @@warned_no_providers
|
data/lib/trackdown/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: trackdown
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rameerez
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-02-
|
|
10
|
+
date: 2026-02-24 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: countries
|