-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Closed
Description
Regression from .net 7.
As part of managed implementation of QUIC, I have QuicSocketContext, which uses Socket.ReceiveFromAsync (for servers) and SocketReceiveAsync (for clients) to receive UDP datagrams. After rebasing the project onto latest main, server started receiving wrong port and thus ends up sending datagrams to itself, instead of to the clients.
Minimized repro code:
using System.Net.Http;
using System.Security.Authentication;
using System.Buffers;
using System.Diagnostics;
using System.Net.Sockets;
using System.Net;
using System.Threading;
var serverListeningAddress = new IPEndPoint(IPAddress.Loopback, 5000);
System.Console.WriteLine($"Starting server on {serverListeningAddress}");
var server = new QuicSocketContext(serverListeningAddress, null, true);
server.Start();
System.Console.WriteLine($"Server started on {server.LocalEndPoint}");
var client = new QuicSocketContext(null, server.LocalEndPoint, false);
client.Start();
System.Console.WriteLine($"===== Sending first datagram =====");
client.SendDatagram(new DatagramInfo(new byte[] { 1, 2, 3 }, 3, server.LocalEndPoint));
Thread.Sleep(-1);
internal readonly record struct DatagramInfo(byte[] Buffer, int Length, EndPoint RemoteEndpoint);
internal class QuicSocketContext
{
private readonly EndPoint? _localEndPoint;
private readonly EndPoint? _remoteEndPoint;
private readonly bool _isServer;
private bool _started;
private readonly Socket _socket = new Socket(SocketType.Dgram, ProtocolType.Udp);
private readonly SocketAsyncEventArgs _socketReceiveEventArgs = new SocketAsyncEventArgs();
public QuicSocketContext(EndPoint? localEndPoint, EndPoint? remoteEndPoint, bool isServer)
{
_localEndPoint = localEndPoint;
_remoteEndPoint = remoteEndPoint;
_isServer = isServer;
SetupSocket(localEndPoint, remoteEndPoint);
_socketReceiveEventArgs.Completed += (sender, args) => OnReceiveFinished(args);
}
private void SetupSocket(EndPoint? localEndPoint, EndPoint? remoteEndPoint)
{
if (localEndPoint != null)
{
_socket.Bind(localEndPoint);
}
if (remoteEndPoint != null)
{
_socket.Connect(remoteEndPoint);
}
}
public IPEndPoint LocalEndPoint => (IPEndPoint)_socket.LocalEndPoint!;
public void Start()
{
if (_started)
{
return;
}
_started = true;
var args = _socketReceiveEventArgs;
while (!ReceiveFromAsync(args))
{
// this should not really happen, as the socket should be just opened, but we want to be sure and don't
// miss any incoming datagrams
OnDatagramReceived(ExtractDatagram(args));
}
}
private DatagramInfo ExtractDatagram(SocketAsyncEventArgs args)
{
return new DatagramInfo(args.Buffer!, args.SocketError == SocketError.Success ? args.BytesTransferred : 0,
args.RemoteEndPoint ?? _remoteEndPoint!);
}
private void OnReceiveFinished(SocketAsyncEventArgs args)
{
System.Console.WriteLine($"{(_isServer ? "Server" : "Client")}: OnReceiveFinished, RemoteEndPoint = {args.RemoteEndPoint}");
bool pending;
do
{
DatagramInfo datagram = ExtractDatagram(args);
// immediately issue another async receive
System.Console.WriteLine($"{(_isServer ? "Server" : "Client")}: Starting next receive");
pending = !ReceiveFromAsync(args);
OnDatagramReceived(datagram);
} while (pending);
}
protected void OnDatagramReceived(in DatagramInfo datagram)
{
System.Console.WriteLine($"{(_isServer ? "Server" : "Client")}: processing datagram from {datagram.RemoteEndpoint}");
Thread.Sleep(1000);
SendDatagram(datagram);
ArrayPool.Return(datagram.Buffer);
}
internal ArrayPool<byte> ArrayPool { get; } = ArrayPool<byte>.Shared;
private bool ReceiveFromAsync(SocketAsyncEventArgs args)
{
// use fresh buffer for each receive, since the previous one is still being processed
var buffer = ArrayPool.Rent(1200);
args.SetBuffer(buffer, 0, buffer.Length);
if (_remoteEndPoint != null)
{
System.Console.WriteLine($"Client: Socket.ReceiveAsync");
return _socket.ReceiveAsync(args);
}
args.RemoteEndPoint = _localEndPoint!;
System.Console.WriteLine($"Server: Socket.ReceiveFromAsync with RemotEndPoint = {_localEndPoint}");
return _socket.ReceiveFromAsync(args);
}
internal void SendDatagram(in DatagramInfo datagram)
{
if (_remoteEndPoint != null)
{
// connected socket - > Socket.Send
System.Console.WriteLine($"Client: Send over connected socket to {_remoteEndPoint}");
_socket.Send(datagram.Buffer.AsSpan(0, datagram.Length), SocketFlags.None);
}
else
{
System.Console.WriteLine($"Server: SendTo {datagram.RemoteEndpoint}");
_socket.SendTo(datagram.Buffer, 0, datagram.Length, SocketFlags.None, datagram.RemoteEndpoint);
}
}
}.NET 7 output:
Starting server on 127.0.0.1:5000
Server: Socket.ReceiveFromAsync with RemotEndPoint = 127.0.0.1:5000
Server started on [::ffff:127.0.0.1]:5000
Client: Socket.ReceiveAsync
===== Sending first datagram =====
Client: Send over connected socket to [::ffff:127.0.0.1]:5000
Server: OnReceiveFinished, RemoteEndPoint = [::ffff:127.0.0.1]:57206
Server: Starting next receive
Server: Socket.ReceiveFromAsync with RemotEndPoint = 127.0.0.1:5000
Server: processing datagram from [::ffff:127.0.0.1]:57206
Server: SendTo [::ffff:127.0.0.1]:57206
Client: OnReceiveFinished, RemoteEndPoint =
Client: Starting next receive
Client: Socket.ReceiveAsync
Client: processing datagram from [::ffff:127.0.0.1]:5000
Client: Send over connected socket to [::ffff:127.0.0.1]:5000
Server: OnReceiveFinished, RemoteEndPoint = [::ffff:127.0.0.1]:57206
Server: Starting next receive
Server: Socket.ReceiveFromAsync with RemotEndPoint = 127.0.0.1:5000
Server: processing datagram from [::ffff:127.0.0.1]:57206
Server: SendTo [::ffff:127.0.0.1]:57206
Client: OnReceiveFinished, RemoteEndPoint =
Client: Starting next receive
Client: Socket.ReceiveAsync
Client: processing datagram from [::ffff:127.0.0.1]:5000
Client: Send over connected socket to [::ffff:127.0.0.1]:5000
Server: OnReceiveFinished, RemoteEndPoint = [::ffff:127.0.0.1]:57206
Server: Starting next receive
Server: Socket.ReceiveFromAsync with RemotEndPoint = 127.0.0.1:5000
Server: processing datagram from [::ffff:127.0.0.1]:57206
....
.NET 8 output, manual emphasis on first wrong line
Starting server on 127.0.0.1:5000
Server: Socket.ReceiveFromAsync with RemotEndPoint = 127.0.0.1:5000
Server started on [::ffff:127.0.0.1]:5000
Client: Socket.ReceiveAsync
===== Sending first datagram =====
Client: Send over connected socket to [::ffff:127.0.0.1]:5000
Server: OnReceiveFinished, RemoteEndPoint = [::ffff:127.0.0.1]:65505
Server: Starting next receive
Server: Socket.ReceiveFromAsync with RemotEndPoint = 127.0.0.1:5000
Server: processing datagram from [::ffff:127.0.0.1]:65505
Server: SendTo [::ffff:127.0.0.1]:65505
Client: OnReceiveFinished, RemoteEndPoint =
Client: Starting next receive
Client: Socket.ReceiveAsync
Client: processing datagram from [::ffff:127.0.0.1]:5000
Client: Send over connected socket to [::ffff:127.0.0.1]:5000
Server: OnReceiveFinished, RemoteEndPoint = 127.0.0.1:5000 // <---- here, server starts talking to itself because provided RemoteEndPoint is incorrect.
Server: Starting next receive
Server: Socket.ReceiveFromAsync with RemotEndPoint = 127.0.0.1:5000
Server: processing datagram from 127.0.0.1:5000
Server: SendTo 127.0.0.1:5000
Server: OnReceiveFinished, RemoteEndPoint = 127.0.0.1:5000
Server: Starting next receive
Server: Socket.ReceiveFromAsync with RemotEndPoint = 127.0.0.1:5000
Server: processing datagram from 127.0.0.1:5000
Server: SendTo 127.0.0.1:5000
Server: OnReceiveFinished, RemoteEndPoint = 127.0.0.1:5000
Server: Starting next receive
Server: Socket.ReceiveFromAsync with RemotEndPoint = 127.0.0.1:5000
Server: processing datagram from 127.0.0.1:5000
...