Spaces:
Running
Running
github-actions[bot]
commited on
Commit
·
78ac079
1
Parent(s):
86d88d1
Update from GitHub Actions
Browse files- src/worker-vless.js +636 -0
- src/worker-with-socks5-experimental.js +806 -0
src/worker-vless.js
ADDED
@@ -0,0 +1,636 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// <!--GAMFC-->version base on commit 58686d5d125194d34a1137913b3a64ddcf55872f, time is 2024-11-27 09:26:01 UTC<!--GAMFC-END-->.
|
2 |
+
// @ts-ignore
|
3 |
+
import { connect } from 'cloudflare:sockets';
|
4 |
+
|
5 |
+
// How to generate your own UUID:
|
6 |
+
// [Windows] Press "Win + R", input cmd and run: Powershell -NoExit -Command "[guid]::NewGuid()"
|
7 |
+
let userID = 'd342d11e-d424-4583-b36e-524ab1f0afa4';
|
8 |
+
|
9 |
+
let proxyIP = '';
|
10 |
+
|
11 |
+
|
12 |
+
if (!isValidUUID(userID)) {
|
13 |
+
throw new Error('uuid is not valid');
|
14 |
+
}
|
15 |
+
|
16 |
+
export default {
|
17 |
+
/**
|
18 |
+
* @param {import("@cloudflare/workers-types").Request} request
|
19 |
+
* @param {{UUID: string, PROXYIP: string}} env
|
20 |
+
* @param {import("@cloudflare/workers-types").ExecutionContext} ctx
|
21 |
+
* @returns {Promise<Response>}
|
22 |
+
*/
|
23 |
+
async fetch(request, env, ctx) {
|
24 |
+
try {
|
25 |
+
userID = env.UUID || userID;
|
26 |
+
proxyIP = env.PROXYIP || proxyIP;
|
27 |
+
const upgradeHeader = request.headers.get('Upgrade');
|
28 |
+
if (!upgradeHeader || upgradeHeader !== 'websocket') {
|
29 |
+
const url = new URL(request.url);
|
30 |
+
switch (url.pathname) {
|
31 |
+
case '/':
|
32 |
+
return new Response(JSON.stringify(request.cf), { status: 200 });
|
33 |
+
case `/${userID}`: {
|
34 |
+
const vlessConfig = getVLESSConfig(userID, request.headers.get('Host'));
|
35 |
+
return new Response(`${vlessConfig}`, {
|
36 |
+
status: 200,
|
37 |
+
headers: {
|
38 |
+
"Content-Type": "text/plain;charset=utf-8",
|
39 |
+
}
|
40 |
+
});
|
41 |
+
}
|
42 |
+
default:
|
43 |
+
return new Response('Not found', { status: 404 });
|
44 |
+
}
|
45 |
+
} else {
|
46 |
+
return await vlessOverWSHandler(request);
|
47 |
+
}
|
48 |
+
} catch (err) {
|
49 |
+
/** @type {Error} */ let e = err;
|
50 |
+
return new Response(e.toString());
|
51 |
+
}
|
52 |
+
},
|
53 |
+
};
|
54 |
+
|
55 |
+
|
56 |
+
|
57 |
+
|
58 |
+
/**
|
59 |
+
*
|
60 |
+
* @param {import("@cloudflare/workers-types").Request} request
|
61 |
+
*/
|
62 |
+
async function vlessOverWSHandler(request) {
|
63 |
+
|
64 |
+
/** @type {import("@cloudflare/workers-types").WebSocket[]} */
|
65 |
+
// @ts-ignore
|
66 |
+
const webSocketPair = new WebSocketPair();
|
67 |
+
const [client, webSocket] = Object.values(webSocketPair);
|
68 |
+
|
69 |
+
webSocket.accept();
|
70 |
+
|
71 |
+
let address = '';
|
72 |
+
let portWithRandomLog = '';
|
73 |
+
const log = (/** @type {string} */ info, /** @type {string | undefined} */ event) => {
|
74 |
+
console.log(`[${address}:${portWithRandomLog}] ${info}`, event || '');
|
75 |
+
};
|
76 |
+
const earlyDataHeader = request.headers.get('sec-websocket-protocol') || '';
|
77 |
+
|
78 |
+
const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log);
|
79 |
+
|
80 |
+
/** @type {{ value: import("@cloudflare/workers-types").Socket | null}}*/
|
81 |
+
let remoteSocketWapper = {
|
82 |
+
value: null,
|
83 |
+
};
|
84 |
+
let udpStreamWrite = null;
|
85 |
+
let isDns = false;
|
86 |
+
|
87 |
+
// ws --> remote
|
88 |
+
readableWebSocketStream.pipeTo(new WritableStream({
|
89 |
+
async write(chunk, controller) {
|
90 |
+
if (isDns && udpStreamWrite) {
|
91 |
+
return udpStreamWrite(chunk);
|
92 |
+
}
|
93 |
+
if (remoteSocketWapper.value) {
|
94 |
+
const writer = remoteSocketWapper.value.writable.getWriter()
|
95 |
+
await writer.write(chunk);
|
96 |
+
writer.releaseLock();
|
97 |
+
return;
|
98 |
+
}
|
99 |
+
|
100 |
+
const {
|
101 |
+
hasError,
|
102 |
+
message,
|
103 |
+
portRemote = 443,
|
104 |
+
addressRemote = '',
|
105 |
+
rawDataIndex,
|
106 |
+
vlessVersion = new Uint8Array([0, 0]),
|
107 |
+
isUDP,
|
108 |
+
} = processVlessHeader(chunk, userID);
|
109 |
+
address = addressRemote;
|
110 |
+
portWithRandomLog = `${portRemote}--${Math.random()} ${isUDP ? 'udp ' : 'tcp '
|
111 |
+
} `;
|
112 |
+
if (hasError) {
|
113 |
+
// controller.error(message);
|
114 |
+
throw new Error(message); // cf seems has bug, controller.error will not end stream
|
115 |
+
// webSocket.close(1000, message);
|
116 |
+
return;
|
117 |
+
}
|
118 |
+
// if UDP but port not DNS port, close it
|
119 |
+
if (isUDP) {
|
120 |
+
if (portRemote === 53) {
|
121 |
+
isDns = true;
|
122 |
+
} else {
|
123 |
+
// controller.error('UDP proxy only enable for DNS which is port 53');
|
124 |
+
throw new Error('UDP proxy only enable for DNS which is port 53'); // cf seems has bug, controller.error will not end stream
|
125 |
+
return;
|
126 |
+
}
|
127 |
+
}
|
128 |
+
// ["version", "附加信息长度 N"]
|
129 |
+
const vlessResponseHeader = new Uint8Array([vlessVersion[0], 0]);
|
130 |
+
const rawClientData = chunk.slice(rawDataIndex);
|
131 |
+
|
132 |
+
// TODO: support udp here when cf runtime has udp support
|
133 |
+
if (isDns) {
|
134 |
+
const { write } = await handleUDPOutBound(webSocket, vlessResponseHeader, log);
|
135 |
+
udpStreamWrite = write;
|
136 |
+
udpStreamWrite(rawClientData);
|
137 |
+
return;
|
138 |
+
}
|
139 |
+
handleTCPOutBound(remoteSocketWapper, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log);
|
140 |
+
},
|
141 |
+
close() {
|
142 |
+
log(`readableWebSocketStream is close`);
|
143 |
+
},
|
144 |
+
abort(reason) {
|
145 |
+
log(`readableWebSocketStream is abort`, JSON.stringify(reason));
|
146 |
+
},
|
147 |
+
})).catch((err) => {
|
148 |
+
log('readableWebSocketStream pipeTo error', err);
|
149 |
+
});
|
150 |
+
|
151 |
+
return new Response(null, {
|
152 |
+
status: 101,
|
153 |
+
// @ts-ignore
|
154 |
+
webSocket: client,
|
155 |
+
});
|
156 |
+
}
|
157 |
+
|
158 |
+
/**
|
159 |
+
* Handles outbound TCP connections.
|
160 |
+
*
|
161 |
+
* @param {any} remoteSocket
|
162 |
+
* @param {string} addressRemote The remote address to connect to.
|
163 |
+
* @param {number} portRemote The remote port to connect to.
|
164 |
+
* @param {Uint8Array} rawClientData The raw client data to write.
|
165 |
+
* @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to pass the remote socket to.
|
166 |
+
* @param {Uint8Array} vlessResponseHeader The VLESS response header.
|
167 |
+
* @param {function} log The logging function.
|
168 |
+
* @returns {Promise<void>} The remote socket.
|
169 |
+
*/
|
170 |
+
async function handleTCPOutBound(remoteSocket, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log,) {
|
171 |
+
async function connectAndWrite(address, port) {
|
172 |
+
/** @type {import("@cloudflare/workers-types").Socket} */
|
173 |
+
const tcpSocket = connect({
|
174 |
+
hostname: address,
|
175 |
+
port: port,
|
176 |
+
});
|
177 |
+
remoteSocket.value = tcpSocket;
|
178 |
+
log(`connected to ${address}:${port}`);
|
179 |
+
const writer = tcpSocket.writable.getWriter();
|
180 |
+
await writer.write(rawClientData); // first write, nomal is tls client hello
|
181 |
+
writer.releaseLock();
|
182 |
+
return tcpSocket;
|
183 |
+
}
|
184 |
+
|
185 |
+
// if the cf connect tcp socket have no incoming data, we retry to redirect ip
|
186 |
+
async function retry() {
|
187 |
+
const tcpSocket = await connectAndWrite(proxyIP || addressRemote, portRemote)
|
188 |
+
// no matter retry success or not, close websocket
|
189 |
+
tcpSocket.closed.catch(error => {
|
190 |
+
console.log('retry tcpSocket closed error', error);
|
191 |
+
}).finally(() => {
|
192 |
+
safeCloseWebSocket(webSocket);
|
193 |
+
})
|
194 |
+
remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, null, log);
|
195 |
+
}
|
196 |
+
|
197 |
+
const tcpSocket = await connectAndWrite(addressRemote, portRemote);
|
198 |
+
|
199 |
+
// when remoteSocket is ready, pass to websocket
|
200 |
+
// remote--> ws
|
201 |
+
remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, retry, log);
|
202 |
+
}
|
203 |
+
|
204 |
+
/**
|
205 |
+
*
|
206 |
+
* @param {import("@cloudflare/workers-types").WebSocket} webSocketServer
|
207 |
+
* @param {string} earlyDataHeader for ws 0rtt
|
208 |
+
* @param {(info: string)=> void} log for ws 0rtt
|
209 |
+
*/
|
210 |
+
function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) {
|
211 |
+
let readableStreamCancel = false;
|
212 |
+
const stream = new ReadableStream({
|
213 |
+
start(controller) {
|
214 |
+
webSocketServer.addEventListener('message', (event) => {
|
215 |
+
if (readableStreamCancel) {
|
216 |
+
return;
|
217 |
+
}
|
218 |
+
const message = event.data;
|
219 |
+
controller.enqueue(message);
|
220 |
+
});
|
221 |
+
|
222 |
+
// The event means that the client closed the client -> server stream.
|
223 |
+
// However, the server -> client stream is still open until you call close() on the server side.
|
224 |
+
// The WebSocket protocol says that a separate close message must be sent in each direction to fully close the socket.
|
225 |
+
webSocketServer.addEventListener('close', () => {
|
226 |
+
// client send close, need close server
|
227 |
+
// if stream is cancel, skip controller.close
|
228 |
+
safeCloseWebSocket(webSocketServer);
|
229 |
+
if (readableStreamCancel) {
|
230 |
+
return;
|
231 |
+
}
|
232 |
+
controller.close();
|
233 |
+
}
|
234 |
+
);
|
235 |
+
webSocketServer.addEventListener('error', (err) => {
|
236 |
+
log('webSocketServer has error');
|
237 |
+
controller.error(err);
|
238 |
+
}
|
239 |
+
);
|
240 |
+
// for ws 0rtt
|
241 |
+
const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader);
|
242 |
+
if (error) {
|
243 |
+
controller.error(error);
|
244 |
+
} else if (earlyData) {
|
245 |
+
controller.enqueue(earlyData);
|
246 |
+
}
|
247 |
+
},
|
248 |
+
|
249 |
+
pull(controller) {
|
250 |
+
// if ws can stop read if stream is full, we can implement backpressure
|
251 |
+
// https://streams.spec.whatwg.org/#example-rs-push-backpressure
|
252 |
+
},
|
253 |
+
cancel(reason) {
|
254 |
+
// 1. pipe WritableStream has error, this cancel will called, so ws handle server close into here
|
255 |
+
// 2. if readableStream is cancel, all controller.close/enqueue need skip,
|
256 |
+
// 3. but from testing controller.error still work even if readableStream is cancel
|
257 |
+
if (readableStreamCancel) {
|
258 |
+
return;
|
259 |
+
}
|
260 |
+
log(`ReadableStream was canceled, due to ${reason}`)
|
261 |
+
readableStreamCancel = true;
|
262 |
+
safeCloseWebSocket(webSocketServer);
|
263 |
+
}
|
264 |
+
});
|
265 |
+
|
266 |
+
return stream;
|
267 |
+
|
268 |
+
}
|
269 |
+
|
270 |
+
// https://xtls.github.io/development/protocols/vless.html
|
271 |
+
// https://github.com/zizifn/excalidraw-backup/blob/main/v2ray-protocol.excalidraw
|
272 |
+
|
273 |
+
/**
|
274 |
+
*
|
275 |
+
* @param { ArrayBuffer} vlessBuffer
|
276 |
+
* @param {string} userID
|
277 |
+
* @returns
|
278 |
+
*/
|
279 |
+
function processVlessHeader(
|
280 |
+
vlessBuffer,
|
281 |
+
userID
|
282 |
+
) {
|
283 |
+
if (vlessBuffer.byteLength < 24) {
|
284 |
+
return {
|
285 |
+
hasError: true,
|
286 |
+
message: 'invalid data',
|
287 |
+
};
|
288 |
+
}
|
289 |
+
const version = new Uint8Array(vlessBuffer.slice(0, 1));
|
290 |
+
let isValidUser = false;
|
291 |
+
let isUDP = false;
|
292 |
+
if (stringify(new Uint8Array(vlessBuffer.slice(1, 17))) === userID) {
|
293 |
+
isValidUser = true;
|
294 |
+
}
|
295 |
+
if (!isValidUser) {
|
296 |
+
return {
|
297 |
+
hasError: true,
|
298 |
+
message: 'invalid user',
|
299 |
+
};
|
300 |
+
}
|
301 |
+
|
302 |
+
const optLength = new Uint8Array(vlessBuffer.slice(17, 18))[0];
|
303 |
+
//skip opt for now
|
304 |
+
|
305 |
+
const command = new Uint8Array(
|
306 |
+
vlessBuffer.slice(18 + optLength, 18 + optLength + 1)
|
307 |
+
)[0];
|
308 |
+
|
309 |
+
// 0x01 TCP
|
310 |
+
// 0x02 UDP
|
311 |
+
// 0x03 MUX
|
312 |
+
if (command === 1) {
|
313 |
+
} else if (command === 2) {
|
314 |
+
isUDP = true;
|
315 |
+
} else {
|
316 |
+
return {
|
317 |
+
hasError: true,
|
318 |
+
message: `command ${command} is not support, command 01-tcp,02-udp,03-mux`,
|
319 |
+
};
|
320 |
+
}
|
321 |
+
const portIndex = 18 + optLength + 1;
|
322 |
+
const portBuffer = vlessBuffer.slice(portIndex, portIndex + 2);
|
323 |
+
// port is big-Endian in raw data etc 80 == 0x005d
|
324 |
+
const portRemote = new DataView(portBuffer).getUint16(0);
|
325 |
+
|
326 |
+
let addressIndex = portIndex + 2;
|
327 |
+
const addressBuffer = new Uint8Array(
|
328 |
+
vlessBuffer.slice(addressIndex, addressIndex + 1)
|
329 |
+
);
|
330 |
+
|
331 |
+
// 1--> ipv4 addressLength =4
|
332 |
+
// 2--> domain name addressLength=addressBuffer[1]
|
333 |
+
// 3--> ipv6 addressLength =16
|
334 |
+
const addressType = addressBuffer[0];
|
335 |
+
let addressLength = 0;
|
336 |
+
let addressValueIndex = addressIndex + 1;
|
337 |
+
let addressValue = '';
|
338 |
+
switch (addressType) {
|
339 |
+
case 1:
|
340 |
+
addressLength = 4;
|
341 |
+
addressValue = new Uint8Array(
|
342 |
+
vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength)
|
343 |
+
).join('.');
|
344 |
+
break;
|
345 |
+
case 2:
|
346 |
+
addressLength = new Uint8Array(
|
347 |
+
vlessBuffer.slice(addressValueIndex, addressValueIndex + 1)
|
348 |
+
)[0];
|
349 |
+
addressValueIndex += 1;
|
350 |
+
addressValue = new TextDecoder().decode(
|
351 |
+
vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength)
|
352 |
+
);
|
353 |
+
break;
|
354 |
+
case 3:
|
355 |
+
addressLength = 16;
|
356 |
+
const dataView = new DataView(
|
357 |
+
vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength)
|
358 |
+
);
|
359 |
+
// 2001:0db8:85a3:0000:0000:8a2e:0370:7334
|
360 |
+
const ipv6 = [];
|
361 |
+
for (let i = 0; i < 8; i++) {
|
362 |
+
ipv6.push(dataView.getUint16(i * 2).toString(16));
|
363 |
+
}
|
364 |
+
addressValue = ipv6.join(':');
|
365 |
+
// seems no need add [] for ipv6
|
366 |
+
break;
|
367 |
+
default:
|
368 |
+
return {
|
369 |
+
hasError: true,
|
370 |
+
message: `invild addressType is ${addressType}`,
|
371 |
+
};
|
372 |
+
}
|
373 |
+
if (!addressValue) {
|
374 |
+
return {
|
375 |
+
hasError: true,
|
376 |
+
message: `addressValue is empty, addressType is ${addressType}`,
|
377 |
+
};
|
378 |
+
}
|
379 |
+
|
380 |
+
return {
|
381 |
+
hasError: false,
|
382 |
+
addressRemote: addressValue,
|
383 |
+
addressType,
|
384 |
+
portRemote,
|
385 |
+
rawDataIndex: addressValueIndex + addressLength,
|
386 |
+
vlessVersion: version,
|
387 |
+
isUDP,
|
388 |
+
};
|
389 |
+
}
|
390 |
+
|
391 |
+
|
392 |
+
/**
|
393 |
+
*
|
394 |
+
* @param {import("@cloudflare/workers-types").Socket} remoteSocket
|
395 |
+
* @param {import("@cloudflare/workers-types").WebSocket} webSocket
|
396 |
+
* @param {ArrayBuffer} vlessResponseHeader
|
397 |
+
* @param {(() => Promise<void>) | null} retry
|
398 |
+
* @param {*} log
|
399 |
+
*/
|
400 |
+
async function remoteSocketToWS(remoteSocket, webSocket, vlessResponseHeader, retry, log) {
|
401 |
+
// remote--> ws
|
402 |
+
let remoteChunkCount = 0;
|
403 |
+
let chunks = [];
|
404 |
+
/** @type {ArrayBuffer | null} */
|
405 |
+
let vlessHeader = vlessResponseHeader;
|
406 |
+
let hasIncomingData = false; // check if remoteSocket has incoming data
|
407 |
+
await remoteSocket.readable
|
408 |
+
.pipeTo(
|
409 |
+
new WritableStream({
|
410 |
+
start() {
|
411 |
+
},
|
412 |
+
/**
|
413 |
+
*
|
414 |
+
* @param {Uint8Array} chunk
|
415 |
+
* @param {*} controller
|
416 |
+
*/
|
417 |
+
async write(chunk, controller) {
|
418 |
+
hasIncomingData = true;
|
419 |
+
// remoteChunkCount++;
|
420 |
+
if (webSocket.readyState !== WS_READY_STATE_OPEN) {
|
421 |
+
controller.error(
|
422 |
+
'webSocket.readyState is not open, maybe close'
|
423 |
+
);
|
424 |
+
}
|
425 |
+
if (vlessHeader) {
|
426 |
+
webSocket.send(await new Blob([vlessHeader, chunk]).arrayBuffer());
|
427 |
+
vlessHeader = null;
|
428 |
+
} else {
|
429 |
+
// seems no need rate limit this, CF seems fix this??..
|
430 |
+
// if (remoteChunkCount > 20000) {
|
431 |
+
// // cf one package is 4096 byte(4kb), 4096 * 20000 = 80M
|
432 |
+
// await delay(1);
|
433 |
+
// }
|
434 |
+
webSocket.send(chunk);
|
435 |
+
}
|
436 |
+
},
|
437 |
+
close() {
|
438 |
+
log(`remoteConnection!.readable is close with hasIncomingData is ${hasIncomingData}`);
|
439 |
+
// 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.
|
440 |
+
},
|
441 |
+
abort(reason) {
|
442 |
+
console.error(`remoteConnection!.readable abort`, reason);
|
443 |
+
},
|
444 |
+
})
|
445 |
+
)
|
446 |
+
.catch((error) => {
|
447 |
+
console.error(
|
448 |
+
`remoteSocketToWS has exception `,
|
449 |
+
error.stack || error
|
450 |
+
);
|
451 |
+
safeCloseWebSocket(webSocket);
|
452 |
+
});
|
453 |
+
|
454 |
+
// seems is cf connect socket have error,
|
455 |
+
// 1. Socket.closed will have error
|
456 |
+
// 2. Socket.readable will be close without any data coming
|
457 |
+
if (hasIncomingData === false && retry) {
|
458 |
+
log(`retry`)
|
459 |
+
retry();
|
460 |
+
}
|
461 |
+
}
|
462 |
+
|
463 |
+
/**
|
464 |
+
*
|
465 |
+
* @param {string} base64Str
|
466 |
+
* @returns
|
467 |
+
*/
|
468 |
+
function base64ToArrayBuffer(base64Str) {
|
469 |
+
if (!base64Str) {
|
470 |
+
return { error: null };
|
471 |
+
}
|
472 |
+
try {
|
473 |
+
// go use modified Base64 for URL rfc4648 which js atob not support
|
474 |
+
base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/');
|
475 |
+
const decode = atob(base64Str);
|
476 |
+
const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0));
|
477 |
+
return { earlyData: arryBuffer.buffer, error: null };
|
478 |
+
} catch (error) {
|
479 |
+
return { error };
|
480 |
+
}
|
481 |
+
}
|
482 |
+
|
483 |
+
/**
|
484 |
+
* This is not real UUID validation
|
485 |
+
* @param {string} uuid
|
486 |
+
*/
|
487 |
+
function isValidUUID(uuid) {
|
488 |
+
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;
|
489 |
+
return uuidRegex.test(uuid);
|
490 |
+
}
|
491 |
+
|
492 |
+
const WS_READY_STATE_OPEN = 1;
|
493 |
+
const WS_READY_STATE_CLOSING = 2;
|
494 |
+
/**
|
495 |
+
* Normally, WebSocket will not has exceptions when close.
|
496 |
+
* @param {import("@cloudflare/workers-types").WebSocket} socket
|
497 |
+
*/
|
498 |
+
function safeCloseWebSocket(socket) {
|
499 |
+
try {
|
500 |
+
if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) {
|
501 |
+
socket.close();
|
502 |
+
}
|
503 |
+
} catch (error) {
|
504 |
+
console.error('safeCloseWebSocket error', error);
|
505 |
+
}
|
506 |
+
}
|
507 |
+
|
508 |
+
const byteToHex = [];
|
509 |
+
for (let i = 0; i < 256; ++i) {
|
510 |
+
byteToHex.push((i + 256).toString(16).slice(1));
|
511 |
+
}
|
512 |
+
function unsafeStringify(arr, offset = 0) {
|
513 |
+
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();
|
514 |
+
}
|
515 |
+
function stringify(arr, offset = 0) {
|
516 |
+
const uuid = unsafeStringify(arr, offset);
|
517 |
+
if (!isValidUUID(uuid)) {
|
518 |
+
throw TypeError("Stringified UUID is invalid");
|
519 |
+
}
|
520 |
+
return uuid;
|
521 |
+
}
|
522 |
+
|
523 |
+
|
524 |
+
/**
|
525 |
+
*
|
526 |
+
* @param {import("@cloudflare/workers-types").WebSocket} webSocket
|
527 |
+
* @param {ArrayBuffer} vlessResponseHeader
|
528 |
+
* @param {(string)=> void} log
|
529 |
+
*/
|
530 |
+
async function handleUDPOutBound(webSocket, vlessResponseHeader, log) {
|
531 |
+
|
532 |
+
let isVlessHeaderSent = false;
|
533 |
+
const transformStream = new TransformStream({
|
534 |
+
start(controller) {
|
535 |
+
|
536 |
+
},
|
537 |
+
transform(chunk, controller) {
|
538 |
+
// udp message 2 byte is the the length of udp data
|
539 |
+
// TODO: this should have bug, beacsue maybe udp chunk can be in two websocket message
|
540 |
+
for (let index = 0; index < chunk.byteLength;) {
|
541 |
+
const lengthBuffer = chunk.slice(index, index + 2);
|
542 |
+
const udpPakcetLength = new DataView(lengthBuffer).getUint16(0);
|
543 |
+
const udpData = new Uint8Array(
|
544 |
+
chunk.slice(index + 2, index + 2 + udpPakcetLength)
|
545 |
+
);
|
546 |
+
index = index + 2 + udpPakcetLength;
|
547 |
+
controller.enqueue(udpData);
|
548 |
+
}
|
549 |
+
},
|
550 |
+
flush(controller) {
|
551 |
+
}
|
552 |
+
});
|
553 |
+
|
554 |
+
// only handle dns udp for now
|
555 |
+
transformStream.readable.pipeTo(new WritableStream({
|
556 |
+
async write(chunk) {
|
557 |
+
const resp = await fetch('https://1.1.1.1/dns-query',
|
558 |
+
{
|
559 |
+
method: 'POST',
|
560 |
+
headers: {
|
561 |
+
'content-type': 'application/dns-message',
|
562 |
+
},
|
563 |
+
body: chunk,
|
564 |
+
})
|
565 |
+
const dnsQueryResult = await resp.arrayBuffer();
|
566 |
+
const udpSize = dnsQueryResult.byteLength;
|
567 |
+
// console.log([...new Uint8Array(dnsQueryResult)].map((x) => x.toString(16)));
|
568 |
+
const udpSizeBuffer = new Uint8Array([(udpSize >> 8) & 0xff, udpSize & 0xff]);
|
569 |
+
if (webSocket.readyState === WS_READY_STATE_OPEN) {
|
570 |
+
log(`doh success and dns message length is ${udpSize}`);
|
571 |
+
if (isVlessHeaderSent) {
|
572 |
+
webSocket.send(await new Blob([udpSizeBuffer, dnsQueryResult]).arrayBuffer());
|
573 |
+
} else {
|
574 |
+
webSocket.send(await new Blob([vlessResponseHeader, udpSizeBuffer, dnsQueryResult]).arrayBuffer());
|
575 |
+
isVlessHeaderSent = true;
|
576 |
+
}
|
577 |
+
}
|
578 |
+
}
|
579 |
+
})).catch((error) => {
|
580 |
+
log('dns udp has error' + error)
|
581 |
+
});
|
582 |
+
|
583 |
+
const writer = transformStream.writable.getWriter();
|
584 |
+
|
585 |
+
return {
|
586 |
+
/**
|
587 |
+
*
|
588 |
+
* @param {Uint8Array} chunk
|
589 |
+
*/
|
590 |
+
write(chunk) {
|
591 |
+
writer.write(chunk);
|
592 |
+
}
|
593 |
+
};
|
594 |
+
}
|
595 |
+
|
596 |
+
/**
|
597 |
+
*
|
598 |
+
* @param {string} userID
|
599 |
+
* @param {string | null} hostName
|
600 |
+
* @returns {string}
|
601 |
+
*/
|
602 |
+
function getVLESSConfig(userID, hostName) {
|
603 |
+
const protocol = "vless";
|
604 |
+
const vlessMain =
|
605 |
+
`${protocol}` +
|
606 |
+
`://${userID}@${hostName}:443`+
|
607 |
+
`?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#${hostName}`;
|
608 |
+
|
609 |
+
return `
|
610 |
+
################################################################
|
611 |
+
v2ray
|
612 |
+
---------------------------------------------------------------
|
613 |
+
${vlessMain}
|
614 |
+
---------------------------------------------------------------
|
615 |
+
################################################################
|
616 |
+
clash-meta
|
617 |
+
---------------------------------------------------------------
|
618 |
+
- type: vless
|
619 |
+
name: ${hostName}
|
620 |
+
server: ${hostName}
|
621 |
+
port: 443
|
622 |
+
uuid: ${userID}
|
623 |
+
network: ws
|
624 |
+
tls: true
|
625 |
+
udp: false
|
626 |
+
sni: ${hostName}
|
627 |
+
client-fingerprint: chrome
|
628 |
+
ws-opts:
|
629 |
+
path: "/?ed=2048"
|
630 |
+
headers:
|
631 |
+
host: ${hostName}
|
632 |
+
---------------------------------------------------------------
|
633 |
+
################################################################
|
634 |
+
`;
|
635 |
+
}
|
636 |
+
|
src/worker-with-socks5-experimental.js
ADDED
@@ -0,0 +1,806 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// <!--GAMFC-->version base on commit 58686d5d125194d34a1137913b3a64ddcf55872f, time is 2024-11-27 09:26:02 UTC<!--GAMFC-END-->.
|
2 |
+
// @ts-ignore
|
3 |
+
import { connect } from 'cloudflare:sockets';
|
4 |
+
|
5 |
+
// How to generate your own UUID:
|
6 |
+
// [Windows] Press "Win + R", input cmd and run: Powershell -NoExit -Command "[guid]::NewGuid()"
|
7 |
+
let userID = 'd342d11e-d424-4583-b36e-524ab1f0afa4';
|
8 |
+
|
9 |
+
let proxyIP = '';
|
10 |
+
|
11 |
+
// The user name and password do not contain special characters
|
12 |
+
// Setting the address will ignore proxyIP
|
13 |
+
// Example: user:pass@host:port or host:port
|
14 |
+
let socks5Address = '';
|
15 |
+
|
16 |
+
if (!isValidUUID(userID)) {
|
17 |
+
throw new Error('uuid is not valid');
|
18 |
+
}
|
19 |
+
|
20 |
+
let parsedSocks5Address = {};
|
21 |
+
let enableSocks = false;
|
22 |
+
|
23 |
+
export default {
|
24 |
+
/**
|
25 |
+
* @param {import("@cloudflare/workers-types").Request} request
|
26 |
+
* @param {{UUID: string, PROXYIP: string}} env
|
27 |
+
* @param {import("@cloudflare/workers-types").ExecutionContext} ctx
|
28 |
+
* @returns {Promise<Response>}
|
29 |
+
*/
|
30 |
+
async fetch(request, env, ctx) {
|
31 |
+
try {
|
32 |
+
userID = env.UUID || userID;
|
33 |
+
proxyIP = env.PROXYIP || proxyIP;
|
34 |
+
socks5Address = env.SOCKS5 || socks5Address;
|
35 |
+
if (socks5Address) {
|
36 |
+
try {
|
37 |
+
parsedSocks5Address = socks5AddressParser(socks5Address);
|
38 |
+
enableSocks = true;
|
39 |
+
} catch (err) {
|
40 |
+
/** @type {Error} */ let e = err;
|
41 |
+
console.log(e.toString());
|
42 |
+
enableSocks = false;
|
43 |
+
}
|
44 |
+
}
|
45 |
+
const upgradeHeader = request.headers.get('Upgrade');
|
46 |
+
if (!upgradeHeader || upgradeHeader !== 'websocket') {
|
47 |
+
const url = new URL(request.url);
|
48 |
+
switch (url.pathname) {
|
49 |
+
case '/':
|
50 |
+
return new Response(JSON.stringify(request.cf), { status: 200 });
|
51 |
+
case `/${userID}`: {
|
52 |
+
const vlessConfig = getVLESSConfig(userID, request.headers.get('Host'));
|
53 |
+
return new Response(`${vlessConfig}`, {
|
54 |
+
status: 200,
|
55 |
+
headers: {
|
56 |
+
"Content-Type": "text/plain;charset=utf-8",
|
57 |
+
}
|
58 |
+
});
|
59 |
+
}
|
60 |
+
default:
|
61 |
+
return new Response('Not found', { status: 404 });
|
62 |
+
}
|
63 |
+
} else {
|
64 |
+
return await vlessOverWSHandler(request);
|
65 |
+
}
|
66 |
+
} catch (err) {
|
67 |
+
/** @type {Error} */ let e = err;
|
68 |
+
return new Response(e.toString());
|
69 |
+
}
|
70 |
+
},
|
71 |
+
};
|
72 |
+
|
73 |
+
|
74 |
+
|
75 |
+
|
76 |
+
/**
|
77 |
+
*
|
78 |
+
* @param {import("@cloudflare/workers-types").Request} request
|
79 |
+
*/
|
80 |
+
async function vlessOverWSHandler(request) {
|
81 |
+
|
82 |
+
/** @type {import("@cloudflare/workers-types").WebSocket[]} */
|
83 |
+
// @ts-ignore
|
84 |
+
const webSocketPair = new WebSocketPair();
|
85 |
+
const [client, webSocket] = Object.values(webSocketPair);
|
86 |
+
|
87 |
+
webSocket.accept();
|
88 |
+
|
89 |
+
let address = '';
|
90 |
+
let portWithRandomLog = '';
|
91 |
+
const log = (/** @type {string} */ info, /** @type {string | undefined} */ event) => {
|
92 |
+
console.log(`[${address}:${portWithRandomLog}] ${info}`, event || '');
|
93 |
+
};
|
94 |
+
const earlyDataHeader = request.headers.get('sec-websocket-protocol') || '';
|
95 |
+
|
96 |
+
const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log);
|
97 |
+
|
98 |
+
/** @type {{ value: import("@cloudflare/workers-types").Socket | null}}*/
|
99 |
+
let remoteSocketWapper = {
|
100 |
+
value: null,
|
101 |
+
};
|
102 |
+
let isDns = false;
|
103 |
+
|
104 |
+
// ws --> remote
|
105 |
+
readableWebSocketStream.pipeTo(new WritableStream({
|
106 |
+
async write(chunk, controller) {
|
107 |
+
if (isDns) {
|
108 |
+
return await handleDNSQuery(chunk, webSocket, null, log);
|
109 |
+
}
|
110 |
+
if (remoteSocketWapper.value) {
|
111 |
+
const writer = remoteSocketWapper.value.writable.getWriter()
|
112 |
+
await writer.write(chunk);
|
113 |
+
writer.releaseLock();
|
114 |
+
return;
|
115 |
+
}
|
116 |
+
|
117 |
+
const {
|
118 |
+
hasError,
|
119 |
+
message,
|
120 |
+
addressType,
|
121 |
+
portRemote = 443,
|
122 |
+
addressRemote = '',
|
123 |
+
rawDataIndex,
|
124 |
+
vlessVersion = new Uint8Array([0, 0]),
|
125 |
+
isUDP,
|
126 |
+
} = processVlessHeader(chunk, userID);
|
127 |
+
address = addressRemote;
|
128 |
+
portWithRandomLog = `${portRemote}--${Math.random()} ${isUDP ? 'udp ' : 'tcp '
|
129 |
+
} `;
|
130 |
+
if (hasError) {
|
131 |
+
// controller.error(message);
|
132 |
+
throw new Error(message); // cf seems has bug, controller.error will not end stream
|
133 |
+
// webSocket.close(1000, message);
|
134 |
+
return;
|
135 |
+
}
|
136 |
+
// if UDP but port not DNS port, close it
|
137 |
+
if (isUDP) {
|
138 |
+
if (portRemote === 53) {
|
139 |
+
isDns = true;
|
140 |
+
} else {
|
141 |
+
// controller.error('UDP proxy only enable for DNS which is port 53');
|
142 |
+
throw new Error('UDP proxy only enable for DNS which is port 53'); // cf seems has bug, controller.error will not end stream
|
143 |
+
return;
|
144 |
+
}
|
145 |
+
}
|
146 |
+
// ["version", "附加信息长度 N"]
|
147 |
+
const vlessResponseHeader = new Uint8Array([vlessVersion[0], 0]);
|
148 |
+
const rawClientData = chunk.slice(rawDataIndex);
|
149 |
+
|
150 |
+
if (isDns) {
|
151 |
+
return handleDNSQuery(rawClientData, webSocket, vlessResponseHeader, log);
|
152 |
+
}
|
153 |
+
handleTCPOutBound(remoteSocketWapper, addressType, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log);
|
154 |
+
},
|
155 |
+
close() {
|
156 |
+
log(`readableWebSocketStream is close`);
|
157 |
+
},
|
158 |
+
abort(reason) {
|
159 |
+
log(`readableWebSocketStream is abort`, JSON.stringify(reason));
|
160 |
+
},
|
161 |
+
})).catch((err) => {
|
162 |
+
log('readableWebSocketStream pipeTo error', err);
|
163 |
+
});
|
164 |
+
|
165 |
+
return new Response(null, {
|
166 |
+
status: 101,
|
167 |
+
// @ts-ignore
|
168 |
+
webSocket: client,
|
169 |
+
});
|
170 |
+
}
|
171 |
+
|
172 |
+
/**
|
173 |
+
* Handles outbound TCP connections.
|
174 |
+
*
|
175 |
+
* @param {any} remoteSocket
|
176 |
+
* @param {number} addressType The remote address type to connect to.
|
177 |
+
* @param {string} addressRemote The remote address to connect to.
|
178 |
+
* @param {number} portRemote The remote port to connect to.
|
179 |
+
* @param {Uint8Array} rawClientData The raw client data to write.
|
180 |
+
* @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to pass the remote socket to.
|
181 |
+
* @param {Uint8Array} vlessResponseHeader The VLESS response header.
|
182 |
+
* @param {function} log The logging function.
|
183 |
+
* @returns {Promise<void>} The remote socket.
|
184 |
+
*/
|
185 |
+
async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log,) {
|
186 |
+
async function connectAndWrite(address, port, socks = false) {
|
187 |
+
/** @type {import("@cloudflare/workers-types").Socket} */
|
188 |
+
const tcpSocket = socks ? await socks5Connect(addressType, address, port, log)
|
189 |
+
: connect({
|
190 |
+
hostname: address,
|
191 |
+
port: port,
|
192 |
+
});
|
193 |
+
remoteSocket.value = tcpSocket;
|
194 |
+
log(`connected to ${address}:${port}`);
|
195 |
+
const writer = tcpSocket.writable.getWriter();
|
196 |
+
await writer.write(rawClientData); // first write, normal is tls client hello
|
197 |
+
writer.releaseLock();
|
198 |
+
return tcpSocket;
|
199 |
+
}
|
200 |
+
|
201 |
+
// if the cf connect tcp socket have no incoming data, we retry to redirect ip
|
202 |
+
async function retry() {
|
203 |
+
if (enableSocks) {
|
204 |
+
tcpSocket = await connectAndWrite(addressRemote, portRemote, true);
|
205 |
+
} else {
|
206 |
+
tcpSocket = await connectAndWrite(proxyIP || addressRemote, portRemote);
|
207 |
+
}
|
208 |
+
// no matter retry success or not, close websocket
|
209 |
+
tcpSocket.closed.catch(error => {
|
210 |
+
console.log('retry tcpSocket closed error', error);
|
211 |
+
}).finally(() => {
|
212 |
+
safeCloseWebSocket(webSocket);
|
213 |
+
})
|
214 |
+
remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, null, log);
|
215 |
+
}
|
216 |
+
|
217 |
+
let tcpSocket = await connectAndWrite(addressRemote, portRemote);
|
218 |
+
|
219 |
+
// when remoteSocket is ready, pass to websocket
|
220 |
+
// remote--> ws
|
221 |
+
remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, retry, log);
|
222 |
+
}
|
223 |
+
|
224 |
+
/**
|
225 |
+
*
|
226 |
+
* @param {import("@cloudflare/workers-types").WebSocket} webSocketServer
|
227 |
+
* @param {string} earlyDataHeader for ws 0rtt
|
228 |
+
* @param {(info: string)=> void} log for ws 0rtt
|
229 |
+
*/
|
230 |
+
function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) {
|
231 |
+
let readableStreamCancel = false;
|
232 |
+
const stream = new ReadableStream({
|
233 |
+
start(controller) {
|
234 |
+
webSocketServer.addEventListener('message', (event) => {
|
235 |
+
if (readableStreamCancel) {
|
236 |
+
return;
|
237 |
+
}
|
238 |
+
const message = event.data;
|
239 |
+
controller.enqueue(message);
|
240 |
+
});
|
241 |
+
|
242 |
+
// The event means that the client closed the client -> server stream.
|
243 |
+
// However, the server -> client stream is still open until you call close() on the server side.
|
244 |
+
// The WebSocket protocol says that a separate close message must be sent in each direction to fully close the socket.
|
245 |
+
webSocketServer.addEventListener('close', () => {
|
246 |
+
// client send close, need close server
|
247 |
+
// if stream is cancel, skip controller.close
|
248 |
+
safeCloseWebSocket(webSocketServer);
|
249 |
+
if (readableStreamCancel) {
|
250 |
+
return;
|
251 |
+
}
|
252 |
+
controller.close();
|
253 |
+
}
|
254 |
+
);
|
255 |
+
webSocketServer.addEventListener('error', (err) => {
|
256 |
+
log('webSocketServer has error');
|
257 |
+
controller.error(err);
|
258 |
+
}
|
259 |
+
);
|
260 |
+
// for ws 0rtt
|
261 |
+
const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader);
|
262 |
+
if (error) {
|
263 |
+
controller.error(error);
|
264 |
+
} else if (earlyData) {
|
265 |
+
controller.enqueue(earlyData);
|
266 |
+
}
|
267 |
+
},
|
268 |
+
|
269 |
+
pull(controller) {
|
270 |
+
// if ws can stop read if stream is full, we can implement backpressure
|
271 |
+
// https://streams.spec.whatwg.org/#example-rs-push-backpressure
|
272 |
+
},
|
273 |
+
cancel(reason) {
|
274 |
+
// 1. pipe WritableStream has error, this cancel will called, so ws handle server close into here
|
275 |
+
// 2. if readableStream is cancel, all controller.close/enqueue need skip,
|
276 |
+
// 3. but from testing controller.error still work even if readableStream is cancel
|
277 |
+
if (readableStreamCancel) {
|
278 |
+
return;
|
279 |
+
}
|
280 |
+
log(`ReadableStream was canceled, due to ${reason}`)
|
281 |
+
readableStreamCancel = true;
|
282 |
+
safeCloseWebSocket(webSocketServer);
|
283 |
+
}
|
284 |
+
});
|
285 |
+
|
286 |
+
return stream;
|
287 |
+
|
288 |
+
}
|
289 |
+
|
290 |
+
// https://xtls.github.io/development/protocols/vless.html
|
291 |
+
// https://github.com/zizifn/excalidraw-backup/blob/main/v2ray-protocol.excalidraw
|
292 |
+
|
293 |
+
/**
|
294 |
+
*
|
295 |
+
* @param { ArrayBuffer} vlessBuffer
|
296 |
+
* @param {string} userID
|
297 |
+
* @returns
|
298 |
+
*/
|
299 |
+
function processVlessHeader(
|
300 |
+
vlessBuffer,
|
301 |
+
userID
|
302 |
+
) {
|
303 |
+
if (vlessBuffer.byteLength < 24) {
|
304 |
+
return {
|
305 |
+
hasError: true,
|
306 |
+
message: 'invalid data',
|
307 |
+
};
|
308 |
+
}
|
309 |
+
const version = new Uint8Array(vlessBuffer.slice(0, 1));
|
310 |
+
let isValidUser = false;
|
311 |
+
let isUDP = false;
|
312 |
+
if (stringify(new Uint8Array(vlessBuffer.slice(1, 17))) === userID) {
|
313 |
+
isValidUser = true;
|
314 |
+
}
|
315 |
+
if (!isValidUser) {
|
316 |
+
return {
|
317 |
+
hasError: true,
|
318 |
+
message: 'invalid user',
|
319 |
+
};
|
320 |
+
}
|
321 |
+
|
322 |
+
const optLength = new Uint8Array(vlessBuffer.slice(17, 18))[0];
|
323 |
+
//skip opt for now
|
324 |
+
|
325 |
+
const command = new Uint8Array(
|
326 |
+
vlessBuffer.slice(18 + optLength, 18 + optLength + 1)
|
327 |
+
)[0];
|
328 |
+
|
329 |
+
// 0x01 TCP
|
330 |
+
// 0x02 UDP
|
331 |
+
// 0x03 MUX
|
332 |
+
if (command === 1) {
|
333 |
+
} else if (command === 2) {
|
334 |
+
isUDP = true;
|
335 |
+
} else {
|
336 |
+
return {
|
337 |
+
hasError: true,
|
338 |
+
message: `command ${command} is not support, command 01-tcp,02-udp,03-mux`,
|
339 |
+
};
|
340 |
+
}
|
341 |
+
const portIndex = 18 + optLength + 1;
|
342 |
+
const portBuffer = vlessBuffer.slice(portIndex, portIndex + 2);
|
343 |
+
// port is big-Endian in raw data etc 80 == 0x005d
|
344 |
+
const portRemote = new DataView(portBuffer).getUint16(0);
|
345 |
+
|
346 |
+
let addressIndex = portIndex + 2;
|
347 |
+
const addressBuffer = new Uint8Array(
|
348 |
+
vlessBuffer.slice(addressIndex, addressIndex + 1)
|
349 |
+
);
|
350 |
+
|
351 |
+
// 1--> ipv4 addressLength =4
|
352 |
+
// 2--> domain name addressLength=addressBuffer[1]
|
353 |
+
// 3--> ipv6 addressLength =16
|
354 |
+
const addressType = addressBuffer[0];
|
355 |
+
let addressLength = 0;
|
356 |
+
let addressValueIndex = addressIndex + 1;
|
357 |
+
let addressValue = '';
|
358 |
+
switch (addressType) {
|
359 |
+
case 1:
|
360 |
+
addressLength = 4;
|
361 |
+
addressValue = new Uint8Array(
|
362 |
+
vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength)
|
363 |
+
).join('.');
|
364 |
+
break;
|
365 |
+
case 2:
|
366 |
+
addressLength = new Uint8Array(
|
367 |
+
vlessBuffer.slice(addressValueIndex, addressValueIndex + 1)
|
368 |
+
)[0];
|
369 |
+
addressValueIndex += 1;
|
370 |
+
addressValue = new TextDecoder().decode(
|
371 |
+
vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength)
|
372 |
+
);
|
373 |
+
break;
|
374 |
+
case 3:
|
375 |
+
addressLength = 16;
|
376 |
+
const dataView = new DataView(
|
377 |
+
vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength)
|
378 |
+
);
|
379 |
+
// 2001:0db8:85a3:0000:0000:8a2e:0370:7334
|
380 |
+
const ipv6 = [];
|
381 |
+
for (let i = 0; i < 8; i++) {
|
382 |
+
ipv6.push(dataView.getUint16(i * 2).toString(16));
|
383 |
+
}
|
384 |
+
addressValue = ipv6.join(':');
|
385 |
+
// seems no need add [] for ipv6
|
386 |
+
break;
|
387 |
+
default:
|
388 |
+
return {
|
389 |
+
hasError: true,
|
390 |
+
message: `invild addressType is ${addressType}`,
|
391 |
+
};
|
392 |
+
}
|
393 |
+
if (!addressValue) {
|
394 |
+
return {
|
395 |
+
hasError: true,
|
396 |
+
message: `addressValue is empty, addressType is ${addressType}`,
|
397 |
+
};
|
398 |
+
}
|
399 |
+
|
400 |
+
return {
|
401 |
+
hasError: false,
|
402 |
+
addressRemote: addressValue,
|
403 |
+
addressType,
|
404 |
+
portRemote,
|
405 |
+
rawDataIndex: addressValueIndex + addressLength,
|
406 |
+
vlessVersion: version,
|
407 |
+
isUDP,
|
408 |
+
};
|
409 |
+
}
|
410 |
+
|
411 |
+
|
412 |
+
/**
|
413 |
+
*
|
414 |
+
* @param {import("@cloudflare/workers-types").Socket} remoteSocket
|
415 |
+
* @param {import("@cloudflare/workers-types").WebSocket} webSocket
|
416 |
+
* @param {ArrayBuffer} vlessResponseHeader
|
417 |
+
* @param {(() => Promise<void>) | null} retry
|
418 |
+
* @param {*} log
|
419 |
+
*/
|
420 |
+
async function remoteSocketToWS(remoteSocket, webSocket, vlessResponseHeader, retry, log) {
|
421 |
+
// remote--> ws
|
422 |
+
let remoteChunkCount = 0;
|
423 |
+
let chunks = [];
|
424 |
+
/** @type {ArrayBuffer | null} */
|
425 |
+
let vlessHeader = vlessResponseHeader;
|
426 |
+
let hasIncomingData = false; // check if remoteSocket has incoming data
|
427 |
+
await remoteSocket.readable
|
428 |
+
.pipeTo(
|
429 |
+
new WritableStream({
|
430 |
+
start() {
|
431 |
+
},
|
432 |
+
/**
|
433 |
+
*
|
434 |
+
* @param {Uint8Array} chunk
|
435 |
+
* @param {*} controller
|
436 |
+
*/
|
437 |
+
async write(chunk, controller) {
|
438 |
+
hasIncomingData = true;
|
439 |
+
// remoteChunkCount++;
|
440 |
+
if (webSocket.readyState !== WS_READY_STATE_OPEN) {
|
441 |
+
controller.error(
|
442 |
+
'webSocket.readyState is not open, maybe close'
|
443 |
+
);
|
444 |
+
}
|
445 |
+
if (vlessHeader) {
|
446 |
+
webSocket.send(await new Blob([vlessHeader, chunk]).arrayBuffer());
|
447 |
+
vlessHeader = null;
|
448 |
+
} else {
|
449 |
+
// seems no need rate limit this, CF seems fix this??..
|
450 |
+
// if (remoteChunkCount > 20000) {
|
451 |
+
// // cf one package is 4096 byte(4kb), 4096 * 20000 = 80M
|
452 |
+
// await delay(1);
|
453 |
+
// }
|
454 |
+
webSocket.send(chunk);
|
455 |
+
}
|
456 |
+
},
|
457 |
+
close() {
|
458 |
+
log(`remoteConnection!.readable is close with hasIncomingData is ${hasIncomingData}`);
|
459 |
+
// 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.
|
460 |
+
},
|
461 |
+
abort(reason) {
|
462 |
+
console.error(`remoteConnection!.readable abort`, reason);
|
463 |
+
},
|
464 |
+
})
|
465 |
+
)
|
466 |
+
.catch((error) => {
|
467 |
+
console.error(
|
468 |
+
`remoteSocketToWS has exception `,
|
469 |
+
error.stack || error
|
470 |
+
);
|
471 |
+
safeCloseWebSocket(webSocket);
|
472 |
+
});
|
473 |
+
|
474 |
+
// seems is cf connect socket have error,
|
475 |
+
// 1. Socket.closed will have error
|
476 |
+
// 2. Socket.readable will be close without any data coming
|
477 |
+
if (hasIncomingData === false && retry) {
|
478 |
+
log(`retry`)
|
479 |
+
retry();
|
480 |
+
}
|
481 |
+
}
|
482 |
+
|
483 |
+
/**
|
484 |
+
*
|
485 |
+
* @param {string} base64Str
|
486 |
+
* @returns
|
487 |
+
*/
|
488 |
+
function base64ToArrayBuffer(base64Str) {
|
489 |
+
if (!base64Str) {
|
490 |
+
return { error: null };
|
491 |
+
}
|
492 |
+
try {
|
493 |
+
// go use modified Base64 for URL rfc4648 which js atob not support
|
494 |
+
base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/');
|
495 |
+
const decode = atob(base64Str);
|
496 |
+
const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0));
|
497 |
+
return { earlyData: arryBuffer.buffer, error: null };
|
498 |
+
} catch (error) {
|
499 |
+
return { error };
|
500 |
+
}
|
501 |
+
}
|
502 |
+
|
503 |
+
/**
|
504 |
+
* This is not real UUID validation
|
505 |
+
* @param {string} uuid
|
506 |
+
*/
|
507 |
+
function isValidUUID(uuid) {
|
508 |
+
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;
|
509 |
+
return uuidRegex.test(uuid);
|
510 |
+
}
|
511 |
+
|
512 |
+
const WS_READY_STATE_OPEN = 1;
|
513 |
+
const WS_READY_STATE_CLOSING = 2;
|
514 |
+
/**
|
515 |
+
* Normally, WebSocket will not has exceptions when close.
|
516 |
+
* @param {import("@cloudflare/workers-types").WebSocket} socket
|
517 |
+
*/
|
518 |
+
function safeCloseWebSocket(socket) {
|
519 |
+
try {
|
520 |
+
if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) {
|
521 |
+
socket.close();
|
522 |
+
}
|
523 |
+
} catch (error) {
|
524 |
+
console.error('safeCloseWebSocket error', error);
|
525 |
+
}
|
526 |
+
}
|
527 |
+
|
528 |
+
const byteToHex = [];
|
529 |
+
for (let i = 0; i < 256; ++i) {
|
530 |
+
byteToHex.push((i + 256).toString(16).slice(1));
|
531 |
+
}
|
532 |
+
function unsafeStringify(arr, offset = 0) {
|
533 |
+
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();
|
534 |
+
}
|
535 |
+
function stringify(arr, offset = 0) {
|
536 |
+
const uuid = unsafeStringify(arr, offset);
|
537 |
+
if (!isValidUUID(uuid)) {
|
538 |
+
throw TypeError("Stringified UUID is invalid");
|
539 |
+
}
|
540 |
+
return uuid;
|
541 |
+
}
|
542 |
+
|
543 |
+
/**
|
544 |
+
*
|
545 |
+
* @param {ArrayBuffer} udpChunk
|
546 |
+
* @param {import("@cloudflare/workers-types").WebSocket} webSocket
|
547 |
+
* @param {ArrayBuffer} vlessResponseHeader
|
548 |
+
* @param {(string)=> void} log
|
549 |
+
*/
|
550 |
+
async function handleDNSQuery(udpChunk, webSocket, vlessResponseHeader, log) {
|
551 |
+
// no matter which DNS server client send, we alwasy use hard code one.
|
552 |
+
// beacsue someof DNS server is not support DNS over TCP
|
553 |
+
try {
|
554 |
+
const dnsServer = '8.8.4.4'; // change to 1.1.1.1 after cf fix connect own ip bug
|
555 |
+
const dnsPort = 53;
|
556 |
+
/** @type {ArrayBuffer | null} */
|
557 |
+
let vlessHeader = vlessResponseHeader;
|
558 |
+
/** @type {import("@cloudflare/workers-types").Socket} */
|
559 |
+
const tcpSocket = connect({
|
560 |
+
hostname: dnsServer,
|
561 |
+
port: dnsPort,
|
562 |
+
});
|
563 |
+
|
564 |
+
log(`connected to ${dnsServer}:${dnsPort}`);
|
565 |
+
const writer = tcpSocket.writable.getWriter();
|
566 |
+
await writer.write(udpChunk);
|
567 |
+
writer.releaseLock();
|
568 |
+
await tcpSocket.readable.pipeTo(new WritableStream({
|
569 |
+
async write(chunk) {
|
570 |
+
if (webSocket.readyState === WS_READY_STATE_OPEN) {
|
571 |
+
if (vlessHeader) {
|
572 |
+
webSocket.send(await new Blob([vlessHeader, chunk]).arrayBuffer());
|
573 |
+
vlessHeader = null;
|
574 |
+
} else {
|
575 |
+
webSocket.send(chunk);
|
576 |
+
}
|
577 |
+
}
|
578 |
+
},
|
579 |
+
close() {
|
580 |
+
log(`dns server(${dnsServer}) tcp is close`);
|
581 |
+
},
|
582 |
+
abort(reason) {
|
583 |
+
console.error(`dns server(${dnsServer}) tcp is abort`, reason);
|
584 |
+
},
|
585 |
+
}));
|
586 |
+
} catch (error) {
|
587 |
+
console.error(
|
588 |
+
`handleDNSQuery have exception, error: ${error.message}`
|
589 |
+
);
|
590 |
+
}
|
591 |
+
}
|
592 |
+
|
593 |
+
/**
|
594 |
+
*
|
595 |
+
* @param {number} addressType
|
596 |
+
* @param {string} addressRemote
|
597 |
+
* @param {number} portRemote
|
598 |
+
* @param {function} log The logging function.
|
599 |
+
*/
|
600 |
+
async function socks5Connect(addressType, addressRemote, portRemote, log) {
|
601 |
+
const { username, password, hostname, port } = parsedSocks5Address;
|
602 |
+
// Connect to the SOCKS server
|
603 |
+
const socket = connect({
|
604 |
+
hostname,
|
605 |
+
port,
|
606 |
+
});
|
607 |
+
|
608 |
+
// Request head format (Worker -> Socks Server):
|
609 |
+
// +----+----------+----------+
|
610 |
+
// |VER | NMETHODS | METHODS |
|
611 |
+
// +----+----------+----------+
|
612 |
+
// | 1 | 1 | 1 to 255 |
|
613 |
+
// +----+----------+----------+
|
614 |
+
|
615 |
+
// https://en.wikipedia.org/wiki/SOCKS#SOCKS5
|
616 |
+
// For METHODS:
|
617 |
+
// 0x00 NO AUTHENTICATION REQUIRED
|
618 |
+
// 0x02 USERNAME/PASSWORD https://datatracker.ietf.org/doc/html/rfc1929
|
619 |
+
const socksGreeting = new Uint8Array([5, 2, 0, 2]);
|
620 |
+
|
621 |
+
const writer = socket.writable.getWriter();
|
622 |
+
|
623 |
+
await writer.write(socksGreeting);
|
624 |
+
log('sent socks greeting');
|
625 |
+
|
626 |
+
const reader = socket.readable.getReader();
|
627 |
+
const encoder = new TextEncoder();
|
628 |
+
let res = (await reader.read()).value;
|
629 |
+
// Response format (Socks Server -> Worker):
|
630 |
+
// +----+--------+
|
631 |
+
// |VER | METHOD |
|
632 |
+
// +----+--------+
|
633 |
+
// | 1 | 1 |
|
634 |
+
// +----+--------+
|
635 |
+
if (res[0] !== 0x05) {
|
636 |
+
log(`socks server version error: ${res[0]} expected: 5`);
|
637 |
+
return;
|
638 |
+
}
|
639 |
+
if (res[1] === 0xff) {
|
640 |
+
log("no acceptable methods");
|
641 |
+
return;
|
642 |
+
}
|
643 |
+
|
644 |
+
// if return 0x0502
|
645 |
+
if (res[1] === 0x02) {
|
646 |
+
log("socks server needs auth");
|
647 |
+
if (!username || !password) {
|
648 |
+
log("please provide username/password");
|
649 |
+
return;
|
650 |
+
}
|
651 |
+
// +----+------+----------+------+----------+
|
652 |
+
// |VER | ULEN | UNAME | PLEN | PASSWD |
|
653 |
+
// +----+------+----------+------+----------+
|
654 |
+
// | 1 | 1 | 1 to 255 | 1 | 1 to 255 |
|
655 |
+
// +----+------+----------+------+----------+
|
656 |
+
const authRequest = new Uint8Array([
|
657 |
+
1,
|
658 |
+
username.length,
|
659 |
+
...encoder.encode(username),
|
660 |
+
password.length,
|
661 |
+
...encoder.encode(password)
|
662 |
+
]);
|
663 |
+
await writer.write(authRequest);
|
664 |
+
res = (await reader.read()).value;
|
665 |
+
// expected 0x0100
|
666 |
+
if (res[0] !== 0x01 || res[1] !== 0x00) {
|
667 |
+
log("fail to auth socks server");
|
668 |
+
return;
|
669 |
+
}
|
670 |
+
}
|
671 |
+
|
672 |
+
// Request data format (Worker -> Socks Server):
|
673 |
+
// +----+-----+-------+------+----------+----------+
|
674 |
+
// |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
|
675 |
+
// +----+-----+-------+------+----------+----------+
|
676 |
+
// | 1 | 1 | X'00' | 1 | Variable | 2 |
|
677 |
+
// +----+-----+-------+------+----------+----------+
|
678 |
+
// ATYP: address type of following address
|
679 |
+
// 0x01: IPv4 address
|
680 |
+
// 0x03: Domain name
|
681 |
+
// 0x04: IPv6 address
|
682 |
+
// DST.ADDR: desired destination address
|
683 |
+
// DST.PORT: desired destination port in network octet order
|
684 |
+
|
685 |
+
// addressType
|
686 |
+
// 1--> ipv4 addressLength =4
|
687 |
+
// 2--> domain name
|
688 |
+
// 3--> ipv6 addressLength =16
|
689 |
+
let DSTADDR; // DSTADDR = ATYP + DST.ADDR
|
690 |
+
switch (addressType) {
|
691 |
+
case 1:
|
692 |
+
DSTADDR = new Uint8Array(
|
693 |
+
[1, ...addressRemote.split('.').map(Number)]
|
694 |
+
);
|
695 |
+
break;
|
696 |
+
case 2:
|
697 |
+
DSTADDR = new Uint8Array(
|
698 |
+
[3, addressRemote.length, ...encoder.encode(addressRemote)]
|
699 |
+
);
|
700 |
+
break;
|
701 |
+
case 3:
|
702 |
+
DSTADDR = new Uint8Array(
|
703 |
+
[4, ...addressRemote.split(':').flatMap(x => [parseInt(x.slice(0, 2), 16), parseInt(x.slice(2), 16)])]
|
704 |
+
);
|
705 |
+
break;
|
706 |
+
default:
|
707 |
+
log(`invild addressType is ${addressType}`);
|
708 |
+
return;
|
709 |
+
}
|
710 |
+
const socksRequest = new Uint8Array([5, 1, 0, ...DSTADDR, portRemote >> 8, portRemote & 0xff]);
|
711 |
+
await writer.write(socksRequest);
|
712 |
+
log('sent socks request');
|
713 |
+
|
714 |
+
res = (await reader.read()).value;
|
715 |
+
// Response format (Socks Server -> Worker):
|
716 |
+
// +----+-----+-------+------+----------+----------+
|
717 |
+
// |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
|
718 |
+
// +----+-----+-------+------+----------+----------+
|
719 |
+
// | 1 | 1 | X'00' | 1 | Variable | 2 |
|
720 |
+
// +----+-----+-------+------+----------+----------+
|
721 |
+
if (res[1] === 0x00) {
|
722 |
+
log("socks connection opened");
|
723 |
+
} else {
|
724 |
+
log("fail to open socks connection");
|
725 |
+
return;
|
726 |
+
}
|
727 |
+
writer.releaseLock();
|
728 |
+
reader.releaseLock();
|
729 |
+
return socket;
|
730 |
+
}
|
731 |
+
|
732 |
+
|
733 |
+
/**
|
734 |
+
*
|
735 |
+
* @param {string} address
|
736 |
+
*/
|
737 |
+
function socks5AddressParser(address) {
|
738 |
+
let [latter, former] = address.split("@").reverse();
|
739 |
+
let username, password, hostname, port;
|
740 |
+
if (former) {
|
741 |
+
const formers = former.split(":");
|
742 |
+
if (formers.length !== 2) {
|
743 |
+
throw new Error('Invalid SOCKS address format');
|
744 |
+
}
|
745 |
+
[username, password] = formers;
|
746 |
+
}
|
747 |
+
const latters = latter.split(":");
|
748 |
+
port = Number(latters.pop());
|
749 |
+
if (isNaN(port)) {
|
750 |
+
throw new Error('Invalid SOCKS address format');
|
751 |
+
}
|
752 |
+
hostname = latters.join(":");
|
753 |
+
const regex = /^\[.*\]$/;
|
754 |
+
if (hostname.includes(":") && !regex.test(hostname)) {
|
755 |
+
throw new Error('Invalid SOCKS address format');
|
756 |
+
}
|
757 |
+
return {
|
758 |
+
username,
|
759 |
+
password,
|
760 |
+
hostname,
|
761 |
+
port,
|
762 |
+
}
|
763 |
+
}
|
764 |
+
|
765 |
+
/**
|
766 |
+
*
|
767 |
+
* @param {string} userID
|
768 |
+
* @param {string | null} hostName
|
769 |
+
* @returns {string}
|
770 |
+
*/
|
771 |
+
function getVLESSConfig(userID, hostName) {
|
772 |
+
const protocol = "vless";
|
773 |
+
const vlessMain =
|
774 |
+
`${protocol}` +
|
775 |
+
`://${userID}@${hostName}:443`+
|
776 |
+
`?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#${hostName}`;
|
777 |
+
|
778 |
+
return `
|
779 |
+
################################################################
|
780 |
+
v2ray
|
781 |
+
---------------------------------------------------------------
|
782 |
+
${vlessMain}
|
783 |
+
---------------------------------------------------------------
|
784 |
+
################################################################
|
785 |
+
clash-meta
|
786 |
+
---------------------------------------------------------------
|
787 |
+
- type: vless
|
788 |
+
name: ${hostName}
|
789 |
+
server: ${hostName}
|
790 |
+
port: 443
|
791 |
+
uuid: ${userID}
|
792 |
+
network: ws
|
793 |
+
tls: true
|
794 |
+
udp: false
|
795 |
+
sni: ${hostName}
|
796 |
+
client-fingerprint: chrome
|
797 |
+
ws-opts:
|
798 |
+
path: "/?ed=2048"
|
799 |
+
headers:
|
800 |
+
host: ${hostName}
|
801 |
+
---------------------------------------------------------------
|
802 |
+
################################################################
|
803 |
+
`;
|
804 |
+
}
|
805 |
+
|
806 |
+
|