
Research
Two Malicious Rust Crates Impersonate Popular Logger to Steal Wallet Keys
Socket uncovers malicious Rust crates impersonating fast_log to steal Solana and Ethereum wallet keys from source code.
@web3-pi/tunnel
Advanced tools
Easily create a secure TCP tunnel between your Web3 Pi and a remote server.
A set of tools to tunnel TCP traffic from a local port to a remote port. Supports TLS and authentication. No dependencies other than node.js.
⚠️ Note: While this project is functional and stable, it is still in early development. It may contain bugs and the API could change in future versions. Use in production with caution.
Basic example (no TLS, no authentication):
import { TunnelServer } from "@web3-pi/tunnel";
const tunnelServer = new TunnelServer(); // No auth, no TLS
tunnelServer.events.on("main-server-start", ({ port, secure }) => {
console.log(`Tunnel control server started on port ${port} (TLS: ${secure})`);
});
tunnelServer.events.on(
"tunnel-created",
({ clientAuthenticationCredentials, secure, clientTunnel }) => {
const tunnelAddr = clientTunnel.tunnelAddress;
console.log(
`Tunnel created for client ${JSON.stringify(
clientAuthenticationCredentials
)} at public port ${tunnelAddr?.port} (TLS: ${secure})`
);
}
);
tunnelServer.events.on("client-disconnected", ({ clientTunnel }) => {
console.log(
`Client with credentials ${JSON.stringify(
clientTunnel.authenticationCredentials
)} disconnected`
);
});
tunnelServer.events.on("error", ({ err }) => {
console.error("Generic Server Error:", err);
});
tunnelServer.start(9000); // Start control server on port 9000
Server with authentication:
import { TunnelServer } from "@web3-pi/tunnel";
const tunnelServer = new TunnelServer({
// Only allow clients whose credentials have id === 'allowed-client'
connectionFilter: (credentials) => {
console.log("Authenticating client with credentials:", credentials);
return credentials?.id === "allowed-client";
},
// connectionFilter can also be async
// connectionFilter: async (credentials) => {
// console.log("Authenticating client with credentials:", credentials);
// const isAuthenticated = await someAsyncOperation(credentials);
// return isAuthenticated;
// },
});
// ... add event listeners ...
tunnelServer.start(9000);
Server with Hop-by-Hop TLS:
Requires a TLS certificate and key. For testing purposes, you can generate a self-signed certificate with openssl
:
# Generate a private key
openssl genpkey -algorithm RSA -out server-key.pem -pkeyopt rsa_keygen_bits:2048
# Generate a Certificate Signing Request (CSR)
openssl req -new -key server-key.pem -out server-csr.pem -subj "/CN=localhost"
# Generate a self-signed certificate valid for 365 days
openssl x509 -req -days 365 -in server-csr.pem -signkey server-key.pem -out server-cert.pem
# Clean up CSR (optional)
rm server-csr.pem
import { TunnelServer } from "@web3-pi/tunnel";
import fs from "node:fs";
import path from "node:path";
const tlsOptions = {
// Secure the main control channel
mainServer: {
key: fs.readFileSync(path.join(__dirname, "server-key.pem")),
cert: fs.readFileSync(path.join(__dirname, "server-cert.pem")),
},
// Also secure the visitor-facing tunnel ports
// It can be the same as the main server, or a different one
tunnelServer: {
key: fs.readFileSync(path.join(__dirname, "server-key.pem")),
cert: fs.readFileSync(path.join(__dirname, "server-cert.pem")),
},
};
const tunnelServer = new TunnelServer({
tls: tlsOptions,
});
// ... add event listeners ...
tunnelServer.start(9000);
Basic example (no TLS):
import { TunnelClient } from "@web3-pi/tunnel";
const client = new TunnelClient({
tunnelServerHost: "your-server-hostname.com", // Server hostname or IP
tunnelServerPort: 9000, // Server control port
localServicePort: 3000, // Local service port (e.g., web server)
authenticationCredentials: {
id: "allowed-client", // Credentials to send to server
// Add other credentials as needed
},
});
client.events.on("tunnel-connection-established", () => {
console.log("Established connection to the tunnel server");
});
client.events.on("authentication-credentials-sent", () => {
console.log("Sent authentication credentials to the tunnel server");
});
client.events.on("authentication-acknowledged", ({ assignedPort }) => {
console.log(`Authentication successful! Tunnel public port: ${assignedPort}`);
});
client.events.on("tunnel-disconnected", ({ hadError }) => {
console.log(
`Disconnected from tunnel server. Error: ${hadError}. Attempting reconnect...`
);
// Reconnect logic is handled internally by default
});
client.events.on("tunnel-error", ({ err }) => {
console.error("Tunnel connection error:", err);
});
client.events.on("service-error", ({ err }) => {
console.error(
"Error connecting to or communicating with local service:",
err
);
});
client.start();
Client connecting to TLS server:
import { TunnelClient } from "@web3-pi/tunnel";
import fs from "node:fs";
import path from "node:path";
const client = new TunnelClient({
tunnelServerHost: "your-server-hostname.com",
tunnelServerPort: 9001, // Connect to the server's TLS port
localServicePort: 3000,
authenticationCredentials: { id: "allowed-client" },
tls: {
// CA certificate needed to verify the server (if server uses self-signed cert)
ca: fs.readFileSync(path.join(__dirname, "server-cert.pem")),
// For production with valid certs, 'ca' might not be needed.
// rejectUnauthorized defaults to true (recommended). Set to false ONLY for testing.
// rejectUnauthorized: false, // DANGEROUS for production
},
});
// ... add event listeners ...
client.start();
The communication between the Tunnel Client and Tunnel Server uses a custom TCP-based protocol designed for multiplexing multiple streams over a single connection.
W3PTUNL
(7 bytes) +-------------------+------------------------------------+
| Length (4 bytes) | Message Body (Length bytes) |
+-------------------+------------------------------------+
REASONABLE_MAX_MESSAGE_LENGTH
) is in place to prevent excessively large declared lengths, potentially caused by corrupted data or malicious clients.The Message Body
contains the actual payload and control information. Its internal structure depends on the message type.
a) Handshake Message (0x00
)
+-------------+-------------------+------------------------------------+
| MAGIC_BYTES | Length (4 bytes) | Message Body (JSON String) |
+-------------+-------------------+------------------------------------+
{"id":"secret"}
). The specific structure depends on the server's connectionFilter
.{"port": 34567}
).b) Tunnel Messages (0x01
, 0x02
, 0x03
)
These messages are used after the initial handshake to manage and relay data for the individual TCP streams being tunneled.
+-------------------+------------------------------------+
| Length (4 bytes) | Message Body |
+-------------------+------------------------------------+
+-------------------+--------------------+---------------------+
| StreamID (4 bytes)| Msg Type (1 byte) | Payload (variable) |
+-------------------+--------------------+---------------------+
0x01
(data
): The Payload
contains raw TCP data to be forwarded.0x02
(close
): Indicates the stream associated with StreamID
has been closed cleanly by the sender. The Payload
is empty.0x03
(error
): Indicates an error occurred on the stream associated with StreamID
, forcing its closure. The Payload
is empty.data
messages. Contains the raw bytes received from either the visitor (Server -> Client) or the local service (Client -> Server).MAGIC_BYTES + Length + Handshake(Credentials)
message.MAGIC_BYTES
.Length
, then reads the Handshake
body.connectionFilter
.tunnel
) listening on a random available port.tunnel
server is listening, Server sends MAGIC_BYTES + Length + Handshak({"port": assigned_port})
message back to the Client.MAGIC_BYTES
.Length
, then reads the Handshake
body.assigned_port
. Tunnel is now established.tunnel
server on the assigned_port
.visitorSocket
.
StreamID
(a random uint32).StreamID
-> visitorSocket
.visitorSocket
receives data (chunk
).Length + DataMsg(StreamID, 0x01, chunk)
.DataMsg
.
Length
, StreamID
, MsgType
, Payload
.data
message for a new StreamID
.localSocket
) to localhost:localServicePort
.StreamID
-> localSocket
.Payload
to the localSocket
.localSocket
receives data (chunk
).Length + DataMsg(StreamID, 0x01, chunk)
.DataMsg
.
Length
, StreamID
, MsgType
, Payload
.visitorSocket
using StreamID
.Payload
to the visitorSocket
.visitorSocket
closes/errors, Server sends Length + CloseMsg(StreamID, 0x02/0x03)
to Client.Client finds localSocket
via StreamID
and destroys it.localSocket
closes/errors, Client sends Length + CloseMsg(StreamID, 0x02/0x03)
to Server. Server finds visitorSocket
via StreamID
and destroys it.This multiplexing allows many visitors to connect concurrently, each getting their own StreamID
and corresponding connection to the local service, all tunneled over the single persistent connection between the Client and Server.
This project requires node 23.x or higher. If you have nvm installed, you can set the version defined in .nvmrc
with:
nvm use
To install development dependencies:
npm install
To run tests use the integrated node test runner:
node --test tests/*.test.ts
To format your code using biome:
npm run format
FAQs
Easily create a secure TCP tunnel between your Web3 Pi and a remote server.
We found that @web3-pi/tunnel demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 3 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Socket uncovers malicious Rust crates impersonating fast_log to steal Solana and Ethereum wallet keys from source code.
Research
A malicious package uses a QR code as steganography in an innovative technique.
Research
/Security News
Socket identified 80 fake candidates targeting engineering roles, including suspected North Korean operators, exposing the new reality of hiring as a security function.