From 0089833b74c1fc06e5bb0ad849715b4b3ad16ea4 Mon Sep 17 00:00:00 2001 From: neckfire Date: Tue, 14 Jul 2026 16:18:35 +0200 Subject: [PATCH] fix(signalr): return LAN SignalR URL to local clients MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GetSignalRConnection now serves the SignalR URL on 192.168.1.136 to clients whose remote IP is on the LAN, and keeps the public domain for external clients. The real-time WebSocket (libwebsockets, incl. under Proton/Wine) couldn't reach the public domain from inside the LAN (hairpin NAT) — it failed right after the intro as "error code 5". External friends are unaffected (still get the domain). Co-Authored-By: Claude Opus 4.8 --- .../Functions/GetSignalRConnection.cs | 46 ++++++++++++++++--- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/src/Prospect.Server.Api/Services/CloudScript/Functions/GetSignalRConnection.cs b/src/Prospect.Server.Api/Services/CloudScript/Functions/GetSignalRConnection.cs index 4808610..4e404bf 100644 --- a/src/Prospect.Server.Api/Services/CloudScript/Functions/GetSignalRConnection.cs +++ b/src/Prospect.Server.Api/Services/CloudScript/Functions/GetSignalRConnection.cs @@ -1,4 +1,6 @@ -using Microsoft.Extensions.Options; +using System.Net; +using System.Net.Sockets; +using Microsoft.Extensions.Options; using Prospect.Server.Api.Config; using Prospect.Server.Api.Services.CloudScript.Models; @@ -7,20 +9,52 @@ namespace Prospect.Server.Api.Services.CloudScript.Functions; [CloudScriptFunction("GetSignalRConnection")] public class GetSignalRConnection : ICloudScriptFunction { - private readonly PlayFabSettings _settings; + // LAN address of the homelab host. Clients on the local network get the SignalR URL on + // this IP (which their cert already validates via the IP/DNS SAN), while external clients + // keep the public domain from configuration. This matters because the real-time WebSocket + // (libwebsockets, incl. under Proton/Wine) can't reach the public domain from inside the + // LAN (hairpin NAT), which surfaced as "error code 5" right after the intro. + private const string LanHost = "192.168.1.136"; - public GetSignalRConnection(IOptions settings) + private readonly PlayFabSettings _settings; + private readonly IHttpContextAccessor _httpContextAccessor; + + public GetSignalRConnection(IOptions settings, IHttpContextAccessor httpContextAccessor) { _settings = settings.Value; + _httpContextAccessor = httpContextAccessor; } public Task ExecuteAsync(FYGetSignalRConnection request) { - // The game client connects to SignalR only over HTTPS + // The game client connects to SignalR only over HTTPS. + var url = _settings.SignalRURL; + + var remote = _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress; + if (remote != null && IsLan(remote)) + { + try + { + url = new UriBuilder(url) { Host = LanHost }.Uri.AbsoluteUri; + } + catch { /* keep the configured URL on any parsing issue */ } + } + return Task.FromResult(new FYGetSignalRConnectionResult { - Url = _settings.SignalRURL, + Url = url, AccessToken = _settings.SignalRAccessToken }); } -} \ No newline at end of file + + private static bool IsLan(IPAddress ip) + { + if (IPAddress.IsLoopback(ip)) return true; + if (ip.IsIPv4MappedToIPv6) ip = ip.MapToIPv4(); + if (ip.AddressFamily != AddressFamily.InterNetwork) return false; + var b = ip.GetAddressBytes(); + return b[0] == 10 + || (b[0] == 172 && b[1] >= 16 && b[1] <= 31) + || (b[0] == 192 && b[1] == 168); + } +}