Skip to content

Commit 50909cf

Browse files
committed
Fix Android LAN discovery connectivity
1 parent a663d63 commit 50909cf

4 files changed

Lines changed: 145 additions & 26 deletions

File tree

Basis Server/BasisNetworkCore/BasisLanServerBrowser.cs

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,29 +25,44 @@ public sealed class BasisLanServerBrowser : IDisposable
2525
private readonly CancellationTokenSource _cancellation = new CancellationTokenSource();
2626
private readonly Action<BasisLanAdvertisement, IPAddress> _found;
2727
private readonly Action<Guid> _removed;
28+
private MulticastService _mdns;
2829
private ServiceDiscovery _discovery;
2930
private volatile bool _disposed;
3031

3132
public BasisLanServerBrowser(
3233
Action<BasisLanAdvertisement, IPAddress> found,
33-
Action<Guid> removed)
34+
Action<Guid> removed,
35+
bool useIpv6 = true)
3436
{
3537
_found = found ?? throw new ArgumentNullException(nameof(found));
3638
_removed = removed ?? throw new ArgumentNullException(nameof(removed));
3739

40+
MulticastService mdns = null;
41+
ServiceDiscovery discovery = null;
3842
try
3943
{
40-
_discovery = new ServiceDiscovery();
41-
_discovery.Mdns.IgnoreDuplicateMessages = true;
42-
_discovery.ServiceInstanceDiscovered += OnServiceDiscovered;
43-
_discovery.ServiceInstanceShutdown += OnServiceShutdown;
44+
mdns = new MulticastService
45+
{
46+
UseIpv4 = Socket.OSSupportsIPv4,
47+
UseIpv6 = useIpv6 && Socket.OSSupportsIPv6,
48+
IgnoreDuplicateMessages = true,
49+
};
50+
discovery = new ServiceDiscovery(mdns);
51+
discovery.ServiceInstanceDiscovered += OnServiceDiscovered;
52+
discovery.ServiceInstanceShutdown += OnServiceShutdown;
53+
54+
_mdns = mdns;
55+
_discovery = discovery;
56+
mdns.Start();
4457
Query();
4558
_ = Task.Run(() => QueryLoopAsync(_cancellation.Token));
4659
}
4760
catch
4861
{
49-
_discovery?.Dispose();
62+
discovery?.Dispose();
63+
mdns?.Dispose();
5064
_discovery = null;
65+
_mdns = null;
5166
_cancellation.Dispose();
5267
throw;
5368
}
@@ -273,11 +288,6 @@ private static IPAddress SelectAddress(
273288
DomainName hostName,
274289
IPAddress remoteAddress)
275290
{
276-
if (BasisLanAddressUtility.IsUsable(remoteAddress, allowLoopback: true))
277-
{
278-
return remoteAddress;
279-
}
280-
281291
IPAddress selected = null;
282292
foreach (ResourceRecord record in records)
283293
{
@@ -296,6 +306,20 @@ private static IPAddress SelectAddress(
296306
selected = candidate;
297307
}
298308
}
309+
310+
if (BasisLanAddressUtility.IsUsable(remoteAddress, allowLoopback: true))
311+
{
312+
// The IPv4 packet source is the interface that actually reached us, so it is
313+
// safer than unrelated VPN/cellular A records. Android can emit mDNS over IPv6
314+
// while its hosted LiteNetLib socket is only reachable over Wi-Fi IPv4; in that
315+
// case prefer the advertised IPv4 address.
316+
if (remoteAddress.AddressFamily == AddressFamily.InterNetwork
317+
|| selected?.AddressFamily != AddressFamily.InterNetwork)
318+
{
319+
return remoteAddress;
320+
}
321+
}
322+
299323
return selected;
300324
}
301325

@@ -316,6 +340,7 @@ private static IPAddress RestoreScope(IPAddress address, IPAddress remoteAddress
316340

317341
public void Dispose()
318342
{
343+
MulticastService mdns;
319344
ServiceDiscovery discovery;
320345
lock (_gate)
321346
{
@@ -329,7 +354,9 @@ public void Dispose()
329354
catch (ObjectDisposedException) { }
330355

331356
discovery = _discovery;
357+
mdns = _mdns;
332358
_discovery = null;
359+
_mdns = null;
333360
if (discovery != null)
334361
{
335362
discovery.ServiceInstanceDiscovered -= OnServiceDiscovered;
@@ -338,6 +365,7 @@ public void Dispose()
338365
}
339366

340367
discovery?.Dispose();
368+
mdns?.Dispose();
341369
_cancellation.Dispose();
342370
}
343371
}

Basis Server/BasisServerTests/BasisLanDiscoveryTests.cs

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,55 @@ public void EmptyAddressRecords_UseResponseSourceAddress()
8888
string.Empty,
8989
false,
9090
Array.Empty<IPAddress>());
91+
IPAddress responseSource = IPAddress.Parse("192.168.1.25");
9192

92-
BasisLanAdvertisement advertisement = Extract(profile);
93+
BasisLanAdvertisement advertisement = Extract(profile, responseSource, out IPAddress address);
9394

9495
Assert.Equal(id, advertisement.InstanceId);
96+
Assert.Equal(responseSource, address);
9597
Assert.DoesNotContain(profile.Resources, resource => resource is AddressRecord);
9698
}
9799

100+
[Fact]
101+
public void AdvertisedIpv4_IsPreferredOverIpv6ResponseSource()
102+
{
103+
IPAddress advertisedIpv4 = IPAddress.Parse("192.168.1.25");
104+
IPAddress responseSource = new IPAddress(
105+
IPAddress.Parse("fe80::1234").GetAddressBytes(),
106+
7);
107+
ServiceProfile profile = BasisLanServerAnnouncer.CreateProfile(
108+
Guid.NewGuid(),
109+
4296,
110+
BasisNetworkStackRegistry.LiteNetLibId,
111+
"Android host",
112+
string.Empty,
113+
false,
114+
new[] { advertisedIpv4 });
115+
116+
Extract(profile, responseSource, out IPAddress address);
117+
118+
Assert.Equal(advertisedIpv4, address);
119+
}
120+
121+
[Fact]
122+
public void Ipv4ResponseSource_IsPreferredOverOtherAdvertisedIpv4()
123+
{
124+
IPAddress advertisedIpv4 = IPAddress.Parse("10.0.0.5");
125+
IPAddress responseSource = IPAddress.Parse("192.168.1.25");
126+
ServiceProfile profile = BasisLanServerAnnouncer.CreateProfile(
127+
Guid.NewGuid(),
128+
4296,
129+
BasisNetworkStackRegistry.LiteNetLibId,
130+
"Wi-Fi host",
131+
string.Empty,
132+
false,
133+
new[] { advertisedIpv4 });
134+
135+
Extract(profile, responseSource, out IPAddress address);
136+
137+
Assert.Equal(responseSource, address);
138+
}
139+
98140
[Fact]
99141
public void AddressPreference_PrioritizesRoutableAddressesOverLinkLocal()
100142
{
@@ -131,6 +173,17 @@ private static ServiceProfile CreateProfile(
131173
}
132174

133175
private static BasisLanAdvertisement Extract(ServiceProfile profile)
176+
{
177+
IPAddress expectedAddress = IPAddress.Parse("192.168.1.25");
178+
BasisLanAdvertisement advertisement = Extract(profile, expectedAddress, out IPAddress address);
179+
Assert.Equal(expectedAddress, address);
180+
return advertisement;
181+
}
182+
183+
private static BasisLanAdvertisement Extract(
184+
ServiceProfile profile,
185+
IPAddress responseSource,
186+
out IPAddress address)
134187
{
135188
Message message = new Message();
136189
foreach (ResourceRecord resource in profile.Resources)
@@ -148,10 +201,10 @@ private static BasisLanAdvertisement Extract(ServiceProfile profile)
148201
Assert.True(BasisLanServerBrowser.TryExtractAdvertisement(
149202
message,
150203
profile.FullyQualifiedName,
151-
IPAddress.Parse("192.168.1.25"),
204+
responseSource,
152205
out BasisLanAdvertisement advertisement,
153-
out IPAddress? address));
154-
Assert.Equal(IPAddress.Parse("192.168.1.25"), address);
206+
out IPAddress? selectedAddress));
207+
address = Assert.IsType<IPAddress>(selectedAddress);
155208
return advertisement;
156209
}
157210

Basis/Packages/com.basis.framework/Networking/LanServersDirectorySource.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,17 @@ private void StartListening()
129129
AcquireAndroidMulticastLock();
130130
try
131131
{
132+
#if UNITY_ANDROID && !UNITY_EDITOR
133+
// MeaMod's IPv6 multicast receiver can fail during construction on Android and
134+
// prevent its IPv4 receiver from starting. LAN-hosted Basis servers already
135+
// advertise IPv4 addresses, so browse over IPv4 on Android.
136+
_browser = new BasisLanServerBrowser(
137+
ProcessAdvertisement,
138+
RemoveAdvertisement,
139+
useIpv6: false);
140+
#else
132141
_browser = new BasisLanServerBrowser(ProcessAdvertisement, RemoveAdvertisement);
142+
#endif
133143
_ = Task.Run(() => CleanupLoopAsync(_cancellation.Token));
134144
}
135145
catch (Exception ex)

Basis/Packages/com.basis.server/BasisNetworkCore/BasisLanServerBrowser.cs

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,29 +25,44 @@ public sealed class BasisLanServerBrowser : IDisposable
2525
private readonly CancellationTokenSource _cancellation = new CancellationTokenSource();
2626
private readonly Action<BasisLanAdvertisement, IPAddress> _found;
2727
private readonly Action<Guid> _removed;
28+
private MulticastService _mdns;
2829
private ServiceDiscovery _discovery;
2930
private volatile bool _disposed;
3031

3132
public BasisLanServerBrowser(
3233
Action<BasisLanAdvertisement, IPAddress> found,
33-
Action<Guid> removed)
34+
Action<Guid> removed,
35+
bool useIpv6 = true)
3436
{
3537
_found = found ?? throw new ArgumentNullException(nameof(found));
3638
_removed = removed ?? throw new ArgumentNullException(nameof(removed));
3739

40+
MulticastService mdns = null;
41+
ServiceDiscovery discovery = null;
3842
try
3943
{
40-
_discovery = new ServiceDiscovery();
41-
_discovery.Mdns.IgnoreDuplicateMessages = true;
42-
_discovery.ServiceInstanceDiscovered += OnServiceDiscovered;
43-
_discovery.ServiceInstanceShutdown += OnServiceShutdown;
44+
mdns = new MulticastService
45+
{
46+
UseIpv4 = Socket.OSSupportsIPv4,
47+
UseIpv6 = useIpv6 && Socket.OSSupportsIPv6,
48+
IgnoreDuplicateMessages = true,
49+
};
50+
discovery = new ServiceDiscovery(mdns);
51+
discovery.ServiceInstanceDiscovered += OnServiceDiscovered;
52+
discovery.ServiceInstanceShutdown += OnServiceShutdown;
53+
54+
_mdns = mdns;
55+
_discovery = discovery;
56+
mdns.Start();
4457
Query();
4558
_ = Task.Run(() => QueryLoopAsync(_cancellation.Token));
4659
}
4760
catch
4861
{
49-
_discovery?.Dispose();
62+
discovery?.Dispose();
63+
mdns?.Dispose();
5064
_discovery = null;
65+
_mdns = null;
5166
_cancellation.Dispose();
5267
throw;
5368
}
@@ -273,11 +288,6 @@ private static IPAddress SelectAddress(
273288
DomainName hostName,
274289
IPAddress remoteAddress)
275290
{
276-
if (BasisLanAddressUtility.IsUsable(remoteAddress, allowLoopback: true))
277-
{
278-
return remoteAddress;
279-
}
280-
281291
IPAddress selected = null;
282292
foreach (ResourceRecord record in records)
283293
{
@@ -296,6 +306,20 @@ private static IPAddress SelectAddress(
296306
selected = candidate;
297307
}
298308
}
309+
310+
if (BasisLanAddressUtility.IsUsable(remoteAddress, allowLoopback: true))
311+
{
312+
// The IPv4 packet source is the interface that actually reached us, so it is
313+
// safer than unrelated VPN/cellular A records. Android can emit mDNS over IPv6
314+
// while its hosted LiteNetLib socket is only reachable over Wi-Fi IPv4; in that
315+
// case prefer the advertised IPv4 address.
316+
if (remoteAddress.AddressFamily == AddressFamily.InterNetwork
317+
|| selected?.AddressFamily != AddressFamily.InterNetwork)
318+
{
319+
return remoteAddress;
320+
}
321+
}
322+
299323
return selected;
300324
}
301325

@@ -316,6 +340,7 @@ private static IPAddress RestoreScope(IPAddress address, IPAddress remoteAddress
316340

317341
public void Dispose()
318342
{
343+
MulticastService mdns;
319344
ServiceDiscovery discovery;
320345
lock (_gate)
321346
{
@@ -329,7 +354,9 @@ public void Dispose()
329354
catch (ObjectDisposedException) { }
330355

331356
discovery = _discovery;
357+
mdns = _mdns;
332358
_discovery = null;
359+
_mdns = null;
333360
if (discovery != null)
334361
{
335362
discovery.ServiceInstanceDiscovered -= OnServiceDiscovered;
@@ -338,6 +365,7 @@ public void Dispose()
338365
}
339366

340367
discovery?.Dispose();
368+
mdns?.Dispose();
341369
_cancellation.Dispose();
342370
}
343371
}

0 commit comments

Comments
 (0)