Skip to content

Commit

Permalink
Xamarin.iOS: Add workaround for new restrictions in iOS 14.5+ regardi…
Browse files Browse the repository at this point in the history
…ng mDNS browsing
  • Loading branch information
ringe committed Aug 10, 2021
1 parent f96ac23 commit 1c9d216
Show file tree
Hide file tree
Showing 9 changed files with 792 additions and 3 deletions.
489 changes: 489 additions & 0 deletions Zeroconf/BonjourBrowser.cs

Large diffs are not rendered by default.

67 changes: 67 additions & 0 deletions Zeroconf/Sockaddr.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#if __IOS__
using Foundation;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using UIKit;

namespace Zeroconf
{
public enum SockAddrFamily
{
Inet = 2,
Inet6 = 23
}

[StructLayout(LayoutKind.Explicit, Size = 28)]
public struct Sockaddr
{
[FieldOffset(0)] public byte sin_len;
[FieldOffset(1)] public byte sin_family;
[FieldOffset(2)] public short sin_port;
[FieldOffset(4)] public int sin_addr;

// IPv6
[FieldOffset(4)] public uint sin6_flowinfo;
[FieldOffset(8)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] public byte[] sin6_addr8;
[FieldOffset(24)] public uint sin6_scope_id;

public static Sockaddr CreateSockaddr(IntPtr bytes)
{
Sockaddr sock = (Sockaddr)Marshal.PtrToStructure(bytes, typeof(Sockaddr));
return sock;
}

public static IPAddress CreateIPAddress(Sockaddr addr)
{
byte[] bytes = null;

switch (addr.sin_family)
{
case (byte)SockAddrFamily.Inet:
byte[] ipv4addr = new byte[4];
ipv4addr[0] = (byte)(addr.sin_addr & 0x000000FF);
ipv4addr[1] = (byte)((addr.sin_addr & 0x0000FF00) >> 8);
ipv4addr[2] = (byte)((addr.sin_addr & 0x00FF0000) >> 16);
ipv4addr[3] = (byte)((addr.sin_addr & 0xFF000000) >> 24);
bytes = ipv4addr;
break;
case (byte)SockAddrFamily.Inet6:
bytes = addr.sin6_addr8;
break;
default:
#if false
Console.WriteLine($"Unknown socket address family {addr.sin_family}");
#endif
bytes = null;
break;
}

return (bytes != null) ? new IPAddress(bytes): null;
}
}
}
#endif
4 changes: 2 additions & 2 deletions Zeroconf/Zeroconf.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="MSBuild.Sdk.Extras">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net46</TargetFrameworks>
<TargetFrameworks>netstandard2.0;net46;xamarinios10</TargetFrameworks>
<Authors>Claire Novotny</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/novotnyllc/Zeroconf</PackageProjectUrl>
Expand Down
111 changes: 111 additions & 0 deletions Zeroconf/ZeroconfNetServiceBrowser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#if __IOS__
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Zeroconf
{
static class ZeroconfNetServiceBrowser
{
static internal async Task<IReadOnlyList<IZeroconfHost>> ResolveAsync(ResolveOptions options,
Action<IZeroconfHost> callback = null,
CancellationToken cancellationToken = default(CancellationToken),
System.Net.NetworkInformation.NetworkInterface[] netInterfacesToSendRequestOn = null)
{
if (options == null) throw new ArgumentNullException(nameof(options));
if (netInterfacesToSendRequestOn != null)
{
throw new NotImplementedException($"iOS NSNetServiceBrowser/NSNetService does not support per-network interface requests");
}

List<IZeroconfHost> combinedResultList = new List<IZeroconfHost>();

// Seems you must reuse the one BonjourBrowser (which is really an NSNetServiceBrowser)... multiple instances do not play well together

BonjourBrowser bonjourBrowser = new BonjourBrowser();

foreach (var protocol in options.Protocols)
{
ResolveOptions perProtocolBrowseOption = new ResolveOptions(protocol)
{
AllowOverlappedQueries = options.AllowOverlappedQueries,
Retries = options.Retries,
RetryDelay = options.RetryDelay,
ScanQueryType = options.ScanQueryType,
ScanTime = options.ScanTime,
};
bonjourBrowser.SetResolveOptions(perProtocolBrowseOption, callback, cancellationToken, netInterfacesToSendRequestOn);

bonjourBrowser.StartServiceSearch();

await Task.Delay(options.ScanTime, cancellationToken).ConfigureAwait(false);

bonjourBrowser.StopServiceSearch();

// Simpleminded callback implementation
var results = bonjourBrowser.ReturnZeroconfHostResults();
foreach (var result in results)
{
if (callback != null)
{
callback(result);
}
}

combinedResultList.AddRange(results);
}

return combinedResultList;
}

static internal async Task<ILookup<string, string>> BrowseDomainsAsync(List<string> browseDomainProtocolList, BrowseDomainsOptions options,
Action<string, string> callback = null,
CancellationToken cancellationToken = default(CancellationToken),
System.Net.NetworkInformation.NetworkInterface[] netInterfacesToSendRequestOn = null)
{
if (options == null) throw new ArgumentNullException(nameof(options));
if (netInterfacesToSendRequestOn != null)
{
throw new NotImplementedException($"iOS NSNetServiceBrowser/NSNetService does not support per-network interface requests");
}

ResolveOptions resolveOptions = new ResolveOptions(browseDomainProtocolList);
var zeroconfResults = await ResolveAsync(resolveOptions, callback: null, cancellationToken, netInterfacesToSendRequestOn);

List<IntermediateResult> resultsList = new List<IntermediateResult>();
foreach (var host in zeroconfResults)
{
foreach (var service in host.Services)
{
foreach (var ipAddr in host.IPAddresses)
{
IntermediateResult b = new IntermediateResult();
b.ServiceNameAndDomain = service.Key;
b.HostIPAndService = $"{ipAddr}: {BonjourBrowser.GetServiceType(service.Value.Name, includeTcpUdpDelimiter: false)}";

resultsList.Add(b);

// Simpleminded callback implementation
if (callback != null)
{
callback(service.Key, ipAddr);
}
}
}
}

ILookup<string, string> results = resultsList.ToLookup(k => k.ServiceNameAndDomain, h => h.HostIPAndService);
return results;
}

class IntermediateResult
{
public string ServiceNameAndDomain;
public string HostIPAndService;
}
}
}
#endif
93 changes: 93 additions & 0 deletions Zeroconf/ZeroconfResolver.Async.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
using System.Threading.Tasks;
using Heijden.DNS;

#if __IOS__
using UIKit;
#endif

namespace Zeroconf
{
static partial class ZeroconfResolver
Expand Down Expand Up @@ -87,6 +91,7 @@ public static async Task<IReadOnlyList<IZeroconfHost>> ResolveAsync(ResolveOptio
System.Net.NetworkInformation.NetworkInterface[] netInterfacesToSendRequestOn = null)
{
if (options == null) throw new ArgumentNullException(nameof(options));
#if !__IOS__
Action<string, Response> wrappedAction = null;

if (callback != null)
Expand All @@ -110,6 +115,61 @@ public static async Task<IReadOnlyList<IZeroconfHost>> ResolveAsync(ResolveOptio
return dict.Select(pair => ResponseToZeroconf(pair.Value, pair.Key, options))
.Where(zh => zh.Services.Any(s => options.Protocols.Contains(s.Key))) // Ensure we only return records that have matching services
.ToList();
#else
if (UIDevice.CurrentDevice.CheckSystemVersion(14, 5))
{
return await ZeroconfNetServiceBrowser.ResolveAsync(options, callback, cancellationToken, netInterfacesToSendRequestOn);
}
else
{
Action<string, Response> wrappedAction = null;

if (callback != null)
{
wrappedAction = (address, resp) =>
{
var zc = ResponseToZeroconf(resp, address, options);
if (zc.Services.Any(s => options.Protocols.Contains(s.Key)))
{
callback(zc);
}
};
}

var dict = await ResolveInternal(options,
wrappedAction,
cancellationToken,
netInterfacesToSendRequestOn)
.ConfigureAwait(false);

return dict.Select(pair => ResponseToZeroconf(pair.Value, pair.Key, options))
.Where(zh => zh.Services.Any(s => options.Protocols.Contains(s.Key))) // Ensure we only return records that have matching services
.ToList();
}
#endif
}

// Should be set to the list of allowed protocols from info.plist; entries must include domain including terminating dot (usually ".local.")
// Used by BrowseDomainAsync only; the hack is that "browsing" is really just ResolveAsync() with the result formatted differently
static List<string> browseDomainProtocolList = new List<string>();

/// <summary>
/// Sets browse domain protocols (provided using pattern "[protocol].[domain].") for Xamarin iOS 14.5+ integration
/// </summary>
/// <param name="protocols">IEnumerable of string browse domain protocols</param>
/// <returns></returns>
public static void SetBrowseDomainProtocols(IEnumerable<string> protocols)
{
if (protocols == null) { throw new ArgumentException(nameof(protocols)); }
browseDomainProtocolList.Clear();

foreach (var protocol in protocols)
{
if (protocol != null)
{
browseDomainProtocolList.Add(protocol);
}
}
}

/// <summary>
Expand Down Expand Up @@ -160,6 +220,7 @@ public static async Task<ILookup<string, string>> BrowseDomainsAsync(BrowseDomai
{
if (options == null) throw new ArgumentNullException(nameof(options));

#if !__IOS__
Action<string, Response> wrappedAction = null;
if (callback != null)
{
Expand All @@ -183,6 +244,38 @@ from service in BrowseResponseParser(kvp.Value)
select new { Service = service, Address = kvp.Key };

return r.ToLookup(k => k.Service, k => k.Address);
#else
if (UIDevice.CurrentDevice.CheckSystemVersion(14, 5))
{
return await ZeroconfNetServiceBrowser.BrowseDomainsAsync(browseDomainProtocolList, options, callback, cancellationToken, netInterfacesToSendRequestOn);
}
else
{
Action<string, Response> wrappedAction = null;
if (callback != null)
{
wrappedAction = (address, response) =>
{
foreach (var service in BrowseResponseParser(response))
{
callback(service, address);
}
};
}

var dict = await ResolveInternal(options,
wrappedAction,
cancellationToken,
netInterfacesToSendRequestOn)
.ConfigureAwait(false);

var r = from kvp in dict
from service in BrowseResponseParser(kvp.Value)
select new { Service = service, Address = kvp.Key };

return r.ToLookup(k => k.Service, k => k.Address);
}
#endif
}

/// <summary>
Expand Down
9 changes: 9 additions & 0 deletions ZeroconfTest.Xamarin/ZeroconfTest.Xamarin.iOS/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>Looking for local mDNS/Bonjour services</string>
<key>NSBonjourServices</key>
<array>
<string>_audioplayer-discovery._tcp</string>
<string>_http._tcp</string>
<string>_printer._tcp</string>
<string>_apple-mobdev2._tcp</string>
</array>
<key>MinimumOSVersion</key>
<string>10.0</string>
<key>CFBundleDisplayName</key>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
<MtouchProfiling>False</MtouchProfiling>
<MtouchFastDev>False</MtouchFastDev>
<MtouchEnableGenericValueTypeSharing>True</MtouchEnableGenericValueTypeSharing>
<MtouchArch>ARMv7</MtouchArch>
<MtouchArch>ARMv7, ARM64</MtouchArch>
<MtouchUseLlvm>False</MtouchUseLlvm>
<MtouchUseThumb>False</MtouchUseThumb>
<MtouchUseSGen>False</MtouchUseSGen>
Expand Down
15 changes: 15 additions & 0 deletions ZeroconfTest.Xamarin/ZeroconfTest.Xamarin/App.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ public static Page GetMainPage()
};
}

// See ZeroconfResolver.Async.cs
// Use the array of NSBonjourServices from Info.plist; however, in this list, append the domain and terminating period (usually ".local.")
static List<string> BrowseDomainProtocolList = new List<string>()
{
"_audioplayer-discovery._tcp.local.",
"_http._tcp.local.",
"_printer._tcp.local.",
"_apple-mobdev2._tcp.local.",
};

#pragma warning disable RECS0165 // Asynchronous methods should return a Task instead of void
static async void BrowseOnClicked(Label output)
#pragma warning restore RECS0165 // Asynchronous methods should return a Task instead of void
Expand All @@ -57,6 +67,9 @@ static async void BrowseOnClicked(Label output)

//});

// Xamarin iOS on iOS 14.5+ only: BrowseDomainsAsync() is not usable without this initialization
ZeroconfResolver.SetBrowseDomainProtocols(BrowseDomainProtocolList);

responses = await ZeroconfResolver.BrowseDomainsAsync();
foreach (var service in responses)
{
Expand All @@ -78,6 +91,8 @@ static async void ResolveOnClicked(Label output)

//});

// Xamarin.iOS on iOS 14.5+ only: BrowseDomainsAsync() is not usable without this initialization
ZeroconfResolver.SetBrowseDomainProtocols(BrowseDomainProtocolList);

var domains = await ZeroconfResolver.BrowseDomainsAsync();

Expand Down
5 changes: 5 additions & 0 deletions global.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"msbuild-sdks": {
"MSBuild.Sdk.Extras": "3.0.23"
}
}

0 comments on commit 1c9d216

Please sign in to comment.