use socks5 instead of HTTP Proxy to allow more protocols
This commit is contained in:
381
BUILD.md
381
BUILD.md
@@ -1,37 +1,45 @@
|
||||
# PSK-Proxy-Tunnel Build Guide
|
||||
# PSK-Proxy-Tunnel Build & Usage Guide
|
||||
|
||||
This guide explains how to build single executable binaries for the PSK-Proxy-Tunnel project using the `pkg` tool.
|
||||
This guide explains how to build single executable binaries and how to run the TLS-PSK tunnel with a local SOCKS5 proxy client (supporting TCP CONNECT and UDP ASSOCIATE).
|
||||
|
||||
Key changes:
|
||||
- Local proxy is now SOCKS5 (replaces the previous HTTP proxy).
|
||||
- The tunnel supports multiplexed TCP and UDP relaying.
|
||||
- Existing frame protocol extended with UDP_* frames for SOCKS5 UDP ASSOCIATE.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Node.js**: Version 18.0.0 or higher
|
||||
- **npm**: Usually comes with Node.js
|
||||
- **Git**: To clone the repository
|
||||
- Node.js: Version 18.0.0 or higher
|
||||
- npm: Usually comes with Node.js
|
||||
- Git: To clone the repository
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Install Dependencies
|
||||
### 1) Install dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2. Build for Current Platform
|
||||
### 2) Build for your current platform
|
||||
|
||||
```bash
|
||||
# macOS/Linux: auto-detect current platform via script
|
||||
# macOS/Linux
|
||||
./build.sh
|
||||
|
||||
# Windows: auto-detect Windows via script
|
||||
# Windows
|
||||
build.bat
|
||||
|
||||
# Or run a specific platform via npm
|
||||
npm run build:macos # macOS
|
||||
npm run build:linux # Linux
|
||||
npm run build:windows # Windows
|
||||
```
|
||||
|
||||
### 3. Build for All Platforms
|
||||
Or via npm directly:
|
||||
|
||||
```bash
|
||||
npm run build:macos # macOS
|
||||
npm run build:linux # Linux
|
||||
npm run build:windows # Windows
|
||||
```
|
||||
|
||||
### 3) Build for all platforms
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
@@ -39,133 +47,217 @@ npm run build
|
||||
|
||||
## Build Scripts
|
||||
|
||||
### Using the Build Scripts
|
||||
### Using the build scripts
|
||||
|
||||
#### Unix/Linux/macOS
|
||||
Unix/Linux/macOS:
|
||||
```bash
|
||||
# Make the script executable
|
||||
chmod +x build.sh
|
||||
|
||||
# Build for current platform
|
||||
./build.sh
|
||||
|
||||
# Build for specific platform
|
||||
./build.sh # current platform
|
||||
./build.sh --platform macos
|
||||
./build.sh --platform linux
|
||||
./build.sh --platform windows
|
||||
|
||||
# Build for all platforms
|
||||
./build.sh --all
|
||||
|
||||
# Clean build artifacts
|
||||
./build.sh --clean
|
||||
|
||||
# Show help
|
||||
./build.sh --all # all platforms
|
||||
./build.sh --clean # remove dist/
|
||||
./build.sh --help
|
||||
```
|
||||
|
||||
#### Windows
|
||||
Windows:
|
||||
```cmd
|
||||
# Build for Windows (default)
|
||||
build.bat
|
||||
|
||||
# Build for all platforms
|
||||
build.bat --all
|
||||
|
||||
# Clean build artifacts
|
||||
build.bat # Windows (default)
|
||||
build.bat --all # all platforms
|
||||
build.bat --clean
|
||||
|
||||
# Show help
|
||||
build.bat --help
|
||||
```
|
||||
|
||||
### Using npm Scripts Directly
|
||||
### Using npm scripts directly
|
||||
|
||||
```bash
|
||||
# Build for specific platform
|
||||
npm run build:macos
|
||||
npm run build:linux
|
||||
npm run build:windows
|
||||
|
||||
# Build for all platforms
|
||||
npm run build
|
||||
|
||||
# Clean build artifacts
|
||||
npm run clean
|
||||
npm run build # all platforms
|
||||
npm run clean # remove dist/
|
||||
```
|
||||
|
||||
## Build Output
|
||||
|
||||
After building, you'll find the executables in the `dist/` directory:
|
||||
Executables are created in `dist/`:
|
||||
|
||||
```
|
||||
dist/
|
||||
├── psk-proxy-server-macos # macOS server executable
|
||||
├── psk-proxy-client-macos # macOS client executable
|
||||
├── psk-proxy-server-linux # Linux server executable
|
||||
├── psk-proxy-client-linux # Linux client executable
|
||||
├── psk-proxy-server-windows.exe # Windows server executable
|
||||
└── psk-proxy-client-windows.exe # Windows client executable
|
||||
├── psk-proxy-server-macos
|
||||
├── psk-proxy-client-macos
|
||||
├── psk-proxy-server-linux
|
||||
├── psk-proxy-client-linux
|
||||
├── psk-proxy-server-windows.exe
|
||||
└── psk-proxy-client-windows.exe
|
||||
```
|
||||
|
||||
## Platform-Specific Builds
|
||||
## Running the Server and Client
|
||||
|
||||
### macOS
|
||||
The PSK (pre-shared key) file must contain a hex-encoded key string used by both sides. Example (256-bit key):
|
||||
```
|
||||
0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
|
||||
```
|
||||
|
||||
### Server (Out-Node)
|
||||
|
||||
- Listens for a single TLS-PSK tunnel connection from the client.
|
||||
- Performs outbound TCP connects and UDP sends on behalf of the client.
|
||||
|
||||
macOS/Linux:
|
||||
```bash
|
||||
npm run build:macos
|
||||
# Creates: psk-proxy-server-macos, psk-proxy-client-macos
|
||||
./dist/psk-proxy-server-macos \
|
||||
--tunnel-port 8443 \
|
||||
--host 0.0.0.0 \
|
||||
--psk-file /path/to/psk.hex
|
||||
```
|
||||
|
||||
### Linux
|
||||
Windows:
|
||||
```cmd
|
||||
.\dist\psk-proxy-server-windows.exe ^
|
||||
--tunnel-port 8443 ^
|
||||
--host 0.0.0.0 ^
|
||||
--psk-file C:\path\to\psk.hex
|
||||
```
|
||||
|
||||
Required options:
|
||||
- `--tunnel-port <port>`: TLS-PSK tunnel port
|
||||
- `--host <host>`: Bind host (e.g., 0.0.0.0)
|
||||
- `--psk-file <path>`: File containing hex PSK
|
||||
|
||||
Optional:
|
||||
- `--connect-timeout <ms>`: Outbound TCP connect timeout (default 10000)
|
||||
|
||||
### Client (Local SOCKS5 Proxy)
|
||||
|
||||
- Runs a local SOCKS5 proxy (TCP CONNECT and UDP ASSOCIATE).
|
||||
- Multiplexes many local connections over one TLS-PSK tunnel to the server.
|
||||
|
||||
macOS/Linux:
|
||||
```bash
|
||||
npm run build:linux
|
||||
# Creates: psk-proxy-server-linux, psk-proxy-client-linux
|
||||
./dist/psk-proxy-client-macos \
|
||||
--server-host server.example.com \
|
||||
--server-port 8443 \
|
||||
--psk-file /path/to/psk.hex \
|
||||
--identity client1 \
|
||||
--socks-port 1080 \
|
||||
--bind-host 127.0.0.1
|
||||
```
|
||||
|
||||
### Windows
|
||||
Windows:
|
||||
```cmd
|
||||
.\dist\psk-proxy-client-windows.exe ^
|
||||
--server-host server.example.com ^
|
||||
--server-port 8443 ^
|
||||
--psk-file C:\path\to\psk.hex ^
|
||||
--identity client1 ^
|
||||
--socks-port 1080 ^
|
||||
--bind-host 127.0.0.1
|
||||
```
|
||||
|
||||
Required options:
|
||||
- `--server-host <host>`: Remote out-node address
|
||||
- `--server-port <port>`: Remote out-node port
|
||||
- `--psk-file <path>`: File containing hex PSK
|
||||
- `--identity <id>`: Identity string (logged on server)
|
||||
- `--socks-port <port>`: Local SOCKS5 proxy port
|
||||
|
||||
Optional:
|
||||
- `--bind-host <host>`: Local bind host (default `127.0.0.1`)
|
||||
- `--connect-timeout <ms>`: Waiting time for OPEN/UDP_OPEN result (default 10000)
|
||||
- `--idle-timeout <ms>`: Idle timeout for TCP sockets (default 60000, 0=disabled)
|
||||
- `--udp-idle-timeout <ms>`: Idle timeout for UDP association (default 60000, 0=disabled)
|
||||
|
||||
## Protocol Summary
|
||||
|
||||
All multiplexed over a single TLS-PSK socket:
|
||||
|
||||
Header (9 bytes):
|
||||
- 1 byte type
|
||||
- 4 bytes connection id (unsigned)
|
||||
- 4 bytes payload length (unsigned)
|
||||
- payload bytes
|
||||
|
||||
Types:
|
||||
- TCP:
|
||||
- DATA (2): stream data
|
||||
- CLOSE (3): close stream
|
||||
- OPEN (4): payload = [2B hostLen][host][2B port]
|
||||
- OPEN_RESULT (5): payload = [1B status] (1=success, 0=failure)
|
||||
- UDP (for SOCKS5 UDP ASSOCIATE):
|
||||
- UDP_OPEN (6): payload empty
|
||||
- UDP_OPEN_RESULT (7): payload = [1B status] (1=success, 0=failure)
|
||||
- UDP_SEND (8): payload = [2B hostLen][host][2B port][2B dataLen][data]
|
||||
- UDP_RECV (9): same layout as UDP_SEND
|
||||
- UDP_CLOSE (10): payload empty
|
||||
|
||||
## Testing
|
||||
|
||||
Assuming the client runs a local SOCKS5 proxy at 127.0.0.1:1080:
|
||||
|
||||
- HTTP over SOCKS5 (remote DNS resolution via socks5h):
|
||||
```bash
|
||||
curl -x socks5h://127.0.0.1:1080 http://example.com/
|
||||
```
|
||||
|
||||
- HTTPS over SOCKS5:
|
||||
```bash
|
||||
curl -x socks5h://127.0.0.1:1080 https://ifconfig.me
|
||||
```
|
||||
|
||||
- SSH over SOCKS5 (using `nc` as ProxyCommand):
|
||||
```bash
|
||||
ssh -o ProxyCommand="nc -x 127.0.0.1:1080 -X 5 %h %p" user@your.remote.host
|
||||
```
|
||||
|
||||
- Applications with SOCKS5 (TCP and UDP):
|
||||
- Many apps support SOCKS5 TCP CONNECT (browsers, package managers, etc.).
|
||||
- For UDP ASSOCIATE (e.g., some torrent clients, DNS forwarders with SOCKS5-UDP support), configure them to use 127.0.0.1:1080 SOCKS5 and enable UDP if supported. Note: many CLI tools do not implement SOCKS5 UDP.
|
||||
|
||||
Notes:
|
||||
- `socks5h` in curl ensures DNS names are resolved through the proxy.
|
||||
- UDP test coverage depends on the client application having SOCKS5 UDP support.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
1) "pkg command not found"
|
||||
```bash
|
||||
npm run build:windows
|
||||
# Creates: psk-proxy-server-windows.exe, psk-proxy-client-windows.exe
|
||||
npm install -g pkg
|
||||
# or
|
||||
npx pkg proxy-server.js
|
||||
```
|
||||
|
||||
## Cross-Platform Building
|
||||
|
||||
You can build for other platforms from any operating system:
|
||||
|
||||
2) Build fails with permission errors
|
||||
```bash
|
||||
# Build for all platforms from macOS
|
||||
npm run build
|
||||
|
||||
# Build for all platforms from Linux
|
||||
npm run build
|
||||
|
||||
# Build for all platforms from Windows
|
||||
npm run build
|
||||
chmod +x build.sh
|
||||
sudo npm run build # if necessary
|
||||
```
|
||||
|
||||
## Using the Built Executables
|
||||
3) Executable doesn't run on target platform
|
||||
- Ensure you built for the correct target
|
||||
- Verify executable permissions
|
||||
- Confirm compatible Node target in `pkg` config
|
||||
|
||||
### Server
|
||||
```bash
|
||||
# macOS/Linux
|
||||
./dist/psk-proxy-server-macos --tunnel-port 8443 --tcp-port 8080 --host 0.0.0.0 --psk-file /path/to/psk.key
|
||||
4) Tunnel connects but traffic fails
|
||||
- Confirm both server and client use the exact same hex PSK
|
||||
- Check connectivity from server to target hosts/ports (firewall, outbound rules)
|
||||
- Use `--connect-timeout` to tune behavior
|
||||
|
||||
# Windows
|
||||
.\dist\psk-proxy-server-windows.exe --tunnel-port 8443 --tcp-port 8080 --host 0.0.0.0 --psk-file C:\path\to\psk.key
|
||||
```
|
||||
5) SOCKS5 UDP doesn't appear to work
|
||||
- Confirm your application actually supports SOCKS5 UDP ASSOCIATE
|
||||
- Check that the client printed "Local SOCKS5 proxy listening ..."
|
||||
- Inspect server logs for UDP_* activity
|
||||
|
||||
### Client
|
||||
```bash
|
||||
# macOS/Linux
|
||||
./dist/psk-proxy-client-macos --server-host server.example.com --server-port 8443 --origin-host 127.0.0.1 --origin-port 8080 --psk-file /path/to/psk.key --identity client1
|
||||
## Performance Considerations
|
||||
|
||||
# Windows
|
||||
.\dist\psk-proxy-client-windows.exe --server-host server.example.com --server-port 8443 --origin-host 127.0.0.1 --origin-port 8080 --psk-file C:\path\to\psk.key --identity client1
|
||||
```
|
||||
- Single tunnel multiplexes many flows (reduced connection overhead).
|
||||
- UDP sockets are ephemeral and idle-timed to conserve resources.
|
||||
- TCP and UDP buffers are kept minimal; tune OS limits as needed.
|
||||
|
||||
## Build Configuration
|
||||
|
||||
The build configuration is defined in `package.json` under the `pkg` section:
|
||||
Defined in `package.json` under `pkg`:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -181,103 +273,8 @@ The build configuration is defined in `package.json` under the `pkg` section:
|
||||
}
|
||||
```
|
||||
|
||||
### Available Targets
|
||||
|
||||
- `node18-macos-x64`: macOS 64-bit
|
||||
- `node18-linux-x64`: Linux 64-bit
|
||||
- `node18-win-x64`: Windows 64-bit
|
||||
|
||||
You can modify these targets to support different Node.js versions or architectures.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **"pkg command not found"**
|
||||
```bash
|
||||
npm install -g pkg
|
||||
# or use the local version
|
||||
npx pkg proxy-server.js
|
||||
```
|
||||
|
||||
2. **Build fails with permission errors**
|
||||
```bash
|
||||
# Make sure the build script is executable
|
||||
chmod +x build.sh
|
||||
|
||||
# Or run with sudo if needed
|
||||
sudo npm run build
|
||||
```
|
||||
|
||||
3. **Executable doesn't run on target platform**
|
||||
- Ensure you're building for the correct target platform
|
||||
- Check that the target platform supports the Node.js version you're using
|
||||
- Verify the executable has proper permissions
|
||||
|
||||
4. **Missing dependencies in built executable**
|
||||
- The `pkg` tool automatically bundles Node.js built-in modules
|
||||
- External dependencies are included automatically
|
||||
- If you have custom assets, add them to the `pkg.assets` array
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
- **File Size**: Single executables are larger than the source code because they include Node.js runtime
|
||||
- **Startup Time**: Slightly slower startup compared to running with `node` directly
|
||||
- **Memory Usage**: Similar to running with Node.js directly
|
||||
|
||||
### Security Notes
|
||||
|
||||
- Built executables contain your source code (though it's compiled)
|
||||
- The `pkg` tool doesn't obfuscate or encrypt your code
|
||||
- Consider this when distributing executables with sensitive information
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Custom pkg Options
|
||||
|
||||
You can customize the build process by modifying the `pkg` section in `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"pkg": {
|
||||
"assets": [
|
||||
"config/*.json",
|
||||
"templates/*.html"
|
||||
],
|
||||
"scripts": [
|
||||
"scripts/**/*.js"
|
||||
],
|
||||
"targets": [
|
||||
"node18-macos-x64",
|
||||
"node18-linux-x64",
|
||||
"node18-win-x64"
|
||||
],
|
||||
"outputPath": "custom-dist"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
You can set environment variables to customize the build:
|
||||
|
||||
```bash
|
||||
# Set custom output directory
|
||||
PKG_OUTPUT_PATH=./my-dist npm run build
|
||||
|
||||
# Set custom Node.js version
|
||||
PKG_NODE_VERSION=16 npm run build
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding new build targets or modifying the build process:
|
||||
|
||||
1. Update the `package.json` scripts
|
||||
2. Update the build scripts (`build.sh` and `build.bat`)
|
||||
3. Test builds on all target platforms
|
||||
4. Update this documentation
|
||||
|
||||
## License
|
||||
|
||||
This build system is part of the PSK-Proxy-Tunnel project and follows the same license terms.
|
||||
- Keep CLI flags in `proxy-client.js` and `proxy-server.js` in sync with this document.
|
||||
- Update build scripts (`build.sh`, `build.bat`) when adding platforms.
|
||||
- Test TCP (HTTP/HTTPS/SSH) and at least one UDP-capable client where possible.
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "psk-proxy-tunnel",
|
||||
"version": "1.0.0",
|
||||
"description": "TLS-PSK multiplexed TCP tunnel server and local HTTP proxy client for secure NAT traversal and TCP forwarding",
|
||||
"description": "TLS-PSK multiplexed TCP+UDP tunnel server and local SOCKS5 proxy client (CONNECT and UDP ASSOCIATE) for secure NAT traversal and protocol forwarding",
|
||||
"main": "proxy-client.js",
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
|
779
proxy-client.js
779
proxy-client.js
@@ -1,9 +1,12 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* PSK HTTP/HTTPS Proxy (Client)
|
||||
* PSK SOCKS5 Proxy (Client)
|
||||
*
|
||||
* - Runs a local HTTP proxy (supports CONNECT for HTTPS and absolute/origin-form HTTP for port 80)
|
||||
* - Runs a local SOCKS5 proxy (supports:
|
||||
* - CONNECT for arbitrary TCP protocols (HTTP, HTTPS, SSH, etc.)
|
||||
* - UDP ASSOCIATE for UDP-based protocols (e.g., DNS)
|
||||
* - BIND: not supported, returns error)
|
||||
* - Maintains a single TLS-PSK tunnel to a remote out-node (proxy-server.js)
|
||||
* - Multiplexes many local client connections over one TLS tunnel using frames:
|
||||
*
|
||||
@@ -11,28 +14,32 @@
|
||||
* Types:
|
||||
* DATA (2)
|
||||
* CLOSE (3)
|
||||
* OPEN (4) payload = [2B hostLen][host utf8][2B port]
|
||||
* OPEN_RESULT (5) payload = [1B status] 1=success,0=failure
|
||||
* OPEN (4) payload = [2B hostLen][host utf8][2B port]
|
||||
* OPEN_RESULT (5) payload = [1B status] 1=success,0=failure
|
||||
*
|
||||
* HTTPS: Use CONNECT method. We respond 200 after OPEN_RESULT success, then tunnel raw TLS.
|
||||
* HTTP: Convert absolute-form to origin-form and forward over opened upstream (default port 80 if not specified).
|
||||
* UDP_OPEN (6) payload = empty
|
||||
* UDP_OPEN_RESULT (7) payload = [1B status]
|
||||
* UDP_SEND (8) payload = [2B hostLen][host][2B port][2B dataLen][data...]
|
||||
* UDP_RECV (9) payload = same as UDP_SEND (host:port,data)
|
||||
* UDP_CLOSE (10) payload = empty
|
||||
*/
|
||||
|
||||
const net = require('net');
|
||||
const tls = require('tls');
|
||||
const fs = require('fs');
|
||||
const { URL } = require('url');
|
||||
const dgram = require('dgram');
|
||||
const { program } = require('commander');
|
||||
|
||||
program
|
||||
.requiredOption('--server-host <host>', 'Proxy out-node server host')
|
||||
.requiredOption('--server-port <port>', 'Proxy out-node server port')
|
||||
.requiredOption('--psk-file <path>', 'Path to PSK key file')
|
||||
.requiredOption('--psk-file <path>', 'Path to PSK key file (hex)')
|
||||
.requiredOption('--identity <identity>', 'PSK identity string')
|
||||
.requiredOption('--proxy-port <port>', 'Local HTTP proxy port to listen on')
|
||||
.requiredOption('--socks-port <port>', 'Local SOCKS5 proxy port to listen on')
|
||||
.option('--bind-host <host>', 'Local bind host', '127.0.0.1')
|
||||
.option('--connect-timeout <ms>', 'Timeout waiting OPEN_RESULT (ms)', '10000')
|
||||
.option('--idle-timeout <ms>', 'Idle timeout for local sockets (ms)', '60000')
|
||||
.option('--connect-timeout <ms>', 'Timeout waiting OPEN/UDP_OPEN result (ms)', '10000')
|
||||
.option('--idle-timeout <ms>', 'Idle timeout for local TCP sockets (ms)', '60000')
|
||||
.option('--udp-idle-timeout <ms>', 'Idle timeout for UDP associate socket (ms, 0=never)', '60000')
|
||||
.parse();
|
||||
|
||||
const options = program.opts();
|
||||
@@ -47,6 +54,7 @@ try {
|
||||
|
||||
const OPEN_RESULT_TIMEOUT = parseInt(options.connectTimeout, 10) || 10000;
|
||||
const IDLE_TIMEOUT = parseInt(options.idleTimeout, 10) || 60000;
|
||||
const UDP_IDLE_TIMEOUT = parseInt(options.udpIdleTimeout, 10) || 60000;
|
||||
|
||||
// Message Types
|
||||
const MSG_TYPES = {
|
||||
@@ -54,6 +62,12 @@ const MSG_TYPES = {
|
||||
CLOSE: 3,
|
||||
OPEN: 4,
|
||||
OPEN_RESULT: 5,
|
||||
|
||||
UDP_OPEN: 6,
|
||||
UDP_OPEN_RESULT: 7,
|
||||
UDP_SEND: 8,
|
||||
UDP_RECV: 9,
|
||||
UDP_CLOSE: 10,
|
||||
};
|
||||
|
||||
function buildOpenPayload(host, port) {
|
||||
@@ -66,6 +80,18 @@ function buildOpenPayload(host, port) {
|
||||
return buf;
|
||||
}
|
||||
|
||||
function buildUdpPayload(host, port, data) {
|
||||
const hostBuf = Buffer.from(host, 'utf8');
|
||||
const buf = Buffer.allocUnsafe(2 + hostBuf.length + 2 + 2 + data.length);
|
||||
let off = 0;
|
||||
buf.writeUInt16BE(hostBuf.length, off); off += 2;
|
||||
hostBuf.copy(buf, off); off += hostBuf.length;
|
||||
buf.writeUInt16BE(port, off); off += 2;
|
||||
buf.writeUInt16BE(data.length, off); off += 2;
|
||||
data.copy(buf, off);
|
||||
return buf;
|
||||
}
|
||||
|
||||
function writeMessage(socket, type, connectionId, data = Buffer.alloc(0)) {
|
||||
if (!socket || socket.destroyed) return;
|
||||
const header = Buffer.allocUnsafe(9);
|
||||
@@ -112,7 +138,7 @@ function createMessageReader() {
|
||||
function pskCallback(/* hint */) {
|
||||
return {
|
||||
identity: options.identity,
|
||||
psk: Buffer.from(pskKey, 'hex')
|
||||
psk: Buffer.from(pskKey, 'hex'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -120,15 +146,22 @@ function pskCallback(/* hint */) {
|
||||
let tunnelSocket = null;
|
||||
let reader = null;
|
||||
|
||||
const connections = new Map(); // connectionId -> ConnState
|
||||
// TCP connection state: id -> { id, localSocket, opened, openTimer }
|
||||
const connections = new Map();
|
||||
let nextConnId = 1;
|
||||
|
||||
// UDP association state: udpId -> {
|
||||
// id, controlSocket, udpSocket, openTimer, opened,
|
||||
// clients: Set of "host:port" strings to reply to,
|
||||
// }
|
||||
const udpAssocs = new Map();
|
||||
|
||||
function genConnId() {
|
||||
let id = nextConnId >>> 0;
|
||||
do {
|
||||
id = (id + 1) >>> 0;
|
||||
if (id === 0) id = 1;
|
||||
} while (connections.has(id));
|
||||
} while (connections.has(id) || udpAssocs.has(id));
|
||||
nextConnId = id;
|
||||
return id;
|
||||
}
|
||||
@@ -138,15 +171,18 @@ function connectTunnel() {
|
||||
const port = parseInt(options.serverPort, 10);
|
||||
console.log(`Connecting to proxy out-node ${host}:${port} via TLS-PSK...`);
|
||||
|
||||
const sock = tls.connect({
|
||||
host,
|
||||
port,
|
||||
pskCallback,
|
||||
ciphers: 'PSK-AES256-GCM-SHA384:PSK-AES128-GCM-SHA256',
|
||||
checkServerIdentity: () => undefined
|
||||
}, () => {
|
||||
console.log('Proxy tunnel connected');
|
||||
});
|
||||
const sock = tls.connect(
|
||||
{
|
||||
host,
|
||||
port,
|
||||
pskCallback,
|
||||
ciphers: 'PSK-AES256-GCM-SHA384:PSK-AES128-GCM-SHA256',
|
||||
checkServerIdentity: () => undefined,
|
||||
},
|
||||
() => {
|
||||
console.log('Proxy tunnel connected');
|
||||
}
|
||||
);
|
||||
|
||||
sock.setNoDelay(true);
|
||||
sock.setKeepAlive(true, 30000);
|
||||
@@ -156,66 +192,94 @@ function connectTunnel() {
|
||||
|
||||
sock.on('data', (data) => {
|
||||
reader(data, (type, connectionId, payload) => {
|
||||
const st = connections.get(connectionId);
|
||||
if (!st) {
|
||||
// Unknown or already closed
|
||||
if (type === MSG_TYPES.OPEN_RESULT) {
|
||||
// No state; ignore
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === MSG_TYPES.OPEN_RESULT) {
|
||||
const st = connections.get(connectionId);
|
||||
if (!st) return;
|
||||
const ok = payload.length > 0 ? payload.readUInt8(0) === 1 : false;
|
||||
if (st.opened) return;
|
||||
clearTimeout(st.openTimer);
|
||||
if (!ok) {
|
||||
// Notify client error
|
||||
if (st.mode === 'CONNECT') {
|
||||
safeWrite(st.localSocket, Buffer.from(
|
||||
'HTTP/1.1 502 Bad Gateway\r\nProxy-Agent: PSK-Proxy\r\n\r\n'
|
||||
));
|
||||
} else {
|
||||
safeWrite(st.localSocket, Buffer.from(
|
||||
'HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\nContent-Length: 11\r\nContent-Type: text/plain\r\n\r\nBad Gateway'
|
||||
));
|
||||
}
|
||||
destroyLocal(connectionId);
|
||||
// SOCKS5 CONNECT failure -> reply and close
|
||||
try {
|
||||
const rep = buildSocksReply(0x05 /* connection refused */, '0.0.0.0', 0);
|
||||
st.localSocket.write(rep);
|
||||
} catch (_) {}
|
||||
destroyLocalTCP(connectionId);
|
||||
return;
|
||||
}
|
||||
|
||||
st.opened = true;
|
||||
// For CONNECT: send 200 OK to client, then flush any buffered data (likely none)
|
||||
if (st.mode === 'CONNECT') {
|
||||
safeWrite(st.localSocket, Buffer.from(
|
||||
'HTTP/1.1 200 Connection Established\r\nProxy-Agent: PSK-Proxy\r\n\r\n'
|
||||
));
|
||||
// If there was any extra data after headers (shouldn't for CONNECT), flush it
|
||||
flushBufferedToTunnel(connectionId);
|
||||
} else {
|
||||
// HTTP mode: first send the rewritten initial request, then any buffered tail
|
||||
if (st.initialUpstream && st.initialUpstream.length > 0) {
|
||||
writeMessage(tunnelSocket, MSG_TYPES.DATA, connectionId, st.initialUpstream);
|
||||
st.initialUpstream = null;
|
||||
}
|
||||
flushBufferedToTunnel(connectionId);
|
||||
}
|
||||
// Reply success for CONNECT
|
||||
try {
|
||||
const rep = buildSocksReply(0x00 /* succeeded */, '0.0.0.0', 0);
|
||||
st.localSocket.write(rep);
|
||||
} catch (_) {}
|
||||
// Switch to streaming mode
|
||||
armTcpStreaming(st);
|
||||
} else if (type === MSG_TYPES.DATA) {
|
||||
if (!st.localSocket.destroyed) {
|
||||
const st = connections.get(connectionId);
|
||||
if (st && !st.localSocket.destroyed) {
|
||||
st.localSocket.write(payload);
|
||||
}
|
||||
} else if (type === MSG_TYPES.CLOSE) {
|
||||
// Upstream closed
|
||||
destroyLocal(connectionId);
|
||||
destroyLocalTCP(connectionId);
|
||||
|
||||
} else if (type === MSG_TYPES.UDP_OPEN_RESULT) {
|
||||
const as = udpAssocs.get(connectionId);
|
||||
if (!as) return;
|
||||
const ok = payload.length > 0 ? payload.readUInt8(0) === 1 : false;
|
||||
clearTimeout(as.openTimer);
|
||||
if (!ok) {
|
||||
// Tell SOCKS5 client UDP associate failed
|
||||
try {
|
||||
const rep = buildSocksReply(0x01 /* general failure */, '0.0.0.0', 0);
|
||||
as.controlSocket.write(rep);
|
||||
} catch (_) {}
|
||||
closeUdpAssoc(connectionId);
|
||||
try { as.controlSocket.destroy(); } catch (_) {}
|
||||
udpAssocs.delete(connectionId);
|
||||
return;
|
||||
}
|
||||
as.opened = true;
|
||||
// Reply with our UDP bind address to client
|
||||
const addrInfo = as.udpSocket.address();
|
||||
const bndAddr = isIPv6Address(addrInfo.address) ? '::' : addrInfo.address || '0.0.0.0';
|
||||
const bndPort = addrInfo.port || 0;
|
||||
try {
|
||||
const rep = buildSocksReply(0x00 /* succeeded */, bndAddr, bndPort);
|
||||
as.controlSocket.write(rep);
|
||||
} catch (_) {}
|
||||
// Keep control connection open; UDP messages handled on udpSocket
|
||||
} else if (type === MSG_TYPES.UDP_RECV) {
|
||||
// Payload: [hostLen][host][port][dataLen][data...]
|
||||
const parsed = parseUdpPayload(payload);
|
||||
if (!parsed) return;
|
||||
const as = udpAssocs.get(connectionId);
|
||||
if (!as) return;
|
||||
// Build SOCKS5 UDP response and send to all known client endpoints
|
||||
const { host, port, data } = parsed;
|
||||
const udpPacket = buildSocksUdpDatagram(host, port, data);
|
||||
for (const ep of as.clients) {
|
||||
const [caddr, cportStr] = ep.split(':');
|
||||
const cport = parseInt(cportStr, 10);
|
||||
try {
|
||||
as.udpSocket.send(udpPacket, cport, caddr);
|
||||
} catch (_) {}
|
||||
}
|
||||
} else {
|
||||
// ignore unknown message types
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
sock.on('close', () => {
|
||||
console.log('Proxy tunnel closed. Cleaning up local connections and retrying in 2s...');
|
||||
// Close all local connections
|
||||
// Close all local TCP connections
|
||||
for (const [id] of connections) {
|
||||
destroyLocal(id);
|
||||
destroyLocalTCP(id);
|
||||
}
|
||||
// Close all UDP assocs
|
||||
for (const [id] of udpAssocs) {
|
||||
closeUdpAssoc(id);
|
||||
}
|
||||
tunnelSocket = null;
|
||||
setTimeout(connectTunnel, 2000);
|
||||
@@ -227,21 +291,11 @@ function connectTunnel() {
|
||||
}
|
||||
|
||||
function safeWrite(socket, buf) {
|
||||
if (!socket.destroyed) {
|
||||
try { socket.write(buf); } catch (_) {}
|
||||
}
|
||||
if (!socket || socket.destroyed) return;
|
||||
try { socket.write(buf); } catch (_) {}
|
||||
}
|
||||
|
||||
function flushBufferedToTunnel(connectionId) {
|
||||
const st = connections.get(connectionId);
|
||||
if (!st || !tunnelSocket || tunnelSocket.destroyed) return;
|
||||
while (st.bufferQueue.length > 0) {
|
||||
const chunk = st.bufferQueue.shift();
|
||||
writeMessage(tunnelSocket, MSG_TYPES.DATA, connectionId, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
function destroyLocal(connectionId) {
|
||||
function destroyLocalTCP(connectionId) {
|
||||
const st = connections.get(connectionId);
|
||||
if (!st) return;
|
||||
connections.delete(connectionId);
|
||||
@@ -249,71 +303,215 @@ function destroyLocal(connectionId) {
|
||||
try { st.localSocket.destroy(); } catch (_) {}
|
||||
}
|
||||
|
||||
function parseHeadersUntil(buffer) {
|
||||
const idx = buffer.indexOf('\r\n\r\n');
|
||||
if (idx === -1) return null;
|
||||
const headerPart = buffer.subarray(0, idx).toString('utf8');
|
||||
const rest = buffer.subarray(idx + 4);
|
||||
const lines = headerPart.split('\r\n');
|
||||
const requestLine = lines.shift() || '';
|
||||
const headers = {};
|
||||
for (const line of lines) {
|
||||
const p = line.indexOf(':');
|
||||
if (p > -1) {
|
||||
const name = line.slice(0, p).trim();
|
||||
const val = line.slice(p + 1).trim();
|
||||
const key = name.toLowerCase();
|
||||
if (headers[key] === undefined) headers[key] = val;
|
||||
else if (Array.isArray(headers[key])) headers[key].push(val);
|
||||
else headers[key] = [headers[key], val];
|
||||
}
|
||||
}
|
||||
return { requestLine, rawHeaderLines: lines, headers, rest };
|
||||
}
|
||||
|
||||
function buildRequestLine(method, target, version) {
|
||||
return `${method} ${target} ${version}`;
|
||||
}
|
||||
|
||||
function startOpen(connectionId, host, port, st) {
|
||||
function startOpenTcp(connectionId, host, port, st) {
|
||||
if (!tunnelSocket || tunnelSocket.destroyed) {
|
||||
// No tunnel available
|
||||
if (st.mode === 'CONNECT') {
|
||||
safeWrite(st.localSocket, Buffer.from(
|
||||
'HTTP/1.1 503 Service Unavailable\r\nProxy-Agent: PSK-Proxy\r\n\r\n'
|
||||
));
|
||||
} else {
|
||||
safeWrite(st.localSocket, Buffer.from(
|
||||
'HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\nContent-Length: 19\r\nContent-Type: text/plain\r\n\r\nService Unavailable'
|
||||
));
|
||||
}
|
||||
destroyLocal(connectionId);
|
||||
// Reply SOCKS failure
|
||||
try {
|
||||
const rep = buildSocksReply(0x01 /* general failure */, '0.0.0.0', 0);
|
||||
st.localSocket.write(rep);
|
||||
} catch (_) {}
|
||||
destroyLocalTCP(connectionId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set a timer for open result
|
||||
st.openTimer = setTimeout(() => {
|
||||
if (!st.opened) {
|
||||
// Timeout
|
||||
if (st.mode === 'CONNECT') {
|
||||
safeWrite(st.localSocket, Buffer.from(
|
||||
'HTTP/1.1 504 Gateway Timeout\r\nProxy-Agent: PSK-Proxy\r\n\r\n'
|
||||
));
|
||||
} else {
|
||||
safeWrite(st.localSocket, Buffer.from(
|
||||
'HTTP/1.1 504 Gateway Timeout\r\nConnection: close\r\nContent-Length: 15\r\nContent-Type: text/plain\r\n\r\nGateway Timeout'
|
||||
));
|
||||
}
|
||||
// Best-effort to close upstream
|
||||
try {
|
||||
const rep = buildSocksReply(0x06 /* TTL expired (timeout) */, '0.0.0.0', 0);
|
||||
st.localSocket.write(rep);
|
||||
} catch (_) {}
|
||||
writeMessage(tunnelSocket, MSG_TYPES.CLOSE, connectionId);
|
||||
destroyLocal(connectionId);
|
||||
destroyLocalTCP(connectionId);
|
||||
}
|
||||
}, OPEN_RESULT_TIMEOUT);
|
||||
|
||||
writeMessage(tunnelSocket, MSG_TYPES.OPEN, connectionId, buildOpenPayload(host, port));
|
||||
}
|
||||
|
||||
function handleLocalConnection(localSocket) {
|
||||
function armTcpStreaming(st) {
|
||||
// After CONNECT success, forward subsequent data
|
||||
const forward = (chunk) => {
|
||||
if (!tunnelSocket || tunnelSocket.destroyed) return;
|
||||
writeMessage(tunnelSocket, MSG_TYPES.DATA, st.id, chunk);
|
||||
};
|
||||
st.localSocket.on('data', forward);
|
||||
st.localSocket.once('close', () => {
|
||||
writeMessage(tunnelSocket, MSG_TYPES.CLOSE, st.id);
|
||||
connections.delete(st.id);
|
||||
clearTimeout(st.openTimer);
|
||||
});
|
||||
st.localSocket.once('error', () => {
|
||||
writeMessage(tunnelSocket, MSG_TYPES.CLOSE, st.id);
|
||||
connections.delete(st.id);
|
||||
clearTimeout(st.openTimer);
|
||||
});
|
||||
}
|
||||
|
||||
/* ============== SOCKS5 Helpers ============== */
|
||||
|
||||
function isIPv4Address(addr) {
|
||||
return net.isIPv4(addr);
|
||||
}
|
||||
function isIPv6Address(addr) {
|
||||
return net.isIPv6(addr);
|
||||
}
|
||||
|
||||
function buildSocksReply(rep, bndAddr, bndPort) {
|
||||
// VER=5, REP, RSV=0, ATYP, BND.ADDR, BND.PORT
|
||||
const addrBufInfo = encodeSocksAddr(bndAddr);
|
||||
const buf = Buffer.allocUnsafe(4 + addrBufInfo.addr.length + 2);
|
||||
let off = 0;
|
||||
buf.writeUInt8(5, off++); // VER
|
||||
buf.writeUInt8(rep, off++); // REP
|
||||
buf.writeUInt8(0, off++); // RSV
|
||||
buf.writeUInt8(addrBufInfo.atyp, off++); // ATYP
|
||||
addrBufInfo.addr.copy(buf, off); off += addrBufInfo.addr.length;
|
||||
buf.writeUInt16BE(bndPort >>> 0, off); off += 2;
|
||||
return buf;
|
||||
}
|
||||
|
||||
function encodeSocksAddr(addr) {
|
||||
if (isIPv4Address(addr)) {
|
||||
return { atyp: 0x01, addr: Buffer.from(addr.split('.').map((x) => parseInt(x, 10))) };
|
||||
}
|
||||
if (isIPv6Address(addr)) {
|
||||
return { atyp: 0x04, addr: ipToBuffer(addr) };
|
||||
}
|
||||
const nameBuf = Buffer.from(addr, 'utf8');
|
||||
const len = Math.min(nameBuf.length, 255);
|
||||
const nb = nameBuf.subarray(0, len);
|
||||
return { atyp: 0x03, addr: Buffer.concat([Buffer.from([nb.length]), nb]) };
|
||||
}
|
||||
|
||||
function ipToBuffer(ip) {
|
||||
// Convert IPv6 or IPv4-mapped IPv6 to buffer
|
||||
if (isIPv4Address(ip)) {
|
||||
return Buffer.from(ip.split('.').map((x) => parseInt(x, 10)));
|
||||
}
|
||||
// For IPv6 literals, use URL parser trick
|
||||
// Node doesn't provide direct conversion; implement simple parser:
|
||||
const sections = ip.split('::');
|
||||
let head = [];
|
||||
let tail = [];
|
||||
if (sections.length === 1) {
|
||||
head = ip.split(':');
|
||||
} else if (sections.length === 2) {
|
||||
head = sections[0] ? sections[0].split(':') : [];
|
||||
tail = sections[1] ? sections[1].split(':') : [];
|
||||
} else {
|
||||
// invalid
|
||||
return Buffer.alloc(16);
|
||||
}
|
||||
// Expand head and tail into 8 blocks
|
||||
const total = head.length + tail.length;
|
||||
const fill = 8 - total;
|
||||
const parts = [...head, ...Array(fill).fill('0'), ...tail].map((p) => parseInt(p || '0', 16));
|
||||
const buf = Buffer.alloc(16);
|
||||
for (let i = 0; i < 8; i++) {
|
||||
buf.writeUInt16BE(parts[i] || 0, i * 2);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
function parseSocksRequest(buf) {
|
||||
// VER CMD RSV ATYP DST.ADDR DST.PORT
|
||||
if (buf.length < 4) return null;
|
||||
const ver = buf.readUInt8(0);
|
||||
if (ver !== 5) return { error: 'VERSION' };
|
||||
const cmd = buf.readUInt8(1);
|
||||
const rsv = buf.readUInt8(2);
|
||||
const atyp = buf.readUInt8(3);
|
||||
let off = 4;
|
||||
let host = null;
|
||||
if (atyp === 0x01) {
|
||||
if (buf.length < off + 4 + 2) return null;
|
||||
host = Array.from(buf.subarray(off, off + 4)).join('.');
|
||||
off += 4;
|
||||
} else if (atyp === 0x03) {
|
||||
if (buf.length < off + 1) return null;
|
||||
const len = buf.readUInt8(off); off += 1;
|
||||
if (buf.length < off + len + 2) return null;
|
||||
host = buf.subarray(off, off + len).toString('utf8');
|
||||
off += len;
|
||||
} else if (atyp === 0x04) {
|
||||
if (buf.length < off + 16 + 2) return null;
|
||||
const b = buf.subarray(off, off + 16);
|
||||
host = [...Array(8).keys()].map((i) => b.readUInt16BE(i * 2).toString(16)).join(':');
|
||||
off += 16;
|
||||
} else {
|
||||
return { error: 'ATYP' };
|
||||
}
|
||||
const port = buf.readUInt16BE(off); off += 2;
|
||||
return { cmd, host, port, size: off };
|
||||
}
|
||||
|
||||
function buildSocksMethodsResponse(method) {
|
||||
const buf = Buffer.allocUnsafe(2);
|
||||
buf.writeUInt8(5, 0);
|
||||
buf.writeUInt8(method, 1);
|
||||
return buf;
|
||||
}
|
||||
|
||||
function parseUdpPayload(buf) {
|
||||
if (buf.length < 2) return null;
|
||||
let off = 0;
|
||||
const hostLen = buf.readUInt16BE(off); off += 2;
|
||||
if (buf.length < 2 + hostLen + 2 + 2) return null;
|
||||
const host = buf.subarray(off, off + hostLen).toString('utf8'); off += hostLen;
|
||||
const port = buf.readUInt16BE(off); off += 2;
|
||||
const dataLen = buf.readUInt16BE(off); off += 2;
|
||||
if (buf.length < 2 + hostLen + 2 + 2 + dataLen) return null;
|
||||
const data = buf.subarray(off, off + dataLen);
|
||||
return { host, port, data };
|
||||
}
|
||||
|
||||
function buildSocksUdpDatagram(host, port, data) {
|
||||
const addrInfo = encodeSocksAddr(host);
|
||||
const header = Buffer.allocUnsafe(2 + 1 + 1);
|
||||
header.writeUInt8(0x00, 0);
|
||||
header.writeUInt8(0x00, 1);
|
||||
header.writeUInt8(0x00, 2); // FRAG=0
|
||||
header.writeUInt8(addrInfo.atyp, 3);
|
||||
const portBuf = Buffer.allocUnsafe(2);
|
||||
portBuf.writeUInt16BE(port >>> 0, 0);
|
||||
return Buffer.concat([header, addrInfo.addr, portBuf, data]);
|
||||
}
|
||||
|
||||
function parseSocksUdpRequest(buf) {
|
||||
// UDP request: RSV(2)=0x0000, FRAG(1), ATYP, DST.ADDR, DST.PORT, DATA
|
||||
if (buf.length < 4) return null;
|
||||
const rsv0 = buf.readUInt8(0), rsv1 = buf.readUInt8(1);
|
||||
if (rsv0 !== 0x00 || rsv1 !== 0x00) return { error: 'RSV' };
|
||||
const frag = buf.readUInt8(2);
|
||||
if (frag !== 0x00) return { error: 'FRAG' }; // we don't support fragmentation
|
||||
const atyp = buf.readUInt8(3);
|
||||
let off = 4;
|
||||
let host = null;
|
||||
if (atyp === 0x01) { // IPv4
|
||||
if (buf.length < off + 4 + 2) return null;
|
||||
host = Array.from(buf.subarray(off, off + 4)).join('.');
|
||||
off += 4;
|
||||
} else if (atyp === 0x03) { // domain
|
||||
if (buf.length < off + 1) return null;
|
||||
const len = buf.readUInt8(off); off += 1;
|
||||
if (buf.length < off + len + 2) return null;
|
||||
host = buf.subarray(off, off + len).toString('utf8'); off += len;
|
||||
} else if (atyp === 0x04) { // IPv6
|
||||
if (buf.length < off + 16 + 2) return null;
|
||||
const b = buf.subarray(off, off + 16);
|
||||
host = [...Array(8).keys()].map((i) => b.readUInt16BE(i * 2).toString(16)).join(':');
|
||||
off += 16;
|
||||
} else {
|
||||
return { error: 'ATYP' };
|
||||
}
|
||||
const port = buf.readUInt16BE(off); off += 2;
|
||||
const data = buf.subarray(off);
|
||||
return { host, port, data };
|
||||
}
|
||||
|
||||
/* ============== Local SOCKS5 Server and Flow ============== */
|
||||
|
||||
function handleSocksConnection(localSocket) {
|
||||
localSocket.setNoDelay(true);
|
||||
localSocket.setKeepAlive(true, 30000);
|
||||
if (IDLE_TIMEOUT > 0) {
|
||||
@@ -322,169 +520,210 @@ function handleLocalConnection(localSocket) {
|
||||
});
|
||||
}
|
||||
|
||||
const connectionId = genConnId();
|
||||
const state = {
|
||||
id: connectionId,
|
||||
localSocket,
|
||||
bufferQueue: [],
|
||||
opened: false,
|
||||
openTimer: null,
|
||||
mode: null, // 'CONNECT' | 'HTTP'
|
||||
initialUpstream: null // Buffer to send immediately after OPEN_RESULT in HTTP mode
|
||||
};
|
||||
connections.set(connectionId, state);
|
||||
|
||||
let firstBuffer = Buffer.alloc(0);
|
||||
let headersParsed = false;
|
||||
let stage = 'NEGOTIATION';
|
||||
let buf = Buffer.alloc(0);
|
||||
let tcpConnId = null; // for CONNECT
|
||||
let udpAssocId = null; // for UDP ASSOCIATE
|
||||
|
||||
function onData(chunk) {
|
||||
if (!headersParsed) {
|
||||
firstBuffer = Buffer.concat([firstBuffer, chunk]);
|
||||
buf = Buffer.concat([buf, chunk]);
|
||||
|
||||
// Limit header size to prevent abuse (64KB)
|
||||
if (firstBuffer.length > 64 * 1024) {
|
||||
safeWrite(localSocket, Buffer.from(
|
||||
'HTTP/1.1 431 Request Header Fields Too Large\r\nConnection: close\r\n\r\n'
|
||||
));
|
||||
destroyLocal(connectionId);
|
||||
if (stage === 'NEGOTIATION') {
|
||||
if (buf.length < 2) return;
|
||||
const ver = buf.readUInt8(0);
|
||||
const nmethods = buf.readUInt8(1);
|
||||
if (ver !== 5) {
|
||||
localSocket.end();
|
||||
return;
|
||||
}
|
||||
if (buf.length < 2 + nmethods) return;
|
||||
const methods = Array.from(buf.subarray(2, 2 + nmethods));
|
||||
buf = buf.subarray(2 + nmethods);
|
||||
|
||||
const parsed = parseHeadersUntil(firstBuffer);
|
||||
if (!parsed) {
|
||||
// Wait for more
|
||||
// We support only NO AUTH (0x00)
|
||||
const method = methods.includes(0x00) ? 0x00 : 0xFF;
|
||||
localSocket.write(buildSocksMethodsResponse(method));
|
||||
if (method === 0xFF) {
|
||||
localSocket.end();
|
||||
return;
|
||||
}
|
||||
|
||||
headersParsed = true;
|
||||
const { requestLine, rawHeaderLines, headers, rest } = parsed;
|
||||
|
||||
const parts = requestLine.split(' ');
|
||||
if (parts.length < 3) {
|
||||
safeWrite(localSocket, Buffer.from(
|
||||
'HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n'
|
||||
));
|
||||
destroyLocal(connectionId);
|
||||
return;
|
||||
}
|
||||
|
||||
const method = parts[0];
|
||||
const target = parts[1];
|
||||
const version = parts[2];
|
||||
|
||||
if (method.toUpperCase() === 'CONNECT') {
|
||||
state.mode = 'CONNECT';
|
||||
// target expected as host:port
|
||||
let host = target;
|
||||
let port = 443;
|
||||
const cidx = host.lastIndexOf(':');
|
||||
if (cidx !== -1) {
|
||||
port = parseInt(host.slice(cidx + 1), 10) || 443;
|
||||
host = host.slice(0, cidx);
|
||||
}
|
||||
// Initiate OPEN
|
||||
startOpen(connectionId, host, port, state);
|
||||
// Any rest after headers (rare for CONNECT) will be buffered until open
|
||||
if (rest.length > 0) {
|
||||
state.bufferQueue.push(rest);
|
||||
}
|
||||
} else {
|
||||
state.mode = 'HTTP';
|
||||
// Determine host, port, and path; rebuild request line to origin-form
|
||||
let host = null;
|
||||
let port = 80;
|
||||
let path = target;
|
||||
|
||||
if (/^http:\/\//i.test(target)) {
|
||||
try {
|
||||
const u = new URL(target);
|
||||
host = u.hostname;
|
||||
port = u.port ? parseInt(u.port, 10) : 80;
|
||||
path = u.pathname + (u.search || '');
|
||||
if (path.length === 0) path = '/';
|
||||
} catch {
|
||||
safeWrite(localSocket, Buffer.from(
|
||||
'HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n'
|
||||
));
|
||||
destroyLocal(connectionId);
|
||||
return;
|
||||
}
|
||||
} else if (/^https:\/\//i.test(target)) {
|
||||
// We do not support absolute-form HTTPS over plaintext proxy without CONNECT
|
||||
safeWrite(localSocket, Buffer.from(
|
||||
'HTTP/1.1 400 Bad Request\r\nConnection: close\r\nContent-Length: 49\r\nContent-Type: text/plain\r\n\r\nUse CONNECT method for HTTPS requests through this proxy'
|
||||
));
|
||||
destroyLocal(connectionId);
|
||||
return;
|
||||
} else {
|
||||
// origin-form: must use Host header
|
||||
const hostHeader = headers['host'];
|
||||
if (!hostHeader) {
|
||||
safeWrite(localSocket, Buffer.from(
|
||||
'HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n'
|
||||
));
|
||||
destroyLocal(connectionId);
|
||||
return;
|
||||
}
|
||||
const hostVal = Array.isArray(hostHeader) ? hostHeader[0] : hostHeader;
|
||||
const cidx = hostVal.lastIndexOf(':');
|
||||
if (cidx !== -1) {
|
||||
host = hostVal.slice(0, cidx);
|
||||
port = parseInt(hostVal.slice(cidx + 1), 10) || 80;
|
||||
} else {
|
||||
host = hostVal;
|
||||
port = 80;
|
||||
}
|
||||
// path is already origin-form target
|
||||
}
|
||||
|
||||
// Rebuild request line to origin-form
|
||||
const newRequestLine = buildRequestLine(method, path, version);
|
||||
const headerStr = [newRequestLine, ...rawHeaderLines].join('\r\n') + '\r\n\r\n';
|
||||
state.initialUpstream = Buffer.concat([Buffer.from(headerStr, 'utf8'), rest]);
|
||||
|
||||
// Initiate OPEN then send initialUpstream upon success
|
||||
startOpen(connectionId, host, port, state);
|
||||
}
|
||||
return;
|
||||
stage = 'REQUEST';
|
||||
}
|
||||
|
||||
// After headers parsed: if not yet opened, buffer; else forward
|
||||
if (!state.opened) {
|
||||
state.bufferQueue.push(chunk);
|
||||
} else {
|
||||
writeMessage(tunnelSocket, MSG_TYPES.DATA, connectionId, chunk);
|
||||
while (stage === 'REQUEST') {
|
||||
const parsed = parseSocksRequest(buf);
|
||||
if (parsed === null) return; // need more
|
||||
if (parsed.error) {
|
||||
// invalid
|
||||
try { localSocket.write(buildSocksReply(0x01, '0.0.0.0', 0)); } catch (_) {}
|
||||
localSocket.end();
|
||||
return;
|
||||
}
|
||||
const { cmd, host, port, size } = parsed;
|
||||
buf = buf.subarray(size);
|
||||
|
||||
if (cmd === 0x01 /* CONNECT */) {
|
||||
// Allocate TCP connection id and OPEN through tunnel
|
||||
tcpConnId = genConnId();
|
||||
const state = {
|
||||
id: tcpConnId,
|
||||
localSocket,
|
||||
opened: false,
|
||||
openTimer: null,
|
||||
};
|
||||
connections.set(tcpConnId, state);
|
||||
startOpenTcp(tcpConnId, host, port, state);
|
||||
// Wait for OPEN_RESULT to send reply, then go streaming
|
||||
stage = 'CONNECT_WAIT';
|
||||
|
||||
} else if (cmd === 0x03 /* UDP ASSOCIATE */) {
|
||||
// Create UDP association
|
||||
udpAssocId = genConnId();
|
||||
const as = {
|
||||
id: udpAssocId,
|
||||
controlSocket: localSocket,
|
||||
udpSocket: null,
|
||||
openTimer: null,
|
||||
opened: false,
|
||||
clients: new Set(),
|
||||
};
|
||||
udpAssocs.set(udpAssocId, as);
|
||||
|
||||
// Create local UDP socket for client to send datagrams to
|
||||
const udpSock = dgram.createSocket('udp4');
|
||||
as.udpSocket = udpSock;
|
||||
|
||||
udpSock.on('message', (msg, rinfo) => {
|
||||
// Parse SOCKS5 UDP request
|
||||
const req = parseSocksUdpRequest(msg);
|
||||
if (!req || req.error) {
|
||||
// ignore invalid/unhandled fragmentation
|
||||
return;
|
||||
}
|
||||
// Track client endpoint so we can send replies back
|
||||
as.clients.add(`${rinfo.address}:${rinfo.port}`);
|
||||
// Send over tunnel as UDP_SEND
|
||||
if (!tunnelSocket || tunnelSocket.destroyed) return;
|
||||
writeMessage(tunnelSocket, MSG_TYPES.UDP_SEND, udpAssocId, buildUdpPayload(req.host, req.port, req.data));
|
||||
});
|
||||
|
||||
udpSock.on('error', () => {
|
||||
// Close association
|
||||
try { udpSock.close(); } catch (_) {}
|
||||
writeMessage(tunnelSocket, MSG_TYPES.UDP_CLOSE, udpAssocId);
|
||||
udpAssocs.delete(udpAssocId);
|
||||
try { localSocket.write(buildSocksReply(0x01, '0.0.0.0', 0)); } catch (_) {}
|
||||
try { localSocket.destroy(); } catch (_) {}
|
||||
});
|
||||
|
||||
udpSock.bind(0, options.bindHost, () => {
|
||||
// After binding, request UDP_OPEN on tunnel
|
||||
if (!tunnelSocket || tunnelSocket.destroyed) {
|
||||
try { localSocket.write(buildSocksReply(0x01, '0.0.0.0', 0)); } catch (_) {}
|
||||
try { localSocket.destroy(); } catch (_) {}
|
||||
return;
|
||||
}
|
||||
|
||||
as.openTimer = setTimeout(() => {
|
||||
if (!as.opened) {
|
||||
try { localSocket.write(buildSocksReply(0x06, '0.0.0.0', 0)); } catch (_) {}
|
||||
writeMessage(tunnelSocket, MSG_TYPES.UDP_CLOSE, udpAssocId);
|
||||
closeUdpAssoc(udpAssocId);
|
||||
try { localSocket.destroy(); } catch (_) {}
|
||||
}
|
||||
}, OPEN_RESULT_TIMEOUT);
|
||||
|
||||
writeMessage(tunnelSocket, MSG_TYPES.UDP_OPEN, udpAssocId);
|
||||
});
|
||||
|
||||
if (UDP_IDLE_TIMEOUT > 0) {
|
||||
udpSock.on('listening', () => {
|
||||
udpSock.setRecvBufferSize?.(1 << 20);
|
||||
});
|
||||
let idleTimer = setTimeout(() => {
|
||||
try { udpSock.close(); } catch (_) {}
|
||||
writeMessage(tunnelSocket, MSG_TYPES.UDP_CLOSE, udpAssocId);
|
||||
udpAssocs.delete(udpAssocId);
|
||||
try { localSocket.destroy(); } catch (_) {}
|
||||
}, UDP_IDLE_TIMEOUT);
|
||||
// Reset idle timer on activity
|
||||
udpSock.on('message', () => {
|
||||
clearTimeout(idleTimer);
|
||||
idleTimer = setTimeout(() => {
|
||||
try { udpSock.close(); } catch (_) {}
|
||||
writeMessage(tunnelSocket, MSG_TYPES.UDP_CLOSE, udpAssocId);
|
||||
udpAssocs.delete(udpAssocId);
|
||||
try { localSocket.destroy(); } catch (_) {}
|
||||
}, UDP_IDLE_TIMEOUT);
|
||||
});
|
||||
}
|
||||
|
||||
// Keep stage as REQUEST to allow multiple requests? SOCKS typically one per TCP session.
|
||||
// We keep control connection open until client closes.
|
||||
stage = 'ASSOCIATED';
|
||||
|
||||
} else if (cmd === 0x02 /* BIND */) {
|
||||
// Not supported
|
||||
try { localSocket.write(buildSocksReply(0x07 /* Command not supported */, '0.0.0.0', 0)); } catch (_) {}
|
||||
localSocket.end();
|
||||
return;
|
||||
} else {
|
||||
// Unknown
|
||||
try { localSocket.write(buildSocksReply(0x07, '0.0.0.0', 0)); } catch (_) {}
|
||||
localSocket.end();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
localSocket.on('data', onData);
|
||||
|
||||
localSocket.on('close', () => {
|
||||
// Inform remote
|
||||
writeMessage(tunnelSocket, MSG_TYPES.CLOSE, connectionId);
|
||||
connections.delete(connectionId);
|
||||
clearTimeout(state.openTimer);
|
||||
// For TCP CONNECT stream
|
||||
if (tcpConnId !== null) {
|
||||
writeMessage(tunnelSocket, MSG_TYPES.CLOSE, tcpConnId);
|
||||
connections.delete(tcpConnId);
|
||||
}
|
||||
// For UDP associate
|
||||
if (udpAssocId !== null) {
|
||||
writeMessage(tunnelSocket, MSG_TYPES.UDP_CLOSE, udpAssocId);
|
||||
closeUdpAssoc(udpAssocId);
|
||||
}
|
||||
});
|
||||
|
||||
localSocket.on('error', () => {
|
||||
writeMessage(tunnelSocket, MSG_TYPES.CLOSE, connectionId);
|
||||
connections.delete(connectionId);
|
||||
clearTimeout(state.openTimer);
|
||||
if (tcpConnId !== null) {
|
||||
writeMessage(tunnelSocket, MSG_TYPES.CLOSE, tcpConnId);
|
||||
connections.delete(tcpConnId);
|
||||
}
|
||||
if (udpAssocId !== null) {
|
||||
writeMessage(tunnelSocket, MSG_TYPES.UDP_CLOSE, udpAssocId);
|
||||
closeUdpAssoc(udpAssocId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Start local HTTP proxy server
|
||||
const proxyServer = net.createServer((socket) => {
|
||||
handleLocalConnection(socket);
|
||||
function closeUdpAssoc(id) {
|
||||
const as = udpAssocs.get(id);
|
||||
if (!as) return;
|
||||
try { as.udpSocket && as.udpSocket.close(); } catch (_) {}
|
||||
udpAssocs.delete(id);
|
||||
}
|
||||
|
||||
/* ============== Server bootstrap ============== */
|
||||
|
||||
// Start local SOCKS5 server
|
||||
const socksServer = net.createServer((socket) => {
|
||||
handleSocksConnection(socket);
|
||||
});
|
||||
|
||||
proxyServer.on('error', (err) => {
|
||||
console.error('Local proxy server error:', err.message);
|
||||
socksServer.on('error', (err) => {
|
||||
console.error('Local SOCKS5 server error:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
proxyServer.listen(parseInt(options.proxyPort, 10), options.bindHost, () => {
|
||||
console.log(`Local HTTP proxy listening on ${options.bindHost}:${options.proxyPort}`);
|
||||
socksServer.listen(parseInt(options.socksPort, 10), options.bindHost, () => {
|
||||
console.log(`Local SOCKS5 proxy listening on ${options.bindHost}:${options.socksPort}`);
|
||||
});
|
||||
|
||||
// Connect tunnel and maintain it
|
||||
@@ -493,10 +732,14 @@ connectTunnel();
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', () => {
|
||||
console.log('Shutting down...');
|
||||
try { proxyServer.close(); } catch (_) {}
|
||||
try { if (tunnelSocket) tunnelSocket.destroy(); } catch (_) {}
|
||||
try { socksServer.close(); } catch (_) {}
|
||||
try { tunnelSocket && tunnelSocket.destroy(); } catch (_) {}
|
||||
for (const [id] of connections) {
|
||||
destroyLocal(id);
|
||||
destroyLocalTCP(id);
|
||||
}
|
||||
for (const [id] of udpAssocs) {
|
||||
writeMessage(tunnelSocket, MSG_TYPES.UDP_CLOSE, id);
|
||||
closeUdpAssoc(id);
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
|
285
proxy-server.js
285
proxy-server.js
@@ -3,23 +3,33 @@
|
||||
/**
|
||||
* PSK Proxy Out-Node (Server)
|
||||
*
|
||||
* Listens for a single TLS-PSK tunnel connection from the proxy client.
|
||||
* Listens for a TLS-PSK tunnel connection from the proxy client.
|
||||
* Receives OPEN(host,port) to create outbound TCP connections to remote servers,
|
||||
* then forwards DATA/CLOSE frames bidirectionally.
|
||||
*
|
||||
* Also supports UDP relaying for SOCKS5 UDP ASSOCIATE via new UDP_* frames.
|
||||
*
|
||||
* Protocol (over a single TLS socket):
|
||||
* Header: [1 byte type][4 bytes connection id][4 bytes data length][data...]
|
||||
*
|
||||
* Message Types:
|
||||
* DATA (2): Carry stream data
|
||||
* CLOSE (3): Close a stream
|
||||
* OPEN (4): Open stream to host:port, payload = [2B hostLen][host][2B port]
|
||||
* OPEN_RESULT (5): Result for OPEN, payload = [1B status] (1 = success, 0 = failure)
|
||||
* DATA (2): Carry TCP stream data
|
||||
* CLOSE (3): Close a TCP stream
|
||||
* OPEN (4): Open TCP stream to host:port, payload = [2B hostLen][host][2B port]
|
||||
* OPEN_RESULT (5): Result for OPEN, payload = [1B status] (1 = success, 0 = failure)
|
||||
*
|
||||
* UDP_OPEN (6): Create a UDP association (bidirectional relay); payload = empty
|
||||
* UDP_OPEN_RESULT (7): Result for UDP_OPEN, payload = [1B status] (1 = success, 0 = failure)
|
||||
* UDP_SEND (8): Send one UDP datagram to host:port
|
||||
* payload = [2B hostLen][host][2B port][2B dataLen][data...]
|
||||
* UDP_RECV (9): UDP datagram received from remote; same payload format as UDP_SEND
|
||||
* UDP_CLOSE (10): Close UDP association; payload = empty
|
||||
*/
|
||||
|
||||
const net = require('net');
|
||||
const tls = require('tls');
|
||||
const fs = require('fs');
|
||||
const dgram = require('dgram');
|
||||
const { program } = require('commander');
|
||||
|
||||
program
|
||||
@@ -47,6 +57,12 @@ const MSG_TYPES = {
|
||||
CLOSE: 3,
|
||||
OPEN: 4,
|
||||
OPEN_RESULT: 5,
|
||||
|
||||
UDP_OPEN: 6,
|
||||
UDP_OPEN_RESULT: 7,
|
||||
UDP_SEND: 8,
|
||||
UDP_RECV: 9,
|
||||
UDP_CLOSE: 10,
|
||||
};
|
||||
|
||||
function writeMessage(socket, type, connectionId, data = Buffer.alloc(0)) {
|
||||
@@ -109,6 +125,32 @@ function buildOpenResultPayload(success) {
|
||||
return b;
|
||||
}
|
||||
|
||||
function parseUdpPayload(buf) {
|
||||
// [2B hostLen][host][2B port][2B dataLen][data...]
|
||||
if (buf.length < 2) return null;
|
||||
let off = 0;
|
||||
const hostLen = buf.readUInt16BE(off); off += 2;
|
||||
if (buf.length < 2 + hostLen + 2 + 2) return null;
|
||||
const host = buf.subarray(off, off + hostLen).toString('utf8'); off += hostLen;
|
||||
const port = buf.readUInt16BE(off); off += 2;
|
||||
const dataLen = buf.readUInt16BE(off); off += 2;
|
||||
if (buf.length < 2 + hostLen + 2 + 2 + dataLen) return null;
|
||||
const data = buf.subarray(off, off + dataLen);
|
||||
return { host, port, data };
|
||||
}
|
||||
|
||||
function buildUdpPayload(host, port, data) {
|
||||
const hostBuf = Buffer.from(host, 'utf8');
|
||||
const buf = Buffer.allocUnsafe(2 + hostBuf.length + 2 + 2 + data.length);
|
||||
let off = 0;
|
||||
buf.writeUInt16BE(hostBuf.length, off); off += 2;
|
||||
hostBuf.copy(buf, off); off += hostBuf.length;
|
||||
buf.writeUInt16BE(port, off); off += 2;
|
||||
buf.writeUInt16BE(data.length, off); off += 2;
|
||||
data.copy(buf, off);
|
||||
return buf;
|
||||
}
|
||||
|
||||
function pskCallback(socket, identity) {
|
||||
console.log(`Tunnel client identity: ${identity}`);
|
||||
return Buffer.from(pskKey, 'hex');
|
||||
@@ -117,109 +159,181 @@ function pskCallback(socket, identity) {
|
||||
let tunnelSocket = null;
|
||||
const upstreamConns = new Map(); // connectionId -> net.Socket
|
||||
|
||||
// UDP association mapping: connectionId -> { udp4?: dgram.Socket, udp6?: dgram.Socket }
|
||||
const udpAssoc = new Map();
|
||||
|
||||
function closeAllUpstreams() {
|
||||
for (const [id, s] of upstreamConns) {
|
||||
for (const [, s] of upstreamConns) {
|
||||
try { s.destroy(); } catch (_) {}
|
||||
}
|
||||
upstreamConns.clear();
|
||||
}
|
||||
|
||||
const server = tls.createServer({
|
||||
pskCallback,
|
||||
ciphers: 'PSK-AES256-GCM-SHA384:PSK-AES128-GCM-SHA256',
|
||||
}, (socket) => {
|
||||
console.log('Proxy tunnel client connected');
|
||||
tunnelSocket = socket;
|
||||
function closeAllUdp() {
|
||||
for (const [, obj] of udpAssoc) {
|
||||
try { obj.udp4 && obj.udp4.close(); } catch (_) {}
|
||||
try { obj.udp6 && obj.udp6.close(); } catch (_) {}
|
||||
}
|
||||
udpAssoc.clear();
|
||||
}
|
||||
|
||||
socket.setNoDelay(true);
|
||||
socket.setKeepAlive(true, 30000);
|
||||
function ensureUdpSocketsForAssoc(connectionId) {
|
||||
if (udpAssoc.has(connectionId)) return udpAssoc.get(connectionId);
|
||||
const onMessage = (msg, rinfo) => {
|
||||
// Forward incoming datagrams to client
|
||||
writeMessage(tunnelSocket, MSG_TYPES.UDP_RECV, connectionId, buildUdpPayload(rinfo.address, rinfo.port, msg));
|
||||
};
|
||||
// Create both IPv4 and IPv6 sockets to support all targets
|
||||
const u4 = dgram.createSocket('udp4');
|
||||
const u6 = dgram.createSocket('udp6');
|
||||
|
||||
const reader = createMessageReader();
|
||||
u4.on('message', onMessage);
|
||||
u6.on('message', onMessage);
|
||||
|
||||
socket.on('data', (data) => {
|
||||
reader(data, (type, connectionId, payload) => {
|
||||
if (type === MSG_TYPES.OPEN) {
|
||||
const spec = parseOpenPayload(payload);
|
||||
if (!spec) {
|
||||
console.warn(`Invalid OPEN payload for connection ${connectionId}`);
|
||||
writeMessage(tunnelSocket, MSG_TYPES.OPEN_RESULT, connectionId, buildOpenResultPayload(false));
|
||||
return;
|
||||
}
|
||||
u4.on('error', (err) => { /* log but keep running */ console.warn(`UDP4 error (conn ${connectionId}): ${err.message}`); });
|
||||
u6.on('error', (err) => { /* log but keep running */ console.warn(`UDP6 error (conn ${connectionId}): ${err.message}`); });
|
||||
|
||||
const { host, port } = spec;
|
||||
// Create outbound TCP connection
|
||||
const upstream = net.createConnection({ host, port });
|
||||
upstream.setNoDelay(true);
|
||||
upstream.setKeepAlive(true, 30000);
|
||||
// Bind to ephemeral ports to receive replies
|
||||
try { u4.bind(0); } catch (_) {}
|
||||
try { u6.bind(0); } catch (_) {}
|
||||
|
||||
let connected = false;
|
||||
const connectTimer = setTimeout(() => {
|
||||
if (!connected) {
|
||||
upstream.destroy(new Error('Connect timeout'));
|
||||
const entry = { udp4: u4, udp6: u6 };
|
||||
udpAssoc.set(connectionId, entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
const server = tls.createServer(
|
||||
{
|
||||
pskCallback,
|
||||
ciphers: 'PSK-AES256-GCM-SHA384:PSK-AES128-GCM-SHA256',
|
||||
},
|
||||
(socket) => {
|
||||
console.log('Proxy tunnel client connected');
|
||||
tunnelSocket = socket;
|
||||
|
||||
socket.setNoDelay(true);
|
||||
socket.setKeepAlive(true, 30000);
|
||||
|
||||
const reader = createMessageReader();
|
||||
|
||||
socket.on('data', (data) => {
|
||||
reader(data, (type, connectionId, payload) => {
|
||||
if (type === MSG_TYPES.OPEN) {
|
||||
const spec = parseOpenPayload(payload);
|
||||
if (!spec) {
|
||||
console.warn(`Invalid OPEN payload for connection ${connectionId}`);
|
||||
writeMessage(tunnelSocket, MSG_TYPES.OPEN_RESULT, connectionId, buildOpenResultPayload(false));
|
||||
return;
|
||||
}
|
||||
}, OUT_CONNECT_TIMEOUT);
|
||||
|
||||
upstream.once('connect', () => {
|
||||
connected = true;
|
||||
clearTimeout(connectTimer);
|
||||
upstreamConns.set(connectionId, upstream);
|
||||
// Notify open success
|
||||
writeMessage(tunnelSocket, MSG_TYPES.OPEN_RESULT, connectionId, buildOpenResultPayload(true));
|
||||
});
|
||||
const { host, port } = spec;
|
||||
// Create outbound TCP connection
|
||||
const upstream = net.createConnection({ host, port });
|
||||
upstream.setNoDelay(true);
|
||||
upstream.setKeepAlive(true, 30000);
|
||||
|
||||
upstream.on('data', (chunk) => {
|
||||
writeMessage(tunnelSocket, MSG_TYPES.DATA, connectionId, chunk);
|
||||
});
|
||||
let connected = false;
|
||||
const connectTimer = setTimeout(() => {
|
||||
if (!connected) {
|
||||
upstream.destroy(new Error('Connect timeout'));
|
||||
}
|
||||
}, OUT_CONNECT_TIMEOUT);
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(connectTimer);
|
||||
if (upstreamConns.get(connectionId) === upstream) {
|
||||
upstream.once('connect', () => {
|
||||
connected = true;
|
||||
clearTimeout(connectTimer);
|
||||
upstreamConns.set(connectionId, upstream);
|
||||
// Notify open success
|
||||
writeMessage(tunnelSocket, MSG_TYPES.OPEN_RESULT, connectionId, buildOpenResultPayload(true));
|
||||
});
|
||||
|
||||
upstream.on('data', (chunk) => {
|
||||
writeMessage(tunnelSocket, MSG_TYPES.DATA, connectionId, chunk);
|
||||
});
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(connectTimer);
|
||||
if (upstreamConns.get(connectionId) === upstream) {
|
||||
upstreamConns.delete(connectionId);
|
||||
}
|
||||
writeMessage(tunnelSocket, MSG_TYPES.CLOSE, connectionId);
|
||||
};
|
||||
|
||||
upstream.on('error', (err) => {
|
||||
if (!connected) {
|
||||
clearTimeout(connectTimer);
|
||||
writeMessage(tunnelSocket, MSG_TYPES.OPEN_RESULT, connectionId, buildOpenResultPayload(false));
|
||||
} else {
|
||||
// Upstream error after established
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
upstream.on('close', () => {
|
||||
cleanup();
|
||||
});
|
||||
} else if (type === MSG_TYPES.DATA) {
|
||||
const upstream = upstreamConns.get(connectionId);
|
||||
if (upstream && !upstream.destroyed) {
|
||||
upstream.write(payload);
|
||||
}
|
||||
} else if (type === MSG_TYPES.CLOSE) {
|
||||
const upstream = upstreamConns.get(connectionId);
|
||||
if (upstream) {
|
||||
upstream.destroy();
|
||||
upstreamConns.delete(connectionId);
|
||||
}
|
||||
writeMessage(tunnelSocket, MSG_TYPES.CLOSE, connectionId);
|
||||
};
|
||||
|
||||
upstream.on('error', (err) => {
|
||||
if (!connected) {
|
||||
clearTimeout(connectTimer);
|
||||
writeMessage(tunnelSocket, MSG_TYPES.OPEN_RESULT, connectionId, buildOpenResultPayload(false));
|
||||
} else {
|
||||
// Upstream error after established
|
||||
cleanup();
|
||||
// UDP handling
|
||||
} else if (type === MSG_TYPES.UDP_OPEN) {
|
||||
try {
|
||||
ensureUdpSocketsForAssoc(connectionId);
|
||||
writeMessage(tunnelSocket, MSG_TYPES.UDP_OPEN_RESULT, connectionId, buildOpenResultPayload(true));
|
||||
} catch (e) {
|
||||
console.warn(`Failed to create UDP association for ${connectionId}: ${e.message}`);
|
||||
writeMessage(tunnelSocket, MSG_TYPES.UDP_OPEN_RESULT, connectionId, buildOpenResultPayload(false));
|
||||
}
|
||||
});
|
||||
|
||||
upstream.on('close', () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
} else if (type === MSG_TYPES.DATA) {
|
||||
const upstream = upstreamConns.get(connectionId);
|
||||
if (upstream && !upstream.destroyed) {
|
||||
upstream.write(payload);
|
||||
} else if (type === MSG_TYPES.UDP_SEND) {
|
||||
const parsed = parseUdpPayload(payload);
|
||||
if (!parsed) {
|
||||
console.warn(`Invalid UDP_SEND payload for ${connectionId}`);
|
||||
return;
|
||||
}
|
||||
const { host, port, data } = parsed;
|
||||
const entry = udpAssoc.get(connectionId) || ensureUdpSocketsForAssoc(connectionId);
|
||||
// Choose v6 socket if IPv6 literal detected
|
||||
const isV6 = host.includes(':');
|
||||
const sock = isV6 ? entry.udp6 : entry.udp4;
|
||||
try {
|
||||
sock.send(data, port, host);
|
||||
} catch (e) {
|
||||
console.warn(`UDP send failed (conn ${connectionId}) to ${host}:${port} - ${e.message}`);
|
||||
}
|
||||
} else if (type === MSG_TYPES.UDP_CLOSE) {
|
||||
const entry = udpAssoc.get(connectionId);
|
||||
if (entry) {
|
||||
try { entry.udp4 && entry.udp4.close(); } catch (_) {}
|
||||
try { entry.udp6 && entry.udp6.close(); } catch (_) {}
|
||||
udpAssoc.delete(connectionId);
|
||||
}
|
||||
} else {
|
||||
// ignore unknown types
|
||||
}
|
||||
} else if (type === MSG_TYPES.CLOSE) {
|
||||
const upstream = upstreamConns.get(connectionId);
|
||||
if (upstream) {
|
||||
upstream.destroy();
|
||||
upstreamConns.delete(connectionId);
|
||||
}
|
||||
} else {
|
||||
// ignore unknown types
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('close', () => {
|
||||
console.log('Proxy tunnel disconnected');
|
||||
tunnelSocket = null;
|
||||
closeAllUpstreams();
|
||||
});
|
||||
socket.on('close', () => {
|
||||
console.log('Proxy tunnel disconnected');
|
||||
tunnelSocket = null;
|
||||
closeAllUpstreams();
|
||||
closeAllUdp();
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
console.error('Tunnel socket error:', err.message);
|
||||
});
|
||||
});
|
||||
socket.on('error', (err) => {
|
||||
console.error('Tunnel socket error:', err.message);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
server.listen(parseInt(options.tunnelPort, 10), options.host, () => {
|
||||
console.log(`PSK Proxy Out-Node listening on ${options.host}:${options.tunnelPort}`);
|
||||
@@ -229,5 +343,6 @@ process.on('SIGINT', () => {
|
||||
console.log('Shutting down...');
|
||||
try { server.close(); } catch (_) {}
|
||||
closeAllUpstreams();
|
||||
closeAllUdp();
|
||||
process.exit(0);
|
||||
});
|
||||
|
Reference in New Issue
Block a user