diff --git a/src/OpenClaw.Connection/ManagedLocalGatewayPortProvenanceService.cs b/src/OpenClaw.Connection/ManagedLocalGatewayPortProvenanceService.cs index 5222c04bb..25e7824f2 100644 --- a/src/OpenClaw.Connection/ManagedLocalGatewayPortProvenanceService.cs +++ b/src/OpenClaw.Connection/ManagedLocalGatewayPortProvenanceService.cs @@ -358,7 +358,10 @@ cached.ProcessStartTimeUtc is not DateTime expectedStart || return false; } - return _platform.IsExpectedWslGatewayListening(managedDistroName, uri.Port); + if (!_platform.IsExpectedWslGatewayListening(managedDistroName, uri.Port)) + return false; + + return ListenerSnapshotStillCurrent(current, uri); } private GatewayEndpointProvenance InspectCore(GatewayRecord record) diff --git a/tests/OpenClaw.Connection.Tests/ManagedLocalGatewayPortProvenanceServiceTests.cs b/tests/OpenClaw.Connection.Tests/ManagedLocalGatewayPortProvenanceServiceTests.cs index cccca6506..4dd85bfd2 100644 --- a/tests/OpenClaw.Connection.Tests/ManagedLocalGatewayPortProvenanceServiceTests.cs +++ b/tests/OpenClaw.Connection.Tests/ManagedLocalGatewayPortProvenanceServiceTests.cs @@ -381,6 +381,27 @@ public void InteractiveCredentialGate_GuestSocketOwnershipChanges_FailsClosed() Assert.False(service.IsStrongCredentialAllowed(gateway, credential)); } + [Fact] + public void InteractiveCredentialGate_CachedWslRelayReplacedDuringGuestProbe_FailsClosed() + { + var platform = new FakePlatform(); + var start = new DateTime(2026, 7, 24, 1, 0, 0, DateTimeKind.Utc); + platform.Listeners.Add(new WindowsTcpListenerInfo( + IPAddress.Loopback, 18789, 102, "wslrelay", + @"C:\Program Files\WSL\wslrelay.exe", start)); + var service = new ManagedLocalGatewayPortProvenanceService(platform, NullLogger.Instance); + var gateway = ManagedRecord(); + var credential = new GatewayCredential( + "shared-token", false, CredentialResolver.SourceSharedGatewayToken); + Assert.Equal(GatewayEndpointProvenanceKind.ExpectedManagedGateway, service.Inspect(gateway).Kind); + + Assert.True(service.IsStrongCredentialAllowed(gateway, credential)); + + platform.ReplaceListenerDuringGuestProof = true; + + Assert.False(service.IsStrongCredentialAllowed(gateway, credential)); + } + [Fact] public void InteractiveCredentialGate_RelaylessProofRevalidatesEmptySnapshotAndGuestOwner() { @@ -561,6 +582,7 @@ private sealed class FakePlatform : IManagedLocalGatewayPortPlatform public bool ReplaceProcessIdentityOnEnd { get; set; } public bool ReplaceOwnerOnSecondCapture { get; set; } public bool IntroduceUnknownListenerDuringGuestProof { get; set; } + public bool ReplaceListenerDuringGuestProof { get; set; } public bool Ipv4Complete { get; set; } = true; public bool Ipv6Complete { get; set; } = true; public int TrustedWslRelayChecks { get; private set; } @@ -648,6 +670,19 @@ public bool IsExpectedWslGatewayListening(string distroName, int port) "unknown", @"C:\unknown.exe")); } + + if (ReplaceListenerDuringGuestProof && Listeners.Count > 0) + { + ReplaceListenerDuringGuestProof = false; + Listeners[0] = new WindowsTcpListenerInfo( + Listeners[0].Address, + port, + 999, + "unknown", + @"C:\unknown.exe", + Listeners[0].ProcessStartTimeUtc?.AddSeconds(1)); + } + return ExpectedDistroListening; } public string? ReadScheduledTaskXml(string taskName) => TaskXml;