Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 30 additions & 16 deletions node/rustchain_v2_integrated_v2.2.1_rip200.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,32 +129,46 @@ def _parse_trusted_proxies() -> Tuple[set, list]:
_TRUSTED_PROXY_IPS, _TRUSTED_PROXY_NETS = _parse_trusted_proxies()


def _is_trusted_proxy_ip(ip_text: str) -> bool:
"""Return True if an IP belongs to configured trusted proxies."""
if not ip_text:
return False
try:
ip_obj = ipaddress.ip_address(ip_text)
if ip_text in _TRUSTED_PROXY_IPS:
return True
for net in _TRUSTED_PROXY_NETS:
if ip_obj in net:
return True
return False
except Exception:
return ip_text in _TRUSTED_PROXY_IPS


def client_ip_from_request(req) -> str:
remote = (req.remote_addr or "").strip()
if not remote:
return ""

trusted = False
try:
ip = ipaddress.ip_address(remote)
if remote in _TRUSTED_PROXY_IPS:
trusted = True
else:
for net in _TRUSTED_PROXY_NETS:
if ip in net:
trusted = True
break
except Exception:
trusted = remote in _TRUSTED_PROXY_IPS

if not trusted:
if not _is_trusted_proxy_ip(remote):
return remote

xff = (req.headers.get("X-Forwarded-For", "") or "").strip()
if not xff:
return remote
first = xff.split(",")[0].strip()
return first or remote

# Walk right-to-left to resist client-controlled header injection.
# Proxies append their observed client to the right side.
hops = [h.strip() for h in xff.split(",") if h.strip()]
hops.append(remote)
for hop in reversed(hops):
try:
ipaddress.ip_address(hop)
except Exception:
continue
if not _is_trusted_proxy_ip(hop):
return hop
return remote

# Register Hall of Rust blueprint (tables initialized after DB_PATH is set)
try:
Expand Down
27 changes: 27 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from unittest.mock import patch, MagicMock
import sys
from pathlib import Path
from types import SimpleNamespace

# Modules are pre-loaded in conftest.py
integrated_node = sys.modules["integrated_node"]
Expand Down Expand Up @@ -67,3 +68,29 @@ def test_api_miners(client):
assert data[0]['miner'] == "addr1"
assert data[0]['hardware_type'] == "PowerPC G4 (Vintage)"
assert data[0]['antiquity_multiplier'] == 2.5


def test_client_ip_from_request_ignores_leftmost_xff_spoof(monkeypatch):
"""Trusted-proxy mode should ignore client-injected left-most XFF entries."""
monkeypatch.setattr(integrated_node, "_TRUSTED_PROXY_IPS", {"127.0.0.1"})
monkeypatch.setattr(integrated_node, "_TRUSTED_PROXY_NETS", [])

req = SimpleNamespace(
remote_addr="127.0.0.1",
headers={"X-Forwarded-For": "203.0.113.250, 198.51.100.77"},
)

assert integrated_node.client_ip_from_request(req) == "198.51.100.77"


def test_client_ip_from_request_untrusted_remote_uses_remote_addr(monkeypatch):
"""When not behind a trusted proxy, XFF must be ignored."""
monkeypatch.setattr(integrated_node, "_TRUSTED_PROXY_IPS", {"127.0.0.1"})
monkeypatch.setattr(integrated_node, "_TRUSTED_PROXY_NETS", [])

req = SimpleNamespace(
remote_addr="198.51.100.12",
headers={"X-Forwarded-For": "203.0.113.250"},
)

assert integrated_node.client_ip_from_request(req) == "198.51.100.12"
Loading