Debugging network traffic in an Electron app is usually straightforward. Open DevTools, click the Network tab, and Chromium shows you everything that flows through its own network stack. That works right up until the app you’re debugging doesn’t use Chromium’s network stack for the traffic you care about.
This comes up more than you’d think. A lot of production Electron apps ship with native addons, Rust libraries compiled to .node files, or dynamically linked C/C++ libraries that handle their own I/O. These libraries bypass XMLHttpRequest, fetch, and even Node’s http module entirely. They call directly into sockets, TLS libraries, or platform APIs. From Chromium’s perspective, the network tab stays blank.
A recent post on seg6.space documents one approach to this problem: use FFI to intercept calls inside the native library, proxy the captured traffic, and inject synthetic Chrome DevTools Protocol events directly into DevTools’ network panel. It’s the kind of hack that works precisely because it understands what the network tab actually is.
What the Network Tab Actually Is
The DevTools network panel isn’t a passive view of HTTP traffic. It’s a rendering of Chrome DevTools Protocol events emitted by the Network domain. When Chromium sends a request, the browser engine fires Network.requestWillBeSent. When the response headers arrive, it fires Network.responseReceived. When the body comes through, Network.dataReceived. When the load completes, Network.loadingFinished.
DevTools subscribes to these events over a WebSocket connection to the CDP endpoint and renders them. The panel has no independent visibility into actual socket traffic. It only knows what the CDP event stream tells it.
This means the network tab is fully injectable. If you can speak CDP to DevTools, you can make arbitrary requests and responses appear in the panel with full headers, timing, body content, and status codes. The panel has no way to verify that these events correspond to real Chromium network activity.
Electron exposes a CDP endpoint for both the main process and each renderer. By default, you launch it with --remote-debugging-port=9222 and connect via WebSocket to the target listed at http://localhost:9222/json. From there, the full CDP surface is available.
The FFI Layer
The harder problem is getting the data in the first place. If the app is using a native addon that calls into a C library for HTTP, you need to intercept those calls before the library sends them and after it receives responses.
The standard tool for this in Node.js/Electron is node-ffi-napi or its more modern replacement koffi. Both let you call into shared libraries from JavaScript and, critically, pass JavaScript functions as callbacks to C code.
const koffi = require('koffi');
const lib = koffi.load('libnetwork.so'); // or .dll, .dylib
// Declare the original function signature
const http_request = lib.func('http_request', 'int', [
'const char *', // url
'const char *', // method
'const char *', // body
]);
The deeper technique in the article goes beyond calling exported functions. To intercept calls made inside a library that doesn’t expose clean hooks, you need function hooking at the binary level. On Linux this means patching the PLT (Procedure Linkage Table), the mechanism the dynamic linker uses to resolve external symbols. On Windows you’re looking at IAT (Import Address Table) patching or inline hooking, where you overwrite the first bytes of a function with a jump to your handler.
Libraries like frida-gum expose this kind of interception from JavaScript, and Frida has first-class Electron support. You attach to the process, find the function you want to intercept by name or address, and install an interceptor:
Interceptor.attach(Module.getExportByName('libcurl.so.4', 'curl_easy_perform'), {
onEnter(args) {
this.handle = args[0];
// read URL, headers, body from the CURL handle
},
onLeave(retval) {
// read response status, headers, body
emitCDPNetworkEvents(this.capturedRequest, this.capturedResponse);
}
});
Building the CDP Injection
Once you have request and response data, injecting into the network tab requires a sequence of CDP calls over the WebSocket connection. You need to synthesize a requestId (any unique string), then fire the events in order with consistent timing:
const ws = new WebSocket('ws://localhost:9222/devtools/page/TARGET_ID');
function emitNetworkEvents(request, response) {
const requestId = crypto.randomUUID();
const now = Date.now();
send('Network.requestWillBeSent', {
requestId,
loaderId: requestId,
documentURL: request.url,
request: {
url: request.url,
method: request.method,
headers: request.headers,
postData: request.body,
},
timestamp: now / 1000,
wallTime: now / 1000,
initiator: { type: 'script' },
type: 'XHR',
});
send('Network.responseReceived', {
requestId,
loaderId: requestId,
timestamp: (now + response.timing) / 1000,
type: 'XHR',
response: {
url: request.url,
status: response.status,
statusText: response.statusText,
headers: response.headers,
mimeType: response.mimeType,
},
});
send('Network.loadingFinished', {
requestId,
timestamp: (now + response.timing) / 1000,
encodedDataLength: response.body.length,
});
}
The Network.getResponseBody CDP method can also be handled to let DevTools show the full response body when you click a request. This requires caching the body on your side and responding to the CDP method call with the stored data.
Why Not Just Use a Proxy
The obvious question is why this is necessary when tools like mitmproxy, Charles Proxy, or Proxyman exist. The answer is that all of these tools intercept at the HTTP layer, and they depend on the application routing its traffic through a proxy, either via environment variables (HTTP_PROXY, HTTPS_PROXY), system proxy settings, or explicit configuration.
Native libraries that manage their own sockets often ignore all of these. A Rust library using reqwest will respect HTTP_PROXY if it’s built to do so. A C library using raw POSIX sockets with its own TLS implementation might not. Certificate pinning makes HTTPS interception impossible without patching the binary anyway.
Wireshark sits lower still and can capture everything, but it gives you raw packets. Reconstructing HTTP/2 streams from QUIC packets with application-level decryption is not a comfortable way to debug API calls. The network tab gives you structured request/response pairs with decoded headers and formatted bodies; that’s worth engineering toward.
The Broader Tooling Gap
The technique in the article is a symptom of a tooling gap that’s grown as Electron apps have gotten more complex. The initial promise of Electron was that you get full DevTools support for free. That’s true for pure-JavaScript apps. Once native code enters the picture, the abstraction breaks and you’re back to lower-level techniques.
Frida has become the standard tool for this class of problem. It works on Electron because Electron is just a V8 runtime with Chromium, and Frida can attach to any process. The frida-tools ecosystem includes scripts for common interception scenarios, and the community has built adapters for visualizing captured traffic in Burp Suite, mitmproxy, and other tools.
What makes the CDP injection approach particularly clean is that it puts the visualization where developers already spend time. You don’t need a separate proxy UI or a terminal full of hex dumps. The data lands in the tool you’re already using, formatted the way you’re used to reading it. The network tab was built for exactly this kind of structured request/response visualization; it just needed a path to receive data that didn’t originate from Chromium’s own stack.
The implementation requires understanding three things that don’t usually appear together: binary-level interception via FFI or a hooking library, the CDP event schema for the Network domain, and the WebSocket plumbing to connect them. Each piece is documented. The insight is recognizing that they compose.