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();
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;
@@ -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<IPAddress> _bindAddresses;
private readonly IReadOnlyList<int> _ports;
private readonly MsQuicDatagramCallback _datagramHandler;
private readonly INodeLog _log;
@@ -36,7 +36,12 @@ internal sealed class MsQuicListener : IAsyncDisposable
INodeLog log,
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;
_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<int>(_ports.Count);
var bound = new List<int>(_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
{