@@ -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