diff --git a/backend/atlas_portal/routes/account_wolf.py b/backend/atlas_portal/routes/account_wolf.py index d090557..603b78c 100644 --- a/backend/atlas_portal/routes/account_wolf.py +++ b/backend/atlas_portal/routes/account_wolf.py @@ -1,5 +1,6 @@ from __future__ import annotations +import ipaddress from typing import Any from flask import jsonify, request @@ -8,10 +9,45 @@ from .. import ariadne_client from ..keycloak import require_auth, require_account_access +def _valid_ip(value: str) -> str: + return str(ipaddress.ip_address(value.strip())) + + +def _public_ip_from_chain(values: list[str]) -> str: + """Return the nearest public client IP from a trusted proxy chain.""" + + candidates: list[str] = [] + for value in values: + for item in value.split(","): + item = item.strip() + if item: + candidates.append(item) + + for candidate in reversed(candidates): + try: + ip = ipaddress.ip_address(candidate) + except ValueError: + continue + if ip.is_global: + return str(ip) + + for candidate in reversed(candidates): + try: + return _valid_ip(candidate) + except ValueError: + continue + return "" + + def _client_ip() -> str: - # ProxyFix normalizes this from the trusted Traefik hop. Reading raw - # forwarding headers here would let a browser request choose an unlock IP. - return request.remote_addr or "" + # Walk right-to-left through proxy-added headers. This ignores spoofed + # left-most values while still finding the public IP before cluster hops. + chain = [ + request.headers.get("X-Forwarded-For", ""), + request.headers.get("X-Real-IP", ""), + request.remote_addr or "", + ] + return _public_ip_from_chain(chain) def _json_payload() -> dict[str, Any]: diff --git a/backend/tests/test_account_wolf.py b/backend/tests/test_account_wolf.py index b755f22..577eab4 100644 --- a/backend/tests/test_account_wolf.py +++ b/backend/tests/test_account_wolf.py @@ -38,7 +38,7 @@ def test_wolf_status_proxies_source_ip(monkeypatch) -> None: resp = client.get( "/api/account/wolf", environ_base={"REMOTE_ADDR": "1.2.3.4"}, - headers={"X-Forwarded-For": "9.9.9.9, 10.0.0.1"}, + headers={"X-Forwarded-For": "9.9.9.9, 181.1.87.186, 10.0.0.1"}, ) assert resp.status_code == 200 @@ -52,13 +52,27 @@ def test_wolf_unlock_uses_current_source_ip(monkeypatch) -> None: "/api/account/wolf/firewall/unlock", json={"ttl_seconds": 120}, environ_base={"REMOTE_ADDR": "5.6.7.8"}, - headers={"X-Real-IP": "9.9.9.9"}, + headers={"X-Real-IP": "10.42.28.0"}, ) assert resp.status_code == 200 assert ariadne.calls == [("POST", "/api/game-stream/firewall/unlock", {"ttl_seconds": 120, "ip": "5.6.7.8"}, None)] +def test_wolf_source_ip_prefers_nearest_public_proxy_value(monkeypatch) -> None: + client, ariadne = make_client(monkeypatch) + + resp = client.post( + "/api/account/wolf/firewall/unlock", + json={}, + environ_base={"REMOTE_ADDR": "10.42.28.0"}, + headers={"X-Forwarded-For": "9.9.9.9, 181.1.87.186, 10.42.28.0"}, + ) + + assert resp.status_code == 200 + assert ariadne.calls == [("POST", "/api/game-stream/firewall/unlock", {"ip": "181.1.87.186"}, None)] + + def test_wolf_pairing_and_admin_actions_proxy(monkeypatch) -> None: client, ariadne = make_client(monkeypatch) diff --git a/frontend/src/auth.js b/frontend/src/auth.js index b32d342..84a5978 100644 --- a/frontend/src/auth.js +++ b/frontend/src/auth.js @@ -93,7 +93,7 @@ export async function initAuth() { pkceMethod: "S256", silentCheckSsoRedirectUri: `${window.location.origin}/silent-check-sso.html`, checkLoginIframe: true, - scope: "openid profile email", + scope: "openid profile email groups", }); auth.authenticated = authenticated; diff --git a/frontend/src/components/WolfAccountCard.vue b/frontend/src/components/WolfAccountCard.vue index e40820f..41a2175 100644 --- a/frontend/src/components/WolfAccountCard.vue +++ b/frontend/src/components/WolfAccountCard.vue @@ -19,7 +19,7 @@
- Moonlight + Moonlight host {{ wolf.moonlightHost }}
@@ -44,8 +44,9 @@ - +
+
Use the host above in Moonlight after unlocking your current IP.
@@ -113,3 +114,5 @@ defineProps({ defineEmits(["refresh", "unlock", "pair", "game-mode", "admin-unlock"]); + +