xamarin-macios icon indicating copy to clipboard operation
xamarin-macios copied to clipboard

SetSocketOption with SocketOptionName.ReuseAddress does not work

Open divil5000 opened this issue 2 years ago • 4 comments

Steps to Reproduce

  1. Create a UdpClient to listen for packets on a specific port (we use 4000)
  2. Set SocketOptionName.ReuseAddress to true for the UdpClient.Client so that other apps can also listen on this port
  3. Call Bind on the UdpClient.Client
  4. In a second app (you can use split screen), perform the same actions

IMPORTANT NOTE

I think I know why this is happening. With BSD sockets you need to specify SO_REUSEPORT as well as SO_REUSEADDR in order to achieve the behaviour that (on Win32) you get with only SO_REUSEADDR. My problem is that there doesn't appear to be a way to specify SO_REUSEPORT in MonoTouch.

Is the Mono Socket class secretly wrapping a native CFSocket? If so, then a workaround would be to expose it so that I can set SO_REUSEPORT on it. If not, then your implementation is broken until SocketOptionName.ReuseAddress allows reusing the port as well as the address, like it does on Win32.

Would greatly appreciate input from your networking people.

Expected Behavior

The second app will successfully bind to the port, so that both apps are ready to receive UDP packets on the port. This is the case when using the code under Win32.

Actual Behavior

The second app will fail at the Bind call, raising an Exception saying the address is already in use.

Environment

Version information === Visual Studio Professional 2019 for Mac ===

Version 8.10.21 (build 4) Installation UUID: b055d637-6673-4c9b-a724-4aaa3d1873bf GTK+ 2.24.23 (Raleigh theme) Xamarin.Mac 6.18.0.23 (d16-6 / 088c73638)

Package version: 612000162

=== Mono Framework MDK ===

Runtime: Mono 6.12.0.162 (2020-02/2ca650f1f62) (64-bit) Package version: 612000162

=== Roslyn (Language Service) ===

3.10.0-4.21269.26+029847714208ebe49668667c60ea5b0a294e0fcb

=== NuGet ===

Version: 5.9.0.7134

=== .NET SDK (x64) ===

SDK: /usr/local/share/dotnet/x64/sdk/6.0.103/Sdks SDK Versions: 6.0.103 6.0.406 3.1.417 MSBuild SDKs: /Applications/Visual Studio.app/Contents/Resources/lib/monodevelop/bin/MSBuild/Current/bin/Sdks

=== .NET Core Runtime ===

Runtime: /usr/local/share/dotnet/x64/dotnet Runtime Versions: 7.0.3 5.0.15 3.1.23

=== .NET Core 3.1 SDK ===

SDK: 3.1.417

=== .NET 5.0 SDK ===

SDK: 5.0.406

=== Xamarin.Profiler ===

Version: 1.6.15.68 Location: /Applications/Xamarin Profiler.app/Contents/MacOS/Xamarin Profiler

=== Updater ===

Version: 11

=== Xamarin.Android ===

Not Installed

=== Eclipse Temurin JDK ===

Java SDK: Not Found

=== Android SDK Manager ===

Version: 16.10.0.13 Hash: 1b81df5 Branch: remotes/origin/d16-10 Build date: 2021-11-12 00:17:32 UTC

=== Android Device Manager ===

Version: 16.10.0.15 Hash: 89dcc0b Branch: remotes/origin/d16-10 Build date: 2021-11-12 00:17:52 UTC

=== Apple Developer Tools ===

Xcode 13.3 (20102) Build 13E113

=== Xamarin.Mac ===

Xamarin.Mac not installed. Can't find /Library/Frameworks/Xamarin.Mac.framework/Versions/Current/Version.

=== Xamarin.iOS ===

Version: 15.8.0.0 (Visual Studio Professional) Hash: f10d9e023 Branch: xcode13.3 Build date: 2022-03-15 11:47:35-0400

=== Xamarin Designer ===

Version: 16.11.0.60 Hash: 56f9b80b0 Branch: remotes/origin/d16-11 Build date: 2021-12-15 01:44:16 UTC

=== Build Information ===

Release ID: 810210004 Git revision: eb0b2f7259d35b7d767c79d91d356881227e0985 Build date: 2022-03-17 17:18:28-04 Build branch: release-8.10

=== Operating System ===

Mac OS X 12.3.1 Darwin 21.4.0 Darwin Kernel Version 21.4.0 Fri Mar 18 00:47:26 PDT 2022 root:xnu-8020.101.4~15/RELEASE_ARM64_T8101 x86_64

Build Logs

Example Project (If Possible)

// These four lines of code will reproduce the issue. The first bind will work fine, but in another app if you do the same thing, it will fail. var dc = new UdpClient(); dc.ExclusiveAddressUse = false; dc.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); dc.Client.Bind(new IPEndPoint(IPAddress.Any, 4000));

divil5000 avatar May 18 '22 10:05 divil5000

Hello,

There is certain information missing that will make things easier for use to investigate further and decide if this is an issue on the iOS/Mac bindings or an actual issue in mono. I do not want to make any assumptions so could you please specify the following:

  • Platform (my assumption is that we are taking about two macOS applications trying to talk between them).

My suspicion is that the issue in reality lives in the mono implementation of the UDP client: https://github.com/mono/mono/blob/main/mcs/class/referencesource/System/net/System/Net/Sockets/UDPClient.cs

With that assumption, can you please try the same code with dotnet6 (console application)? If that works we need to look deeper on how we can help you to have the UI version working. Are you using Xamarin.Forms or are you writing a native app (being native using the iOS/Mac SDKs directly rather than via Xaml).

mandel-macaque avatar May 19 '22 15:05 mandel-macaque

Hi @divil5000. We have added the "need-info" label to this issue, which indicates that we have an open question for you before we can take further action. This issue will be closed automatically in 7 days if we do not hear back from you by then - please feel free to re-open it if you come back to this issue after that time.

msftbot[bot] avatar May 19 '22 15:05 msftbot[bot]

Apologies, this is iOS I am running on. I have suspicions though that the behaviour may be the same, as iOS is a very similar operating system to macOS.

This is a native iOS app, we do not use Xamarin.Forms.

It's not clear how I could try a dotnet6 console app on iOS. I have verified that on Win32, though, everything works as expected.

It would surprise me if the issue was in UDPClient as it's the underlying Socket itself that will not bind. Is the Socket you're using a Mono implementation, and if so, does it have any equivalent of SO_REUSEPORT?

divil5000 avatar May 19 '22 15:05 divil5000

A month has passed without comment. Come on, we need a workaround here please.

divil5000 avatar Jun 27 '22 14:06 divil5000

It's not clear how I could try a dotnet6 console app on iOS.

Executing this from a terminal will create a new iOS app using .NET:

dotnet new ios

rolfbjarne avatar Jan 20 '23 10:01 rolfbjarne

Hi @divil5000. We have added the "need-info" label to this issue, which indicates that we have an open question for you before we can take further action. This issue will be closed automatically in 7 days if we do not hear back from you by then - please feel free to re-open it if you come back to this issue after that time.

msftbot[bot] avatar Jan 20 '23 10:01 msftbot[bot]

Thanks. I did spin up two blank apps, both trying to bind to the same UDP port, and there was no problem.

The problem occurs with our app in combination with a specific third-party app, which can no longer bind to the UDP port if our app has already done so. I am fairly confident that I know what is happening (read my investigation above). Are you able to comment on the SO_REUSEPORT suggestion?

divil5000 avatar Jan 20 '23 14:01 divil5000

The problem occurs with our app in combination with a specific third-party app, which can no longer bind to the UDP port if our app has already done so. I am fairly confident that I know what is happening (read my investigation above). Are you able to comment on the SO_REUSEPORT suggestion?

It seems SO_REUSEPORT is set under some circumstances in .NET:

https://github.com/dotnet/runtime/blob/5d1b7e77e054f74de05d6cd34de11c55ffbd125f/src/native/libs/System.Native/pal_networking.c#L1979

but I'm not certain if that's your case.

We'd likely need a test project that reproduces the problem in order to figure it out though.

rolfbjarne avatar Jan 20 '23 15:01 rolfbjarne

Thanks for the link, that's encouraging that somebody has obviously thought about this.

Unfortunately a test project would be extremely difficult in this case, as you'd need a license/trial to use the specific third-party product and also a piece of third-party hardware that sends the network traffic being received.

I'm not familiar with the source you linked to, but can you confirm that SO_REUSEPORT is #defined for iOS, and what call I actually need to make on Socket to end up triggering SystemNative_SetSockOpt in the code you linked to? At least that way I can confirm we are doing all we can. At the moment we are doing the below, which (optimistically) ought to be triggering that SO_REUSEPORT code.

socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);

divil5000 avatar Jan 20 '23 15:01 divil5000

Thanks for the link, that's encouraging that somebody has obviously thought about this.

Unfortunately a test project would be extremely difficult in this case, as you'd need a license/trial to use the specific third-party product and also a piece of third-party hardware that sends the network traffic being received.

I'm not familiar with the source you linked to, but can you confirm that SO_REUSEPORT is #defined for iOS, and what call I actually need to make on Socket to end up triggering SystemNative_SetSockOpt in the code you linked to?

I can look at the code all I want, but that doesn't mean I won't miss something and say something wrong :)

At least that way I can confirm we are doing all we can. At the moment we are doing the below, which (optimistically) ought to be triggering that SO_REUSEPORT code.

socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);

What you can do is to debug using a native debugger (lldb), set a breakpoint on the relevant native function (which I believe is setsockopt) and then inspect the parameters to ensure the SO_REUSEPORT flag is set. That way you can confirm 100% either way the behavior, and not have to guess anymore.

rolfbjarne avatar Jan 20 '23 15:01 rolfbjarne

Unfortunately I've never used a native debugger and certainly not on an iOS device attached to the system. We're well out of my comfort zone here.

divil5000 avatar Jan 20 '23 15:01 divil5000

Unfortunately I've never used a native debugger and certainly not on an iOS device attached to the system. We're well out of my comfort zone here.

Can you reproduce in the simulator, or does it only happen on device?

rolfbjarne avatar Jan 23 '23 12:01 rolfbjarne

As it depends upon a particular third-party app being installed and licensed, I cannot reproduce the issue in a simulator.

divil5000 avatar Jan 23 '23 12:01 divil5000

Unfortunately I've never used a native debugger and certainly not on an iOS device attached to the system. We're well out of my comfort zone here.

If you're up for a challenge, something like this should work:

  1. Create the file ~/.mtouch-launch-with-lldb. This can be done with this command:
$ touch ~/.mtouch-launch-with-lldb
  1. Open the solution in VSMac, and launch on device. The app won't launch, but the application output will give you instructions about what to do, which is to execute the following in a terminal:
$ lldb -s /tmp/mtouch-lldb-prep-cmds

this will launch lldb and connect to your device.

  1. Once the lldb as launched, you'll get the (lldb) prompt. Then you execute:
(lldb) run

which will launch the app.

  1. Do whatever you need to do in the app to get as close as possible to where the problem occurs. Then break in lldb:
(lldb) process interrupt
  1. Then add a breakpoint on the setsockopt and bind functions:
(lldb) b setsockopt
(lldb) b bind
  1. Continue the app:
(lldb) continue
  1. Reproduce the problem. Hopefully lldb will now stop at the setsockopt function, and you can inspect the parameters to the function:
(lldb) p $x0       # this will show the first argument
(lldb) p $x1        # and the second...
(lldb) p $x2
(lldb) p $x3
(lldb) p $x4

The setsockopt has 5 parameters, so that way you should be able to check them all:

int
setsockopt(int socket, int level, int option_name, const void *option_value, socklen_t option_len);

(this is from the man setsockopt command)

  1. Important! Don't forget to remove the file we created in the first step:
$ rm -f ~/.mtouch-launch-with-lldb

in order to launch normally again in VSMac once you're done.

rolfbjarne avatar Jan 23 '23 22:01 rolfbjarne

Thanks Rolf. It worked and I was able to break on setsockopt 5 times before reaching bind, with the simple C# code I pasted at the top. Debug output below. I can see that it's getting called with SO_REUSEADDR and SO_REUSEPORT but I can't see what values are being passed. How do I inspect what's in *option_value?

Break 1 at setsockopt:

$0 = 7 $1 = 65535 $2 = 4 $3 = 6096169756 $4 = 4

Break 2 at setsockopt:

$0 = 7 $1 = 65535 $2 = 4 $3 = 6096169764 $4 = 4

Break 3 at setsockopt:

$0 = 7 $1 = 65535 $2 = 512 $3 = 6096169764 $4 = 4

Break 4 at setsockopt:

$0 = 7 $1 = 65535 $2 = 4 $3 = 6096169780 $4 = 4

Break 5 at setsockopt:

$0 = 7 $1 = 65535 $2 = 512 $3 = 6096169780 $4 = 4

Break 6 at bind

divil5000 avatar Jan 24 '23 12:01 divil5000

How do I inspect what's in *option_value?

I believe this should work (x = read memory, /w: size 4 bytes)

(lldb) x/w $x3

rolfbjarne avatar Jan 24 '23 16:01 rolfbjarne

Thank you, that worked perfectly. Inspecting the results gives the following output, which indicates SO_REUSEADDR and SO_REUSEPORT are both being set to 1 (true) for the above C# code. Therefore I think the platform is doing everything correctly and I cannot explain why our app does not coexist properly with the third-party app in terms of both being able to listed for the same UDP packets.

Break 1 at setsockopt

19, 65535, 4, 1, 4

Break 2 at setsockopt

19, 65535, 4, 1, 4

Break 3 at setsockopt

19, 65535, 512, 1, 4

Break 4 at setsockopt

19, 65535, 4, 1, 4

Break 5 at setsockopt

19, 65535, 512, 1, 4

divil5000 avatar Jan 25 '23 11:01 divil5000

Therefore I think the platform is doing everything correctly

OK, I'll close this then. Feel free to reopen if you end up finding it's our fault in the end!

rolfbjarne avatar Jan 25 '23 11:01 rolfbjarne