Socket.EndReceiveFrom will never be called
EndReceiveFrom() will never be called here, because AsyncCallback is null: https://github.com/sipsorcery-org/sipsorcery/blob/96ea5d7e81406a077c7571819f07f36ab39b7e89/src/sys/Net/NetServices.cs#L503
https://docs.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.endreceivefrom?view=net-6.0
Before calling BeginReceiveFrom, you need to create a callback method that implements the AsyncCallback delegate. This callback method executes in a separate thread and is called by the system after BeginReceiveFrom returns. The callback method must accept the IAsyncResult returned by the BeginReceiveFrom method as a parameter.
The BeginReceiveFrom call is only being used to test whether the underlying OS supports dual mode IPv4 and IPv6 sockets. Nothing will be sent to the socket as part of the test hence no EndReceiveFrom call.
As you may see in the framework source or for example in this issue discussion, starting with net6.0, all socket.Begin*() methods are just wrappers over async methods, and therefore every IAsyncResult returned but those methods is a Task.
So actually there's a need to always consume those tasks somehow, otherwise there's a chance that such task would fail with exception (e.g. when socket is closed) and then it would result in a task unobserved exception.
Cripes thats a bit nasty! I wasn't aware of that behaviour. I suspect it could impact other areas of the code as well.
It sounds like one, or both, of you maye have already come across this? Does this look like a reasonable fix:
testSocket.BeginReceiveFrom(buf, 0, buf.Length, SocketFlags.None, ref remoteEP, (ar) => { try { testSocket.EndReceiveFrom(ar, ref remoteEP); } catch { } }, null);
Cripes thats a bit nasty! I wasn't aware of that behaviour. I suspect it could impact other areas of the code as well.
Yes, it's reather unfortunate breaking change they did. And yes, other socket.Begin*() calls are to be checked. For example, check this EndReceiveFrom handler: https://github.com/sipsorcery-org/sipsorcery/blob/84f0673054abee907dd39d8d92d8017bdd717861/src/core/SIP/Channels/SIPUDPChannel.cs#L127
It calls m_udpSocket.EndReceiveFrom() only if the channel not closed, and otherwise IAsyncResult argument is not touched. Which again is a road to unobserved exception. As far as I get the situation.
We actually have been facing such exceptions in our code using SipSorcery after switching to net6.0 runtime, which is why we are here with the issue. The worst part of the problem is that exception that you get has very short and useless stack trace like this:
System.Net.Sockets.SocketException (995): The I/O operation has been aborted because of either a thread exit or an application request.
at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.ThrowException(SocketError error, CancellationToken cancellationToken)
at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.System.Threading.Tasks.Sources.IValueTaskSource<System.Net.Sockets.SocketReceiveFromResult>.GetResult(Int16 token)
at System.Threading.Tasks.ValueTask`1.ValueTaskSourceAsTask.<>c.<.cctor>b__4_0(Object state)
and there's no way to trace back to the original cause of the problem.
Does this look like a reasonable fix:
I think, yes. And no.
I've spent a weekend investigating net6.0 runtime sources, and as far as I understand, if you provide a callback to a Begin*() operation, then it defnetely gets called at some time, even of the operation itself fails. And in the callback you have to always try a corresponding End*() operation, which in turn would observe a possible exception in the task. (this is a "yes" part).
And "no" part comes from the way the End*() operations are implemented. See for example https://github.com/dotnet/runtime/blob/c24d9a9c91c5d04b7b4de71f1a9f33ac35e09663/src/libraries/System.Net.Sockets/src/System/Net/Sockets/Socket.cs#L2524
It calls ThrowIfDisposed() before getting result of the task via TaskToApm.End(). So as far as I understand, in case the socket gets disposed, and a pending operation fails with an ObjectDisposed exception, this exception will not get observed even if you didn't forget to call End*().
And this is the problem discussed in the last part of the issue starting from here
As far as I see, they added this issue to 7.0.0 milestone which is due to Novermber 11th, so hopefully this "no" part would get fixed sometime in future.
So for now we are to at least fix what's in our code. That is — to assure End*()is always called.