Skip to content

Commit d9a51b0

Browse files
committed
fix: support TCP RTT on Windows
1 parent b5e7487 commit d9a51b0

7 files changed

Lines changed: 598 additions & 8 deletions

File tree

.github/workflows/nodejs.yml

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,18 @@ jobs:
1414

1515
steps:
1616
- uses: actions/checkout@v4
17+
- uses: pnpm/action-setup@v4
18+
with:
19+
version: 10.29.3
1720
- name: Use Node.js ${{ matrix.node-version }}
1821
uses: actions/setup-node@v4
1922
with:
2023
node-version: ${{ matrix.node-version }}
21-
- name: npm install, build, and test
24+
cache: pnpm
25+
- name: pnpm install, build, and test
2226
run: |
23-
npm install
24-
npm run test
27+
pnpm install --frozen-lockfile
28+
pnpm test
2529
env:
2630
CI: true
2731

@@ -35,14 +39,18 @@ jobs:
3539

3640
steps:
3741
- uses: actions/checkout@v4
42+
- uses: pnpm/action-setup@v4
43+
with:
44+
version: 10.29.3
3845
- name: Use Node.js ${{ matrix.node-version }}
3946
uses: actions/setup-node@v4
4047
with:
4148
node-version: ${{ matrix.node-version }}
49+
cache: pnpm
4250
- name: Build native prebuild
4351
run: |
44-
npm install
45-
npm run native:prebuild
52+
pnpm install --frozen-lockfile
53+
pnpm native:prebuild
4654
env:
4755
CI: true
4856
- uses: actions/upload-artifact@v4

app/dimensions/tcprtt/nativeprovider.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ interface NativeTcpRttResult {
1414

1515
interface NativeTcpRttAddon {
1616
getTcpRttMicros(fd: number): NativeTcpRttResult;
17+
getTcpRttMicrosByEndpoint?: (localAddress: string, localPort: number, remoteAddress: string, remotePort: number) => NativeTcpRttResult;
1718
}
1819

1920
type SocketWithHandle = Net.Socket & {
@@ -27,6 +28,28 @@ function getSocketFd(socket: Net.Socket): number | null {
2728
return typeof fd === "number" && Number.isFinite(fd) ? fd : null;
2829
}
2930

31+
function getSocketEndpoint(socket: Net.Socket): { localAddress: string, localPort: number, remoteAddress: string, remotePort: number } | null {
32+
const localAddress = socket.localAddress;
33+
const localPort = socket.localPort;
34+
const remoteAddress = socket.remoteAddress;
35+
const remotePort = socket.remotePort;
36+
if (
37+
typeof localAddress !== "string" ||
38+
typeof localPort !== "number" ||
39+
typeof remoteAddress !== "string" ||
40+
typeof remotePort !== "number"
41+
) {
42+
return null;
43+
}
44+
45+
return {
46+
localAddress,
47+
localPort,
48+
remoteAddress,
49+
remotePort,
50+
};
51+
}
52+
3053
function getPackageRoot(): string {
3154
const dirname = path.dirname(fileURLToPath(import.meta.url));
3255
return path.resolve(dirname, "../../..");
@@ -55,6 +78,10 @@ class NativeTcpRttProvider implements TcpRttProvider {
5578
return unavailableTcpRttSample(this.loadError ?? "native TCP RTT addon unavailable");
5679
}
5780

81+
if (process.platform === "win32") {
82+
return this.sampleWindows(socket);
83+
}
84+
5885
const fd = getSocketFd(socket);
5986
if (fd === null) {
6087
return unavailableTcpRttSample("socket file descriptor unavailable");
@@ -73,6 +100,30 @@ class NativeTcpRttProvider implements TcpRttProvider {
73100

74101
return tcpInfoSample(result.rttMicros);
75102
}
103+
104+
private sampleWindows(socket: Net.Socket): TcpRttSample {
105+
if (this.addon?.getTcpRttMicrosByEndpoint === undefined) {
106+
return unavailableTcpRttSample("native TCP RTT addon does not support Windows endpoint lookup");
107+
}
108+
109+
const endpoint = getSocketEndpoint(socket);
110+
if (endpoint === null) {
111+
return unavailableTcpRttSample("socket endpoint unavailable");
112+
}
113+
114+
let result: NativeTcpRttResult;
115+
try {
116+
result = this.addon.getTcpRttMicrosByEndpoint(endpoint.localAddress, endpoint.localPort, endpoint.remoteAddress, endpoint.remotePort);
117+
} catch (e) {
118+
return unavailableTcpRttSample(ErrorHelper.toMessage(e));
119+
}
120+
121+
if (!result.available || typeof result.rttMicros !== "number") {
122+
return unavailableTcpRttSample(result.error ?? "TCP RTT unavailable");
123+
}
124+
125+
return tcpInfoSample(result.rttMicros);
126+
}
76127
}
77128

78129
export default NativeTcpRttProvider;

makerelease.sh

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ cp native-addon/binding.gyp release/native-addon/binding.gyp
1212
cp native-addon/tcp_rtt.cc release/native-addon/tcp_rtt.cc
1313
if [ -d native-addon/prebuilds ]; then
1414
cp -r native-addon/prebuilds release/native-addon/prebuilds
15-
fi
16-
if [ -d native-addon/build/Release ]; then
15+
elif [ -d native-addon/build/Release ]; then
1716
mkdir -p release/native-addon/build
1817
cp -r native-addon/build/Release release/native-addon/build/Release
1918
fi

native-addon/binding.gyp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"OS==\"win\"",
1414
{
1515
"libraries": [
16-
"ws2_32.lib"
16+
"ws2_32.lib",
17+
"iphlpapi.lib"
1718
]
1819
}
1920
]

native-addon/tcp_rtt.cc

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
#include <winsock2.h>
1212
#include <ws2tcpip.h>
1313
#include <mstcpip.h>
14+
#include <iphlpapi.h>
15+
#include <tcpestats.h>
16+
#include <vector>
1417
#else
1518
#include <errno.h>
1619
#include <limits.h>
@@ -59,6 +62,177 @@ static napi_value Unavailable(napi_env env, const char* error) {
5962
return result;
6063
}
6164

65+
#if defined(_WIN32)
66+
static void FormatWindowsError(const char* operation, DWORD error, char* message, size_t message_size) {
67+
snprintf(message, message_size, "%s failed: %lu", operation, static_cast<unsigned long>(error));
68+
}
69+
70+
static bool GetStringArg(napi_env env, napi_value value, char* buffer, size_t buffer_size) {
71+
size_t length = 0;
72+
if (napi_get_value_string_utf8(env, value, buffer, buffer_size, &length) != napi_ok) {
73+
return false;
74+
}
75+
76+
return length > 0 && length < buffer_size;
77+
}
78+
79+
static bool GetPortArg(napi_env env, napi_value value, uint16_t* port) {
80+
uint32_t port_value = 0;
81+
if (napi_get_value_uint32(env, value, &port_value) != napi_ok) {
82+
return false;
83+
}
84+
85+
if (port_value == 0 || port_value > 65535) {
86+
return false;
87+
}
88+
89+
*port = static_cast<uint16_t>(port_value);
90+
return true;
91+
}
92+
93+
static bool ParseIpv4Address(const char* address, DWORD* parsed_address) {
94+
const char* normalized_address = address;
95+
if (strncmp(address, "::ffff:", 7) == 0) {
96+
normalized_address = address + 7;
97+
}
98+
99+
IN_ADDR in_address;
100+
if (InetPtonA(AF_INET, normalized_address, &in_address) != 1) {
101+
return false;
102+
}
103+
104+
*parsed_address = in_address.S_un.S_addr;
105+
return true;
106+
}
107+
108+
static bool FindTcpRowByEndpoint(
109+
DWORD local_address,
110+
uint16_t local_port,
111+
DWORD remote_address,
112+
uint16_t remote_port,
113+
MIB_TCPROW* row,
114+
char* error,
115+
size_t error_size
116+
) {
117+
DWORD table_size = 0;
118+
DWORD result = GetTcpTable(nullptr, &table_size, FALSE);
119+
if (result != ERROR_INSUFFICIENT_BUFFER) {
120+
FormatWindowsError("GetTcpTable(size)", result, error, error_size);
121+
return false;
122+
}
123+
124+
std::vector<unsigned char> buffer(table_size);
125+
PMIB_TCPTABLE table = reinterpret_cast<PMIB_TCPTABLE>(buffer.data());
126+
result = GetTcpTable(table, &table_size, FALSE);
127+
if (result != NO_ERROR) {
128+
FormatWindowsError("GetTcpTable", result, error, error_size);
129+
return false;
130+
}
131+
132+
const DWORD local_port_network_order = static_cast<DWORD>(htons(local_port));
133+
const DWORD remote_port_network_order = static_cast<DWORD>(htons(remote_port));
134+
for (DWORD i = 0; i < table->dwNumEntries; i++) {
135+
const MIB_TCPROW& candidate = table->table[i];
136+
if (
137+
candidate.dwLocalAddr == local_address &&
138+
candidate.dwLocalPort == local_port_network_order &&
139+
candidate.dwRemoteAddr == remote_address &&
140+
candidate.dwRemotePort == remote_port_network_order
141+
) {
142+
*row = candidate;
143+
return true;
144+
}
145+
}
146+
147+
snprintf(error, error_size, "TCP connection not found in IPv4 table");
148+
return false;
149+
}
150+
151+
static napi_value GetTcpRttMicrosByEndpoint(napi_env env, napi_callback_info info) {
152+
size_t argc = 4;
153+
napi_value args[4];
154+
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
155+
if (argc < 4) {
156+
return Unavailable(env, "local address, local port, remote address, and remote port arguments are required");
157+
}
158+
159+
char local_address_string[96];
160+
char remote_address_string[96];
161+
if (!GetStringArg(env, args[0], local_address_string, sizeof(local_address_string))) {
162+
return Unavailable(env, "local address must be a non-empty string");
163+
}
164+
165+
uint16_t local_port = 0;
166+
if (!GetPortArg(env, args[1], &local_port)) {
167+
return Unavailable(env, "local port must be a TCP port from 1 to 65535");
168+
}
169+
170+
if (!GetStringArg(env, args[2], remote_address_string, sizeof(remote_address_string))) {
171+
return Unavailable(env, "remote address must be a non-empty string");
172+
}
173+
174+
uint16_t remote_port = 0;
175+
if (!GetPortArg(env, args[3], &remote_port)) {
176+
return Unavailable(env, "remote port must be a TCP port from 1 to 65535");
177+
}
178+
179+
DWORD local_address = 0;
180+
if (!ParseIpv4Address(local_address_string, &local_address)) {
181+
return Unavailable(env, "local address is not an IPv4 address");
182+
}
183+
184+
DWORD remote_address = 0;
185+
if (!ParseIpv4Address(remote_address_string, &remote_address)) {
186+
return Unavailable(env, "remote address is not an IPv4 address");
187+
}
188+
189+
char error[128];
190+
MIB_TCPROW row;
191+
memset(&row, 0, sizeof(row));
192+
if (!FindTcpRowByEndpoint(local_address, local_port, remote_address, remote_port, &row, error, sizeof(error))) {
193+
return Unavailable(env, error);
194+
}
195+
196+
TCP_ESTATS_FINE_RTT_RW_v0 rw;
197+
memset(&rw, 0, sizeof(rw));
198+
rw.EnableCollection = TRUE;
199+
DWORD result = SetPerTcpConnectionEStats(
200+
&row,
201+
TcpConnectionEstatsFineRtt,
202+
reinterpret_cast<PUCHAR>(&rw),
203+
0,
204+
sizeof(rw),
205+
0
206+
);
207+
if (result != NO_ERROR) {
208+
FormatWindowsError("SetPerTcpConnectionEStats(TcpConnectionEstatsFineRtt)", result, error, sizeof(error));
209+
return Unavailable(env, error);
210+
}
211+
212+
TCP_ESTATS_FINE_RTT_ROD_v0 rod;
213+
memset(&rod, 0, sizeof(rod));
214+
result = GetPerTcpConnectionEStats(
215+
&row,
216+
TcpConnectionEstatsFineRtt,
217+
nullptr,
218+
0,
219+
0,
220+
nullptr,
221+
0,
222+
0,
223+
reinterpret_cast<PUCHAR>(&rod),
224+
0,
225+
sizeof(rod)
226+
);
227+
if (result != NO_ERROR) {
228+
FormatWindowsError("GetPerTcpConnectionEStats(TcpConnectionEstatsFineRtt)", result, error, sizeof(error));
229+
return Unavailable(env, error);
230+
}
231+
232+
return Available(env, rod.SumRtt);
233+
}
234+
#endif
235+
62236
static napi_value GetTcpRttMicros(napi_env env, napi_callback_info info) {
63237
size_t argc = 1;
64238
napi_value args[1];
@@ -127,6 +301,10 @@ static napi_value Init(napi_env env, napi_value exports) {
127301
napi_value fn;
128302
napi_create_function(env, nullptr, 0, GetTcpRttMicros, nullptr, &fn);
129303
napi_set_named_property(env, exports, "getTcpRttMicros", fn);
304+
#if defined(_WIN32)
305+
napi_create_function(env, nullptr, 0, GetTcpRttMicrosByEndpoint, nullptr, &fn);
306+
napi_set_named_property(env, exports, "getTcpRttMicrosByEndpoint", fn);
307+
#endif
130308
return exports;
131309
}
132310

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"name": "@popstarfreas/dimensions",
33
"version": "7.1.0-alpha.3",
44
"type": "module",
5+
"packageManager": "pnpm@10.29.3",
56
"repository": {
67
"type": "git",
78
"url": "git://github.com/popstarfreas/Dimensions.git"

0 commit comments

Comments
 (0)