From 502c0347894659fde8959d2a8eb0bc289d4bbf20 Mon Sep 17 00:00:00 2001 From: nsoto-development Date: Wed, 22 Jul 2026 21:12:15 -0400 Subject: [PATCH 1/2] Fix WoL directed broadcast: use host-bits-set address, not the network address WakeFunctionToAllNics computed the per-interface broadcast by setting the last octet to 0 (x.x.x.0), which is the network address rather than the directed broadcast, and hardcoded a /24. Compute the directed broadcast from the interface's actual prefix length instead (e.g. 192.168.100.255 for a /24). --- Shared/Services/WolService.cs | 36 +++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/Shared/Services/WolService.cs b/Shared/Services/WolService.cs index 2457086..cb22935 100644 --- a/Shared/Services/WolService.cs +++ b/Shared/Services/WolService.cs @@ -78,10 +78,10 @@ private bool WakeFunctionToAllNics(string macAddress, string ipAddressString) 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); + if (uip.PrefixLength is > 0 and < 31) + { + BroadcastWol(uip.Address, GetDirectedBroadcast(uip.Address, uip.PrefixLength), data); + } result = true; } @@ -100,6 +100,34 @@ private bool WakeFunctionToAllNics(string macAddress, string ipAddressString) return result; } + 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. From af4b08a7fd754f30b5dd15098d8df214e49ccb52 Mon Sep 17 00:00:00 2001 From: nsoto-development Date: Wed, 22 Jul 2026 21:13:08 -0400 Subject: [PATCH 2/2] Fix WoL over VPN: target the device's subnet instead of spraying all NICs WakeFunctionToAllNics broadcast the magic packet out of every network interface. With a VPN virtual adapter present (e.g. Proton VPN), packets were sourced from the VPN adapter and routed into the tunnel, so they never reached a device on a directly-connected LAN subnet. When the target device's IP is known, select the local interface whose subnet contains the target (using its actual prefix length) and send bound to that interface using the subnet's directed broadcast. Other interfaces, including VPN adapters, are no longer used. The all-interfaces broadcast is kept as a fallback for when the target IP is unknown or no local interface shares its subnet. --- Shared/Services/WolService.cs | 104 +++++++++++++++++++++++++--------- 1 file changed, 77 insertions(+), 27 deletions(-) diff --git a/Shared/Services/WolService.cs b/Shared/Services/WolService.cs index cb22935..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); - - if (uip.PrefixLength is > 0 and < 31) - { - BroadcastWol(uip.Address, GetDirectedBroadcast(uip.Address, uip.PrefixLength), 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,32 @@ 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();