.oO SearXNG Developer Documentation Oo.
Loading...
Searching...
No Matches
trusted_proxies.py
Go to the documentation of this file.
1# SPDX-License-Identifier: AGPL-3.0-or-later
2"""Implementation of a middleware to determine the real IP of an HTTP request
3(:py:obj:`flask.request.remote_addr`) behind a proxy chain."""
4# pylint: disable=too-many-branches
5
6from __future__ import annotations
7import typing as t
8
9from collections import abc
10from ipaddress import IPv4Address, IPv6Address, ip_address, ip_network, IPv4Network, IPv6Network
11from werkzeug.http import parse_list_header
12
13from . import config
14from ._helpers import log_error_only_once, logger
15
16if t.TYPE_CHECKING:
17 from _typeshed.wsgi import StartResponse
18 from _typeshed.wsgi import WSGIApplication
19 from _typeshed.wsgi import WSGIEnvironment
20
21
23 """A middleware like the ProxyFix_ class, where the ``x_for`` argument is
24 replaced by a method that determines the number of trusted proxies via the
25 ``botdetection.trusted_proxies`` setting.
26
27 .. sidebar:: :py:obj:`flask.Request.remote_addr`
28
29 SearXNG uses Werkzeug's ProxyFix_ (with it default ``x_for=1``).
30
31 The remote IP (:py:obj:`flask.Request.remote_addr`) of the request is taken
32 from (first match):
33
34 - X-Forwarded-For_: If the header is set, the first untrusted IP that comes
35 before the IPs that are still part of the ``botdetection.trusted_proxies``
36 is used.
37
38 - `X-Real-IP <https://github.com/searxng/searxng/issues/1237#issuecomment-1147564516>`__:
39 If X-Forwarded-For_ is not set, `X-Real-IP` is used
40 (``botdetection.trusted_proxies`` is ignored).
41
42 If none of the header is set, the REMOTE_ADDR_ from the WSGI layer is used.
43 If (for whatever reasons) none IP can be determined, an error message is
44 displayed and ``100::`` is used instead (:rfc:`6666`).
45
46 .. _ProxyFix:
47 https://werkzeug.palletsprojects.com/middleware/proxy_fix/
48
49 .. _X-Forwarded-For:
50 https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For
51
52 .. _REMOTE_ADDR:
53 https://wsgi.readthedocs.io/en/latest/proposals-2.0.html#making-some-keys-required
54
55 """
56
57 def __init__(self, wsgi_app: WSGIApplication) -> None:
58 self.wsgi_app = wsgi_app
59
60 def trusted_proxies(self) -> list[IPv4Network | IPv6Network]:
61 cfg = config.get_global_cfg()
62 proxy_list: list[str] = cfg.get("botdetection.trusted_proxies", default=[])
63 return [ip_network(net, strict=False) for net in proxy_list]
64
66 self,
67 x_forwarded_for: list[IPv4Address | IPv6Address],
68 trusted_proxies: list[IPv4Network | IPv6Network],
69 ) -> str:
70 # always rtl
71 for addr in reversed(x_forwarded_for):
72 trust: bool = False
73
74 for net in trusted_proxies:
75 if addr.version == net.version and addr in net:
76 logger.debug("trust proxy %s (member of %s)", addr, net)
77 trust = True
78 break
79
80 # client address
81 if not trust:
82 return addr.compressed
83
84 # fallback to first address
85 return x_forwarded_for[0].compressed
86
87 def __call__(self, environ: WSGIEnvironment, start_response: StartResponse) -> abc.Iterable[bytes]:
88 # pylint: disable=too-many-statements
89
90 trusted_proxies = self.trusted_proxies()
91
92 # We do not rely on the REMOTE_ADDR from the WSGI environment / the
93 # variable is first removed from the WSGI environment and explicitly set
94 # in this function!
95
96 orig_remote_addr: str | None = environ.pop("REMOTE_ADDR")
97
98 # Validate the IPs involved in this game and delete all invalid ones
99 # from the WSGI environment.
100
101 if orig_remote_addr:
102 try:
103 addr = ip_address(orig_remote_addr)
104 if addr.version == 6 and addr.ipv4_mapped:
105 addr = addr.ipv4_mapped
106 orig_remote_addr = addr.compressed
107 except ValueError as exc:
108 logger.error("REMOTE_ADDR: %s / discard REMOTE_ADDR from WSGI environment", exc)
109 orig_remote_addr = None
110
111 x_real_ip: str | None = environ.get("HTTP_X_REAL_IP")
112 if x_real_ip:
113 try:
114 addr = ip_address(x_real_ip)
115 if addr.version == 6 and addr.ipv4_mapped:
116 addr = addr.ipv4_mapped
117 x_real_ip = addr.compressed
118 except ValueError as exc:
119 logger.error("X-Real-IP: %s / discard HTTP_X_REAL_IP from WSGI environment", exc)
120 environ.pop("HTTP_X_REAL_IP")
121 x_real_ip = None
122
123 x_forwarded_for: list[IPv4Address | IPv6Address] = []
124 if environ.get("HTTP_X_FORWARDED_FOR"):
125 for x_for_ip in parse_list_header(str(environ.get("HTTP_X_FORWARDED_FOR"))):
126 try:
127 addr = ip_address(x_for_ip)
128 except ValueError as exc:
129 logger.error("X-Forwarded-For: %s / discard HTTP_X_FORWARDED_FOR from WSGI environment", exc)
130 environ.pop("HTTP_X_FORWARDED_FOR")
131 x_forwarded_for = []
132 break
133
134 if addr.version == 6 and addr.ipv4_mapped:
135 addr = addr.ipv4_mapped
136 x_forwarded_for.append(addr)
137
138 # log questionable WSGI environments
139
140 if not x_forwarded_for and not x_real_ip:
141 log_error_only_once("X-Forwarded-For nor X-Real-IP header is set!")
142
143 if x_forwarded_for and not trusted_proxies:
144 log_error_only_once("missing botdetection.trusted_proxies config")
145 # without trusted_proxies, this variable is useless for determining
146 # the real IP
147 x_forwarded_for = []
148
149 # securing the WSGI environment variables that are adjusted
150
151 environ.update({"botdetection.trusted_proxies.orig": {"REMOTE_ADDR": orig_remote_addr}})
152
153 # determine *the real IP*
154
155 if x_forwarded_for:
156 environ["REMOTE_ADDR"] = self.trusted_remote_addr(x_forwarded_for, trusted_proxies)
157
158 elif x_real_ip:
159 environ["REMOTE_ADDR"] = x_real_ip
160
161 elif orig_remote_addr:
162 environ["REMOTE_ADDR"] = orig_remote_addr
163
164 else:
165 logger.error("No remote IP could be determined, use black-hole address: 100::")
166 environ["REMOTE_ADDR"] = "100::"
167
168 try:
169 _ = ip_address(environ["REMOTE_ADDR"])
170 except ValueError as exc:
171 logger.error("REMOTE_ADDR: %s, use black-hole address: 100::", exc)
172 environ["REMOTE_ADDR"] = "100::"
173
174 logger.debug("final REMOTE_ADDR is: %s", environ["REMOTE_ADDR"])
175 return self.wsgi_app(environ, start_response)
str trusted_remote_addr(self, list[IPv4Address|IPv6Address] x_forwarded_for, list[IPv4Network|IPv6Network] trusted_proxies)
list[IPv4Network|IPv6Network] trusted_proxies(self)
abc.Iterable[bytes] __call__(self, WSGIEnvironment environ, StartResponse start_response)
None __init__(self, WSGIApplication wsgi_app)