Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Major update: port mapping, vless outbound, and more #190

Open
wants to merge 44 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
23eb8b7
Abstract worker-vless.js, create launcher for node
Jun 27, 2023
c7ee1a0
Fix indentation
Jun 27, 2023
0a6d83a
Move NodeJS launcher to a separated folder
Jun 27, 2023
d7cf50e
Archieve worker-vless.js and prepare to revert
Jun 27, 2023
30c01a8
Abstract socks5 version
Jun 27, 2023
7b9b464
Better logging
Jun 27, 2023
40100e1
Revert worker-vless.js
Jun 27, 2023
6869e32
Introduce fallback and globalConfig
Jun 28, 2023
3e70643
Add fallback and code clean up
Jun 28, 2023
00f4d39
Better websocket close logic
Jun 28, 2023
eaeb94a
Fix tryOutbound
Jun 28, 2023
6dad634
Add vless header maker
Jun 28, 2023
2e4208f
Impl Vless client without error handling
Jun 28, 2023
c69f300
Add some error handling to vless
Jun 28, 2023
7adaeef
Add Vless string parser
Jun 28, 2023
89a5bc9
Better Vless outbound
Jun 29, 2023
d1c4fca
More comments
Jun 29, 2023
f6eecad
Fix vless on workers
Jun 29, 2023
6a1ae11
Allowing the worker to push log to a remote server
Jun 30, 2023
7637d59
Support UDP tunneling via vless
Jul 1, 2023
29542c5
Add UDP outbound support if run on node
Jul 3, 2023
e54a093
Support IPv6 UDP outbound
Jul 3, 2023
f8434ba
Simply DNS over TCP implementation
Jul 6, 2023
85301ea
Fix indentation
Jul 9, 2023
54734bd
Fix UDP outbound on discontinued streams
Jul 10, 2023
d0ce27a
Fix DNS over TCP
Jul 10, 2023
a88b9fb
Use Uint8Array throughout the code
Jul 16, 2023
2dbb1cf
Fix earlydata
rikkagcp1 Jul 16, 2023
a63c7d3
Remove the use of Blob
Jul 17, 2023
217ddb3
better shadowrocket compatiblity
Jul 25, 2023
ad473ba
Code clean-up
Jul 25, 2023
d171ec9
More clean up
Jul 25, 2023
1c64027
Less verbose log
Jul 25, 2023
a58b6b2
More code clean up
Jul 25, 2023
ead7a93
Massive code clean-up, add type annotations.
Dec 1, 2023
3d332fb
Restore worker-with-socks5-experimental.js
Dec 1, 2023
6988af0
Add websocket message processor
Dec 2, 2023
9527803
Delay the instantiation of response processor
Dec 2, 2023
d69aad5
Add deno support, improve node support
Dec 3, 2023
dd33548
Fix IPv6 UDP on deno
Dec 3, 2023
c2334b2
Fix IPv6 inbound
Dec 6, 2023
41a7e75
Move type definition, better outbound impl
Dec 12, 2023
06334a4
Fix no 0rtt
Dec 12, 2023
43c2330
Fix zero-length earlyData handling
Dec 12, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Better Vless outbound
  • Loading branch information
rikkagcp1 committed Jun 29, 2023
commit 89a5bc995dd73001f061ee2ff08567537a4ee1a5
127 changes: 73 additions & 54 deletions src/worker-with-socks5-experimental.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>}
* }
* }>
* }
*/
connect: null,
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -465,48 +467,21 @@ 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
try {
await openPromise;
} catch (err) {
log(`Cannot open Websocket connection: ${err.message}`);
wsToVlessServer.close();
return null;
}

/** @type {Promise<Uint8Array>} */
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);
},
Expand All @@ -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<ReadableStream>} */
Expand All @@ -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();

Expand All @@ -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) {
Expand All @@ -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);
});

Expand Down Expand Up @@ -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) => {
Expand Down