Matthias Nott
2026-03-08 4c266155785aad5050ebff7211e3d5f9e15c3238
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
/**
 * Wake-on-LAN service — sends a magic packet to wake a sleeping Mac.
 *
 * The magic packet is 6 bytes of 0xFF followed by the target MAC address
 * repeated 16 times, sent as a UDP broadcast on port 9.
 */
import dgram from "react-native-udp";
function parseMac(mac: string): number[] {
  const parts = mac
    .replace(/[:-]/g, "")
    .match(/.{2}/g);
  if (!parts || parts.length !== 6) {
    throw new Error(`Invalid MAC address: ${mac}`);
  }
  return parts.map((h) => parseInt(h, 16));
}
function buildMagicPacket(mac: string): Uint8Array {
  const macBytes = parseMac(mac);
  const packet = new Uint8Array(102);
  // 6 bytes of 0xFF
  for (let i = 0; i < 6; i++) {
    packet[i] = 0xff;
  }
  // MAC address repeated 16 times
  for (let i = 0; i < 16; i++) {
    const offset = 6 + i * 6;
    for (let j = 0; j < 6; j++) {
      packet[offset + j] = macBytes[j];
    }
  }
  return packet;
}
/**
 * Send a Wake-on-LAN magic packet to the given MAC address.
 * Sends to both the subnet broadcast (derived from host) and 255.255.255.255.
 */
export async function sendWol(mac: string, host?: string): Promise<void> {
  const packet = buildMagicPacket(mac);
  // Derive broadcast address from host IP (replace last octet with 255)
  const broadcastAddresses = ["255.255.255.255"];
  if (host) {
    const parts = host.split(".");
    if (parts.length === 4) {
      parts[3] = "255";
      const subnetBroadcast = parts.join(".");
      if (!broadcastAddresses.includes(subnetBroadcast)) {
        broadcastAddresses.push(subnetBroadcast);
      }
    }
  }
  const TIMEOUT_MS = 5000;
  return new Promise<void>((resolve, reject) => {
    let settled = false;
    const settle = (fn: () => void) => {
      if (settled) return;
      settled = true;
      clearTimeout(timer);
      fn();
    };
    const timer = setTimeout(() => {
      settle(() => {
        try { socket.close(); } catch { /* ignore */ }
        reject(new Error("WoL timed out — magic packet may not have been sent"));
      });
    }, TIMEOUT_MS);
    const socket = dgram.createSocket({ type: "udp4" });
    socket.once("error", (err: Error) => {
      settle(() => {
        try { socket.close(); } catch { /* ignore */ }
        reject(err);
      });
    });
    socket.bind(0, () => {
      try {
        socket.setBroadcast(true);
      } catch {
        // Some platforms don't support setBroadcast — continue anyway
      }
      let pending = broadcastAddresses.length;
      for (const addr of broadcastAddresses) {
        socket.send(packet, 0, packet.length, 9, addr, (err?: Error) => {
          if (err) {
            settle(() => {
              try { socket.close(); } catch { /* ignore */ }
              reject(err);
            });
            return;
          }
          pending--;
          if (pending === 0) {
            settle(() => {
              try { socket.close(); } catch { /* ignore */ }
              resolve();
            });
          }
        });
      }
    });
  });
}
/**
 * Validate a MAC address string.
 */
export function isValidMac(mac: string): boolean {
  return /^([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}$/.test(mac.trim());
}