diff --git a/Shared/Services/WolService.cs b/Shared/Services/WolService.cs index 2457086..8a11280 100644 --- a/Shared/Services/WolService.cs +++ b/Shared/Services/WolService.cs @@ -59,38 +59,62 @@ private bool WakeFunctionToAllNics(string macAddress, string ipAddressString) var physicalAddress = PhysicalAddress.Parse(macAddress); var addressBytes = physicalAddress.GetAddressBytes(); var data = GetWolPacket(addressBytes); - var ipAddress = ipAddressString != null ? IPAddress.Parse(ipAddressString) : null; + var targetAddress = ipAddressString != null ? IPAddress.Parse(ipAddressString) : null; - var interfaces = NetworkInterface.GetAllNetworkInterfaces(); - - interfaces + // Gather every usable IPv4 unicast address across the viable interfaces. + var candidates = NetworkInterface.GetAllNetworkInterfaces() .Where(IsViableWOLInterface) - .Each(ni => + .SelectMany(ni => ni.GetIPProperties().UnicastAddresses + .Where(uip => uip.Address.AddressFamily == AddressFamily.InterNetwork + && !uip.Address.ToString().StartsWith("169.254")) + .Select(uip => (ni, uip))) + .ToList(); + + // Preferred path: when the target IP is known, only broadcast on the interface that + // shares the target's subnet, using that subnet's directed broadcast address. This + // avoids spraying every NIC (e.g. a VPN adapter) which sources the packet from the + // wrong local address and never reaches a directly-connected target. + if (targetAddress != null) + { + var match = candidates.FirstOrDefault(c => IsInSameSubnet(c.uip.Address, targetAddress, c.uip.PrefixLength)); + + if (match.uip != null) + { + var localAddress = match.uip.Address; + var directedBroadcast = GetDirectedBroadcast(localAddress, match.uip.PrefixLength); + Logger.Debug($"Sending WOL on subnet-matched interface: {match.ni.Name} ({match.ni.Description}), local address: {localAddress}, directed broadcast: {directedBroadcast}, target: {targetAddress}"); + + BroadcastWol(localAddress, directedBroadcast, data); + BroadcastWol(localAddress, IPAddress.Broadcast, data); + + return true; + } + + Logger.Debug($"No local interface found on the target's subnet ({targetAddress}); falling back to broadcasting on all interfaces"); + } + + // Fallback: broadcast on every viable interface (target IP unknown, or no local + // interface shares its subnet). + foreach (var (ni, uip) in candidates) + { + try { - foreach (var uip in ni.GetIPProperties().UnicastAddresses) + var localAddress = uip.Address; + Logger.Debug($"Broadcast WOL in network: {ni.Name} ({ni.Description}), local address: {localAddress}, destination IP-address: {ipAddressString ?? "broadcast"}"); + BroadcastWol(localAddress, IPAddress.Broadcast, data); + + if (uip.PrefixLength is > 0 and < 31) { - if (uip.Address.ToString().StartsWith("169.254") || uip.Address.AddressFamily != AddressFamily.InterNetwork) - { - continue; - } - try - { - Logger.Debug($"Broadcast WOL in network: {ni.Name} ({ni.Description}), local address: {uip.Address}, destination IP-address: {ipAddressString ?? "broadcast"}"); - BroadcastWol(uip.Address, IPAddress.Broadcast, data); - - var parts = uip.Address.ToString().Split(".").ToList(); - parts[3] = "0"; - var broadcastAddress = IPAddress.Parse(string.Join(".", parts)); - BroadcastWol(uip.Address, broadcastAddress, data); - - result = true; - } - catch (Exception ex) - { - Logger.Error($"WakeFunctionToAllNics: while sending to specific network: {ni.Name} ({ni.Description}): {ex.ToLogString()}"); - } + BroadcastWol(localAddress, GetDirectedBroadcast(localAddress, uip.PrefixLength), data); } - }); + + result = true; + } + catch (Exception ex) + { + Logger.Error($"WakeFunctionToAllNics: while sending to specific network: {ni.Name} ({ni.Description}): {ex.ToLogString()}"); + } + } } catch (Exception ex) { @@ -100,6 +124,60 @@ private bool WakeFunctionToAllNics(string macAddress, string ipAddressString) return result; } + private static bool IsInSameSubnet(IPAddress local, IPAddress target, int prefixLength) + { + if (local.AddressFamily != AddressFamily.InterNetwork || target.AddressFamily != AddressFamily.InterNetwork) + { + return false; + } + if (prefixLength < 0 || prefixLength > 32) + { + return false; + } + + var localBytes = local.GetAddressBytes(); + var targetBytes = target.GetAddressBytes(); + var maskBytes = GetMaskBytes(prefixLength); + + for (var i = 0; i < 4; i++) + { + if ((localBytes[i] & maskBytes[i]) != (targetBytes[i] & maskBytes[i])) + { + return false; + } + } + + return true; + } + + private static IPAddress GetDirectedBroadcast(IPAddress local, int prefixLength) + { + var localBytes = local.GetAddressBytes(); + var maskBytes = GetMaskBytes(prefixLength); + var broadcastBytes = new byte[4]; + + for (var i = 0; i < 4; i++) + { + broadcastBytes[i] = (byte)(localBytes[i] | (byte)~maskBytes[i]); + } + + return new IPAddress(broadcastBytes); + } + + private static byte[] GetMaskBytes(int prefixLength) + { + // Guard prefixLength 0 explicitly: shifting a uint by 32 is undefined in C#. + var mask = prefixLength == 0 ? 0u : uint.MaxValue << (32 - prefixLength); + + return + [ + (byte)(mask >> 24), + (byte)(mask >> 16), + (byte)(mask >> 8), + (byte)mask + ]; + } + /// is null. /// The length of the array is not 6. /// The length of the array is not 0 or 6.