@@ -26,27 +26,38 @@ public sealed class BasisLanServerBrowser : IDisposable
2626 private readonly Action < BasisLanAdvertisement , IPAddress > _found ;
2727 private readonly Action < Guid > _removed ;
2828 private readonly BasisLanIpv4Subnet [ ] _localIpv4Subnets ;
29+ private readonly BasisLanIpv6Subnet [ ] _localIpv6Subnets ;
2930 private MulticastService _mdns ;
3031 private ServiceDiscovery _discovery ;
3132 private volatile bool _disposed ;
3233
3334 public BasisLanServerBrowser (
3435 Action < BasisLanAdvertisement , IPAddress > found ,
3536 Action < Guid > removed ,
37+ bool useIpv4 = true ,
3638 bool useIpv6 = true )
3739 {
3840 _found = found ?? throw new ArgumentNullException ( nameof ( found ) ) ;
3941 _removed = removed ?? throw new ArgumentNullException ( nameof ( removed ) ) ;
4042 _localIpv4Subnets = BasisLanAddressUtility . GetLocalIpv4Subnets ( ) ;
43+ _localIpv6Subnets = BasisLanAddressUtility . GetLocalIpv6Subnets ( ) ;
44+
45+ bool ipv4Enabled = useIpv4 && Socket . OSSupportsIPv4 ;
46+ bool ipv6Enabled = useIpv6 && Socket . OSSupportsIPv6 ;
47+ if ( ! ipv4Enabled && ! ipv6Enabled )
48+ {
49+ throw new PlatformNotSupportedException (
50+ "LAN discovery requires at least one supported IP address family." ) ;
51+ }
4152
4253 MulticastService mdns = null ;
4354 ServiceDiscovery discovery = null ;
4455 try
4556 {
4657 mdns = new MulticastService
4758 {
48- UseIpv4 = Socket . OSSupportsIPv4 ,
49- UseIpv6 = useIpv6 && Socket . OSSupportsIPv6 ,
59+ UseIpv4 = ipv4Enabled ,
60+ UseIpv6 = ipv6Enabled ,
5061 IgnoreDuplicateMessages = true ,
5162 } ;
5263 discovery = new ServiceDiscovery ( mdns ) ;
@@ -139,7 +150,8 @@ private void OnServiceDiscovered(object sender, ServiceInstanceDiscoveryEventArg
139150 args . RemoteEndPoint ? . Address ,
140151 out BasisLanAdvertisement advertisement ,
141152 out IPAddress address ,
142- _localIpv4Subnets ) )
153+ _localIpv4Subnets ,
154+ _localIpv6Subnets ) )
143155 {
144156 _found ( advertisement , address ) ;
145157 }
@@ -164,7 +176,8 @@ internal static bool TryExtractAdvertisement(
164176 IPAddress remoteAddress ,
165177 out BasisLanAdvertisement advertisement ,
166178 out IPAddress address ,
167- IReadOnlyList < BasisLanIpv4Subnet > localIpv4Subnets = null )
179+ IReadOnlyList < BasisLanIpv4Subnet > localIpv4Subnets = null ,
180+ IReadOnlyList < BasisLanIpv6Subnet > localIpv6Subnets = null )
168181 {
169182 advertisement = default ;
170183 address = null ;
@@ -210,7 +223,12 @@ internal static bool TryExtractAdvertisement(
210223 return false ;
211224 }
212225
213- address = SelectAddress ( records , service . Target , remoteAddress , localIpv4Subnets ) ;
226+ address = SelectAddress (
227+ records ,
228+ service . Target ,
229+ remoteAddress ,
230+ localIpv4Subnets ,
231+ localIpv6Subnets ) ;
214232 if ( address == null )
215233 {
216234 return false ;
@@ -294,17 +312,17 @@ private static IPAddress SelectAddress(
294312 IEnumerable < ResourceRecord > records ,
295313 DomainName hostName ,
296314 IPAddress remoteAddress ,
297- IReadOnlyList < BasisLanIpv4Subnet > localIpv4Subnets )
315+ IReadOnlyList < BasisLanIpv4Subnet > localIpv4Subnets ,
316+ IReadOnlyList < BasisLanIpv6Subnet > localIpv6Subnets )
298317 {
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- }
318+ IPAddress selected = BasisLanAddressUtility . IsUsable ( remoteAddress , allowLoopback : true )
319+ ? remoteAddress
320+ : null ;
321+ bool selectedOnLocalSubnet = IsOnLocalSubnet (
322+ selected ,
323+ localIpv4Subnets ,
324+ localIpv6Subnets ) ;
305325
306- IPAddress selected = null ;
307- bool selectedOnLocalSubnet = false ;
308326 foreach ( ResourceRecord record in records )
309327 {
310328 if ( ! ( record is AddressRecord addressRecord )
@@ -314,8 +332,14 @@ private static IPAddress SelectAddress(
314332 continue ;
315333 }
316334
317- IPAddress candidate = RestoreScope ( addressRecord . Address , remoteAddress ) ;
318- bool candidateOnLocalSubnet = IsOnLocalIpv4Subnet ( candidate , localIpv4Subnets ) ;
335+ IPAddress candidate = RestoreScope (
336+ addressRecord . Address ,
337+ remoteAddress ,
338+ localIpv6Subnets ) ;
339+ bool candidateOnLocalSubnet = IsOnLocalSubnet (
340+ candidate ,
341+ localIpv4Subnets ,
342+ localIpv6Subnets ) ;
319343 if ( selected == null
320344 || ( candidateOnLocalSubnet && ! selectedOnLocalSubnet )
321345 || ( candidateOnLocalSubnet == selectedOnLocalSubnet
@@ -327,64 +351,117 @@ private static IPAddress SelectAddress(
327351 }
328352 }
329353
330- if ( selected ? . AddressFamily == AddressFamily . InterNetwork
331- && localIpv4Subnets ? . Count > 0
354+ if ( selected != null
355+ && HasLocalSubnetsForFamily ( selected , localIpv4Subnets , localIpv6Subnets )
332356 && ! selectedOnLocalSubnet )
333357 {
334358 // 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-
339- if ( BasisLanAddressUtility . IsUsable ( remoteAddress , allowLoopback : true ) )
340- {
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 )
345- {
346- return remoteAddress ;
347- }
359+ // virtual/VPN subnet. A later complete response can provide the same-link address.
360+ return null ;
348361 }
349362
350363 return selected ;
351364 }
352365
353- private static bool IsOnLocalIpv4Subnet (
366+ private static bool HasLocalSubnetsForFamily (
367+ IPAddress address ,
368+ IReadOnlyList < BasisLanIpv4Subnet > localIpv4Subnets ,
369+ IReadOnlyList < BasisLanIpv6Subnet > localIpv6Subnets )
370+ {
371+ return address ? . AddressFamily == AddressFamily . InterNetwork
372+ ? localIpv4Subnets ? . Count > 0
373+ : address ? . AddressFamily == AddressFamily . InterNetworkV6
374+ && localIpv6Subnets ? . Count > 0 ;
375+ }
376+
377+ private static bool IsOnLocalSubnet (
354378 IPAddress candidate ,
355- IReadOnlyList < BasisLanIpv4Subnet > localIpv4Subnets )
379+ IReadOnlyList < BasisLanIpv4Subnet > localIpv4Subnets ,
380+ IReadOnlyList < BasisLanIpv6Subnet > localIpv6Subnets )
356381 {
357- if ( candidate ? . AddressFamily != AddressFamily . InterNetwork
358- || localIpv4Subnets == null )
382+ if ( candidate ? . AddressFamily == AddressFamily . InterNetwork )
383+ {
384+ if ( localIpv4Subnets == null )
385+ {
386+ return false ;
387+ }
388+
389+ for ( int index = 0 ; index < localIpv4Subnets . Count ; index ++ )
390+ {
391+ if ( BasisLanAddressUtility . IsOnSameIpv4Subnet (
392+ candidate ,
393+ localIpv4Subnets [ index ] ) )
394+ {
395+ return true ;
396+ }
397+ }
398+ return false ;
399+ }
400+
401+ if ( candidate ? . AddressFamily != AddressFamily . InterNetworkV6
402+ || localIpv6Subnets == null
403+ || ( candidate . IsIPv6LinkLocal && candidate . ScopeId == 0 ) )
359404 {
360405 return false ;
361406 }
362407
363- for ( int index = 0 ; index < localIpv4Subnets . Count ; index ++ )
408+ for ( int index = 0 ; index < localIpv6Subnets . Count ; index ++ )
364409 {
365- if ( BasisLanAddressUtility . IsOnSameIpv4Subnet (
410+ if ( BasisLanAddressUtility . IsOnSameIpv6Subnet (
366411 candidate ,
367- localIpv4Subnets [ index ] ) )
412+ localIpv6Subnets [ index ] ) )
368413 {
369414 return true ;
370415 }
371416 }
372417 return false ;
373418 }
374419
375- private static IPAddress RestoreScope ( IPAddress address , IPAddress remoteAddress )
420+ private static IPAddress RestoreScope (
421+ IPAddress address ,
422+ IPAddress remoteAddress ,
423+ IReadOnlyList < BasisLanIpv6Subnet > localIpv6Subnets )
376424 {
377- if ( address != null
378- && address . AddressFamily == AddressFamily . InterNetworkV6
379- && address . IsIPv6LinkLocal
380- && address . ScopeId == 0
381- && remoteAddress != null
425+ if ( address == null
426+ || address . AddressFamily != AddressFamily . InterNetworkV6
427+ || ! address . IsIPv6LinkLocal
428+ || address . ScopeId != 0 )
429+ {
430+ return address ;
431+ }
432+
433+ if ( remoteAddress != null
382434 && remoteAddress . AddressFamily == AddressFamily . InterNetworkV6
383435 && remoteAddress . ScopeId != 0 )
384436 {
385437 return new IPAddress ( address . GetAddressBytes ( ) , remoteAddress . ScopeId ) ;
386438 }
387- return address ;
439+
440+ long selectedScope = 0 ;
441+ if ( localIpv6Subnets != null )
442+ {
443+ for ( int index = 0 ; index < localIpv6Subnets . Count ; index ++ )
444+ {
445+ BasisLanIpv6Subnet subnet = localIpv6Subnets [ index ] ;
446+ if ( subnet . Address ? . ScopeId == 0
447+ || ! BasisLanAddressUtility . IsOnSameIpv6Subnet ( address , subnet ) )
448+ {
449+ continue ;
450+ }
451+
452+ if ( selectedScope != 0 && selectedScope != subnet . Address . ScopeId )
453+ {
454+ // Multiple interfaces share fe80::/64 and the response did not identify
455+ // which one received it. Leave it unscoped rather than choosing incorrectly.
456+ return address ;
457+ }
458+ selectedScope = subnet . Address . ScopeId ;
459+ }
460+ }
461+
462+ return selectedScope == 0
463+ ? address
464+ : new IPAddress ( address . GetAddressBytes ( ) , selectedScope ) ;
388465 }
389466
390467 public void Dispose ( )
0 commit comments