dart-lang / sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
https://dart.dev
BSD 3-Clause "New" or "Revised" License
10.25k stars 1.58k forks source link

Binding to multicast socket fails in iOS >= 11.0 #42250

Open bugeats opened 4 years ago

bugeats commented 4 years ago

Starting with iOS 11.0 the dart:io network interface library fails with an OS error when trying to join a multicast group:

[VERBOSE-2:ui_dart_state.cc(157)] Unhandled Exception: OS Error: Can't assign requested address, errno = 49 #0 _NativeSocket.nativeJoinMulticast (dart:io-patch/socket_patch.dart:1379:56) #1 _NativeSocket.joinMulticast (dart:io-patch/socket_patch.dart:1331:5) #2 _RawDatagramSocket.joinMulticast (dart:io-patch/socket_patch.dart:2127:13) #3 demonstrateFailure (package:sockfix/main.dart:18:10)

import 'dart:io';
import 'package:flutter/widgets.dart';

Future<void> demonstrateFailure() async {
  final interfaces = await NetworkInterface.list(
    includeLinkLocal: true,
    type: InternetAddressType.IPv4,
    includeLoopback: true,
  );

  final interface = interfaces[1];
  final address = InternetAddress('224.0.0.251');

  final socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 12347);

  // FAILS HERE
  // OS Error: Can't assign requested address, errno = 49;
  socket.joinMulticast(address, interface);
}

void main() async {
  await demonstrateFailure();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

I have confirmed that the above works as expected with Flutter in Linux, and on an iOS simulator running 10.3.

This bug affects the Flutter multicast_dns package as described in https://github.com/flutter/flutter/issues/42102 , but it appears to be fundamentally a Dart SDK issue. As of this moment, the current version of iOS is 13.5.1 so this regression has snuck by several major updates.

bugeats commented 4 years ago

Error seems to be coming from this bit of native code Socket_JoinMulticast: https://github.com/dart-lang/sdk/blob/2.8.4/runtime/bin/socket.cc#L1174

EDIT: actual native code described here: https://github.com/dart-lang/sdk/issues/42250#issuecomment-642117024

jmagman commented 4 years ago

@zanderso do you happen to know the best person to poke about this?

zanderso commented 4 years ago

/cc @zichangg @sortie @a-siva

dnfield commented 4 years ago

@zichangg

zichangg commented 4 years ago

Thanks for reporting. Let me assign myself first!

zichangg commented 4 years ago

@bugeats From your description, I think this is a problem of iOS instead of Dart. If it worked on iOS 10.3, it means our setup is good and something internal has changed since 10.3. But I was not able to find the relative docs or articles. Apple doesn't have a clear documentation on low-level POSIX APIs. I have no idea on how to mitigate the problem, unless we decide to change underlying implementation using higher-level APIs(They are well-documented!). Any suggestion? @dnfield @zanderso

bugeats commented 4 years ago

@zichangg I opened a Feedback Assistant issue with Apple yesterday and pointed them to this ticket. I also reviewed iOS change logs and documentation and didn't find any mention of changes to POSIX sockets or anything like that.

My hope is that someone familiar with the Dart native internals like Socket_JoinMulticast (linked above) can further isolate the issue down to the line so we actually know what we're working with if we do decide to take this to Apple.

dnfield commented 4 years ago

@bugeats - it boils down to https://github.com/dart-lang/sdk/blob/97e291ba9fbf681f924e4fd71e7118ea14fecb8b/runtime/bin/socket_base_macos.cc#L447-L448 - a simple call to setsockopt.

It is possible that if you only tested this on the simulator, yo uwere actually using more like the host API than iOS API. Did this work for you on an iOS 10.3 device?

bugeats commented 4 years ago

@dnfield I only tested simulators, bisecting iOS versions until I arrived at 10.3. This was after I discovered the failure on my 13.5 physical device. I'm going to go grab an old iPhone and install 10.3, but I suspect the simulator is a red herring.

EDIT: I'm afraid Apple does not allow downgrading devices. I can't repro on a 10.3 physical device without going down a jailbreak rabbit hole, and ain't nobody got time for that.

zzdota commented 4 years ago

@dnfield I only tested simulators, bisecting iOS versions until I arrived at 10.3. This was after I discovered the failure on my 13.5 physical device. I'm going to go grab an old iPhone and install 10.3, but I suspect the simulator is a red herring.

EDIT: I'm afraid Apple does not allow downgrading devices. I can't repro on a 10.3 physical device without going down a jailbreak rabbit hole, and ain't nobody got time for that.

I meet the same problem, my iPhone version is 11.2.6; but the test on the Android phone is normal

clarkap commented 4 years ago

Any Time Frame on a fix for this, It's a blocking issue for me atm.

hacker1024 commented 4 years ago

Same here - this issue also extends to macOS, meaning any app that requires UDP multicasting/broadcasting can't be ported to any Apple devices.

zichangg commented 4 years ago

@hacker1024 Which version of macOS you are running? Can you also provide a piece of reproduction code if possible? I'm using my macOS 10.15.4 to run the sample. It doesn't pop the error.

dnfield commented 4 years ago

@zichangg - the error is happenning on iOS.

I haven't written up a minimal example of this, but it's sounding like Apple has broken this API. I'm not sure what Dart can do about that.

zichangg commented 4 years ago

Same here - this issue also extends to macOS,

@hacker1024 has seen a failure on macOS. That's why I'm asking.

dnfield commented 4 years ago

Oh sorry I missed that.

bugeats commented 4 years ago

Hey all I finally got a response from the Apple ticket I submitted a month ago. Here it is verbatim:

Engineering has provided the following information regarding this issue: Usually the error "Can't assign requested address", errno = 49 (aka EADDRNOTAVAIL) is returned when the interface is not up. You need to make sure the interface he want to use is up.

hacker1024 commented 4 years ago

@bugeats Our network interfaces are definitely up, I made sure of that as that seems to cause this issue in unrelated apps.

@zichangg I'm on Big Sur Beta 2, so perhaps it's only broken in that version. For sample code, I'm calling the following method in the udp package:

UDP.bind(Endpoint.broadcast(port: 2925));

I tried a range of ports, including 25565 (which I know the Minecraft server binds to for UDP broadcasts), so I don't think the port has anything to do with it - some people report that the port can cause the error in other non-Flutter circumstances.

aam commented 4 years ago

Just tried reproducing on xcode 11.3 simulator, but it seems to work fine.

import 'dart:io';
import 'dart:async';
import 'package:flutter/widgets.dart';

Future<void> demonstrateFailure() async {
  final interfaces = await NetworkInterface.list(
    includeLinkLocal: true,
    type: InternetAddressType.IPv4,
    includeLoopback: true,
  );

  final interface = interfaces[1];
  final address = InternetAddress('224.0.0.251');
  final port = 12347;

  RawDatagramSocket.bind(InternetAddress.anyIPv4, 0).then((RawDatagramSocket socket){
    print('Sending from ${socket.address.address}:${socket.port}');
    Timer.periodic(Duration(seconds: 1), (_) {
      print('Sent ${DateTime.now()}');
      socket.send('${DateTime.now()}'.codeUnits, address, port);
    });
  });

  final socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, port);
  print("interface: $interface, socket: $socket, port:${socket.port}");

  // FAILS HERE
  // OS Error: Can't assign requested address, errno = 49;
  socket.joinMulticast(address, interface);

  print("joined multicast");
  socket.listen((_) {
    final d = socket.receive();
    if (d == null) return;
    print('Datagram from ${d.address.address}:${d.port}: ${String.fromCharCodes(d.data)}');
  });
}

void main() async {
  await demonstrateFailure();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

and it seems to work fine(after I accept pop-up permissions dialog):

Xcode build done.                                           16.6s
flutter: Sending from 0.0.0.0:49617
flutter: interface: NetworkInterface('en7', [InternetAddress('192.168.86.23', IPv4)]), socket: Instance of '_RawDatagramSocket', port:12347
flutter: joined multicast
Waiting for iPhone 11 Pro Max to report its views...                 4ms
Syncing files to device iPhone 11 Pro Max...                       132ms

Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h Repeat this help message.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).
An Observatory debugger and profiler on iPhone 11 Pro Max is available at: http://127.0.0.1:52747/mYAJoA1-HSM=/
flutter: Sent 2020-07-08 20:55:58.943408
flutter: Sent 2020-07-08 20:55:59.941748
flutter: Sent 2020-07-08 20:56:00.942935
flutter: Datagram from 192.168.86.23:49617: 2020-07-08 20:55:58.945481
flutter: Datagram from 192.168.86.23:49617: 2020-07-08 20:55:59.941989
flutter: Datagram from 192.168.86.23:49617: 2020-07-08 20:56:00.943231
flutter: Sent 2020-07-08 20:56:01.942048
CimZzz commented 4 years ago

@aam simulator always fine on my side, but physical device is not

aam commented 4 years ago

@CimZzz are you able to run repro sample from above on your device? Can you share the output/how does it fail?

johannesschrott commented 4 years ago

I tried it in my current project and I also get an error 49. But i get the name of the network interface (en0) and an ip address, so the answer @bugeats did get from apple is not 100% correct I assume.

zichangg commented 4 years ago

Looks like no evidence proves this is a VM bug. I'll unassign myself first.

liufeihe commented 4 years ago

@aam

dart is 2.9.3 my device is ipad, 13.7

error is below:

2020-09-27 18:21:00.870410+0800 Runner[852:721510] flutter: InternetAddress('224.0.0.251', IPv4) 2020-09-27 18:21:00.877627+0800 Runner[852:721510] [VERBOSE-2:ui_dart_state.cc(166)] Unhandled Exception: OS Error: Can't assign requested address, errno = 49

0 _NativeSocket.nativeJoinMulticast (dart:io-patch/socket_patch.dart:1420:56)

1 _NativeSocket.joinMulticast (dart:io-patch/socket_patch.dart:1371:5)

2 _RawDatagramSocket.joinMulticast (dart:io-patch/socket_patch.dart:2163:13)

3 MDnsClient.start (package:play_flutter/multi_dns/multi_dns.dart:152:17)

dnfield commented 4 years ago

Does it work with https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_networking_multicast?

liufeihe commented 4 years ago

Does it work with https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_networking_multicast?

this entitlement requires ios 14.0+, my ios is 13.7; and the entitlement is requied when use the nwconnectiongroup, https://developer.apple.com/documentation/network/nwconnectiongroup

alexbatalov commented 3 years ago

This issue exists on my super old iPad 3 which does not update beyond iOS 9.3.6. I bet the issue is not about iOS version, but the macOS/iOS specifics, which is already mentioned in the code. Take a look at the multicastAddress method, it has special handing for macOS but not iOS:

https://github.com/dart-lang/sdk/blob/318babd5ac8799c7251cfe5270e3623ddbc49b58/sdk/lib/_internal/vm/bin/socket_patch.dart#L1343-L1345

Since both macOS and iOS share the same underlying native implementation it should be:

if ((Platform.isMacOS || Platform.isIOS) && addr.type == InternetAddressType.IPv4) {

Someone asked if it ever worked in iOS. Well you can make it work right now. Joining a multicast group is actually setting special socket option. joinMulticast does that for you with some additional processing (that part is broken in case of iOS). So you need to replace it with setRawOption and provide it with byte representation of struct ip_mreq. Here is a simplified example for multicast_dns package:

// Join multicast on this interface.
// _incoming.joinMulticast(_mDnsAddress, interface);
final value = Uint8List.fromList(_mDnsAddress.rawAddress + interface.addresses[0].rawAddress);
_incoming.setRawOption(RawSocketOption(RawSocketOption.levelIPv4, 12, value));