Skip to content

Commit 4c7251b

Browse files
committed
Prefer reachable LAN discovery addresses
1 parent 99b037d commit 4c7251b

7 files changed

Lines changed: 504 additions & 56 deletions

File tree

Basis Server/BasisNetworkCore/BasisLanDiscoveryProtocol.cs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using MeaMod.DNS.Multicast;
22
using System;
33
using System.Collections.Generic;
4+
using System.Linq;
45
using System.Net;
6+
using System.Net.NetworkInformation;
57
using System.Net.Sockets;
68
using System.Text;
79

@@ -34,6 +36,18 @@ public BasisLanAdvertisement(
3436
}
3537
}
3638

39+
internal readonly struct BasisLanIpv4Subnet
40+
{
41+
public readonly IPAddress Address;
42+
public readonly IPAddress Mask;
43+
44+
public BasisLanIpv4Subnet(IPAddress address, IPAddress mask)
45+
{
46+
Address = address;
47+
Mask = mask;
48+
}
49+
}
50+
3751
/// <summary>Shared address filtering and preference rules for Basis LAN discovery.</summary>
3852
public static class BasisLanAddressUtility
3953
{
@@ -69,6 +83,137 @@ public static int PreferenceRank(IPAddress address)
6983
? address.IsIPv6LinkLocal ? 3 : 1
7084
: int.MaxValue;
7185
}
86+
87+
internal static IPAddress[] GetPreferredAdvertisedAddresses()
88+
{
89+
List<IPAddress> gatewayAddresses = new List<IPAddress>();
90+
List<IPAddress> fallbackAddresses = new List<IPAddress>();
91+
92+
try
93+
{
94+
foreach (NetworkInterface networkInterface in NetworkInterface.GetAllNetworkInterfaces())
95+
{
96+
if (networkInterface.OperationalStatus != OperationalStatus.Up
97+
|| networkInterface.NetworkInterfaceType == NetworkInterfaceType.Loopback
98+
|| networkInterface.NetworkInterfaceType == NetworkInterfaceType.Tunnel)
99+
{
100+
continue;
101+
}
102+
103+
try
104+
{
105+
IPInterfaceProperties properties = networkInterface.GetIPProperties();
106+
bool hasUsableGateway = properties.GatewayAddresses
107+
.Any(gateway => IsUsable(gateway?.Address));
108+
109+
foreach (UnicastIPAddressInformation unicast in properties.UnicastAddresses)
110+
{
111+
if (!IsUsable(unicast?.Address))
112+
{
113+
continue;
114+
}
115+
116+
fallbackAddresses.Add(unicast.Address);
117+
if (hasUsableGateway)
118+
{
119+
gatewayAddresses.Add(unicast.Address);
120+
}
121+
}
122+
}
123+
catch
124+
{
125+
// Ignore interfaces that disappear or reject property queries mid-enumeration.
126+
}
127+
}
128+
}
129+
catch
130+
{
131+
// Fall through to an empty set; the DNS-SD response source remains usable as a fallback.
132+
}
133+
134+
List<IPAddress> selected = gatewayAddresses.Count > 0
135+
? gatewayAddresses
136+
: fallbackAddresses;
137+
return selected
138+
.Distinct()
139+
.OrderBy(PreferenceRank)
140+
.ToArray();
141+
}
142+
143+
internal static BasisLanIpv4Subnet[] GetLocalIpv4Subnets()
144+
{
145+
List<BasisLanIpv4Subnet> gatewaySubnets = new List<BasisLanIpv4Subnet>();
146+
List<BasisLanIpv4Subnet> fallbackSubnets = new List<BasisLanIpv4Subnet>();
147+
try
148+
{
149+
foreach (NetworkInterface networkInterface in NetworkInterface.GetAllNetworkInterfaces())
150+
{
151+
if (networkInterface.OperationalStatus != OperationalStatus.Up
152+
|| networkInterface.NetworkInterfaceType == NetworkInterfaceType.Loopback
153+
|| networkInterface.NetworkInterfaceType == NetworkInterfaceType.Tunnel)
154+
{
155+
continue;
156+
}
157+
158+
try
159+
{
160+
IPInterfaceProperties properties = networkInterface.GetIPProperties();
161+
bool hasUsableGateway = properties.GatewayAddresses
162+
.Any(gateway => IsUsable(gateway?.Address));
163+
foreach (UnicastIPAddressInformation unicast in properties.UnicastAddresses)
164+
{
165+
if (unicast?.Address?.AddressFamily != AddressFamily.InterNetwork
166+
|| unicast.IPv4Mask?.AddressFamily != AddressFamily.InterNetwork
167+
|| !IsUsable(unicast.Address))
168+
{
169+
continue;
170+
}
171+
172+
BasisLanIpv4Subnet subnet =
173+
new BasisLanIpv4Subnet(unicast.Address, unicast.IPv4Mask);
174+
fallbackSubnets.Add(subnet);
175+
if (hasUsableGateway)
176+
{
177+
gatewaySubnets.Add(subnet);
178+
}
179+
}
180+
}
181+
catch
182+
{
183+
// Ignore interfaces that disappear or reject property queries mid-enumeration.
184+
}
185+
}
186+
}
187+
catch
188+
{
189+
}
190+
return (gatewaySubnets.Count > 0 ? gatewaySubnets : fallbackSubnets).ToArray();
191+
}
192+
193+
internal static bool IsOnSameIpv4Subnet(
194+
IPAddress candidate,
195+
BasisLanIpv4Subnet subnet)
196+
{
197+
if (candidate?.AddressFamily != AddressFamily.InterNetwork
198+
|| subnet.Address?.AddressFamily != AddressFamily.InterNetwork
199+
|| subnet.Mask?.AddressFamily != AddressFamily.InterNetwork)
200+
{
201+
return false;
202+
}
203+
204+
byte[] candidateBytes = candidate.GetAddressBytes();
205+
byte[] localBytes = subnet.Address.GetAddressBytes();
206+
byte[] maskBytes = subnet.Mask.GetAddressBytes();
207+
for (int index = 0; index < candidateBytes.Length; index++)
208+
{
209+
if ((candidateBytes[index] & maskBytes[index])
210+
!= (localBytes[index] & maskBytes[index]))
211+
{
212+
return false;
213+
}
214+
}
215+
return true;
216+
}
72217
}
73218

74219
/// <summary>Shared constants and bounded TXT metadata encoding for Basis LAN DNS-SD.</summary>

Basis Server/BasisNetworkCore/BasisLanServerAnnouncer.cs

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using MeaMod.DNS.Model;
22
using MeaMod.DNS.Multicast;
33
using System;
4-
using System.Linq;
54
using System.Net;
65

76
namespace Basis.Network.Core
@@ -105,18 +104,12 @@ internal static ServiceProfile CreateProfile(
105104

106105
private static IPAddress[] GetAdvertisedAddresses()
107106
{
108-
try
109-
{
110-
return MulticastService.GetIPAddresses()
111-
.Where(address => BasisLanAddressUtility.IsUsable(address))
112-
.Distinct()
113-
.ToArray();
114-
}
115-
catch (Exception ex)
107+
IPAddress[] addresses = BasisLanAddressUtility.GetPreferredAdvertisedAddresses();
108+
if (addresses.Length == 0)
116109
{
117-
BNL.LogWarning($"Basis LAN address discovery failed: {ex.Message}");
118-
return Array.Empty<IPAddress>();
110+
BNL.LogWarning("Basis LAN address discovery found no usable local addresses.");
119111
}
112+
return addresses;
120113
}
121114

122115
public void Dispose()

Basis Server/BasisNetworkCore/BasisLanServerBrowser.cs

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ 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 readonly BasisLanIpv4Subnet[] _localIpv4Subnets;
2829
private MulticastService _mdns;
2930
private ServiceDiscovery _discovery;
3031
private volatile bool _disposed;
@@ -36,6 +37,7 @@ public BasisLanServerBrowser(
3637
{
3738
_found = found ?? throw new ArgumentNullException(nameof(found));
3839
_removed = removed ?? throw new ArgumentNullException(nameof(removed));
40+
_localIpv4Subnets = BasisLanAddressUtility.GetLocalIpv4Subnets();
3941

4042
MulticastService mdns = null;
4143
ServiceDiscovery discovery = null;
@@ -136,7 +138,8 @@ private void OnServiceDiscovered(object sender, ServiceInstanceDiscoveryEventArg
136138
args.ServiceInstanceName,
137139
args.RemoteEndPoint?.Address,
138140
out BasisLanAdvertisement advertisement,
139-
out IPAddress address))
141+
out IPAddress address,
142+
_localIpv4Subnets))
140143
{
141144
_found(advertisement, address);
142145
}
@@ -160,7 +163,8 @@ internal static bool TryExtractAdvertisement(
160163
DomainName serviceInstanceName,
161164
IPAddress remoteAddress,
162165
out BasisLanAdvertisement advertisement,
163-
out IPAddress address)
166+
out IPAddress address,
167+
IReadOnlyList<BasisLanIpv4Subnet> localIpv4Subnets = null)
164168
{
165169
advertisement = default;
166170
address = null;
@@ -206,7 +210,7 @@ internal static bool TryExtractAdvertisement(
206210
return false;
207211
}
208212

209-
address = SelectAddress(records, service.Target, remoteAddress);
213+
address = SelectAddress(records, service.Target, remoteAddress, localIpv4Subnets);
210214
if (address == null)
211215
{
212216
return false;
@@ -289,9 +293,18 @@ private static Dictionary<string, string> ReadProperties(IList<string> values)
289293
private static IPAddress SelectAddress(
290294
IEnumerable<ResourceRecord> records,
291295
DomainName hostName,
292-
IPAddress remoteAddress)
296+
IPAddress remoteAddress,
297+
IReadOnlyList<BasisLanIpv4Subnet> localIpv4Subnets)
293298
{
299+
if (BasisLanAddressUtility.IsUsable(remoteAddress, allowLoopback: true)
300+
&& remoteAddress.AddressFamily == AddressFamily.InterNetwork)
301+
{
302+
// A usable IPv4 response source is the interface that actually reached us.
303+
return remoteAddress;
304+
}
305+
294306
IPAddress selected = null;
307+
bool selectedOnLocalSubnet = false;
295308
foreach (ResourceRecord record in records)
296309
{
297310
if (!(record is AddressRecord addressRecord)
@@ -302,22 +315,33 @@ private static IPAddress SelectAddress(
302315
}
303316

304317
IPAddress candidate = RestoreScope(addressRecord.Address, remoteAddress);
318+
bool candidateOnLocalSubnet = IsOnLocalIpv4Subnet(candidate, localIpv4Subnets);
305319
if (selected == null
306-
|| BasisLanAddressUtility.PreferenceRank(candidate)
307-
< BasisLanAddressUtility.PreferenceRank(selected))
320+
|| (candidateOnLocalSubnet && !selectedOnLocalSubnet)
321+
|| (candidateOnLocalSubnet == selectedOnLocalSubnet
322+
&& BasisLanAddressUtility.PreferenceRank(candidate)
323+
< BasisLanAddressUtility.PreferenceRank(selected)))
308324
{
309325
selected = candidate;
326+
selectedOnLocalSubnet = candidateOnLocalSubnet;
310327
}
311328
}
312329

330+
if (selected?.AddressFamily == AddressFamily.InterNetwork
331+
&& localIpv4Subnets?.Count > 0
332+
&& !selectedOnLocalSubnet)
333+
{
334+
// Do not create an entry from a partial response that only contains an unrelated
335+
// virtual/VPN subnet. A later complete response can provide the same-subnet address.
336+
selected = null;
337+
}
338+
313339
if (BasisLanAddressUtility.IsUsable(remoteAddress, allowLoopback: true))
314340
{
315-
// The IPv4 packet source is the interface that actually reached us, so it is
316-
// safer than unrelated VPN/cellular A records. Android can emit mDNS over IPv6
317-
// while its hosted LiteNetLib socket is only reachable over Wi-Fi IPv4; in that
318-
// case prefer the advertised IPv4 address.
319-
if (remoteAddress.AddressFamily == AddressFamily.InterNetwork
320-
|| selected?.AddressFamily != AddressFamily.InterNetwork)
341+
// Android can receive an IPv6 mDNS response while the hosted LiteNetLib server
342+
// is reachable over IPv4. Prefer a same-subnet advertised IPv4 in that case.
343+
if (!selectedOnLocalSubnet
344+
&& selected?.AddressFamily != AddressFamily.InterNetwork)
321345
{
322346
return remoteAddress;
323347
}
@@ -326,6 +350,28 @@ private static IPAddress SelectAddress(
326350
return selected;
327351
}
328352

353+
private static bool IsOnLocalIpv4Subnet(
354+
IPAddress candidate,
355+
IReadOnlyList<BasisLanIpv4Subnet> localIpv4Subnets)
356+
{
357+
if (candidate?.AddressFamily != AddressFamily.InterNetwork
358+
|| localIpv4Subnets == null)
359+
{
360+
return false;
361+
}
362+
363+
for (int index = 0; index < localIpv4Subnets.Count; index++)
364+
{
365+
if (BasisLanAddressUtility.IsOnSameIpv4Subnet(
366+
candidate,
367+
localIpv4Subnets[index]))
368+
{
369+
return true;
370+
}
371+
}
372+
return false;
373+
}
374+
329375
private static IPAddress RestoreScope(IPAddress address, IPAddress remoteAddress)
330376
{
331377
if (address != null

0 commit comments

Comments
 (0)