python-ipware 2.0.5__tar.gz → 4.1.0__tar.gz

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.
Files changed (36) hide show
  1. python_ipware-4.1.0/.gitignore +60 -0
  2. python_ipware-4.1.0/CHANGELOG.md +152 -0
  3. python_ipware-4.1.0/PKG-INFO +372 -0
  4. python_ipware-4.1.0/README.md +330 -0
  5. python_ipware-4.1.0/SECURITY.md +18 -0
  6. python_ipware-4.1.0/pyproject.toml +126 -0
  7. python_ipware-4.1.0/python_ipware/__init__.py +6 -0
  8. python_ipware-4.1.0/python_ipware/__version__.py +1 -0
  9. {python_ipware-2.0.5 → python_ipware-4.1.0/python_ipware/legacy}/README.md +13 -0
  10. python_ipware-4.1.0/python_ipware/legacy/__init__.py +12 -0
  11. python_ipware-4.1.0/python_ipware/modern/__init__.py +5 -0
  12. python_ipware-4.1.0/python_ipware/modern/defaults.py +52 -0
  13. python_ipware-4.1.0/python_ipware/modern/engine.py +256 -0
  14. python_ipware-4.1.0/python_ipware/modern/parsers.py +210 -0
  15. python_ipware-4.1.0/python_ipware/router.py +83 -0
  16. python_ipware-4.1.0/tests/__init__.py +0 -0
  17. python_ipware-4.1.0/tests/legacy/__init__.py +0 -0
  18. python_ipware-4.1.0/tests/legacy/run_against_legacy.py +41 -0
  19. python_ipware-4.1.0/tests/tests_modern_config.py +274 -0
  20. python_ipware-4.1.0/tests/tests_modern_matrix.py +391 -0
  21. python_ipware-4.1.0/tests/tests_router.py +367 -0
  22. python_ipware-2.0.5/PKG-INFO +0 -344
  23. python_ipware-2.0.5/pyproject.toml +0 -90
  24. python_ipware-2.0.5/python_ipware/__init__.py +0 -2
  25. python_ipware-2.0.5/python_ipware/__version__.py +0 -1
  26. python_ipware-2.0.5/python_ipware.egg-info/PKG-INFO +0 -344
  27. python_ipware-2.0.5/python_ipware.egg-info/SOURCES.txt +0 -14
  28. python_ipware-2.0.5/python_ipware.egg-info/dependency_links.txt +0 -1
  29. python_ipware-2.0.5/python_ipware.egg-info/requires.txt +0 -6
  30. python_ipware-2.0.5/python_ipware.egg-info/top_level.txt +0 -1
  31. python_ipware-2.0.5/setup.cfg +0 -4
  32. {python_ipware-2.0.5 → python_ipware-4.1.0}/LICENSE +0 -0
  33. /python_ipware-2.0.5/python_ipware/python_ipware.py → /python_ipware-4.1.0/python_ipware/legacy/engine.py +0 -0
  34. {python_ipware-2.0.5 → python_ipware-4.1.0}/python_ipware/py.typed +0 -0
  35. {python_ipware-2.0.5/tests → python_ipware-4.1.0/tests/legacy}/tests_ipv4.py +0 -0
  36. {python_ipware-2.0.5/tests → python_ipware-4.1.0/tests/legacy}/tests_ipv6.py +0 -0
@@ -0,0 +1,60 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+
5
+ # C extensions
6
+ *.so
7
+
8
+ # Distribution / packaging
9
+ .Python
10
+ env/
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ *.egg-info/
23
+ .installed.cfg
24
+ *.egg
25
+
26
+ # PyInstaller
27
+ # Usually these files are written by a python script from a template
28
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
29
+ *.manifest
30
+ *.spec
31
+
32
+ # Installer logs
33
+ pip-log.txt
34
+ pip-delete-this-directory.txt
35
+
36
+ # Unit test / coverage reports
37
+ htmlcov/
38
+ .tox/
39
+ .coverage
40
+ .coverage.*
41
+ .cache
42
+ nosetests.xml
43
+ coverage.xml
44
+ *,cover
45
+
46
+ # Translations
47
+ *.mo
48
+ *.pot
49
+
50
+ # Django stuff:
51
+ *.log
52
+
53
+ # Sphinx documentation
54
+ docs/_build/
55
+
56
+ # PyBuilder
57
+ target/
58
+
59
+ .DS_Store
60
+ .ruff_cache
@@ -0,0 +1,152 @@
1
+ ## 4.1.0
2
+
3
+ Best match (modern engine only; legacy is unchanged). Results can differ from 4.0.0 on well-formed input,
4
+ always toward a better address; use `algorithm="legacy"` for exact v3 results:
5
+ - Addresses are ranked public > private > link-local > loopback. Unspecified (`0.0.0.0`, `::`),
6
+ multicast, broadcast, and reserved addresses are never returned. Python reports multicast as
7
+ `is_global`, so v3 could return `224.0.0.1` as the client.
8
+ - Without `proxy_count` / `proxy_list`, the first public hop of a chain wins, not only the first hop:
9
+ `10.0.0.1, 177.139.233.139` now yields `177.139.233.139`. With `leftmost=False` the scan runs from the
10
+ right. With proxy settings, the client position is fixed exactly as before. Note that the public hop
11
+ may be an upstream proxy: if you must identify private (intranet / VPN) clients, set `proxy_count` or
12
+ `proxy_list`.
13
+ - NAT64 well-known-prefix addresses (`64:ff9b::a.b.c.d`, RFC 6052) are unwrapped to the embedded IPv4
14
+ client, like IPv4-mapped addresses. v3 returned the IPv6 form.
15
+ - `trusted_route` is `True` for any address resolved through a matching proxy config, including private
16
+ clients; v3 reported `False` for them.
17
+ - New exhaustive tests: every 1–3 hop chain over ten address kinds, in every leftmost / strict / proxy
18
+ configuration, checked against an independent reference model; every header assignment and dict order;
19
+ every spelling of a hop; never-worse-than-legacy across the whole matrix; seeded fuzzing.
20
+
21
+ Enhance (modern engine only; legacy is unchanged):
22
+ - Parse RFC 7239 `Forwarded` elements by their `for=` value, including quoted, bracketed IPv6 with a port.
23
+ Previously `Forwarded` never produced an IP, so when it is present it can now resolve at its existing
24
+ precedence slot, which is above the CDN headers. Like `X-Forwarded-For`, a client can send it; behind a
25
+ CDN, pass an explicit `precedence` naming that CDN's header. Obfuscated hops (`for=unknown`, `for=_hidden`) count as invalid tokens.
26
+ - New default headers, added only between the 4.0.0 entries and `REMOTE_ADDR`, so none outranks a header
27
+ that resolved requests before: Azure Front Door `X-Azure-ClientIP`, DigitalOcean `DO-Connecting-IP`,
28
+ Envoy/Istio `X-Envoy-External-Address`, plus the missing `HTTP_X_CLIENT_IP` and raw `X-AppEngine-User-IP`
29
+ forms of headers already on the list.
30
+ - Header names match case-insensitively (`-` and `_` equivalent), so lowercase keys such as AWS Lambda's
31
+ work. Exact keys still take priority. When several spellings fold to the same header, the dash spelling
32
+ wins, whatever the dict order, so a client-sent `x_forwarded_for` cannot shadow the proxy's
33
+ `x-forwarded-for`. Dash spellings that disagree are treated as absent.
34
+
35
+ Harden (modern engine only):
36
+ - Reject malformed tokens instead of truncating them: unclosed brackets (`[::1`), text after a bracket
37
+ (`[::1]junk`), and non-numeric, empty, or out-of-range ports (`1.2.3.4:abc`, `1.2.3.4:70000`). Note v3
38
+ accepted `1.2.3.4:abc` as `1.2.3.4`.
39
+ - Non-string header values (`None`, bytes) are skipped instead of raising `AttributeError`.
40
+ - `proxy_list` entries are stripped of whitespace. An empty entry now raises `ValueError`: it used to match
41
+ every address and mark any spoofed chain as trusted.
42
+ - Trusted-proxy matching is tighter. A complete IP entry is matched exactly: v3 prefix-matched it, so
43
+ `"1.2.3.4"` also trusted `1.2.3.45`, letting that host forge the client IP. Prefixes match on whole
44
+ octets or groups (`"10.1"` matches `10.1.x.x`, not `10.100.x.x`). IPv6 entries are case- and
45
+ zero-insensitive. An entry ending in `:` stays a prefix, so `"2001:db8::"` behaves as in 4.0.0.
46
+ IPv4-mapped and NAT64 CIDR entries (`::ffff:10.0.0.0/104`) match the unwrapped IPv4 hops.
47
+ - Configuration errors raise `ValueError` at construction instead of misbehaving silently: `proxy_list`
48
+ or `precedence` passed as a bare string (each character became an entry), a non-IP `proxy_list` entry
49
+ such as `"foo"`, or a `proxy_count` that is a bool, a float, or a string. `precedence` and `proxy_list`
50
+ are copied, so later changes to the caller's lists have no effect.
51
+ - A `meta` that is not a mapping raises `TypeError` with a clear message. Non-string keys are skipped.
52
+ - RFC 7239 `Forwarded` parsing is quote-aware: a `,` or `;` inside a quoted value no longer splits a hop,
53
+ so `ext="x,8.8.8.8"` cannot smuggle in a fake address or a fake proxy hop.
54
+ - More address classes: `0.0.0.0/8` is never returned. Deprecated site-local `fec0::/10` ranks as
55
+ private, since Python reports it as global. RFC 8215 local-use NAT64 `64:ff9b:1::/48` ranks as private,
56
+ since Python reports it as reserved.
57
+ - The unused `is_valid_ip` helper was removed from `python_ipware.modern.parsers`. It was never exported.
58
+ - Test suite: 100% line and branch coverage of the modern engine.
59
+
60
+ CI:
61
+ - Bump `actions/upload-artifact` to v7 and `actions/download-artifact` to v8, which run on Node 24.
62
+
63
+ ## 4.0.0
64
+
65
+ Community (thank you!):
66
+ - Trusted proxies in `proxy_list` can now be CIDR networks, IPv4 or IPv6 (e.g. `100.64.0.0/10`), matched by
67
+ real network membership; plain prefixes still work. Modern engine only. Requested by @griffi-gh (#26).
68
+ - Added Fly.io's `Fly-Client-IP` header to the modern default precedence. Suggested by @mdalp (#23).
69
+ - README now shows how to put a CDN header such as Cloudflare's first via `precedence`, when all traffic
70
+ comes through that CDN. Suggested by @iloveitaly (#25).
71
+ - CI covers Python 3.13 and newer. Suggested by @iloveitaly (#24).
72
+
73
+ Enhance:
74
+ - Introduce a pluggable algorithm router: `IpWare(algorithm=...)` with `"auto"` (default), `"modern"`, and `"legacy"`.
75
+ - `"auto"` resolves to `"modern"` — the enhanced engine and the forward-moving default.
76
+ - `"legacy"` is an explicit escape hatch that runs the frozen v3 algorithm byte-for-byte.
77
+ - New `modern` engine: hardened IPv6 / bracketed-port / IPv4-mapped parsing and an expanded default header
78
+ precedence list (adds `True-Client-IP`, `Fastly-Client-IP`, App Engine, Azure `X-Client-IP`).
79
+ - The frozen v3 algorithm is preserved unchanged under `python_ipware.legacy`; the original v3 test suite
80
+ passes against both the legacy and modern engines.
81
+
82
+ Modernize:
83
+ - Migrate packaging to PEP 621 with the Hatchling build backend; version is read from `__version__.py`.
84
+ - Drop end-of-life Python 3.7 / 3.8; requires Python 3.9+, tested on 3.9–3.14.
85
+ - Bump ruff config to the `lint.*` table layout.
86
+
87
+ Note:
88
+ - No source change is required for existing users: `from python_ipware import IpWare` continues to work and
89
+ now defaults to the modern engine. On well-formed headers modern returns the same result as v3 (the full
90
+ v3 suite and a legacy-vs-modern differential test pass). It differs only on malformed values:
91
+ - quoted addresses such as `"1.2.3.4"` are accepted (v3 ignored them);
92
+ - a value with more than one port-like suffix, such as `1.2.3.4:80:90`, is rejected (v3 took `1.2.3.4`).
93
+ Use `IpWare(algorithm="legacy")` if you depend on the exact v3 handling of those inputs.
94
+
95
+ ## 3.0.0
96
+
97
+ Fix:
98
+ - Release major version, as there is a possibility of api change causing minimal backward incompatibly
99
+
100
+ ## 2.0.5
101
+
102
+ Enhance:
103
+ - AI assisted clean up
104
+
105
+ ## 2.0.4
106
+
107
+ Enhance:
108
+ - Added `proxy_count=0` as an option (@FraKraBa)
109
+
110
+ ## 2.0.3
111
+
112
+ Enhance:
113
+ - Added `HTTP_CF_CONNECTING_IP` to list of known ip headers (Adam M.)
114
+
115
+ ## 2.0.2
116
+
117
+ Enhance:
118
+ - Added logger name
119
+
120
+ ## 2.0.1
121
+
122
+ Issue:
123
+ - Remove `HTTP_VIA` header support (unreliable IP information) (@yourcelf)
124
+
125
+ Enhance:
126
+ - Include support for python 3.12
127
+
128
+ ## 2.0.0
129
+
130
+ - Introduced breaking changes to avoid conflicts with the `django-ipware` package.
131
+ - Renamed the imported module from `ipware` to `python_ipware` in the `python-ipware` package.
132
+ - Old usage: `from ipware import IpWare`
133
+ - New usage: `from python_ipware import IpWare`
134
+
135
+ ## 1.0.5
136
+
137
+ - Enhance: Readme updates
138
+
139
+ ## 1.0.0
140
+
141
+ Features:
142
+
143
+ - Added `X-CLIENT-IP` header support
144
+ - Adds PEP 561 Compatibility (@stumpylog)
145
+ - Streamline pyproject.toml & add trusted publishing (@stumpylog)
146
+ - Publish version 1.0.0
147
+
148
+ ## 0.9.0
149
+
150
+ Features:
151
+
152
+ - Initial Release
@@ -0,0 +1,372 @@
1
+ Metadata-Version: 2.5
2
+ Name: python-ipware
3
+ Version: 4.1.0
4
+ Summary: A Python package to retrieve a client's real IP address.
5
+ Project-URL: Homepage, https://github.com/un33k/python-ipware
6
+ Project-URL: Documentation, https://github.com/un33k/python-ipware#readme
7
+ Project-URL: Repository, https://github.com/un33k/python-ipware
8
+ Project-URL: Issues, https://github.com/un33k/python-ipware/issues
9
+ Project-URL: Changelog, https://github.com/un33k/python-ipware/blob/main/CHANGELOG.md
10
+ Author-email: Val Neekman <info@neekware.com>
11
+ Maintainer-email: Val Neekman <info@neekware.com>
12
+ License-Expression: MIT
13
+ License-File: LICENSE
14
+ Keywords: client ip,ip,ipware,proxy,python,real ip,remote addr,x-forwarded-for
15
+ Classifier: Development Status :: 5 - Production/Stable
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Natural Language :: English
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Programming Language :: Python
21
+ Classifier: Programming Language :: Python :: 3
22
+ Classifier: Programming Language :: Python :: 3 :: Only
23
+ Classifier: Programming Language :: Python :: 3.9
24
+ Classifier: Programming Language :: Python :: 3.10
25
+ Classifier: Programming Language :: Python :: 3.11
26
+ Classifier: Programming Language :: Python :: 3.12
27
+ Classifier: Programming Language :: Python :: 3.13
28
+ Classifier: Programming Language :: Python :: 3.14
29
+ Classifier: Topic :: Internet :: WWW/HTTP
30
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
31
+ Classifier: Typing :: Typed
32
+ Requires-Python: >=3.9
33
+ Provides-Extra: dev
34
+ Requires-Dist: build; extra == 'dev'
35
+ Requires-Dist: coverage[toml]; extra == 'dev'
36
+ Requires-Dist: ruff==0.16.8; extra == 'dev'
37
+ Requires-Dist: twine; extra == 'dev'
38
+ Provides-Extra: test
39
+ Requires-Dist: coverage[toml]; extra == 'test'
40
+ Requires-Dist: ruff==0.16.8; extra == 'test'
41
+ Description-Content-Type: text/markdown
42
+
43
+ # Python IPware
44
+
45
+ Best-effort client IP detection for Python server applications — Django, Flask, or any WSGI/ASGI framework.
46
+
47
+ [![status-image]][status-link]
48
+ [![version-image]][version-link]
49
+ [![coverage-image]][coverage-link]
50
+ [![maintained-image]][maintained-link]
51
+
52
+ ## Quickstart
53
+
54
+ ```sh
55
+ python -m pip install --upgrade python-ipware
56
+ ```
57
+
58
+ ```python
59
+ from python_ipware import IpWare
60
+
61
+ ipw = IpWare()
62
+
63
+ # Django: request.META | Flask: request.environ
64
+ ip, trusted_route = ipw.get_client_ip(request.META)
65
+
66
+ if ip:
67
+ # ip is an ipaddress.IPv4Address or IPv6Address
68
+ ip.is_global # publicly routable
69
+ ip.is_private # private network
70
+ ip.is_loopback # 127.0.0.1 / ::1
71
+
72
+ if trusted_route:
73
+ # the request came through your configured proxies (proxy_count / proxy_list)
74
+ ...
75
+ ```
76
+
77
+ Python 3.9+ is supported (tested on 3.9 – 3.14), with no upper version cap. No runtime dependencies.
78
+ On a newer Python that isn't in the test matrix yet? It should just work — if it doesn't,
79
+ [open an issue](https://github.com/un33k/python-ipware/issues) and we'll fix it.
80
+
81
+ > **Legacy:** the frozen 3.x algorithm is still available with `IpWare(algorithm="legacy")`.
82
+ > See the [legacy guide](https://github.com/un33k/python-ipware/blob/main/python_ipware/legacy/README.md).
83
+
84
+ ## What it's used for
85
+
86
+ ```mermaid
87
+ flowchart LR
88
+ R["Incoming request"] --> I["IpWare().get_client_ip(...)"]
89
+ I --> RL["Rate limiting and throttling"]
90
+ I --> GEO["Geo-location and localization"]
91
+ I --> LOG["Audit and access logs"]
92
+ I --> FR["Abuse and fraud signals<br/>(check trusted_route)"]
93
+ I --> AUTH["Login anomaly checks<br/>(check trusted_route)"]
94
+ ```
95
+
96
+ ## Security notice
97
+
98
+ > **Found a security issue?** Please email **info@neekware.com** privately — do not open a public
99
+ > issue or pull request. See [SECURITY.md](https://github.com/un33k/python-ipware/blob/main/SECURITY.md).
100
+
101
+ There is no perfect defense against IP address spoofing. Headers such as `X-Forwarded-For` are set by
102
+ clients and proxies, and can be forged. If you use `python-ipware` for authentication, rate limiting, or
103
+ anti-fraud, configure `proxy_count` and/or `proxy_list` for your network topology and treat it as one
104
+ layer alongside your firewall — never as the only defense.
105
+
106
+ ```mermaid
107
+ sequenceDiagram
108
+ participant A as Attacker (real IP 8.8.8.8)
109
+ participant P as Your proxy chain
110
+ participant App as Your app
111
+ A->>P: X-Forwarded-For: 1.2.3.4 (forged)
112
+ P->>App: X-Forwarded-For: 1.2.3.4, 8.8.8.8, 104.16.0.1, 34.120.0.1
113
+ Note over App: IpWare() trusts the left-most entry and returns 1.2.3.4 (spoofed)
114
+ Note over App: IpWare(proxy_count=2) counts from the right and returns 8.8.8.8
115
+ Note over App: Adding strict=True rejects the tampered header entirely
116
+ ```
117
+
118
+ ## API
119
+
120
+ ```python
121
+ IpWare(
122
+ precedence=None, # tuple of header keys to check, in order
123
+ leftmost=True, # client is the left-most IP in the chain
124
+ proxy_count=None, # expected number of proxies in front of your server
125
+ proxy_list=None, # trusted proxies: CIDR networks or IP prefixes
126
+ )
127
+
128
+ ip, trusted_route = ipw.get_client_ip(meta, strict=False)
129
+ ```
130
+
131
+ | Parameter | Description |
132
+ | --- | --- |
133
+ | `precedence` | Header keys to search, top to bottom. Defaults to the list below. |
134
+ | `leftmost` | `True` (default) follows the de-facto `client, proxy1, proxy2` order. Use `False` only for networks that put the client right-most. |
135
+ | `proxy_count` | Number of proxies expected after the client. `0` is valid; `None` disables the check. |
136
+ | `proxy_list` | Trusted proxies nearest your server, one entry per hop. Each entry is a CIDR network (`"100.64.0.0/10"`, `"fd7a:115c:a1e0::/48"`), a complete IP matched exactly (`"198.84.193.157"`), or an IP prefix matched on whole octets (`"10.1."`). See [Trusted proxies](#trusted-proxies). |
137
+ | `strict` | `False`: at least `proxy_count` / `proxy_list` proxies. `True`: exactly that many — extra or invalid entries reject the header. |
138
+
139
+ | Output | Description |
140
+ | --- | --- |
141
+ | `ip` | `IPv4Address`, `IPv6Address`, or `None` |
142
+ | `trusted_route` | `True` when `proxy_count` or `proxy_list` was configured and matched, for any returned IP (v3 only set it for public IPs) |
143
+
144
+ ### Selection rules
145
+
146
+ Headers are checked in precedence order. Every address is ranked:
147
+
148
+ | Rank | Addresses |
149
+ | --- | --- |
150
+ | 1. public | globally routable |
151
+ | 2. private | RFC 1918, IPv6 ULA, CGNAT `100.64.0.0/10`, documentation ranges |
152
+ | 3. link-local | `169.254.0.0/16`, `fe80::/10` |
153
+ | 4. loopback | `127.0.0.0/8`, `::1` |
154
+ | never returned | `0.0.0.0`, `::`, multicast, broadcast, reserved |
155
+
156
+ The first **public** IP wins. If none is found, the best-ranked IP wins, and the earlier header
157
+ wins a tie.
158
+
159
+ Within one header, the client entry depends on your proxy settings:
160
+
161
+ - **`proxy_count` / `proxy_list` set:** the entry just before your trusted proxies, exactly as in v3.
162
+ - **Neither set:** the first public entry in the chain, not only the first entry. So
163
+ `10.0.0.1, 177.139.233.139` yields `177.139.233.139` (v3 returned `10.0.0.1`). With
164
+ `leftmost=False` the chain is scanned from the right. That public entry may be an upstream proxy
165
+ rather than the client. If you need to identify private clients (intranet, VPN), set
166
+ `proxy_count` or `proxy_list` so the client position is fixed.
167
+
168
+ Every default header can be sent by a client, and the modern engine now also reads `Forwarded`,
169
+ which sits above the CDN headers. Behind a CDN, pass an explicit `precedence` naming that CDN's
170
+ header (see below).
171
+
172
+ ```mermaid
173
+ flowchart TD
174
+ A["Request headers (...)"] --> B["Take the next header in precedence order"]
175
+ B --> C{"Header present?"}
176
+ C -->|no| B
177
+ C -->|yes| D["Split the chain: client, proxy1, proxy2"]
178
+ D --> E{"Matches proxy_count and proxy_list?"}
179
+ E -->|no| B
180
+ E -->|yes| F["Pick the client entry"]
181
+ F --> G{"Public IP?"}
182
+ G -->|yes| H["Return (ip, trusted_route)"]
183
+ G -->|no| I["Keep if it outranks the current fallback"]
184
+ I --> B
185
+ B -->|no headers left| J["Return the best fallback, else None"]
186
+ ```
187
+
188
+ The legacy engine keeps v3's rules. A combination suite checks that the modern engine never returns a
189
+ worse address than legacy for the same input.
190
+
191
+ Ports are stripped (`1.2.3.4:8080`, `[2001:db8::1]:443`). IPv4-mapped (`::ffff:1.2.3.4`) and NAT64
192
+ well-known-prefix (`64:ff9b::1.2.3.4`) addresses are returned as plain IPv4. RFC 7239 `Forwarded` elements are read by their
193
+ `for=` value (`for="[2001:db8::1]:4711";proto=https`). Malformed tokens such as `[::1`,
194
+ `[::1]junk`, or `1.2.3.4:abc` are rejected rather than truncated. Header names match
195
+ case-insensitively, so lowercase keys (AWS Lambda / API Gateway v2) work too.
196
+
197
+ ## Default header precedence
198
+
199
+ ```python
200
+ (
201
+ "X_FORWARDED_FOR", # load balancers / proxies (AWS ELB, etc.)
202
+ "HTTP_X_FORWARDED_FOR",
203
+ "HTTP_CLIENT_IP", # Amazon EC2, Heroku
204
+ "HTTP_X_REAL_IP",
205
+ "HTTP_X_FORWARDED", # Squid
206
+ "HTTP_X_CLUSTER_CLIENT_IP", # Rackspace LB, Riverbed Stingray
207
+ "HTTP_FORWARDED_FOR", # de facto variant
208
+ "HTTP_FORWARDED", # RFC 7239
209
+ "HTTP_CF_CONNECTING_IP", # Cloudflare
210
+ "HTTP_TRUE_CLIENT_IP", # Cloudflare Enterprise, Akamai
211
+ "HTTP_FASTLY_CLIENT_IP", # Fastly, Firebase
212
+ "HTTP_FLY_CLIENT_IP", # Fly.io
213
+ "HTTP_X_APPENGINE_USER_IP", # Google App Engine
214
+ "X-CLIENT-IP", # Microsoft Azure
215
+ "X-REAL-IP", # NGINX
216
+ "X-CLUSTER-CLIENT-IP", # Rackspace Cloud Load Balancers
217
+ "X_FORWARDED",
218
+ "FORWARDED_FOR",
219
+ "CF-CONNECTING-IP",
220
+ "TRUE-CLIENT-IP",
221
+ "FASTLY-CLIENT-IP",
222
+ "FLY-CLIENT-IP",
223
+ "FORWARDED",
224
+ "CLIENT-IP",
225
+ # added after 4.0.0 — always below every earlier entry, above REMOTE_ADDR
226
+ "HTTP_X_CLIENT_IP", # Microsoft Azure (Django/WSGI form)
227
+ "X-APPENGINE-USER-IP", # Google App Engine (raw form)
228
+ "HTTP_X_AZURE_CLIENTIP", # Azure Front Door
229
+ "X-AZURE-CLIENTIP",
230
+ "HTTP_DO_CONNECTING_IP", # DigitalOcean App Platform
231
+ "DO-CONNECTING-IP",
232
+ "HTTP_X_ENVOY_EXTERNAL_ADDRESS", # Envoy / Istio
233
+ "X-ENVOY-EXTERNAL-ADDRESS",
234
+ "REMOTE_ADDR", # direct connection
235
+ )
236
+ ```
237
+
238
+ Headers released earlier never move. New ones are added only just above `REMOTE_ADDR`, so an
239
+ upgrade can never let a new header outrank one that already resolved your requests.
240
+
241
+ Narrow it to what your infrastructure actually sets:
242
+
243
+ ```python
244
+ ipw = IpWare(precedence=("HTTP_X_FORWARDED_FOR", "REMOTE_ADDR"))
245
+ ```
246
+
247
+ If **all** your traffic comes through a CDN, put its header first. Only do this when the app is not
248
+ reachable directly, because clients can send these headers themselves:
249
+
250
+ ```python
251
+ # Behind Cloudflare only
252
+ ipw = IpWare(precedence=("HTTP_CF_CONNECTING_IP", "HTTP_X_FORWARDED_FOR", "REMOTE_ADDR"))
253
+ ```
254
+
255
+ ## Trusted proxies
256
+
257
+ If your server sits behind known proxies, pass their IPs or prefixes:
258
+
259
+ Each entry can be (modern engine):
260
+
261
+ - **a complete IP**, matched exactly: `"198.84.193.157"` never matches `198.84.193.15x`, and IPv6
262
+ spelling (case, leading zeros) does not matter;
263
+ - **a CIDR network** (IPv4 or IPv6), matched by membership. This is the recommended form for IPv6;
264
+ - **an IP prefix**, matched on whole octets or groups: `"10.1"` and `"10.1."` match `10.1.x.x` but not
265
+ `10.100.x.x`. IPv6 prefixes compare against the compressed form (`2001:db8::5`), so prefer CIDR.
266
+
267
+ IPv4-mapped (`::ffff:a.b.c.d`) and NAT64 (`64:ff9b::a.b.c.d`) hops are unwrapped to IPv4 before matching.
268
+ Misconfiguration raises `ValueError` at construction: a bare string instead of a list, empty or
269
+ non-IP entries, an invalid CIDR, or a negative or non-integer `proxy_count`. `proxy_count` and
270
+ `proxy_list` may both be set: the list fixes the client position, and the count is a hop-count
271
+ requirement (a minimum, or exact when `strict=True`).
272
+
273
+ ```python
274
+ ipw = IpWare(proxy_list=["198.84.193.157"]) # one proxy
275
+ ipw = IpWare(proxy_list=["198.84.193.157", "198.84.193.158"]) # two proxies
276
+ ipw = IpWare(proxy_list=["177.139.", "177.140"]) # prefixes for dynamic IPs
277
+ ipw = IpWare(proxy_list=["100.64.0.0/10"]) # CIDR network (IPv4 or IPv6)
278
+
279
+ # non-strict — X-Forwarded-For: <fake>, <client>, <proxy1>, <proxy2>
280
+ ip, trusted_route = ipw.get_client_ip(request.META)
281
+
282
+ # strict — X-Forwarded-For must be exactly: <client>, <proxy1>, <proxy2>
283
+ ip, trusted_route = ipw.get_client_ip(request.META, strict=True)
284
+ ```
285
+
286
+ ```mermaid
287
+ flowchart LR
288
+ RC["Real client<br/>8.8.8.8"] --> LB["Trusted proxy<br/>198.84.193.157"]
289
+ LB -->|"XFF: 8.8.8.8, 198.84.193.157"| APP["Your app<br/>proxy_list: 198.84.193.157"]
290
+ FC["Fake client<br/>5.6.7.8"] -->|"bypasses the proxy<br/>XFF: 1.2.3.4 (forged)"| APP
291
+ APP --> OK["Real request: (8.8.8.8, True)"]
292
+ APP --> NO["Fake request: (None, False)"]
293
+ ```
294
+
295
+ ## Proxy count
296
+
297
+ If you know how many proxies are in front of you but not their IPs (for example, across providers):
298
+
299
+ ```python
300
+ ipw = IpWare(proxy_count=2)
301
+
302
+ # non-strict — at least 2 proxies
303
+ ip, trusted_route = ipw.get_client_ip(request.META)
304
+
305
+ # strict — exactly 2 proxies: <client>, <proxy1>, <proxy2>
306
+ ip, trusted_route = ipw.get_client_ip(request.META, strict=True)
307
+ ```
308
+
309
+ ```mermaid
310
+ flowchart LR
311
+ C["Client<br/>8.8.8.8"] --> P1["Proxy 1<br/>104.16.0.1"] --> P2["Proxy 2<br/>34.120.0.1"] --> APP["Your app<br/>proxy_count=2"]
312
+ APP --> H1["XFF: 8.8.8.8, 104.16.0.1, 34.120.0.1<br/>returns (8.8.8.8, True)"]
313
+ APP --> H2["XFF: 1.2.3.4, 8.8.8.8, 104.16.0.1, 34.120.0.1<br/>forged prefix ignored: (8.8.8.8, True)<br/>strict=True: (None, False)"]
314
+ ```
315
+
316
+ Combine both for the tightest check:
317
+
318
+ ```python
319
+ ipw = IpWare(proxy_count=1, proxy_list=["198.84.193.157"])
320
+ ```
321
+
322
+ ## Right-most client networks
323
+
324
+ The [de-facto standard](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) puts the
325
+ originating client left-most. For the rare network that puts it right-most:
326
+
327
+ ```python
328
+ ipw = IpWare(leftmost=False)
329
+ ```
330
+
331
+ ```mermaid
332
+ flowchart LR
333
+ S["Standard: client, proxy1, proxy2"] -->|"leftmost=True (default)"| A["client = first entry"]
334
+ R["Reversed: proxy2, proxy1, client"] -->|"leftmost=False"| B["client = last entry"]
335
+ ```
336
+
337
+ See [docs/nginx.md](https://github.com/un33k/python-ipware/blob/main/docs/nginx.md) for an NGINX configuration example.
338
+
339
+ ## Development
340
+
341
+ ```sh
342
+ python -m pip install -e '.[dev]'
343
+ ruff check .
344
+ python -m unittest discover -s tests -p "tests_*.py" # full suite
345
+ python -m tests.legacy.run_against_legacy # v3 suite against the legacy engine
346
+ python -m build && python -m twine check dist/*
347
+ ```
348
+
349
+ ## License
350
+
351
+ Released under the [MIT](https://github.com/un33k/python-ipware/blob/main/LICENSE) license.
352
+
353
+ ## Maintenance
354
+
355
+ `python-ipware` is actively maintained with [Dojo](https://heydojo.ai) ⛩️. The legacy engine is frozen for
356
+ backward compatibility; all improvements target the modern engine. Need support? Reach
357
+ [Neekware Inc.](https://neekware.com) at info@neekware.com.
358
+
359
+ ## Sponsors
360
+
361
+ [Neekware Inc.](https://neekware.com) — creator of [Dojo Workspace](https://heydojo.ai), your AI workspace for building, learning, and getting things done.
362
+
363
+ 🚀 Created with [Dojo](https://heydojo.ai) ⛩️
364
+
365
+ [status-image]: https://github.com/un33k/python-ipware/actions/workflows/ci.yml/badge.svg
366
+ [status-link]: https://github.com/un33k/python-ipware/actions/workflows/ci.yml
367
+ [version-image]: https://img.shields.io/pypi/v/python-ipware.svg
368
+ [version-link]: https://pypi.org/project/python-ipware/
369
+ [coverage-image]: https://coveralls.io/repos/github/un33k/python-ipware/badge.svg?branch=main
370
+ [coverage-link]: https://coveralls.io/github/un33k/python-ipware?branch=main
371
+ [maintained-image]: https://img.shields.io/badge/maintained%20with-Dojo%20%E2%9B%A9%EF%B8%8F-1f2937
372
+ [maintained-link]: https://heydojo.ai