From b5e275c51ff38aa97d3a3171081c969b4585af26 Mon Sep 17 00:00:00 2001 From: JustMe Date: Sun, 11 Oct 2026 04:45:27 +0800 Subject: [PATCH] fix(quic): handle Linux dual-stack listeners [CI] Signed-off-by: JustMe --- .../Net/MsQuicConnection.cs | 36 ++++++++++++++++--- .../MoShiro.Measurement/Net/MsQuicListener.cs | 28 ++++++++++----- 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/backend/shared/MoShiro.Measurement/Net/MsQuicConnection.cs b/backend/shared/MoShiro.Measurement/Net/MsQuicConnection.cs index c71a838..fe50448 100644 --- a/backend/shared/MoShiro.Measurement/Net/MsQuicConnection.cs +++ b/backend/shared/MoShiro.Measurement/Net/MsQuicConnection.cs @@ -346,11 +346,22 @@ internal sealed class MsQuicConnection : IAsyncDisposable, IDatagramConnection connection._connected.TrySetResult(); break; case ConnectionEventShutdownByTransport: - case ConnectionEventShutdownByPeer: - connection._connected.TrySetException(new IOException("QUIC connection closed during handshake.")); + { + uint status = unchecked((uint)Marshal.ReadInt32(eventPointer, IntPtr.Size)); + ulong errorCode = unchecked((ulong)Marshal.ReadInt64(eventPointer, IntPtr.Size * 2)); + connection._log.Error("MsQuic QUIC transport shutdown", ("status", $"0x{status:X8}"), ("error_code", errorCode)); + connection._connected.TrySetException(new IOException($"QUIC connection closed during handshake (transport status 0x{status:X8}, error code {errorCode}).")); break; + } + case ConnectionEventShutdownByPeer: + { + ulong errorCode = unchecked((ulong)Marshal.ReadInt64(eventPointer, IntPtr.Size)); + connection._log.Warn("MsQuic QUIC peer shutdown", ("error_code", errorCode)); + connection._connected.TrySetException(new IOException($"QUIC connection closed during handshake by peer (error code {errorCode}).")); + break; + } case ConnectionEventShutdownComplete: - connection.OnShutdownComplete(handle); + connection.OnShutdownComplete(handle, eventPointer); break; case ConnectionEventPeerAddressChanged: { @@ -467,7 +478,14 @@ internal sealed class MsQuicConnection : IAsyncDisposable, IDatagramConnection } using X509Certificate2 peerCertificate = X509CertificateLoader.LoadCertificate(der); - bool valid = ExportSubjectPublicKeyInfo(peerCertificate).AsSpan().SequenceEqual(_expectedPeerSpki); + byte[] actualSpki = ExportSubjectPublicKeyInfo(peerCertificate); + bool valid = actualSpki.AsSpan().SequenceEqual(_expectedPeerSpki); + if (!valid) + { + _log.Error("MsQuic peer certificate SPKI mismatch", + ("expected_sha256", Convert.ToHexStringLower(SHA256.HashData(_expectedPeerSpki))), + ("actual_sha256", Convert.ToHexStringLower(SHA256.HashData(actualSpki)))); + } return valid ? 0 : OperatingSystem.IsWindows() ? 0x80004005u : 1u; } @@ -513,8 +531,16 @@ internal sealed class MsQuicConnection : IAsyncDisposable, IDatagramConnection state.Release(); } - private void OnShutdownComplete(nint handle) + private void OnShutdownComplete(nint handle, nint eventPointer) { + if (!_connected.Task.IsCompletedSuccessfully) + { + byte flags = Marshal.ReadByte(eventPointer, IntPtr.Size); + _log.Warn("MsQuic shutdown completed before handshake notification", + ("handshake_completed", (flags & 1) != 0), + ("peer_acknowledged", (flags & 2) != 0)); + } + if (Interlocked.Exchange(ref _handle, 0) == 0) { return; diff --git a/backend/shared/MoShiro.Measurement/Net/MsQuicListener.cs b/backend/shared/MoShiro.Measurement/Net/MsQuicListener.cs index c2d5467..dcf2dac 100644 --- a/backend/shared/MoShiro.Measurement/Net/MsQuicListener.cs +++ b/backend/shared/MoShiro.Measurement/Net/MsQuicListener.cs @@ -15,7 +15,7 @@ internal sealed class MsQuicListener : IAsyncDisposable private const uint ListenerLocalAddress = 0x04000000; private static readonly MsQuicApi.ListenerCallback s_callback = OnListenerEvent; - private readonly IPAddress _bindAddress; + private readonly IReadOnlyList _bindAddresses; private readonly IReadOnlyList _ports; private readonly MsQuicDatagramCallback _datagramHandler; private readonly INodeLog _log; @@ -36,7 +36,12 @@ internal sealed class MsQuicListener : IAsyncDisposable INodeLog log, Action? peerAddressChanged) { - _bindAddress = bindAddress; + // MsQuic opens dual-mode wildcard sockets, but filters incoming connections using + // the address family supplied to ListenerStart. An IPv6Any listener alone rejects + // IPv4-mapped connections with TLS alert 120 (no_application_protocol). + _bindAddresses = OperatingSystem.IsLinux() && bindAddress.Equals(IPAddress.IPv6Any) + ? [IPAddress.Any, IPAddress.IPv6Any] + : [bindAddress]; _ports = ports.Count == 0 ? [7443] : ports; _certificate = certificate; _datagramHandler = datagramHandler; @@ -59,17 +64,22 @@ internal sealed class MsQuicListener : IAsyncDisposable { _configuration = MsQuicApi.ConfigurationOpen(_registration, QuicAlpn.Value); MsQuicConnection.LoadServerCredentials(_configuration, _certificate); - var bound = new List(_ports.Count); + var bound = new List(_ports.Count * _bindAddresses.Count); foreach (int port in _ports) { - nint listener = MsQuicApi.ListenerOpen(_registration, s_callback, GCHandle.ToIntPtr(_self)); - _listeners.Add(listener); - byte[] address = Serialize(new IPEndPoint(_bindAddress, port)); - MsQuicApi.ListenerStart(listener, QuicAlpn.Value, address); - bound.Add(ReadPort(MsQuicApi.GetAddress(listener, ListenerLocalAddress))); + int effectivePort = port; + foreach (IPAddress bindAddress in _bindAddresses) + { + nint listener = MsQuicApi.ListenerOpen(_registration, s_callback, GCHandle.ToIntPtr(_self)); + _listeners.Add(listener); + byte[] address = Serialize(new IPEndPoint(bindAddress, effectivePort)); + MsQuicApi.ListenerStart(listener, QuicAlpn.Value, address); + effectivePort = ReadPort(MsQuicApi.GetAddress(listener, ListenerLocalAddress)); + bound.Add(effectivePort); + } } - BoundPorts = bound; + BoundPorts = bound.Distinct().ToArray(); } catch {