fix(quic): handle Linux dual-stack listeners [CI]
ci/woodpecker/push/web-build Pipeline was successful
ci/woodpecker/push/verify Pipeline was successful
ci/woodpecker/push/dev-image Pipeline was successful

Signed-off-by: JustMe <mr.toing@outlook.com>
This commit is contained in:
justme committed 2026-10-11 04:45:27 +08:00
1 parent b9ec6d80b0
commit b5e275c51f
2 files changed
+50 -14

No files matched your search

@@ -346,11 +346,22 @@ internal sealed class MsQuicConnection : IAsyncDisposable, IDatagramConnection
connection._connected.TrySetResult(); connection._connected.TrySetResult();
break; break;
case ConnectionEventShutdownByTransport: 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; 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: case ConnectionEventShutdownComplete:
connection.OnShutdownComplete(handle); connection.OnShutdownComplete(handle, eventPointer);
break; break;
case ConnectionEventPeerAddressChanged: case ConnectionEventPeerAddressChanged:
{ {
@@ -467,7 +478,14 @@ internal sealed class MsQuicConnection : IAsyncDisposable, IDatagramConnection
} }
using X509Certificate2 peerCertificate = X509CertificateLoader.LoadCertificate(der); 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; return valid ? 0 : OperatingSystem.IsWindows() ? 0x80004005u : 1u;
} }
@@ -513,8 +531,16 @@ internal sealed class MsQuicConnection : IAsyncDisposable, IDatagramConnection
state.Release(); 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) if (Interlocked.Exchange(ref _handle, 0) == 0)
{ {
return; return;
@@ -15,7 +15,7 @@ internal sealed class MsQuicListener : IAsyncDisposable
private const uint ListenerLocalAddress = 0x04000000; private const uint ListenerLocalAddress = 0x04000000;
private static readonly MsQuicApi.ListenerCallback s_callback = OnListenerEvent; private static readonly MsQuicApi.ListenerCallback s_callback = OnListenerEvent;
private readonly IPAddress _bindAddress; private readonly IReadOnlyList<IPAddress> _bindAddresses;
private readonly IReadOnlyList<int> _ports; private readonly IReadOnlyList<int> _ports;
private readonly MsQuicDatagramCallback _datagramHandler; private readonly MsQuicDatagramCallback _datagramHandler;
private readonly INodeLog _log; private readonly INodeLog _log;
@@ -36,7 +36,12 @@ internal sealed class MsQuicListener : IAsyncDisposable
INodeLog log, INodeLog log,
Action<IPEndPoint>? peerAddressChanged) Action<IPEndPoint>? 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; _ports = ports.Count == 0 ? [7443] : ports;
_certificate = certificate; _certificate = certificate;
_datagramHandler = datagramHandler; _datagramHandler = datagramHandler;
@@ -59,17 +64,22 @@ internal sealed class MsQuicListener : IAsyncDisposable
{ {
_configuration = MsQuicApi.ConfigurationOpen(_registration, QuicAlpn.Value); _configuration = MsQuicApi.ConfigurationOpen(_registration, QuicAlpn.Value);
MsQuicConnection.LoadServerCredentials(_configuration, _certificate); MsQuicConnection.LoadServerCredentials(_configuration, _certificate);
var bound = new List<int>(_ports.Count); var bound = new List<int>(_ports.Count * _bindAddresses.Count);
foreach (int port in _ports) foreach (int port in _ports)
{ {
nint listener = MsQuicApi.ListenerOpen(_registration, s_callback, GCHandle.ToIntPtr(_self)); int effectivePort = port;
_listeners.Add(listener); foreach (IPAddress bindAddress in _bindAddresses)
byte[] address = Serialize(new IPEndPoint(_bindAddress, port)); {
MsQuicApi.ListenerStart(listener, QuicAlpn.Value, address); nint listener = MsQuicApi.ListenerOpen(_registration, s_callback, GCHandle.ToIntPtr(_self));
bound.Add(ReadPort(MsQuicApi.GetAddress(listener, ListenerLocalAddress))); _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 catch
{ {