@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/sockets.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { Pointer } from 'bun:ffi';
|
|
2
|
+
|
|
3
|
+
import { ipv4FromU32, ipv6FromBytes, portFromNetworkOrder } from './addr';
|
|
4
|
+
import { AF_INET, AF_INET6, type AddressFamilyName, TCP_TABLE_OWNER_MODULE_ALL, TCP_TABLE_OWNER_PID_ALL, TCPIP_OWNER_MODULE_INFO_BASIC, tcpStateName, UDP_TABLE_OWNER_PID } from './constants';
|
|
5
|
+
import { Iphlpapi, Kernel32, readWideAt, SizedBufferState } from './win32';
|
|
6
|
+
|
|
7
|
+
// Fixed x64 row strides (do NOT derive from buffer size — net-xray ground truth).
|
|
8
|
+
const TCP4_PID_ROW = 24; // MIB_TCPROW_OWNER_PID
|
|
9
|
+
const TCP6_PID_ROW = 56; // MIB_TCP6ROW_OWNER_PID
|
|
10
|
+
const TCP4_MODULE_ROW = 160; // MIB_TCPROW_OWNER_MODULE (24 basic + liCreateTimestamp(8) + OwningModuleInfo[16](128))
|
|
11
|
+
const TCP6_MODULE_ROW = 192; // MIB_TCP6ROW_OWNER_MODULE (56 basic + 8 + 128)
|
|
12
|
+
const UDP4_ROW = 12; // MIB_UDPROW_OWNER_PID
|
|
13
|
+
const UDP6_ROW = 28; // MIB_UDP6ROW_OWNER_PID
|
|
14
|
+
const PROCESS_QUERY_LIMITED_INFORMATION = 0x0000_1000;
|
|
15
|
+
|
|
16
|
+
export type NameResolution = 'none' | 'module' | 'image';
|
|
17
|
+
|
|
18
|
+
export interface SocketOptions {
|
|
19
|
+
family?: AddressFamilyName;
|
|
20
|
+
resolveNames?: NameResolution;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TcpConnection {
|
|
24
|
+
family: 'ipv4' | 'ipv6';
|
|
25
|
+
localAddress: string;
|
|
26
|
+
localPort: number;
|
|
27
|
+
remoteAddress: string;
|
|
28
|
+
remotePort: number;
|
|
29
|
+
state: string;
|
|
30
|
+
pid: number;
|
|
31
|
+
processName?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface UdpEndpoint {
|
|
35
|
+
family: 'ipv4' | 'ipv6';
|
|
36
|
+
localAddress: string;
|
|
37
|
+
localPort: number;
|
|
38
|
+
pid: number;
|
|
39
|
+
processName?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const tcp4State = new SizedBufferState();
|
|
43
|
+
const tcp6State = new SizedBufferState();
|
|
44
|
+
const udp4State = new SizedBufferState();
|
|
45
|
+
const udp6State = new SizedBufferState();
|
|
46
|
+
const moduleState = new SizedBufferState(0x0000_2000);
|
|
47
|
+
const imageNameBuffer = Buffer.allocUnsafeSlow(0x0000_0420); // 528 wchars, own buffer
|
|
48
|
+
const imageSizeBuffer = Buffer.allocUnsafeSlow(4);
|
|
49
|
+
|
|
50
|
+
// The owning MODULE name (iphlpapi-only moat) — pModuleName is an offset-pointer INTO the out buffer.
|
|
51
|
+
function resolveModuleName(rowPointer: Pointer): string {
|
|
52
|
+
try {
|
|
53
|
+
moduleState.fill((dataPointer, sizePointer) => Iphlpapi.GetOwnerModuleFromTcpEntry(rowPointer, TCPIP_OWNER_MODULE_INFO_BASIC, dataPointer, sizePointer));
|
|
54
|
+
} catch {
|
|
55
|
+
return ''; // ERROR_ACCESS_DENIED for other-user / protected PIDs — fall back to the bare PID
|
|
56
|
+
}
|
|
57
|
+
const base = moduleState.buffer;
|
|
58
|
+
const moduleNamePointer = Number(base.readBigUInt64LE(0)); // TCPIP_OWNER_MODULE_BASIC_INFO.pModuleName
|
|
59
|
+
return moduleNamePointer === 0 ? '' : readWideAt(base, moduleNamePointer - Number(base.ptr));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The full image basename via kernel32 (the richer, cross-DLL mode).
|
|
63
|
+
function resolveImageName(pid: number): string {
|
|
64
|
+
if (pid <= 4) return pid === 4 ? 'System' : 'Idle';
|
|
65
|
+
const handle = Kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
|
|
66
|
+
if (handle === 0n) return '';
|
|
67
|
+
try {
|
|
68
|
+
imageSizeBuffer.writeUInt32LE(0x0000_0210, 0); // 528 chars in/out
|
|
69
|
+
if (!Kernel32.QueryFullProcessImageNameW(handle, 0, imageNameBuffer.ptr, imageSizeBuffer.ptr)) return '';
|
|
70
|
+
const path = imageNameBuffer.toString('utf16le', 0, imageSizeBuffer.readUInt32LE(0) * 2);
|
|
71
|
+
const slash = path.lastIndexOf('\\');
|
|
72
|
+
return slash < 0 ? path : path.slice(slash + 1);
|
|
73
|
+
} finally {
|
|
74
|
+
Kernel32.CloseHandle(handle);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function decodeTcp(view: DataView, base: Buffer, family: 'ipv4' | 'ipv6', rowStride: number, headerOffset: number, resolveNames: NameResolution, cache: Map<number, string>, result: TcpConnection[]): void {
|
|
79
|
+
const count = view.getUint32(0, true);
|
|
80
|
+
for (let index = 0; index < count; index++) {
|
|
81
|
+
const offset = headerOffset + index * rowStride;
|
|
82
|
+
let state: number;
|
|
83
|
+
let localAddress: string;
|
|
84
|
+
let localPort: number;
|
|
85
|
+
let remoteAddress: string;
|
|
86
|
+
let remotePort: number;
|
|
87
|
+
let pid: number;
|
|
88
|
+
if (family === 'ipv4') {
|
|
89
|
+
state = view.getUint32(offset, true);
|
|
90
|
+
localAddress = ipv4FromU32(view.getUint32(offset + 4, true));
|
|
91
|
+
localPort = portFromNetworkOrder(view.getUint32(offset + 8, true));
|
|
92
|
+
remoteAddress = ipv4FromU32(view.getUint32(offset + 12, true));
|
|
93
|
+
remotePort = portFromNetworkOrder(view.getUint32(offset + 16, true));
|
|
94
|
+
pid = view.getUint32(offset + 20, true);
|
|
95
|
+
} else {
|
|
96
|
+
localAddress = ipv6FromBytes(base, offset);
|
|
97
|
+
localPort = portFromNetworkOrder(view.getUint32(offset + 20, true));
|
|
98
|
+
remoteAddress = ipv6FromBytes(base, offset + 24);
|
|
99
|
+
remotePort = portFromNetworkOrder(view.getUint32(offset + 44, true));
|
|
100
|
+
state = view.getUint32(offset + 48, true);
|
|
101
|
+
pid = view.getUint32(offset + 52, true);
|
|
102
|
+
}
|
|
103
|
+
let processName: string | undefined;
|
|
104
|
+
if (resolveNames !== 'none') {
|
|
105
|
+
processName = cache.get(pid);
|
|
106
|
+
if (processName === undefined) {
|
|
107
|
+
processName = resolveNames === 'module' ? resolveModuleName(base.subarray(offset, offset + rowStride).ptr) : resolveImageName(pid);
|
|
108
|
+
cache.set(pid, processName);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
result.push({ family, localAddress, localPort, remoteAddress, remotePort, state: tcpStateName(state), pid, processName });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function decodeUdp(view: DataView, base: Buffer, family: 'ipv4' | 'ipv6', rowStride: number, resolveNames: NameResolution, cache: Map<number, string>, result: UdpEndpoint[]): void {
|
|
116
|
+
const count = view.getUint32(0, true);
|
|
117
|
+
for (let index = 0; index < count; index++) {
|
|
118
|
+
const offset = 4 + index * rowStride;
|
|
119
|
+
let localAddress: string;
|
|
120
|
+
let localPort: number;
|
|
121
|
+
let pid: number;
|
|
122
|
+
if (family === 'ipv4') {
|
|
123
|
+
localAddress = ipv4FromU32(view.getUint32(offset, true));
|
|
124
|
+
localPort = portFromNetworkOrder(view.getUint32(offset + 4, true));
|
|
125
|
+
pid = view.getUint32(offset + 8, true);
|
|
126
|
+
} else {
|
|
127
|
+
localAddress = ipv6FromBytes(base, offset);
|
|
128
|
+
localPort = portFromNetworkOrder(view.getUint32(offset + 20, true));
|
|
129
|
+
pid = view.getUint32(offset + 24, true);
|
|
130
|
+
}
|
|
131
|
+
// UDP has no owning-module table here → process names use the kernel32 image basename.
|
|
132
|
+
let processName: string | undefined;
|
|
133
|
+
if (resolveNames !== 'none') {
|
|
134
|
+
processName = cache.get(pid);
|
|
135
|
+
if (processName === undefined) {
|
|
136
|
+
processName = resolveImageName(pid);
|
|
137
|
+
cache.set(pid, processName);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
result.push({ family, localAddress, localPort, pid, processName });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Socket→PID(+module) over GetExtendedTcpTable — one syscall, no netstat -ano parse. IPv4 + IPv6. */
|
|
145
|
+
export function tcpConnections(options: SocketOptions = {}): TcpConnection[] {
|
|
146
|
+
const family = options.family ?? 'all';
|
|
147
|
+
const resolveNames = options.resolveNames ?? 'none';
|
|
148
|
+
const useModule = resolveNames === 'module';
|
|
149
|
+
const tableClass = useModule ? TCP_TABLE_OWNER_MODULE_ALL : TCP_TABLE_OWNER_PID_ALL;
|
|
150
|
+
const headerOffset = useModule ? 8 : 4; // OWNER_MODULE rows are 8-aligned (liCreateTimestamp) → table[] starts at +8, not +4
|
|
151
|
+
const cache = new Map<number, string>();
|
|
152
|
+
const result: TcpConnection[] = [];
|
|
153
|
+
if (family === 'all' || family === 'ipv4') {
|
|
154
|
+
const view = tcp4State.fill((dataPointer, sizePointer) => Iphlpapi.GetExtendedTcpTable(dataPointer, sizePointer, 0, AF_INET, tableClass, 0));
|
|
155
|
+
decodeTcp(view, tcp4State.buffer, 'ipv4', useModule ? TCP4_MODULE_ROW : TCP4_PID_ROW, headerOffset, resolveNames, cache, result);
|
|
156
|
+
}
|
|
157
|
+
if (family === 'all' || family === 'ipv6') {
|
|
158
|
+
const view = tcp6State.fill((dataPointer, sizePointer) => Iphlpapi.GetExtendedTcpTable(dataPointer, sizePointer, 0, AF_INET6, tableClass, 0));
|
|
159
|
+
decodeTcp(view, tcp6State.buffer, 'ipv6', useModule ? TCP6_MODULE_ROW : TCP6_PID_ROW, headerOffset, resolveNames, cache, result);
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** UDP endpoints with owning PID over GetExtendedUdpTable. IPv4 + IPv6. */
|
|
165
|
+
export function udpListeners(options: SocketOptions = {}): UdpEndpoint[] {
|
|
166
|
+
const family = options.family ?? 'all';
|
|
167
|
+
const resolveNames = options.resolveNames ?? 'none';
|
|
168
|
+
const cache = new Map<number, string>();
|
|
169
|
+
const result: UdpEndpoint[] = [];
|
|
170
|
+
if (family === 'all' || family === 'ipv4') {
|
|
171
|
+
const view = udp4State.fill((dataPointer, sizePointer) => Iphlpapi.GetExtendedUdpTable(dataPointer, sizePointer, 0, AF_INET, UDP_TABLE_OWNER_PID, 0));
|
|
172
|
+
decodeUdp(view, udp4State.buffer, 'ipv4', UDP4_ROW, resolveNames, cache, result);
|
|
173
|
+
}
|
|
174
|
+
if (family === 'all' || family === 'ipv6') {
|
|
175
|
+
const view = udp6State.fill((dataPointer, sizePointer) => Iphlpapi.GetExtendedUdpTable(dataPointer, sizePointer, 0, AF_INET6, UDP_TABLE_OWNER_PID, 0));
|
|
176
|
+
decodeUdp(view, udp6State.buffer, 'ipv6', UDP6_ROW, resolveNames, cache, result);
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
package/stats.ts
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { addressFamilyValue } from './constants';
|
|
2
|
+
import { Iphlpapi, mibTable, readWideAt, Win32Error } from './win32';
|
|
3
|
+
|
|
4
|
+
type StatsFamily = 'ipv4' | 'ipv6';
|
|
5
|
+
|
|
6
|
+
// MIB_TCPSTATS / MIB_IPSTATS / MIB_UDPSTATS are flat DWORD arrays (Ex variants take a family).
|
|
7
|
+
const TCP_STATS_SIZE = 60;
|
|
8
|
+
const IP_STATS_SIZE = 92;
|
|
9
|
+
const UDP_STATS_SIZE = 20;
|
|
10
|
+
|
|
11
|
+
// MIB_IF_TABLE2 { ULONG NumEntries; MIB_IF_ROW2 Table[] } — rows 8-aligned; GetIfTable2 self-allocates → mibTable.
|
|
12
|
+
const IF_TABLE_FIRST_ROW = 8;
|
|
13
|
+
const IF_ROW_SIZE = 1352; // sizeof(MIB_IF_ROW2) x64 — verified vs alias decode + climbing octets (S9.2)
|
|
14
|
+
const IF_ROW_INTERFACE_INDEX = 8;
|
|
15
|
+
const IF_ROW_ALIAS = 28; // WCHAR Alias[257]
|
|
16
|
+
const IF_ROW_MTU = 1124;
|
|
17
|
+
const IF_ROW_OPER_STATUS = 1156;
|
|
18
|
+
const IF_ROW_IN_OCTETS = 1208; // ULONG64
|
|
19
|
+
const IF_ROW_IN_DISCARDS = 1232;
|
|
20
|
+
const IF_ROW_IN_ERRORS = 1240;
|
|
21
|
+
const IF_ROW_OUT_OCTETS = 1280;
|
|
22
|
+
const IF_ROW_OUT_DISCARDS = 1304;
|
|
23
|
+
const IF_ROW_OUT_ERRORS = 1312;
|
|
24
|
+
|
|
25
|
+
const OPER_STATUS_NAMES: ReadonlyMap<number, string> = new Map([
|
|
26
|
+
[1, 'up'],
|
|
27
|
+
[2, 'down'],
|
|
28
|
+
[3, 'testing'],
|
|
29
|
+
[4, 'unknown'],
|
|
30
|
+
[5, 'dormant'],
|
|
31
|
+
[6, 'not-present'],
|
|
32
|
+
[7, 'lower-layer-down'],
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
export interface TcpStatistics {
|
|
36
|
+
activeOpens: number;
|
|
37
|
+
passiveOpens: number;
|
|
38
|
+
attemptFails: number;
|
|
39
|
+
establishedResets: number;
|
|
40
|
+
currentEstablished: number;
|
|
41
|
+
inSegments: number;
|
|
42
|
+
outSegments: number;
|
|
43
|
+
retransmittedSegments: number;
|
|
44
|
+
inErrors: number;
|
|
45
|
+
outResets: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface IpStatistics {
|
|
49
|
+
inReceives: number;
|
|
50
|
+
inHeaderErrors: number;
|
|
51
|
+
inAddressErrors: number;
|
|
52
|
+
forwardedDatagrams: number;
|
|
53
|
+
inDiscards: number;
|
|
54
|
+
inDelivers: number;
|
|
55
|
+
outRequests: number;
|
|
56
|
+
outDiscards: number;
|
|
57
|
+
outNoRoutes: number;
|
|
58
|
+
reassemblyRequired: number;
|
|
59
|
+
reassemblyOk: number;
|
|
60
|
+
fragmentsOk: number;
|
|
61
|
+
fragmentsFailed: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface UdpStatistics {
|
|
65
|
+
inDatagrams: number;
|
|
66
|
+
noPorts: number;
|
|
67
|
+
inErrors: number;
|
|
68
|
+
outDatagrams: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface InterfaceCounters {
|
|
72
|
+
interfaceIndex: number;
|
|
73
|
+
name: string;
|
|
74
|
+
mtu: number;
|
|
75
|
+
operStatus: string;
|
|
76
|
+
inOctets: number;
|
|
77
|
+
outOctets: number;
|
|
78
|
+
inErrors: number;
|
|
79
|
+
outErrors: number;
|
|
80
|
+
inDiscards: number;
|
|
81
|
+
outDiscards: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface ThroughputSample {
|
|
85
|
+
interfaceIndex: number;
|
|
86
|
+
name: string;
|
|
87
|
+
rxBytesPerSec: number;
|
|
88
|
+
txBytesPerSec: number;
|
|
89
|
+
rxErrors: number;
|
|
90
|
+
txErrors: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const tcpStatsBuffer = Buffer.allocUnsafeSlow(TCP_STATS_SIZE);
|
|
94
|
+
const ipStatsBuffer = Buffer.allocUnsafeSlow(IP_STATS_SIZE);
|
|
95
|
+
const udpStatsBuffer = Buffer.allocUnsafeSlow(UDP_STATS_SIZE);
|
|
96
|
+
|
|
97
|
+
/** TCP protocol counters (typed netstat -s) — 32-bit DWORDs that wrap on busy hosts; treat deltas modulo 2^32. */
|
|
98
|
+
export function tcpStatistics(family: StatsFamily = 'ipv4'): TcpStatistics {
|
|
99
|
+
const error = Iphlpapi.GetTcpStatisticsEx(tcpStatsBuffer.ptr, addressFamilyValue(family));
|
|
100
|
+
if (error !== 0) throw new Win32Error(error);
|
|
101
|
+
const buffer = tcpStatsBuffer;
|
|
102
|
+
return {
|
|
103
|
+
activeOpens: buffer.readUInt32LE(16),
|
|
104
|
+
passiveOpens: buffer.readUInt32LE(20),
|
|
105
|
+
attemptFails: buffer.readUInt32LE(24),
|
|
106
|
+
establishedResets: buffer.readUInt32LE(28),
|
|
107
|
+
currentEstablished: buffer.readUInt32LE(32),
|
|
108
|
+
inSegments: buffer.readUInt32LE(36),
|
|
109
|
+
outSegments: buffer.readUInt32LE(40),
|
|
110
|
+
retransmittedSegments: buffer.readUInt32LE(44),
|
|
111
|
+
inErrors: buffer.readUInt32LE(48),
|
|
112
|
+
outResets: buffer.readUInt32LE(52),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** IP protocol counters. */
|
|
117
|
+
export function ipStatistics(family: StatsFamily = 'ipv4'): IpStatistics {
|
|
118
|
+
const error = Iphlpapi.GetIpStatisticsEx(ipStatsBuffer.ptr, addressFamilyValue(family));
|
|
119
|
+
if (error !== 0) throw new Win32Error(error);
|
|
120
|
+
const buffer = ipStatsBuffer;
|
|
121
|
+
return {
|
|
122
|
+
inReceives: buffer.readUInt32LE(8),
|
|
123
|
+
inHeaderErrors: buffer.readUInt32LE(12),
|
|
124
|
+
inAddressErrors: buffer.readUInt32LE(16),
|
|
125
|
+
forwardedDatagrams: buffer.readUInt32LE(20),
|
|
126
|
+
inDiscards: buffer.readUInt32LE(28),
|
|
127
|
+
inDelivers: buffer.readUInt32LE(32),
|
|
128
|
+
outRequests: buffer.readUInt32LE(36),
|
|
129
|
+
outDiscards: buffer.readUInt32LE(44),
|
|
130
|
+
outNoRoutes: buffer.readUInt32LE(48),
|
|
131
|
+
reassemblyRequired: buffer.readUInt32LE(56),
|
|
132
|
+
reassemblyOk: buffer.readUInt32LE(60),
|
|
133
|
+
fragmentsOk: buffer.readUInt32LE(68),
|
|
134
|
+
fragmentsFailed: buffer.readUInt32LE(72),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** UDP protocol counters. */
|
|
139
|
+
export function udpStatistics(family: StatsFamily = 'ipv4'): UdpStatistics {
|
|
140
|
+
const error = Iphlpapi.GetUdpStatisticsEx(udpStatsBuffer.ptr, addressFamilyValue(family));
|
|
141
|
+
if (error !== 0) throw new Win32Error(error);
|
|
142
|
+
const buffer = udpStatsBuffer;
|
|
143
|
+
return { inDatagrams: buffer.readUInt32LE(0), noPorts: buffer.readUInt32LE(4), inErrors: buffer.readUInt32LE(8), outDatagrams: buffer.readUInt32LE(12) };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Per-interface counters from GetIfTable2 — octets are ULONG64; error/discard counters systeminformation drops. */
|
|
147
|
+
export function interfaceCounters(): InterfaceCounters[] {
|
|
148
|
+
return mibTable(
|
|
149
|
+
(tablePointer) => Iphlpapi.GetIfTable2(tablePointer),
|
|
150
|
+
IF_TABLE_FIRST_ROW,
|
|
151
|
+
IF_ROW_SIZE,
|
|
152
|
+
(table, row) => ({
|
|
153
|
+
interfaceIndex: table.readUInt32LE(row + IF_ROW_INTERFACE_INDEX),
|
|
154
|
+
name: readWideAt(table, row + IF_ROW_ALIAS),
|
|
155
|
+
mtu: table.readUInt32LE(row + IF_ROW_MTU),
|
|
156
|
+
operStatus: OPER_STATUS_NAMES.get(table.readUInt32LE(row + IF_ROW_OPER_STATUS)) ?? 'unknown',
|
|
157
|
+
inOctets: Number(table.readBigUInt64LE(row + IF_ROW_IN_OCTETS)),
|
|
158
|
+
outOctets: Number(table.readBigUInt64LE(row + IF_ROW_OUT_OCTETS)),
|
|
159
|
+
inErrors: Number(table.readBigUInt64LE(row + IF_ROW_IN_ERRORS)),
|
|
160
|
+
outErrors: Number(table.readBigUInt64LE(row + IF_ROW_OUT_ERRORS)),
|
|
161
|
+
inDiscards: Number(table.readBigUInt64LE(row + IF_ROW_IN_DISCARDS)),
|
|
162
|
+
outDiscards: Number(table.readBigUInt64LE(row + IF_ROW_OUT_DISCARDS)),
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
interface OctetSample {
|
|
168
|
+
interfaceIndex: number;
|
|
169
|
+
name: string;
|
|
170
|
+
inOctets: bigint;
|
|
171
|
+
outOctets: bigint;
|
|
172
|
+
inErrors: bigint;
|
|
173
|
+
outErrors: bigint;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function sampleOctets(): Map<number, OctetSample> {
|
|
177
|
+
const rows = mibTable(
|
|
178
|
+
(tablePointer) => Iphlpapi.GetIfTable2(tablePointer),
|
|
179
|
+
IF_TABLE_FIRST_ROW,
|
|
180
|
+
IF_ROW_SIZE,
|
|
181
|
+
(table, row) => ({
|
|
182
|
+
interfaceIndex: table.readUInt32LE(row + IF_ROW_INTERFACE_INDEX),
|
|
183
|
+
name: readWideAt(table, row + IF_ROW_ALIAS),
|
|
184
|
+
inOctets: table.readBigUInt64LE(row + IF_ROW_IN_OCTETS),
|
|
185
|
+
outOctets: table.readBigUInt64LE(row + IF_ROW_OUT_OCTETS),
|
|
186
|
+
inErrors: table.readBigUInt64LE(row + IF_ROW_IN_ERRORS),
|
|
187
|
+
outErrors: table.readBigUInt64LE(row + IF_ROW_OUT_ERRORS),
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
190
|
+
const map = new Map<number, OctetSample>();
|
|
191
|
+
for (const sample of rows) map.set(sample.interfaceIndex, sample);
|
|
192
|
+
return map;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Live per-interface throughput: two GetIfTable2 samples, octet deltas in bigint,
|
|
197
|
+
* narrowed to bytes/sec by the MEASURED elapsed time (so pacing quantization can't
|
|
198
|
+
* skew the rate). The dashboard primitive — sub-ms syscall, zero shell.
|
|
199
|
+
*/
|
|
200
|
+
export async function throughput(intervalMs = 1000): Promise<ThroughputSample[]> {
|
|
201
|
+
const first = sampleOctets();
|
|
202
|
+
const startNanos = Bun.nanoseconds();
|
|
203
|
+
await Bun.sleep(intervalMs);
|
|
204
|
+
const second = sampleOctets();
|
|
205
|
+
const elapsedSeconds = (Bun.nanoseconds() - startNanos) / 1_000_000_000;
|
|
206
|
+
const result: ThroughputSample[] = [];
|
|
207
|
+
for (const [interfaceIndex, later] of second) {
|
|
208
|
+
const earlier = first.get(interfaceIndex);
|
|
209
|
+
if (earlier === undefined) continue;
|
|
210
|
+
result.push({
|
|
211
|
+
interfaceIndex,
|
|
212
|
+
name: later.name,
|
|
213
|
+
rxBytesPerSec: Number(later.inOctets - earlier.inOctets) / elapsedSeconds,
|
|
214
|
+
txBytesPerSec: Number(later.outOctets - earlier.outOctets) / elapsedSeconds,
|
|
215
|
+
rxErrors: Number(later.inErrors - earlier.inErrors),
|
|
216
|
+
txErrors: Number(later.outErrors - earlier.outErrors),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
}
|
package/traceroute.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ICMP_SUCCESS, icmpStatusName } from './constants';
|
|
2
|
+
import { resolveIPv4, sendEcho } from './ping';
|
|
3
|
+
|
|
4
|
+
export interface TraceHop {
|
|
5
|
+
ttl: number;
|
|
6
|
+
address: string;
|
|
7
|
+
roundTripMs: number;
|
|
8
|
+
status: number;
|
|
9
|
+
statusText: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TracerouteOptions {
|
|
13
|
+
maxHops?: number;
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
payloadSize?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* No-admin, no-raw-socket traceroute: an IcmpSendEcho TTL ramp. A router that
|
|
20
|
+
* decrements TTL to 0 replies IP_TTL_EXPIRED_TRANSIT with its own address (the
|
|
21
|
+
* hop); the walk stops when the destination echoes back (IP_SUCCESS).
|
|
22
|
+
*/
|
|
23
|
+
export async function traceroute(host: string, options: TracerouteOptions = {}): Promise<TraceHop[]> {
|
|
24
|
+
const destination = resolveIPv4(host);
|
|
25
|
+
const maxHops = options.maxHops ?? 30;
|
|
26
|
+
const timeoutMs = options.timeoutMs ?? 2000;
|
|
27
|
+
const payloadSize = options.payloadSize ?? 32;
|
|
28
|
+
const hops: TraceHop[] = [];
|
|
29
|
+
for (let ttl = 1; ttl <= maxHops; ttl++) {
|
|
30
|
+
const echo = sendEcho(destination, ttl, timeoutMs, payloadSize);
|
|
31
|
+
hops.push({ ttl, address: echo.replied ? echo.address : '*', roundTripMs: echo.roundTripMs, status: echo.status, statusText: icmpStatusName(echo.status) });
|
|
32
|
+
if (echo.replied && echo.status === ICMP_SUCCESS) break;
|
|
33
|
+
}
|
|
34
|
+
return hops;
|
|
35
|
+
}
|