From 23eb8b7a918af5522b4f3af0d8445461631700bc Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Tue, 27 Jun 2023 14:01:32 +1000 Subject: [PATCH 01/44] Abstract worker-vless.js, create launcher for node --- src/run-on-node.js | 136 ++++++++++++++++++++++++++++++++++++++++++++ src/worker-vless.js | 95 ++++++++++++++++++------------- 2 files changed, 192 insertions(+), 39 deletions(-) create mode 100644 src/run-on-node.js diff --git a/src/run-on-node.js b/src/run-on-node.js new file mode 100644 index 0000000000..6cfba6fce3 --- /dev/null +++ b/src/run-on-node.js @@ -0,0 +1,136 @@ +// Run this on NodeJS to test edgeworker without wrangler. +// Only need to install package ws from npm +// To run, rename is file to "index.js" and place it in the same folder as "worker-vless.js", +// then create "package.json" as follows: + +/** package.json: +{ + "main": "index.js", + "type": "module", + "dependencies": { + "ws": "^7.5.9" + } +} + */ + +import http from 'http'; +import net from 'net'; +import WebSocket from 'ws'; + +import {vlessOverWSHandler, setTCPConnectionHandler, getVLESSConfig} from './worker-vless.js'; + +// Create an HTTP server +const server = http.createServer((req, res) => { + switch (req.url) { + case '/': + res.write('Hello from the HTTP server!'); + break; + case '/vless_config': + res.write(getVLESSConfig('YOUR-HOSTNAME')); + break; + default: + res.statusCode = 404; + } + res.end(); +}); + +// Create a WebSocket server and attach it to the HTTP server +const wss = new WebSocket.Server({ server }); + +// Define what should happen when a new WebSocket connection is established +wss.on('connection', (ws, req) => { + vlessOverWSHandler(ws, req.headers['sec-websocket-protocol'] || ''); +}); + +// Start the server on port 8080 +server.listen(8080); + +function buf2hex(buffer) { // buffer is an ArrayBuffer + return [...new Uint8Array(buffer)] + .map(x => x.toString(16).padStart(2, '0')) + .join(' '); +} + +/** + * Portable function for creating a outbound TCP connections. + * Has to be "async" because some platforms open TCP connection asynchronously. + * + * @param {string} address The remote address to connect to. + * @param {number} port The remote port to connect to. + * @param {function} log A destination-dependent logging function + * @returns {object} The wrapped TCP connection, to be compatible with Cloudflare Workers + */ +setTCPConnectionHandler(async (address, port, log) => { + const socket = net.createConnection(port, address); + + let readableStreamCancel = false; + const readableStream = new ReadableStream({ + start(controller) { + socket.on('data', (data) => { + if (readableStreamCancel) { + return; + } + controller.enqueue(data); + }); + + socket.on('close', () => { + socket.destroy(); + if (readableStreamCancel) { + return; + } + controller.close(); + }); + + socket.on('error', (err) => { + log('TCP outbound has an error: ' + err.message); + }); + }, + + pull(controller) { + // if ws can stop read if stream is full, we can implement backpressure + // https://streams.spec.whatwg.org/#example-rs-push-backpressure + }, + cancel(reason) { + // 1. pipe WritableStream has error, this cancel will called, so ws handle server close into here + // 2. if readableStream is cancel, all controller.close/enqueue need skip, + // 3. but from testing controller.error still work even if readableStream is cancel + if (readableStreamCancel) { + return; + } + log(`ReadableStream was canceled, due to ${reason}`) + readableStreamCancel = true; + socket.destroy(); + } + }); + + return { + // A ReadableStream Object + readable: readableStream, + + // Contains functions to write to a TCP stream + writable: { + getWriter: () => { + return { + write: (data) => { + socket.write(data); + }, + releaseLock: () => { + // log('Dummy writer.releaseLock()'); + } + }; + } + }, + + // Handles socket close + closed: { + catch: (exceptionHandler) => { + socket.on('close', exceptionHandler); + return { + finally: (finallyHandler) => { + finallyHandler(); + } + }; + } + } + }; +}); diff --git a/src/worker-vless.js b/src/worker-vless.js index 27bec48bee..e8e84a6b6a 100644 --- a/src/worker-vless.js +++ b/src/worker-vless.js @@ -1,13 +1,12 @@ // version base on commit 2b9927a1b12e03f8ad4731541caee2bc5c8f2e8e, time is 2023-06-22 15:09:34 UTC. // @ts-ignore -import { connect } from 'cloudflare:sockets'; // How to generate your own UUID: // [Windows] Press "Win + R", input cmd and run: Powershell -NoExit -Command "[guid]::NewGuid()" let userID = 'd342d11e-d424-4583-b36e-524ab1f0afa4'; let proxyIP = ''; - +let proxyPortMap = {}; if (!isValidUUID(userID)) { throw new Error('uuid is not valid'); @@ -24,6 +23,10 @@ export default { try { userID = env.UUID || userID; proxyIP = env.PROXYIP || proxyIP; + if (env.PROXYPORTMAP) { + proxyPortMap = JSON.parse(env.PROXYPORTMAP); + } + const upgradeHeader = request.headers.get('Upgrade'); if (!upgradeHeader || upgradeHeader !== 'websocket') { const url = new URL(request.url); @@ -31,7 +34,7 @@ export default { case '/': return new Response(JSON.stringify(request.cf), { status: 200 }); case `/${userID}`: { - const vlessConfig = getVLESSConfig(userID, request.headers.get('Host')); + const vlessConfig = getVLESSConfig(request.headers.get('Host')); return new Response(`${vlessConfig}`, { status: 200, headers: { @@ -43,7 +46,20 @@ export default { return new Response('Not found', { status: 404 }); } } else { - return await vlessOverWSHandler(request); + /** @type {import("@cloudflare/workers-types").WebSocket[]} */ + // @ts-ignore + const webSocketPair = new WebSocketPair(); + const [client, webSocket] = Object.values(webSocketPair); + + webSocket.accept(); + const earlyDataHeader = request.headers.get('sec-websocket-protocol') || ''; + vlessOverWSHandler(webSocket, earlyDataHeader); + + return new Response(null, { + status: 101, + // @ts-ignore + webSocket: client, + }); } } catch (err) { /** @type {Error} */ let e = err; @@ -52,28 +68,31 @@ export default { }, }; +let createTCPConnection; +export function setTCPConnectionHandler(handler) { + createTCPConnection = handler; +}; - +try { + const module = await import('cloudflare:sockets'); + setTCPConnectionHandler(async (address, port, log) => { + return module.connect({hostname: address, port: port}); + }); +} catch (error) { + console.log('Not on Cloudflare Workers!'); +} /** - * - * @param {import("@cloudflare/workers-types").Request} request + * @param {WebSocket} webSocket The established websocket connection to the client, must be an accepted + * @param {string} earlyDataHeader for ws 0rtt, an optional field "sec-websocket-protocol" in the request header + * may contain some base64 encoded data. */ -async function vlessOverWSHandler(request) { - - /** @type {import("@cloudflare/workers-types").WebSocket[]} */ - // @ts-ignore - const webSocketPair = new WebSocketPair(); - const [client, webSocket] = Object.values(webSocketPair); - - webSocket.accept(); - +export function vlessOverWSHandler(webSocket, earlyDataHeader) { let address = ''; let portWithRandomLog = ''; const log = (/** @type {string} */ info, /** @type {string | undefined} */ event) => { console.log(`[${address}:${portWithRandomLog}] ${info}`, event || ''); }; - const earlyDataHeader = request.headers.get('sec-websocket-protocol') || ''; const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log); @@ -147,12 +166,6 @@ async function vlessOverWSHandler(request) { })).catch((err) => { log('readableWebSocketStream pipeTo error', err); }); - - return new Response(null, { - status: 101, - // @ts-ignore - webSocket: client, - }); } /** @@ -169,11 +182,7 @@ async function vlessOverWSHandler(request) { */ async function handleTCPOutBound(remoteSocket, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log,) { async function connectAndWrite(address, port) { - /** @type {import("@cloudflare/workers-types").Socket} */ - const tcpSocket = connect({ - hostname: address, - port: port, - }); + const tcpSocket = await createTCPConnection(address, port, log); remoteSocket.value = tcpSocket; log(`connected to ${address}:${port}`); const writer = tcpSocket.writable.getWriter(); @@ -184,10 +193,19 @@ async function handleTCPOutBound(remoteSocket, addressRemote, portRemote, rawCli // if the cf connect tcp socket have no incoming data, we retry to redirect ip async function retry() { - const tcpSocket = await connectAndWrite(proxyIP || addressRemote, portRemote) + let addrDest = addressRemote; + let portDest = portRemote; + if (proxyIP) { + addrDest = proxyIP; + if (typeof proxyPortMap === "object" && proxyPortMap[portRemote] !== undefined) { + portDest = proxyPortMap[portRemote]; + } + log('Forward to ', addrDest + ':' + portDest); + } + const tcpSocket = await connectAndWrite(addrDest, portDest); // no matter retry success or not, close websocket tcpSocket.closed.catch(error => { - console.log('retry tcpSocket closed error', error); + log('retry tcpSocket closed error', error); }).finally(() => { safeCloseWebSocket(webSocket); }) @@ -273,7 +291,7 @@ function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) { /** * * @param { ArrayBuffer} vlessBuffer - * @param {string} userID + * @param {string} userID * @returns */ function processVlessHeader( @@ -319,9 +337,9 @@ function processVlessHeader( }; } const portIndex = 18 + optLength + 1; - const portBuffer = vlessBuffer.slice(portIndex, portIndex + 2); - // port is big-Endian in raw data etc 80 == 0x005d - const portRemote = new DataView(portBuffer).getUint16(0); + const portBuffer = new Uint8Array(vlessBuffer.slice(portIndex, portIndex + 2)); + // port is big-Endian in raw data etc 80 == 0x0050 + const portRemote = new DataView(portBuffer.buffer).getUint16(0); let addressIndex = portIndex + 2; const addressBuffer = new Uint8Array( @@ -367,13 +385,13 @@ function processVlessHeader( default: return { hasError: true, - message: `invild addressType is ${addressType}`, + message: `Invild addressType: ${addressType}`, }; } if (!addressValue) { return { hasError: true, - message: `addressValue is empty, addressType is ${addressType}`, + message: `Empty addressValue!`, }; } @@ -595,11 +613,10 @@ async function handleUDPOutBound(webSocket, vlessResponseHeader, log) { /** * - * @param {string} userID * @param {string | null} hostName * @returns {string} */ -function getVLESSConfig(userID, hostName) { +export function getVLESSConfig(hostName) { const vlessMain = `vless://${userID}@${hostName}:443?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#${hostName}` return ` ################################################################ @@ -623,7 +640,7 @@ clash-meta ws-opts: path: "/?ed=2048" headers: - host: ${hostName} + host: ${hostName} --------------------------------------------------------------- ################################################################ `; From c7ee1a0b6ce725f3e759887d52bf5a492db8131a Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Tue, 27 Jun 2023 14:18:45 +1000 Subject: [PATCH 02/44] Fix indentation --- src/worker-vless.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/worker-vless.js b/src/worker-vless.js index e8e84a6b6a..07368345ef 100644 --- a/src/worker-vless.js +++ b/src/worker-vless.js @@ -640,7 +640,7 @@ clash-meta ws-opts: path: "/?ed=2048" headers: - host: ${hostName} + host: ${hostName} --------------------------------------------------------------- ################################################################ `; From 0a6d83a2fcd7fccdb12e99cd59004a94f9c2180f Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Wed, 28 Jun 2023 02:53:26 +1000 Subject: [PATCH 03/44] Move NodeJS launcher to a separated folder --- .gitignore | 5 +- .vscode/launch.json | 17 ++++++ node/index.js | 125 ++++++++++++++++++++++++++++++++++++++++ node/package.json | 7 +++ node/setup.sh | 3 + src/run-on-node.js | 136 -------------------------------------------- 6 files changed, 155 insertions(+), 138 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 node/index.js create mode 100644 node/package.json create mode 100755 node/setup.sh delete mode 100644 src/run-on-node.js diff --git a/.gitignore b/.gitignore index 784c970c62..3c0ae8f248 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .DS_Store -/node_modules +node_modules *-lock.* *.lock *.log -dist \ No newline at end of file +dist +src/package.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..a4775cb339 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}/node/index.js" + } + ] +} \ No newline at end of file diff --git a/node/index.js b/node/index.js new file mode 100644 index 0000000000..be0607a713 --- /dev/null +++ b/node/index.js @@ -0,0 +1,125 @@ +// Run this on NodeJS to test edgeworker without wrangler. +// The only dependency is the ws package from npm +// To run, first run "setup.sh", then "node index.js" + +import http from 'http'; +import net from 'net'; +import WebSocket from 'ws'; + +import {vlessOverWSHandler, setTCPConnectionHandler, getVLESSConfig} from '../src/worker-vless.js'; + +// Create an HTTP server +const server = http.createServer((req, res) => { + switch (req.url) { + case '/': + res.write('Hello from the HTTP server!'); + break; + case '/vless_config': + res.write(getVLESSConfig('YOUR-HOSTNAME')); + break; + default: + res.statusCode = 404; + } + res.end(); +}); + +// Create a WebSocket server and attach it to the HTTP server +const wss = new WebSocket.Server({ server }); + +// Define what should happen when a new WebSocket connection is established +wss.on('connection', (ws, req) => { + vlessOverWSHandler(ws, req.headers['sec-websocket-protocol'] || ''); +}); + +// Start the server on port 8080 +server.listen(8080); + +function buf2hex(buffer) { // buffer is an ArrayBuffer + return [...new Uint8Array(buffer)] + .map(x => x.toString(16).padStart(2, '0')) + .join(' '); +} + +/** + * Portable function for creating a outbound TCP connections. + * Has to be "async" because some platforms open TCP connection asynchronously. + * + * @param {string} address The remote address to connect to. + * @param {number} port The remote port to connect to. + * @param {function} log A destination-dependent logging function + * @returns {object} The wrapped TCP connection, to be compatible with Cloudflare Workers + */ +setTCPConnectionHandler(async (address, port, log) => { + const socket = net.createConnection(port, address); + + let readableStreamCancel = false; + const readableStream = new ReadableStream({ + start(controller) { + socket.on('data', (data) => { + if (readableStreamCancel) { + return; + } + controller.enqueue(data); + }); + + socket.on('close', () => { + socket.destroy(); + if (readableStreamCancel) { + return; + } + controller.close(); + }); + + socket.on('error', (err) => { + log('TCP outbound has an error: ' + err.message); + }); + }, + + pull(controller) { + // if ws can stop read if stream is full, we can implement backpressure + // https://streams.spec.whatwg.org/#example-rs-push-backpressure + }, + cancel(reason) { + // 1. pipe WritableStream has error, this cancel will called, so ws handle server close into here + // 2. if readableStream is cancel, all controller.close/enqueue need skip, + // 3. but from testing controller.error still work even if readableStream is cancel + if (readableStreamCancel) { + return; + } + log(`ReadableStream was canceled, due to ${reason}`) + readableStreamCancel = true; + socket.destroy(); + } + }); + + return { + // A ReadableStream Object + readable: readableStream, + + // Contains functions to write to a TCP stream + writable: { + getWriter: () => { + return { + write: (data) => { + socket.write(data); + }, + releaseLock: () => { + // log('Dummy writer.releaseLock()'); + } + }; + } + }, + + // Handles socket close + closed: { + catch: (exceptionHandler) => { + socket.on('close', exceptionHandler); + return { + finally: (finallyHandler) => { + finallyHandler(); + } + }; + } + } + }; +}); diff --git a/node/package.json b/node/package.json new file mode 100644 index 0000000000..1d8bbee3d6 --- /dev/null +++ b/node/package.json @@ -0,0 +1,7 @@ +{ + "main": "index.js", + "type": "module", + "dependencies": { + "ws": "^7.5.9" + } +} diff --git a/node/setup.sh b/node/setup.sh new file mode 100755 index 0000000000..afccb17dcb --- /dev/null +++ b/node/setup.sh @@ -0,0 +1,3 @@ +#!/bin/sh +npm install ws +echo '{"type": "module"}' > ../src/package.json \ No newline at end of file diff --git a/src/run-on-node.js b/src/run-on-node.js deleted file mode 100644 index 6cfba6fce3..0000000000 --- a/src/run-on-node.js +++ /dev/null @@ -1,136 +0,0 @@ -// Run this on NodeJS to test edgeworker without wrangler. -// Only need to install package ws from npm -// To run, rename is file to "index.js" and place it in the same folder as "worker-vless.js", -// then create "package.json" as follows: - -/** package.json: -{ - "main": "index.js", - "type": "module", - "dependencies": { - "ws": "^7.5.9" - } -} - */ - -import http from 'http'; -import net from 'net'; -import WebSocket from 'ws'; - -import {vlessOverWSHandler, setTCPConnectionHandler, getVLESSConfig} from './worker-vless.js'; - -// Create an HTTP server -const server = http.createServer((req, res) => { - switch (req.url) { - case '/': - res.write('Hello from the HTTP server!'); - break; - case '/vless_config': - res.write(getVLESSConfig('YOUR-HOSTNAME')); - break; - default: - res.statusCode = 404; - } - res.end(); -}); - -// Create a WebSocket server and attach it to the HTTP server -const wss = new WebSocket.Server({ server }); - -// Define what should happen when a new WebSocket connection is established -wss.on('connection', (ws, req) => { - vlessOverWSHandler(ws, req.headers['sec-websocket-protocol'] || ''); -}); - -// Start the server on port 8080 -server.listen(8080); - -function buf2hex(buffer) { // buffer is an ArrayBuffer - return [...new Uint8Array(buffer)] - .map(x => x.toString(16).padStart(2, '0')) - .join(' '); -} - -/** - * Portable function for creating a outbound TCP connections. - * Has to be "async" because some platforms open TCP connection asynchronously. - * - * @param {string} address The remote address to connect to. - * @param {number} port The remote port to connect to. - * @param {function} log A destination-dependent logging function - * @returns {object} The wrapped TCP connection, to be compatible with Cloudflare Workers - */ -setTCPConnectionHandler(async (address, port, log) => { - const socket = net.createConnection(port, address); - - let readableStreamCancel = false; - const readableStream = new ReadableStream({ - start(controller) { - socket.on('data', (data) => { - if (readableStreamCancel) { - return; - } - controller.enqueue(data); - }); - - socket.on('close', () => { - socket.destroy(); - if (readableStreamCancel) { - return; - } - controller.close(); - }); - - socket.on('error', (err) => { - log('TCP outbound has an error: ' + err.message); - }); - }, - - pull(controller) { - // if ws can stop read if stream is full, we can implement backpressure - // https://streams.spec.whatwg.org/#example-rs-push-backpressure - }, - cancel(reason) { - // 1. pipe WritableStream has error, this cancel will called, so ws handle server close into here - // 2. if readableStream is cancel, all controller.close/enqueue need skip, - // 3. but from testing controller.error still work even if readableStream is cancel - if (readableStreamCancel) { - return; - } - log(`ReadableStream was canceled, due to ${reason}`) - readableStreamCancel = true; - socket.destroy(); - } - }); - - return { - // A ReadableStream Object - readable: readableStream, - - // Contains functions to write to a TCP stream - writable: { - getWriter: () => { - return { - write: (data) => { - socket.write(data); - }, - releaseLock: () => { - // log('Dummy writer.releaseLock()'); - } - }; - } - }, - - // Handles socket close - closed: { - catch: (exceptionHandler) => { - socket.on('close', exceptionHandler); - return { - finally: (finallyHandler) => { - finallyHandler(); - } - }; - } - } - }; -}); From d7cf50ed7da2fb66b312b6f8ebaf7c68ee353154 Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Wed, 28 Jun 2023 03:44:44 +1000 Subject: [PATCH 04/44] Archieve worker-vless.js and prepare to revert --- src/worker-vless.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/worker-vless.js b/src/worker-vless.js index 07368345ef..6269875d20 100644 --- a/src/worker-vless.js +++ b/src/worker-vless.js @@ -1,5 +1,4 @@ // version base on commit 2b9927a1b12e03f8ad4731541caee2bc5c8f2e8e, time is 2023-06-22 15:09:34 UTC. -// @ts-ignore // How to generate your own UUID: // [Windows] Press "Win + R", input cmd and run: Powershell -NoExit -Command "[guid]::NewGuid()" From 30c01a8cfd81dc40f7eab22edd11137036b7ff28 Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Wed, 28 Jun 2023 03:44:59 +1000 Subject: [PATCH 05/44] Abstract socks5 version --- node/index.js | 2 +- src/worker-with-socks5-experimental.js | 118 ++++++++++++++----------- 2 files changed, 66 insertions(+), 54 deletions(-) diff --git a/node/index.js b/node/index.js index be0607a713..e4e9a257df 100644 --- a/node/index.js +++ b/node/index.js @@ -6,7 +6,7 @@ import http from 'http'; import net from 'net'; import WebSocket from 'ws'; -import {vlessOverWSHandler, setTCPConnectionHandler, getVLESSConfig} from '../src/worker-vless.js'; +import {vlessOverWSHandler, setTCPConnectionHandler, getVLESSConfig} from '../src/worker-with-socks5-experimental.js'; // Create an HTTP server const server = http.createServer((req, res) => { diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js index 500b3b57db..b6daa4ee35 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-with-socks5-experimental.js @@ -1,12 +1,11 @@ // version base on commit 2b9927a1b12e03f8ad4731541caee2bc5c8f2e8e, time is 2023-06-22 15:09:37 UTC. -// @ts-ignore -import { connect } from 'cloudflare:sockets'; // How to generate your own UUID: // [Windows] Press "Win + R", input cmd and run: Powershell -NoExit -Command "[guid]::NewGuid()" let userID = 'd342d11e-d424-4583-b36e-524ab1f0afa4'; let proxyIP = ''; +let proxyPortMap = {}; // The user name and password do not contain special characters // Setting the address will ignore proxyIP @@ -31,6 +30,9 @@ export default { try { userID = env.UUID || userID; proxyIP = env.PROXYIP || proxyIP; + if (env.PROXYPORTMAP) { + proxyPortMap = JSON.parse(env.PROXYPORTMAP); + } socks5Address = env.SOCKS5 || socks5Address; if (socks5Address) { try { @@ -42,6 +44,7 @@ export default { enableSocks = false; } } + const upgradeHeader = request.headers.get('Upgrade'); if (!upgradeHeader || upgradeHeader !== 'websocket') { const url = new URL(request.url); @@ -49,7 +52,7 @@ export default { case '/': return new Response(JSON.stringify(request.cf), { status: 200 }); case `/${userID}`: { - const vlessConfig = getVLESSConfig(userID, request.headers.get('Host')); + const vlessConfig = getVLESSConfig(request.headers.get('Host')); return new Response(`${vlessConfig}`, { status: 200, headers: { @@ -61,7 +64,20 @@ export default { return new Response('Not found', { status: 404 }); } } else { - return await vlessOverWSHandler(request); + /** @type {import("@cloudflare/workers-types").WebSocket[]} */ + // @ts-ignore + const webSocketPair = new WebSocketPair(); + const [client, webSocket] = Object.values(webSocketPair); + + webSocket.accept(); + const earlyDataHeader = request.headers.get('sec-websocket-protocol') || ''; + vlessOverWSHandler(webSocket, earlyDataHeader); + + return new Response(null, { + status: 101, + // @ts-ignore + webSocket: client, + }); } } catch (err) { /** @type {Error} */ let e = err; @@ -70,28 +86,31 @@ export default { }, }; +let createTCPConnection; +export function setTCPConnectionHandler(handler) { + createTCPConnection = handler; +}; - +try { + const module = await import('cloudflare:sockets'); + setTCPConnectionHandler(async (address, port, log) => { + return module.connect({hostname: address, port: port}); + }); +} catch (error) { + console.log('Not on Cloudflare Workers!'); +} /** - * - * @param {import("@cloudflare/workers-types").Request} request + * @param {WebSocket} webSocket The established websocket connection to the client, must be an accepted + * @param {string} earlyDataHeader for ws 0rtt, an optional field "sec-websocket-protocol" in the request header + * may contain some base64 encoded data. */ -async function vlessOverWSHandler(request) { - - /** @type {import("@cloudflare/workers-types").WebSocket[]} */ - // @ts-ignore - const webSocketPair = new WebSocketPair(); - const [client, webSocket] = Object.values(webSocketPair); - - webSocket.accept(); - +export function vlessOverWSHandler(webSocket, earlyDataHeader) { let address = ''; let portWithRandomLog = ''; const log = (/** @type {string} */ info, /** @type {string | undefined} */ event) => { console.log(`[${address}:${portWithRandomLog}] ${info}`, event || ''); }; - const earlyDataHeader = request.headers.get('sec-websocket-protocol') || ''; const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log); @@ -161,12 +180,6 @@ async function vlessOverWSHandler(request) { })).catch((err) => { log('readableWebSocketStream pipeTo error', err); }); - - return new Response(null, { - status: 101, - // @ts-ignore - webSocket: client, - }); } /** @@ -184,37 +197,43 @@ async function vlessOverWSHandler(request) { */ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log,) { async function connectAndWrite(address, port, socks = false) { - /** @type {import("@cloudflare/workers-types").Socket} */ const tcpSocket = socks ? await socks5Connect(addressType, address, port, log) - : connect({ - hostname: address, - port: port, - }); + : await createTCPConnection(address, port, log); remoteSocket.value = tcpSocket; - log(`connected to ${address}:${port}`); + log(`Connected to ${address}:${port}`); const writer = tcpSocket.writable.getWriter(); - await writer.write(rawClientData); // first write, normal is tls client hello + await writer.write(rawClientData); // First write, normally is tls client hello writer.releaseLock(); return tcpSocket; } // if the cf connect tcp socket have no incoming data, we retry to redirect ip async function retry() { + let tcpSocket; if (enableSocks) { tcpSocket = await connectAndWrite(addressRemote, portRemote, true); } else { - tcpSocket = await connectAndWrite(proxyIP || addressRemote, portRemote); + let addrDest = addressRemote; + let portDest = portRemote; + if (proxyIP) { + addrDest = proxyIP; + if (typeof proxyPortMap === "object" && proxyPortMap[portRemote] !== undefined) { + portDest = proxyPortMap[portRemote]; + } + log('Forward to ', addrDest + ':' + portDest); + } + tcpSocket = await connectAndWrite(addrDest, portDest); } // no matter retry success or not, close websocket tcpSocket.closed.catch(error => { - console.log('retry tcpSocket closed error', error); + log('retry tcpSocket closed error', error); }).finally(() => { safeCloseWebSocket(webSocket); }) remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, null, log); } - let tcpSocket = await connectAndWrite(addressRemote, portRemote); + const tcpSocket = await connectAndWrite(addressRemote, portRemote); // when remoteSocket is ready, pass to websocket // remote--> ws @@ -339,9 +358,9 @@ function processVlessHeader( }; } const portIndex = 18 + optLength + 1; - const portBuffer = vlessBuffer.slice(portIndex, portIndex + 2); - // port is big-Endian in raw data etc 80 == 0x005d - const portRemote = new DataView(portBuffer).getUint16(0); + const portBuffer = new Uint8Array(vlessBuffer.slice(portIndex, portIndex + 2)); + // port is big-Endian in raw data etc 80 == 0x0050 + const portRemote = new DataView(portBuffer.buffer).getUint16(0); let addressIndex = portIndex + 2; const addressBuffer = new Uint8Array( @@ -387,13 +406,13 @@ function processVlessHeader( default: return { hasError: true, - message: `invild addressType is ${addressType}`, + message: `Invild addressType: ${addressType}`, }; } if (!addressValue) { return { hasError: true, - message: `addressValue is empty, addressType is ${addressType}`, + message: `Empty addressValue!`, }; } @@ -544,22 +563,20 @@ function stringify(arr, offset = 0) { * * @param {ArrayBuffer} udpChunk * @param {import("@cloudflare/workers-types").WebSocket} webSocket - * @param {ArrayBuffer} vlessResponseHeader + * @param {ArrayBuffer} vlessResponseHeader null means the header has been sent. * @param {(string)=> void} log */ async function handleDNSQuery(udpChunk, webSocket, vlessResponseHeader, log) { - // no matter which DNS server client send, we alwasy use hard code one. - // beacsue someof DNS server is not support DNS over TCP + // We always ignore the DNS server requested by the client and use our hard code one, + // as some DNS server does not support DNS over TCP. try { - const dnsServer = '8.8.4.4'; // change to 1.1.1.1 after cf fix connect own ip bug + // TODO: Switch to 1.1.1.1 after Cloudflare fixes its well known Workers TCP problem. + const dnsServer = '8.8.4.4'; const dnsPort = 53; /** @type {ArrayBuffer | null} */ let vlessHeader = vlessResponseHeader; /** @type {import("@cloudflare/workers-types").Socket} */ - const tcpSocket = connect({ - hostname: dnsServer, - port: dnsPort, - }); + const tcpSocket = await createTCPConnection(dnsServer, dnsPort); log(`connected to ${dnsServer}:${dnsPort}`); const writer = tcpSocket.writable.getWriter(); @@ -600,10 +617,7 @@ async function handleDNSQuery(udpChunk, webSocket, vlessResponseHeader, log) { async function socks5Connect(addressType, addressRemote, portRemote, log) { const { username, password, hostname, port } = parsedSocks5Address; // Connect to the SOCKS server - const socket = connect({ - hostname, - port, - }); + const socket = await createTCPConnection(hostname, port); // Request head format (Worker -> Socks Server): // +----+----------+----------+ @@ -764,11 +778,10 @@ function socks5AddressParser(address) { /** * - * @param {string} userID * @param {string | null} hostName * @returns {string} */ -function getVLESSConfig(userID, hostName) { +export function getVLESSConfig(hostName) { const vlessMain = `vless://${userID}@${hostName}:443?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#${hostName}` return ` ################################################################ @@ -798,4 +811,3 @@ clash-meta `; } - From 7b9b4648bbed6ed3517c3181afa8080a680e2ac8 Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Wed, 28 Jun 2023 04:02:53 +1000 Subject: [PATCH 06/44] Better logging --- src/worker-with-socks5-experimental.js | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js index b6daa4ee35..9b549c71de 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-with-socks5-experimental.js @@ -86,7 +86,13 @@ export default { }, }; +/** + * A wrapper for the TCP API. + * @type {(host: string, port: number) => object} + * @returns {object} a socket, should be Cloudflare Worker compatible + */ let createTCPConnection; +/** @param {(host: string, port: number) => object} handler */ export function setTCPConnectionHandler(handler) { createTCPConnection = handler; }; @@ -106,10 +112,9 @@ try { * may contain some base64 encoded data. */ export function vlessOverWSHandler(webSocket, earlyDataHeader) { - let address = ''; - let portWithRandomLog = ''; + let logPrefix = ''; const log = (/** @type {string} */ info, /** @type {string | undefined} */ event) => { - console.log(`[${address}:${portWithRandomLog}] ${info}`, event || ''); + console.log(`[${logPrefix}] ${info}`, event || ''); }; const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log); @@ -143,9 +148,8 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { vlessVersion = new Uint8Array([0, 0]), isUDP, } = processVlessHeader(chunk, userID); - address = addressRemote; - portWithRandomLog = `${portRemote}--${Math.random()} ${isUDP ? 'udp ' : 'tcp ' - } `; + const randTag = Math.round(Math.random()*1000000).toString(16).padStart(5, '0'); + logPrefix = `${addressRemote}:${portRemote} ${randTag} ${isUDP ? 'UDP' : 'TCP'}`; if (hasError) { // controller.error(message); throw new Error(message); // cf seems has bug, controller.error will not end stream @@ -172,10 +176,10 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { handleTCPOutBound(remoteSocketWapper, addressType, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log); }, close() { - log(`readableWebSocketStream is close`); + log(`readableWebSocketStream has been closed`); }, abort(reason) { - log(`readableWebSocketStream is abort`, JSON.stringify(reason)); + log(`readableWebSocketStream aborts`, JSON.stringify(reason)); }, })).catch((err) => { log('readableWebSocketStream pipeTo error', err); @@ -200,7 +204,7 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR const tcpSocket = socks ? await socks5Connect(addressType, address, port, log) : await createTCPConnection(address, port, log); remoteSocket.value = tcpSocket; - log(`Connected to ${address}:${port}`); + log(`[${socks ? 'Socks' : 'Direct'}] Connected to ${address}:${port}`); const writer = tcpSocket.writable.getWriter(); await writer.write(rawClientData); // First write, normally is tls client hello writer.releaseLock(); @@ -474,11 +478,11 @@ async function remoteSocketToWS(remoteSocket, webSocket, vlessResponseHeader, re } }, close() { - log(`remoteConnection!.readable is close with hasIncomingData is ${hasIncomingData}`); + log(`remoteSocket.readable is close, hasIncomingData = ${hasIncomingData}`); // safeCloseWebSocket(webSocket); // no need server close websocket frist for some case will casue HTTP ERR_CONTENT_LENGTH_MISMATCH issue, client will send close event anyway. }, abort(reason) { - console.error(`remoteConnection!.readable abort`, reason); + console.error(`remoteSocket.readable aborts`, reason); }, }) ) From 40100e1c008518165eeba3c847a2d4167b33f675 Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Wed, 28 Jun 2023 04:14:28 +1000 Subject: [PATCH 07/44] Revert worker-vless.js --- src/worker-vless.js | 94 +++++++++++++++++++-------------------------- 1 file changed, 39 insertions(+), 55 deletions(-) diff --git a/src/worker-vless.js b/src/worker-vless.js index 6269875d20..27bec48bee 100644 --- a/src/worker-vless.js +++ b/src/worker-vless.js @@ -1,11 +1,13 @@ // version base on commit 2b9927a1b12e03f8ad4731541caee2bc5c8f2e8e, time is 2023-06-22 15:09:34 UTC. +// @ts-ignore +import { connect } from 'cloudflare:sockets'; // How to generate your own UUID: // [Windows] Press "Win + R", input cmd and run: Powershell -NoExit -Command "[guid]::NewGuid()" let userID = 'd342d11e-d424-4583-b36e-524ab1f0afa4'; let proxyIP = ''; -let proxyPortMap = {}; + if (!isValidUUID(userID)) { throw new Error('uuid is not valid'); @@ -22,10 +24,6 @@ export default { try { userID = env.UUID || userID; proxyIP = env.PROXYIP || proxyIP; - if (env.PROXYPORTMAP) { - proxyPortMap = JSON.parse(env.PROXYPORTMAP); - } - const upgradeHeader = request.headers.get('Upgrade'); if (!upgradeHeader || upgradeHeader !== 'websocket') { const url = new URL(request.url); @@ -33,7 +31,7 @@ export default { case '/': return new Response(JSON.stringify(request.cf), { status: 200 }); case `/${userID}`: { - const vlessConfig = getVLESSConfig(request.headers.get('Host')); + const vlessConfig = getVLESSConfig(userID, request.headers.get('Host')); return new Response(`${vlessConfig}`, { status: 200, headers: { @@ -45,20 +43,7 @@ export default { return new Response('Not found', { status: 404 }); } } else { - /** @type {import("@cloudflare/workers-types").WebSocket[]} */ - // @ts-ignore - const webSocketPair = new WebSocketPair(); - const [client, webSocket] = Object.values(webSocketPair); - - webSocket.accept(); - const earlyDataHeader = request.headers.get('sec-websocket-protocol') || ''; - vlessOverWSHandler(webSocket, earlyDataHeader); - - return new Response(null, { - status: 101, - // @ts-ignore - webSocket: client, - }); + return await vlessOverWSHandler(request); } } catch (err) { /** @type {Error} */ let e = err; @@ -67,31 +52,28 @@ export default { }, }; -let createTCPConnection; -export function setTCPConnectionHandler(handler) { - createTCPConnection = handler; -}; -try { - const module = await import('cloudflare:sockets'); - setTCPConnectionHandler(async (address, port, log) => { - return module.connect({hostname: address, port: port}); - }); -} catch (error) { - console.log('Not on Cloudflare Workers!'); -} + /** - * @param {WebSocket} webSocket The established websocket connection to the client, must be an accepted - * @param {string} earlyDataHeader for ws 0rtt, an optional field "sec-websocket-protocol" in the request header - * may contain some base64 encoded data. + * + * @param {import("@cloudflare/workers-types").Request} request */ -export function vlessOverWSHandler(webSocket, earlyDataHeader) { +async function vlessOverWSHandler(request) { + + /** @type {import("@cloudflare/workers-types").WebSocket[]} */ + // @ts-ignore + const webSocketPair = new WebSocketPair(); + const [client, webSocket] = Object.values(webSocketPair); + + webSocket.accept(); + let address = ''; let portWithRandomLog = ''; const log = (/** @type {string} */ info, /** @type {string | undefined} */ event) => { console.log(`[${address}:${portWithRandomLog}] ${info}`, event || ''); }; + const earlyDataHeader = request.headers.get('sec-websocket-protocol') || ''; const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log); @@ -165,6 +147,12 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { })).catch((err) => { log('readableWebSocketStream pipeTo error', err); }); + + return new Response(null, { + status: 101, + // @ts-ignore + webSocket: client, + }); } /** @@ -181,7 +169,11 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { */ async function handleTCPOutBound(remoteSocket, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log,) { async function connectAndWrite(address, port) { - const tcpSocket = await createTCPConnection(address, port, log); + /** @type {import("@cloudflare/workers-types").Socket} */ + const tcpSocket = connect({ + hostname: address, + port: port, + }); remoteSocket.value = tcpSocket; log(`connected to ${address}:${port}`); const writer = tcpSocket.writable.getWriter(); @@ -192,19 +184,10 @@ async function handleTCPOutBound(remoteSocket, addressRemote, portRemote, rawCli // if the cf connect tcp socket have no incoming data, we retry to redirect ip async function retry() { - let addrDest = addressRemote; - let portDest = portRemote; - if (proxyIP) { - addrDest = proxyIP; - if (typeof proxyPortMap === "object" && proxyPortMap[portRemote] !== undefined) { - portDest = proxyPortMap[portRemote]; - } - log('Forward to ', addrDest + ':' + portDest); - } - const tcpSocket = await connectAndWrite(addrDest, portDest); + const tcpSocket = await connectAndWrite(proxyIP || addressRemote, portRemote) // no matter retry success or not, close websocket tcpSocket.closed.catch(error => { - log('retry tcpSocket closed error', error); + console.log('retry tcpSocket closed error', error); }).finally(() => { safeCloseWebSocket(webSocket); }) @@ -290,7 +273,7 @@ function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) { /** * * @param { ArrayBuffer} vlessBuffer - * @param {string} userID + * @param {string} userID * @returns */ function processVlessHeader( @@ -336,9 +319,9 @@ function processVlessHeader( }; } const portIndex = 18 + optLength + 1; - const portBuffer = new Uint8Array(vlessBuffer.slice(portIndex, portIndex + 2)); - // port is big-Endian in raw data etc 80 == 0x0050 - const portRemote = new DataView(portBuffer.buffer).getUint16(0); + const portBuffer = vlessBuffer.slice(portIndex, portIndex + 2); + // port is big-Endian in raw data etc 80 == 0x005d + const portRemote = new DataView(portBuffer).getUint16(0); let addressIndex = portIndex + 2; const addressBuffer = new Uint8Array( @@ -384,13 +367,13 @@ function processVlessHeader( default: return { hasError: true, - message: `Invild addressType: ${addressType}`, + message: `invild addressType is ${addressType}`, }; } if (!addressValue) { return { hasError: true, - message: `Empty addressValue!`, + message: `addressValue is empty, addressType is ${addressType}`, }; } @@ -612,10 +595,11 @@ async function handleUDPOutBound(webSocket, vlessResponseHeader, log) { /** * + * @param {string} userID * @param {string | null} hostName * @returns {string} */ -export function getVLESSConfig(hostName) { +function getVLESSConfig(userID, hostName) { const vlessMain = `vless://${userID}@${hostName}:443?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#${hostName}` return ` ################################################################ From 6869e32c2a282c5f9ed6c97a60c2b4a9fd74b431 Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Wed, 28 Jun 2023 17:40:27 +1000 Subject: [PATCH 08/44] Introduce fallback and globalConfig --- node/index.js | 38 ++- src/worker-with-socks5-experimental.js | 337 +++++++++++++++++-------- 2 files changed, 247 insertions(+), 128 deletions(-) diff --git a/node/index.js b/node/index.js index e4e9a257df..58ad0eaf07 100644 --- a/node/index.js +++ b/node/index.js @@ -6,7 +6,7 @@ import http from 'http'; import net from 'net'; import WebSocket from 'ws'; -import {vlessOverWSHandler, setTCPConnectionHandler, getVLESSConfig} from '../src/worker-with-socks5-experimental.js'; +import {globalConfig, vlessOverWSHandler, platformAPI, getVLESSConfig} from '../src/worker-with-socks5-experimental.js'; // Create an HTTP server const server = http.createServer((req, res) => { @@ -49,7 +49,7 @@ function buf2hex(buffer) { // buffer is an ArrayBuffer * @param {function} log A destination-dependent logging function * @returns {object} The wrapped TCP connection, to be compatible with Cloudflare Workers */ -setTCPConnectionHandler(async (address, port, log) => { +platformAPI.connect = async (address, port, log) => { const socket = net.createConnection(port, address); let readableStreamCancel = false; @@ -68,10 +68,11 @@ setTCPConnectionHandler(async (address, port, log) => { return; } controller.close(); + console.log(`TCP to ${address}:${port} has closed`); }); socket.on('error', (err) => { - log('TCP outbound has an error: ' + err.message); + console.log('TCP outbound has an error: ' + err.message); }); }, @@ -100,26 +101,19 @@ setTCPConnectionHandler(async (address, port, log) => { writable: { getWriter: () => { return { - write: (data) => { - socket.write(data); - }, - releaseLock: () => { - // log('Dummy writer.releaseLock()'); - } - }; - } - }, - - // Handles socket close - closed: { - catch: (exceptionHandler) => { - socket.on('close', exceptionHandler); - return { - finally: (finallyHandler) => { - finallyHandler(); + write: (data) => { + socket.write(data); + }, + releaseLock: () => { + // log('Dummy writer.releaseLock()'); } }; } - } + }, }; -}); +}; + +platformAPI.newWebSocket = (url) => new WebSocket(url); + +import {customConfig} from './config.js'; +globalConfig.outbounds = customConfig.outbounds; \ No newline at end of file diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js index 9b549c71de..427877e75b 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-with-socks5-experimental.js @@ -2,22 +2,161 @@ // How to generate your own UUID: // [Windows] Press "Win + R", input cmd and run: Powershell -NoExit -Command "[guid]::NewGuid()" -let userID = 'd342d11e-d424-4583-b36e-524ab1f0afa4'; +export let globalConfig = { + userID: 'd342d11e-d424-4583-b36e-524ab1f0afa4', -let proxyIP = ''; -let proxyPortMap = {}; + // The order controls where to send the traffic after the previous one fails + outbounds: [ + { + protocol: "freedom" // Compulsory, outbound locally. + } + ] +}; -// The user name and password do not contain special characters -// Setting the address will ignore proxyIP -// Example: user:pass@host:port or host:port -let socks5Address = ''; +export let platformAPI = { + /** + * A wrapper for the TCP API, should return a Cloudflare Worker compatible socket. + * See: https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/ + * @type {async (host: string, port: number, log: function) => + * { + * readable: ReadableStream, + * writable: {getWriter: () => {write: (data) => void, releaseLock: () => void}} + * } + * } + */ + connect: null, + + /** + * A wrapper for the TCP API. + * @type {(url: string) => WebSocket} returns a WebSocket, should be compatile with the standard WebSocket API. + */ + newWebSocket: null, +} -if (!isValidUUID(userID)) { - throw new Error('uuid is not valid'); +/** + * Foreach globalConfig.outbounds, start with {index: 0, serverIndex: 0} + * @param {{index: number, serverIndex: number}} curPos + * @returns {protocol: string, address: string, port: number, user: string, pass: string, + * portMap: {Number: number}}} + */ +function getOutbound(curPos) { + if (curPos.index >= globalConfig.outbounds.length) { + // End of the outbounds array + return null; + } + + const outbound = globalConfig.outbounds.at(curPos.index); + let serverCount = 0; + /** @type {[{}]} */ + let servers; + /** @type {{address: string, port: number}} */ + let curServer; + let retVal = { protocol: outbound.protocol }; + switch (outbound.protocol) { + case 'freedom': + break; + + case 'socks': + servers = outbound.settings.vnext; + serverCount = servers.length; + curServer = servers.at(curPos.serverIndex); + retVal.address = curServer.address; + retVal.port = curServer.port; + + if (curServer.users && curServer.users.length > 0) { + const firstUser = curServer.users.at(0); + retVal.user = firstUser.user; + retVal.pass = firstUser.pass; + } + break; + + case 'vless': + servers = outbound.settings.vnext; + serverCount = servers.length; + curServer = servers.at(curPos.serverIndex); + retVal.address = curServer.address; + retVal.port = curServer.port; + + retVal.pass = curServer.users.at(0).id; + break; + + default: + throw new Error(`Unknown outbound protocol: ${outbound.protocol}`); + } + + curPos.serverIndex++; + if (curPos.serverIndex >= serverCount) { + // End of the vnext array + curPos.serverIndex = 0; + curPos.index++; + } + + return retVal; } -let parsedSocks5Address = {}; -let enableSocks = false; +/** + * @param {{ + * UUID: string, + * PROXYIP: string, + * SOCKS5: string + * }} env + */ +export function setConfigFromEnv(env) { + globalConfig.userID = env.UUID || globalConfig.userID; + + if (env.PROXYIP) { + let forward = { + protocol: "forward", + address: env.PROXYIP + }; + + if (env.PROXYPORTMAP) { + forward.portMap = JSON.parse(env.PROXYPORTMAP); + } else { + forward.portMap = {}; + } + + globalConfig['outbounds'].push(forward); + } + + // The user name and password should not contain special characters + // Example: user:pass@host:port or host:port + if (env.SOCKS5) { + try { + const { + username, + password, + hostname, + port, + } = socks5AddressParser(env.SOCKS5); + + let socks = { + "address": hostname, + "port": port + } + + if (username) { + socks.users = [ // We only support one user per socks server + { + "user": username, + "pass": password + } + ] + } + + globalConfig['outbounds'].push({ + protocol: "socks", + settings: { + "vnext": [ socks ] + } + }); + } catch (err) { + /** @type {Error} */ + let e = err; + console.log(e.toString()); + } + } +} export default { /** @@ -28,30 +167,14 @@ export default { */ async fetch(request, env, ctx) { try { - userID = env.UUID || userID; - proxyIP = env.PROXYIP || proxyIP; - if (env.PROXYPORTMAP) { - proxyPortMap = JSON.parse(env.PROXYPORTMAP); - } - socks5Address = env.SOCKS5 || socks5Address; - if (socks5Address) { - try { - parsedSocks5Address = socks5AddressParser(socks5Address); - enableSocks = true; - } catch (err) { - /** @type {Error} */ let e = err; - console.log(e.toString()); - enableSocks = false; - } - } - + setConfigFromEnv(env); const upgradeHeader = request.headers.get('Upgrade'); if (!upgradeHeader || upgradeHeader !== 'websocket') { const url = new URL(request.url); switch (url.pathname) { case '/': return new Response(JSON.stringify(request.cf), { status: 200 }); - case `/${userID}`: { + case `/${globalConfig.userID}`: { const vlessConfig = getVLESSConfig(request.headers.get('Host')); return new Response(`${vlessConfig}`, { status: 200, @@ -86,22 +209,13 @@ export default { }, }; -/** - * A wrapper for the TCP API. - * @type {(host: string, port: number) => object} - * @returns {object} a socket, should be Cloudflare Worker compatible - */ -let createTCPConnection; -/** @param {(host: string, port: number) => object} handler */ -export function setTCPConnectionHandler(handler) { - createTCPConnection = handler; -}; - try { const module = await import('cloudflare:sockets'); - setTCPConnectionHandler(async (address, port, log) => { + platformAPI.connect = async (address, port, log) => { return module.connect({hostname: address, port: port}); - }); + }; + + platformAPI.newWebSocket = (url) => new WebSocket(url); } catch (error) { console.log('Not on Cloudflare Workers!'); } @@ -147,7 +261,7 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { rawDataIndex, vlessVersion = new Uint8Array([0, 0]), isUDP, - } = processVlessHeader(chunk, userID); + } = processVlessHeader(chunk, globalConfig.userID); const randTag = Math.round(Math.random()*1000000).toString(16).padStart(5, '0'); logPrefix = `${addressRemote}:${portRemote} ${randTag} ${isUDP ? 'UDP' : 'TCP'}`; if (hasError) { @@ -200,48 +314,67 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { * @returns {Promise} The remote socket. */ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log,) { - async function connectAndWrite(address, port, socks = false) { - const tcpSocket = socks ? await socks5Connect(addressType, address, port, log) - : await createTCPConnection(address, port, log); + let curOutBoundPtr = {index: 0, serverIndex: 0}; + + async function direct() { + const tcpSocket = await platformAPI.connect(addressRemote, portRemote, log); remoteSocket.value = tcpSocket; - log(`[${socks ? 'Socks' : 'Direct'}] Connected to ${address}:${port}`); + log(`Connected to ${addressRemote}:${portRemote}`); const writer = tcpSocket.writable.getWriter(); await writer.write(rawClientData); // First write, normally is tls client hello writer.releaseLock(); - return tcpSocket; + return tcpSocket.readable; } - // if the cf connect tcp socket have no incoming data, we retry to redirect ip - async function retry() { - let tcpSocket; - if (enableSocks) { - tcpSocket = await connectAndWrite(addressRemote, portRemote, true); - } else { - let addrDest = addressRemote; - let portDest = portRemote; - if (proxyIP) { - addrDest = proxyIP; - if (typeof proxyPortMap === "object" && proxyPortMap[portRemote] !== undefined) { - portDest = proxyPortMap[portRemote]; - } - log('Forward to ', addrDest + ':' + portDest); - } - tcpSocket = await connectAndWrite(addrDest, portDest); + async function socks5(address, port, user, pass) { + const tcpSocket = await platformAPI.connect(address, port, log); + log(`Connected to ${addressRemote}:${portRemote} via socks5 ${address}:${port}`); + try { + await socks5Connect(tcpSocket, user, pass, addressType, addressRemote, portRemote, log); + } catch(err) { + log(`Socks5 outbound failed with: ${err.message}`); + return null; } - // no matter retry success or not, close websocket - tcpSocket.closed.catch(error => { - log('retry tcpSocket closed error', error); - }).finally(() => { - safeCloseWebSocket(webSocket); - }) - remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, null, log); + remoteSocket.value = tcpSocket; + const writer = tcpSocket.writable.getWriter(); + await writer.write(rawClientData); // First write, normally is tls client hello + writer.releaseLock(); + return tcpSocket.readable; } + + /** @returns {Promise} */ + async function connectAndWrite() { + const outbound = getOutbound(curOutBoundPtr); + + switch (outbound.protocol) { + case 'freedom': + return await direct(); + case 'socks': + return await socks5(outbound.address, outbound.port, outbound.user, outbound.pass); + } + } + + // if the cf connect tcp socket have no incoming data, we retry to redirect ip + async function tryOutbound() { + let outboundReadableStream = await connectAndWrite(); - const tcpSocket = await connectAndWrite(addressRemote, portRemote); + while (outboundReadableStream == null && curOutBoundPtr.index < globalConfig.outbounds.length) { + outboundReadableStream = await connectAndWrite(); + } + if (outboundReadableStream == null) { + log('Reached end of the outbound chain, abort!'); + safeCloseWebSocket(webSocket); + } else { + remoteSocketToWS(outboundReadableStream, webSocket, vlessResponseHeader, tryOutbound, log); + } + } + + //const vlessWs = platformAPI.newWebSocket('wss://114514.rikkagcp1.workers.dev'); + //const vlessWsReadable = makeReadableWebSocketStream(vlessWs, null, log); // when remoteSocket is ready, pass to websocket // remote--> ws - remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, retry, log); + await tryOutbound(); } /** @@ -316,7 +449,7 @@ function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) { /** * * @param { ArrayBuffer} vlessBuffer - * @param {string} userID + * @param {string} userID the expected userID * @returns */ function processVlessHeader( @@ -434,21 +567,20 @@ function processVlessHeader( /** * - * @param {import("@cloudflare/workers-types").Socket} remoteSocket + * @param {ReadableStream} remoteSocketReader * @param {import("@cloudflare/workers-types").WebSocket} webSocket * @param {ArrayBuffer} vlessResponseHeader * @param {(() => Promise) | null} retry * @param {*} log */ -async function remoteSocketToWS(remoteSocket, webSocket, vlessResponseHeader, retry, log) { +async function remoteSocketToWS(remoteSocketReader, webSocket, vlessResponseHeader, retry, log) { // remote--> ws let remoteChunkCount = 0; let chunks = []; /** @type {ArrayBuffer | null} */ let vlessHeader = vlessResponseHeader; let hasIncomingData = false; // check if remoteSocket has incoming data - await remoteSocket.readable - .pipeTo( + await remoteSocketReader.pipeTo( new WritableStream({ start() { }, @@ -580,7 +712,7 @@ async function handleDNSQuery(udpChunk, webSocket, vlessResponseHeader, log) { /** @type {ArrayBuffer | null} */ let vlessHeader = vlessResponseHeader; /** @type {import("@cloudflare/workers-types").Socket} */ - const tcpSocket = await createTCPConnection(dnsServer, dnsPort); + const tcpSocket = await platformAPI.connect(dnsServer, dnsPort); log(`connected to ${dnsServer}:${dnsPort}`); const writer = tcpSocket.writable.getWriter(); @@ -612,16 +744,16 @@ async function handleDNSQuery(udpChunk, webSocket, vlessResponseHeader, log) { } /** - * + * @param {{readable: ReadableStream, writable: {getWriter: () => {write: (data) => void, releaseLock: () => void}}}} socket + * @param {string} username + * @param {string} password * @param {number} addressType * @param {string} addressRemote * @param {number} portRemote * @param {function} log The logging function. */ -async function socks5Connect(addressType, addressRemote, portRemote, log) { - const { username, password, hostname, port } = parsedSocks5Address; - // Connect to the SOCKS server - const socket = await createTCPConnection(hostname, port); +async function socks5Connect(socket, username, password, addressType, addressRemote, portRemote, log) { + const writer = socket.writable.getWriter(); // Request head format (Worker -> Socks Server): // +----+----------+----------+ @@ -634,16 +766,15 @@ async function socks5Connect(addressType, addressRemote, portRemote, log) { // For METHODS: // 0x00 NO AUTHENTICATION REQUIRED // 0x02 USERNAME/PASSWORD https://datatracker.ietf.org/doc/html/rfc1929 - const socksGreeting = new Uint8Array([5, 2, 0, 2]); - - const writer = socket.writable.getWriter(); - - await writer.write(socksGreeting); - log('sent socks greeting'); + await writer.write(new Uint8Array([5, 2, 0, 2])); const reader = socket.readable.getReader(); const encoder = new TextEncoder(); let res = (await reader.read()).value; + if (!res) { + throw new Error(`No response from the server`); + } + // Response format (Socks Server -> Worker): // +----+--------+ // |VER | METHOD | @@ -651,20 +782,17 @@ async function socks5Connect(addressType, addressRemote, portRemote, log) { // | 1 | 1 | // +----+--------+ if (res[0] !== 0x05) { - log(`socks server version error: ${res[0]} expected: 5`); - return; + throw new Error(`Wrong server version: ${res[0]} expected: 5`); } if (res[1] === 0xff) { - log("no acceptable methods"); - return; + throw new Error("No accepted authentication methods"); } // if return 0x0502 if (res[1] === 0x02) { - log("socks server needs auth"); + log("Socks5: Server asks for authentication"); if (!username || !password) { - log("please provide username/password"); - return; + throw new Error("Please provide username/password"); } // +----+------+----------+------+----------+ // |VER | ULEN | UNAME | PLEN | PASSWD | @@ -682,8 +810,7 @@ async function socks5Connect(addressType, addressRemote, portRemote, log) { res = (await reader.read()).value; // expected 0x0100 if (res[0] !== 0x01 || res[1] !== 0x00) { - log("fail to auth socks server"); - return; + throw new Error("Authentication failed"); } } @@ -727,7 +854,7 @@ async function socks5Connect(addressType, addressRemote, portRemote, log) { } const socksRequest = new Uint8Array([5, 1, 0, ...DSTADDR, portRemote >> 8, portRemote & 0xff]); await writer.write(socksRequest); - log('sent socks request'); + log('Socks5: Sent request'); res = (await reader.read()).value; // Response format (Socks Server -> Worker): @@ -737,14 +864,12 @@ async function socks5Connect(addressType, addressRemote, portRemote, log) { // | 1 | 1 | X'00' | 1 | Variable | 2 | // +----+-----+-------+------+----------+----------+ if (res[1] === 0x00) { - log("socks connection opened"); + log("Socks5: Connection opened"); } else { - log("fail to open socks connection"); - return; + throw new Error("Connection failed"); } writer.releaseLock(); reader.releaseLock(); - return socket; } @@ -786,7 +911,7 @@ function socks5AddressParser(address) { * @returns {string} */ export function getVLESSConfig(hostName) { - const vlessMain = `vless://${userID}@${hostName}:443?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#${hostName}` + const vlessMain = `vless://${globalConfig.userID}@${hostName}:443?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#${hostName}` return ` ################################################################ v2ray @@ -800,7 +925,7 @@ clash-meta name: ${hostName} server: ${hostName} port: 443 - uuid: ${userID} + uuid: ${globalConfig.userID} network: ws tls: true udp: false From 3e706438af46fa9043017bd51afe5f3d23441260 Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Wed, 28 Jun 2023 17:52:40 +1000 Subject: [PATCH 09/44] Add fallback and code clean up --- src/worker-with-socks5-experimental.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js index 427877e75b..2bf09de1c8 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-with-socks5-experimental.js @@ -56,6 +56,11 @@ function getOutbound(curPos) { case 'freedom': break; + case 'forward': + retVal.address = outbound.address; + retVal.portMap = outbound.portMap; + break; + case 'socks': servers = outbound.settings.vnext; serverCount = servers.length; @@ -326,6 +331,21 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR return tcpSocket.readable; } + async function forward(proxyServer, portMap) { + let portDest = portRemote; + if (typeof portMap === "object" && portMap[portRemote] !== undefined) { + portDest = portMap[portRemote]; + } + + const tcpSocket = await platformAPI.connect(proxyServer, portDest, log); + remoteSocket.value = tcpSocket; + log(`Forward ${addressRemote}:${portRemote} to ${proxyServer}:${portDest}`); + const writer = tcpSocket.writable.getWriter(); + await writer.write(rawClientData); // First write, normally is tls client hello + writer.releaseLock(); + return tcpSocket.readable; + } + async function socks5(address, port, user, pass) { const tcpSocket = await platformAPI.connect(address, port, log); log(`Connected to ${addressRemote}:${portRemote} via socks5 ${address}:${port}`); @@ -349,6 +369,8 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR switch (outbound.protocol) { case 'freedom': return await direct(); + case 'forward': + return await forward(outbound.address, outbound.portMap); case 'socks': return await socks5(outbound.address, outbound.port, outbound.user, outbound.pass); } From 00f4d39e891f6afc406e427bd98d999f3addf2d3 Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Thu, 29 Jun 2023 00:42:38 +1000 Subject: [PATCH 10/44] Better websocket close logic --- node/index.js | 22 +++++++++++++++++----- src/worker-with-socks5-experimental.js | 18 +++++++++--------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/node/index.js b/node/index.js index 58ad0eaf07..1cfd2db213 100644 --- a/node/index.js +++ b/node/index.js @@ -68,11 +68,6 @@ platformAPI.connect = async (address, port, log) => { return; } controller.close(); - console.log(`TCP to ${address}:${port} has closed`); - }); - - socket.on('error', (err) => { - console.log('TCP outbound has an error: ' + err.message); }); }, @@ -93,6 +88,20 @@ platformAPI.connect = async (address, port, log) => { } }); + const onSocketCloses = new Promise((resolve, reject) => { + socket.on('close', (err) => { + if (err) { + reject(socket.errored); + } else { + resolve(); + } + }); + + socket.on('error', (err) => { + reject(err); + }); + }); + return { // A ReadableStream Object readable: readableStream, @@ -110,6 +119,9 @@ platformAPI.connect = async (address, port, log) => { }; } }, + + // Handles socket close + closed: onSocketCloses }; }; diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js index 2bf09de1c8..6a93b610a5 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-with-socks5-experimental.js @@ -21,6 +21,7 @@ export let platformAPI = { * { * readable: ReadableStream, * writable: {getWriter: () => {write: (data) => void, releaseLock: () => void}} + * closed: {Promise} * } * } */ @@ -323,8 +324,9 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR async function direct() { const tcpSocket = await platformAPI.connect(addressRemote, portRemote, log); + tcpSocket.closed.catch(error => log('[freedom] tcpSocket closed with error: ', error.message)); remoteSocket.value = tcpSocket; - log(`Connected to ${addressRemote}:${portRemote}`); + log(`Connecting to ${addressRemote}:${portRemote}`); const writer = tcpSocket.writable.getWriter(); await writer.write(rawClientData); // First write, normally is tls client hello writer.releaseLock(); @@ -338,8 +340,9 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR } const tcpSocket = await platformAPI.connect(proxyServer, portDest, log); + tcpSocket.closed.catch(error => log('[forward] tcpSocket closed with error: ', error.message)); remoteSocket.value = tcpSocket; - log(`Forward ${addressRemote}:${portRemote} to ${proxyServer}:${portDest}`); + log(`Forwarding ${addressRemote}:${portRemote} to ${proxyServer}:${portDest}`); const writer = tcpSocket.writable.getWriter(); await writer.write(rawClientData); // First write, normally is tls client hello writer.releaseLock(); @@ -348,7 +351,8 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR async function socks5(address, port, user, pass) { const tcpSocket = await platformAPI.connect(address, port, log); - log(`Connected to ${addressRemote}:${portRemote} via socks5 ${address}:${port}`); + tcpSocket.closed.catch(error => log('[socks] tcpSocket closed with error: ', error.message)); + log(`Connecting to ${addressRemote}:${portRemote} via socks5 ${address}:${port}`); try { await socks5Connect(tcpSocket, user, pass, addressType, addressRemote, portRemote, log); } catch(err) { @@ -391,11 +395,7 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR remoteSocketToWS(outboundReadableStream, webSocket, vlessResponseHeader, tryOutbound, log); } } - - //const vlessWs = platformAPI.newWebSocket('wss://114514.rikkagcp1.workers.dev'); - //const vlessWsReadable = makeReadableWebSocketStream(vlessWs, null, log); - // when remoteSocket is ready, pass to websocket - // remote--> ws + await tryOutbound(); } @@ -642,7 +642,7 @@ async function remoteSocketToWS(remoteSocketReader, webSocket, vlessResponseHead ) .catch((error) => { console.error( - `remoteSocketToWS has exception `, + `remoteSocketToWS has exception, readyState = ${webSocket.readyState} :`, error.stack || error ); safeCloseWebSocket(webSocket); From eaeb94aa5cd28bf051350f7159cd3d6b375a8add Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Thu, 29 Jun 2023 00:51:15 +1000 Subject: [PATCH 11/44] Fix tryOutbound --- src/worker-with-socks5-experimental.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js index 6a93b610a5..a3c134ef75 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-with-socks5-experimental.js @@ -368,7 +368,11 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR /** @returns {Promise} */ async function connectAndWrite() { + console.log('connectAndWrite'); const outbound = getOutbound(curOutBoundPtr); + if (outbound == null) { + return null; + } switch (outbound.protocol) { case 'freedom': @@ -378,6 +382,8 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR case 'socks': return await socks5(outbound.address, outbound.port, outbound.user, outbound.pass); } + + return null; } // if the cf connect tcp socket have no incoming data, we retry to redirect ip From 6dad63479ea09ec70bd9212fe788d3a1285d8f9a Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Thu, 29 Jun 2023 03:03:26 +1000 Subject: [PATCH 12/44] Add vless header maker --- node/index.js | 9 +- src/worker-with-socks5-experimental.js | 157 ++++++++++++++++++++++++- 2 files changed, 155 insertions(+), 11 deletions(-) diff --git a/node/index.js b/node/index.js index 1cfd2db213..712af70e91 100644 --- a/node/index.js +++ b/node/index.js @@ -46,10 +46,10 @@ function buf2hex(buffer) { // buffer is an ArrayBuffer * * @param {string} address The remote address to connect to. * @param {number} port The remote port to connect to. - * @param {function} log A destination-dependent logging function + * @param {boolean} useTLS * @returns {object} The wrapped TCP connection, to be compatible with Cloudflare Workers */ -platformAPI.connect = async (address, port, log) => { +platformAPI.connect = async (address, port, useTLS) => { const socket = net.createConnection(port, address); let readableStreamCancel = false; @@ -82,7 +82,6 @@ platformAPI.connect = async (address, port, log) => { if (readableStreamCancel) { return; } - log(`ReadableStream was canceled, due to ${reason}`) readableStreamCancel = true; socket.destroy(); } @@ -114,7 +113,7 @@ platformAPI.connect = async (address, port, log) => { socket.write(data); }, releaseLock: () => { - // log('Dummy writer.releaseLock()'); + // console.log('Dummy writer.releaseLock()'); } }; } @@ -128,4 +127,4 @@ platformAPI.connect = async (address, port, log) => { platformAPI.newWebSocket = (url) => new WebSocket(url); import {customConfig} from './config.js'; -globalConfig.outbounds = customConfig.outbounds; \ No newline at end of file +globalConfig.outbounds = customConfig.outbounds; diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js index a3c134ef75..cae0e96f2e 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-with-socks5-experimental.js @@ -84,6 +84,7 @@ function getOutbound(curPos) { retVal.port = curServer.port; retVal.pass = curServer.users.at(0).id; + retVal.streamSettings = outbound.streamSettings; break; default: @@ -217,8 +218,8 @@ export default { try { const module = await import('cloudflare:sockets'); - platformAPI.connect = async (address, port, log) => { - return module.connect({hostname: address, port: port}); + platformAPI.connect = async (address, port, useTLS) => { + return module.connect({hostname: address, port: port}, {secureTransport: useTLS ? 'on' : 'off'}); }; platformAPI.newWebSocket = (url) => new WebSocket(url); @@ -323,7 +324,7 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR let curOutBoundPtr = {index: 0, serverIndex: 0}; async function direct() { - const tcpSocket = await platformAPI.connect(addressRemote, portRemote, log); + const tcpSocket = await platformAPI.connect(addressRemote, portRemote, false); tcpSocket.closed.catch(error => log('[freedom] tcpSocket closed with error: ', error.message)); remoteSocket.value = tcpSocket; log(`Connecting to ${addressRemote}:${portRemote}`); @@ -339,7 +340,7 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR portDest = portMap[portRemote]; } - const tcpSocket = await platformAPI.connect(proxyServer, portDest, log); + const tcpSocket = await platformAPI.connect(proxyServer, portDest, false); tcpSocket.closed.catch(error => log('[forward] tcpSocket closed with error: ', error.message)); remoteSocket.value = tcpSocket; log(`Forwarding ${addressRemote}:${portRemote} to ${proxyServer}:${portDest}`); @@ -350,7 +351,7 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR } async function socks5(address, port, user, pass) { - const tcpSocket = await platformAPI.connect(address, port, log); + const tcpSocket = await platformAPI.connect(address, port, false); tcpSocket.closed.catch(error => log('[socks] tcpSocket closed with error: ', error.message)); log(`Connecting to ${addressRemote}:${portRemote} via socks5 ${address}:${port}`); try { @@ -365,6 +366,42 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR writer.releaseLock(); return tcpSocket.readable; } + + /** + * + * @param {string} address + * @param {number} port + * @param {string} uuid + * @param {{network: string, security: string}} streamSettings + */ + async function vless(address, port, uuid, streamSettings) { + try { + checkVlessConfig(address, streamSettings); + } catch(err) { + log(`Vless outbound failed with: ${err.message}`); + return null; + } + + let wsURL = streamSettings.security === 'tls' ? 'wss://' : 'ws://'; + wsURL = wsURL + address + ':' + port; + if (streamSettings.wsSettings && streamSettings.wsSettings.path) { + wsURL = wsURL + '/' + streamSettings.wsSettings.path; + } + + const webSocket = platformAPI.newWebSocket(wsURL); + const openPromise = new Promise((resolve, reject) => { + webSocket.onopen = () => resolve(); + webSocket.onerror = (error) => reject(error); + }); + + // Wait for the connection to open + await openPromise; + + const vlessFirstPacket = makeVlessHeader(addressType, addressRemote, portRemote, uuid, rawClientData); + const readableStream = makeReadableWebSocketStream(webSocket, null, log); + log(`Connected to ${addressRemote}:${portRemote} via vless-ws ${address}:${port}`); + return makeReadableWebSocketStream(webSocket, null, log); + } /** @returns {Promise} */ async function connectAndWrite() { @@ -381,6 +418,8 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR return await forward(outbound.address, outbound.portMap); case 'socks': return await socks5(outbound.address, outbound.port, outbound.user, outbound.pass); + case 'vless': + return await vless(outbound.address, outbound.port, outbound.pass, outbound.streamSettings); } return null; @@ -740,7 +779,7 @@ async function handleDNSQuery(udpChunk, webSocket, vlessResponseHeader, log) { /** @type {ArrayBuffer | null} */ let vlessHeader = vlessResponseHeader; /** @type {import("@cloudflare/workers-types").Socket} */ - const tcpSocket = await platformAPI.connect(dnsServer, dnsPort); + const tcpSocket = await platformAPI.connect(dnsServer, dnsPort, false); log(`connected to ${dnsServer}:${dnsPort}`); const writer = tcpSocket.writable.getWriter(); @@ -933,6 +972,112 @@ function socks5AddressParser(address) { } } +/** + * @param {number} destType + * @param {string} destAddr + * @param {number} destPort + * @param {string} uuid + * @param {ArrayBuffer | Uint8Array} rawClientData + * @returns {Uint8Array} + */ +async function makeVlessHeader(destType, destAddr, destPort, uuid, rawClientData) { + /** @type {number} */ + let addressFieldLength; + /** @type {Uint8Array | undefined} */ + let addressEncoded; + switch (destType) { + case 1: + addressFieldLength = 4; + break; + case 2: + addressEncoded = new TextEncoder().encode(destAddr); + addressFieldLength = addressEncoded.length + 1; + break; + case 3: + addressFieldLength = 16; + break; + default: + throw new Error(`Unknown address type: ${destType}`); + } + + const uuidString = uuid.replace(/-/g, ''); + const uuidOffset = 1; + const vlessHeader = new Uint8Array(22 + addressFieldLength + rawClientData.length); + + // Protocol Version = 0 + vlessHeader[0] = 0x00; + + for (let i = 0; i < uuidString.length; i += 2) { + vlessHeader[uuidOffset + i / 2] = parseInt(uuidString.substr(i, 2), 16); + } + + // Additional Information Length M = 0 + vlessHeader[17] = 0x00; + + // Instruction + // 0x01 TCP + // 0x02 UDP + // 0x03 MUX + vlessHeader[18] = 0x01; // Assume TCP + + // Port, 2-byte big-endian + vlessHeader[19] = destPort >> 8; + vlessHeader[20] = destPort & 0xFF ; + + // Address Type + // 1--> ipv4 addressLength =4 + // 2--> domain name addressLength=addressBuffer[1] + // 3--> ipv6 addressLength =16 + vlessHeader[21] = destType; + + // Address + switch (destType) { + case 1: + const octetsIPv4 = destAddr.split('.'); + for (let i = 0; i < 4; i++) { + vlessHeader[22 + i] = parseInt(octetsIPv4[i]); + } + break; + case 2: + vlessHeader[22] = addressEncoded.length; + vlessHeader.set(addressEncoded, 23); + break; + case 3: + const groupsIPv6 = ipv6.split(':'); + for (let i = 0; i < 8; i++) { + const hexGroup = parseInt(groupsIPv6[i], 16); + vlessHeader[i * 2 + 22] = hexGroup >> 8; + vlessHeader[i * 2 + 23] = hexGroup & 0xFF; + } + break; + default: + throw new Error(`Unknown address type: ${destType}`); + } + + // Payload + vlessHeader.set(addressEncoded, 23 + addressFieldLength); + + return vlessHeader; +} + +function checkVlessConfig(address, streamSettings) { + if (streamSettings.network !== 'ws') { + throw new Error(`Unsupported outbound stream method: ${streamSettings.network}, has to be ws (Websocket)`); + } + + if (streamSettings.security !== 'tls' && streamSettings.security !== 'none') { + throw new Error(`Usupported security layer: ${streamSettings.network}, has to be none or tls.`); + } + + if (streamSettings.wsSettings && streamSettings.wsSettings.headers && streamSettings.wsSettings.headers.Host !== address) { + throw new Error(`The Host field in the http header is different from the server address, this is unsupported due to Cloudflare API restrictions`); + } + + if (streamSettings.tlsSettings && streamSettings.tlsSettings.serverName !== address) { + throw new Error(`The SNI is different from the server address, this is unsupported due to Cloudflare API restrictions`); + } +} + /** * * @param {string | null} hostName From 2e4208f5b0bdad4cc374ac13b0218f058aab6a88 Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Thu, 29 Jun 2023 04:33:58 +1000 Subject: [PATCH 13/44] Impl Vless client without error handling --- src/worker-with-socks5-experimental.js | 82 +++++++++++++++++--------- 1 file changed, 54 insertions(+), 28 deletions(-) diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js index cae0e96f2e..214237c112 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-with-socks5-experimental.js @@ -201,10 +201,10 @@ export default { webSocket.accept(); const earlyDataHeader = request.headers.get('sec-websocket-protocol') || ''; - vlessOverWSHandler(webSocket, earlyDataHeader); + const statusCode = vlessOverWSHandler(webSocket, earlyDataHeader); return new Response(null, { - status: 101, + status: statusCode, // @ts-ignore webSocket: client, }); @@ -231,6 +231,7 @@ try { * @param {WebSocket} webSocket The established websocket connection to the client, must be an accepted * @param {string} earlyDataHeader for ws 0rtt, an optional field "sec-websocket-protocol" in the request header * may contain some base64 encoded data. + * @returns {number} status code */ export function vlessOverWSHandler(webSocket, earlyDataHeader) { let logPrefix = ''; @@ -238,11 +239,17 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { console.log(`[${logPrefix}] ${info}`, event || ''); }; - const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log); + // for ws 0rtt + const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader); + if (error !== null) { + return 500; + } + + const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyData, log); - /** @type {{ value: import("@cloudflare/workers-types").Socket | null}}*/ + /** @type {{ writableStream: WritableStream | null}}*/ let remoteSocketWapper = { - value: null, + writableStream: null, }; let isDns = false; @@ -252,8 +259,8 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { if (isDns) { return await handleDNSQuery(chunk, webSocket, null, log); } - if (remoteSocketWapper.value) { - const writer = remoteSocketWapper.value.writable.getWriter() + if (remoteSocketWapper.writableStream) { + const writer = remoteSocketWapper.writableStream.getWriter(); await writer.write(chunk); writer.releaseLock(); return; @@ -305,12 +312,14 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { })).catch((err) => { log('readableWebSocketStream pipeTo error', err); }); + + return 101; } /** * Handles outbound TCP connections. * - * @param {any} remoteSocket + * @param {{ writableStream: WritableStream | null}} remoteSocket * @param {number} addressType The remote address type to connect to. * @param {string} addressRemote The remote address to connect to. * @param {number} portRemote The remote port to connect to. @@ -326,7 +335,7 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR async function direct() { const tcpSocket = await platformAPI.connect(addressRemote, portRemote, false); tcpSocket.closed.catch(error => log('[freedom] tcpSocket closed with error: ', error.message)); - remoteSocket.value = tcpSocket; + remoteSocket.writableStream = tcpSocket.writable; log(`Connecting to ${addressRemote}:${portRemote}`); const writer = tcpSocket.writable.getWriter(); await writer.write(rawClientData); // First write, normally is tls client hello @@ -342,7 +351,7 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR const tcpSocket = await platformAPI.connect(proxyServer, portDest, false); tcpSocket.closed.catch(error => log('[forward] tcpSocket closed with error: ', error.message)); - remoteSocket.value = tcpSocket; + remoteSocket.writableStream = tcpSocket.writable; log(`Forwarding ${addressRemote}:${portRemote} to ${proxyServer}:${portDest}`); const writer = tcpSocket.writable.getWriter(); await writer.write(rawClientData); // First write, normally is tls client hello @@ -360,7 +369,7 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR log(`Socks5 outbound failed with: ${err.message}`); return null; } - remoteSocket.value = tcpSocket; + remoteSocket.writableStream = tcpSocket.writable; const writer = tcpSocket.writable.getWriter(); await writer.write(rawClientData); // First write, normally is tls client hello writer.releaseLock(); @@ -388,19 +397,39 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR wsURL = wsURL + '/' + streamSettings.wsSettings.path; } - const webSocket = platformAPI.newWebSocket(wsURL); + const wsToVlessServer = platformAPI.newWebSocket(wsURL); const openPromise = new Promise((resolve, reject) => { - webSocket.onopen = () => resolve(); - webSocket.onerror = (error) => reject(error); + wsToVlessServer.onopen = () => resolve(); + wsToVlessServer.onerror = (error) => reject(error); }); // Wait for the connection to open await openPromise; + /** @type {Promise} */ + const recvPromise = new Promise((resolve, reject) => { + wsToVlessServer.onmessage = (event) => resolve(event.data); + wsToVlessServer.onerror = (error) => reject(error); + }); + const vlessFirstPacket = makeVlessHeader(addressType, addressRemote, portRemote, uuid, rawClientData); - const readableStream = makeReadableWebSocketStream(webSocket, null, log); + + // Send the first packet (header + rawClientData), then strip the response header + wsToVlessServer.send(vlessFirstPacket); + const firstResponse = await recvPromise; + const responseVersion = firstResponse[0]; + const addtionalBytes = firstResponse[1]; + const earlyData = firstResponse.slice(2 + addtionalBytes); + log(`Connected to ${addressRemote}:${portRemote} via vless-ws ${address}:${port}`); - return makeReadableWebSocketStream(webSocket, null, log); + + remoteSocket.writableStream = new WritableStream({ + async write(chunk, controller) { + wsToVlessServer.send(chunk); + } + }); + + return makeReadableWebSocketStream(wsToVlessServer, earlyData, log); } /** @returns {Promise} */ @@ -447,13 +476,17 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR /** * * @param {import("@cloudflare/workers-types").WebSocket} webSocketServer - * @param {string} earlyDataHeader for ws 0rtt - * @param {(info: string)=> void} log for ws 0rtt + * @param {Uint8Array} earlyData + * @param {(info: string)=> void} log */ -function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) { +function makeReadableWebSocketStream(webSocketServer, earlyData, log) { let readableStreamCancel = false; const stream = new ReadableStream({ start(controller) { + if (earlyData) { + controller.enqueue(earlyData); + } + webSocketServer.addEventListener('message', (event) => { if (readableStreamCancel) { return; @@ -480,13 +513,6 @@ function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) { controller.error(err); } ); - // for ws 0rtt - const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader); - if (error) { - controller.error(error); - } else if (earlyData) { - controller.enqueue(earlyData); - } }, pull(controller) { @@ -980,7 +1006,7 @@ function socks5AddressParser(address) { * @param {ArrayBuffer | Uint8Array} rawClientData * @returns {Uint8Array} */ -async function makeVlessHeader(destType, destAddr, destPort, uuid, rawClientData) { +function makeVlessHeader(destType, destAddr, destPort, uuid, rawClientData) { /** @type {number} */ let addressFieldLength; /** @type {Uint8Array | undefined} */ @@ -1055,7 +1081,7 @@ async function makeVlessHeader(destType, destAddr, destPort, uuid, rawClientData } // Payload - vlessHeader.set(addressEncoded, 23 + addressFieldLength); + vlessHeader.set(rawClientData, 22 + addressFieldLength); return vlessHeader; } From c69f30088db3c146048fba9c30fca2e3af344576 Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Thu, 29 Jun 2023 05:05:08 +1000 Subject: [PATCH 14/44] Add some error handling to vless --- src/worker-with-socks5-experimental.js | 41 +++++++++++++++++++++----- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js index 214237c112..16d0e822c7 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-with-socks5-experimental.js @@ -396,37 +396,65 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR if (streamSettings.wsSettings && streamSettings.wsSettings.path) { wsURL = wsURL + '/' + streamSettings.wsSettings.path; } + log(`Connecting to ${addressRemote}:${portRemote} via vless ${wsURL}`); const wsToVlessServer = platformAPI.newWebSocket(wsURL); const openPromise = new Promise((resolve, reject) => { wsToVlessServer.onopen = () => resolve(); + wsToVlessServer.onclose = (code, reason) => + reject(new Error(`Closed with code ${code}, reason: ${reason}`)); wsToVlessServer.onerror = (error) => reject(error); + setTimeout(() => { + wsToVlessServer.close(); + reject({message: `Open connection timeout`}); + }, 1000); }); // Wait for the connection to open - await openPromise; + try { + await openPromise; + } catch (err) { + log(`Cannot open Websocket connection: ${err.message}`); + return null; + } /** @type {Promise} */ const recvPromise = new Promise((resolve, reject) => { wsToVlessServer.onmessage = (event) => resolve(event.data); + wsToVlessServer.onclose = (code, reason) => + reject(new Error(`Closed with code ${code}, reason: ${reason}`)); wsToVlessServer.onerror = (error) => reject(error); + setTimeout(() => { + wsToVlessServer.close(); + reject({message: `Receive response timeout`}); + }, 1000); }); const vlessFirstPacket = makeVlessHeader(addressType, addressRemote, portRemote, uuid, rawClientData); // Send the first packet (header + rawClientData), then strip the response header wsToVlessServer.send(vlessFirstPacket); - const firstResponse = await recvPromise; - const responseVersion = firstResponse[0]; + let firstResponse; + try { + firstResponse = await recvPromise; + } catch (err) { + log(`Cannot open Websocket connection: ${err.message}`); + return null; + } + const responseVersion = firstResponse[0]; // We should expect 0 here for now const addtionalBytes = firstResponse[1]; const earlyData = firstResponse.slice(2 + addtionalBytes); - log(`Connected to ${addressRemote}:${portRemote} via vless-ws ${address}:${port}`); - remoteSocket.writableStream = new WritableStream({ async write(chunk, controller) { wsToVlessServer.send(chunk); - } + }, + close() { + log(`Vless Websocket closed`); + }, + abort(reason) { + console.error(`Vless Websocket aborted`, reason); + }, }); return makeReadableWebSocketStream(wsToVlessServer, earlyData, log); @@ -533,7 +561,6 @@ function makeReadableWebSocketStream(webSocketServer, earlyData, log) { }); return stream; - } // https://xtls.github.io/development/protocols/vless.html From 7adaeefa96f3490c5a323ea8358489592c03300d Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Thu, 29 Jun 2023 06:17:00 +1000 Subject: [PATCH 15/44] Add Vless string parser --- src/worker-with-socks5-experimental.js | 142 ++++++++++++++++++++----- 1 file changed, 117 insertions(+), 25 deletions(-) diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js index 16d0e822c7..be1d919a28 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-with-socks5-experimental.js @@ -102,9 +102,13 @@ function getOutbound(curPos) { } /** + * Setup the config (uuid & outbounds) from environmental variables. + * This is the simplest case and should be preferred where possible. * @param {{ * UUID: string, * PROXYIP: string, + * PORTMAP: string, + * VLESS: string, * SOCKS5: string * }} env */ @@ -117,8 +121,8 @@ export function setConfigFromEnv(env) { address: env.PROXYIP }; - if (env.PROXYPORTMAP) { - forward.portMap = JSON.parse(env.PROXYPORTMAP); + if (env.PORTMAP) { + forward.portMap = JSON.parse(env.PORTMAP); } else { forward.portMap = {}; } @@ -126,6 +130,62 @@ export function setConfigFromEnv(env) { globalConfig['outbounds'].push(forward); } + // Example: vless://uuid@domain.name:port?type=ws&security=tls + if (env.VLESS) { + try { + const { + uuid, + remoteHost, + remotePort, + queryParams, + descriptiveText + } = parseVlessString(env.VLESS); + + let vless = { + "address": remoteHost, + "port": remotePort, + "users": [ + { + "id": uuid + } + ] + }; + + let streamSettings = { + "network": queryParams['type'], + "security": queryParams['security'], + } + + if (queryParams['type'] == 'ws') { + streamSettings.wsSettings = { + "headers": { + "Host": remoteHost + }, + "path": decodeURIComponent(queryParams['path']) + }; + } + + if (queryParams['security'] == 'tls') { + streamSettings.tlsSettings = { + "serverName": remoteHost, + "allowInsecure": false + }; + } + + globalConfig['outbounds'].push({ + protocol: "vless", + settings: { + "vnext": [ vless ] + }, + streamSettings: streamSettings + }); + } catch (err) { + /** @type {Error} */ + let e = err; + console.log(e.toString()); + } + } + // The user name and password should not contain special characters // Example: user:pass@host:port or host:port if (env.SOCKS5) { @@ -332,17 +392,25 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log,) { let curOutBoundPtr = {index: 0, serverIndex: 0}; - async function direct() { - const tcpSocket = await platformAPI.connect(addressRemote, portRemote, false); - tcpSocket.closed.catch(error => log('[freedom] tcpSocket closed with error: ', error.message)); + /** + * @param {object} tcpSocket + * @returns {ReadableStream} + */ + async function viaTCP(tcpSocket) { remoteSocket.writableStream = tcpSocket.writable; - log(`Connecting to ${addressRemote}:${portRemote}`); const writer = tcpSocket.writable.getWriter(); await writer.write(rawClientData); // First write, normally is tls client hello writer.releaseLock(); return tcpSocket.readable; } + async function direct() { + const tcpSocket = await platformAPI.connect(addressRemote, portRemote, false); + tcpSocket.closed.catch(error => log('[freedom] tcpSocket closed with error: ', error.message)); + log(`Connecting to ${addressRemote}:${portRemote}`); + return viaTCP(tcpSocket); + } + async function forward(proxyServer, portMap) { let portDest = portRemote; if (typeof portMap === "object" && portMap[portRemote] !== undefined) { @@ -351,14 +419,11 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR const tcpSocket = await platformAPI.connect(proxyServer, portDest, false); tcpSocket.closed.catch(error => log('[forward] tcpSocket closed with error: ', error.message)); - remoteSocket.writableStream = tcpSocket.writable; log(`Forwarding ${addressRemote}:${portRemote} to ${proxyServer}:${portDest}`); - const writer = tcpSocket.writable.getWriter(); - await writer.write(rawClientData); // First write, normally is tls client hello - writer.releaseLock(); - return tcpSocket.readable; + return viaTCP(tcpSocket); } + // TODO: known problem, if we send an unreachable request to a valid socks5 server, it will wait indefinitely async function socks5(address, port, user, pass) { const tcpSocket = await platformAPI.connect(address, port, false); tcpSocket.closed.catch(error => log('[socks] tcpSocket closed with error: ', error.message)); @@ -369,11 +434,7 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR log(`Socks5 outbound failed with: ${err.message}`); return null; } - remoteSocket.writableStream = tcpSocket.writable; - const writer = tcpSocket.writable.getWriter(); - await writer.write(rawClientData); // First write, normally is tls client hello - writer.releaseLock(); - return tcpSocket.readable; + return viaTCP(tcpSocket); } /** @@ -394,7 +455,7 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR let wsURL = streamSettings.security === 'tls' ? 'wss://' : 'ws://'; wsURL = wsURL + address + ':' + port; if (streamSettings.wsSettings && streamSettings.wsSettings.path) { - wsURL = wsURL + '/' + streamSettings.wsSettings.path; + wsURL = wsURL + streamSettings.wsSettings.path; } log(`Connecting to ${addressRemote}:${portRemote} via vless ${wsURL}`); @@ -404,10 +465,10 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR wsToVlessServer.onclose = (code, reason) => reject(new Error(`Closed with code ${code}, reason: ${reason}`)); wsToVlessServer.onerror = (error) => reject(error); - setTimeout(() => { - wsToVlessServer.close(); - reject({message: `Open connection timeout`}); - }, 1000); + // setTimeout(() => { + // wsToVlessServer.close(); + // reject({message: `Open connection timeout`}); + // }, 1000); }); // Wait for the connection to open @@ -424,10 +485,10 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR wsToVlessServer.onclose = (code, reason) => reject(new Error(`Closed with code ${code}, reason: ${reason}`)); wsToVlessServer.onerror = (error) => reject(error); - setTimeout(() => { - wsToVlessServer.close(); - reject({message: `Receive response timeout`}); - }, 1000); + // setTimeout(() => { + // wsToVlessServer.close(); + // reject({message: `Receive response timeout`}); + // }, 1000); }); const vlessFirstPacket = makeVlessHeader(addressType, addressRemote, portRemote, uuid, rawClientData); @@ -1131,6 +1192,37 @@ function checkVlessConfig(address, streamSettings) { } } +function parseVlessString(url) { + const regex = /^(.+):\/\/(.+?)@(.+?):(\d+)(\?[^#]*)?(#.*)?$/; + const match = url.match(regex); + + if (!match) { + throw new Error('Invalid URL format'); + } + + const [, protocol, uuid, remoteHost, remotePort, query, descriptiveText] = match; + + const json = { + protocol, + uuid, + remoteHost, + remotePort: parseInt(remotePort), + descriptiveText: descriptiveText ? descriptiveText.substring(1) : '', + queryParams: {} + }; + + if (query) { + const queryFields = query.substring(1).split('&'); + queryFields.forEach(field => { + const [key, value] = field.split('='); + json.queryParams[key] = value; + }); + } + + return json; +} + + /** * * @param {string | null} hostName From 89a5bc995dd73001f061ee2ff08567537a4ee1a5 Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Thu, 29 Jun 2023 17:20:06 +1000 Subject: [PATCH 16/44] Better Vless outbound --- src/worker-with-socks5-experimental.js | 127 ++++++++++++++----------- 1 file changed, 73 insertions(+), 54 deletions(-) diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js index be1d919a28..6e09fc4e33 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-with-socks5-experimental.js @@ -17,12 +17,12 @@ export let platformAPI = { /** * A wrapper for the TCP API, should return a Cloudflare Worker compatible socket. * See: https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/ - * @type {async (host: string, port: number, log: function) => + * @type {(host: string, port: number, log: function) => Promise< * { * readable: ReadableStream, - * writable: {getWriter: () => {write: (data) => void, releaseLock: () => void}} + * writable: {getWriter: () => {write: (data) => void, releaseLock: () => void}}, * closed: {Promise} - * } + * }> * } */ connect: null, @@ -305,7 +305,7 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { return 500; } - const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyData, log); + const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyData, null, log); /** @type {{ writableStream: WritableStream | null}}*/ let remoteSocketWapper = { @@ -393,22 +393,22 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR let curOutBoundPtr = {index: 0, serverIndex: 0}; /** - * @param {object} tcpSocket - * @returns {ReadableStream} + * @param {WritableStream} writableStream + * @param {Uint8Array} firstChunk */ - async function viaTCP(tcpSocket) { - remoteSocket.writableStream = tcpSocket.writable; - const writer = tcpSocket.writable.getWriter(); - await writer.write(rawClientData); // First write, normally is tls client hello + async function writeFirstChunk(writableStream, firstChunk) { + remoteSocket.writableStream = writableStream; + const writer = writableStream.getWriter(); + await writer.write(firstChunk); // First write, normally is tls client hello writer.releaseLock(); - return tcpSocket.readable; } async function direct() { const tcpSocket = await platformAPI.connect(addressRemote, portRemote, false); tcpSocket.closed.catch(error => log('[freedom] tcpSocket closed with error: ', error.message)); log(`Connecting to ${addressRemote}:${portRemote}`); - return viaTCP(tcpSocket); + writeFirstChunk(tcpSocket.writable, rawClientData); + return tcpSocket.readable; } async function forward(proxyServer, portMap) { @@ -420,7 +420,8 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR const tcpSocket = await platformAPI.connect(proxyServer, portDest, false); tcpSocket.closed.catch(error => log('[forward] tcpSocket closed with error: ', error.message)); log(`Forwarding ${addressRemote}:${portRemote} to ${proxyServer}:${portDest}`); - return viaTCP(tcpSocket); + writeFirstChunk(tcpSocket.writable, rawClientData); + return tcpSocket.readable; } // TODO: known problem, if we send an unreachable request to a valid socks5 server, it will wait indefinitely @@ -434,7 +435,8 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR log(`Socks5 outbound failed with: ${err.message}`); return null; } - return viaTCP(tcpSocket); + writeFirstChunk(tcpSocket.writable, rawClientData); + return tcpSocket.readable; } /** @@ -465,10 +467,9 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR wsToVlessServer.onclose = (code, reason) => reject(new Error(`Closed with code ${code}, reason: ${reason}`)); wsToVlessServer.onerror = (error) => reject(error); - // setTimeout(() => { - // wsToVlessServer.close(); - // reject({message: `Open connection timeout`}); - // }, 1000); + setTimeout(() => { + reject({message: `Open connection timeout`}); + }, 1000); }); // Wait for the connection to open @@ -476,37 +477,11 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR await openPromise; } catch (err) { log(`Cannot open Websocket connection: ${err.message}`); + wsToVlessServer.close(); return null; } - /** @type {Promise} */ - const recvPromise = new Promise((resolve, reject) => { - wsToVlessServer.onmessage = (event) => resolve(event.data); - wsToVlessServer.onclose = (code, reason) => - reject(new Error(`Closed with code ${code}, reason: ${reason}`)); - wsToVlessServer.onerror = (error) => reject(error); - // setTimeout(() => { - // wsToVlessServer.close(); - // reject({message: `Receive response timeout`}); - // }, 1000); - }); - - const vlessFirstPacket = makeVlessHeader(addressType, addressRemote, portRemote, uuid, rawClientData); - - // Send the first packet (header + rawClientData), then strip the response header - wsToVlessServer.send(vlessFirstPacket); - let firstResponse; - try { - firstResponse = await recvPromise; - } catch (err) { - log(`Cannot open Websocket connection: ${err.message}`); - return null; - } - const responseVersion = firstResponse[0]; // We should expect 0 here for now - const addtionalBytes = firstResponse[1]; - const earlyData = firstResponse.slice(2 + addtionalBytes); - - remoteSocket.writableStream = new WritableStream({ + const writableStream = new WritableStream({ async write(chunk, controller) { wsToVlessServer.send(chunk); }, @@ -518,7 +493,31 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR }, }); - return makeReadableWebSocketStream(wsToVlessServer, earlyData, log); + /** @type {(firstChunk : Uint8Array) => Uint8Array} */ + const headerStripper = (firstChunk) => { + if (firstChunk.length < 2) { + throw new Error('Too short vless response'); + } + + const responseVersion = firstChunk[0]; + const addtionalBytes = firstChunk[1]; + + if (responseVersion > 0) { + log('Warning: unexpected vless version: ${responseVersion}, only supports 0.'); + } + + if (addtionalBytes > 0) { + log('Warning: ignored ${addtionalBytes} byte(s) of additional information in the response.'); + } + + return firstChunk.slice(2 + addtionalBytes); + }; + + const readableStream = makeReadableWebSocketStream(wsToVlessServer, null, headerStripper, log); + const vlessFirstPacket = makeVlessHeader(addressType, addressRemote, portRemote, uuid, rawClientData); + // Send the first packet (header + rawClientData), then strip the response header with headerStripper + writeFirstChunk(writableStream, vlessFirstPacket); + return readableStream; } /** @returns {Promise} */ @@ -543,7 +542,7 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR return null; } - // if the cf connect tcp socket have no incoming data, we retry to redirect ip + // Try each outbound method until we find a working one. async function tryOutbound() { let outboundReadableStream = await connectAndWrite(); @@ -563,13 +562,18 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR } /** + * Make a source out of a WebSocket connection. + * A ReadableStream should be created before performing any kind of write operation. * * @param {import("@cloudflare/workers-types").WebSocket} webSocketServer - * @param {Uint8Array} earlyData + * @param {Uint8Array} earlyData Data received before the ReadableStream was created + * @param {(firstChunk : Uint8Array) => Uint8Array} headStripper In some protocol like Vless, + * a header is prepended to the first data chunk, it is necessary to strip that header. * @param {(info: string)=> void} log */ -function makeReadableWebSocketStream(webSocketServer, earlyData, log) { +function makeReadableWebSocketStream(webSocketServer, earlyData, headStripper, log) { let readableStreamCancel = false; + let headStripped = false; const stream = new ReadableStream({ start(controller) { if (earlyData) { @@ -580,7 +584,22 @@ function makeReadableWebSocketStream(webSocketServer, earlyData, log) { if (readableStreamCancel) { return; } - const message = event.data; + + let message = event.data; + if (!headStripped) { + headStripped = true; + + if (headStripper != null) { + try { + message = headStripper(message); + } catch (err) { + readableStreamCancel = true; + controller.error(err); + return; + } + } + } + controller.enqueue(message); }); @@ -794,9 +813,9 @@ async function remoteSocketToWS(remoteSocketReader, webSocket, vlessResponseHead log(`remoteSocket.readable is close, hasIncomingData = ${hasIncomingData}`); // safeCloseWebSocket(webSocket); // no need server close websocket frist for some case will casue HTTP ERR_CONTENT_LENGTH_MISMATCH issue, client will send close event anyway. }, - abort(reason) { - console.error(`remoteSocket.readable aborts`, reason); - }, + // abort(reason) { + // console.error(`remoteSocket.readable aborts`, reason); + // }, }) ) .catch((error) => { From d1c4fcadbd03674c3e073d86ded338cfd2c52316 Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Thu, 29 Jun 2023 18:14:16 +1000 Subject: [PATCH 17/44] More comments --- .gitignore | 1 + node/index.js | 24 +++++++++++++++++++++--- node/setup.sh | 2 +- src/worker-with-socks5-experimental.js | 25 +++++++++++++++++-------- 4 files changed, 40 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index 3c0ae8f248..90e8c8487b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ node_modules *.log dist src/package.json +node/config.js diff --git a/node/index.js b/node/index.js index 712af70e91..599be3aeeb 100644 --- a/node/index.js +++ b/node/index.js @@ -6,7 +6,7 @@ import http from 'http'; import net from 'net'; import WebSocket from 'ws'; -import {globalConfig, vlessOverWSHandler, platformAPI, getVLESSConfig} from '../src/worker-with-socks5-experimental.js'; +import {globalConfig, platformAPI, setConfigFromEnv, vlessOverWSHandler, getVLESSConfig} from '../src/worker-with-socks5-experimental.js'; // Create an HTTP server const server = http.createServer((req, res) => { @@ -51,6 +51,7 @@ function buf2hex(buffer) { // buffer is an ArrayBuffer */ platformAPI.connect = async (address, port, useTLS) => { const socket = net.createConnection(port, address); + // TODO: Implement TLS support? let readableStreamCancel = false; const readableStream = new ReadableStream({ @@ -126,5 +127,22 @@ platformAPI.connect = async (address, port, useTLS) => { platformAPI.newWebSocket = (url) => new WebSocket(url); -import {customConfig} from './config.js'; -globalConfig.outbounds = customConfig.outbounds; +async function loadModule() { + try { + const customConfig = await import('./config.js'); + + if (customConfig.useCustomOutbound) { + globalConfig.outbounds = customConfig.outbounds; + } else { + setConfigFromEnv(customConfig.env); + if (customConfig.forceProxy) { + globalConfig.outbounds = globalConfig.outbounds.filter(obj => obj.protocol !== "freedom"); + } + } + console.log(JSON.stringify(globalConfig.outbounds)); + } catch (err) { + console.error('Failed to load the module', err); + } +} + +loadModule(); diff --git a/node/setup.sh b/node/setup.sh index afccb17dcb..526ebf8eee 100755 --- a/node/setup.sh +++ b/node/setup.sh @@ -1,3 +1,3 @@ #!/bin/sh npm install ws -echo '{"type": "module"}' > ../src/package.json \ No newline at end of file +echo '{"type": "module"}' > ../src/package.json diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js index 6e09fc4e33..e5438d764f 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-with-socks5-experimental.js @@ -2,6 +2,7 @@ // How to generate your own UUID: // [Windows] Press "Win + R", input cmd and run: Powershell -NoExit -Command "[guid]::NewGuid()" +// [Linux] Run uuidgen in terminal export let globalConfig = { userID: 'd342d11e-d424-4583-b36e-524ab1f0afa4', @@ -13,9 +14,11 @@ export let globalConfig = { ] }; +// If you use this file as an ES module, you should set all fields below. export let platformAPI = { /** * A wrapper for the TCP API, should return a Cloudflare Worker compatible socket. + * The result is wrapped in a Promise, as in some platforms, the socket creation is async. * See: https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/ * @type {(host: string, port: number, log: function) => Promise< * { @@ -106,10 +109,10 @@ function getOutbound(curPos) { * This is the simplest case and should be preferred where possible. * @param {{ * UUID: string, - * PROXYIP: string, - * PORTMAP: string, - * VLESS: string, - * SOCKS5: string + * PROXYIP: string, // E.g. 114.51.4.0 + * PORTMAP: string, // E.g. {443:8443} + * VLESS: string, // E.g. vless://uuid@domain.name:port?type=ws&security=tls + * SOCKS5: string // E.g. user:pass@host:port or host:port * }} env */ export function setConfigFromEnv(env) { @@ -225,6 +228,7 @@ export function setConfigFromEnv(env) { } } +// Cloudflare Workers entry export default { /** * @param {import("@cloudflare/workers-types").Request} request @@ -288,6 +292,8 @@ try { } /** + * If you use this file as an ES module, you call this function whenever your Websocket server accepts a new connection. + * * @param {WebSocket} webSocket The established websocket connection to the client, must be an accepted * @param {string} earlyDataHeader for ws 0rtt, an optional field "sec-websocket-protocol" in the request header * may contain some base64 encoded data. @@ -319,7 +325,10 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { if (isDns) { return await handleDNSQuery(chunk, webSocket, null, log); } + if (remoteSocketWapper.writableStream) { + // After we parse the header and send the first chunk to the remote destination + // We assume that after the handshake, the stream only contains the original traffic. const writer = remoteSocketWapper.writableStream.getWriter(); await writer.write(chunk); writer.releaseLock(); @@ -766,10 +775,10 @@ function processVlessHeader( /** - * - * @param {ReadableStream} remoteSocketReader - * @param {import("@cloudflare/workers-types").WebSocket} webSocket - * @param {ArrayBuffer} vlessResponseHeader + * Stream data from the remote destination (any) to the client side (Websocket) + * @param {ReadableStream} remoteSocketReader from the remote destination + * @param {import("@cloudflare/workers-types").WebSocket} webSocket to the client side + * @param {ArrayBuffer} vlessResponseHeader header that should be send to the client side * @param {(() => Promise) | null} retry * @param {*} log */ From f6eecad9692af8a56064e7a2562fe8c4a90deb72 Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Thu, 29 Jun 2023 23:58:16 +1000 Subject: [PATCH 18/44] Fix vless on workers --- src/worker-with-socks5-experimental.js | 35 +++++++++++++++----------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js index e5438d764f..13e6634d71 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-with-socks5-experimental.js @@ -40,7 +40,7 @@ export let platformAPI = { /** * Foreach globalConfig.outbounds, start with {index: 0, serverIndex: 0} * @param {{index: number, serverIndex: number}} curPos - * @returns {protocol: string, address: string, port: number, user: string, pass: string, + * @returns {{protocol: string, address: string, port: number, user: string, pass: string, * portMap: {Number: number}}} */ function getOutbound(curPos) { @@ -118,6 +118,12 @@ function getOutbound(curPos) { export function setConfigFromEnv(env) { globalConfig.userID = env.UUID || globalConfig.userID; + globalConfig.outbounds = [ + { + protocol: "freedom" // Compulsory, outbound locally. + } + ]; + if (env.PROXYIP) { let forward = { protocol: "forward", @@ -523,18 +529,20 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR }; const readableStream = makeReadableWebSocketStream(wsToVlessServer, null, headerStripper, log); - const vlessFirstPacket = makeVlessHeader(addressType, addressRemote, portRemote, uuid, rawClientData); + const vlessReqHeader = makeVlessReqHeader(addressType, addressRemote, portRemote, uuid, rawClientData); // Send the first packet (header + rawClientData), then strip the response header with headerStripper - writeFirstChunk(writableStream, vlessFirstPacket); + writeFirstChunk(writableStream, await new Blob([vlessReqHeader, rawClientData]).arrayBuffer()); return readableStream; } /** @returns {Promise} */ async function connectAndWrite() { - console.log('connectAndWrite'); const outbound = getOutbound(curOutBoundPtr); if (outbound == null) { + log('Reached end of the outbound chain'); return null; + } else { + log(`Trying outbound ${curOutBoundPtr.index}:${curOutBoundPtr.serverIndex}`); } switch (outbound.protocol) { @@ -560,7 +568,7 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR } if (outboundReadableStream == null) { - log('Reached end of the outbound chain, abort!'); + log('No more available outbound chain, abort!'); safeCloseWebSocket(webSocket); } else { remoteSocketToWS(outboundReadableStream, webSocket, vlessResponseHeader, tryOutbound, log); @@ -600,7 +608,9 @@ function makeReadableWebSocketStream(webSocketServer, earlyData, headStripper, l if (headStripper != null) { try { - message = headStripper(message); + // We have to make sure that we are on a Uint8Array. + const firstChunk = new Uint8Array(message); + message = headStripper(firstChunk); } catch (err) { readableStreamCancel = true; controller.error(err); @@ -626,7 +636,7 @@ function makeReadableWebSocketStream(webSocketServer, earlyData, headStripper, l } ); webSocketServer.addEventListener('error', (err) => { - log('webSocketServer has error'); + log('webSocketServer has error: ' + err.message); controller.error(err); } ); @@ -839,7 +849,7 @@ async function remoteSocketToWS(remoteSocketReader, webSocket, vlessResponseHead // 1. Socket.closed will have error // 2. Socket.readable will be close without any data coming if (hasIncomingData === false && retry) { - log(`retry`) + log(`No incoming data from the remote host, retry`); retry(); } } @@ -1115,14 +1125,14 @@ function socks5AddressParser(address) { } /** + * Generate a vless request header. * @param {number} destType * @param {string} destAddr * @param {number} destPort * @param {string} uuid - * @param {ArrayBuffer | Uint8Array} rawClientData * @returns {Uint8Array} */ -function makeVlessHeader(destType, destAddr, destPort, uuid, rawClientData) { +function makeVlessReqHeader(destType, destAddr, destPort, uuid) { /** @type {number} */ let addressFieldLength; /** @type {Uint8Array | undefined} */ @@ -1144,7 +1154,7 @@ function makeVlessHeader(destType, destAddr, destPort, uuid, rawClientData) { const uuidString = uuid.replace(/-/g, ''); const uuidOffset = 1; - const vlessHeader = new Uint8Array(22 + addressFieldLength + rawClientData.length); + const vlessHeader = new Uint8Array(22 + addressFieldLength); // Protocol Version = 0 vlessHeader[0] = 0x00; @@ -1196,9 +1206,6 @@ function makeVlessHeader(destType, destAddr, destPort, uuid, rawClientData) { throw new Error(`Unknown address type: ${destType}`); } - // Payload - vlessHeader.set(rawClientData, 22 + addressFieldLength); - return vlessHeader; } From 6a1ae11ba8b2b888e025798ec71b6399a08582f4 Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Fri, 30 Jun 2023 19:36:28 +1000 Subject: [PATCH 19/44] Allowing the worker to push log to a remote server via env.LOGPOST --- src/worker-with-socks5-experimental.js | 41 +++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js index 13e6634d71..32b0ff6378 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-with-socks5-experimental.js @@ -6,6 +6,9 @@ export let globalConfig = { userID: 'd342d11e-d424-4583-b36e-524ab1f0afa4', + // Time to wait before an outbound Websocket connection is established, in ms. + openWSOutboundTimeout: 10000, + // The order controls where to send the traffic after the previous one fails outbounds: [ { @@ -243,6 +246,10 @@ export default { * @returns {Promise} */ async fetch(request, env, ctx) { + if (env.LOGPOST) { + redirectConsoleLog(env.LOGPOST, crypto.randomUUID()); + } + try { setConfigFromEnv(env); const upgradeHeader = request.headers.get('Upgrade'); @@ -286,6 +293,38 @@ export default { }, }; +/** + * @param {string} logServer URL of the log server + * @param {string} instanceId a UUID representing each instance + */ +export function redirectConsoleLog(logServer, instanceId) { + let logID = 0; + const oldConsoleLog = console.log; + console.log = async (data) => { + oldConsoleLog(data); + if (data == null) { + return; + } + + let msg; + if (data instanceof Object) { + msg = JSON.stringify(data); + } else { + msg = String(data); + } + + try { + await fetch(logServer, { + method: 'POST', + headers: { 'Content-Type': "text/plain;charset=UTF-8" }, + body: instanceId + ` ${logID++} ` + msg + }); + } catch (err) { + oldConsoleLog(err.message); + } + }; +} + try { const module = await import('cloudflare:sockets'); platformAPI.connect = async (address, port, useTLS) => { @@ -484,7 +523,7 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR wsToVlessServer.onerror = (error) => reject(error); setTimeout(() => { reject({message: `Open connection timeout`}); - }, 1000); + }, globalConfig.openWSOutboundTimeout); }); // Wait for the connection to open From 7637d591377b06aa47f57e01293f701a8282db23 Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Sat, 1 Jul 2023 22:30:43 +1000 Subject: [PATCH 20/44] Support UDP tunneling via vless --- src/worker-with-socks5-experimental.js | 111 +++++++++++++++---------- 1 file changed, 65 insertions(+), 46 deletions(-) diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js index 32b0ff6378..126c1d28f5 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-with-socks5-experimental.js @@ -34,7 +34,7 @@ export let platformAPI = { connect: null, /** - * A wrapper for the TCP API. + * A wrapper for the Websocket API. * @type {(url: string) => WebSocket} returns a WebSocket, should be compatile with the standard WebSocket API. */ newWebSocket: null, @@ -107,6 +107,14 @@ function getOutbound(curPos) { return retVal; } +function canOutboundUDPVia(protocolName) { + switch(protocolName) { + case 'vless': + return true; + } + return false; +} + /** * Setup the config (uuid & outbounds) from environmental variables. * This is the simplest case and should be preferred where possible. @@ -398,24 +406,17 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { // webSocket.close(1000, message); return; } - // if UDP but port not DNS port, close it - if (isUDP) { - if (portRemote === 53) { - isDns = true; - } else { - // controller.error('UDP proxy only enable for DNS which is port 53'); - throw new Error('UDP proxy only enable for DNS which is port 53'); // cf seems has bug, controller.error will not end stream - return; - } - } + // ["version", "附加信息长度 N"] const vlessResponseHeader = new Uint8Array([vlessVersion[0], 0]); const rawClientData = chunk.slice(rawDataIndex); - if (isDns) { - return handleDNSQuery(rawClientData, webSocket, vlessResponseHeader, log); + if (isUDP && portRemote === 53) { + // Short circuit UDP DNS query to a TCP DNS query + handleDNSQuery(rawClientData, webSocket, vlessResponseHeader, log); + } else { + handleOutBound(remoteSocketWapper, isUDP, addressType, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log); } - handleTCPOutBound(remoteSocketWapper, addressType, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log); }, close() { log(`readableWebSocketStream has been closed`); @@ -431,9 +432,10 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { } /** - * Handles outbound TCP connections. + * Handles outbound connections. * * @param {{ writableStream: WritableStream | null}} remoteSocket + * @param {boolean} isUDP * @param {number} addressType The remote address type to connect to. * @param {string} addressRemote The remote address to connect to. * @param {number} portRemote The remote port to connect to. @@ -443,7 +445,7 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { * @param {function} log The logging function. * @returns {Promise} The remote socket. */ -async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log,) { +async function handleOutBound(remoteSocket, isUDP, addressType, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log,) { let curOutBoundPtr = {index: 0, serverIndex: 0}; /** @@ -460,7 +462,7 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR async function direct() { const tcpSocket = await platformAPI.connect(addressRemote, portRemote, false); tcpSocket.closed.catch(error => log('[freedom] tcpSocket closed with error: ', error.message)); - log(`Connecting to ${addressRemote}:${portRemote}`); + log(`Connecting to tcp://${addressRemote}:${portRemote}`); writeFirstChunk(tcpSocket.writable, rawClientData); return tcpSocket.readable; } @@ -473,16 +475,17 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR const tcpSocket = await platformAPI.connect(proxyServer, portDest, false); tcpSocket.closed.catch(error => log('[forward] tcpSocket closed with error: ', error.message)); - log(`Forwarding ${addressRemote}:${portRemote} to ${proxyServer}:${portDest}`); + log(`Forwarding tcp://${addressRemote}:${portRemote} to ${proxyServer}:${portDest}`); writeFirstChunk(tcpSocket.writable, rawClientData); return tcpSocket.readable; } // TODO: known problem, if we send an unreachable request to a valid socks5 server, it will wait indefinitely + // TODO: Add support for proxying UDP via socks5 on runtimes that support UDP outbound async function socks5(address, port, user, pass) { const tcpSocket = await platformAPI.connect(address, port, false); tcpSocket.closed.catch(error => log('[socks] tcpSocket closed with error: ', error.message)); - log(`Connecting to ${addressRemote}:${portRemote} via socks5 ${address}:${port}`); + log(`Connecting to ${isUDP ? 'UDP' : 'TCP'}://${addressRemote}:${portRemote} via socks5 ${address}:${port}`); try { await socks5Connect(tcpSocket, user, pass, addressType, addressRemote, portRemote, log); } catch(err) { @@ -494,6 +497,13 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR } /** + * Start streaming traffic to a remote vless server. + * The first message must contain the query header plus part of the payload! + * The vless server responds to it with a response header plus part of the response from the destination. + * After the first message exchange, in the case of TCP, the streams in both directions carry raw TCP streams. + * Fragmentation won't cause any problem after the first message exchange. + * In the case of UDP, a 16-bit big-endian length field is prepended to each UDP datagram and then send through the streams. + * The first message exchange still applies. * * @param {string} address * @param {number} port @@ -513,7 +523,7 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR if (streamSettings.wsSettings && streamSettings.wsSettings.path) { wsURL = wsURL + streamSettings.wsSettings.path; } - log(`Connecting to ${addressRemote}:${portRemote} via vless ${wsURL}`); + log(`Connecting to ${isUDP ? 'UDP' : 'TCP'}://${addressRemote}:${portRemote} via vless ${wsURL}`); const wsToVlessServer = platformAPI.newWebSocket(wsURL); const openPromise = new Promise((resolve, reject) => { @@ -568,7 +578,7 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR }; const readableStream = makeReadableWebSocketStream(wsToVlessServer, null, headerStripper, log); - const vlessReqHeader = makeVlessReqHeader(addressType, addressRemote, portRemote, uuid, rawClientData); + const vlessReqHeader = makeVlessReqHeader(isUDP ? VlessCmd.UDP : VlessCmd.TCP, addressType, addressRemote, portRemote, uuid, rawClientData); // Send the first packet (header + rawClientData), then strip the response header with headerStripper writeFirstChunk(writableStream, await new Blob([vlessReqHeader, rawClientData]).arrayBuffer()); return readableStream; @@ -584,6 +594,11 @@ async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portR log(`Trying outbound ${curOutBoundPtr.index}:${curOutBoundPtr.serverIndex}`); } + if (isUDP && !canOutboundUDPVia(outbound.protocol)) { + // This outbound method does not support UDP + return null; + } + switch (outbound.protocol) { case 'freedom': return await direct(); @@ -740,16 +755,13 @@ function processVlessHeader( vlessBuffer.slice(18 + optLength, 18 + optLength + 1) )[0]; - // 0x01 TCP - // 0x02 UDP - // 0x03 MUX - if (command === 1) { - } else if (command === 2) { + if (command === VlessCmd.TCP) { + } else if (command === VlessCmd.UDP) { isUDP = true; } else { return { hasError: true, - message: `command ${command} is not support, command 01-tcp,02-udp,03-mux`, + message: `Invalid command type: ${command}, only accepts: ${JSON.stringify(VlessCmd)}`, }; } const portIndex = 18 + optLength + 1; @@ -762,21 +774,18 @@ function processVlessHeader( vlessBuffer.slice(addressIndex, addressIndex + 1) ); - // 1--> ipv4 addressLength =4 - // 2--> domain name addressLength=addressBuffer[1] - // 3--> ipv6 addressLength =16 const addressType = addressBuffer[0]; let addressLength = 0; let addressValueIndex = addressIndex + 1; let addressValue = ''; switch (addressType) { - case 1: + case VlessAddrType.IPv4: addressLength = 4; addressValue = new Uint8Array( vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) ).join('.'); break; - case 2: + case VlessAddrType.DomainName: addressLength = new Uint8Array( vlessBuffer.slice(addressValueIndex, addressValueIndex + 1) )[0]; @@ -785,7 +794,7 @@ function processVlessHeader( vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) ); break; - case 3: + case VlessAddrType.IPv6: addressLength = 16; const dataView = new DataView( vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) @@ -801,7 +810,7 @@ function processVlessHeader( default: return { hasError: true, - message: `Invild addressType: ${addressType}`, + message: `Invalid address type: ${addressType}, only accepts: ${JSON.stringify(VlessAddrType)}`, }; } if (!addressValue) { @@ -1163,28 +1172,41 @@ function socks5AddressParser(address) { } } +const VlessCmd = { + TCP: 1, + UDP: 2, + MUX: 3, +}; + +const VlessAddrType = { + IPv4: 1, // 4-bytes + DomainName: 2, // The first byte indicates the length of the following domain name + IPv6: 3, // 16-bytes +}; + /** * Generate a vless request header. - * @param {number} destType + * @param {number} command see VlessCmd + * @param {number} destType see VlessAddrType * @param {string} destAddr * @param {number} destPort * @param {string} uuid * @returns {Uint8Array} */ -function makeVlessReqHeader(destType, destAddr, destPort, uuid) { +function makeVlessReqHeader(command, destType, destAddr, destPort, uuid) { /** @type {number} */ let addressFieldLength; /** @type {Uint8Array | undefined} */ let addressEncoded; switch (destType) { - case 1: + case VlessAddrType.IPv4: addressFieldLength = 4; break; - case 2: + case VlessAddrType.DomainName: addressEncoded = new TextEncoder().encode(destAddr); addressFieldLength = addressEncoded.length + 1; break; - case 3: + case VlessAddrType.IPv6: addressFieldLength = 16; break; default: @@ -1206,10 +1228,7 @@ function makeVlessReqHeader(destType, destAddr, destPort, uuid) { vlessHeader[17] = 0x00; // Instruction - // 0x01 TCP - // 0x02 UDP - // 0x03 MUX - vlessHeader[18] = 0x01; // Assume TCP + vlessHeader[18] = command; // Port, 2-byte big-endian vlessHeader[19] = destPort >> 8; @@ -1223,17 +1242,17 @@ function makeVlessReqHeader(destType, destAddr, destPort, uuid) { // Address switch (destType) { - case 1: + case VlessAddrType.IPv4: const octetsIPv4 = destAddr.split('.'); for (let i = 0; i < 4; i++) { vlessHeader[22 + i] = parseInt(octetsIPv4[i]); } break; - case 2: + case VlessAddrType.DomainName: vlessHeader[22] = addressEncoded.length; vlessHeader.set(addressEncoded, 23); break; - case 3: + case VlessAddrType.IPv6: const groupsIPv6 = ipv6.split(':'); for (let i = 0; i < 8; i++) { const hexGroup = parseInt(groupsIPv6[i], 16); From 29542c528563d7a4c14c0bac654ecd41304da344 Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Mon, 3 Jul 2023 14:48:40 +1000 Subject: [PATCH 21/44] Add UDP outbound support if run on node --- node/index.js | 21 ++++- src/worker-with-socks5-experimental.js | 108 ++++++++++++++++++++++++- 2 files changed, 127 insertions(+), 2 deletions(-) diff --git a/node/index.js b/node/index.js index 599be3aeeb..805f19efd4 100644 --- a/node/index.js +++ b/node/index.js @@ -5,6 +5,7 @@ import http from 'http'; import net from 'net'; import WebSocket from 'ws'; +import {createSocket as createUDPSocket} from 'dgram'; import {globalConfig, platformAPI, setConfigFromEnv, vlessOverWSHandler, getVLESSConfig} from '../src/worker-with-socks5-experimental.js'; @@ -127,6 +128,24 @@ platformAPI.connect = async (address, port, useTLS) => { platformAPI.newWebSocket = (url) => new WebSocket(url); +platformAPI.associate = async (isIPv6) => { + const UDPSocket = createUDPSocket('udp4'); + return { + send: (datagram, offset, length, port, address, sendDoneCallback) => { + UDPSocket.send(datagram, offset, length, port, address, sendDoneCallback); + }, + close: () => { + UDPSocket.close(); + }, + onmessage: (handler) => { + UDPSocket.on('message', handler); + }, + onerror: (handler) => { + UDPSocket.on('error', handler); + } + }; +} + async function loadModule() { try { const customConfig = await import('./config.js'); @@ -144,5 +163,5 @@ async function loadModule() { console.error('Failed to load the module', err); } } - + loadModule(); diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js index 126c1d28f5..12a3a4429f 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-with-socks5-experimental.js @@ -23,7 +23,7 @@ export let platformAPI = { * A wrapper for the TCP API, should return a Cloudflare Worker compatible socket. * The result is wrapped in a Promise, as in some platforms, the socket creation is async. * See: https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/ - * @type {(host: string, port: number, log: function) => Promise< + * @type {(host: string, port: number, useTLS: boolean) => Promise< * { * readable: ReadableStream, * writable: {getWriter: () => {write: (data) => void, releaseLock: () => void}}, @@ -38,6 +38,20 @@ export let platformAPI = { * @type {(url: string) => WebSocket} returns a WebSocket, should be compatile with the standard WebSocket API. */ newWebSocket: null, + + /** + * A wrapper for the UDP API, should return a NodeJS compatible UDP socket. + * The result is wrapped in a Promise, as in some platforms, the socket creation is async. + * @type {(isIPv6: boolean) => Promise< + * { + * send: (datagram: any, offset: number, length: number, port: number, address: string, sendDoneCallback: (err: Error | null, bytes: number) => void) => void, + * close: () => void, + * onmessage: (handler: (msg: Buffer, rinfo: RemoteInfo) => void) => void, + * onerror: (handler: (err: Error) => void) => void, + * }> + * } + */ + associate: null, } /** @@ -109,6 +123,8 @@ function getOutbound(curPos) { function canOutboundUDPVia(protocolName) { switch(protocolName) { + case 'freedom': + return platformAPI.associate != null; case 'vless': return true; } @@ -460,6 +476,15 @@ async function handleOutBound(remoteSocket, isUDP, addressType, addressRemote, p } async function direct() { + if (isUDP) { + const udpClient = await platformAPI.associate(false); + const writableStream = makeWritableUDPStream(udpClient, addressRemote, portRemote, log); + const readableStream = makeReadableUDPStream(udpClient, log); + log(`Connected to UDP://${addressRemote}:${portRemote}`); + writeFirstChunk(writableStream, rawClientData); + return readableStream; + } + const tcpSocket = await platformAPI.connect(addressRemote, portRemote, false); tcpSocket.closed.catch(error => log('[freedom] tcpSocket closed with error: ', error.message)); log(`Connecting to tcp://${addressRemote}:${portRemote}`); @@ -632,6 +657,87 @@ async function handleOutBound(remoteSocket, isUDP, addressType, addressRemote, p await tryOutbound(); } +/** + * Make a source out of a UDP socket, wrap each datagram with vless UDP packing. + * Each receive datagram will be prepended with a 16-bit big-endian length field. + * + * @param {*} udpClient + * @param {(info: string)=> void} log + * @returns {ReadableStream} Datagrams received will be wrapped and made available in this stream. + */ +function makeReadableUDPStream(udpClient, log) { + return new ReadableStream({ + start(controller) { + udpClient.onmessage((message, info) => { + // log(`Received ${info.size} bytes from UDP://${info.address}:${info.port}`) + // Prepend length to each UDP datagram + new Blob([new Uint8Array([(info.size >> 8) & 0xff, info.size & 0xff]), message]).arrayBuffer().then(encodedChunk => { + controller.enqueue(encodedChunk); + }); + }); + udpClient.onerror((error) => { + log('UDP Error: ', error); + controller.error(error); + }); + }, + cancel(reason) { + log(`UDP ReadableStream closed:`, reason); + safeCloseUDP(udpClient); + }, + }); +} + +/** + * Make a sink out of a UDP socket, the input stream assumes valid vless UDP packing. + * Each datagram to be sent should be prepended with a 16-bit big-endian length field. + * + * @param {*} udpClient + * @param {string} addressRemote + * @param {port} portRemote + * @param {(info: string)=> void} log + * @returns {WritableStream} write to this stream will send datagrams via UDP. + */ +function makeWritableUDPStream(udpClient, addressRemote, portRemote, log) { + return new WritableStream({ + /** @param {ArrayBuffer} chunk */ + async write(chunk, controller) { + const byteArray = new Uint8Array(chunk); + let i = 0; + while (i < byteArray.length) { + // Big-endian + const datagramLen = (byteArray[i] << 8) | byteArray[i+1]; + + await new Promise((resolve, reject) => { + udpClient.send(byteArray, i + 2, datagramLen, portRemote, addressRemote, (err, bytes) => { + if (err != null) { + console.log('UDP send error', err); + controller.error(`Failed to send UDP packet !! ${err}`); + safeCloseUDP(udpClient); + } + }); + resolve(); + }); + + i += datagramLen + 2; + } + }, + close() { + log(`UDP WritableStream closed`); + }, + abort(reason) { + console.error(`UDP WritableStream aborted`, reason); + }, + }); +} + +function safeCloseUDP(socket) { + try { + socket.close(); + } catch (error) { + console.error('safeCloseUDP error', error); + } +} + /** * Make a source out of a WebSocket connection. * A ReadableStream should be created before performing any kind of write operation. From e54a09395856bf611c07ceff21ea904e53f1305b Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Mon, 3 Jul 2023 14:52:47 +1000 Subject: [PATCH 22/44] Support IPv6 UDP outbound Check: Domain names that only resolves to AAAA may not work with UDP --- node/index.js | 2 +- src/worker-with-socks5-experimental.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/node/index.js b/node/index.js index 805f19efd4..7f7444a966 100644 --- a/node/index.js +++ b/node/index.js @@ -129,7 +129,7 @@ platformAPI.connect = async (address, port, useTLS) => { platformAPI.newWebSocket = (url) => new WebSocket(url); platformAPI.associate = async (isIPv6) => { - const UDPSocket = createUDPSocket('udp4'); + const UDPSocket = createUDPSocket(isIPv6 ? 'udp6' : 'udp4'); return { send: (datagram, offset, length, port, address, sendDoneCallback) => { UDPSocket.send(datagram, offset, length, port, address, sendDoneCallback); diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js index 12a3a4429f..6e34c83653 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-with-socks5-experimental.js @@ -477,7 +477,8 @@ async function handleOutBound(remoteSocket, isUDP, addressType, addressRemote, p async function direct() { if (isUDP) { - const udpClient = await platformAPI.associate(false); + // TODO: Check what will happen if addressType == VlessAddrType.DomainName and that domain only resolves to a IPv6 + const udpClient = await platformAPI.associate(addressType == VlessAddrType.IPv6); const writableStream = makeWritableUDPStream(udpClient, addressRemote, portRemote, log); const readableStream = makeReadableUDPStream(udpClient, log); log(`Connected to UDP://${addressRemote}:${portRemote}`); From f8434bab2dc5bb1b5f9d7d276019d7c90c3bae37 Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Fri, 7 Jul 2023 03:45:50 +1000 Subject: [PATCH 23/44] Simply DNS over TCP implementation --- src/worker-with-socks5-experimental.js | 84 ++++++-------------------- 1 file changed, 20 insertions(+), 64 deletions(-) diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js index 6e34c83653..e9a80d4ab1 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-with-socks5-experimental.js @@ -9,6 +9,10 @@ export let globalConfig = { // Time to wait before an outbound Websocket connection is established, in ms. openWSOutboundTimeout: 10000, + // Since Cloudflare Worker does not support UDP outbound, we may try DNS over TCP. + // Set to an empty string to disable UDP to TCP forwarding for DNS queries. + dnsTCPServer: "8.8.4.4", + // The order controls where to send the traffic after the previous one fails outbounds: [ { @@ -386,15 +390,10 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { let remoteSocketWapper = { writableStream: null, }; - let isDns = false; // ws --> remote readableWebSocketStream.pipeTo(new WritableStream({ async write(chunk, controller) { - if (isDns) { - return await handleDNSQuery(chunk, webSocket, null, log); - } - if (remoteSocketWapper.writableStream) { // After we parse the header and send the first chunk to the remote destination // We assume that after the handshake, the stream only contains the original traffic. @@ -420,19 +419,13 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { // controller.error(message); throw new Error(message); // cf seems has bug, controller.error will not end stream // webSocket.close(1000, message); - return; } - // ["version", "附加信息长度 N"] + // ["version", "length of additional info"] const vlessResponseHeader = new Uint8Array([vlessVersion[0], 0]); const rawClientData = chunk.slice(rawDataIndex); - if (isUDP && portRemote === 53) { - // Short circuit UDP DNS query to a TCP DNS query - handleDNSQuery(rawClientData, webSocket, vlessResponseHeader, log); - } else { - handleOutBound(remoteSocketWapper, isUDP, addressType, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log); - } + handleOutBound(remoteSocketWapper, isUDP, addressType, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log); }, close() { log(`readableWebSocketStream has been closed`); @@ -476,7 +469,12 @@ async function handleOutBound(remoteSocket, isUDP, addressType, addressRemote, p } async function direct() { - if (isUDP) { + // Check if we should forward UDP DNS requests to a designated TCP DNS server. + // The vless packing of UDP datagrams is identical to the one used in TCP DNS protocol, + // so we can directly send raw vless traffic to the TCP DNS server. + const forwardDNS = isUDP && (portRemote == 53) && (globalConfig.dnsTCPServer ? true : false); + + if (isUDP && !forwardDNS) { // TODO: Check what will happen if addressType == VlessAddrType.DomainName and that domain only resolves to a IPv6 const udpClient = await platformAPI.associate(addressType == VlessAddrType.IPv6); const writableStream = makeWritableUDPStream(udpClient, addressRemote, portRemote, log); @@ -486,9 +484,15 @@ async function handleOutBound(remoteSocket, isUDP, addressType, addressRemote, p return readableStream; } - const tcpSocket = await platformAPI.connect(addressRemote, portRemote, false); + let addressTCP = addressRemote; + if (forwardDNS) { + addressTCP = globalConfig.dnsTCPServer; + log(`Redirect DNS request sent to UDP://${addressRemote}:${portRemote}`); + } + + const tcpSocket = await platformAPI.connect(addressTCP, portRemote, false); tcpSocket.closed.catch(error => log('[freedom] tcpSocket closed with error: ', error.message)); - log(`Connecting to tcp://${addressRemote}:${portRemote}`); + log(`Connecting to tcp://${addressTCP}:${portRemote}`); writeFirstChunk(tcpSocket.writable, rawClientData); return tcpSocket.readable; } @@ -1069,54 +1073,6 @@ function stringify(arr, offset = 0) { return uuid; } -/** - * - * @param {ArrayBuffer} udpChunk - * @param {import("@cloudflare/workers-types").WebSocket} webSocket - * @param {ArrayBuffer} vlessResponseHeader null means the header has been sent. - * @param {(string)=> void} log - */ -async function handleDNSQuery(udpChunk, webSocket, vlessResponseHeader, log) { - // We always ignore the DNS server requested by the client and use our hard code one, - // as some DNS server does not support DNS over TCP. - try { - // TODO: Switch to 1.1.1.1 after Cloudflare fixes its well known Workers TCP problem. - const dnsServer = '8.8.4.4'; - const dnsPort = 53; - /** @type {ArrayBuffer | null} */ - let vlessHeader = vlessResponseHeader; - /** @type {import("@cloudflare/workers-types").Socket} */ - const tcpSocket = await platformAPI.connect(dnsServer, dnsPort, false); - - log(`connected to ${dnsServer}:${dnsPort}`); - const writer = tcpSocket.writable.getWriter(); - await writer.write(udpChunk); - writer.releaseLock(); - await tcpSocket.readable.pipeTo(new WritableStream({ - async write(chunk) { - if (webSocket.readyState === WS_READY_STATE_OPEN) { - if (vlessHeader) { - webSocket.send(await new Blob([vlessHeader, chunk]).arrayBuffer()); - vlessHeader = null; - } else { - webSocket.send(chunk); - } - } - }, - close() { - log(`dns server(${dnsServer}) tcp is close`); - }, - abort(reason) { - console.error(`dns server(${dnsServer}) tcp is abort`, reason); - }, - })); - } catch (error) { - console.error( - `handleDNSQuery have exception, error: ${error.message}` - ); - } -} - /** * @param {{readable: ReadableStream, writable: {getWriter: () => {write: (data) => void, releaseLock: () => void}}}} socket * @param {string} username From 85301eac69e255455681e5f6d0dca1cc1ffb2281 Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Sun, 9 Jul 2023 23:20:33 +1000 Subject: [PATCH 24/44] Fix indentation --- src/worker-with-socks5-experimental.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js index e9a80d4ab1..b03afc8919 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-with-socks5-experimental.js @@ -199,17 +199,17 @@ export function setConfigFromEnv(env) { if (queryParams['type'] == 'ws') { streamSettings.wsSettings = { "headers": { - "Host": remoteHost - }, - "path": decodeURIComponent(queryParams['path']) - }; + "Host": remoteHost + }, + "path": decodeURIComponent(queryParams['path']) + }; } if (queryParams['security'] == 'tls') { streamSettings.tlsSettings = { - "serverName": remoteHost, - "allowInsecure": false - }; + "serverName": remoteHost, + "allowInsecure": false + }; } globalConfig['outbounds'].push({ From 54734bde02f91c4e6c1b72318dee0238902e6854 Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Mon, 10 Jul 2023 17:08:44 +1000 Subject: [PATCH 25/44] Fix UDP outbound on discontinued streams --- src/worker-with-socks5-experimental.js | 48 ++++++++++++++++++-------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js index b03afc8919..256ec25680 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-with-socks5-experimental.js @@ -703,24 +703,43 @@ function makeReadableUDPStream(udpClient, log) { * @returns {WritableStream} write to this stream will send datagrams via UDP. */ function makeWritableUDPStream(udpClient, addressRemote, portRemote, log) { + /** @type {Uint8Array} */ + let leftoverData = new Uint8Array(0); + return new WritableStream({ /** @param {ArrayBuffer} chunk */ - async write(chunk, controller) { - const byteArray = new Uint8Array(chunk); + write(chunk, controller) { + let byteArray = new Uint8Array(chunk); + if (leftoverData.byteLength > 0) { + // If we have any leftover data from previous chunk, merge it first + byteArray = new Uint8Array(leftoverData.byteLength + chunk.byteLength); + byteArray.set(leftoverData, 0); + byteArray.set(new Uint8Array(chunk), leftoverData.byteLength); + } + let i = 0; while (i < byteArray.length) { + if (i+1 >= byteArray.length) { + // The length field is not intact + leftoverData = byteArray.slice(i); + break; + } + // Big-endian const datagramLen = (byteArray[i] << 8) | byteArray[i+1]; - await new Promise((resolve, reject) => { - udpClient.send(byteArray, i + 2, datagramLen, portRemote, addressRemote, (err, bytes) => { - if (err != null) { - console.log('UDP send error', err); - controller.error(`Failed to send UDP packet !! ${err}`); - safeCloseUDP(udpClient); - } - }); - resolve(); + if (i+2+datagramLen > byteArray.length) { + // This UDP datagram is not intact + leftoverData = byteArray.slice(i); + break; + } + + udpClient.send(byteArray, i + 2, datagramLen, portRemote, addressRemote, (err, bytes) => { + if (err != null) { + console.log('UDP send error', err); + controller.error(`Failed to send UDP packet !! ${err}`); + safeCloseUDP(udpClient); + } }); i += datagramLen + 2; @@ -907,13 +926,12 @@ function processVlessHeader( break; case VlessAddrType.IPv6: addressLength = 16; - const dataView = new DataView( - vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) - ); + const ipv6Bytes = (new Uint8Array(vlessBuffer)).slice(addressValueIndex, addressValueIndex + addressLength); // 2001:0db8:85a3:0000:0000:8a2e:0370:7334 const ipv6 = []; for (let i = 0; i < 8; i++) { - ipv6.push(dataView.getUint16(i * 2).toString(16)); + const uint16_val = ipv6Bytes[i*2] << 8 | ipv6Bytes[i*2+1]; + ipv6.push(uint16_val.toString(16)); } addressValue = ipv6.join(':'); // seems no need add [] for ipv6 From d0ce27a4365df7d26c60900f3a5fd07fac714be0 Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Tue, 11 Jul 2023 03:41:39 +1000 Subject: [PATCH 26/44] Fix DNS over TCP --- src/worker-with-socks5-experimental.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js index 256ec25680..719d7a8ed3 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-with-socks5-experimental.js @@ -457,6 +457,17 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { async function handleOutBound(remoteSocket, isUDP, addressType, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log,) { let curOutBoundPtr = {index: 0, serverIndex: 0}; + // Check if we should forward UDP DNS requests to a designated TCP DNS server. + // The vless packing of UDP datagrams is identical to the one used in TCP DNS protocol, + // so we can directly send raw vless traffic to the TCP DNS server. + // TCP DNS requests will not be touched. + // If fail to directly reach the TCP DNS server, UDP DNS request will be attempted on the other outbounds + const forwardDNS = isUDP && (portRemote == 53) && (globalConfig.dnsTCPServer ? true : false); + + // True if we absolutely need UDP outbound, fail otherwise + // False if we may use TCP to somehow resolve that UDP query + const enforceUDP = isUDP && !forwardDNS; + /** * @param {WritableStream} writableStream * @param {Uint8Array} firstChunk @@ -469,12 +480,7 @@ async function handleOutBound(remoteSocket, isUDP, addressType, addressRemote, p } async function direct() { - // Check if we should forward UDP DNS requests to a designated TCP DNS server. - // The vless packing of UDP datagrams is identical to the one used in TCP DNS protocol, - // so we can directly send raw vless traffic to the TCP DNS server. - const forwardDNS = isUDP && (portRemote == 53) && (globalConfig.dnsTCPServer ? true : false); - - if (isUDP && !forwardDNS) { + if (enforceUDP) { // TODO: Check what will happen if addressType == VlessAddrType.DomainName and that domain only resolves to a IPv6 const udpClient = await platformAPI.associate(addressType == VlessAddrType.IPv6); const writableStream = makeWritableUDPStream(udpClient, addressRemote, portRemote, log); @@ -624,7 +630,7 @@ async function handleOutBound(remoteSocket, isUDP, addressType, addressRemote, p log(`Trying outbound ${curOutBoundPtr.index}:${curOutBoundPtr.serverIndex}`); } - if (isUDP && !canOutboundUDPVia(outbound.protocol)) { + if (enforceUDP && !canOutboundUDPVia(outbound.protocol)) { // This outbound method does not support UDP return null; } From a88b9fb65db23ea51941f2be1d820e32f323fdb7 Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Sun, 16 Jul 2023 22:35:21 +1000 Subject: [PATCH 27/44] Use Uint8Array throughout the code --- node/index.js | 4 +- src/worker-with-socks5-experimental.js | 51 +++++++++++--------------- 2 files changed, 23 insertions(+), 32 deletions(-) diff --git a/node/index.js b/node/index.js index 7f7444a966..16263ee93a 100644 --- a/node/index.js +++ b/node/index.js @@ -47,12 +47,10 @@ function buf2hex(buffer) { // buffer is an ArrayBuffer * * @param {string} address The remote address to connect to. * @param {number} port The remote port to connect to. - * @param {boolean} useTLS * @returns {object} The wrapped TCP connection, to be compatible with Cloudflare Workers */ -platformAPI.connect = async (address, port, useTLS) => { +platformAPI.connect = async (address, port) => { const socket = net.createConnection(port, address); - // TODO: Implement TLS support? let readableStreamCancel = false; const readableStream = new ReadableStream({ diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js index 719d7a8ed3..9a1cdb6f11 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-with-socks5-experimental.js @@ -27,7 +27,7 @@ export let platformAPI = { * A wrapper for the TCP API, should return a Cloudflare Worker compatible socket. * The result is wrapped in a Promise, as in some platforms, the socket creation is async. * See: https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/ - * @type {(host: string, port: number, useTLS: boolean) => Promise< + * @type {(host: string, port: number) => Promise< * { * readable: ReadableStream, * writable: {getWriter: () => {write: (data) => void, releaseLock: () => void}}, @@ -355,8 +355,8 @@ export function redirectConsoleLog(logServer, instanceId) { try { const module = await import('cloudflare:sockets'); - platformAPI.connect = async (address, port, useTLS) => { - return module.connect({hostname: address, port: port}, {secureTransport: useTLS ? 'on' : 'off'}); + platformAPI.connect = async (address, port) => { + return module.connect({hostname: address, port: port}); }; platformAPI.newWebSocket = (url) => new WebSocket(url); @@ -496,7 +496,7 @@ async function handleOutBound(remoteSocket, isUDP, addressType, addressRemote, p log(`Redirect DNS request sent to UDP://${addressRemote}:${portRemote}`); } - const tcpSocket = await platformAPI.connect(addressTCP, portRemote, false); + const tcpSocket = await platformAPI.connect(addressTCP, portRemote); tcpSocket.closed.catch(error => log('[freedom] tcpSocket closed with error: ', error.message)); log(`Connecting to tcp://${addressTCP}:${portRemote}`); writeFirstChunk(tcpSocket.writable, rawClientData); @@ -509,7 +509,7 @@ async function handleOutBound(remoteSocket, isUDP, addressType, addressRemote, p portDest = portMap[portRemote]; } - const tcpSocket = await platformAPI.connect(proxyServer, portDest, false); + const tcpSocket = await platformAPI.connect(proxyServer, portDest); tcpSocket.closed.catch(error => log('[forward] tcpSocket closed with error: ', error.message)); log(`Forwarding tcp://${addressRemote}:${portRemote} to ${proxyServer}:${portDest}`); writeFirstChunk(tcpSocket.writable, rawClientData); @@ -519,7 +519,7 @@ async function handleOutBound(remoteSocket, isUDP, addressType, addressRemote, p // TODO: known problem, if we send an unreachable request to a valid socks5 server, it will wait indefinitely // TODO: Add support for proxying UDP via socks5 on runtimes that support UDP outbound async function socks5(address, port, user, pass) { - const tcpSocket = await platformAPI.connect(address, port, false); + const tcpSocket = await platformAPI.connect(address, port); tcpSocket.closed.catch(error => log('[socks] tcpSocket closed with error: ', error.message)); log(`Connecting to ${isUDP ? 'UDP' : 'TCP'}://${addressRemote}:${portRemote} via socks5 ${address}:${port}`); try { @@ -777,6 +777,7 @@ function safeCloseUDP(socket) { * @param {(firstChunk : Uint8Array) => Uint8Array} headStripper In some protocol like Vless, * a header is prepended to the first data chunk, it is necessary to strip that header. * @param {(info: string)=> void} log + * @returns {ReadableStream} a source of Uint8Array chunks */ function makeReadableWebSocketStream(webSocketServer, earlyData, headStripper, log) { let readableStreamCancel = false; @@ -792,15 +793,16 @@ function makeReadableWebSocketStream(webSocketServer, earlyData, headStripper, l return; } - let message = event.data; + // Make sure that we use Uint8Array through out the process. + // On Nodejs, event.data can be a Buffer or an ArrayBuffer + // On Cloudflare Workers, event.data tend to be an ArrayBuffer + let message = new Uint8Array(event.data); if (!headStripped) { headStripped = true; if (headStripper != null) { try { - // We have to make sure that we are on a Uint8Array. - const firstChunk = new Uint8Array(message); - message = headStripper(firstChunk); + message = headStripper(message); } catch (err) { readableStreamCancel = true; controller.error(err); @@ -857,7 +859,7 @@ function makeReadableWebSocketStream(webSocketServer, earlyData, headStripper, l /** * - * @param { ArrayBuffer} vlessBuffer + * @param { Uint8Array } vlessBuffer * @param {string} userID the expected userID * @returns */ @@ -871,10 +873,10 @@ function processVlessHeader( message: 'invalid data', }; } - const version = new Uint8Array(vlessBuffer.slice(0, 1)); + const version = vlessBuffer.slice(0, 1); let isValidUser = false; let isUDP = false; - if (stringify(new Uint8Array(vlessBuffer.slice(1, 17))) === userID) { + if (stringify(vlessBuffer.slice(1, 17)) === userID) { isValidUser = true; } if (!isValidUser) { @@ -884,12 +886,10 @@ function processVlessHeader( }; } - const optLength = new Uint8Array(vlessBuffer.slice(17, 18))[0]; //skip opt for now + const optLength = vlessBuffer.slice(17, 18)[0]; - const command = new Uint8Array( - vlessBuffer.slice(18 + optLength, 18 + optLength + 1) - )[0]; + const command = vlessBuffer.slice(18 + optLength, 18 + optLength + 1)[0]; if (command === VlessCmd.TCP) { } else if (command === VlessCmd.UDP) { @@ -901,14 +901,11 @@ function processVlessHeader( }; } const portIndex = 18 + optLength + 1; - const portBuffer = new Uint8Array(vlessBuffer.slice(portIndex, portIndex + 2)); // port is big-Endian in raw data etc 80 == 0x0050 - const portRemote = new DataView(portBuffer.buffer).getUint16(0); + const portRemote = (vlessBuffer[portIndex] << 8) | vlessBuffer[portIndex + 1]; let addressIndex = portIndex + 2; - const addressBuffer = new Uint8Array( - vlessBuffer.slice(addressIndex, addressIndex + 1) - ); + const addressBuffer = vlessBuffer.slice(addressIndex, addressIndex + 1); const addressType = addressBuffer[0]; let addressLength = 0; @@ -917,14 +914,10 @@ function processVlessHeader( switch (addressType) { case VlessAddrType.IPv4: addressLength = 4; - addressValue = new Uint8Array( - vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) - ).join('.'); + addressValue = vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength).join('.'); break; case VlessAddrType.DomainName: - addressLength = new Uint8Array( - vlessBuffer.slice(addressValueIndex, addressValueIndex + 1) - )[0]; + addressLength = vlessBuffer.slice(addressValueIndex, addressValueIndex + 1)[0]; addressValueIndex += 1; addressValue = new TextDecoder().decode( vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) @@ -932,7 +925,7 @@ function processVlessHeader( break; case VlessAddrType.IPv6: addressLength = 16; - const ipv6Bytes = (new Uint8Array(vlessBuffer)).slice(addressValueIndex, addressValueIndex + addressLength); + const ipv6Bytes = vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength); // 2001:0db8:85a3:0000:0000:8a2e:0370:7334 const ipv6 = []; for (let i = 0; i < 8; i++) { From 2dbb1cf2e33e0fc68c585c443dbbe36496659efb Mon Sep 17 00:00:00 2001 From: rikkagcp1 <131783080+rikkagcp1@users.noreply.github.com> Date: Mon, 17 Jul 2023 07:15:45 +1000 Subject: [PATCH 28/44] Fix earlydata --- src/worker-with-socks5-experimental.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js index 9a1cdb6f11..7a66f0a5ea 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-with-socks5-experimental.js @@ -1043,8 +1043,8 @@ function base64ToArrayBuffer(base64Str) { // go use modified Base64 for URL rfc4648 which js atob not support base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/'); const decode = atob(base64Str); - const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0)); - return { earlyData: arryBuffer.buffer, error: null }; + const buffer = Uint8Array.from(decode, (c) => c.charCodeAt(0)); + return { earlyData: buffer, error: null }; } catch (error) { return { error }; } From a63c7d3d3fdf708d58f410b77c9e6575d6daf35b Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Tue, 18 Jul 2023 01:54:49 +1000 Subject: [PATCH 29/44] Remove the use of Blob --- src/worker-with-socks5-experimental.js | 27 +++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js index 7a66f0a5ea..efdb211e27 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-with-socks5-experimental.js @@ -616,7 +616,7 @@ async function handleOutBound(remoteSocket, isUDP, addressType, addressRemote, p const readableStream = makeReadableWebSocketStream(wsToVlessServer, null, headerStripper, log); const vlessReqHeader = makeVlessReqHeader(isUDP ? VlessCmd.UDP : VlessCmd.TCP, addressType, addressRemote, portRemote, uuid, rawClientData); // Send the first packet (header + rawClientData), then strip the response header with headerStripper - writeFirstChunk(writableStream, await new Blob([vlessReqHeader, rawClientData]).arrayBuffer()); + writeFirstChunk(writableStream, joinUint8Array(vlessReqHeader, rawClientData)); return readableStream; } @@ -682,9 +682,9 @@ function makeReadableUDPStream(udpClient, log) { udpClient.onmessage((message, info) => { // log(`Received ${info.size} bytes from UDP://${info.address}:${info.port}`) // Prepend length to each UDP datagram - new Blob([new Uint8Array([(info.size >> 8) & 0xff, info.size & 0xff]), message]).arrayBuffer().then(encodedChunk => { - controller.enqueue(encodedChunk); - }); + const header = new Uint8Array([(info.size >> 8) & 0xff, info.size & 0xff]); + const encodedChunk = joinUint8Array(header, message); + controller.enqueue(encodedChunk); }); udpClient.onerror((error) => { log('UDP Error: ', error); @@ -964,7 +964,7 @@ function processVlessHeader( * Stream data from the remote destination (any) to the client side (Websocket) * @param {ReadableStream} remoteSocketReader from the remote destination * @param {import("@cloudflare/workers-types").WebSocket} webSocket to the client side - * @param {ArrayBuffer} vlessResponseHeader header that should be send to the client side + * @param {Uint8Array} vlessResponseHeader header that should be send to the client side * @param {(() => Promise) | null} retry * @param {*} log */ @@ -972,7 +972,7 @@ async function remoteSocketToWS(remoteSocketReader, webSocket, vlessResponseHead // remote--> ws let remoteChunkCount = 0; let chunks = []; - /** @type {ArrayBuffer | null} */ + /** @type {Uint8Array | null} */ let vlessHeader = vlessResponseHeader; let hasIncomingData = false; // check if remoteSocket has incoming data await remoteSocketReader.pipeTo( @@ -993,7 +993,7 @@ async function remoteSocketToWS(remoteSocketReader, webSocket, vlessResponseHead ); } if (vlessHeader) { - webSocket.send(await new Blob([vlessHeader, chunk]).arrayBuffer()); + webSocket.send(joinUint8Array(vlessHeader, chunk)); vlessHeader = null; } else { // seems no need rate limit this, CF seems fix this??.. @@ -1075,6 +1075,19 @@ function safeCloseWebSocket(socket) { } } +/** + * + * @param {Uint8Array} array1 + * @param {Uint8Array} array2 + * @returns {Uint8Array} the merged Uint8Array + */ +export function joinUint8Array(array1, array2) { + let result = new Uint8Array(array1.byteLength + array2.byteLength); + result.set(array1); + result.set(array2, array1.byteLength); + return result; +} + const byteToHex = []; for (let i = 0; i < 256; ++i) { byteToHex.push((i + 256).toString(16).slice(1)); From 217ddb3bb3de41800a159d41ca422882005c880d Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Tue, 25 Jul 2023 16:28:07 +1000 Subject: [PATCH 30/44] better shadowrocket compatiblity --- src/worker-with-socks5-experimental.js | 153 ++++++++++++++----------- 1 file changed, 89 insertions(+), 64 deletions(-) diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js index efdb211e27..ae55ef12dc 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-with-socks5-experimental.js @@ -387,45 +387,69 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyData, null, log); /** @type {{ writableStream: WritableStream | null}}*/ - let remoteSocketWapper = { + let toRemoteWrapper = { writableStream: null, }; + let vlessHeader = null; + + // This source stream only contains raw traffic from the client + // The vless header is stripped and parsed first. + const fromClientTraffic = readableWebSocketStream.pipeThrough(new TransformStream({ + start() { + }, + transform(chunk, controller) { + if (vlessHeader) { + controller.enqueue(chunk); + } else { + vlessHeader = processVlessHeader(chunk, globalConfig.userID); + if (vlessHeader.hasError) { + controller.error(`Failed to process Vless header: ${vlessHeader.message}`); + controller.terminate(); + return; + } + const randTag = Math.round(Math.random()*1000000).toString(16).padStart(5, '0'); + logPrefix = `${vlessHeader.addressRemote}:${vlessHeader.portRemote} ${randTag} ${vlessHeader.isUDP ? 'UDP' : 'TCP'}`; + const firstPayloadLen = chunk.byteLength - vlessHeader.rawDataIndex; + log(`First payload length = ${firstPayloadLen}`); + if (firstPayloadLen > 0) { + controller.enqueue(chunk.slice(vlessHeader.rawDataIndex)); + } + } + }, + flush(controller){ + } + })); + + let outboundEstablished = false; // ws --> remote - readableWebSocketStream.pipeTo(new WritableStream({ + fromClientTraffic.pipeTo(new WritableStream({ async write(chunk, controller) { - if (remoteSocketWapper.writableStream) { + // log(`outboundEstablished: ${outboundEstablished}`) + if (outboundEstablished) { // After we parse the header and send the first chunk to the remote destination // We assume that after the handshake, the stream only contains the original traffic. - const writer = remoteSocketWapper.writableStream.getWriter(); + // log('Send traffic from vless client to remote host'); + const writer = toRemoteWrapper.writableStream.getWriter(); + await writer.ready; await writer.write(chunk); writer.releaseLock(); return; } - const { - hasError, - message, addressType, - portRemote = 443, - addressRemote = '', - rawDataIndex, - vlessVersion = new Uint8Array([0, 0]), + addressRemote, + portRemote, + vlessVersion, isUDP, - } = processVlessHeader(chunk, globalConfig.userID); - const randTag = Math.round(Math.random()*1000000).toString(16).padStart(5, '0'); - logPrefix = `${addressRemote}:${portRemote} ${randTag} ${isUDP ? 'UDP' : 'TCP'}`; - if (hasError) { - // controller.error(message); - throw new Error(message); // cf seems has bug, controller.error will not end stream - // webSocket.close(1000, message); - } - + } = vlessHeader; // ["version", "length of additional info"] const vlessResponseHeader = new Uint8Array([vlessVersion[0], 0]); - const rawClientData = chunk.slice(rawDataIndex); - - handleOutBound(remoteSocketWapper, isUDP, addressType, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log); + + // Need to ensure the outbound proxy (if any) is ready before proceeding. + await handleOutBound(toRemoteWrapper, isUDP, addressType, addressRemote, portRemote, chunk, webSocket, vlessResponseHeader, log); + outboundEstablished = true; + // log('Outbound established!'); }, close() { log(`readableWebSocketStream has been closed`); @@ -443,7 +467,7 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { /** * Handles outbound connections. * - * @param {{ writableStream: WritableStream | null}} remoteSocket + * @param {{writableStream: WritableStream | null}} toRemoteWrapper * @param {boolean} isUDP * @param {number} addressType The remote address type to connect to. * @param {string} addressRemote The remote address to connect to. @@ -452,11 +476,11 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to pass the remote socket to. * @param {Uint8Array} vlessResponseHeader The VLESS response header. * @param {function} log The logging function. - * @returns {Promise} The remote socket. + * @returns {Promise} a fulfill indicates the success connection to the destination or the remote proxy server */ -async function handleOutBound(remoteSocket, isUDP, addressType, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log,) { +async function handleOutBound(toRemoteWrapper, isUDP, addressType, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log,) { let curOutBoundPtr = {index: 0, serverIndex: 0}; - + // Check if we should forward UDP DNS requests to a designated TCP DNS server. // The vless packing of UDP datagrams is identical to the one used in TCP DNS protocol, // so we can directly send raw vless traffic to the TCP DNS server. @@ -473,10 +497,10 @@ async function handleOutBound(remoteSocket, isUDP, addressType, addressRemote, p * @param {Uint8Array} firstChunk */ async function writeFirstChunk(writableStream, firstChunk) { - remoteSocket.writableStream = writableStream; const writer = writableStream.getWriter(); await writer.write(firstChunk); // First write, normally is tls client hello writer.releaseLock(); + toRemoteWrapper.writableStream = writableStream; } async function direct() { @@ -486,7 +510,7 @@ async function handleOutBound(remoteSocket, isUDP, addressType, addressRemote, p const writableStream = makeWritableUDPStream(udpClient, addressRemote, portRemote, log); const readableStream = makeReadableUDPStream(udpClient, log); log(`Connected to UDP://${addressRemote}:${portRemote}`); - writeFirstChunk(writableStream, rawClientData); + await writeFirstChunk(writableStream, rawClientData); return readableStream; } @@ -499,7 +523,7 @@ async function handleOutBound(remoteSocket, isUDP, addressType, addressRemote, p const tcpSocket = await platformAPI.connect(addressTCP, portRemote); tcpSocket.closed.catch(error => log('[freedom] tcpSocket closed with error: ', error.message)); log(`Connecting to tcp://${addressTCP}:${portRemote}`); - writeFirstChunk(tcpSocket.writable, rawClientData); + await writeFirstChunk(tcpSocket.writable, rawClientData); return tcpSocket.readable; } @@ -512,7 +536,7 @@ async function handleOutBound(remoteSocket, isUDP, addressType, addressRemote, p const tcpSocket = await platformAPI.connect(proxyServer, portDest); tcpSocket.closed.catch(error => log('[forward] tcpSocket closed with error: ', error.message)); log(`Forwarding tcp://${addressRemote}:${portRemote} to ${proxyServer}:${portDest}`); - writeFirstChunk(tcpSocket.writable, rawClientData); + await writeFirstChunk(tcpSocket.writable, rawClientData); return tcpSocket.readable; } @@ -528,7 +552,7 @@ async function handleOutBound(remoteSocket, isUDP, addressType, addressRemote, p log(`Socks5 outbound failed with: ${err.message}`); return null; } - writeFirstChunk(tcpSocket.writable, rawClientData); + await writeFirstChunk(tcpSocket.writable, rawClientData); return tcpSocket.readable; } @@ -616,7 +640,7 @@ async function handleOutBound(remoteSocket, isUDP, addressType, addressRemote, p const readableStream = makeReadableWebSocketStream(wsToVlessServer, null, headerStripper, log); const vlessReqHeader = makeVlessReqHeader(isUDP ? VlessCmd.UDP : VlessCmd.TCP, addressType, addressRemote, portRemote, uuid, rawClientData); // Send the first packet (header + rawClientData), then strip the response header with headerStripper - writeFirstChunk(writableStream, joinUint8Array(vlessReqHeader, rawClientData)); + await writeFirstChunk(writableStream, joinUint8Array(vlessReqHeader, rawClientData)); return readableStream; } @@ -650,22 +674,25 @@ async function handleOutBound(remoteSocket, isUDP, addressType, addressRemote, p } // Try each outbound method until we find a working one. - async function tryOutbound() { - let outboundReadableStream = await connectAndWrite(); - - while (outboundReadableStream == null && curOutBoundPtr.index < globalConfig.outbounds.length) { + let outboundReadableStream = null; + while (curOutBoundPtr.index < globalConfig.outbounds.length) { + if (outboundReadableStream == null) { outboundReadableStream = await connectAndWrite(); - } + } + + if (outboundReadableStream != null) { + const hasIncomingData = await remoteSocketToWS(outboundReadableStream, webSocket, vlessResponseHeader, log); + if (hasIncomingData) { + return; + } - if (outboundReadableStream == null) { - log('No more available outbound chain, abort!'); - safeCloseWebSocket(webSocket); - } else { - remoteSocketToWS(outboundReadableStream, webSocket, vlessResponseHeader, tryOutbound, log); + // This outbound connects but does not work + outboundReadableStream = null; } } - await tryOutbound(); + log('No more available outbound chain, abort!'); + safeCloseWebSocket(webSocket); } /** @@ -959,23 +986,23 @@ function processVlessHeader( }; } - /** * Stream data from the remote destination (any) to the client side (Websocket) * @param {ReadableStream} remoteSocketReader from the remote destination * @param {import("@cloudflare/workers-types").WebSocket} webSocket to the client side * @param {Uint8Array} vlessResponseHeader header that should be send to the client side - * @param {(() => Promise) | null} retry * @param {*} log + * @returns {Promise} has hasIncomingData */ -async function remoteSocketToWS(remoteSocketReader, webSocket, vlessResponseHeader, retry, log) { - // remote--> ws - let remoteChunkCount = 0; - let chunks = []; - /** @type {Uint8Array | null} */ - let vlessHeader = vlessResponseHeader; - let hasIncomingData = false; // check if remoteSocket has incoming data - await remoteSocketReader.pipeTo( +async function remoteSocketToWS(remoteSocketReader, webSocket, vlessResponseHeader, log) { + // This promise fulfills if: + // 1. There is any incoming data + // 2. The remoteSocketReader closes without any data + const toRemotePromise = new Promise((resolve) => { + let headerSent = false; + let hasIncomingData = false; + + remoteSocketReader.pipeTo( new WritableStream({ start() { }, @@ -986,15 +1013,17 @@ async function remoteSocketToWS(remoteSocketReader, webSocket, vlessResponseHead */ async write(chunk, controller) { hasIncomingData = true; + resolve(true); + // remoteChunkCount++; if (webSocket.readyState !== WS_READY_STATE_OPEN) { controller.error( 'webSocket.readyState is not open, maybe close' ); } - if (vlessHeader) { - webSocket.send(joinUint8Array(vlessHeader, chunk)); - vlessHeader = null; + if (!headerSent) { + webSocket.send(joinUint8Array(vlessResponseHeader, chunk)); + headerSent = true; } else { // seems no need rate limit this, CF seems fix this??.. // if (remoteChunkCount > 20000) { @@ -1006,6 +1035,7 @@ async function remoteSocketToWS(remoteSocketReader, webSocket, vlessResponseHead }, close() { log(`remoteSocket.readable is close, hasIncomingData = ${hasIncomingData}`); + resolve(hasIncomingData); // safeCloseWebSocket(webSocket); // no need server close websocket frist for some case will casue HTTP ERR_CONTENT_LENGTH_MISMATCH issue, client will send close event anyway. }, // abort(reason) { @@ -1020,14 +1050,9 @@ async function remoteSocketToWS(remoteSocketReader, webSocket, vlessResponseHead ); safeCloseWebSocket(webSocket); }); + }); - // seems is cf connect socket have error, - // 1. Socket.closed will have error - // 2. Socket.readable will be close without any data coming - if (hasIncomingData === false && retry) { - log(`No incoming data from the remote host, retry`); - retry(); - } + return await toRemotePromise; } /** From ad473ba1ec9017c84d9173190d660d6de98a5546 Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Wed, 26 Jul 2023 04:33:14 +1000 Subject: [PATCH 31/44] Code clean-up --- src/worker-with-socks5-experimental.js | 117 +++++++++++++------------ 1 file changed, 59 insertions(+), 58 deletions(-) diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js index ae55ef12dc..cc9d3b2855 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-with-socks5-experimental.js @@ -386,11 +386,6 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyData, null, log); - /** @type {{ writableStream: WritableStream | null}}*/ - let toRemoteWrapper = { - writableStream: null, - }; - let vlessHeader = null; // This source stream only contains raw traffic from the client @@ -421,34 +416,29 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { } })); - let outboundEstablished = false; + /** @type {WritableStream | null}*/ + let remoteTrafficSink = null; + // ws --> remote fromClientTraffic.pipeTo(new WritableStream({ async write(chunk, controller) { - // log(`outboundEstablished: ${outboundEstablished}`) - if (outboundEstablished) { + log(`remoteTrafficSink: ${remoteTrafficSink == null ? 'null' : 'ready'}`); + if (remoteTrafficSink) { // After we parse the header and send the first chunk to the remote destination // We assume that after the handshake, the stream only contains the original traffic. // log('Send traffic from vless client to remote host'); - const writer = toRemoteWrapper.writableStream.getWriter(); + const writer = remoteTrafficSink.getWriter(); await writer.ready; await writer.write(chunk); writer.releaseLock(); return; } - const { - addressType, - addressRemote, - portRemote, - vlessVersion, - isUDP, - } = vlessHeader; + // ["version", "length of additional info"] - const vlessResponseHeader = new Uint8Array([vlessVersion[0], 0]); + const vlessResponseHeader = new Uint8Array([vlessHeader.vlessVersion[0], 0]); // Need to ensure the outbound proxy (if any) is ready before proceeding. - await handleOutBound(toRemoteWrapper, isUDP, addressType, addressRemote, portRemote, chunk, webSocket, vlessResponseHeader, log); - outboundEstablished = true; + remoteTrafficSink = await handleOutBound(vlessHeader, chunk, webSocket, vlessResponseHeader, log); // log('Outbound established!'); }, close() { @@ -466,19 +456,14 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { /** * Handles outbound connections. - * - * @param {{writableStream: WritableStream | null}} toRemoteWrapper - * @param {boolean} isUDP - * @param {number} addressType The remote address type to connect to. - * @param {string} addressRemote The remote address to connect to. - * @param {number} portRemote The remote port to connect to. + * @param {{isUDP: boolean, addressType: number, addressRemote: string, portRemote: number}} vlessRequest * @param {Uint8Array} rawClientData The raw client data to write. * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to pass the remote socket to. * @param {Uint8Array} vlessResponseHeader The VLESS response header. * @param {function} log The logging function. - * @returns {Promise} a fulfill indicates the success connection to the destination or the remote proxy server + * @returns {Promise} a non-null fulfill indicates the success connection to the destination or the remote proxy server */ -async function handleOutBound(toRemoteWrapper, isUDP, addressType, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log,) { +async function handleOutBound(vlessRequest, rawClientData, webSocket, vlessResponseHeader, log) { let curOutBoundPtr = {index: 0, serverIndex: 0}; // Check if we should forward UDP DNS requests to a designated TCP DNS server. @@ -486,11 +471,11 @@ async function handleOutBound(toRemoteWrapper, isUDP, addressType, addressRemote // so we can directly send raw vless traffic to the TCP DNS server. // TCP DNS requests will not be touched. // If fail to directly reach the TCP DNS server, UDP DNS request will be attempted on the other outbounds - const forwardDNS = isUDP && (portRemote == 53) && (globalConfig.dnsTCPServer ? true : false); + const forwardDNS = vlessRequest.isUDP && (vlessRequest.portRemote == 53) && (globalConfig.dnsTCPServer ? true : false); // True if we absolutely need UDP outbound, fail otherwise // False if we may use TCP to somehow resolve that UDP query - const enforceUDP = isUDP && !forwardDNS; + const enforceUDP = vlessRequest.isUDP && !forwardDNS; /** * @param {WritableStream} writableStream @@ -500,44 +485,52 @@ async function handleOutBound(toRemoteWrapper, isUDP, addressType, addressRemote const writer = writableStream.getWriter(); await writer.write(firstChunk); // First write, normally is tls client hello writer.releaseLock(); - toRemoteWrapper.writableStream = writableStream; } async function direct() { if (enforceUDP) { // TODO: Check what will happen if addressType == VlessAddrType.DomainName and that domain only resolves to a IPv6 - const udpClient = await platformAPI.associate(addressType == VlessAddrType.IPv6); - const writableStream = makeWritableUDPStream(udpClient, addressRemote, portRemote, log); + const udpClient = await platformAPI.associate(vlessRequest.addressType == VlessAddrType.IPv6); + const writableStream = makeWritableUDPStream(udpClient, vlessRequest.addressRemote, vlessRequest.portRemote, log); const readableStream = makeReadableUDPStream(udpClient, log); - log(`Connected to UDP://${addressRemote}:${portRemote}`); + log(`Connected to UDP://${vlessRequest.addressRemote}:${vlessRequest.portRemote}`); await writeFirstChunk(writableStream, rawClientData); - return readableStream; + return { + readableStream, + writableStream + }; } - let addressTCP = addressRemote; + let addressTCP = vlessRequest.addressRemote; if (forwardDNS) { addressTCP = globalConfig.dnsTCPServer; - log(`Redirect DNS request sent to UDP://${addressRemote}:${portRemote}`); + log(`Redirect DNS request sent to UDP://${vlessRequest.addressRemote}:${vlessRequest.portRemote}`); } - const tcpSocket = await platformAPI.connect(addressTCP, portRemote); + const tcpSocket = await platformAPI.connect(addressTCP, vlessRequest.portRemote); tcpSocket.closed.catch(error => log('[freedom] tcpSocket closed with error: ', error.message)); - log(`Connecting to tcp://${addressTCP}:${portRemote}`); + log(`Connecting to tcp://${addressTCP}:${vlessRequest.portRemote}`); await writeFirstChunk(tcpSocket.writable, rawClientData); - return tcpSocket.readable; + return { + readableStream: tcpSocket.readable, + writableStream: tcpSocket.writable + }; } async function forward(proxyServer, portMap) { - let portDest = portRemote; - if (typeof portMap === "object" && portMap[portRemote] !== undefined) { - portDest = portMap[portRemote]; + let portDest = vlessRequest.portRemote; + if (typeof portMap === "object" && portMap[vlessRequest.portRemote] !== undefined) { + portDest = portMap[vlessRequest.portRemote]; } const tcpSocket = await platformAPI.connect(proxyServer, portDest); tcpSocket.closed.catch(error => log('[forward] tcpSocket closed with error: ', error.message)); - log(`Forwarding tcp://${addressRemote}:${portRemote} to ${proxyServer}:${portDest}`); + log(`Forwarding tcp://${vlessRequest.addressRemote}:${vlessRequest.portRemote} to ${proxyServer}:${portDest}`); await writeFirstChunk(tcpSocket.writable, rawClientData); - return tcpSocket.readable; + return { + readableStream: tcpSocket.readable, + writableStream: tcpSocket.writable + }; } // TODO: known problem, if we send an unreachable request to a valid socks5 server, it will wait indefinitely @@ -545,15 +538,18 @@ async function handleOutBound(toRemoteWrapper, isUDP, addressType, addressRemote async function socks5(address, port, user, pass) { const tcpSocket = await platformAPI.connect(address, port); tcpSocket.closed.catch(error => log('[socks] tcpSocket closed with error: ', error.message)); - log(`Connecting to ${isUDP ? 'UDP' : 'TCP'}://${addressRemote}:${portRemote} via socks5 ${address}:${port}`); + log(`Connecting to ${vlessRequest.isUDP ? 'UDP' : 'TCP'}://${vlessRequest.addressRemote}:${vlessRequest.portRemote} via socks5 ${address}:${port}`); try { - await socks5Connect(tcpSocket, user, pass, addressType, addressRemote, portRemote, log); + await socks5Connect(tcpSocket, user, pass, vlessRequest.addressType, vlessRequest.addressRemote, vlessRequest.portRemote, log); } catch(err) { log(`Socks5 outbound failed with: ${err.message}`); return null; } await writeFirstChunk(tcpSocket.writable, rawClientData); - return tcpSocket.readable; + return { + readableStream: tcpSocket.readable, + writableStream: tcpSocket.writable + }; } /** @@ -583,7 +579,7 @@ async function handleOutBound(toRemoteWrapper, isUDP, addressType, addressRemote if (streamSettings.wsSettings && streamSettings.wsSettings.path) { wsURL = wsURL + streamSettings.wsSettings.path; } - log(`Connecting to ${isUDP ? 'UDP' : 'TCP'}://${addressRemote}:${portRemote} via vless ${wsURL}`); + log(`Connecting to ${vlessRequest.isUDP ? 'UDP' : 'TCP'}://${vlessRequest.addressRemote}:${vlessRequest.portRemote} via vless ${wsURL}`); const wsToVlessServer = platformAPI.newWebSocket(wsURL); const openPromise = new Promise((resolve, reject) => { @@ -638,13 +634,16 @@ async function handleOutBound(toRemoteWrapper, isUDP, addressType, addressRemote }; const readableStream = makeReadableWebSocketStream(wsToVlessServer, null, headerStripper, log); - const vlessReqHeader = makeVlessReqHeader(isUDP ? VlessCmd.UDP : VlessCmd.TCP, addressType, addressRemote, portRemote, uuid, rawClientData); + const vlessReqHeader = makeVlessReqHeader(vlessRequest.isUDP ? VlessCmd.UDP : VlessCmd.TCP, vlessRequest.addressType, vlessRequest.addressRemote, vlessRequest.portRemote, uuid, rawClientData); // Send the first packet (header + rawClientData), then strip the response header with headerStripper await writeFirstChunk(writableStream, joinUint8Array(vlessReqHeader, rawClientData)); - return readableStream; + return { + readableStream, + writableStream + }; } - /** @returns {Promise} */ + /** @returns {Promise<{readableStream: ReadableStream, writableStream: WritableStream} | null>} */ async function connectAndWrite() { const outbound = getOutbound(curOutBoundPtr); if (outbound == null) { @@ -674,25 +673,27 @@ async function handleOutBound(toRemoteWrapper, isUDP, addressType, addressRemote } // Try each outbound method until we find a working one. - let outboundReadableStream = null; + /** @type {{readableStream: ReadableStream, writableStream: WritableStream} | null} */ + let destRWPair = null; while (curOutBoundPtr.index < globalConfig.outbounds.length) { - if (outboundReadableStream == null) { - outboundReadableStream = await connectAndWrite(); + if (destRWPair == null) { + destRWPair = await connectAndWrite(); } - if (outboundReadableStream != null) { - const hasIncomingData = await remoteSocketToWS(outboundReadableStream, webSocket, vlessResponseHeader, log); + if (destRWPair != null) { + const hasIncomingData = await remoteSocketToWS(destRWPair.readableStream, webSocket, vlessResponseHeader, log); if (hasIncomingData) { - return; + return destRWPair.writableStream; } // This outbound connects but does not work - outboundReadableStream = null; + destRWPair = null; } } log('No more available outbound chain, abort!'); safeCloseWebSocket(webSocket); + return null; } /** From d171ec930e9e6cd65e7a951150841d75394c34de Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Wed, 26 Jul 2023 05:08:57 +1000 Subject: [PATCH 32/44] More clean up --- src/worker-with-socks5-experimental.js | 92 ++++++++++++++------------ 1 file changed, 51 insertions(+), 41 deletions(-) diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js index cc9d3b2855..dc7e0b2519 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-with-socks5-experimental.js @@ -1003,47 +1003,57 @@ async function remoteSocketToWS(remoteSocketReader, webSocket, vlessResponseHead let headerSent = false; let hasIncomingData = false; - remoteSocketReader.pipeTo( - new WritableStream({ - start() { - }, - /** - * - * @param {Uint8Array} chunk - * @param {*} controller - */ - async write(chunk, controller) { - hasIncomingData = true; - resolve(true); - - // remoteChunkCount++; - if (webSocket.readyState !== WS_READY_STATE_OPEN) { - controller.error( - 'webSocket.readyState is not open, maybe close' - ); - } - if (!headerSent) { - webSocket.send(joinUint8Array(vlessResponseHeader, chunk)); - headerSent = true; - } else { - // seems no need rate limit this, CF seems fix this??.. - // if (remoteChunkCount > 20000) { - // // cf one package is 4096 byte(4kb), 4096 * 20000 = 80M - // await delay(1); - // } - webSocket.send(chunk); - } - }, - close() { - log(`remoteSocket.readable is close, hasIncomingData = ${hasIncomingData}`); - resolve(hasIncomingData); - // safeCloseWebSocket(webSocket); // no need server close websocket frist for some case will casue HTTP ERR_CONTENT_LENGTH_MISMATCH issue, client will send close event anyway. - }, - // abort(reason) { - // console.error(`remoteSocket.readable aborts`, reason); - // }, - }) - ) + // Add the response header and monitor if there is any traffic coming from the remote host. + remoteSocketReader.pipeThrough(new TransformStream({ + start() { + }, + transform(chunk, controller) { + // Resolve the promise immediately if we got any data from the remote host. + hasIncomingData = true; + resolve(true); + + if (!headerSent) { + controller.enqueue(joinUint8Array(vlessResponseHeader, chunk)); + headerSent = true; + } else { + controller.enqueue(chunk); + } + }, + flush(controller) { + log(`Response transformer flushed, hasIncomingData = ${hasIncomingData}`); + + // The connection has been closed, resolve the promise anyway. + resolve(hasIncomingData); + } + })) + .pipeTo(new WritableStream({ + start() { + }, + async write(chunk, controller) { + // remoteChunkCount++; + if (webSocket.readyState !== WS_READY_STATE_OPEN) { + controller.error( + 'webSocket.readyState is not open, maybe close' + ); + } + + // seems no need rate limit this, CF seems fix this??.. + // if (remoteChunkCount > 20000) { + // // cf one package is 4096 byte(4kb), 4096 * 20000 = 80M + // await delay(1); + // } + webSocket.send(chunk); + }, + close() { + // log(`remoteSocket.readable has been close`); + // The server dont need to close the websocket first, as it will cause ERR_CONTENT_LENGTH_MISMATCH + // The client will close the connection anyway. + // safeCloseWebSocket(webSocket); + }, + // abort(reason) { + // console.error(`remoteSocket.readable aborts`, reason); + // }, + })) .catch((error) => { console.error( `remoteSocketToWS has exception, readyState = ${webSocket.readyState} :`, From 1c64027549302a078c4a0ff84363761f67a3a01b Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Wed, 26 Jul 2023 05:14:29 +1000 Subject: [PATCH 33/44] Less verbose log --- src/worker-with-socks5-experimental.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js index dc7e0b2519..3d9fcd54d8 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-with-socks5-experimental.js @@ -422,7 +422,7 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { // ws --> remote fromClientTraffic.pipeTo(new WritableStream({ async write(chunk, controller) { - log(`remoteTrafficSink: ${remoteTrafficSink == null ? 'null' : 'ready'}`); + // log(`remoteTrafficSink: ${remoteTrafficSink == null ? 'null' : 'ready'}`); if (remoteTrafficSink) { // After we parse the header and send the first chunk to the remote destination // We assume that after the handshake, the stream only contains the original traffic. From a58b6b21016c7cc12d952da819779730d5d7aa44 Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Wed, 26 Jul 2023 05:58:24 +1000 Subject: [PATCH 34/44] More code clean up --- src/worker-with-socks5-experimental.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js index 3d9fcd54d8..71edce3213 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-with-socks5-experimental.js @@ -435,10 +435,12 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { } // ["version", "length of additional info"] - const vlessResponseHeader = new Uint8Array([vlessHeader.vlessVersion[0], 0]); + const vlessResponse = { + header: new Uint8Array([vlessHeader.vlessVersion[0], 0]), + } // Need to ensure the outbound proxy (if any) is ready before proceeding. - remoteTrafficSink = await handleOutBound(vlessHeader, chunk, webSocket, vlessResponseHeader, log); + remoteTrafficSink = await handleOutBound(vlessHeader, chunk, webSocket, vlessResponse, log); // log('Outbound established!'); }, close() { @@ -459,11 +461,11 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { * @param {{isUDP: boolean, addressType: number, addressRemote: string, portRemote: number}} vlessRequest * @param {Uint8Array} rawClientData The raw client data to write. * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to pass the remote socket to. - * @param {Uint8Array} vlessResponseHeader The VLESS response header. + * @param {{header: Uint8Array}} vlessResponse Contains information to produce the vless reponse, such as the header. * @param {function} log The logging function. * @returns {Promise} a non-null fulfill indicates the success connection to the destination or the remote proxy server */ -async function handleOutBound(vlessRequest, rawClientData, webSocket, vlessResponseHeader, log) { +async function handleOutBound(vlessRequest, rawClientData, webSocket, vlessResponse, log) { let curOutBoundPtr = {index: 0, serverIndex: 0}; // Check if we should forward UDP DNS requests to a designated TCP DNS server. @@ -681,7 +683,7 @@ async function handleOutBound(vlessRequest, rawClientData, webSocket, vlessRespo } if (destRWPair != null) { - const hasIncomingData = await remoteSocketToWS(destRWPair.readableStream, webSocket, vlessResponseHeader, log); + const hasIncomingData = await remoteSocketToWS(destRWPair.readableStream, webSocket, vlessResponse, log); if (hasIncomingData) { return destRWPair.writableStream; } @@ -991,11 +993,11 @@ function processVlessHeader( * Stream data from the remote destination (any) to the client side (Websocket) * @param {ReadableStream} remoteSocketReader from the remote destination * @param {import("@cloudflare/workers-types").WebSocket} webSocket to the client side - * @param {Uint8Array} vlessResponseHeader header that should be send to the client side + * @param {{header: Uint8Array}} vlessResponse Contains information to produce the vless reponse, such as the header. * @param {*} log * @returns {Promise} has hasIncomingData */ -async function remoteSocketToWS(remoteSocketReader, webSocket, vlessResponseHeader, log) { +async function remoteSocketToWS(remoteSocketReader, webSocket, vlessResponse, log) { // This promise fulfills if: // 1. There is any incoming data // 2. The remoteSocketReader closes without any data @@ -1013,7 +1015,7 @@ async function remoteSocketToWS(remoteSocketReader, webSocket, vlessResponseHead resolve(true); if (!headerSent) { - controller.enqueue(joinUint8Array(vlessResponseHeader, chunk)); + controller.enqueue(joinUint8Array(vlessResponse.header, chunk)); headerSent = true; } else { controller.enqueue(chunk); From ead7a93bddf673bf2a4fb5dfc99a2b4711dfc4b9 Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Sat, 2 Dec 2023 02:28:11 +1100 Subject: [PATCH 35/44] Massive code clean-up, add type annotations. --- ...h-socks5-experimental.js => worker-neo.js} | 482 +++++++++++++----- 1 file changed, 347 insertions(+), 135 deletions(-) rename src/{worker-with-socks5-experimental.js => worker-neo.js} (79%) diff --git a/src/worker-with-socks5-experimental.js b/src/worker-neo.js similarity index 79% rename from src/worker-with-socks5-experimental.js rename to src/worker-neo.js index 71edce3213..3674aaa5da 100644 --- a/src/worker-with-socks5-experimental.js +++ b/src/worker-neo.js @@ -3,17 +3,25 @@ // How to generate your own UUID: // [Windows] Press "Win + R", input cmd and run: Powershell -NoExit -Command "[guid]::NewGuid()" // [Linux] Run uuidgen in terminal -export let globalConfig = { +export const globalConfig = { userID: 'd342d11e-d424-4583-b36e-524ab1f0afa4', - // Time to wait before an outbound Websocket connection is established, in ms. + /** + * Time to wait before an outbound Websocket connection is considered timeout, in ms. + * */ openWSOutboundTimeout: 10000, - // Since Cloudflare Worker does not support UDP outbound, we may try DNS over TCP. - // Set to an empty string to disable UDP to TCP forwarding for DNS queries. + /** + * Since Cloudflare Worker does not support UDP outbound, we may try DNS over TCP. + * Set to an empty string to disable UDP to TCP forwarding for DNS queries. + * @type {string} + */ dnsTCPServer: "8.8.4.4", - // The order controls where to send the traffic after the previous one fails + /** + * The order controls where to send the traffic after the previous one fails. + * @type {Outbound[]} + */ outbounds: [ { protocol: "freedom" // Compulsory, outbound locally. @@ -21,48 +29,162 @@ export let globalConfig = { ] }; +/** + * Defines a Cloudflare Worker compatible TCP connection. + * @typedef {Object} CloudflareTCPConnection + * @property {ReadableStream} readable the readable side of the TCP socket. + * @property {WritableStream} writable the writable side of the TCP socket, accepts Uint8Array only. + * @property {Promise} closed - This promise is resolved when the socket is closed and is rejected if the socket encounters an error. + */ + +/** + * @typedef {Object} NodeJSUDPRemoteInfo + * @property {string} address + * @property {'IPv4' | 'IPv6'} family + * @property {number} port + * @property {number} size + */ + +/** + * Defines a NodeJS compatible UDP API. + * @typedef {Object} NodeJSUDP + * @property {(datagram: any, offset: number, length: number, port: number, address: string, sendDoneCallback: (err: Error | null, bytes: number) => void) => void} send + * @property {() => void} close + * @property {(handler: (msg: Uint8Array, rinfo: NodeJSUDPRemoteInfo) => void) => void} onmessage + * @property {(handler: (err: Error) => void) => void} onerror + */ + // If you use this file as an ES module, you should set all fields below. -export let platformAPI = { +export const platformAPI = { /** - * A wrapper for the TCP API, should return a Cloudflare Worker compatible socket. - * The result is wrapped in a Promise, as in some platforms, the socket creation is async. - * See: https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/ - * @type {(host: string, port: number) => Promise< - * { - * readable: ReadableStream, - * writable: {getWriter: () => {write: (data) => void, releaseLock: () => void}}, - * closed: {Promise} - * }> - * } - */ + * A wrapper for the TCP API, should return a Cloudflare Worker compatible socket. + * The result is wrapped in a Promise, as in some platforms, the socket creation is async. + * See: https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/ + * @type {(host: string, port: number) => Promise} + */ connect: null, /** - * A wrapper for the Websocket API. - * @type {(url: string) => WebSocket} returns a WebSocket, should be compatile with the standard WebSocket API. - */ + * A wrapper for the Websocket API. + * @type {(url: string) => WebSocket} returns a WebSocket, should be compatile with the standard WebSocket API. + */ newWebSocket: null, /** - * A wrapper for the UDP API, should return a NodeJS compatible UDP socket. - * The result is wrapped in a Promise, as in some platforms, the socket creation is async. - * @type {(isIPv6: boolean) => Promise< - * { - * send: (datagram: any, offset: number, length: number, port: number, address: string, sendDoneCallback: (err: Error | null, bytes: number) => void) => void, - * close: () => void, - * onmessage: (handler: (msg: Buffer, rinfo: RemoteInfo) => void) => void, - * onerror: (handler: (err: Error) => void) => void, - * }> - * } - */ + * A wrapper for the UDP API, should return a NodeJS compatible UDP socket. + * The result is wrapped in a Promise, as in some platforms, the socket creation is async. + * @type {(isIPv6: boolean) => Promise + * } + */ associate: null, } +/** + * The base type of all outbound definitions. + * @typedef {{ + * protocol: string, + * settings?: {} + * }} Outbound + */ + +/** + * Represents a local outbound. + * @typedef {Outbound & { + * protocol: 'freedom', + * settings: undefined, + * }} FreedomOutbound + */ + +/** + * @typedef {{[key: number]: number}} PortMap + */ + +/** + * Represents a forwarding outbound. + * First, the destination port of the request will be mapped according to portMap. + * If none matches, the destination port remains unchanged. + * Then, the request stream will be redirected to the given address. + * @typedef {Outbound & { + * protocol: 'forward', + * address: string, + * portMap?: PortMap + * }} ForwardOutbound + */ + +/** + * @typedef {{ + * address: string, + * port: number, + * users?: { + * user: string, + * pass: string, + * }[] + * }} Socks5Server + */ + +/** + * Represents a socks5 outbound. + * @typedef {Outbound & { + * protocol: 'socks', + * settings: { + * servers: Socks5Server[] + * } + * }} Socks5Outbound + */ + +/** + * @typedef {{ + * address: string, + * port: number, + * users?: { + * id: string, + * }[] + * }} VlessServer + */ + +/** + * Represents a Vless WebSocket outbound. + * @typedef {Outbound & { + * protocol: 'vless', + * settings: { + * vnext: VlessServer[] + * }, + * streamSettings: StreamSettings + * }} VlessWsOutbound + */ + +/** + * @typedef {Object} StreamSettings + * @property {'ws'} network + * @property {'none' | 'tls'} security + * @property {{ + * path?: string, + * headers ?: { + * Host: string + * } + * }} wsSettings + * @property {{ + * serverName: string, + * allowInsecure: boolean, + * }} [tlsSettings] + */ + +/** + * @typedef {{ + * protocol: string, + * address?: string, + * port?: number, + * portMap?: PortMap, + * user?: string, + * pass?: string, + * streamSettings?: StreamSettings, + * }} OutboundInstance + */ + /** * Foreach globalConfig.outbounds, start with {index: 0, serverIndex: 0} * @param {{index: number, serverIndex: number}} curPos - * @returns {{protocol: string, address: string, port: number, user: string, pass: string, - * portMap: {Number: number}}} + * @returns {OutboundInstance?} */ function getOutbound(curPos) { if (curPos.index >= globalConfig.outbounds.length) { @@ -70,46 +192,58 @@ function getOutbound(curPos) { return null; } - const outbound = globalConfig.outbounds.at(curPos.index); + const outbound = globalConfig.outbounds[curPos.index]; let serverCount = 0; - /** @type {[{}]} */ - let servers; - /** @type {{address: string, port: number}} */ - let curServer; - let retVal = { protocol: outbound.protocol }; + + /** @type {OutboundInstance} */ + const retVal = { protocol: outbound.protocol }; switch (outbound.protocol) { case 'freedom': break; - case 'forward': - retVal.address = outbound.address; - retVal.portMap = outbound.portMap; + case 'forward': { + /** @type {ForwardOutbound} */ + // @ts-ignore: type casting + const forwardOutbound = outbound; + retVal.address = forwardOutbound.address; + retVal.portMap = forwardOutbound.portMap; break; + } - case 'socks': - servers = outbound.settings.vnext; + case 'socks': { + /** @type {Socks5Outbound} */ + // @ts-ignore: type casting + const socks5Outbound = outbound; + const servers = socks5Outbound.settings.servers; serverCount = servers.length; - curServer = servers.at(curPos.serverIndex); + + const curServer = servers[curPos.serverIndex]; retVal.address = curServer.address; retVal.port = curServer.port; if (curServer.users && curServer.users.length > 0) { - const firstUser = curServer.users.at(0); + const firstUser = curServer.users[0]; retVal.user = firstUser.user; retVal.pass = firstUser.pass; } break; + } - case 'vless': - servers = outbound.settings.vnext; + case 'vless': { + /** @type {VlessWsOutbound} */ + // @ts-ignore: type casting + const vlessOutbound = outbound; + const servers = vlessOutbound.settings.vnext; serverCount = servers.length; - curServer = servers.at(curPos.serverIndex); + + const curServer = servers[curPos.serverIndex]; retVal.address = curServer.address; retVal.port = curServer.port; - retVal.pass = curServer.users.at(0).id; - retVal.streamSettings = outbound.streamSettings; + retVal.pass = curServer.users[0].id; + retVal.streamSettings = vlessOutbound.streamSettings; break; + } default: throw new Error(`Unknown outbound protocol: ${outbound.protocol}`); @@ -125,6 +259,10 @@ function getOutbound(curPos) { return retVal; } +/** + * @param {string} protocolName + * @returns {boolean} true if the given protocol supports UDP outbound. + */ function canOutboundUDPVia(protocolName) { switch(protocolName) { case 'freedom': @@ -139,11 +277,11 @@ function canOutboundUDPVia(protocolName) { * Setup the config (uuid & outbounds) from environmental variables. * This is the simplest case and should be preferred where possible. * @param {{ - * UUID: string, - * PROXYIP: string, // E.g. 114.51.4.0 - * PORTMAP: string, // E.g. {443:8443} - * VLESS: string, // E.g. vless://uuid@domain.name:port?type=ws&security=tls - * SOCKS5: string // E.g. user:pass@host:port or host:port + * UUID?: string, + * PROXYIP?: string, // E.g. 114.51.4.0 + * PORTMAP?: string, // E.g. {443:8443} + * VLESS?: string, // E.g. vless://uuid@domain.name:port?type=ws&security=tls + * SOCKS5?: string // E.g. user:pass@host:port or host:port * }} env */ export function setConfigFromEnv(env) { @@ -156,7 +294,8 @@ export function setConfigFromEnv(env) { ]; if (env.PROXYIP) { - let forward = { + /** @type {ForwardOutbound} */ + const forward = { protocol: "forward", address: env.PROXYIP }; @@ -181,7 +320,8 @@ export function setConfigFromEnv(env) { descriptiveText } = parseVlessString(env.VLESS); - let vless = { + /** @type {VlessServer} */ + const vless = { "address": remoteHost, "port": remotePort, "users": [ @@ -191,7 +331,9 @@ export function setConfigFromEnv(env) { ] }; - let streamSettings = { + // TODO: Validate vless here + /** @type {StreamSettings} */ + const streamSettings = { "network": queryParams['type'], "security": queryParams['security'], } @@ -212,16 +354,18 @@ export function setConfigFromEnv(env) { }; } - globalConfig['outbounds'].push({ + /** @type {VlessWsOutbound} */ + const vlessOutbound = { protocol: "vless", settings: { "vnext": [ vless ] }, streamSettings: streamSettings - }); + }; + globalConfig['outbounds'].push(vlessOutbound); } catch (err) { /** @type {Error} */ - let e = err; + const e = err; console.log(e.toString()); } } @@ -237,7 +381,8 @@ export function setConfigFromEnv(env) { port, } = socks5AddressParser(env.SOCKS5); - let socks = { + /** @type {Socks5Server} */ + const socks = { "address": hostname, "port": port } @@ -254,7 +399,7 @@ export function setConfigFromEnv(env) { globalConfig['outbounds'].push({ protocol: "socks", settings: { - "vnext": [ socks ] + "servers": [ socks ] } }); } catch (err) { @@ -268,9 +413,9 @@ export function setConfigFromEnv(env) { // Cloudflare Workers entry export default { /** - * @param {import("@cloudflare/workers-types").Request} request + * @param {Request} request * @param {{UUID: string, PROXYIP: string}} env - * @param {import("@cloudflare/workers-types").ExecutionContext} ctx + * @param {ExecutionContext} ctx * @returns {Promise} */ async fetch(request, env, ctx) { @@ -299,7 +444,7 @@ export default { return new Response('Not found', { status: 404 }); } } else { - /** @type {import("@cloudflare/workers-types").WebSocket[]} */ + /** @type {WebSocket[]} */ // @ts-ignore const webSocketPair = new WebSocketPair(); const [client, webSocket] = Object.values(webSocketPair); @@ -364,6 +509,10 @@ try { console.log('Not on Cloudflare Workers!'); } +/** + * @typedef {(info: string, event?: string) => void} LogFunction + */ + /** * If you use this file as an ES module, you call this function whenever your Websocket server accepts a new connection. * @@ -374,7 +523,8 @@ try { */ export function vlessOverWSHandler(webSocket, earlyDataHeader) { let logPrefix = ''; - const log = (/** @type {string} */ info, /** @type {string | undefined} */ event) => { + /** @type {LogFunction} */ + const log = (info, event) => { console.log(`[${logPrefix}] ${info}`, event || ''); }; @@ -390,7 +540,8 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { // This source stream only contains raw traffic from the client // The vless header is stripped and parsed first. - const fromClientTraffic = readableWebSocketStream.pipeThrough(new TransformStream({ + /** @type {TransformStream} */ + const vlessHeaderProcessor = new TransformStream({ start() { }, transform(chunk, controller) { @@ -414,9 +565,10 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { }, flush(controller){ } - })); + }); + const fromClientTraffic = readableWebSocketStream.pipeThrough(vlessHeaderProcessor); - /** @type {WritableStream | null}*/ + /** @type {WritableStream | null}*/ let remoteTrafficSink = null; // ws --> remote @@ -456,17 +608,24 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { return 101; } +/** + * @typedef {{ + * readableStream: ReadableStream, + * writableStream: WritableStream, + * }} OutboundConnection + */ + /** * Handles outbound connections. * @param {{isUDP: boolean, addressType: number, addressRemote: string, portRemote: number}} vlessRequest * @param {Uint8Array} rawClientData The raw client data to write. - * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to pass the remote socket to. - * @param {{header: Uint8Array}} vlessResponse Contains information to produce the vless reponse, such as the header. - * @param {function} log The logging function. - * @returns {Promise} a non-null fulfill indicates the success connection to the destination or the remote proxy server + * @param {WebSocket} webSocket The WebSocket to pass the remote socket to. + * @param {{header: Uint8Array}} vlessResponse Contains information to produce the vless response, such as the header. + * @param {LogFunction} log The logger function. + * @returns a non-null fulfill indicates the success connection to the destination or the remote proxy server */ async function handleOutBound(vlessRequest, rawClientData, webSocket, vlessResponse, log) { - let curOutBoundPtr = {index: 0, serverIndex: 0}; + const curOutBoundPtr = {index: 0, serverIndex: 0}; // Check if we should forward UDP DNS requests to a designated TCP DNS server. // The vless packing of UDP datagrams is identical to the one used in TCP DNS protocol, @@ -489,6 +648,9 @@ async function handleOutBound(vlessRequest, rawClientData, webSocket, vlessRespo writer.releaseLock(); } + /** + * @returns {Promise} + */ async function direct() { if (enforceUDP) { // TODO: Check what will happen if addressType == VlessAddrType.DomainName and that domain only resolves to a IPv6 @@ -519,6 +681,11 @@ async function handleOutBound(vlessRequest, rawClientData, webSocket, vlessRespo }; } + /** + * @param {string} proxyServer + * @param {PortMap} [portMap] + * @returns {Promise} + */ async function forward(proxyServer, portMap) { let portDest = vlessRequest.portRemote; if (typeof portMap === "object" && portMap[vlessRequest.portRemote] !== undefined) { @@ -537,6 +704,13 @@ async function handleOutBound(vlessRequest, rawClientData, webSocket, vlessRespo // TODO: known problem, if we send an unreachable request to a valid socks5 server, it will wait indefinitely // TODO: Add support for proxying UDP via socks5 on runtimes that support UDP outbound + /** + * @param {string} address + * @param {number} port + * @param {string} user + * @param {string} pass + * @returns {Promise} + */ async function socks5(address, port, user, pass) { const tcpSocket = await platformAPI.connect(address, port); tcpSocket.closed.catch(error => log('[socks] tcpSocket closed with error: ', error.message)); @@ -563,10 +737,11 @@ async function handleOutBound(vlessRequest, rawClientData, webSocket, vlessRespo * In the case of UDP, a 16-bit big-endian length field is prepended to each UDP datagram and then send through the streams. * The first message exchange still applies. * - * @param {string} address - * @param {number} port - * @param {string} uuid - * @param {{network: string, security: string}} streamSettings + * @param {string} address + * @param {number} port + * @param {string} uuid + * @param {StreamSettings} streamSettings + * @returns {Promise} */ async function vless(address, port, uuid, streamSettings) { try { @@ -603,6 +778,7 @@ async function handleOutBound(vlessRequest, rawClientData, webSocket, vlessRespo return null; } + /** @type {WritableStream} */ const writableStream = new WritableStream({ async write(chunk, controller) { wsToVlessServer.send(chunk); @@ -645,7 +821,7 @@ async function handleOutBound(vlessRequest, rawClientData, webSocket, vlessRespo }; } - /** @returns {Promise<{readableStream: ReadableStream, writableStream: WritableStream} | null>} */ + /** @returns {Promise} */ async function connectAndWrite() { const outbound = getOutbound(curOutBoundPtr); if (outbound == null) { @@ -675,7 +851,7 @@ async function handleOutBound(vlessRequest, rawClientData, webSocket, vlessRespo } // Try each outbound method until we find a working one. - /** @type {{readableStream: ReadableStream, writableStream: WritableStream} | null} */ + /** @type {OutboundConnection | null} */ let destRWPair = null; while (curOutBoundPtr.index < globalConfig.outbounds.length) { if (destRWPair == null) { @@ -702,11 +878,12 @@ async function handleOutBound(vlessRequest, rawClientData, webSocket, vlessRespo * Make a source out of a UDP socket, wrap each datagram with vless UDP packing. * Each receive datagram will be prepended with a 16-bit big-endian length field. * - * @param {*} udpClient - * @param {(info: string)=> void} log - * @returns {ReadableStream} Datagrams received will be wrapped and made available in this stream. + * @param {NodeJSUDP} udpClient + * @param {LogFunction} log + * @returns {ReadableStream} Datagrams received will be wrapped and made available in this stream. */ function makeReadableUDPStream(udpClient, log) { + /** @type {ReadableStream} */ return new ReadableStream({ start(controller) { udpClient.onmessage((message, info) => { @@ -732,25 +909,23 @@ function makeReadableUDPStream(udpClient, log) { * Make a sink out of a UDP socket, the input stream assumes valid vless UDP packing. * Each datagram to be sent should be prepended with a 16-bit big-endian length field. * - * @param {*} udpClient - * @param {string} addressRemote - * @param {port} portRemote - * @param {(info: string)=> void} log - * @returns {WritableStream} write to this stream will send datagrams via UDP. + * @param {NodeJSUDP} udpClient + * @param {string} addressRemote + * @param {number} portRemote + * @param {LogFunction} log + * @returns {WritableStream} write to this stream will send datagrams via UDP. */ function makeWritableUDPStream(udpClient, addressRemote, portRemote, log) { /** @type {Uint8Array} */ let leftoverData = new Uint8Array(0); + /** @type {WritableStream} */ return new WritableStream({ - /** @param {ArrayBuffer} chunk */ write(chunk, controller) { let byteArray = new Uint8Array(chunk); if (leftoverData.byteLength > 0) { // If we have any leftover data from previous chunk, merge it first - byteArray = new Uint8Array(leftoverData.byteLength + chunk.byteLength); - byteArray.set(leftoverData, 0); - byteArray.set(new Uint8Array(chunk), leftoverData.byteLength); + byteArray = joinUint8Array(leftoverData, byteArray); } let i = 0; @@ -790,9 +965,12 @@ function makeWritableUDPStream(udpClient, addressRemote, portRemote, log) { }); } -function safeCloseUDP(socket) { +/** + * @param {NodeJSUDP} udpClient + */ +function safeCloseUDP(udpClient) { try { - socket.close(); + udpClient.close(); } catch (error) { console.error('safeCloseUDP error', error); } @@ -802,16 +980,18 @@ function safeCloseUDP(socket) { * Make a source out of a WebSocket connection. * A ReadableStream should be created before performing any kind of write operation. * - * @param {import("@cloudflare/workers-types").WebSocket} webSocketServer + * @param {WebSocket} webSocketServer * @param {Uint8Array} earlyData Data received before the ReadableStream was created * @param {(firstChunk : Uint8Array) => Uint8Array} headStripper In some protocol like Vless, * a header is prepended to the first data chunk, it is necessary to strip that header. - * @param {(info: string)=> void} log - * @returns {ReadableStream} a source of Uint8Array chunks + * @param {LogFunction} log + * @returns {ReadableStream} a source of Uint8Array chunks */ function makeReadableWebSocketStream(webSocketServer, earlyData, headStripper, log) { let readableStreamCancel = false; let headStripped = false; + + /** @type {ReadableStream} */ const stream = new ReadableStream({ start(controller) { if (earlyData) { @@ -906,7 +1086,7 @@ function processVlessHeader( const version = vlessBuffer.slice(0, 1); let isValidUser = false; let isUDP = false; - if (stringify(vlessBuffer.slice(1, 17)) === userID) { + if (uuidFromBytesSafe(vlessBuffer.slice(1, 17)) === userID) { isValidUser = true; } if (!isValidUser) { @@ -921,10 +1101,9 @@ function processVlessHeader( const command = vlessBuffer.slice(18 + optLength, 18 + optLength + 1)[0]; - if (command === VlessCmd.TCP) { - } else if (command === VlessCmd.UDP) { + if (command === VlessCmd.UDP) { isUDP = true; - } else { + } else if (command !== VlessCmd.TCP) { return { hasError: true, message: `Invalid command type: ${command}, only accepts: ${JSON.stringify(VlessCmd)}`, @@ -934,7 +1113,7 @@ function processVlessHeader( // port is big-Endian in raw data etc 80 == 0x0050 const portRemote = (vlessBuffer[portIndex] << 8) | vlessBuffer[portIndex + 1]; - let addressIndex = portIndex + 2; + const addressIndex = portIndex + 2; const addressBuffer = vlessBuffer.slice(addressIndex, addressIndex + 1); const addressType = addressBuffer[0]; @@ -953,7 +1132,7 @@ function processVlessHeader( vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) ); break; - case VlessAddrType.IPv6: + case VlessAddrType.IPv6: { addressLength = 16; const ipv6Bytes = vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength); // 2001:0db8:85a3:0000:0000:8a2e:0370:7334 @@ -965,6 +1144,7 @@ function processVlessHeader( addressValue = ipv6.join(':'); // seems no need add [] for ipv6 break; + } default: return { hasError: true, @@ -991,16 +1171,17 @@ function processVlessHeader( /** * Stream data from the remote destination (any) to the client side (Websocket) - * @param {ReadableStream} remoteSocketReader from the remote destination - * @param {import("@cloudflare/workers-types").WebSocket} webSocket to the client side + * @param {ReadableStream} remoteSocketReader from the remote destination + * @param {WebSocket} webSocket to the client side * @param {{header: Uint8Array}} vlessResponse Contains information to produce the vless reponse, such as the header. - * @param {*} log + * @param {LogFunction} log * @returns {Promise} has hasIncomingData */ async function remoteSocketToWS(remoteSocketReader, webSocket, vlessResponse, log) { // This promise fulfills if: // 1. There is any incoming data // 2. The remoteSocketReader closes without any data + /** @type {Promise} */ const toRemotePromise = new Promise((resolve) => { let headerSent = false; let hasIncomingData = false; @@ -1031,7 +1212,7 @@ async function remoteSocketToWS(remoteSocketReader, webSocket, vlessResponse, lo .pipeTo(new WritableStream({ start() { }, - async write(chunk, controller) { + write(chunk, controller) { // remoteChunkCount++; if (webSocket.readyState !== WS_READY_STATE_OPEN) { controller.error( @@ -1097,11 +1278,46 @@ function isValidUUID(uuid) { return uuidRegex.test(uuid); } +/** + * Convert ArrayBuffer to a UUID string and check it against isValidUUID() + * @param {ArrayBufferLike} buffer + */ +function uuidFromBytesSafe(buffer, offset = 0) { + const uuid = uuidStrFromBytes(buffer, offset); + if (!isValidUUID(uuid)) { + throw TypeError("Stringified UUID is invalid"); + } + return uuid; +} + +/** + * Convert ArrayBuffer to a UUID string + * @param {ArrayBufferLike} buffer + * @returns {string} UUID in lower-case + */ +export function uuidStrFromBytes(buffer, offset = 0) { + const bytes = new Uint8Array(buffer); + let uuid = ''; + + for (let i = 0; i < 16; i++) { + let byteHex = bytes[i + offset].toString(16).toLowerCase(); + if (byteHex.length === 1) { + byteHex = '0' + byteHex; // Ensure byte is always represented by two characters + } + uuid += byteHex; + if (i === 3 || i === 5 || i === 7 || i === 9) { + uuid += '-'; + } + } + + return uuid; +} + const WS_READY_STATE_OPEN = 1; const WS_READY_STATE_CLOSING = 2; /** * Normally, WebSocket will not has exceptions when close. - * @param {import("@cloudflare/workers-types").WebSocket} socket + * @param {WebSocket} socket */ function safeCloseWebSocket(socket) { try { @@ -1120,35 +1336,20 @@ function safeCloseWebSocket(socket) { * @returns {Uint8Array} the merged Uint8Array */ export function joinUint8Array(array1, array2) { - let result = new Uint8Array(array1.byteLength + array2.byteLength); + const result = new Uint8Array(array1.byteLength + array2.byteLength); result.set(array1); result.set(array2, array1.byteLength); return result; } -const byteToHex = []; -for (let i = 0; i < 256; ++i) { - byteToHex.push((i + 256).toString(16).slice(1)); -} -function unsafeStringify(arr, offset = 0) { - return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); -} -function stringify(arr, offset = 0) { - const uuid = unsafeStringify(arr, offset); - if (!isValidUUID(uuid)) { - throw TypeError("Stringified UUID is invalid"); - } - return uuid; -} - /** - * @param {{readable: ReadableStream, writable: {getWriter: () => {write: (data) => void, releaseLock: () => void}}}} socket + * @param {CloudflareTCPConnection} socket * @param {string} username * @param {string} password * @param {number} addressType * @param {string} addressRemote * @param {number} portRemote - * @param {function} log The logging function. + * @param {LogFunction} log The logging function. */ async function socks5Connect(socket, username, password, addressType, addressRemote, portRemote, log) { const writer = socket.writable.getWriter(); @@ -1229,6 +1430,7 @@ async function socks5Connect(socket, username, password, addressType, addressRem // 1--> ipv4 addressLength =4 // 2--> domain name // 3--> ipv6 addressLength =16 + /** @type {Uint8Array?} */ let DSTADDR; // DSTADDR = ATYP + DST.ADDR switch (addressType) { case 1: @@ -1276,8 +1478,8 @@ async function socks5Connect(socket, username, password, addressType, addressRem * @param {string} address */ function socks5AddressParser(address) { - let [latter, former] = address.split("@").reverse(); - let username, password, hostname, port; + const [latter, former] = address.split("@").reverse(); + let username, password; if (former) { const formers = former.split(":"); if (formers.length !== 2) { @@ -1286,11 +1488,11 @@ function socks5AddressParser(address) { [username, password] = formers; } const latters = latter.split(":"); - port = Number(latters.pop()); + const port = Number(latters.pop()); if (isNaN(port)) { throw new Error('Invalid SOCKS address format'); } - hostname = latters.join(":"); + const hostname = latters.join(":"); const regex = /^\[.*\]$/; if (hostname.includes(":") && !regex.test(hostname)) { throw new Error('Invalid SOCKS address format'); @@ -1373,24 +1575,26 @@ function makeVlessReqHeader(command, destType, destAddr, destPort, uuid) { // Address switch (destType) { - case VlessAddrType.IPv4: + case VlessAddrType.IPv4: { const octetsIPv4 = destAddr.split('.'); for (let i = 0; i < 4; i++) { vlessHeader[22 + i] = parseInt(octetsIPv4[i]); } break; + } case VlessAddrType.DomainName: vlessHeader[22] = addressEncoded.length; vlessHeader.set(addressEncoded, 23); break; - case VlessAddrType.IPv6: - const groupsIPv6 = ipv6.split(':'); + case VlessAddrType.IPv6: { + const groupsIPv6 = destAddr.replace(/\[|\]/g, '').split(':'); for (let i = 0; i < 8; i++) { const hexGroup = parseInt(groupsIPv6[i], 16); vlessHeader[i * 2 + 22] = hexGroup >> 8; vlessHeader[i * 2 + 23] = hexGroup & 0xFF; } break; + } default: throw new Error(`Unknown address type: ${destType}`); } @@ -1398,6 +1602,10 @@ function makeVlessReqHeader(command, destType, destAddr, destPort, uuid) { return vlessHeader; } +/** + * @param {string} address Domain name, HTTP request Hostname, and the SNI of the remote host. + * @param {StreamSettings} streamSettings + */ function checkVlessConfig(address, streamSettings) { if (streamSettings.network !== 'ws') { throw new Error(`Unsupported outbound stream method: ${streamSettings.network}, has to be ws (Websocket)`); @@ -1416,6 +1624,10 @@ function checkVlessConfig(address, streamSettings) { } } +/** + * Parse a Vless URL into its components. + * @param {string} url + */ function parseVlessString(url) { const regex = /^(.+):\/\/(.+?)@(.+?):(\d+)(\?[^#]*)?(#.*)?$/; const match = url.match(regex); From 3d332fbcd3a292a6b597df85f7e5cec93d7ee4bd Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Sat, 2 Dec 2023 02:33:11 +1100 Subject: [PATCH 36/44] Restore worker-with-socks5-experimental.js --- src/worker-with-socks5-experimental.js | 801 +++++++++++++++++++++++++ 1 file changed, 801 insertions(+) create mode 100644 src/worker-with-socks5-experimental.js diff --git a/src/worker-with-socks5-experimental.js b/src/worker-with-socks5-experimental.js new file mode 100644 index 0000000000..500b3b57db --- /dev/null +++ b/src/worker-with-socks5-experimental.js @@ -0,0 +1,801 @@ +// version base on commit 2b9927a1b12e03f8ad4731541caee2bc5c8f2e8e, time is 2023-06-22 15:09:37 UTC. +// @ts-ignore +import { connect } from 'cloudflare:sockets'; + +// How to generate your own UUID: +// [Windows] Press "Win + R", input cmd and run: Powershell -NoExit -Command "[guid]::NewGuid()" +let userID = 'd342d11e-d424-4583-b36e-524ab1f0afa4'; + +let proxyIP = ''; + +// The user name and password do not contain special characters +// Setting the address will ignore proxyIP +// Example: user:pass@host:port or host:port +let socks5Address = ''; + +if (!isValidUUID(userID)) { + throw new Error('uuid is not valid'); +} + +let parsedSocks5Address = {}; +let enableSocks = false; + +export default { + /** + * @param {import("@cloudflare/workers-types").Request} request + * @param {{UUID: string, PROXYIP: string}} env + * @param {import("@cloudflare/workers-types").ExecutionContext} ctx + * @returns {Promise} + */ + async fetch(request, env, ctx) { + try { + userID = env.UUID || userID; + proxyIP = env.PROXYIP || proxyIP; + socks5Address = env.SOCKS5 || socks5Address; + if (socks5Address) { + try { + parsedSocks5Address = socks5AddressParser(socks5Address); + enableSocks = true; + } catch (err) { + /** @type {Error} */ let e = err; + console.log(e.toString()); + enableSocks = false; + } + } + const upgradeHeader = request.headers.get('Upgrade'); + if (!upgradeHeader || upgradeHeader !== 'websocket') { + const url = new URL(request.url); + switch (url.pathname) { + case '/': + return new Response(JSON.stringify(request.cf), { status: 200 }); + case `/${userID}`: { + const vlessConfig = getVLESSConfig(userID, request.headers.get('Host')); + return new Response(`${vlessConfig}`, { + status: 200, + headers: { + "Content-Type": "text/plain;charset=utf-8", + } + }); + } + default: + return new Response('Not found', { status: 404 }); + } + } else { + return await vlessOverWSHandler(request); + } + } catch (err) { + /** @type {Error} */ let e = err; + return new Response(e.toString()); + } + }, +}; + + + + +/** + * + * @param {import("@cloudflare/workers-types").Request} request + */ +async function vlessOverWSHandler(request) { + + /** @type {import("@cloudflare/workers-types").WebSocket[]} */ + // @ts-ignore + const webSocketPair = new WebSocketPair(); + const [client, webSocket] = Object.values(webSocketPair); + + webSocket.accept(); + + let address = ''; + let portWithRandomLog = ''; + const log = (/** @type {string} */ info, /** @type {string | undefined} */ event) => { + console.log(`[${address}:${portWithRandomLog}] ${info}`, event || ''); + }; + const earlyDataHeader = request.headers.get('sec-websocket-protocol') || ''; + + const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log); + + /** @type {{ value: import("@cloudflare/workers-types").Socket | null}}*/ + let remoteSocketWapper = { + value: null, + }; + let isDns = false; + + // ws --> remote + readableWebSocketStream.pipeTo(new WritableStream({ + async write(chunk, controller) { + if (isDns) { + return await handleDNSQuery(chunk, webSocket, null, log); + } + if (remoteSocketWapper.value) { + const writer = remoteSocketWapper.value.writable.getWriter() + await writer.write(chunk); + writer.releaseLock(); + return; + } + + const { + hasError, + message, + addressType, + portRemote = 443, + addressRemote = '', + rawDataIndex, + vlessVersion = new Uint8Array([0, 0]), + isUDP, + } = processVlessHeader(chunk, userID); + address = addressRemote; + portWithRandomLog = `${portRemote}--${Math.random()} ${isUDP ? 'udp ' : 'tcp ' + } `; + if (hasError) { + // controller.error(message); + throw new Error(message); // cf seems has bug, controller.error will not end stream + // webSocket.close(1000, message); + return; + } + // if UDP but port not DNS port, close it + if (isUDP) { + if (portRemote === 53) { + isDns = true; + } else { + // controller.error('UDP proxy only enable for DNS which is port 53'); + throw new Error('UDP proxy only enable for DNS which is port 53'); // cf seems has bug, controller.error will not end stream + return; + } + } + // ["version", "附加信息长度 N"] + const vlessResponseHeader = new Uint8Array([vlessVersion[0], 0]); + const rawClientData = chunk.slice(rawDataIndex); + + if (isDns) { + return handleDNSQuery(rawClientData, webSocket, vlessResponseHeader, log); + } + handleTCPOutBound(remoteSocketWapper, addressType, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log); + }, + close() { + log(`readableWebSocketStream is close`); + }, + abort(reason) { + log(`readableWebSocketStream is abort`, JSON.stringify(reason)); + }, + })).catch((err) => { + log('readableWebSocketStream pipeTo error', err); + }); + + return new Response(null, { + status: 101, + // @ts-ignore + webSocket: client, + }); +} + +/** + * Handles outbound TCP connections. + * + * @param {any} remoteSocket + * @param {number} addressType The remote address type to connect to. + * @param {string} addressRemote The remote address to connect to. + * @param {number} portRemote The remote port to connect to. + * @param {Uint8Array} rawClientData The raw client data to write. + * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to pass the remote socket to. + * @param {Uint8Array} vlessResponseHeader The VLESS response header. + * @param {function} log The logging function. + * @returns {Promise} The remote socket. + */ +async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log,) { + async function connectAndWrite(address, port, socks = false) { + /** @type {import("@cloudflare/workers-types").Socket} */ + const tcpSocket = socks ? await socks5Connect(addressType, address, port, log) + : connect({ + hostname: address, + port: port, + }); + remoteSocket.value = tcpSocket; + log(`connected to ${address}:${port}`); + const writer = tcpSocket.writable.getWriter(); + await writer.write(rawClientData); // first write, normal is tls client hello + writer.releaseLock(); + return tcpSocket; + } + + // if the cf connect tcp socket have no incoming data, we retry to redirect ip + async function retry() { + if (enableSocks) { + tcpSocket = await connectAndWrite(addressRemote, portRemote, true); + } else { + tcpSocket = await connectAndWrite(proxyIP || addressRemote, portRemote); + } + // no matter retry success or not, close websocket + tcpSocket.closed.catch(error => { + console.log('retry tcpSocket closed error', error); + }).finally(() => { + safeCloseWebSocket(webSocket); + }) + remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, null, log); + } + + let tcpSocket = await connectAndWrite(addressRemote, portRemote); + + // when remoteSocket is ready, pass to websocket + // remote--> ws + remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, retry, log); +} + +/** + * + * @param {import("@cloudflare/workers-types").WebSocket} webSocketServer + * @param {string} earlyDataHeader for ws 0rtt + * @param {(info: string)=> void} log for ws 0rtt + */ +function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) { + let readableStreamCancel = false; + const stream = new ReadableStream({ + start(controller) { + webSocketServer.addEventListener('message', (event) => { + if (readableStreamCancel) { + return; + } + const message = event.data; + controller.enqueue(message); + }); + + // The event means that the client closed the client -> server stream. + // However, the server -> client stream is still open until you call close() on the server side. + // The WebSocket protocol says that a separate close message must be sent in each direction to fully close the socket. + webSocketServer.addEventListener('close', () => { + // client send close, need close server + // if stream is cancel, skip controller.close + safeCloseWebSocket(webSocketServer); + if (readableStreamCancel) { + return; + } + controller.close(); + } + ); + webSocketServer.addEventListener('error', (err) => { + log('webSocketServer has error'); + controller.error(err); + } + ); + // for ws 0rtt + const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader); + if (error) { + controller.error(error); + } else if (earlyData) { + controller.enqueue(earlyData); + } + }, + + pull(controller) { + // if ws can stop read if stream is full, we can implement backpressure + // https://streams.spec.whatwg.org/#example-rs-push-backpressure + }, + cancel(reason) { + // 1. pipe WritableStream has error, this cancel will called, so ws handle server close into here + // 2. if readableStream is cancel, all controller.close/enqueue need skip, + // 3. but from testing controller.error still work even if readableStream is cancel + if (readableStreamCancel) { + return; + } + log(`ReadableStream was canceled, due to ${reason}`) + readableStreamCancel = true; + safeCloseWebSocket(webSocketServer); + } + }); + + return stream; + +} + +// https://xtls.github.io/development/protocols/vless.html +// https://github.com/zizifn/excalidraw-backup/blob/main/v2ray-protocol.excalidraw + +/** + * + * @param { ArrayBuffer} vlessBuffer + * @param {string} userID + * @returns + */ +function processVlessHeader( + vlessBuffer, + userID +) { + if (vlessBuffer.byteLength < 24) { + return { + hasError: true, + message: 'invalid data', + }; + } + const version = new Uint8Array(vlessBuffer.slice(0, 1)); + let isValidUser = false; + let isUDP = false; + if (stringify(new Uint8Array(vlessBuffer.slice(1, 17))) === userID) { + isValidUser = true; + } + if (!isValidUser) { + return { + hasError: true, + message: 'invalid user', + }; + } + + const optLength = new Uint8Array(vlessBuffer.slice(17, 18))[0]; + //skip opt for now + + const command = new Uint8Array( + vlessBuffer.slice(18 + optLength, 18 + optLength + 1) + )[0]; + + // 0x01 TCP + // 0x02 UDP + // 0x03 MUX + if (command === 1) { + } else if (command === 2) { + isUDP = true; + } else { + return { + hasError: true, + message: `command ${command} is not support, command 01-tcp,02-udp,03-mux`, + }; + } + const portIndex = 18 + optLength + 1; + const portBuffer = vlessBuffer.slice(portIndex, portIndex + 2); + // port is big-Endian in raw data etc 80 == 0x005d + const portRemote = new DataView(portBuffer).getUint16(0); + + let addressIndex = portIndex + 2; + const addressBuffer = new Uint8Array( + vlessBuffer.slice(addressIndex, addressIndex + 1) + ); + + // 1--> ipv4 addressLength =4 + // 2--> domain name addressLength=addressBuffer[1] + // 3--> ipv6 addressLength =16 + const addressType = addressBuffer[0]; + let addressLength = 0; + let addressValueIndex = addressIndex + 1; + let addressValue = ''; + switch (addressType) { + case 1: + addressLength = 4; + addressValue = new Uint8Array( + vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) + ).join('.'); + break; + case 2: + addressLength = new Uint8Array( + vlessBuffer.slice(addressValueIndex, addressValueIndex + 1) + )[0]; + addressValueIndex += 1; + addressValue = new TextDecoder().decode( + vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) + ); + break; + case 3: + addressLength = 16; + const dataView = new DataView( + vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) + ); + // 2001:0db8:85a3:0000:0000:8a2e:0370:7334 + const ipv6 = []; + for (let i = 0; i < 8; i++) { + ipv6.push(dataView.getUint16(i * 2).toString(16)); + } + addressValue = ipv6.join(':'); + // seems no need add [] for ipv6 + break; + default: + return { + hasError: true, + message: `invild addressType is ${addressType}`, + }; + } + if (!addressValue) { + return { + hasError: true, + message: `addressValue is empty, addressType is ${addressType}`, + }; + } + + return { + hasError: false, + addressRemote: addressValue, + addressType, + portRemote, + rawDataIndex: addressValueIndex + addressLength, + vlessVersion: version, + isUDP, + }; +} + + +/** + * + * @param {import("@cloudflare/workers-types").Socket} remoteSocket + * @param {import("@cloudflare/workers-types").WebSocket} webSocket + * @param {ArrayBuffer} vlessResponseHeader + * @param {(() => Promise) | null} retry + * @param {*} log + */ +async function remoteSocketToWS(remoteSocket, webSocket, vlessResponseHeader, retry, log) { + // remote--> ws + let remoteChunkCount = 0; + let chunks = []; + /** @type {ArrayBuffer | null} */ + let vlessHeader = vlessResponseHeader; + let hasIncomingData = false; // check if remoteSocket has incoming data + await remoteSocket.readable + .pipeTo( + new WritableStream({ + start() { + }, + /** + * + * @param {Uint8Array} chunk + * @param {*} controller + */ + async write(chunk, controller) { + hasIncomingData = true; + // remoteChunkCount++; + if (webSocket.readyState !== WS_READY_STATE_OPEN) { + controller.error( + 'webSocket.readyState is not open, maybe close' + ); + } + if (vlessHeader) { + webSocket.send(await new Blob([vlessHeader, chunk]).arrayBuffer()); + vlessHeader = null; + } else { + // seems no need rate limit this, CF seems fix this??.. + // if (remoteChunkCount > 20000) { + // // cf one package is 4096 byte(4kb), 4096 * 20000 = 80M + // await delay(1); + // } + webSocket.send(chunk); + } + }, + close() { + log(`remoteConnection!.readable is close with hasIncomingData is ${hasIncomingData}`); + // safeCloseWebSocket(webSocket); // no need server close websocket frist for some case will casue HTTP ERR_CONTENT_LENGTH_MISMATCH issue, client will send close event anyway. + }, + abort(reason) { + console.error(`remoteConnection!.readable abort`, reason); + }, + }) + ) + .catch((error) => { + console.error( + `remoteSocketToWS has exception `, + error.stack || error + ); + safeCloseWebSocket(webSocket); + }); + + // seems is cf connect socket have error, + // 1. Socket.closed will have error + // 2. Socket.readable will be close without any data coming + if (hasIncomingData === false && retry) { + log(`retry`) + retry(); + } +} + +/** + * + * @param {string} base64Str + * @returns + */ +function base64ToArrayBuffer(base64Str) { + if (!base64Str) { + return { error: null }; + } + try { + // go use modified Base64 for URL rfc4648 which js atob not support + base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/'); + const decode = atob(base64Str); + const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0)); + return { earlyData: arryBuffer.buffer, error: null }; + } catch (error) { + return { error }; + } +} + +/** + * This is not real UUID validation + * @param {string} uuid + */ +function isValidUUID(uuid) { + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + return uuidRegex.test(uuid); +} + +const WS_READY_STATE_OPEN = 1; +const WS_READY_STATE_CLOSING = 2; +/** + * Normally, WebSocket will not has exceptions when close. + * @param {import("@cloudflare/workers-types").WebSocket} socket + */ +function safeCloseWebSocket(socket) { + try { + if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) { + socket.close(); + } + } catch (error) { + console.error('safeCloseWebSocket error', error); + } +} + +const byteToHex = []; +for (let i = 0; i < 256; ++i) { + byteToHex.push((i + 256).toString(16).slice(1)); +} +function unsafeStringify(arr, offset = 0) { + return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); +} +function stringify(arr, offset = 0) { + const uuid = unsafeStringify(arr, offset); + if (!isValidUUID(uuid)) { + throw TypeError("Stringified UUID is invalid"); + } + return uuid; +} + +/** + * + * @param {ArrayBuffer} udpChunk + * @param {import("@cloudflare/workers-types").WebSocket} webSocket + * @param {ArrayBuffer} vlessResponseHeader + * @param {(string)=> void} log + */ +async function handleDNSQuery(udpChunk, webSocket, vlessResponseHeader, log) { + // no matter which DNS server client send, we alwasy use hard code one. + // beacsue someof DNS server is not support DNS over TCP + try { + const dnsServer = '8.8.4.4'; // change to 1.1.1.1 after cf fix connect own ip bug + const dnsPort = 53; + /** @type {ArrayBuffer | null} */ + let vlessHeader = vlessResponseHeader; + /** @type {import("@cloudflare/workers-types").Socket} */ + const tcpSocket = connect({ + hostname: dnsServer, + port: dnsPort, + }); + + log(`connected to ${dnsServer}:${dnsPort}`); + const writer = tcpSocket.writable.getWriter(); + await writer.write(udpChunk); + writer.releaseLock(); + await tcpSocket.readable.pipeTo(new WritableStream({ + async write(chunk) { + if (webSocket.readyState === WS_READY_STATE_OPEN) { + if (vlessHeader) { + webSocket.send(await new Blob([vlessHeader, chunk]).arrayBuffer()); + vlessHeader = null; + } else { + webSocket.send(chunk); + } + } + }, + close() { + log(`dns server(${dnsServer}) tcp is close`); + }, + abort(reason) { + console.error(`dns server(${dnsServer}) tcp is abort`, reason); + }, + })); + } catch (error) { + console.error( + `handleDNSQuery have exception, error: ${error.message}` + ); + } +} + +/** + * + * @param {number} addressType + * @param {string} addressRemote + * @param {number} portRemote + * @param {function} log The logging function. + */ +async function socks5Connect(addressType, addressRemote, portRemote, log) { + const { username, password, hostname, port } = parsedSocks5Address; + // Connect to the SOCKS server + const socket = connect({ + hostname, + port, + }); + + // Request head format (Worker -> Socks Server): + // +----+----------+----------+ + // |VER | NMETHODS | METHODS | + // +----+----------+----------+ + // | 1 | 1 | 1 to 255 | + // +----+----------+----------+ + + // https://en.wikipedia.org/wiki/SOCKS#SOCKS5 + // For METHODS: + // 0x00 NO AUTHENTICATION REQUIRED + // 0x02 USERNAME/PASSWORD https://datatracker.ietf.org/doc/html/rfc1929 + const socksGreeting = new Uint8Array([5, 2, 0, 2]); + + const writer = socket.writable.getWriter(); + + await writer.write(socksGreeting); + log('sent socks greeting'); + + const reader = socket.readable.getReader(); + const encoder = new TextEncoder(); + let res = (await reader.read()).value; + // Response format (Socks Server -> Worker): + // +----+--------+ + // |VER | METHOD | + // +----+--------+ + // | 1 | 1 | + // +----+--------+ + if (res[0] !== 0x05) { + log(`socks server version error: ${res[0]} expected: 5`); + return; + } + if (res[1] === 0xff) { + log("no acceptable methods"); + return; + } + + // if return 0x0502 + if (res[1] === 0x02) { + log("socks server needs auth"); + if (!username || !password) { + log("please provide username/password"); + return; + } + // +----+------+----------+------+----------+ + // |VER | ULEN | UNAME | PLEN | PASSWD | + // +----+------+----------+------+----------+ + // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | + // +----+------+----------+------+----------+ + const authRequest = new Uint8Array([ + 1, + username.length, + ...encoder.encode(username), + password.length, + ...encoder.encode(password) + ]); + await writer.write(authRequest); + res = (await reader.read()).value; + // expected 0x0100 + if (res[0] !== 0x01 || res[1] !== 0x00) { + log("fail to auth socks server"); + return; + } + } + + // Request data format (Worker -> Socks Server): + // +----+-----+-------+------+----------+----------+ + // |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | + // +----+-----+-------+------+----------+----------+ + // | 1 | 1 | X'00' | 1 | Variable | 2 | + // +----+-----+-------+------+----------+----------+ + // ATYP: address type of following address + // 0x01: IPv4 address + // 0x03: Domain name + // 0x04: IPv6 address + // DST.ADDR: desired destination address + // DST.PORT: desired destination port in network octet order + + // addressType + // 1--> ipv4 addressLength =4 + // 2--> domain name + // 3--> ipv6 addressLength =16 + let DSTADDR; // DSTADDR = ATYP + DST.ADDR + switch (addressType) { + case 1: + DSTADDR = new Uint8Array( + [1, ...addressRemote.split('.').map(Number)] + ); + break; + case 2: + DSTADDR = new Uint8Array( + [3, addressRemote.length, ...encoder.encode(addressRemote)] + ); + break; + case 3: + DSTADDR = new Uint8Array( + [4, ...addressRemote.split(':').flatMap(x => [parseInt(x.slice(0, 2), 16), parseInt(x.slice(2), 16)])] + ); + break; + default: + log(`invild addressType is ${addressType}`); + return; + } + const socksRequest = new Uint8Array([5, 1, 0, ...DSTADDR, portRemote >> 8, portRemote & 0xff]); + await writer.write(socksRequest); + log('sent socks request'); + + res = (await reader.read()).value; + // Response format (Socks Server -> Worker): + // +----+-----+-------+------+----------+----------+ + // |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | + // +----+-----+-------+------+----------+----------+ + // | 1 | 1 | X'00' | 1 | Variable | 2 | + // +----+-----+-------+------+----------+----------+ + if (res[1] === 0x00) { + log("socks connection opened"); + } else { + log("fail to open socks connection"); + return; + } + writer.releaseLock(); + reader.releaseLock(); + return socket; +} + + +/** + * + * @param {string} address + */ +function socks5AddressParser(address) { + let [latter, former] = address.split("@").reverse(); + let username, password, hostname, port; + if (former) { + const formers = former.split(":"); + if (formers.length !== 2) { + throw new Error('Invalid SOCKS address format'); + } + [username, password] = formers; + } + const latters = latter.split(":"); + port = Number(latters.pop()); + if (isNaN(port)) { + throw new Error('Invalid SOCKS address format'); + } + hostname = latters.join(":"); + const regex = /^\[.*\]$/; + if (hostname.includes(":") && !regex.test(hostname)) { + throw new Error('Invalid SOCKS address format'); + } + return { + username, + password, + hostname, + port, + } +} + +/** + * + * @param {string} userID + * @param {string | null} hostName + * @returns {string} + */ +function getVLESSConfig(userID, hostName) { + const vlessMain = `vless://${userID}@${hostName}:443?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#${hostName}` + return ` +################################################################ +v2ray +--------------------------------------------------------------- +${vlessMain} +--------------------------------------------------------------- +################################################################ +clash-meta +--------------------------------------------------------------- +- type: vless + name: ${hostName} + server: ${hostName} + port: 443 + uuid: ${userID} + network: ws + tls: true + udp: false + sni: ${hostName} + client-fingerprint: chrome + ws-opts: + path: "/?ed=2048" + headers: + host: ${hostName} +--------------------------------------------------------------- +################################################################ +`; +} + + From 6988af0cc8de38429eee0f6a89460e478bd6aa50 Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Sun, 3 Dec 2023 02:06:05 +1100 Subject: [PATCH 37/44] Add websocket message processor --- src/worker-neo.js | 61 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/src/worker-neo.js b/src/worker-neo.js index 3674aaa5da..14d0223aac 100644 --- a/src/worker-neo.js +++ b/src/worker-neo.js @@ -77,6 +77,15 @@ export const platformAPI = { * } */ associate: null, + + /** + * An optional processor to process the incoming WebSocket request and its response. + * @type { null | ((logger: LogFunction) => { + * request: TransformStream, + * response: TransformStream, + * })} + */ + processor: null, } /** @@ -536,6 +545,15 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyData, null, log); + /** @type {null | TransformStream} */ + let vlessResponseProcessor = null; + let vlessTrafficData = readableWebSocketStream; + if (platformAPI.processor != null) { + const processor = platformAPI.processor(log); + vlessResponseProcessor = processor.response; + vlessTrafficData = readableWebSocketStream.pipeThrough(processor.request); + } + let vlessHeader = null; // This source stream only contains raw traffic from the client @@ -566,7 +584,8 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { flush(controller){ } }); - const fromClientTraffic = readableWebSocketStream.pipeThrough(vlessHeaderProcessor); + + const fromClientTraffic = vlessTrafficData.pipeThrough(vlessHeaderProcessor); /** @type {WritableStream | null}*/ let remoteTrafficSink = null; @@ -587,12 +606,10 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { } // ["version", "length of additional info"] - const vlessResponse = { - header: new Uint8Array([vlessHeader.vlessVersion[0], 0]), - } + const vlessResponseHeader = new Uint8Array([vlessHeader.vlessVersion[0], 0]); // Need to ensure the outbound proxy (if any) is ready before proceeding. - remoteTrafficSink = await handleOutBound(vlessHeader, chunk, webSocket, vlessResponse, log); + remoteTrafficSink = await handleOutBound(vlessHeader, chunk, webSocket, vlessResponseHeader, vlessResponseProcessor, log); // log('Outbound established!'); }, close() { @@ -620,11 +637,12 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { * @param {{isUDP: boolean, addressType: number, addressRemote: string, portRemote: number}} vlessRequest * @param {Uint8Array} rawClientData The raw client data to write. * @param {WebSocket} webSocket The WebSocket to pass the remote socket to. - * @param {{header: Uint8Array}} vlessResponse Contains information to produce the vless response, such as the header. + * @param {Uint8Array} vlessResponseHeader Contains information to produce the vless response, such as the header. + * @param {null | TransformStream} vlessResponseProcessor an optional TransformStream to process the Vless response. * @param {LogFunction} log The logger function. * @returns a non-null fulfill indicates the success connection to the destination or the remote proxy server */ -async function handleOutBound(vlessRequest, rawClientData, webSocket, vlessResponse, log) { +async function handleOutBound(vlessRequest, rawClientData, webSocket, vlessResponseHeader, vlessResponseProcessor, log) { const curOutBoundPtr = {index: 0, serverIndex: 0}; // Check if we should forward UDP DNS requests to a designated TCP DNS server. @@ -812,7 +830,7 @@ async function handleOutBound(vlessRequest, rawClientData, webSocket, vlessRespo }; const readableStream = makeReadableWebSocketStream(wsToVlessServer, null, headerStripper, log); - const vlessReqHeader = makeVlessReqHeader(vlessRequest.isUDP ? VlessCmd.UDP : VlessCmd.TCP, vlessRequest.addressType, vlessRequest.addressRemote, vlessRequest.portRemote, uuid, rawClientData); + const vlessReqHeader = makeVlessReqHeader(vlessRequest.isUDP ? VlessCmd.UDP : VlessCmd.TCP, vlessRequest.addressType, vlessRequest.addressRemote, vlessRequest.portRemote, uuid); // Send the first packet (header + rawClientData), then strip the response header with headerStripper await writeFirstChunk(writableStream, joinUint8Array(vlessReqHeader, rawClientData)); return { @@ -859,7 +877,7 @@ async function handleOutBound(vlessRequest, rawClientData, webSocket, vlessRespo } if (destRWPair != null) { - const hasIncomingData = await remoteSocketToWS(destRWPair.readableStream, webSocket, vlessResponse, log); + const hasIncomingData = await remoteSocketToWS(destRWPair.readableStream, webSocket, vlessResponseHeader, vlessResponseProcessor, log); if (hasIncomingData) { return destRWPair.writableStream; } @@ -1173,11 +1191,12 @@ function processVlessHeader( * Stream data from the remote destination (any) to the client side (Websocket) * @param {ReadableStream} remoteSocketReader from the remote destination * @param {WebSocket} webSocket to the client side - * @param {{header: Uint8Array}} vlessResponse Contains information to produce the vless reponse, such as the header. + * @param {Uint8Array} vlessResponseHeader The Vless response header. + * @param {null | TransformStream} vlessResponseProcessor an optional TransformStream to process the Vless response. * @param {LogFunction} log * @returns {Promise} has hasIncomingData */ -async function remoteSocketToWS(remoteSocketReader, webSocket, vlessResponse, log) { +async function remoteSocketToWS(remoteSocketReader, webSocket, vlessResponseHeader, vlessResponseProcessor, log) { // This promise fulfills if: // 1. There is any incoming data // 2. The remoteSocketReader closes without any data @@ -1187,7 +1206,9 @@ async function remoteSocketToWS(remoteSocketReader, webSocket, vlessResponse, lo let hasIncomingData = false; // Add the response header and monitor if there is any traffic coming from the remote host. - remoteSocketReader.pipeThrough(new TransformStream({ + + /** @type {TransformStream} */ + const vlessResponseHeaderPrepender = new TransformStream({ start() { }, transform(chunk, controller) { @@ -1196,7 +1217,7 @@ async function remoteSocketToWS(remoteSocketReader, webSocket, vlessResponse, lo resolve(true); if (!headerSent) { - controller.enqueue(joinUint8Array(vlessResponse.header, chunk)); + controller.enqueue(joinUint8Array(vlessResponseHeader, chunk)); headerSent = true; } else { controller.enqueue(chunk); @@ -1208,8 +1229,10 @@ async function remoteSocketToWS(remoteSocketReader, webSocket, vlessResponse, lo // The connection has been closed, resolve the promise anyway. resolve(hasIncomingData); } - })) - .pipeTo(new WritableStream({ + }) + + /** @type {WritableStream} */ + const toClientWsSink = new WritableStream({ start() { }, write(chunk, controller) { @@ -1236,7 +1259,13 @@ async function remoteSocketToWS(remoteSocketReader, webSocket, vlessResponse, lo // abort(reason) { // console.error(`remoteSocket.readable aborts`, reason); // }, - })) + }); + + const vlessResponseWithHeader = remoteSocketReader.pipeThrough(vlessResponseHeaderPrepender); + const processedVlessResponse = vlessResponseProcessor == null ? vlessResponseWithHeader : + vlessResponseWithHeader.pipeThrough(vlessResponseProcessor); + + processedVlessResponse.pipeTo(toClientWsSink) .catch((error) => { console.error( `remoteSocketToWS has exception, readyState = ${webSocket.readyState} :`, From 95278035c974a2f203fcdf5062c1e465c074ec5b Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Sun, 3 Dec 2023 04:37:19 +1100 Subject: [PATCH 38/44] Delay the instantiation of response processor --- src/worker-neo.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/worker-neo.js b/src/worker-neo.js index 14d0223aac..fe32ecd869 100644 --- a/src/worker-neo.js +++ b/src/worker-neo.js @@ -80,9 +80,10 @@ export const platformAPI = { /** * An optional processor to process the incoming WebSocket request and its response. + * The response processor may need to be created multiple times before truly utilization. * @type { null | ((logger: LogFunction) => { * request: TransformStream, - * response: TransformStream, + * response: () => TransformStream, * })} */ processor: null, @@ -545,7 +546,7 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyData, null, log); - /** @type {null | TransformStream} */ + /** @type {null | (() => TransformStream)} */ let vlessResponseProcessor = null; let vlessTrafficData = readableWebSocketStream; if (platformAPI.processor != null) { @@ -638,7 +639,7 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { * @param {Uint8Array} rawClientData The raw client data to write. * @param {WebSocket} webSocket The WebSocket to pass the remote socket to. * @param {Uint8Array} vlessResponseHeader Contains information to produce the vless response, such as the header. - * @param {null | TransformStream} vlessResponseProcessor an optional TransformStream to process the Vless response. + * @param {null | (() => TransformStream)} vlessResponseProcessor an optional TransformStream to process the Vless response. * @param {LogFunction} log The logger function. * @returns a non-null fulfill indicates the success connection to the destination or the remote proxy server */ @@ -1192,7 +1193,7 @@ function processVlessHeader( * @param {ReadableStream} remoteSocketReader from the remote destination * @param {WebSocket} webSocket to the client side * @param {Uint8Array} vlessResponseHeader The Vless response header. - * @param {null | TransformStream} vlessResponseProcessor an optional TransformStream to process the Vless response. + * @param {null | (() => TransformStream)} vlessResponseProcessor an optional TransformStream to process the Vless response. * @param {LogFunction} log * @returns {Promise} has hasIncomingData */ @@ -1263,7 +1264,7 @@ async function remoteSocketToWS(remoteSocketReader, webSocket, vlessResponseHead const vlessResponseWithHeader = remoteSocketReader.pipeThrough(vlessResponseHeaderPrepender); const processedVlessResponse = vlessResponseProcessor == null ? vlessResponseWithHeader : - vlessResponseWithHeader.pipeThrough(vlessResponseProcessor); + vlessResponseWithHeader.pipeThrough(vlessResponseProcessor()); processedVlessResponse.pipeTo(toClientWsSink) .catch((error) => { From d69aad56e58253a9c68a17221c65dce181468bbc Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Sun, 3 Dec 2023 22:17:20 +1100 Subject: [PATCH 39/44] Add deno support, improve node support --- .gitignore | 1 + deno/deno.json | 5 ++ deno/denoplatform.ts | 100 +++++++++++++++++++++++++++++++++++++ deno/main.ts | 76 +++++++++++++++++++++++++++++ node/index.js | 114 +++---------------------------------------- node/nodeplatform.js | 113 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 301 insertions(+), 108 deletions(-) create mode 100644 deno/deno.json create mode 100644 deno/denoplatform.ts create mode 100644 deno/main.ts create mode 100644 node/nodeplatform.js diff --git a/.gitignore b/.gitignore index 90e8c8487b..59ba39fe83 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ node_modules dist src/package.json node/config.js +deno/deno.lock diff --git a/deno/deno.json b/deno/deno.json new file mode 100644 index 0000000000..8fbaee28f0 --- /dev/null +++ b/deno/deno.json @@ -0,0 +1,5 @@ +{ + "tasks": { + "start": "deno run --allow-net --unstable main.ts" + } +} diff --git a/deno/denoplatform.ts b/deno/denoplatform.ts new file mode 100644 index 0000000000..6ea617d9d9 --- /dev/null +++ b/deno/denoplatform.ts @@ -0,0 +1,100 @@ +import { + platformAPI, + + NodeJSUDP, + NodeJSUDPRemoteInfo, +} from '../src/worker-neo.js' + +platformAPI.connect = async (address, port) => { + const tcpSocket = await Deno.connect({ + hostname: address, + port: port, + transport: 'tcp', + }); + + return { + // A ReadableStream Object + readable: tcpSocket.readable, + + // Contains functions to write to a TCP stream + writable: tcpSocket.writable, + + // Handles socket close + // Deno does not have a onclose callback! + closed: new Promise((resolve, reject) => {}) + }; +}; + +platformAPI.newWebSocket = (url) => new WebSocket(url); + +// deno-lint-ignore require-await +platformAPI.associate = async (isIPv6) => { + const family = isIPv6 ? 'IPv6' : 'IPv4'; + + const UDPSocket = Deno.listenDatagram({ + transport: 'udp', + port: 0, + }); + + let messageHandler: null | ((msg: Uint8Array, rinfo: NodeJSUDPRemoteInfo) => void) = null; + let errorHandler: null | ((err: Error) => void) = null; + + function receivingLoop() { + UDPSocket.receive().then(([buffer, from]) => { + // We only support UDP datagram here + const remoteAddress = from; + + if (messageHandler) { + messageHandler(buffer, { + address: remoteAddress.hostname, + family: family, + port: remoteAddress.port, + size: buffer.byteLength + }); + } + + // Receive more messages + receivingLoop(); + }).catch((err) => { + if (errorHandler) { + errorHandler(err); + } + }); + } + receivingLoop(); + + return { + send: async (datagram, offset, length, port, address, sendDoneCallback) => { + const addr: Deno.Addr = { + transport: 'udp', + hostname: address, + port + }; + + const buffer = new Uint8Array(datagram, offset, length); + + try { + const bytesSent = await UDPSocket.send(buffer, addr); + sendDoneCallback(null, bytesSent); + } catch (err) { + sendDoneCallback(err, 0); + if (errorHandler) { + errorHandler(err); + } + } + }, + close: () => { + UDPSocket.close(); + }, + onmessage: (handler) => { + messageHandler = handler; + }, + onerror: (handler) => { + errorHandler = handler; + } + } as NodeJSUDP; +} + +export function onDenoStart() { + +} \ No newline at end of file diff --git a/deno/main.ts b/deno/main.ts new file mode 100644 index 0000000000..d55196376d --- /dev/null +++ b/deno/main.ts @@ -0,0 +1,76 @@ +import {globalConfig, vlessOverWSHandler, getVLESSConfig} from '../src/worker-neo.js' +import {onDenoStart} from './denoplatform.ts' +onDenoStart(); + +const portString = Deno.env.get("PORT") || '8000'; +Deno.serve({port: Number(portString)}, (req) => { + if (req.headers.get("upgrade") === "websocket") { + const upgradeResult = Deno.upgradeWebSocket(req); + upgradeResult.socket.binaryType = 'arraybuffer'; + upgradeResult.socket.onopen = () => { + const earlyData = req.headers.get('sec-websocket-protocol'); + vlessOverWSHandler(upgradeResult.socket, earlyData || ''); + }; + return upgradeResult.response; + } + + + const reqURL = new URL(req.url); + const hostname = reqURL.hostname; + const path_config = '/' + globalConfig.userID; + const path_qrcode = '/' + globalConfig.userID + '.html'; + console.log(reqURL.pathname); + switch (reqURL.pathname) { + case path_config: + return new Response(getVLESSConfig(hostname), { + status: 200, + headers: { + 'content-type': 'text/plain;charset=UTF-8', + }, + }); + case path_qrcode: { + const vlessMain = `vless://${globalConfig.userID}@${hostname}:443?encryption=none&security=tls&sni=${hostname}&fp=randomized&type=ws&host=${hostname}&path=%2F%3Fed%3D2048#${hostname}` + const htmlContent = ` + + + + + + + +
${vlessMain}
+
+ + + + `; + + return new Response(htmlContent, { + status: 200, + headers: { + 'content-type': 'text/html; charset=utf-8', + }, + }); + } + case '/': + return new Response('Hello from the HTTP server!', { + status: 200, + headers: { + 'content-type': 'text/html; charset=utf-8', + }, + }); + default: + return new Response('Not found! (Code 404)', { + status: 404 + }); + } +}); diff --git a/node/index.js b/node/index.js index 16263ee93a..95fe948827 100644 --- a/node/index.js +++ b/node/index.js @@ -3,11 +3,12 @@ // To run, first run "setup.sh", then "node index.js" import http from 'http'; -import net from 'net'; import WebSocket from 'ws'; -import {createSocket as createUDPSocket} from 'dgram'; -import {globalConfig, platformAPI, setConfigFromEnv, vlessOverWSHandler, getVLESSConfig} from '../src/worker-with-socks5-experimental.js'; +import {globalConfig, setConfigFromEnv, vlessOverWSHandler, getVLESSConfig} from '../src/worker-neo.js'; +import {onNodeStart} from './nodeplatform.js'; + +onNodeStart(); // Create an HTTP server const server = http.createServer((req, res) => { @@ -32,8 +33,8 @@ wss.on('connection', (ws, req) => { vlessOverWSHandler(ws, req.headers['sec-websocket-protocol'] || ''); }); -// Start the server on port 8080 -server.listen(8080); +// Start the server on port 8000 +server.listen(8000); function buf2hex(buffer) { // buffer is an ArrayBuffer return [...new Uint8Array(buffer)] @@ -41,109 +42,6 @@ function buf2hex(buffer) { // buffer is an ArrayBuffer .join(' '); } -/** - * Portable function for creating a outbound TCP connections. - * Has to be "async" because some platforms open TCP connection asynchronously. - * - * @param {string} address The remote address to connect to. - * @param {number} port The remote port to connect to. - * @returns {object} The wrapped TCP connection, to be compatible with Cloudflare Workers - */ -platformAPI.connect = async (address, port) => { - const socket = net.createConnection(port, address); - - let readableStreamCancel = false; - const readableStream = new ReadableStream({ - start(controller) { - socket.on('data', (data) => { - if (readableStreamCancel) { - return; - } - controller.enqueue(data); - }); - - socket.on('close', () => { - socket.destroy(); - if (readableStreamCancel) { - return; - } - controller.close(); - }); - }, - - pull(controller) { - // if ws can stop read if stream is full, we can implement backpressure - // https://streams.spec.whatwg.org/#example-rs-push-backpressure - }, - cancel(reason) { - // 1. pipe WritableStream has error, this cancel will called, so ws handle server close into here - // 2. if readableStream is cancel, all controller.close/enqueue need skip, - // 3. but from testing controller.error still work even if readableStream is cancel - if (readableStreamCancel) { - return; - } - readableStreamCancel = true; - socket.destroy(); - } - }); - - const onSocketCloses = new Promise((resolve, reject) => { - socket.on('close', (err) => { - if (err) { - reject(socket.errored); - } else { - resolve(); - } - }); - - socket.on('error', (err) => { - reject(err); - }); - }); - - return { - // A ReadableStream Object - readable: readableStream, - - // Contains functions to write to a TCP stream - writable: { - getWriter: () => { - return { - write: (data) => { - socket.write(data); - }, - releaseLock: () => { - // console.log('Dummy writer.releaseLock()'); - } - }; - } - }, - - // Handles socket close - closed: onSocketCloses - }; -}; - -platformAPI.newWebSocket = (url) => new WebSocket(url); - -platformAPI.associate = async (isIPv6) => { - const UDPSocket = createUDPSocket(isIPv6 ? 'udp6' : 'udp4'); - return { - send: (datagram, offset, length, port, address, sendDoneCallback) => { - UDPSocket.send(datagram, offset, length, port, address, sendDoneCallback); - }, - close: () => { - UDPSocket.close(); - }, - onmessage: (handler) => { - UDPSocket.on('message', handler); - }, - onerror: (handler) => { - UDPSocket.on('error', handler); - } - }; -} - async function loadModule() { try { const customConfig = await import('./config.js'); diff --git a/node/nodeplatform.js b/node/nodeplatform.js new file mode 100644 index 0000000000..8773def419 --- /dev/null +++ b/node/nodeplatform.js @@ -0,0 +1,113 @@ +import {platformAPI} from '../src/worker-neo.js'; + +import net from 'net'; +import WebSocket from 'ws'; +import {createSocket as createUDPSocket} from 'dgram'; + + +/** + * Portable function for creating a outbound TCP connections. + * Has to be "async" because some platforms open TCP connection asynchronously. + * + * @param {string} address The remote address to connect to. + * @param {number} port The remote port to connect to. + * @returns {object} The wrapped TCP connection, to be compatible with Cloudflare Workers + */ +platformAPI.connect = async (address, port) => { + const socket = net.createConnection(port, address); + + let readableStreamCancel = false; + const readableStream = new ReadableStream({ + start(controller) { + socket.on('data', (data) => { + if (readableStreamCancel) { + return; + } + controller.enqueue(data); + }); + + socket.on('close', () => { + socket.destroy(); + if (readableStreamCancel) { + return; + } + controller.close(); + }); + }, + + pull(controller) { + // if ws can stop read if stream is full, we can implement backpressure + // https://streams.spec.whatwg.org/#example-rs-push-backpressure + }, + cancel(reason) { + // 1. pipe WritableStream has error, this cancel will called, so ws handle server close into here + // 2. if readableStream is cancel, all controller.close/enqueue need skip, + // 3. but from testing controller.error still work even if readableStream is cancel + if (readableStreamCancel) { + return; + } + readableStreamCancel = true; + socket.destroy(); + } + }); + + const onSocketCloses = new Promise((resolve, reject) => { + socket.on('close', (err) => { + if (err) { + reject(socket.errored); + } else { + resolve(); + } + }); + + socket.on('error', (err) => { + reject(err); + }); + }); + + return { + // A ReadableStream Object + readable: readableStream, + + // Contains functions to write to a TCP stream + writable: { + getWriter: () => { + return { + write: (data) => { + socket.write(data); + }, + releaseLock: () => { + // console.log('Dummy writer.releaseLock()'); + } + }; + } + }, + + // Handles socket close + closed: onSocketCloses + }; +}; + +platformAPI.newWebSocket = (url) => new WebSocket(url); + +platformAPI.associate = async (isIPv6) => { + const UDPSocket = createUDPSocket(isIPv6 ? 'udp6' : 'udp4'); + return { + send: (datagram, offset, length, port, address, sendDoneCallback) => { + UDPSocket.send(datagram, offset, length, port, address, sendDoneCallback); + }, + close: () => { + UDPSocket.close(); + }, + onmessage: (handler) => { + UDPSocket.on('message', handler); + }, + onerror: (handler) => { + UDPSocket.on('error', handler); + } + }; +} + +export function onNodeStart() { + +} \ No newline at end of file From dd3354852ce5dacabdab5bc2c79f7111308e0fd9 Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Mon, 4 Dec 2023 04:37:41 +1100 Subject: [PATCH 40/44] Fix IPv6 UDP on deno --- deno/denoplatform.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/deno/denoplatform.ts b/deno/denoplatform.ts index 6ea617d9d9..892f306bcc 100644 --- a/deno/denoplatform.ts +++ b/deno/denoplatform.ts @@ -34,6 +34,7 @@ platformAPI.associate = async (isIPv6) => { const UDPSocket = Deno.listenDatagram({ transport: 'udp', port: 0, + hostname: isIPv6 ? '[::]' : '0.0.0.0', }); let messageHandler: null | ((msg: Uint8Array, rinfo: NodeJSUDPRemoteInfo) => void) = null; From c2334b26e03ec6f2873febe414f5abcd7c57b739 Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Thu, 7 Dec 2023 02:32:08 +1100 Subject: [PATCH 41/44] Fix IPv6 inbound --- src/worker-neo.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/worker-neo.js b/src/worker-neo.js index fe32ecd869..6dcab708a3 100644 --- a/src/worker-neo.js +++ b/src/worker-neo.js @@ -1160,8 +1160,7 @@ function processVlessHeader( const uint16_val = ipv6Bytes[i*2] << 8 | ipv6Bytes[i*2+1]; ipv6.push(uint16_val.toString(16)); } - addressValue = ipv6.join(':'); - // seems no need add [] for ipv6 + addressValue = '[' + ipv6.join(':') + ']'; break; } default: From 41a7e7597a1d0ba348dd4811d2bda26c89c87c23 Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Wed, 13 Dec 2023 04:37:46 +1100 Subject: [PATCH 42/44] Move type definition, better outbound impl --- src/worker-neo.d.ts | 242 +++++++++++++++ src/worker-neo.js | 725 ++++++++++++++++---------------------------- 2 files changed, 496 insertions(+), 471 deletions(-) create mode 100644 src/worker-neo.d.ts diff --git a/src/worker-neo.d.ts b/src/worker-neo.d.ts new file mode 100644 index 0000000000..1ed93d2937 --- /dev/null +++ b/src/worker-neo.d.ts @@ -0,0 +1,242 @@ +/** + * Defines a Cloudflare Worker compatible TCP connection. + */ +export interface CloudflareTCPConnection { + readable: ReadableStream, + writable: WritableStream, + closed: Promise, +} + +export interface NodeJSUDPRemoteInfo { + address: string, + family: 'IPv4' | 'IPv6', + port: number, + size: number, +} + +/** + * Defines a NodeJS compatible UDP API. + */ +export interface NodeJSUDP { + send: (datagram: any, offset: number, length: number, port: number, address: string, sendDoneCallback: (err: Error | null, bytes: number) => void) => void, + close: () => void, + onmessage: (handler: (msg: Uint8Array, rinfo: NodeJSUDPRemoteInfo) => void) => void, + onerror: (handler: (err: Error) => void) => void, +} + +/** + * The base type of all outbound definitions. + */ +export interface Outbound { + protocol: string, + settings?: {} +} + +/** + * Represents a local outbound. + */ +export interface FreedomOutbound extends Outbound { + protocol: 'freedom', + settings: undefined +} + +export type PortMap = {[key: number]: number}; + +/** + * Represents a forwarding outbound. + * First, the destination port of the request will be mapped according to portMap. + * If none matches, the destination port remains unchanged. + * Then, the request stream will be redirected to the given address. + */ +export interface ForwardOutbound extends Outbound { + protocol: 'forward', + address: string, + portMap?: PortMap +} + +export interface Socks5Server { + address: string, + port: number, + users?: { + user: string, + pass: string, + }[] +} + +/** + * Represents a socks5 outbound. + */ +export interface Socks5Outbound extends Outbound { + protocol: 'socks', + settings: { + servers: Socks5Server[] + } +} + +export interface VlessServer { + address: string, + port: number, + users: { + id: string, + }[] +} + +/** + * Represents a Vless WebSocket outbound. + */ +export interface VlessWsOutbound { + protocol: 'vless', + settings: { + vnext: VlessServer[] + }, + streamSettings: StreamSettings +} + +export interface StreamSettings { + network: 'ws', + security: 'none' | 'tls', + wsSettings?: { + path?: string, + headers?: { + Host: string + } + } + tlsSettings?: { + serverName: string, + allowInsecure: boolean, + } +} + +export interface OutboundContext { + enforceUDP: boolean, + forwardDNS: boolean, + log: LogFunction, + firstChunk: Uint8Array, +} + +export type OutboundHanderReturns = Promise<{ + readableStream: ReadableStream, + writableStream: WritableStream, +}>; + +export type OutboundHandler = (vlessRequest: ProcessedVlessHeader, context: OutboundContext) => OutboundHanderReturns; + +export interface OutboundInstance { + protocol: string, + handler: OutboundHandler, +} + +export interface ForwardInstanceArgs { + proxyServer: string, + portMap?: PortMap, +} + +export interface Socks5InstanceArgs { + address: string, + port: number, + user?: string, + pass?: string, +} + +export interface VlessInstanceArgs { + address: string, + port: number, + uuid: string, + streamSettings: StreamSettings, +} + +export interface ProcessedVlessHeader { + addressRemote: string; + addressType: number; + portRemote: number; + rawDataIndex: number; + vlessVersion: Uint8Array; + isUDP: boolean; +} + +export type LogFunction = (...args: any[]) => void; + +// API starts ------------------------------------------------------------------------------------ + +export interface PlatformAPI { + /** + * A wrapper for the TCP API, should return a Cloudflare Worker compatible socket. + * The result is wrapped in a Promise, as in some platforms, the socket creation is async. + * See: https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/ + */ + connect: (host: string, port: number) => Promise, + + /** + * A wrapper for the Websocket API. + */ + newWebSocket: (url: string) => WebSocket, + + /** + * A wrapper for the UDP API, should return a NodeJS compatible UDP socket. + * The result is wrapped in a Promise, as in some platforms, the socket creation is async. + */ + associate: null | ((isIPv6: boolean) => Promise), + + /** + * An optional processor to process the incoming WebSocket request and its response. + * The response processor may need to be created multiple times before truly utilization. + * @type { } + */ + processor: null | ((logger: LogFunction) => { + request: TransformStream, + response: () => TransformStream, + }), +} + +export interface GlobalConfig { + /** The UUID used in Vless authentication. */ + userID: string, + + /** Time to wait before an outbound Websocket connection is considered timeout, in ms. */ + openWSOutboundTimeout: number, + + /** + * Since Cloudflare Worker does not support UDP outbound, we may try DNS over TCP. + * Set to an empty string to disable UDP to TCP forwarding for DNS queries. + */ + dnsTCPServer: string, + + /** The order controls where to send the traffic after the previous one fails. */ + outbounds: Outbound[], +} + +declare const globalConfig: GlobalConfig; +declare const platformAPI: PlatformAPI; + +/** + * Setup the config (uuid & outbounds) from environmental variables. + * This is the simplest case and should be preferred where possible. + */ +declare function setConfigFromEnv(env: { + UUID?: string, + + /** e.g. 114.51.4.0 */ + PROXYIP?: string, + + /** e.g. {443:8443} */ + PORTMAP?: string, + + /** e.g. vless://uuid@domain.name:port?type=ws&security=tls */ + VLESS?: string, + + /** e.g. user:pass@host:port or host:port */ + SOCKS5?: string, +}): void; + +declare function getVLESSConfig(hostName?: string): string; + +/** + * If you use this file as an ES module, you call this function whenever your Websocket server accepts a new connection. + * @param webSocket The established websocket connection, must be an accepted. + * @param earlyDataHeader for ws 0rtt, an optional field "sec-websocket-protocol" in the request header + * may contain some base64 encoded data. + * @returns status code + */ +declare function vlessOverWSHandler(webSocket: WebSocket, earlyDataHeader: string): number; + +declare function redirectConsoleLog(logServer: string, instanceId: string): void; diff --git a/src/worker-neo.js b/src/worker-neo.js index 6dcab708a3..cf478c3fbb 100644 --- a/src/worker-neo.js +++ b/src/worker-neo.js @@ -1,27 +1,23 @@ -// version base on commit 2b9927a1b12e03f8ad4731541caee2bc5c8f2e8e, time is 2023-06-22 15:09:37 UTC. +// @ts-check // How to generate your own UUID: // [Windows] Press "Win + R", input cmd and run: Powershell -NoExit -Command "[guid]::NewGuid()" // [Linux] Run uuidgen in terminal + +/** @type {import("./worker-neo").GlobalConfig} */ export const globalConfig = { userID: 'd342d11e-d424-4583-b36e-524ab1f0afa4', - /** - * Time to wait before an outbound Websocket connection is considered timeout, in ms. - * */ + /** Time to wait before an outbound Websocket connection is considered timeout, in ms. */ openWSOutboundTimeout: 10000, /** * Since Cloudflare Worker does not support UDP outbound, we may try DNS over TCP. * Set to an empty string to disable UDP to TCP forwarding for DNS queries. - * @type {string} */ dnsTCPServer: "8.8.4.4", - /** - * The order controls where to send the traffic after the previous one fails. - * @type {Outbound[]} - */ + /** The order controls where to send the traffic after the previous one fails. */ outbounds: [ { protocol: "freedom" // Compulsory, outbound locally. @@ -30,171 +26,179 @@ export const globalConfig = { }; /** - * Defines a Cloudflare Worker compatible TCP connection. - * @typedef {Object} CloudflareTCPConnection - * @property {ReadableStream} readable the readable side of the TCP socket. - * @property {WritableStream} writable the writable side of the TCP socket, accepts Uint8Array only. - * @property {Promise} closed - This promise is resolved when the socket is closed and is rejected if the socket encounters an error. - */ - -/** - * @typedef {Object} NodeJSUDPRemoteInfo - * @property {string} address - * @property {'IPv4' | 'IPv6'} family - * @property {number} port - * @property {number} size - */ - -/** - * Defines a NodeJS compatible UDP API. - * @typedef {Object} NodeJSUDP - * @property {(datagram: any, offset: number, length: number, port: number, address: string, sendDoneCallback: (err: Error | null, bytes: number) => void) => void} send - * @property {() => void} close - * @property {(handler: (msg: Uint8Array, rinfo: NodeJSUDPRemoteInfo) => void) => void} onmessage - * @property {(handler: (err: Error) => void) => void} onerror + * If you use this file as an ES module, you should set all fields below. + * @type {import("./worker-neo").PlatformAPI} */ - -// If you use this file as an ES module, you should set all fields below. export const platformAPI = { - /** - * A wrapper for the TCP API, should return a Cloudflare Worker compatible socket. - * The result is wrapped in a Promise, as in some platforms, the socket creation is async. - * See: https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/ - * @type {(host: string, port: number) => Promise} - */ + // @ts-expect-error connect: null, - /** - * A wrapper for the Websocket API. - * @type {(url: string) => WebSocket} returns a WebSocket, should be compatile with the standard WebSocket API. - */ + // @ts-expect-error newWebSocket: null, - /** - * A wrapper for the UDP API, should return a NodeJS compatible UDP socket. - * The result is wrapped in a Promise, as in some platforms, the socket creation is async. - * @type {(isIPv6: boolean) => Promise - * } - */ associate: null, - /** - * An optional processor to process the incoming WebSocket request and its response. - * The response processor may need to be created multiple times before truly utilization. - * @type { null | ((logger: LogFunction) => { - * request: TransformStream, - * response: () => TransformStream, - * })} - */ processor: null, } /** - * The base type of all outbound definitions. - * @typedef {{ - * protocol: string, - * settings?: {} - * }} Outbound + * @param {WritableStream} writableStream + * @param {Uint8Array} firstChunk */ +async function writeFirstChunk(writableStream, firstChunk) { + const writer = writableStream.getWriter(); + await writer.write(firstChunk); // First write, normally is tls client hello + writer.releaseLock(); +} -/** - * Represents a local outbound. - * @typedef {Outbound & { - * protocol: 'freedom', - * settings: undefined, - * }} FreedomOutbound - */ +/** @type {Object. import('./worker-neo').OutboundHandler>} */ +const outboundImpl = { + 'freedom': () => async (vlessRequest, context) => { + if (context.enforceUDP) { + // TODO: Check what will happen if addressType == VlessAddrType.DomainName and that domain only resolves to a IPv6 + const udpClient = await /** @type {NonNullable} */(platformAPI.associate)(vlessRequest.addressType == VlessAddrType.IPv6); + const writableStream = makeWritableUDPStream(udpClient, vlessRequest.addressRemote, vlessRequest.portRemote, context.log); + const readableStream = makeReadableUDPStream(udpClient, context.log); + context.log(`Connected to UDP://${vlessRequest.addressRemote}:${vlessRequest.portRemote}`); + await writeFirstChunk(writableStream, context.firstChunk); + return { + readableStream, + writableStream: /** @type {WritableStream} */ (writableStream) + }; + } -/** - * @typedef {{[key: number]: number}} PortMap - */ + let addressTCP = vlessRequest.addressRemote; + if (context.forwardDNS) { + addressTCP = globalConfig.dnsTCPServer; + context.log(`Redirect DNS request sent to UDP://${vlessRequest.addressRemote}:${vlessRequest.portRemote}`); + } -/** - * Represents a forwarding outbound. - * First, the destination port of the request will be mapped according to portMap. - * If none matches, the destination port remains unchanged. - * Then, the request stream will be redirected to the given address. - * @typedef {Outbound & { - * protocol: 'forward', - * address: string, - * portMap?: PortMap - * }} ForwardOutbound - */ + const tcpSocket = await platformAPI.connect(addressTCP, vlessRequest.portRemote); + tcpSocket.closed.catch(error => context.log('[freedom] tcpSocket closed with error: ', error.message)); + context.log(`Connecting to tcp://${addressTCP}:${vlessRequest.portRemote}`); + await writeFirstChunk(tcpSocket.writable, context.firstChunk); + return { + readableStream: tcpSocket.readable, + writableStream: tcpSocket.writable + }; + }, -/** - * @typedef {{ - * address: string, - * port: number, - * users?: { - * user: string, - * pass: string, - * }[] - * }} Socks5Server - */ + 'forward': (/** @type {import('./worker-neo').ForwardInstanceArgs} */ args) => async (vlessRequest, context) => { + let portDest = vlessRequest.portRemote; + if (typeof args.portMap === "object" && args.portMap[vlessRequest.portRemote] !== undefined) { + portDest = args.portMap[vlessRequest.portRemote]; + } -/** - * Represents a socks5 outbound. - * @typedef {Outbound & { - * protocol: 'socks', - * settings: { - * servers: Socks5Server[] - * } - * }} Socks5Outbound - */ + const tcpSocket = await platformAPI.connect(args.proxyServer, portDest); + tcpSocket.closed.catch(error => context.log('[forward] tcpSocket closed with error: ', error.message)); + context.log(`Forwarding tcp://${vlessRequest.addressRemote}:${vlessRequest.portRemote} to ${args.proxyServer}:${portDest}`); + await writeFirstChunk(tcpSocket.writable, context.firstChunk); + return { + readableStream: tcpSocket.readable, + writableStream: tcpSocket.writable + }; + }, -/** - * @typedef {{ - * address: string, - * port: number, - * users?: { - * id: string, - * }[] - * }} VlessServer - */ + // TODO: known problem, if we send an unreachable request to a valid socks5 server, it will wait indefinitely + // TODO: Add support for proxying UDP via socks5 on runtimes that support UDP outbound + 'socks': (/** @type {import('./worker-neo').Socks5InstanceArgs} */ socks) => async (vlessRequest, context) => { + const tcpSocket = await platformAPI.connect(socks.address, socks.port); + tcpSocket.closed.catch(error => context.log('[socks] tcpSocket closed with error: ', error.message)); + context.log(`Connecting to ${vlessRequest.isUDP ? 'UDP' : 'TCP'}://${vlessRequest.addressRemote}:${vlessRequest.portRemote} via socks5 ${socks.address}:${socks.port}`); + await socks5Connect(tcpSocket, socks.user, socks.pass, vlessRequest.addressType, vlessRequest.addressRemote, vlessRequest.portRemote, context.log); + await writeFirstChunk(tcpSocket.writable, context.firstChunk); + return { + readableStream: tcpSocket.readable, + writableStream: tcpSocket.writable + }; + }, -/** - * Represents a Vless WebSocket outbound. - * @typedef {Outbound & { - * protocol: 'vless', - * settings: { - * vnext: VlessServer[] - * }, - * streamSettings: StreamSettings - * }} VlessWsOutbound - */ + /** + * Start streaming traffic to a remote vless server. + * The first message must contain the query header plus part of the payload! + * The vless server responds to it with a response header plus part of the response from the destination. + * After the first message exchange, in the case of TCP, the streams in both directions carry raw TCP streams. + * Fragmentation won't cause any problem after the first message exchange. + * In the case of UDP, a 16-bit big-endian length field is prepended to each UDP datagram and then send through the streams. + * The first message exchange still applies. + */ + 'vless': (/** @type {import('./worker-neo').VlessInstanceArgs} */ vless) => async (vlessRequest, context) => { + checkVlessConfig(vless.address, vless.streamSettings); -/** - * @typedef {Object} StreamSettings - * @property {'ws'} network - * @property {'none' | 'tls'} security - * @property {{ - * path?: string, - * headers ?: { - * Host: string - * } - * }} wsSettings - * @property {{ - * serverName: string, - * allowInsecure: boolean, - * }} [tlsSettings] - */ + let wsURL = vless.streamSettings.security === 'tls' ? 'wss://' : 'ws://'; + wsURL = wsURL + vless.address + ':' + vless.port; + if (vless.streamSettings.wsSettings && vless.streamSettings.wsSettings.path) { + wsURL = wsURL + vless.streamSettings.wsSettings.path; + } + context.log(`Connecting to ${vlessRequest.isUDP ? 'UDP' : 'TCP'}://${vlessRequest.addressRemote}:${vlessRequest.portRemote} via vless ${wsURL}`); -/** - * @typedef {{ - * protocol: string, - * address?: string, - * port?: number, - * portMap?: PortMap, - * user?: string, - * pass?: string, - * streamSettings?: StreamSettings, - * }} OutboundInstance - */ + const wsToVlessServer = platformAPI.newWebSocket(wsURL); + /** @type {Promise} */ + const openPromise = new Promise((resolve, reject) => { + wsToVlessServer.onopen = () => resolve(); + wsToVlessServer.onclose = (event) => + reject(new Error(`Closed with code ${event.code}, reason: ${event.reason}`)); + wsToVlessServer.onerror = (error) => reject(error); + setTimeout(() => { + reject(new Error("Cannot open Websocket connection, open connection timeout")); + }, globalConfig.openWSOutboundTimeout); + }); + + // Wait for the connection to open + try { + await openPromise; + } catch (err) { + wsToVlessServer.close(); + throw new err; + } + + /** @type {WritableStream} */ + const writableStream = new WritableStream({ + async write(chunk, controller) { + wsToVlessServer.send(chunk); + }, + close() { + context.log(`Vless Websocket closed`); + }, + abort(reason) { + console.error(`Vless Websocket aborted`, reason); + }, + }); + + /** @type {(firstChunk : Uint8Array) => Uint8Array} */ + const headerStripper = (firstChunk) => { + if (firstChunk.length < 2) { + throw new Error('Too short vless response'); + } + + const responseVersion = firstChunk[0]; + const addtionalBytes = firstChunk[1]; + + if (responseVersion > 0) { + context.log('Warning: unexpected vless version: ${responseVersion}, only supports 0.'); + } + + if (addtionalBytes > 0) { + context.log('Warning: ignored ${addtionalBytes} byte(s) of additional information in the response.'); + } + + return firstChunk.slice(2 + addtionalBytes); + }; + + const readableStream = makeReadableWebSocketStream(wsToVlessServer, null, headerStripper, context.log); + const vlessReqHeader = makeVlessReqHeader(vlessRequest.isUDP ? VlessCmd.UDP : VlessCmd.TCP, vlessRequest.addressType, vlessRequest.addressRemote, vlessRequest.portRemote, vless.uuid); + // Send the first packet (header + rawClientData), then strip the response header with headerStripper + await writeFirstChunk(writableStream, joinUint8Array(vlessReqHeader, context.firstChunk)); + return { + readableStream, + writableStream + }; + } +}; /** * Foreach globalConfig.outbounds, start with {index: 0, serverIndex: 0} * @param {{index: number, serverIndex: number}} curPos - * @returns {OutboundInstance?} */ function getOutbound(curPos) { if (curPos.index >= globalConfig.outbounds.length) { @@ -205,53 +209,59 @@ function getOutbound(curPos) { const outbound = globalConfig.outbounds[curPos.index]; let serverCount = 0; - /** @type {OutboundInstance} */ - const retVal = { protocol: outbound.protocol }; + let outboundHandlerArgs; switch (outbound.protocol) { case 'freedom': + outboundHandlerArgs = undefined; break; case 'forward': { - /** @type {ForwardOutbound} */ + /** @type {import("./worker-neo").ForwardOutbound} */ // @ts-ignore: type casting const forwardOutbound = outbound; - retVal.address = forwardOutbound.address; - retVal.portMap = forwardOutbound.portMap; + outboundHandlerArgs = /** @type {import("./worker-neo").ForwardInstanceArgs} */ ({ + proxyServer: forwardOutbound.address, + portMap: forwardOutbound.portMap, + }); break; } case 'socks': { - /** @type {Socks5Outbound} */ + /** @type {import("./worker-neo").Socks5Outbound} */ // @ts-ignore: type casting const socks5Outbound = outbound; const servers = socks5Outbound.settings.servers; serverCount = servers.length; const curServer = servers[curPos.serverIndex]; - retVal.address = curServer.address; - retVal.port = curServer.port; + + outboundHandlerArgs = /** @type {import("./worker-neo").Socks5InstanceArgs} */ ({ + address: curServer.address, + port: curServer.port, + }); if (curServer.users && curServer.users.length > 0) { const firstUser = curServer.users[0]; - retVal.user = firstUser.user; - retVal.pass = firstUser.pass; + outboundHandlerArgs.user = firstUser.user; + outboundHandlerArgs.pass = firstUser.pass; } break; } case 'vless': { - /** @type {VlessWsOutbound} */ + /** @type {import("./worker-neo").VlessWsOutbound} */ // @ts-ignore: type casting const vlessOutbound = outbound; const servers = vlessOutbound.settings.vnext; serverCount = servers.length; const curServer = servers[curPos.serverIndex]; - retVal.address = curServer.address; - retVal.port = curServer.port; - - retVal.pass = curServer.users[0].id; - retVal.streamSettings = vlessOutbound.streamSettings; + outboundHandlerArgs = /** @type {import("./worker-neo").VlessInstanceArgs} */ ({ + address: curServer.address, + port: curServer.port, + uuid: curServer.users[0].id, + streamSettings: vlessOutbound.streamSettings, + }); break; } @@ -266,7 +276,10 @@ function getOutbound(curPos) { curPos.index++; } - return retVal; + return { + protocol: outbound.protocol, + handler: outboundImpl[outbound.protocol](outboundHandlerArgs), + }; } /** @@ -283,17 +296,7 @@ function canOutboundUDPVia(protocolName) { return false; } -/** - * Setup the config (uuid & outbounds) from environmental variables. - * This is the simplest case and should be preferred where possible. - * @param {{ - * UUID?: string, - * PROXYIP?: string, // E.g. 114.51.4.0 - * PORTMAP?: string, // E.g. {443:8443} - * VLESS?: string, // E.g. vless://uuid@domain.name:port?type=ws&security=tls - * SOCKS5?: string // E.g. user:pass@host:port or host:port - * }} env - */ +/** @type {import("./worker-neo").setConfigFromEnv} */ export function setConfigFromEnv(env) { globalConfig.userID = env.UUID || globalConfig.userID; @@ -304,7 +307,7 @@ export function setConfigFromEnv(env) { ]; if (env.PROXYIP) { - /** @type {ForwardOutbound} */ + /** @type {import("./worker-neo").ForwardOutbound} */ const forward = { protocol: "forward", address: env.PROXYIP @@ -330,7 +333,7 @@ export function setConfigFromEnv(env) { descriptiveText } = parseVlessString(env.VLESS); - /** @type {VlessServer} */ + /** @type {import("./worker-neo").VlessServer} */ const vless = { "address": remoteHost, "port": remotePort, @@ -342,7 +345,7 @@ export function setConfigFromEnv(env) { }; // TODO: Validate vless here - /** @type {StreamSettings} */ + /** @type {import("./worker-neo").StreamSettings} */ const streamSettings = { "network": queryParams['type'], "security": queryParams['security'], @@ -364,7 +367,7 @@ export function setConfigFromEnv(env) { }; } - /** @type {VlessWsOutbound} */ + /** @type {import("./worker-neo").VlessWsOutbound} */ const vlessOutbound = { protocol: "vless", settings: { @@ -391,13 +394,13 @@ export function setConfigFromEnv(env) { port, } = socks5AddressParser(env.SOCKS5); - /** @type {Socks5Server} */ + /** @type {import("./worker-neo").Socks5Server} */ const socks = { "address": hostname, "port": port } - if (username) { + if (typeof username !== 'undefined' && typeof password !== 'undefined') { socks.users = [ // We only support one user per socks server { "user": username, @@ -476,10 +479,7 @@ export default { }, }; -/** - * @param {string} logServer URL of the log server - * @param {string} instanceId a UUID representing each instance - */ +/** @type {import("./worker-neo").redirectConsoleLog} */ export function redirectConsoleLog(logServer, instanceId) { let logID = 0; const oldConsoleLog = console.log; @@ -519,28 +519,17 @@ try { console.log('Not on Cloudflare Workers!'); } -/** - * @typedef {(info: string, event?: string) => void} LogFunction - */ - -/** - * If you use this file as an ES module, you call this function whenever your Websocket server accepts a new connection. - * - * @param {WebSocket} webSocket The established websocket connection to the client, must be an accepted - * @param {string} earlyDataHeader for ws 0rtt, an optional field "sec-websocket-protocol" in the request header - * may contain some base64 encoded data. - * @returns {number} status code - */ +/** @type {import('./worker-neo').vlessOverWSHandler} */ export function vlessOverWSHandler(webSocket, earlyDataHeader) { let logPrefix = ''; - /** @type {LogFunction} */ - const log = (info, event) => { - console.log(`[${logPrefix}] ${info}`, event || ''); + /** @type {import('./worker-neo').LogFunction} */ + const log = (...args) => { + console.log(`[${logPrefix}]`, args); }; // for ws 0rtt - const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader); - if (error !== null) { + const earlyData = base64ToUint8Array(earlyDataHeader); + if (!(earlyData instanceof Uint8Array)) { return 500; } @@ -555,6 +544,7 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { vlessTrafficData = readableWebSocketStream.pipeThrough(processor.request); } + /** @type {import('./worker-neo').ProcessedVlessHeader | null} */ let vlessHeader = null; // This source stream only contains raw traffic from the client @@ -567,12 +557,14 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { if (vlessHeader) { controller.enqueue(chunk); } else { - vlessHeader = processVlessHeader(chunk, globalConfig.userID); - if (vlessHeader.hasError) { - controller.error(`Failed to process Vless header: ${vlessHeader.message}`); + try { + vlessHeader = processVlessHeader(chunk, globalConfig.userID); + } catch (error) { + controller.error(`Failed to process Vless header: ${error}`); controller.terminate(); return; } + const randTag = Math.round(Math.random()*1000000).toString(16).padStart(5, '0'); logPrefix = `${vlessHeader.addressRemote}:${vlessHeader.portRemote} ${randTag} ${vlessHeader.isUDP ? 'UDP' : 'TCP'}`; const firstPayloadLen = chunk.byteLength - vlessHeader.rawDataIndex; @@ -606,11 +598,13 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { return; } + const header = /** @type {NonNullable} */(vlessHeader); + // ["version", "length of additional info"] - const vlessResponseHeader = new Uint8Array([vlessHeader.vlessVersion[0], 0]); + const vlessResponseHeader = new Uint8Array([header.vlessVersion[0], 0]); // Need to ensure the outbound proxy (if any) is ready before proceeding. - remoteTrafficSink = await handleOutBound(vlessHeader, chunk, webSocket, vlessResponseHeader, vlessResponseProcessor, log); + remoteTrafficSink = await handleOutBound(header, chunk, webSocket, vlessResponseHeader, vlessResponseProcessor, log); // log('Outbound established!'); }, close() { @@ -626,21 +620,14 @@ export function vlessOverWSHandler(webSocket, earlyDataHeader) { return 101; } -/** - * @typedef {{ - * readableStream: ReadableStream, - * writableStream: WritableStream, - * }} OutboundConnection - */ - /** * Handles outbound connections. - * @param {{isUDP: boolean, addressType: number, addressRemote: string, portRemote: number}} vlessRequest + * @param {import("./worker-neo").ProcessedVlessHeader} vlessRequest * @param {Uint8Array} rawClientData The raw client data to write. * @param {WebSocket} webSocket The WebSocket to pass the remote socket to. * @param {Uint8Array} vlessResponseHeader Contains information to produce the vless response, such as the header. * @param {null | (() => TransformStream)} vlessResponseProcessor an optional TransformStream to process the Vless response. - * @param {LogFunction} log The logger function. + * @param {import('./worker-neo').LogFunction} log The logger function. * @returns a non-null fulfill indicates the success connection to the destination or the remote proxy server */ async function handleOutBound(vlessRequest, rawClientData, webSocket, vlessResponseHeader, vlessResponseProcessor, log) { @@ -657,190 +644,6 @@ async function handleOutBound(vlessRequest, rawClientData, webSocket, vlessRespo // False if we may use TCP to somehow resolve that UDP query const enforceUDP = vlessRequest.isUDP && !forwardDNS; - /** - * @param {WritableStream} writableStream - * @param {Uint8Array} firstChunk - */ - async function writeFirstChunk(writableStream, firstChunk) { - const writer = writableStream.getWriter(); - await writer.write(firstChunk); // First write, normally is tls client hello - writer.releaseLock(); - } - - /** - * @returns {Promise} - */ - async function direct() { - if (enforceUDP) { - // TODO: Check what will happen if addressType == VlessAddrType.DomainName and that domain only resolves to a IPv6 - const udpClient = await platformAPI.associate(vlessRequest.addressType == VlessAddrType.IPv6); - const writableStream = makeWritableUDPStream(udpClient, vlessRequest.addressRemote, vlessRequest.portRemote, log); - const readableStream = makeReadableUDPStream(udpClient, log); - log(`Connected to UDP://${vlessRequest.addressRemote}:${vlessRequest.portRemote}`); - await writeFirstChunk(writableStream, rawClientData); - return { - readableStream, - writableStream - }; - } - - let addressTCP = vlessRequest.addressRemote; - if (forwardDNS) { - addressTCP = globalConfig.dnsTCPServer; - log(`Redirect DNS request sent to UDP://${vlessRequest.addressRemote}:${vlessRequest.portRemote}`); - } - - const tcpSocket = await platformAPI.connect(addressTCP, vlessRequest.portRemote); - tcpSocket.closed.catch(error => log('[freedom] tcpSocket closed with error: ', error.message)); - log(`Connecting to tcp://${addressTCP}:${vlessRequest.portRemote}`); - await writeFirstChunk(tcpSocket.writable, rawClientData); - return { - readableStream: tcpSocket.readable, - writableStream: tcpSocket.writable - }; - } - - /** - * @param {string} proxyServer - * @param {PortMap} [portMap] - * @returns {Promise} - */ - async function forward(proxyServer, portMap) { - let portDest = vlessRequest.portRemote; - if (typeof portMap === "object" && portMap[vlessRequest.portRemote] !== undefined) { - portDest = portMap[vlessRequest.portRemote]; - } - - const tcpSocket = await platformAPI.connect(proxyServer, portDest); - tcpSocket.closed.catch(error => log('[forward] tcpSocket closed with error: ', error.message)); - log(`Forwarding tcp://${vlessRequest.addressRemote}:${vlessRequest.portRemote} to ${proxyServer}:${portDest}`); - await writeFirstChunk(tcpSocket.writable, rawClientData); - return { - readableStream: tcpSocket.readable, - writableStream: tcpSocket.writable - }; - } - - // TODO: known problem, if we send an unreachable request to a valid socks5 server, it will wait indefinitely - // TODO: Add support for proxying UDP via socks5 on runtimes that support UDP outbound - /** - * @param {string} address - * @param {number} port - * @param {string} user - * @param {string} pass - * @returns {Promise} - */ - async function socks5(address, port, user, pass) { - const tcpSocket = await platformAPI.connect(address, port); - tcpSocket.closed.catch(error => log('[socks] tcpSocket closed with error: ', error.message)); - log(`Connecting to ${vlessRequest.isUDP ? 'UDP' : 'TCP'}://${vlessRequest.addressRemote}:${vlessRequest.portRemote} via socks5 ${address}:${port}`); - try { - await socks5Connect(tcpSocket, user, pass, vlessRequest.addressType, vlessRequest.addressRemote, vlessRequest.portRemote, log); - } catch(err) { - log(`Socks5 outbound failed with: ${err.message}`); - return null; - } - await writeFirstChunk(tcpSocket.writable, rawClientData); - return { - readableStream: tcpSocket.readable, - writableStream: tcpSocket.writable - }; - } - - /** - * Start streaming traffic to a remote vless server. - * The first message must contain the query header plus part of the payload! - * The vless server responds to it with a response header plus part of the response from the destination. - * After the first message exchange, in the case of TCP, the streams in both directions carry raw TCP streams. - * Fragmentation won't cause any problem after the first message exchange. - * In the case of UDP, a 16-bit big-endian length field is prepended to each UDP datagram and then send through the streams. - * The first message exchange still applies. - * - * @param {string} address - * @param {number} port - * @param {string} uuid - * @param {StreamSettings} streamSettings - * @returns {Promise} - */ - async function vless(address, port, uuid, streamSettings) { - try { - checkVlessConfig(address, streamSettings); - } catch(err) { - log(`Vless outbound failed with: ${err.message}`); - return null; - } - - let wsURL = streamSettings.security === 'tls' ? 'wss://' : 'ws://'; - wsURL = wsURL + address + ':' + port; - if (streamSettings.wsSettings && streamSettings.wsSettings.path) { - wsURL = wsURL + streamSettings.wsSettings.path; - } - log(`Connecting to ${vlessRequest.isUDP ? 'UDP' : 'TCP'}://${vlessRequest.addressRemote}:${vlessRequest.portRemote} via vless ${wsURL}`); - - const wsToVlessServer = platformAPI.newWebSocket(wsURL); - const openPromise = new Promise((resolve, reject) => { - wsToVlessServer.onopen = () => resolve(); - wsToVlessServer.onclose = (code, reason) => - reject(new Error(`Closed with code ${code}, reason: ${reason}`)); - wsToVlessServer.onerror = (error) => reject(error); - setTimeout(() => { - reject({message: `Open connection timeout`}); - }, globalConfig.openWSOutboundTimeout); - }); - - // Wait for the connection to open - try { - await openPromise; - } catch (err) { - log(`Cannot open Websocket connection: ${err.message}`); - wsToVlessServer.close(); - return null; - } - - /** @type {WritableStream} */ - const writableStream = new WritableStream({ - async write(chunk, controller) { - wsToVlessServer.send(chunk); - }, - close() { - log(`Vless Websocket closed`); - }, - abort(reason) { - console.error(`Vless Websocket aborted`, reason); - }, - }); - - /** @type {(firstChunk : Uint8Array) => Uint8Array} */ - const headerStripper = (firstChunk) => { - if (firstChunk.length < 2) { - throw new Error('Too short vless response'); - } - - const responseVersion = firstChunk[0]; - const addtionalBytes = firstChunk[1]; - - if (responseVersion > 0) { - log('Warning: unexpected vless version: ${responseVersion}, only supports 0.'); - } - - if (addtionalBytes > 0) { - log('Warning: ignored ${addtionalBytes} byte(s) of additional information in the response.'); - } - - return firstChunk.slice(2 + addtionalBytes); - }; - - const readableStream = makeReadableWebSocketStream(wsToVlessServer, null, headerStripper, log); - const vlessReqHeader = makeVlessReqHeader(vlessRequest.isUDP ? VlessCmd.UDP : VlessCmd.TCP, vlessRequest.addressType, vlessRequest.addressRemote, vlessRequest.portRemote, uuid); - // Send the first packet (header + rawClientData), then strip the response header with headerStripper - await writeFirstChunk(writableStream, joinUint8Array(vlessReqHeader, rawClientData)); - return { - readableStream, - writableStream - }; - } - - /** @returns {Promise} */ async function connectAndWrite() { const outbound = getOutbound(curOutBoundPtr); if (outbound == null) { @@ -855,28 +658,27 @@ async function handleOutBound(vlessRequest, rawClientData, webSocket, vlessRespo return null; } - switch (outbound.protocol) { - case 'freedom': - return await direct(); - case 'forward': - return await forward(outbound.address, outbound.portMap); - case 'socks': - return await socks5(outbound.address, outbound.port, outbound.user, outbound.pass); - case 'vless': - return await vless(outbound.address, outbound.port, outbound.pass, outbound.streamSettings); + try { + return await outbound.handler(vlessRequest, { + enforceUDP, + forwardDNS, + log, + firstChunk: rawClientData, + }); + } catch (error) { + // Cannot make the connection, e.g., authentication failure + log(`Outbound ${outbound.protocol} failed with:`, error.message); + return null; } - - return null; } // Try each outbound method until we find a working one. - /** @type {OutboundConnection | null} */ let destRWPair = null; while (curOutBoundPtr.index < globalConfig.outbounds.length) { if (destRWPair == null) { destRWPair = await connectAndWrite(); } - + if (destRWPair != null) { const hasIncomingData = await remoteSocketToWS(destRWPair.readableStream, webSocket, vlessResponseHeader, vlessResponseProcessor, log); if (hasIncomingData) { @@ -897,12 +699,11 @@ async function handleOutBound(vlessRequest, rawClientData, webSocket, vlessRespo * Make a source out of a UDP socket, wrap each datagram with vless UDP packing. * Each receive datagram will be prepended with a 16-bit big-endian length field. * - * @param {NodeJSUDP} udpClient - * @param {LogFunction} log + * @param {import("./worker-neo").NodeJSUDP} udpClient + * @param {import('./worker-neo').LogFunction} log * @returns {ReadableStream} Datagrams received will be wrapped and made available in this stream. */ function makeReadableUDPStream(udpClient, log) { - /** @type {ReadableStream} */ return new ReadableStream({ start(controller) { udpClient.onmessage((message, info) => { @@ -913,7 +714,7 @@ function makeReadableUDPStream(udpClient, log) { controller.enqueue(encodedChunk); }); udpClient.onerror((error) => { - log('UDP Error: ', error); + log('UDP Error: ', error.message); controller.error(error); }); }, @@ -928,17 +729,16 @@ function makeReadableUDPStream(udpClient, log) { * Make a sink out of a UDP socket, the input stream assumes valid vless UDP packing. * Each datagram to be sent should be prepended with a 16-bit big-endian length field. * - * @param {NodeJSUDP} udpClient + * @param {import("./worker-neo").NodeJSUDP} udpClient * @param {string} addressRemote * @param {number} portRemote - * @param {LogFunction} log + * @param {import('./worker-neo').LogFunction} log * @returns {WritableStream} write to this stream will send datagrams via UDP. */ function makeWritableUDPStream(udpClient, addressRemote, portRemote, log) { /** @type {Uint8Array} */ let leftoverData = new Uint8Array(0); - /** @type {WritableStream} */ return new WritableStream({ write(chunk, controller) { let byteArray = new Uint8Array(chunk); @@ -985,7 +785,7 @@ function makeWritableUDPStream(udpClient, addressRemote, portRemote, log) { } /** - * @param {NodeJSUDP} udpClient + * @param {import("./worker-neo").NodeJSUDP} udpClient */ function safeCloseUDP(udpClient) { try { @@ -1000,10 +800,10 @@ function safeCloseUDP(udpClient) { * A ReadableStream should be created before performing any kind of write operation. * * @param {WebSocket} webSocketServer - * @param {Uint8Array} earlyData Data received before the ReadableStream was created - * @param {(firstChunk : Uint8Array) => Uint8Array} headStripper In some protocol like Vless, + * @param {Uint8Array | undefined | null} earlyData Data received before the ReadableStream was created + * @param {null | ((firstChunk : Uint8Array) => Uint8Array)} headStripper In some protocol like Vless, * a header is prepended to the first data chunk, it is necessary to strip that header. - * @param {LogFunction} log + * @param {import('./worker-neo').LogFunction} log * @returns {ReadableStream} a source of Uint8Array chunks */ function makeReadableWebSocketStream(webSocketServer, earlyData, headStripper, log) { @@ -1090,17 +890,15 @@ function makeReadableWebSocketStream(webSocketServer, earlyData, headStripper, l * * @param { Uint8Array } vlessBuffer * @param {string} userID the expected userID - * @returns + * @returns {import('./worker-neo').ProcessedVlessHeader} + * @throws {Error} */ function processVlessHeader( vlessBuffer, userID ) { if (vlessBuffer.byteLength < 24) { - return { - hasError: true, - message: 'invalid data', - }; + throw new Error('Invalid data'); } const version = vlessBuffer.slice(0, 1); let isValidUser = false; @@ -1109,10 +907,7 @@ function processVlessHeader( isValidUser = true; } if (!isValidUser) { - return { - hasError: true, - message: 'invalid user', - }; + throw new Error('Invalid user'); } //skip opt for now @@ -1123,10 +918,7 @@ function processVlessHeader( if (command === VlessCmd.UDP) { isUDP = true; } else if (command !== VlessCmd.TCP) { - return { - hasError: true, - message: `Invalid command type: ${command}, only accepts: ${JSON.stringify(VlessCmd)}`, - }; + throw new Error(`Invalid command type: ${command}, only accepts: ${JSON.stringify(VlessCmd)}`); } const portIndex = 18 + optLength + 1; // port is big-Endian in raw data etc 80 == 0x0050 @@ -1164,20 +956,13 @@ function processVlessHeader( break; } default: - return { - hasError: true, - message: `Invalid address type: ${addressType}, only accepts: ${JSON.stringify(VlessAddrType)}`, - }; + throw new Error(`Invalid address type: ${addressType}, only accepts: ${JSON.stringify(VlessAddrType)}`); } if (!addressValue) { - return { - hasError: true, - message: `Empty addressValue!`, - }; + throw new Error('Empty addressValue!'); } return { - hasError: false, addressRemote: addressValue, addressType, portRemote, @@ -1193,7 +978,7 @@ function processVlessHeader( * @param {WebSocket} webSocket to the client side * @param {Uint8Array} vlessResponseHeader The Vless response header. * @param {null | (() => TransformStream)} vlessResponseProcessor an optional TransformStream to process the Vless response. - * @param {LogFunction} log + * @param {import('./worker-neo').LogFunction} log * @returns {Promise} has hasIncomingData */ async function remoteSocketToWS(remoteSocketReader, webSocket, vlessResponseHeader, vlessResponseProcessor, log) { @@ -1279,22 +1064,22 @@ async function remoteSocketToWS(remoteSocketReader, webSocket, vlessResponseHead } /** - * + * Convert a base64 string to a Uint8Array. * @param {string} base64Str - * @returns + * @returns {Uint8Array | any} returns Uint8Array indicates a successful conversion, otherwise error will be returned. */ -function base64ToArrayBuffer(base64Str) { +function base64ToUint8Array(base64Str) { if (!base64Str) { - return { error: null }; + return null; } + try { // go use modified Base64 for URL rfc4648 which js atob not support base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/'); const decode = atob(base64Str); - const buffer = Uint8Array.from(decode, (c) => c.charCodeAt(0)); - return { earlyData: buffer, error: null }; + return Uint8Array.from(decode, (c) => c.charCodeAt(0)); } catch (error) { - return { error }; + return error; } } @@ -1324,7 +1109,7 @@ function uuidFromBytesSafe(buffer, offset = 0) { * @param {ArrayBufferLike} buffer * @returns {string} UUID in lower-case */ -export function uuidStrFromBytes(buffer, offset = 0) { +function uuidStrFromBytes(buffer, offset = 0) { const bytes = new Uint8Array(buffer); let uuid = ''; @@ -1364,7 +1149,7 @@ function safeCloseWebSocket(socket) { * @param {Uint8Array} array2 * @returns {Uint8Array} the merged Uint8Array */ -export function joinUint8Array(array1, array2) { +function joinUint8Array(array1, array2) { const result = new Uint8Array(array1.byteLength + array2.byteLength); result.set(array1); result.set(array2, array1.byteLength); @@ -1372,13 +1157,14 @@ export function joinUint8Array(array1, array2) { } /** - * @param {CloudflareTCPConnection} socket - * @param {string} username - * @param {string} password + * @param {import("./worker-neo").CloudflareTCPConnection} socket + * @param {string | undefined} username + * @param {string | undefined} password * @param {number} addressType * @param {string} addressRemote * @param {number} portRemote - * @param {LogFunction} log The logging function. + * @param {import('./worker-neo').LogFunction} log The logging function. + * @throws {Error} */ async function socks5Connect(socket, username, password, addressType, addressRemote, portRemote, log) { const writer = socket.writable.getWriter(); @@ -1437,7 +1223,7 @@ async function socks5Connect(socket, username, password, addressType, addressRem await writer.write(authRequest); res = (await reader.read()).value; // expected 0x0100 - if (res[0] !== 0x01 || res[1] !== 0x00) { + if (typeof res === 'undefined' || res[0] !== 0x01 || res[1] !== 0x00) { throw new Error("Authentication failed"); } } @@ -1492,7 +1278,7 @@ async function socks5Connect(socket, username, password, addressType, addressRem // +----+-----+-------+------+----------+----------+ // | 1 | 1 | X'00' | 1 | Variable | 2 | // +----+-----+-------+------+----------+----------+ - if (res[1] === 0x00) { + if (typeof res !== 'undefined' && res[1] === 0x00) { log("Socks5: Connection opened"); } else { throw new Error("Connection failed"); @@ -1503,8 +1289,8 @@ async function socks5Connect(socket, username, password, addressType, addressRem /** - * * @param {string} address + * @throws {Error} */ function socks5AddressParser(address) { const [latter, former] = address.split("@").reverse(); @@ -1554,6 +1340,7 @@ const VlessAddrType = { * @param {number} destPort * @param {string} uuid * @returns {Uint8Array} + * @throws {Error} */ function makeVlessReqHeader(command, destType, destAddr, destPort, uuid) { /** @type {number} */ @@ -1612,6 +1399,7 @@ function makeVlessReqHeader(command, destType, destAddr, destPort, uuid) { break; } case VlessAddrType.DomainName: + addressEncoded = /** @type {Uint8Array} */ (addressEncoded); vlessHeader[22] = addressEncoded.length; vlessHeader.set(addressEncoded, 23); break; @@ -1633,7 +1421,7 @@ function makeVlessReqHeader(command, destType, destAddr, destPort, uuid) { /** * @param {string} address Domain name, HTTP request Hostname, and the SNI of the remote host. - * @param {StreamSettings} streamSettings + * @param {import("./worker-neo").StreamSettings} streamSettings */ function checkVlessConfig(address, streamSettings) { if (streamSettings.network !== 'ws') { @@ -1687,12 +1475,7 @@ function parseVlessString(url) { return json; } - -/** - * - * @param {string | null} hostName - * @returns {string} - */ +/** @type {import('./worker-neo').getVLESSConfig} */ export function getVLESSConfig(hostName) { const vlessMain = `vless://${globalConfig.userID}@${hostName}:443?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#${hostName}` return ` From 06334a4a74d3198f3f290f2d85953a2fb6eb2a7c Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Wed, 13 Dec 2023 05:05:19 +1100 Subject: [PATCH 43/44] Fix no 0rtt --- src/worker-neo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/worker-neo.js b/src/worker-neo.js index cf478c3fbb..65c151a729 100644 --- a/src/worker-neo.js +++ b/src/worker-neo.js @@ -1070,7 +1070,7 @@ async function remoteSocketToWS(remoteSocketReader, webSocket, vlessResponseHead */ function base64ToUint8Array(base64Str) { if (!base64Str) { - return null; + return new Uint8Array(0); } try { From 43c2330a40875267e49c6203cfd4cc76c4ca69ba Mon Sep 17 00:00:00 2001 From: rikkagcp1 Date: Wed, 13 Dec 2023 05:17:37 +1100 Subject: [PATCH 44/44] Fix zero-length earlyData handling --- src/worker-neo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/worker-neo.js b/src/worker-neo.js index 65c151a729..a22d01298d 100644 --- a/src/worker-neo.js +++ b/src/worker-neo.js @@ -813,7 +813,7 @@ function makeReadableWebSocketStream(webSocketServer, earlyData, headStripper, l /** @type {ReadableStream} */ const stream = new ReadableStream({ start(controller) { - if (earlyData) { + if (earlyData && earlyData.byteLength > 0) { controller.enqueue(earlyData); }