spoof-d 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +250 -0
- package/bin/cmd.js +313 -0
- package/index.js +1115 -0
- package/package.json +57 -0
package/index.js
ADDED
|
@@ -0,0 +1,1115 @@
|
|
|
1
|
+
/*! spoof. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
|
2
|
+
module.exports = {
|
|
3
|
+
findInterface,
|
|
4
|
+
findInterfaces,
|
|
5
|
+
normalize,
|
|
6
|
+
randomize,
|
|
7
|
+
setInterfaceMAC,
|
|
8
|
+
getInterfaceMAC,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const cp = require("child_process");
|
|
12
|
+
const quote = require("shell-quote").quote;
|
|
13
|
+
const zeroFill = require("zero-fill");
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Custom error classes for better error handling
|
|
17
|
+
*/
|
|
18
|
+
class SpoofyError extends Error {
|
|
19
|
+
constructor(message, code, suggestions = []) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "SpoofyError";
|
|
22
|
+
this.code = code;
|
|
23
|
+
this.suggestions = suggestions;
|
|
24
|
+
Error.captureStackTrace(this, this.constructor);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class ValidationError extends SpoofyError {
|
|
29
|
+
constructor(message, suggestions = []) {
|
|
30
|
+
super(message, "VALIDATION_ERROR", suggestions);
|
|
31
|
+
this.name = "ValidationError";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class PermissionError extends SpoofyError {
|
|
36
|
+
constructor(message, suggestions = []) {
|
|
37
|
+
super(message, "PERMISSION_ERROR", suggestions);
|
|
38
|
+
this.name = "PermissionError";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class NetworkError extends SpoofyError {
|
|
43
|
+
constructor(message, suggestions = []) {
|
|
44
|
+
super(message, "NETWORK_ERROR", suggestions);
|
|
45
|
+
this.name = "NetworkError";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class PlatformError extends SpoofyError {
|
|
50
|
+
constructor(message, suggestions = []) {
|
|
51
|
+
super(message, "PLATFORM_ERROR", suggestions);
|
|
52
|
+
this.name = "PlatformError";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Escapes a string for use in PowerShell commands
|
|
58
|
+
* @param {string} str
|
|
59
|
+
* @return {string}
|
|
60
|
+
*/
|
|
61
|
+
function escapePowerShell(str) {
|
|
62
|
+
return str.replace(/'/g, "''").replace(/"/g, '`"');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Executes a command with timeout and better error handling
|
|
67
|
+
* @param {string} command
|
|
68
|
+
* @param {Object} options
|
|
69
|
+
* @param {number} timeout
|
|
70
|
+
* @return {string}
|
|
71
|
+
*/
|
|
72
|
+
function execWithTimeout(command, options = {}, timeout = 30000) {
|
|
73
|
+
try {
|
|
74
|
+
return cp.execSync(command, {
|
|
75
|
+
...options,
|
|
76
|
+
timeout: timeout,
|
|
77
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
|
78
|
+
}).toString();
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (err.signal === "SIGTERM") {
|
|
81
|
+
throw new NetworkError(
|
|
82
|
+
`Command timed out after ${timeout}ms: ${command.substring(0, 50)}...`,
|
|
83
|
+
["Try again with a slower network connection", "Check if the interface is busy"]
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Retries a function with exponential backoff
|
|
92
|
+
* @param {Function} fn
|
|
93
|
+
* @param {number} maxRetries
|
|
94
|
+
* @param {number} delay
|
|
95
|
+
* @return {Promise}
|
|
96
|
+
*/
|
|
97
|
+
function retry(fn, maxRetries = 3, delay = 500) {
|
|
98
|
+
let lastError;
|
|
99
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
100
|
+
try {
|
|
101
|
+
return fn();
|
|
102
|
+
} catch (err) {
|
|
103
|
+
lastError = err;
|
|
104
|
+
if (i < maxRetries - 1) {
|
|
105
|
+
const waitTime = delay * Math.pow(2, i);
|
|
106
|
+
// Simple sleep for sync operations
|
|
107
|
+
const start = Date.now();
|
|
108
|
+
while (Date.now() - start < waitTime) {
|
|
109
|
+
// Busy wait
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
throw lastError;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Parses PowerShell error output for better error messages
|
|
119
|
+
* @param {string} errorOutput
|
|
120
|
+
* @return {string}
|
|
121
|
+
*/
|
|
122
|
+
function parsePowerShellError(errorOutput) {
|
|
123
|
+
if (!errorOutput) return "Unknown PowerShell error";
|
|
124
|
+
|
|
125
|
+
// Try to extract meaningful error messages
|
|
126
|
+
const errorMatch = errorOutput.match(/Error:\s*(.+?)(?:\r?\n|$)/i);
|
|
127
|
+
if (errorMatch) {
|
|
128
|
+
return errorMatch[1].trim();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Try to find exception messages
|
|
132
|
+
const exceptionMatch = errorOutput.match(/Exception:\s*(.+?)(?:\r?\n|$)/i);
|
|
133
|
+
if (exceptionMatch) {
|
|
134
|
+
return exceptionMatch[1].trim();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Return first non-empty line
|
|
138
|
+
const lines = errorOutput.split(/\r?\n/).filter(line => line.trim());
|
|
139
|
+
if (lines.length > 0) {
|
|
140
|
+
return lines[0].trim();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return errorOutput.trim();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Regex to validate a MAC address
|
|
147
|
+
// Example: 00-00-00-00-00-00 or 00:00:00:00:00:00 or 000000000000
|
|
148
|
+
const MAC_ADDRESS_RE =
|
|
149
|
+
/([0-9A-F]{1,2})[:-]?([0-9A-F]{1,2})[:-]?([0-9A-F]{1,2})[:-]?([0-9A-F]{1,2})[:-]?([0-9A-F]{1,2})[:-]?([0-9A-F]{1,2})/i;
|
|
150
|
+
|
|
151
|
+
// Regex to validate a MAC address in cisco-style
|
|
152
|
+
// Example: 0123.4567.89ab
|
|
153
|
+
const CISCO_MAC_ADDRESS_RE =
|
|
154
|
+
/([0-9A-F]{0,4})\.([0-9A-F]{0,4})\.([0-9A-F]{0,4})/i;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Returns the list of interfaces found on this machine as reported by the
|
|
158
|
+
* `networksetup` command.
|
|
159
|
+
* @param {Array.<string>|null} targets
|
|
160
|
+
* @return {Array.<Object>}
|
|
161
|
+
*/
|
|
162
|
+
function findInterfaces(targets) {
|
|
163
|
+
if (!targets) targets = [];
|
|
164
|
+
|
|
165
|
+
targets = targets.map((target) => target.toLowerCase());
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
if (process.platform === "darwin") {
|
|
169
|
+
return findInterfacesDarwin(targets);
|
|
170
|
+
} else if (process.platform === "linux") {
|
|
171
|
+
return findInterfacesLinux(targets);
|
|
172
|
+
} else if (process.platform === "win32") {
|
|
173
|
+
return findInterfacesWin32(targets);
|
|
174
|
+
} else {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`Unsupported platform: ${process.platform}. ` +
|
|
177
|
+
"Supported platforms: darwin (macOS), linux, win32 (Windows)"
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
} catch (err) {
|
|
181
|
+
// Provide better error messages
|
|
182
|
+
if (err.message.includes("spawn") || err.message.includes("ENOENT")) {
|
|
183
|
+
const suggestions = [];
|
|
184
|
+
if (process.platform === "linux") {
|
|
185
|
+
suggestions.push("Install iproute2: sudo apt-get install iproute2 (Debian/Ubuntu) or sudo yum install iproute (RHEL/CentOS)");
|
|
186
|
+
} else if (process.platform === "win32") {
|
|
187
|
+
suggestions.push("Ensure PowerShell is installed and available in PATH");
|
|
188
|
+
}
|
|
189
|
+
throw new PlatformError(
|
|
190
|
+
`Failed to execute system command. ` +
|
|
191
|
+
`Platform: ${process.platform}. ` +
|
|
192
|
+
`Error: ${err.message}`,
|
|
193
|
+
suggestions
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
throw err;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function findInterfacesDarwin(targets) {
|
|
201
|
+
// Parse the output of `networksetup -listallhardwareports` which gives
|
|
202
|
+
// us 3 fields per port:
|
|
203
|
+
// - the port name,
|
|
204
|
+
// - the device associated with this port, if any,
|
|
205
|
+
// - the MAC address, if any, otherwise 'N/A'
|
|
206
|
+
|
|
207
|
+
let output = cp.execSync("networksetup -listallhardwareports").toString();
|
|
208
|
+
|
|
209
|
+
const details = [];
|
|
210
|
+
while (true) {
|
|
211
|
+
const result = /(?:Hardware Port|Device|Ethernet Address): (.+)/.exec(
|
|
212
|
+
output
|
|
213
|
+
);
|
|
214
|
+
if (!result || !result[1]) {
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
details.push(result[1]);
|
|
218
|
+
output = output.slice(result.index + result[1].length);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const interfaces = []; // to return
|
|
222
|
+
|
|
223
|
+
// Split the results into chunks of 3 (for our three fields) and yield
|
|
224
|
+
// those that match `targets`.
|
|
225
|
+
for (let i = 0; i < details.length; i += 3) {
|
|
226
|
+
const port = details[i];
|
|
227
|
+
const device = details[i + 1];
|
|
228
|
+
let address = details[i + 2];
|
|
229
|
+
|
|
230
|
+
address = MAC_ADDRESS_RE.exec(address.toUpperCase());
|
|
231
|
+
if (address) {
|
|
232
|
+
address = normalize(address[0]);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const it = {
|
|
236
|
+
address: address,
|
|
237
|
+
currentAddress: getInterfaceMAC(device),
|
|
238
|
+
device: device,
|
|
239
|
+
port: port,
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
if (targets.length === 0) {
|
|
243
|
+
// Not trying to match anything in particular, return everything.
|
|
244
|
+
interfaces.push(it);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
for (let j = 0; j < targets.length; j++) {
|
|
249
|
+
const target = targets[j];
|
|
250
|
+
if (target === port.toLowerCase() || target === device.toLowerCase()) {
|
|
251
|
+
interfaces.push(it);
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return interfaces;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function findInterfacesLinux(targets) {
|
|
261
|
+
// Use modern `ip link` command instead of deprecated `ifconfig`
|
|
262
|
+
let output;
|
|
263
|
+
try {
|
|
264
|
+
output = cp.execSync("ip -o link show", { stdio: "pipe" }).toString();
|
|
265
|
+
} catch (err) {
|
|
266
|
+
// Fallback to ifconfig if ip command is not available
|
|
267
|
+
try {
|
|
268
|
+
output = cp.execSync("ifconfig", { stdio: "pipe" }).toString();
|
|
269
|
+
return findInterfacesLinuxLegacy(output, targets);
|
|
270
|
+
} catch (err2) {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const interfaces = [];
|
|
276
|
+
const lines = output.split("\n");
|
|
277
|
+
|
|
278
|
+
for (let i = 0; i < lines.length; i++) {
|
|
279
|
+
const line = lines[i];
|
|
280
|
+
// Parse: <index>: <name>: <flags> ... link/ether <mac> ...
|
|
281
|
+
const match = /^\d+:\s+([^:]+):\s+.*?\s+link\/ether\s+([0-9a-f:]+)/i.exec(line);
|
|
282
|
+
if (!match) continue;
|
|
283
|
+
|
|
284
|
+
const device = match[1].trim();
|
|
285
|
+
let address = match[2] ? normalize(match[2]) : null;
|
|
286
|
+
|
|
287
|
+
const it = {
|
|
288
|
+
address: address,
|
|
289
|
+
currentAddress: getInterfaceMAC(device),
|
|
290
|
+
device: device,
|
|
291
|
+
port: device, // Linux doesn't have port names like macOS
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
if (targets.length === 0) {
|
|
295
|
+
interfaces.push(it);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
for (let j = 0; j < targets.length; j++) {
|
|
300
|
+
const target = targets[j];
|
|
301
|
+
if (target === device.toLowerCase()) {
|
|
302
|
+
interfaces.push(it);
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return interfaces;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function findInterfacesLinuxLegacy(output, targets) {
|
|
312
|
+
// Legacy ifconfig parsing (fallback)
|
|
313
|
+
const details = [];
|
|
314
|
+
while (true) {
|
|
315
|
+
const result = /(.*?)HWaddr(.*)/im.exec(output);
|
|
316
|
+
if (!result || !result[1] || !result[2]) {
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
details.push(result[1], result[2]);
|
|
320
|
+
output = output.slice(result.index + result[0].length);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const interfaces = [];
|
|
324
|
+
|
|
325
|
+
for (let i = 0; i < details.length; i += 2) {
|
|
326
|
+
const s = details[i].split(":");
|
|
327
|
+
|
|
328
|
+
let device, port;
|
|
329
|
+
if (s.length >= 2) {
|
|
330
|
+
device = s[0].split(" ")[0];
|
|
331
|
+
port = s[1].trim();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
let address = details[i + 1].trim();
|
|
335
|
+
if (address) {
|
|
336
|
+
address = normalize(address);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const it = {
|
|
340
|
+
address: address,
|
|
341
|
+
currentAddress: getInterfaceMAC(device),
|
|
342
|
+
device: device,
|
|
343
|
+
port: port || device,
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
if (targets.length === 0) {
|
|
347
|
+
interfaces.push(it);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
for (let j = 0; j < targets.length; j++) {
|
|
352
|
+
const target = targets[j];
|
|
353
|
+
if (target === (port || device).toLowerCase() || target === device.toLowerCase()) {
|
|
354
|
+
interfaces.push(it);
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return interfaces;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function findInterfacesWin32(targets) {
|
|
364
|
+
// Use PowerShell Get-NetAdapter for better reliability
|
|
365
|
+
let interfaces = [];
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
const psCommand = `Get-NetAdapter | Select-Object Name, InterfaceDescription, MacAddress, Status | ConvertTo-Json -Compress`;
|
|
369
|
+
const output = cp
|
|
370
|
+
.execSync(
|
|
371
|
+
`powershell -Command "${psCommand}"`,
|
|
372
|
+
{ stdio: "pipe", shell: true }
|
|
373
|
+
)
|
|
374
|
+
.toString()
|
|
375
|
+
.trim();
|
|
376
|
+
|
|
377
|
+
// Parse JSON output
|
|
378
|
+
const adapters = JSON.parse(output);
|
|
379
|
+
const adapterArray = Array.isArray(adapters) ? adapters : [adapters];
|
|
380
|
+
|
|
381
|
+
for (const adapter of adapterArray) {
|
|
382
|
+
if (!adapter || !adapter.Name) continue;
|
|
383
|
+
|
|
384
|
+
const it = {
|
|
385
|
+
address: adapter.MacAddress ? normalize(adapter.MacAddress) : null,
|
|
386
|
+
currentAddress: adapter.MacAddress ? normalize(adapter.MacAddress) : null,
|
|
387
|
+
device: adapter.Name,
|
|
388
|
+
port: adapter.InterfaceDescription || adapter.Name,
|
|
389
|
+
description: adapter.InterfaceDescription,
|
|
390
|
+
status: adapter.Status,
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
if (targets.length === 0) {
|
|
394
|
+
interfaces.push(it);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
for (let j = 0; j < targets.length; j++) {
|
|
399
|
+
const target = targets[j];
|
|
400
|
+
if (
|
|
401
|
+
target === it.port.toLowerCase() ||
|
|
402
|
+
target === it.device.toLowerCase() ||
|
|
403
|
+
(it.description && target === it.description.toLowerCase())
|
|
404
|
+
) {
|
|
405
|
+
interfaces.push(it);
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
} catch (err) {
|
|
411
|
+
// Fallback to ipconfig method if PowerShell fails
|
|
412
|
+
const output = cp.execSync("ipconfig /all", { stdio: "pipe" }).toString();
|
|
413
|
+
const lines = output.split("\n");
|
|
414
|
+
let it = false;
|
|
415
|
+
|
|
416
|
+
for (let i = 0; i < lines.length; i++) {
|
|
417
|
+
// Check if new device
|
|
418
|
+
let result;
|
|
419
|
+
if (lines[i].substr(0, 1).match(/[A-Z]/)) {
|
|
420
|
+
if (it) {
|
|
421
|
+
if (targets.length === 0) {
|
|
422
|
+
interfaces.push(it);
|
|
423
|
+
} else {
|
|
424
|
+
for (let j = 0; j < targets.length; j++) {
|
|
425
|
+
const target = targets[j];
|
|
426
|
+
if (
|
|
427
|
+
target === it.port.toLowerCase() ||
|
|
428
|
+
target === it.device.toLowerCase()
|
|
429
|
+
) {
|
|
430
|
+
interfaces.push(it);
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
it = {
|
|
438
|
+
port: "",
|
|
439
|
+
device: "",
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
result = /adapter (.+?):/.exec(lines[i]);
|
|
443
|
+
if (!result) {
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
it.device = result[1];
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (!it) {
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Try to find address
|
|
455
|
+
result = /Physical Address.+?:(.*)/im.exec(lines[i]);
|
|
456
|
+
if (result) {
|
|
457
|
+
it.address = normalize(result[1].trim());
|
|
458
|
+
it.currentAddress = it.address;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Try to find description
|
|
463
|
+
result = /description.+?:(.*)/im.exec(lines[i]);
|
|
464
|
+
if (result) {
|
|
465
|
+
it.description = result[1].trim();
|
|
466
|
+
it.port = it.description || it.device;
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Add the last interface
|
|
472
|
+
if (it) {
|
|
473
|
+
if (targets.length === 0) {
|
|
474
|
+
interfaces.push(it);
|
|
475
|
+
} else {
|
|
476
|
+
for (let j = 0; j < targets.length; j++) {
|
|
477
|
+
const target = targets[j];
|
|
478
|
+
if (
|
|
479
|
+
target === it.port.toLowerCase() ||
|
|
480
|
+
target === it.device.toLowerCase()
|
|
481
|
+
) {
|
|
482
|
+
interfaces.push(it);
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return interfaces;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Returns the first interface which matches `target`
|
|
495
|
+
* @param {string} target
|
|
496
|
+
* @return {Object}
|
|
497
|
+
*/
|
|
498
|
+
function findInterface(target) {
|
|
499
|
+
const interfaces = findInterfaces([target]);
|
|
500
|
+
return interfaces && interfaces[0];
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Returns currently-set MAC address of given interface. This is distinct from the
|
|
505
|
+
* interface's hardware MAC address.
|
|
506
|
+
* @return {string}
|
|
507
|
+
*/
|
|
508
|
+
function getInterfaceMAC(device) {
|
|
509
|
+
if (process.platform === "darwin" || process.platform === "linux") {
|
|
510
|
+
let output;
|
|
511
|
+
try {
|
|
512
|
+
output = cp
|
|
513
|
+
.execSync(quote(["ifconfig", device]), { stdio: "pipe" })
|
|
514
|
+
.toString();
|
|
515
|
+
} catch (err) {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const address = MAC_ADDRESS_RE.exec(output);
|
|
520
|
+
return address && normalize(address[0]);
|
|
521
|
+
} else if (process.platform === "win32") {
|
|
522
|
+
// Use PowerShell to get current MAC address
|
|
523
|
+
try {
|
|
524
|
+
const escapedDevice = escapePowerShell(device);
|
|
525
|
+
const psCommand = `Get-NetAdapter -Name '${escapedDevice}' | Select-Object -ExpandProperty MacAddress`;
|
|
526
|
+
const output = cp
|
|
527
|
+
.execSync(
|
|
528
|
+
`powershell -Command "${psCommand}"`,
|
|
529
|
+
{ stdio: "pipe", shell: true }
|
|
530
|
+
)
|
|
531
|
+
.toString()
|
|
532
|
+
.trim();
|
|
533
|
+
|
|
534
|
+
if (output) {
|
|
535
|
+
return normalize(output);
|
|
536
|
+
}
|
|
537
|
+
} catch (err) {
|
|
538
|
+
// Fallback to ipconfig method
|
|
539
|
+
try {
|
|
540
|
+
const output = cp
|
|
541
|
+
.execSync(`ipconfig /all`, { stdio: "pipe" })
|
|
542
|
+
.toString();
|
|
543
|
+
const regex = new RegExp(
|
|
544
|
+
`adapter ${device.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}:[\\s\\S]*?Physical Address[\\s\\S]*?:\\s*([0-9A-F-]+)`,
|
|
545
|
+
"i"
|
|
546
|
+
);
|
|
547
|
+
const match = regex.exec(output);
|
|
548
|
+
if (match && match[1]) {
|
|
549
|
+
return normalize(match[1]);
|
|
550
|
+
}
|
|
551
|
+
} catch (err2) {
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Sets the mac address for given `device` to `mac`.
|
|
561
|
+
*
|
|
562
|
+
* Device varies by platform:
|
|
563
|
+
* OS X, Linux: this is the interface name in ifconfig
|
|
564
|
+
* Windows: this is the network adapter name in ipconfig
|
|
565
|
+
*
|
|
566
|
+
* @param {string} device
|
|
567
|
+
* @param {string} mac
|
|
568
|
+
* @param {string=} port
|
|
569
|
+
*/
|
|
570
|
+
function setInterfaceMAC(device, mac, port) {
|
|
571
|
+
// Validate MAC address format
|
|
572
|
+
if (!mac || typeof mac !== "string") {
|
|
573
|
+
throw new ValidationError("MAC address must be a non-empty string");
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const normalizedMac = normalize(mac);
|
|
577
|
+
if (!normalizedMac) {
|
|
578
|
+
throw new ValidationError(
|
|
579
|
+
`"${mac}" is not a valid MAC address. ` +
|
|
580
|
+
"Expected format: XX:XX:XX:XX:XX:XX or XX-XX-XX-XX-XX-XX",
|
|
581
|
+
[
|
|
582
|
+
"Use colons (:) or dashes (-) as separators",
|
|
583
|
+
"Each byte must be a valid hexadecimal value (00-FF)",
|
|
584
|
+
"Example: 00:11:22:33:44:55"
|
|
585
|
+
]
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Validate MAC address is not all zeros or broadcast
|
|
590
|
+
if (normalizedMac === "00:00:00:00:00:00" || normalizedMac === "FF:FF:FF:FF:FF:FF") {
|
|
591
|
+
throw new ValidationError(
|
|
592
|
+
`"${normalizedMac}" is not a valid MAC address (cannot be all zeros or broadcast address)`,
|
|
593
|
+
["Generate a random MAC address using: spoofy randomize"]
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Validate device name
|
|
598
|
+
if (!device || typeof device !== "string" || device.trim().length === 0) {
|
|
599
|
+
throw new ValidationError(
|
|
600
|
+
"Device name must be a non-empty string",
|
|
601
|
+
["List available devices using: spoofy list"]
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Use normalized MAC address
|
|
606
|
+
mac = normalizedMac;
|
|
607
|
+
|
|
608
|
+
const isWirelessPort = port && port.toLowerCase() === "wi-fi";
|
|
609
|
+
|
|
610
|
+
if (process.platform === "darwin") {
|
|
611
|
+
let macChangeError = null;
|
|
612
|
+
|
|
613
|
+
if (isWirelessPort) {
|
|
614
|
+
// On modern macOS (Sequoia 15.4+, Tahoe 26+), WiFi MAC can only be changed
|
|
615
|
+
// in the brief window after WiFi is powered on but before it connects to a network.
|
|
616
|
+
// We must NOT use ifconfig down as it causes "Network is down" errors.
|
|
617
|
+
try {
|
|
618
|
+
cp.execSync(quote(["networksetup", "-setairportpower", device, "off"]));
|
|
619
|
+
cp.execSync(quote(["networksetup", "-setairportpower", device, "on"]));
|
|
620
|
+
// Change MAC immediately in the window before auto-join
|
|
621
|
+
cp.execFileSync("ifconfig", [device, "ether", mac]);
|
|
622
|
+
} catch (err) {
|
|
623
|
+
macChangeError = err;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
try {
|
|
627
|
+
cp.execSync(quote(["networksetup", "-detectnewhardware"]));
|
|
628
|
+
} catch (err) {
|
|
629
|
+
// Ignore
|
|
630
|
+
}
|
|
631
|
+
} else {
|
|
632
|
+
// Non-WiFi interfaces: standard down/change/up sequence
|
|
633
|
+
try {
|
|
634
|
+
cp.execFileSync("ifconfig", [device, "down"]);
|
|
635
|
+
} catch (err) {
|
|
636
|
+
macChangeError = new Error(
|
|
637
|
+
"Unable to bring interface down: " + err.message
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (!macChangeError) {
|
|
642
|
+
try {
|
|
643
|
+
cp.execFileSync("ifconfig", [device, "ether", mac]);
|
|
644
|
+
} catch (err) {
|
|
645
|
+
macChangeError = err;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
try {
|
|
650
|
+
cp.execFileSync("ifconfig", [device, "up"]);
|
|
651
|
+
} catch (err) {
|
|
652
|
+
if (!macChangeError) {
|
|
653
|
+
macChangeError = new Error(
|
|
654
|
+
"Unable to bring interface up: " + err.message
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (macChangeError) {
|
|
661
|
+
// Verify if the change actually took effect
|
|
662
|
+
const newMac = getInterfaceMAC(device);
|
|
663
|
+
if (newMac && newMac.toLowerCase() === mac.toLowerCase()) {
|
|
664
|
+
// Change succeeded despite error
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
throw new NetworkError(
|
|
669
|
+
`Unable to change MAC address on ${device}: ${macChangeError.message}`,
|
|
670
|
+
[
|
|
671
|
+
"Ensure you have root privileges (use sudo)",
|
|
672
|
+
"On macOS, you may need to disconnect from WiFi networks first",
|
|
673
|
+
"Try disabling and re-enabling the interface manually",
|
|
674
|
+
"Some network adapters may not support MAC address changes (hardware limitation)"
|
|
675
|
+
]
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Verify the change took effect
|
|
680
|
+
const newMac = getInterfaceMAC(device);
|
|
681
|
+
if (newMac && newMac.toLowerCase() !== mac.toLowerCase()) {
|
|
682
|
+
throw new NetworkError(
|
|
683
|
+
`MAC address change verification failed. Expected ${mac}, but got ${newMac}`,
|
|
684
|
+
[
|
|
685
|
+
"The change may not have taken effect",
|
|
686
|
+
"Try running the command again",
|
|
687
|
+
"On macOS, you may need to reconnect to WiFi after the change"
|
|
688
|
+
]
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
} else if (process.platform === "linux") {
|
|
692
|
+
// Modern Linux support using ip link commands
|
|
693
|
+
let macChangeError = null;
|
|
694
|
+
|
|
695
|
+
try {
|
|
696
|
+
// Bring interface down
|
|
697
|
+
cp.execFileSync("ip", ["link", "set", device, "down"]);
|
|
698
|
+
} catch (err) {
|
|
699
|
+
macChangeError = new Error(
|
|
700
|
+
"Unable to bring interface down: " + err.message
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (!macChangeError) {
|
|
705
|
+
try {
|
|
706
|
+
// Set MAC address using ip link
|
|
707
|
+
cp.execFileSync("ip", ["link", "set", device, "address", mac]);
|
|
708
|
+
} catch (err) {
|
|
709
|
+
macChangeError = err;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
try {
|
|
714
|
+
// Bring interface back up
|
|
715
|
+
cp.execFileSync("ip", ["link", "set", device, "up"]);
|
|
716
|
+
} catch (err) {
|
|
717
|
+
if (!macChangeError) {
|
|
718
|
+
macChangeError = new Error(
|
|
719
|
+
"Unable to bring interface up: " + err.message
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (macChangeError) {
|
|
725
|
+
// Verify if the change actually took effect
|
|
726
|
+
const newMac = getInterfaceMAC(device);
|
|
727
|
+
if (newMac && newMac.toLowerCase() === mac.toLowerCase()) {
|
|
728
|
+
// Change succeeded despite error
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
throw new NetworkError(
|
|
733
|
+
`Unable to change MAC address on ${device}: ${macChangeError.message}`,
|
|
734
|
+
[
|
|
735
|
+
"Ensure you have root privileges (use sudo)",
|
|
736
|
+
"Check if the interface is currently in use",
|
|
737
|
+
"Some network adapters may not support MAC address changes"
|
|
738
|
+
]
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Verify the change took effect
|
|
743
|
+
const newMac = getInterfaceMAC(device);
|
|
744
|
+
if (newMac && newMac.toLowerCase() !== mac.toLowerCase()) {
|
|
745
|
+
throw new NetworkError(
|
|
746
|
+
`MAC address change verification failed. Expected ${mac}, but got ${newMac}`,
|
|
747
|
+
[
|
|
748
|
+
"The change may not have taken effect",
|
|
749
|
+
"Try running the command again",
|
|
750
|
+
"Some adapters require a restart to apply MAC changes"
|
|
751
|
+
]
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
} else if (process.platform === "win32") {
|
|
755
|
+
// Windows support using PowerShell and registry
|
|
756
|
+
let macChangeError = null;
|
|
757
|
+
|
|
758
|
+
// Convert MAC address to Windows format (no colons, no dashes)
|
|
759
|
+
const macNoSeparators = mac.replace(/[:-]/g, "");
|
|
760
|
+
|
|
761
|
+
try {
|
|
762
|
+
// Method 1: Try using PowerShell Set-NetAdapter (Windows 8+)
|
|
763
|
+
const escapedDevice = escapePowerShell(device);
|
|
764
|
+
const escapedMac = escapePowerShell(mac);
|
|
765
|
+
const psCommand = `$ErrorActionPreference = 'Stop'; try { $adapter = Get-NetAdapter -Name '${escapedDevice}' -ErrorAction Stop; if ($adapter) { $adapter | Set-NetAdapter -MacAddress '${escapedMac}' -ErrorAction Stop; Write-Host 'Success' } else { throw 'Adapter not found: ${escapedDevice}' } } catch { Write-Error $_.Exception.Message; exit 1 }`;
|
|
766
|
+
try {
|
|
767
|
+
execWithTimeout(
|
|
768
|
+
`powershell -Command "${psCommand}"`,
|
|
769
|
+
{ shell: true },
|
|
770
|
+
30000
|
|
771
|
+
);
|
|
772
|
+
} catch (err) {
|
|
773
|
+
const errorMsg = parsePowerShellError(err.stderr ? err.stderr.toString() : err.message);
|
|
774
|
+
// Method 2: Fallback to registry method
|
|
775
|
+
// Get adapter registry path
|
|
776
|
+
const getGuidCommand = `$ErrorActionPreference = 'Stop'; try { Get-NetAdapter -Name '${escapedDevice}' -ErrorAction Stop | Select-Object -ExpandProperty InterfaceGuid } catch { Write-Error $_.Exception.Message; exit 1 }`;
|
|
777
|
+
let guidOutput;
|
|
778
|
+
try {
|
|
779
|
+
guidOutput = execWithTimeout(
|
|
780
|
+
`powershell -Command "${getGuidCommand}"`,
|
|
781
|
+
{ shell: true },
|
|
782
|
+
30000
|
|
783
|
+
).toString().trim();
|
|
784
|
+
} catch (err) {
|
|
785
|
+
const errorMsg = parsePowerShellError(err.stderr ? err.stderr.toString() : err.message);
|
|
786
|
+
throw new NetworkError(
|
|
787
|
+
`Could not find adapter "${device}": ${errorMsg}`,
|
|
788
|
+
[
|
|
789
|
+
"List available adapters using: spoofy list",
|
|
790
|
+
"Ensure the adapter name is correct (case-sensitive)",
|
|
791
|
+
"Check if the adapter is enabled"
|
|
792
|
+
]
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (!guidOutput || guidOutput.toLowerCase().includes("error")) {
|
|
797
|
+
throw new NetworkError(
|
|
798
|
+
`Could not find adapter GUID for "${device}"`,
|
|
799
|
+
[
|
|
800
|
+
"The adapter may not exist or may be disabled",
|
|
801
|
+
"List available adapters using: spoofy list"
|
|
802
|
+
]
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Disable adapter
|
|
807
|
+
const disableCommand = `$ErrorActionPreference = 'Stop'; try { Disable-NetAdapter -Name '${escapedDevice}' -Confirm:$false -ErrorAction Stop } catch { Write-Error $_.Exception.Message; exit 1 }`;
|
|
808
|
+
try {
|
|
809
|
+
execWithTimeout(
|
|
810
|
+
`powershell -Command "${disableCommand}"`,
|
|
811
|
+
{ shell: true },
|
|
812
|
+
30000
|
|
813
|
+
);
|
|
814
|
+
} catch (err) {
|
|
815
|
+
const errorMsg = parsePowerShellError(err.stderr ? err.stderr.toString() : err.message);
|
|
816
|
+
throw new NetworkError(
|
|
817
|
+
`Could not disable adapter "${device}": ${errorMsg}`,
|
|
818
|
+
[
|
|
819
|
+
"Ensure you have Administrator privileges",
|
|
820
|
+
"The adapter may be in use by another application"
|
|
821
|
+
]
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Set MAC address in registry
|
|
826
|
+
const registryPath = `HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Class\\{4d36e972-e325-11ce-bfc1-08002be10318}`;
|
|
827
|
+
const escapedGuid = escapePowerShell(guidOutput);
|
|
828
|
+
const findGuidCommand = `$ErrorActionPreference = 'Stop'; try { $path = '${registryPath}'; Get-ChildItem -Path $path -ErrorAction Stop | Where-Object { (Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue).NetCfgInstanceId -eq '${escapedGuid}' } | Select-Object -ExpandProperty PSPath } catch { Write-Error $_.Exception.Message; exit 1 }`;
|
|
829
|
+
let adapterPath;
|
|
830
|
+
try {
|
|
831
|
+
adapterPath = execWithTimeout(
|
|
832
|
+
`powershell -Command "${findGuidCommand}"`,
|
|
833
|
+
{ shell: true },
|
|
834
|
+
30000
|
|
835
|
+
).toString().trim();
|
|
836
|
+
} catch (err) {
|
|
837
|
+
// Re-enable adapter before throwing error
|
|
838
|
+
try {
|
|
839
|
+
execWithTimeout(
|
|
840
|
+
`powershell -Command "Enable-NetAdapter -Name '${escapedDevice}' -Confirm:$false"`,
|
|
841
|
+
{ shell: true },
|
|
842
|
+
10000
|
|
843
|
+
);
|
|
844
|
+
} catch (e) {
|
|
845
|
+
// Ignore re-enable errors
|
|
846
|
+
}
|
|
847
|
+
const errorMsg = parsePowerShellError(err.stderr ? err.stderr.toString() : err.message);
|
|
848
|
+
throw new NetworkError(
|
|
849
|
+
`Could not find adapter registry path: ${errorMsg}`,
|
|
850
|
+
[
|
|
851
|
+
"The adapter may not support MAC address changes",
|
|
852
|
+
"Try using the Set-NetAdapter method instead"
|
|
853
|
+
]
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
if (!adapterPath || adapterPath.toLowerCase().includes("error")) {
|
|
858
|
+
// Re-enable adapter before throwing error
|
|
859
|
+
try {
|
|
860
|
+
execWithTimeout(
|
|
861
|
+
`powershell -Command "Enable-NetAdapter -Name '${escapedDevice}' -Confirm:$false"`,
|
|
862
|
+
{ shell: true },
|
|
863
|
+
10000
|
|
864
|
+
);
|
|
865
|
+
} catch (e) {
|
|
866
|
+
// Ignore re-enable errors
|
|
867
|
+
}
|
|
868
|
+
throw new NetworkError(
|
|
869
|
+
`Could not find adapter registry path for "${device}"`,
|
|
870
|
+
[
|
|
871
|
+
"The adapter may not support MAC address changes via registry",
|
|
872
|
+
"Try using a different method or adapter"
|
|
873
|
+
]
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Set NetworkAddress registry value
|
|
878
|
+
const escapedPath = escapePowerShell(adapterPath);
|
|
879
|
+
const setMacCommand = `$ErrorActionPreference = 'Stop'; try { Set-ItemProperty -Path '${escapedPath}' -Name 'NetworkAddress' -Value '${macNoSeparators}' -ErrorAction Stop } catch { Write-Error $_.Exception.Message; exit 1 }`;
|
|
880
|
+
try {
|
|
881
|
+
execWithTimeout(
|
|
882
|
+
`powershell -Command "${setMacCommand}"`,
|
|
883
|
+
{ shell: true },
|
|
884
|
+
30000
|
|
885
|
+
);
|
|
886
|
+
} catch (err) {
|
|
887
|
+
// Re-enable adapter before throwing error
|
|
888
|
+
try {
|
|
889
|
+
execWithTimeout(
|
|
890
|
+
`powershell -Command "Enable-NetAdapter -Name '${escapedDevice}' -Confirm:$false"`,
|
|
891
|
+
{ shell: true },
|
|
892
|
+
10000
|
|
893
|
+
);
|
|
894
|
+
} catch (e) {
|
|
895
|
+
// Ignore re-enable errors
|
|
896
|
+
}
|
|
897
|
+
const errorMsg = parsePowerShellError(err.stderr ? err.stderr.toString() : err.message);
|
|
898
|
+
throw new NetworkError(
|
|
899
|
+
`Could not set MAC address in registry: ${errorMsg}`,
|
|
900
|
+
[
|
|
901
|
+
"Ensure you have Administrator privileges",
|
|
902
|
+
"The adapter may not support MAC address changes"
|
|
903
|
+
]
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Enable adapter
|
|
908
|
+
const enableCommand = `$ErrorActionPreference = 'Stop'; try { Enable-NetAdapter -Name '${escapedDevice}' -Confirm:$false -ErrorAction Stop } catch { Write-Error $_.Exception.Message; exit 1 }`;
|
|
909
|
+
try {
|
|
910
|
+
execWithTimeout(
|
|
911
|
+
`powershell -Command "${enableCommand}"`,
|
|
912
|
+
{ shell: true },
|
|
913
|
+
30000
|
|
914
|
+
);
|
|
915
|
+
} catch (err) {
|
|
916
|
+
const errorMsg = parsePowerShellError(err.stderr ? err.stderr.toString() : err.message);
|
|
917
|
+
throw new NetworkError(
|
|
918
|
+
`Could not re-enable adapter "${device}": ${errorMsg}. ` +
|
|
919
|
+
"The adapter may need to be enabled manually.",
|
|
920
|
+
[
|
|
921
|
+
"Try enabling the adapter manually from Network Settings",
|
|
922
|
+
"The MAC address may have been changed but adapter is disabled"
|
|
923
|
+
]
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
} catch (err) {
|
|
928
|
+
macChangeError = err;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
if (macChangeError) {
|
|
932
|
+
// Verify if the change actually took effect
|
|
933
|
+
const newMac = getInterfaceMAC(device);
|
|
934
|
+
if (newMac && newMac.toLowerCase() === mac.toLowerCase()) {
|
|
935
|
+
// Change succeeded despite error
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const suggestions = [
|
|
940
|
+
"Ensure you are running as Administrator",
|
|
941
|
+
"Some network adapters may not support MAC address changes (hardware limitation)",
|
|
942
|
+
"Try disabling and re-enabling the adapter manually",
|
|
943
|
+
"Check if the adapter is in use by another application"
|
|
944
|
+
];
|
|
945
|
+
|
|
946
|
+
if (macChangeError.message && macChangeError.message.includes("Permission")) {
|
|
947
|
+
suggestions.unshift("Right-click PowerShell/CMD and select 'Run as Administrator'");
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
throw new NetworkError(
|
|
951
|
+
`Unable to change MAC address on "${device}": ${macChangeError.message}`,
|
|
952
|
+
suggestions
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Verify the change took effect (with retry for Windows)
|
|
957
|
+
let newMac;
|
|
958
|
+
try {
|
|
959
|
+
newMac = retry(() => getInterfaceMAC(device), 3, 1000);
|
|
960
|
+
} catch (err) {
|
|
961
|
+
// If we can't verify, assume it worked (better than failing)
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
if (newMac && newMac.toLowerCase() !== mac.toLowerCase()) {
|
|
966
|
+
throw new NetworkError(
|
|
967
|
+
`MAC address change verification failed. Expected ${mac}, but got ${newMac}`,
|
|
968
|
+
[
|
|
969
|
+
"The change may not have taken effect",
|
|
970
|
+
"Try running the command again",
|
|
971
|
+
"Some adapters require a restart to apply MAC changes",
|
|
972
|
+
"The adapter may not support MAC address changes"
|
|
973
|
+
]
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* Generates and returns a random MAC address.
|
|
981
|
+
* @param {boolean} localAdmin locally administered address
|
|
982
|
+
* @return {string}
|
|
983
|
+
*/
|
|
984
|
+
function randomize(localAdmin) {
|
|
985
|
+
// Randomly assign a VM vendor's MAC address prefix, which should
|
|
986
|
+
// decrease chance of colliding with existing device's addresses.
|
|
987
|
+
|
|
988
|
+
const vendors = [
|
|
989
|
+
[0x00, 0x05, 0x69], // VMware
|
|
990
|
+
[0x00, 0x50, 0x56], // VMware
|
|
991
|
+
[0x00, 0x0c, 0x29], // VMware
|
|
992
|
+
[0x00, 0x16, 0x3e], // Xen
|
|
993
|
+
[0x00, 0x03, 0xff], // Microsoft Hyper-V, Virtual Server, Virtual PC
|
|
994
|
+
[0x00, 0x1c, 0x42], // Parallels
|
|
995
|
+
[0x00, 0x0f, 0x4b], // Virtual Iron 4
|
|
996
|
+
[0x08, 0x00, 0x27], // Sun Virtual Box
|
|
997
|
+
];
|
|
998
|
+
|
|
999
|
+
// Windows needs specific prefixes sometimes
|
|
1000
|
+
// http://www.wikihow.com/Change-a-Computer's-Mac-Address-in-Windows
|
|
1001
|
+
const windowsPrefixes = ["D2", "D6", "DA", "DE"];
|
|
1002
|
+
|
|
1003
|
+
const vendor = vendors[random(0, vendors.length - 1)];
|
|
1004
|
+
|
|
1005
|
+
if (process.platform === "win32") {
|
|
1006
|
+
// Parse hex string to number (fix for Windows randomize bug)
|
|
1007
|
+
vendor[0] = parseInt(windowsPrefixes[random(0, 3)], 16);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const mac = [
|
|
1011
|
+
vendor[0],
|
|
1012
|
+
vendor[1],
|
|
1013
|
+
vendor[2],
|
|
1014
|
+
random(0x00, 0x7f),
|
|
1015
|
+
random(0x00, 0xff),
|
|
1016
|
+
random(0x00, 0xff),
|
|
1017
|
+
];
|
|
1018
|
+
|
|
1019
|
+
if (localAdmin) {
|
|
1020
|
+
// Universally administered and locally administered addresses are
|
|
1021
|
+
// distinguished by setting the second least significant bit of the
|
|
1022
|
+
// most significant byte of the address. If the bit is 0, the address
|
|
1023
|
+
// is universally administered. If it is 1, the address is locally
|
|
1024
|
+
// administered. In the example address 02-00-00-00-00-01 the most
|
|
1025
|
+
// significant byte is 02h. The binary is 00000010 and the second
|
|
1026
|
+
// least significant bit is 1. Therefore, it is a locally administered
|
|
1027
|
+
// address.[3] The bit is 0 in all OUIs.
|
|
1028
|
+
mac[0] |= 2;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
return mac
|
|
1032
|
+
.map((byte) => zeroFill(2, byte.toString(16)))
|
|
1033
|
+
.join(":")
|
|
1034
|
+
.toUpperCase();
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* Takes a MAC address in various formats:
|
|
1039
|
+
*
|
|
1040
|
+
* - 00:00:00:00:00:00,
|
|
1041
|
+
* - 00-00-00-00-00-00,
|
|
1042
|
+
* - 0000.0000.0000
|
|
1043
|
+
*
|
|
1044
|
+
* ... and returns it in the format 00:00:00:00:00:00.
|
|
1045
|
+
*
|
|
1046
|
+
* @param {string} mac
|
|
1047
|
+
* @return {string}
|
|
1048
|
+
*/
|
|
1049
|
+
function normalize(mac) {
|
|
1050
|
+
if (!mac || typeof mac !== "string") {
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// Remove whitespace
|
|
1055
|
+
mac = mac.trim();
|
|
1056
|
+
|
|
1057
|
+
if (mac.length === 0) {
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Try Cisco format first (e.g., 0123.4567.89ab)
|
|
1062
|
+
let m = CISCO_MAC_ADDRESS_RE.exec(mac);
|
|
1063
|
+
if (m) {
|
|
1064
|
+
const halfwords = m.slice(1);
|
|
1065
|
+
// Validate all halfwords are present
|
|
1066
|
+
if (halfwords.length === 3 && halfwords.every(hw => hw && hw.length > 0)) {
|
|
1067
|
+
mac = halfwords
|
|
1068
|
+
.map((halfword) => {
|
|
1069
|
+
return zeroFill(4, halfword);
|
|
1070
|
+
})
|
|
1071
|
+
.join("");
|
|
1072
|
+
if (mac.length === 12) {
|
|
1073
|
+
return chunk(mac, 2).join(":").toUpperCase();
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// Try standard MAC format (e.g., 00:11:22:33:44:55 or 00-11-22-33-44-55)
|
|
1079
|
+
m = MAC_ADDRESS_RE.exec(mac);
|
|
1080
|
+
if (m) {
|
|
1081
|
+
const bytes = m.slice(1);
|
|
1082
|
+
// Validate we have exactly 6 bytes
|
|
1083
|
+
if (bytes.length === 6 && bytes.every(byte => byte && byte.length > 0)) {
|
|
1084
|
+
const normalized = bytes
|
|
1085
|
+
.map((byte) => zeroFill(2, byte))
|
|
1086
|
+
.join(":")
|
|
1087
|
+
.toUpperCase();
|
|
1088
|
+
|
|
1089
|
+
// Final validation: should be exactly 17 characters (6 bytes + 5 colons)
|
|
1090
|
+
if (normalized.length === 17) {
|
|
1091
|
+
return normalized;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function chunk(str, n) {
|
|
1100
|
+
const arr = [];
|
|
1101
|
+
for (let i = 0; i < str.length; i += n) {
|
|
1102
|
+
arr.push(str.slice(i, i + n));
|
|
1103
|
+
}
|
|
1104
|
+
return arr;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Return a random integer between min and max (inclusive).
|
|
1109
|
+
* @param {number} min
|
|
1110
|
+
* @param {number} max
|
|
1111
|
+
* @return {number}
|
|
1112
|
+
*/
|
|
1113
|
+
function random(min, max) {
|
|
1114
|
+
return min + Math.floor(Math.random() * (max - min + 1));
|
|
1115
|
+
}
|