@bun-win32/netdiag 1.0.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/AI.md +99 -0
- package/README.md +79 -0
- package/adapters.ts +123 -0
- package/addr.ts +83 -0
- package/arp.ts +62 -0
- package/constants.ts +75 -0
- package/dns.ts +161 -0
- package/extras.ts +76 -0
- package/index.ts +13 -0
- package/package.json +85 -0
- package/ping.ts +193 -0
- package/routes.ts +80 -0
- package/sockets.ts +179 -0
- package/stats.ts +220 -0
- package/traceroute.ts +35 -0
- package/wifi.ts +363 -0
- package/win32.ts +144 -0
package/wifi.ts
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { type Pointer, toArrayBuffer } from 'bun:ffi';
|
|
2
|
+
|
|
3
|
+
import { macFromBytes } from './addr';
|
|
4
|
+
import { readWideAt, Win32Error, Wlanapi } from './win32';
|
|
5
|
+
|
|
6
|
+
const WLAN_API_VERSION_2_0 = 0x0000_0002;
|
|
7
|
+
const WLAN_INTF_OPCODE_CURRENT_CONNECTION = 0x0000_0007;
|
|
8
|
+
const WLAN_CONNECTION_MODE_PROFILE = 0x0000_0000;
|
|
9
|
+
const DOT11_BSS_TYPE_INFRASTRUCTURE = 0x0000_0001;
|
|
10
|
+
const DOT11_BSS_TYPE_ANY = 0x0000_0003;
|
|
11
|
+
const INTERFACE_ENTRY_SIZE = 532; // WLAN_INTERFACE_INFO
|
|
12
|
+
const NETWORK_ENTRY_SIZE = 628; // WLAN_AVAILABLE_NETWORK
|
|
13
|
+
const LIST_HEADER_SIZE = 8; // dwNumberOfItems + dwIndex
|
|
14
|
+
const CONNECTED_FLAG = 0x0000_0001;
|
|
15
|
+
const HAS_PROFILE_FLAG = 0x0000_0002;
|
|
16
|
+
const ERROR_ACCESS_DENIED = 5;
|
|
17
|
+
const SCAN_SETTLE_MS = 4000; // WlanScan may flush the cached list; MSDN-sanctioned settle window
|
|
18
|
+
|
|
19
|
+
const INTERFACE_STATE_NAMES: ReadonlyMap<number, string> = new Map([
|
|
20
|
+
[0, 'not-ready'],
|
|
21
|
+
[1, 'connected'],
|
|
22
|
+
[2, 'ad-hoc'],
|
|
23
|
+
[3, 'disconnecting'],
|
|
24
|
+
[4, 'disconnected'],
|
|
25
|
+
[5, 'associating'],
|
|
26
|
+
[6, 'discovering'],
|
|
27
|
+
[7, 'authenticating'],
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const AUTH_NAMES: ReadonlyMap<number, string> = new Map([
|
|
31
|
+
[1, 'open'],
|
|
32
|
+
[2, 'shared-key'],
|
|
33
|
+
[3, 'wpa'],
|
|
34
|
+
[4, 'wpa-psk'],
|
|
35
|
+
[5, 'wpa-none'],
|
|
36
|
+
[6, 'wpa2'],
|
|
37
|
+
[7, 'wpa2-psk'],
|
|
38
|
+
[8, 'wpa3'],
|
|
39
|
+
[9, 'wpa3-sae'],
|
|
40
|
+
[10, 'owe'],
|
|
41
|
+
[11, 'wpa3-enterprise'],
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const CIPHER_NAMES: ReadonlyMap<number, string> = new Map([
|
|
45
|
+
[0, 'none'],
|
|
46
|
+
[1, 'wep-40'],
|
|
47
|
+
[2, 'tkip'],
|
|
48
|
+
[4, 'ccmp-aes'],
|
|
49
|
+
[5, 'wep-104'],
|
|
50
|
+
[6, 'bip'],
|
|
51
|
+
[8, 'gcmp'],
|
|
52
|
+
[0x100, 'wpa-use-group'],
|
|
53
|
+
[0x101, 'wep'],
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
const PHY_NAMES: ReadonlyMap<number, string> = new Map([
|
|
57
|
+
[1, 'fhss'],
|
|
58
|
+
[2, 'dsss'],
|
|
59
|
+
[3, 'ir-baseband'],
|
|
60
|
+
[4, 'ofdm-a'],
|
|
61
|
+
[5, 'hr-dsss-b'],
|
|
62
|
+
[6, 'erp-g'],
|
|
63
|
+
[7, 'ht-n'],
|
|
64
|
+
[8, 'vht-ac'],
|
|
65
|
+
[9, 'dmg-ad'],
|
|
66
|
+
[10, 'he-ax'],
|
|
67
|
+
[11, 'eht-be'],
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
export interface WifiInterface {
|
|
71
|
+
guid: string;
|
|
72
|
+
description: string;
|
|
73
|
+
state: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface WifiNetwork {
|
|
77
|
+
ssid: string;
|
|
78
|
+
bssCount: number;
|
|
79
|
+
signalQuality: number;
|
|
80
|
+
secured: boolean;
|
|
81
|
+
authAlgorithm: string;
|
|
82
|
+
cipherAlgorithm: string;
|
|
83
|
+
connectable: boolean;
|
|
84
|
+
connected: boolean;
|
|
85
|
+
hasProfile: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface WifiConnection {
|
|
89
|
+
profileName: string;
|
|
90
|
+
ssid: string;
|
|
91
|
+
bssid: string;
|
|
92
|
+
phyType: string;
|
|
93
|
+
signalQuality: number;
|
|
94
|
+
rxRateMbps: number;
|
|
95
|
+
txRateMbps: number;
|
|
96
|
+
secured: boolean;
|
|
97
|
+
authAlgorithm: string;
|
|
98
|
+
cipherAlgorithm: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface WifiBss {
|
|
102
|
+
ssid: string;
|
|
103
|
+
bssid: string;
|
|
104
|
+
rssi: number;
|
|
105
|
+
linkQuality: number;
|
|
106
|
+
phyType: string;
|
|
107
|
+
channelFrequencyKhz: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface WifiScanOptions {
|
|
111
|
+
triggerScan?: boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface WifiConnectOptions {
|
|
115
|
+
beta: boolean;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface WifiConnectResult {
|
|
119
|
+
ok: boolean;
|
|
120
|
+
reasonCode: number;
|
|
121
|
+
reason: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface RawInterface {
|
|
125
|
+
guid: string;
|
|
126
|
+
description: string;
|
|
127
|
+
state: number;
|
|
128
|
+
pointer: Pointer;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let clientHandle = 0n;
|
|
132
|
+
function handle(): bigint {
|
|
133
|
+
if (clientHandle === 0n) {
|
|
134
|
+
const negotiated = Buffer.allocUnsafeSlow(4);
|
|
135
|
+
const handleBuffer = Buffer.allocUnsafeSlow(8);
|
|
136
|
+
const status = Wlanapi.WlanOpenHandle(WLAN_API_VERSION_2_0, null, negotiated.ptr, handleBuffer.ptr);
|
|
137
|
+
if (status !== 0) throw new Win32Error(status);
|
|
138
|
+
clientHandle = handleBuffer.readBigUInt64LE(0);
|
|
139
|
+
}
|
|
140
|
+
return clientHandle;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function formatGuid(buffer: Buffer, offset: number): string {
|
|
144
|
+
const part1 = buffer.readUInt32LE(offset).toString(16).padStart(8, '0');
|
|
145
|
+
const part2 = buffer
|
|
146
|
+
.readUInt16LE(offset + 4)
|
|
147
|
+
.toString(16)
|
|
148
|
+
.padStart(4, '0');
|
|
149
|
+
const part3 = buffer
|
|
150
|
+
.readUInt16LE(offset + 6)
|
|
151
|
+
.toString(16)
|
|
152
|
+
.padStart(4, '0');
|
|
153
|
+
const part4 = macFromBytes(buffer, offset + 8, 2).replaceAll(':', '');
|
|
154
|
+
const part5 = macFromBytes(buffer, offset + 10, 6).replaceAll(':', '');
|
|
155
|
+
return `{${part1}-${part2}-${part3}-${part4}-${part5}}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// DOT11_SSID: uLength (ULONG) then ucSSID[32]; SSID bytes are raw UTF-8 (Unicode/emoji-proof — a node-wifi failure mode).
|
|
159
|
+
function readSsid(buffer: Buffer, offset: number): string {
|
|
160
|
+
const length = buffer.readUInt32LE(offset);
|
|
161
|
+
return buffer.toString('utf8', offset + 4, offset + 4 + Math.min(length, 32));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Returns the (caller-must-free) interface list pointer plus decoded entries whose `pointer` doubles as pInterfaceGuid (GUID@0 trick).
|
|
165
|
+
function enumerateInterfaces(): { listPointer: number; interfaces: RawInterface[] } {
|
|
166
|
+
const listPointerBuffer = Buffer.allocUnsafeSlow(8);
|
|
167
|
+
if (Wlanapi.WlanEnumInterfaces(handle(), null, listPointerBuffer.ptr) !== 0) return { listPointer: 0, interfaces: [] };
|
|
168
|
+
const listPointer = Number(listPointerBuffer.readBigUInt64LE(0));
|
|
169
|
+
if (listPointer === 0) return { listPointer: 0, interfaces: [] };
|
|
170
|
+
const count = Buffer.from(toArrayBuffer(listPointer as Pointer, 0, LIST_HEADER_SIZE)).readUInt32LE(0);
|
|
171
|
+
const interfaces: RawInterface[] = [];
|
|
172
|
+
for (let index = 0; index < count; index++) {
|
|
173
|
+
const pointer = (listPointer + LIST_HEADER_SIZE + index * INTERFACE_ENTRY_SIZE) as Pointer;
|
|
174
|
+
const entry = Buffer.from(toArrayBuffer(pointer, 0, INTERFACE_ENTRY_SIZE));
|
|
175
|
+
interfaces.push({ guid: formatGuid(entry, 0), description: readWideAt(entry, 16), state: entry.readUInt32LE(528), pointer });
|
|
176
|
+
}
|
|
177
|
+
return { listPointer, interfaces };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function selectInterface(interfaces: RawInterface[], interfaceGuid?: string): RawInterface | undefined {
|
|
181
|
+
if (interfaceGuid) return interfaces.find((candidate) => candidate.guid === interfaceGuid);
|
|
182
|
+
return interfaces.find((candidate) => candidate.state === 1) ?? interfaces[0];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Wireless interfaces (empty on Ethernet-only hosts — never throws). */
|
|
186
|
+
export function wifiInterfaces(): WifiInterface[] {
|
|
187
|
+
const { listPointer, interfaces } = enumerateInterfaces();
|
|
188
|
+
try {
|
|
189
|
+
return interfaces.map((candidate) => ({ guid: candidate.guid, description: candidate.description, state: INTERFACE_STATE_NAMES.get(candidate.state) ?? 'unknown' }));
|
|
190
|
+
} finally {
|
|
191
|
+
if (listPointer !== 0) Wlanapi.WlanFreeMemory(listPointer as Pointer);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Visible networks from wlanapi structs — locale-proof, Unicode/emoji-SSID-proof.
|
|
197
|
+
* `triggerScan` (default false) chooses the OS-cached list (instant) vs a FRESH scan
|
|
198
|
+
* (WlanScan → ~4 s settle → list; WlanScan may flush the prior list).
|
|
199
|
+
*/
|
|
200
|
+
export async function wifiScan(options: WifiScanOptions = {}): Promise<WifiNetwork[]> {
|
|
201
|
+
const { listPointer, interfaces } = enumerateInterfaces();
|
|
202
|
+
const networks: WifiNetwork[] = [];
|
|
203
|
+
try {
|
|
204
|
+
for (const iface of interfaces) {
|
|
205
|
+
if (options.triggerScan) {
|
|
206
|
+
Wlanapi.WlanScan(handle(), iface.pointer, null, null, null);
|
|
207
|
+
await Bun.sleep(SCAN_SETTLE_MS);
|
|
208
|
+
}
|
|
209
|
+
const networkListBuffer = Buffer.allocUnsafeSlow(8);
|
|
210
|
+
const status = Wlanapi.WlanGetAvailableNetworkList(handle(), iface.pointer, 0, null, networkListBuffer.ptr);
|
|
211
|
+
if (status === ERROR_ACCESS_DENIED) throw new Error('WiFi scan denied (ERROR_ACCESS_DENIED) — Windows precise-location consent is required (Settings → Privacy → Location). This is an OS gate, not a netdiag bug.');
|
|
212
|
+
if (status !== 0) continue;
|
|
213
|
+
const networkListPointer = Number(networkListBuffer.readBigUInt64LE(0));
|
|
214
|
+
if (networkListPointer === 0) continue;
|
|
215
|
+
try {
|
|
216
|
+
const networkCount = Buffer.from(toArrayBuffer(networkListPointer as Pointer, 0, LIST_HEADER_SIZE)).readUInt32LE(0);
|
|
217
|
+
for (let index = 0; index < networkCount; index++) {
|
|
218
|
+
const entry = Buffer.from(toArrayBuffer((networkListPointer + LIST_HEADER_SIZE + index * NETWORK_ENTRY_SIZE) as Pointer, 0, NETWORK_ENTRY_SIZE));
|
|
219
|
+
const flags = entry.readUInt32LE(620);
|
|
220
|
+
networks.push({
|
|
221
|
+
ssid: readSsid(entry, 512),
|
|
222
|
+
bssCount: entry.readUInt32LE(552),
|
|
223
|
+
signalQuality: entry.readUInt32LE(604),
|
|
224
|
+
secured: entry.readInt32LE(608) !== 0,
|
|
225
|
+
authAlgorithm: AUTH_NAMES.get(entry.readUInt32LE(612)) ?? `auth(${entry.readUInt32LE(612)})`,
|
|
226
|
+
cipherAlgorithm: CIPHER_NAMES.get(entry.readUInt32LE(616)) ?? `cipher(${entry.readUInt32LE(616)})`,
|
|
227
|
+
connectable: entry.readInt32LE(556) !== 0,
|
|
228
|
+
connected: (flags & CONNECTED_FLAG) !== 0,
|
|
229
|
+
hasProfile: (flags & HAS_PROFILE_FLAG) !== 0,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
} finally {
|
|
233
|
+
Wlanapi.WlanFreeMemory(networkListPointer as Pointer);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} finally {
|
|
237
|
+
if (listPointer !== 0) Wlanapi.WlanFreeMemory(listPointer as Pointer);
|
|
238
|
+
}
|
|
239
|
+
return networks;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** The current connection (signal/rate/BSSID/auth) via WlanQueryInterface — null if not connected / no WiFi. */
|
|
243
|
+
export function wifiConnection(interfaceGuid?: string): WifiConnection | null {
|
|
244
|
+
const { listPointer, interfaces } = enumerateInterfaces();
|
|
245
|
+
try {
|
|
246
|
+
const target = selectInterface(interfaces, interfaceGuid);
|
|
247
|
+
if (target === undefined) return null;
|
|
248
|
+
const sizeBuffer = Buffer.allocUnsafeSlow(4);
|
|
249
|
+
const dataPointerBuffer = Buffer.allocUnsafeSlow(8);
|
|
250
|
+
if (Wlanapi.WlanQueryInterface(handle(), target.pointer, WLAN_INTF_OPCODE_CURRENT_CONNECTION, null, sizeBuffer.ptr, dataPointerBuffer.ptr, null) !== 0) return null;
|
|
251
|
+
const dataPointer = Number(dataPointerBuffer.readBigUInt64LE(0));
|
|
252
|
+
if (dataPointer === 0) return null;
|
|
253
|
+
try {
|
|
254
|
+
const attributes = Buffer.from(toArrayBuffer(dataPointer as Pointer, 0, sizeBuffer.readUInt32LE(0)));
|
|
255
|
+
return {
|
|
256
|
+
profileName: readWideAt(attributes, 8),
|
|
257
|
+
ssid: readSsid(attributes, 520),
|
|
258
|
+
bssid: macFromBytes(attributes, 560, 6),
|
|
259
|
+
phyType: PHY_NAMES.get(attributes.readUInt32LE(568)) ?? 'unknown',
|
|
260
|
+
signalQuality: attributes.readUInt32LE(576),
|
|
261
|
+
rxRateMbps: attributes.readUInt32LE(580) / 1000,
|
|
262
|
+
txRateMbps: attributes.readUInt32LE(584) / 1000,
|
|
263
|
+
secured: attributes.readInt32LE(588) !== 0,
|
|
264
|
+
authAlgorithm: AUTH_NAMES.get(attributes.readUInt32LE(596)) ?? 'unknown',
|
|
265
|
+
cipherAlgorithm: CIPHER_NAMES.get(attributes.readUInt32LE(600)) ?? 'unknown',
|
|
266
|
+
};
|
|
267
|
+
} finally {
|
|
268
|
+
Wlanapi.WlanFreeMemory(dataPointer as Pointer);
|
|
269
|
+
}
|
|
270
|
+
} finally {
|
|
271
|
+
if (listPointer !== 0) Wlanapi.WlanFreeMemory(listPointer as Pointer);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// WLAN_BSS_ENTRY (x64) — netdiag authors this decoder; entries are VARIABLE length (advance by ulIeOffset + ulIeSize).
|
|
276
|
+
const BSS_ENTRY_SSID = 0;
|
|
277
|
+
const BSS_ENTRY_BSSID = 40;
|
|
278
|
+
const BSS_ENTRY_PHY_TYPE = 52;
|
|
279
|
+
const BSS_ENTRY_RSSI = 56; // LONG — SIGNED dBm, distinct from the 0–100 signalQuality
|
|
280
|
+
const BSS_ENTRY_LINK_QUALITY = 60;
|
|
281
|
+
const BSS_ENTRY_CHANNEL_FREQUENCY = 92;
|
|
282
|
+
const BSS_ENTRY_IE_OFFSET = 352;
|
|
283
|
+
const BSS_ENTRY_IE_SIZE = 356;
|
|
284
|
+
|
|
285
|
+
/** Per-BSSID census with signed-dBm RSSI + channel-center frequency via WlanGetNetworkBssList. WLAN_BSS_LIST: dwTotalSize@0, dwNumberOfItems@4. */
|
|
286
|
+
export function wifiBssList(interfaceGuid?: string): WifiBss[] {
|
|
287
|
+
const { listPointer, interfaces } = enumerateInterfaces();
|
|
288
|
+
const result: WifiBss[] = [];
|
|
289
|
+
try {
|
|
290
|
+
const target = selectInterface(interfaces, interfaceGuid);
|
|
291
|
+
if (target === undefined) return result;
|
|
292
|
+
const bssListBuffer = Buffer.allocUnsafeSlow(8);
|
|
293
|
+
if (Wlanapi.WlanGetNetworkBssList(handle(), target.pointer, null, DOT11_BSS_TYPE_ANY, 0, null, bssListBuffer.ptr) !== 0) return result;
|
|
294
|
+
const bssListPointer = Number(bssListBuffer.readBigUInt64LE(0));
|
|
295
|
+
if (bssListPointer === 0) return result;
|
|
296
|
+
try {
|
|
297
|
+
const totalSize = Buffer.from(toArrayBuffer(bssListPointer as Pointer, 0, LIST_HEADER_SIZE)).readUInt32LE(0);
|
|
298
|
+
const blob = Buffer.from(toArrayBuffer(bssListPointer as Pointer, 0, totalSize));
|
|
299
|
+
const count = blob.readUInt32LE(4);
|
|
300
|
+
let offset = LIST_HEADER_SIZE;
|
|
301
|
+
for (let index = 0; index < count && offset + BSS_ENTRY_IE_SIZE + 4 <= totalSize; index++) {
|
|
302
|
+
result.push({
|
|
303
|
+
ssid: readSsid(blob, offset + BSS_ENTRY_SSID),
|
|
304
|
+
bssid: macFromBytes(blob, offset + BSS_ENTRY_BSSID, 6),
|
|
305
|
+
rssi: blob.readInt32LE(offset + BSS_ENTRY_RSSI),
|
|
306
|
+
linkQuality: blob.readUInt32LE(offset + BSS_ENTRY_LINK_QUALITY),
|
|
307
|
+
phyType: PHY_NAMES.get(blob.readUInt32LE(offset + BSS_ENTRY_PHY_TYPE)) ?? 'unknown',
|
|
308
|
+
channelFrequencyKhz: blob.readUInt32LE(offset + BSS_ENTRY_CHANNEL_FREQUENCY),
|
|
309
|
+
});
|
|
310
|
+
offset = (offset + blob.readUInt32LE(offset + BSS_ENTRY_IE_OFFSET) + blob.readUInt32LE(offset + BSS_ENTRY_IE_SIZE) + 7) & ~7; // entries are 8-aligned
|
|
311
|
+
}
|
|
312
|
+
} finally {
|
|
313
|
+
Wlanapi.WlanFreeMemory(bssListPointer as Pointer);
|
|
314
|
+
}
|
|
315
|
+
} finally {
|
|
316
|
+
if (listPointer !== 0) Wlanapi.WlanFreeMemory(listPointer as Pointer);
|
|
317
|
+
}
|
|
318
|
+
return result;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function reasonCodeToString(reasonCode: number): string {
|
|
322
|
+
const buffer = Buffer.allocUnsafeSlow(0x0400);
|
|
323
|
+
return Wlanapi.WlanReasonCodeToString(reasonCode, 0x0200, buffer.ptr, null) === 0 ? readWideAt(buffer, 0) : `reason code ${reasonCode}`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* BETA — connect to a saved profile via WlanConnect. Signature-correct but
|
|
328
|
+
* UNEXERCISED against a live AP; pass { beta: true } to acknowledge. Verify the
|
|
329
|
+
* result by polling wifiConnection(), never by callback.
|
|
330
|
+
*/
|
|
331
|
+
export function wifiConnect(profileName: string, interfaceGuid: string | undefined, options: WifiConnectOptions): WifiConnectResult {
|
|
332
|
+
if (!options?.beta) throw new Error('wifiConnect is a BETA capability, unexercised against a live AP — pass { beta: true } to acknowledge.');
|
|
333
|
+
const { listPointer, interfaces } = enumerateInterfaces();
|
|
334
|
+
try {
|
|
335
|
+
const target = selectInterface(interfaces, interfaceGuid);
|
|
336
|
+
if (target === undefined) return { ok: false, reasonCode: 0, reason: 'no WiFi interface' };
|
|
337
|
+
const profile = Buffer.allocUnsafeSlow((profileName.length + 1) * 2);
|
|
338
|
+
profile.writeUInt16LE(0, profile.write(profileName, 'utf16le'));
|
|
339
|
+
const parameters = Buffer.allocUnsafeSlow(40); // WLAN_CONNECTION_PARAMETERS (x64)
|
|
340
|
+
parameters.fill(0);
|
|
341
|
+
parameters.writeUInt32LE(WLAN_CONNECTION_MODE_PROFILE, 0);
|
|
342
|
+
parameters.writeBigUInt64LE(BigInt(Number(profile.ptr)), 8); // strProfile
|
|
343
|
+
parameters.writeUInt32LE(DOT11_BSS_TYPE_INFRASTRUCTURE, 32);
|
|
344
|
+
const status = Wlanapi.WlanConnect(handle(), target.pointer, parameters.ptr, null);
|
|
345
|
+
return { ok: status === 0, reasonCode: status, reason: reasonCodeToString(status) };
|
|
346
|
+
} finally {
|
|
347
|
+
if (listPointer !== 0) Wlanapi.WlanFreeMemory(listPointer as Pointer);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** BETA — disconnect the interface via WlanDisconnect. Pass { beta: true } to acknowledge. */
|
|
352
|
+
export function wifiDisconnect(interfaceGuid: string | undefined, options: WifiConnectOptions): WifiConnectResult {
|
|
353
|
+
if (!options?.beta) throw new Error('wifiDisconnect is a BETA capability — pass { beta: true } to acknowledge.');
|
|
354
|
+
const { listPointer, interfaces } = enumerateInterfaces();
|
|
355
|
+
try {
|
|
356
|
+
const target = selectInterface(interfaces, interfaceGuid);
|
|
357
|
+
if (target === undefined) return { ok: false, reasonCode: 0, reason: 'no WiFi interface' };
|
|
358
|
+
const status = Wlanapi.WlanDisconnect(handle(), target.pointer, null);
|
|
359
|
+
return { ok: status === 0, reasonCode: status, reason: reasonCodeToString(status) };
|
|
360
|
+
} finally {
|
|
361
|
+
if (listPointer !== 0) Wlanapi.WlanFreeMemory(listPointer as Pointer);
|
|
362
|
+
}
|
|
363
|
+
}
|
package/win32.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import Iphlpapi from '@bun-win32/iphlpapi';
|
|
2
|
+
import { read, toArrayBuffer, type Pointer } from 'bun:ffi';
|
|
3
|
+
|
|
4
|
+
export { default as Dnsapi } from '@bun-win32/dnsapi';
|
|
5
|
+
export { default as Kernel32 } from '@bun-win32/kernel32';
|
|
6
|
+
export { default as Wlanapi } from '@bun-win32/wlanapi';
|
|
7
|
+
export { default as Ws2_32 } from '@bun-win32/ws2_32';
|
|
8
|
+
export { Iphlpapi };
|
|
9
|
+
|
|
10
|
+
const ERROR_SUCCESS = 0x0000_0000;
|
|
11
|
+
const ERROR_BUFFER_OVERFLOW = 0x0000_006f; // 111 — Table sizing return
|
|
12
|
+
const ERROR_INSUFFICIENT_BUFFER = 0x0000_007a; // 122 — GetExtended*Table sizing return
|
|
13
|
+
|
|
14
|
+
const WIN32_ERROR_MESSAGES: ReadonlyMap<number, string> = new Map([
|
|
15
|
+
[5, 'access denied — this operation requires Administrator'],
|
|
16
|
+
[50, 'the request is not supported on this system'],
|
|
17
|
+
[87, 'invalid parameter — likely a malformed request struct or address family'],
|
|
18
|
+
[232, 'no data is available'],
|
|
19
|
+
[1168, 'element not found'],
|
|
20
|
+
[1314, 'a required privilege is not held by the client — this operation requires Administrator'],
|
|
21
|
+
[1722, 'the RPC server is unavailable'],
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
export function win32ErrorMessage(code: number): string {
|
|
25
|
+
return WIN32_ERROR_MESSAGES.get(code) ?? `Win32 error ${code}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class Win32Error extends Error {
|
|
29
|
+
readonly code: number;
|
|
30
|
+
constructor(code: number) {
|
|
31
|
+
super(`${win32ErrorMessage(code)} (code ${code})`);
|
|
32
|
+
this.code = code;
|
|
33
|
+
this.name = 'Win32Error';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The Win32 sizing-call idiom, encapsulated. Nearly every IP Helper table API is
|
|
39
|
+
* called twice: once to learn the byte count, once to fill a buffer. This holds
|
|
40
|
+
* one growable data buffer + one persistent DataView + a stable 4-byte size
|
|
41
|
+
* buffer, reused across polls — never allocate per sample.
|
|
42
|
+
*
|
|
43
|
+
* The data buffer is always > 4096 bytes ⇒ an own (off-heap) ArrayBuffer with a
|
|
44
|
+
* stable address: pooled small buffers relocate under GC and dangle their `.ptr`
|
|
45
|
+
* (and the kernel writes absolute in-buffer pointers that a relocation would
|
|
46
|
+
* invalidate). `.ptr` is read INLINE at each call.
|
|
47
|
+
*/
|
|
48
|
+
export class SizedBufferState {
|
|
49
|
+
#buffer: Buffer;
|
|
50
|
+
#view: DataView;
|
|
51
|
+
readonly #sizeBuffer: Buffer;
|
|
52
|
+
|
|
53
|
+
constructor(initialBytes = 0x0000_4000) {
|
|
54
|
+
this.#buffer = Buffer.allocUnsafe(initialBytes);
|
|
55
|
+
this.#view = new DataView(this.#buffer.buffer, this.#buffer.byteOffset, this.#buffer.byteLength);
|
|
56
|
+
this.#sizeBuffer = Buffer.allocUnsafeSlow(4);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get buffer(): Buffer {
|
|
60
|
+
return this.#buffer;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get view(): DataView {
|
|
64
|
+
return this.#view;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Invoke with the current buffer; on ERROR_INSUFFICIENT_BUFFER/ERROR_BUFFER_OVERFLOW
|
|
69
|
+
* grow geometrically-by-need and retry once. `invoke(dataPointer, sizePointer)`
|
|
70
|
+
* returns a Win32 error code. Returns the (possibly regrown) DataView.
|
|
71
|
+
*/
|
|
72
|
+
fill(invoke: (dataPointer: Pointer, sizePointer: Pointer) => number): DataView {
|
|
73
|
+
this.#sizeBuffer.writeUInt32LE(this.#buffer.byteLength, 0);
|
|
74
|
+
let error = invoke(this.#buffer.ptr, this.#sizeBuffer.ptr);
|
|
75
|
+
if (error === ERROR_INSUFFICIENT_BUFFER || error === ERROR_BUFFER_OVERFLOW) {
|
|
76
|
+
const required = this.#sizeBuffer.readUInt32LE(0);
|
|
77
|
+
if (required > this.#buffer.byteLength) {
|
|
78
|
+
this.#buffer = Buffer.allocUnsafe(required);
|
|
79
|
+
this.#view = new DataView(this.#buffer.buffer, this.#buffer.byteOffset, this.#buffer.byteLength);
|
|
80
|
+
}
|
|
81
|
+
this.#sizeBuffer.writeUInt32LE(this.#buffer.byteLength, 0);
|
|
82
|
+
error = invoke(this.#buffer.ptr, this.#sizeBuffer.ptr); // re-read .ptr: the buffer may have moved
|
|
83
|
+
}
|
|
84
|
+
if (error !== ERROR_SUCCESS) throw new Win32Error(error);
|
|
85
|
+
return this.#view;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Walk an in-buffer singly-linked list (the GetAdaptersAddresses shape): every
|
|
91
|
+
* node lives inside one stable buffer, chained by an absolute `Next` pointer at
|
|
92
|
+
* `nextOffset`. Yields each node's byte offset within `base`. `headPointer` is
|
|
93
|
+
* the absolute address of the first node (0 ⇒ empty list); for a top-level list
|
|
94
|
+
* whose first node sits at the buffer start pass `Number(base.ptr)`.
|
|
95
|
+
*/
|
|
96
|
+
export function* walkList(base: Buffer, headPointer: number, nextOffset: number): Generator<number> {
|
|
97
|
+
const baseAddress = Number(base.ptr);
|
|
98
|
+
let pointer = headPointer;
|
|
99
|
+
while (pointer !== 0) {
|
|
100
|
+
const offset = pointer - baseAddress;
|
|
101
|
+
yield offset;
|
|
102
|
+
pointer = Number(base.readBigUInt64LE(offset + nextOffset));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** ASCII string from `offset` up to NUL (or buffer end). Negative offset ⇒ ''. */
|
|
107
|
+
export function readAnsiAt(base: Buffer, offset: number): string {
|
|
108
|
+
if (offset < 0) return '';
|
|
109
|
+
const end = base.indexOf(0, offset);
|
|
110
|
+
return base.toString('ascii', offset, end < 0 ? base.byteLength : end);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** UTF-16LE string from `offset` up to the NUL terminator. Negative offset ⇒ '' (repo memory: Bun's TextDecoder rejects 'utf-16le'; use Buffer). */
|
|
114
|
+
export function readWideAt(base: Buffer, offset: number): string {
|
|
115
|
+
if (offset < 0) return '';
|
|
116
|
+
let end = offset;
|
|
117
|
+
while (end + 1 < base.byteLength && base.readUInt16LE(end) !== 0) end += 2;
|
|
118
|
+
return base.toString('utf16le', offset, end);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const mibTableOut = Buffer.allocUnsafeSlow(8);
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Decode a self-allocating Table2 API (GetIpForwardTable2 / GetIpNetTable2): the
|
|
125
|
+
* API allocates the table and writes its pointer into the out buffer; we read
|
|
126
|
+
* NumEntries, wrap the row region in ONE Buffer over native memory, decode each
|
|
127
|
+
* row, and ALWAYS FreeMibTable in `finally` (or leak native memory every poll).
|
|
128
|
+
* `invoke(tablePointer)` receives the reused 8-byte out buffer pointer and
|
|
129
|
+
* returns a Win32 error code.
|
|
130
|
+
*/
|
|
131
|
+
export function mibTable<T>(invoke: (tablePointer: Pointer) => number, firstRowOffset: number, rowSize: number, decodeRow: (table: Buffer, rowOffset: number) => T): T[] {
|
|
132
|
+
const error = invoke(mibTableOut.ptr);
|
|
133
|
+
if (error !== ERROR_SUCCESS) throw new Win32Error(error);
|
|
134
|
+
const tablePointer = Number(mibTableOut.readBigUInt64LE(0)) as Pointer;
|
|
135
|
+
try {
|
|
136
|
+
const numEntries = read.u32(tablePointer, 0);
|
|
137
|
+
const table = Buffer.from(toArrayBuffer(tablePointer, 0, firstRowOffset + numEntries * rowSize));
|
|
138
|
+
const rows: T[] = new Array(numEntries);
|
|
139
|
+
for (let index = 0; index < numEntries; index++) rows[index] = decodeRow(table, firstRowOffset + index * rowSize);
|
|
140
|
+
return rows;
|
|
141
|
+
} finally {
|
|
142
|
+
Iphlpapi.FreeMibTable(tablePointer);
|
|
143
|
+
}
|
|
144
|
+
}
|