diff --git a/.gitignore b/.gitignore index 9edc8d8..c7e8845 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ Thumbs.db .env node_modules/ package-lock.json +socks5.js diff --git a/_worker.js b/_worker.js index 1fcc3d0..667bfd9 100644 --- a/_worker.js +++ b/_worker.js @@ -14,121 +14,123 @@ let proxyIP = proxyIPs[Math.floor(Math.random() * proxyIPs.length)]; // ipv6 proxyIP example remove comment to use // let proxyIP = "[2a01:4f8:c2c:123f:64:5:6810:c55a]" -let dohURL = 'https://sky.rethinkdns.com/1:-Pf_____9_8A_AMAIgE8kMABVDDmKOHTAKg='; // https://cloudflare-dns.com/dns-query or https://dns.google/dns-query +// Example: user:pass@host:port or host:port +let socks5Address = 'mute0857:Zxc13579@216.146.25.215:10858'; +let socks5Relay = false; if (!isValidUUID(userID)) { - throw new Error('uuid is invalid'); + 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, DNS_RESOLVER_URL: string, NODE_ID: int, API_HOST: string, API_TOKEN: string}} env - * @param {import("@cloudflare/workers-types").ExecutionContext} ctx + * @param {{UUID: string, PROXYIP: string, DNS_RESOLVER_URL: string, SOCKS5: string, SOCKS5_RELAY: string}} env + * @param {import("@cloudflare/workers-types").ExecutionContext} _ctx * @returns {Promise} */ - async fetch(request, env, ctx) { - // uuid_validator(request); + async fetch(request, env, _ctx) { try { - userID = env.UUID || userID; - proxyIP = env.PROXYIP || proxyIP; - dohURL = env.DNS_RESOLVER_URL || dohURL; - let userID_Path = userID; - if (userID.includes(',')) { - userID_Path = userID.split(',')[0]; + const { UUID, PROXYIP, DNS_RESOLVER_URL, SOCKS5, SOCKS5_RELAY } = env; + userID = UUID || userID; + proxyIP = PROXYIP || proxyIP; + dohURL = DNS_RESOLVER_URL || dohURL; + socks5Address = SOCKS5 || socks5Address; + socks5Relay = SOCKS5_RELAY || socks5Relay; + + if (socks5Address) { + try { + parsedSocks5Address = socks5AddressParser(socks5Address); + enableSocks = true; + } catch (err) { + console.log(err.toString()); + enableSocks = false; + } } - const upgradeHeader = request.headers.get('Upgrade'); - if (!upgradeHeader || upgradeHeader !== 'websocket') { - const url = new URL(request.url); + + const userID_Path = userID.includes(',') ? userID.split(',')[0] : userID; + const url = new URL(request.url); + const host = request.headers.get('Host'); + + if (request.headers.get('Upgrade') !== 'websocket') { switch (url.pathname) { - case `/cf`: { + case '/cf': return new Response(JSON.stringify(request.cf, null, 4), { status: 200, - headers: { - "Content-Type": "application/json;charset=utf-8", - }, + headers: { "Content-Type": "application/json;charset=utf-8" }, }); - } - case `/${userID_Path}`: { - const protocolConfig = getConfig(userID, request.headers.get('Host')); - return new Response(`${protocolConfig}`, { + case `/${userID_Path}`: + return new Response(getConfig(userID, host), { status: 200, - headers: { - "Content-Type": "text/html; charset=utf-8", - } + headers: { "Content-Type": "text/html; charset=utf-8" }, }); - }; - case `/sub/${userID_Path}`: { - const url = new URL(request.url); - const searchParams = url.searchParams; - const protocolSubConfig = GenSub(userID, request.headers.get('Host')); - // Construct and return response object - return new Response(btoa(protocolSubConfig), { + case `/sub/${userID_Path}`: + return new Response(btoa(GenSub(userID, host)), { status: 200, - headers: { - "Content-Type": "text/plain;charset=utf-8", - } + headers: { "Content-Type": "text/plain;charset=utf-8" }, }); - }; - case `/bestip/${userID_Path}`: { - const headers = request.headers; - const url = `https://sub.xf.free.hr/auto?host=${request.headers.get('Host')}&uuid=${userID}&path=/`; - const bestSubConfig = await fetch(url, { headers: headers }); - return bestSubConfig; - }; + case `/bestip/${userID_Path}`: + return fetch(`https://sub.xf.free.hr/auto?host=${host}&uuid=${userID}&path=/`, { headers: request.headers }); default: - // return new Response('Not found', { status: 404 }); - // For any other path, reverse proxy to 'ramdom website' and return the original response, caching it in the process - const randomHostname = hostnames[Math.floor(Math.random() * hostnames.length)]; - const newHeaders = new Headers(request.headers); - newHeaders.set('cf-connecting-ip', '1.2.3.4'); - newHeaders.set('x-forwarded-for', '1.2.3.4'); - newHeaders.set('x-real-ip', '1.2.3.4'); - newHeaders.set('referer', 'https://www.google.com/search?q=edtunnel'); - // Use fetch to proxy the request to 15 different domains - const proxyUrl = 'https://' + randomHostname + url.pathname + url.search; - let modifiedRequest = new Request(proxyUrl, { - method: request.method, - headers: newHeaders, - body: request.body, - redirect: 'manual', - }); - const proxyResponse = await fetch(modifiedRequest, { redirect: 'manual' }); - // Check for 302 or 301 redirect status and return an error response - if ([301, 302].includes(proxyResponse.status)) { - return new Response(`Redirects to ${randomHostname} are not allowed.`, { - status: 403, - statusText: 'Forbidden', - }); - } - // Return the response from the proxy server - return proxyResponse; + return handleDefaultPath(url, request); } } else { return await ProtocolOverWSHandler(request); } } catch (err) { - /** @type {Error} */ let e = err; - return new Response(e.toString()); + return new Response(err.toString()); } }, }; +async function handleDefaultPath(url, request) { + const randomHostname = hostnames[Math.floor(Math.random() * hostnames.length)]; + const newHeaders = new Headers(request.headers); + newHeaders.set('cf-connecting-ip', '1.2.3.4'); + newHeaders.set('x-forwarded-for', '1.2.3.4'); + newHeaders.set('x-real-ip', '1.2.3.4'); + newHeaders.set('referer', 'https://www.google.com/search?q=edtunnel'); + + const proxyUrl = 'https://' + randomHostname + url.pathname + url.search; + const modifiedRequest = new Request(proxyUrl, { + method: request.method, + headers: newHeaders, + body: request.body, + redirect: 'manual', + }); + + const proxyResponse = await fetch(modifiedRequest, { redirect: 'manual' }); + if ([301, 302].includes(proxyResponse.status)) { + return new Response(`Redirects to ${randomHostname} are not allowed.`, { + status: 403, + statusText: 'Forbidden', + }); + } + return proxyResponse; +} + /** * Handles protocol over WebSocket requests by creating a WebSocket pair, accepting the WebSocket connection, and processing the protocol header. * @param {import("@cloudflare/workers-types").Request} request The incoming request object. * @returns {Promise} A Promise that resolves to a WebSocket response object. */ async function ProtocolOverWSHandler(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 = ''; - let currentDate = new Date(); const log = (/** @type {string} */ info, /** @type {string | undefined} */ event) => { - console.log(`[${currentDate} ${address}:${portWithRandomLog}] ${info}`, event || ''); + console.log(`[${address}:${portWithRandomLog}] ${info}`, event || ''); }; const earlyDataHeader = request.headers.get('sec-websocket-protocol') || ''; @@ -138,14 +140,13 @@ async function ProtocolOverWSHandler(request) { let remoteSocketWapper = { value: null, }; - let udpStreamWrite = null; let isDns = false; // ws --> remote readableWebSocketStream.pipeTo(new WritableStream({ async write(chunk, controller) { - if (isDns && udpStreamWrite) { - return udpStreamWrite(chunk); + if (isDns) { + return await handleDNSQuery(chunk, webSocket, null, log); } if (remoteSocketWapper.value) { const writer = remoteSocketWapper.value.writable.getWriter() @@ -157,41 +158,40 @@ async function ProtocolOverWSHandler(request) { const { hasError, message, + addressType, portRemote = 443, addressRemote = '', rawDataIndex, - protocolVersion = new Uint8Array([0, 0]), + ProtocolVersion = new Uint8Array([0, 0]), isUDP, - } = processprotocolHeader(chunk, userID); + } = processProtocolHeader(chunk, userID); address = addressRemote; - portWithRandomLog = `${portRemote} ${isUDP ? 'udp' : 'tcp'} `; + 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 and not DNS port, close it - if (isUDP && portRemote !== 53) { - throw new Error('UDP proxy only enabled for DNS which is port 53'); - // cf seems has bug, controller.error will not end stream - } - - if (isUDP && portRemote === 53) { - isDns = true; + // 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 protocolResponseHeader = new Uint8Array([protocolVersion[0], 0]); + const ProtocolResponseHeader = new Uint8Array([ProtocolVersion[0], 0]); const rawClientData = chunk.slice(rawDataIndex); - // TODO: support udp here when cf runtime has udp support if (isDns) { - const { write } = await handleUDPOutBound(webSocket, protocolResponseHeader, log); - udpStreamWrite = write; - udpStreamWrite(rawClientData); - return; + return handleDNSQuery(rawClientData, webSocket, ProtocolResponseHeader, log); } - handleTCPOutBound(remoteSocketWapper, addressRemote, portRemote, rawClientData, webSocket, protocolResponseHeader, log); + handleTCPOutBound(remoteSocketWapper, addressType, addressRemote, portRemote, rawClientData, webSocket, ProtocolResponseHeader, log); }, close() { log(`readableWebSocketStream is close`); @@ -205,6 +205,7 @@ async function ProtocolOverWSHandler(request) { return new Response(null, { status: 101, + // @ts-ignore webSocket: client, }); } @@ -221,47 +222,48 @@ async function ProtocolOverWSHandler(request) { * @param {function} log The logging function. * @returns {Promise} The remote socket. */ -async function handleTCPOutBound(remoteSocket, addressRemote, portRemote, rawClientData, webSocket, protocolResponseHeader, log,) { - - /** - * Connects to a given address and port and writes data to the socket. - * @param {string} address The address to connect to. - * @param {number} port The port to connect to. - * @returns {Promise} A Promise that resolves to the connected socket. - */ - async function connectAndWrite(address, port) { +async function handleTCPOutBound(remoteSocket, addressType, addressRemote, portRemote, rawClientData, webSocket, ProtocolResponseHeader, log,) { + async function connectAndWrite(address, port, socks = false) { /** @type {import("@cloudflare/workers-types").Socket} */ - const tcpSocket = connect({ - hostname: address, - port: port, - }); + let tcpSocket; + if (socks5Relay) { + tcpSocket = await socks5Connect(addressType, address, port, log) + } else { + 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, nomal is tls client hello + await writer.write(rawClientData); // first write, normal is tls client hello writer.releaseLock(); return tcpSocket; } - /** - * Retries connecting to the remote address and port if the Cloudflare socket has no incoming data. - * @returns {Promise} A Promise that resolves when the retry is complete. - */ + // 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) + 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, protocolResponseHeader, null, log); + remoteSocketToWS(tcpSocket, webSocket, ProtocolResponseHeader, null, log); } - const tcpSocket = await connectAndWrite(addressRemote, portRemote); + let tcpSocket = await connectAndWrite(addressRemote, portRemote); // when remoteSocket is ready, pass to websocket // remote--> ws - remoteSocketToWS(tcpSocket, webSocket, protocolResponseHeader, retry, log); + remoteSocketToWS(tcpSocket, webSocket, ProtocolResponseHeader, retry, log); } /** @@ -297,7 +299,7 @@ function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) { } }, - pull(controller) { + pull(_controller) { // if ws can stop read if stream is full, we can implement backpressure // https://streams.spec.whatwg.org/#example-rs-push-backpressure }, @@ -330,113 +332,59 @@ function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) { * isUDP?: boolean * }} An object with the relevant information extracted from the protocol header buffer. */ -function processprotocolHeader(protocolBuffer, userID) { +function processProtocolHeader(protocolBuffer, userID) { if (protocolBuffer.byteLength < 24) { - return { - hasError: true, - message: 'invalid data', - }; + return { hasError: true, message: 'invalid data' }; } - const version = new Uint8Array(protocolBuffer.slice(0, 1)); - let isValidUser = false; - let isUDP = false; - const slicedBuffer = new Uint8Array(protocolBuffer.slice(1, 17)); - const slicedBufferString = stringify(slicedBuffer); - // check if userID is valid uuid or uuids split by , and contains userID in it otherwise return error message to console - const uuids = userID.includes(',') ? userID.split(",") : [userID]; - // uuid_validator(hostName, slicedBufferString); - + const dataView = new DataView(protocolBuffer); + const version = dataView.getUint8(0); + const slicedBufferString = stringify(new Uint8Array(protocolBuffer.slice(1, 17))); - // isValidUser = uuids.some(userUuid => slicedBufferString === userUuid.trim()); - isValidUser = uuids.some(userUuid => slicedBufferString === userUuid.trim()) || uuids.length === 1 && slicedBufferString === uuids[0].trim(); + const uuids = userID.includes(',') ? userID.split(",") : [userID]; + const isValidUser = uuids.some(uuid => slicedBufferString === uuid.trim()) || + (uuids.length === 1 && slicedBufferString === uuids[0].trim()); console.log(`userID: ${slicedBufferString}`); if (!isValidUser) { - return { - hasError: true, - message: 'invalid user', - }; + return { hasError: true, message: 'invalid user' }; } - const optLength = new Uint8Array(protocolBuffer.slice(17, 18))[0]; - //skip opt for now - - const command = new Uint8Array( - protocolBuffer.slice(18 + optLength, 18 + optLength + 1) - )[0]; + const optLength = dataView.getUint8(17); + const command = dataView.getUint8(18 + optLength); - // 0x01 TCP - // 0x02 UDP - // 0x03 MUX - if (command === 1) { - isUDP = false; - } else if (command === 2) { - isUDP = true; - } else { - return { - hasError: true, - message: `command ${command} is not support, command 01-tcp,02-udp,03-mux`, - }; + if (command !== 1 && command !== 2) { + return { hasError: true, message: `command ${command} is not supported, command 01-tcp,02-udp,03-mux` }; } - const portIndex = 18 + optLength + 1; - const portBuffer = protocolBuffer.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( - protocolBuffer.slice(addressIndex, addressIndex + 1) - ); + const portIndex = 18 + optLength + 1; + const portRemote = dataView.getUint16(portIndex); + const addressType = dataView.getUint8(portIndex + 2); + let addressValue, addressLength, addressValueIndex; - // 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( - protocolBuffer.slice(addressValueIndex, addressValueIndex + addressLength) - ).join('.'); + addressValueIndex = portIndex + 3; + addressValue = new Uint8Array(protocolBuffer.slice(addressValueIndex, addressValueIndex + addressLength)).join('.'); break; case 2: - addressLength = new Uint8Array( - protocolBuffer.slice(addressValueIndex, addressValueIndex + 1) - )[0]; - addressValueIndex += 1; - addressValue = new TextDecoder().decode( - protocolBuffer.slice(addressValueIndex, addressValueIndex + addressLength) - ); + addressLength = dataView.getUint8(portIndex + 3); + addressValueIndex = portIndex + 4; + addressValue = new TextDecoder().decode(protocolBuffer.slice(addressValueIndex, addressValueIndex + addressLength)); break; case 3: addressLength = 16; - const dataView = new DataView( - protocolBuffer.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 + addressValueIndex = portIndex + 3; + addressValue = Array.from({ length: 8 }, (_, i) => dataView.getUint16(addressValueIndex + i * 2).toString(16)).join(':'); break; default: - return { - hasError: true, - message: `invild addressType is ${addressType}`, - }; + return { hasError: true, message: `invalid addressType: ${addressType}` }; } + if (!addressValue) { - return { - hasError: true, - message: `addressValue is empty, addressType is ${addressType}`, - }; + return { hasError: true, message: `addressValue is empty, addressType is ${addressType}` }; } return { @@ -445,8 +393,8 @@ function processprotocolHeader(protocolBuffer, userID) { addressType, portRemote, rawDataIndex: addressValueIndex + addressLength, - protocolVersion: version, - isUDP, + protocolVersion: new Uint8Array([version]), + isUDP: command === 2 }; } @@ -461,69 +409,43 @@ function processprotocolHeader(protocolBuffer, userID) { * @returns {Promise} A Promise that resolves when the conversion is complete. */ async function remoteSocketToWS(remoteSocket, webSocket, protocolResponseHeader, retry, log) { - // remote--> ws - let remoteChunkCount = 0; - let chunks = []; - /** @type {ArrayBuffer | null} */ - let protocolHeader = protocolResponseHeader; - let hasIncomingData = false; // check if remoteSocket has incoming data - await remoteSocket.readable - .pipeTo( + let hasIncomingData = false; + + try { + await remoteSocket.readable.pipeTo( new WritableStream({ - start() { - }, - /** - * - * @param {Uint8Array} chunk - * @param {*} controller - */ - async write(chunk, controller) { - hasIncomingData = true; - remoteChunkCount++; + async write(chunk) { if (webSocket.readyState !== WS_READY_STATE_OPEN) { - controller.error( - 'webSocket.readyState is not open, maybe close' - ); + throw new Error('WebSocket is not open'); } - if (protocolHeader) { - webSocket.send(await new Blob([protocolHeader, chunk]).arrayBuffer()); - protocolHeader = null; + + hasIncomingData = true; + + if (protocolResponseHeader) { + webSocket.send(await new Blob([protocolResponseHeader, chunk]).arrayBuffer()); + protocolResponseHeader = null; } else { - // console.log(`remoteSocketToWS send chunk ${chunk.byteLength}`); - // 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. + log(`Remote connection readable closed. Had incoming data: ${hasIncomingData}`); }, abort(reason) { - console.error(`remoteConnection!.readable abort`, reason); + console.error(`Remote connection readable aborted:`, reason); }, }) - ) - .catch((error) => { - console.error( - `remoteSocketToWS has exception `, - error.stack || error - ); - safeCloseWebSocket(webSocket); - }); + ); + } catch (error) { + console.error(`remoteSocketToWS error:`, 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(); + if (!hasIncomingData && retry) { + log(`No incoming data, retrying`); + await retry(); } } - /** * Decodes a base64 string into an ArrayBuffer. * @param {string} base64Str The base64 string to decode. @@ -534,11 +456,17 @@ function base64ToArrayBuffer(base64Str) { return { earlyData: null, error: null }; } try { - // go use modified Base64 for URL rfc4648 which js atob not support + // Convert modified Base64 for URL (RFC 4648) to standard Base64 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 }; + // Decode Base64 string + const binaryStr = atob(base64Str); + // Convert binary string to ArrayBuffer + const buffer = new ArrayBuffer(binaryStr.length); + const view = new Uint8Array(buffer); + for (let i = 0; i < binaryStr.length; i++) { + view[i] = binaryStr.charCodeAt(i); + } + return { earlyData: buffer, error: null }; } catch (error) { return { earlyData: null, error }; } @@ -546,17 +474,18 @@ function base64ToArrayBuffer(base64Str) { /** * Checks if a given string is a valid UUID. - * Note: This is not a real UUID validation. * @param {string} uuid The string to validate as a UUID. * @returns {boolean} True if the string is a valid UUID, false otherwise. */ 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; + // More precise UUID regex pattern + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][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; + /** * Closes a WebSocket connection safely without throwing exceptions. * @param {import("@cloudflare/workers-types").WebSocket} socket The WebSocket connection to close. @@ -567,24 +496,41 @@ function safeCloseWebSocket(socket) { socket.close(); } } catch (error) { - console.error('safeCloseWebSocket error', error); + console.error('safeCloseWebSocket error:', error); } } -const byteToHex = []; - -for (let i = 0; i < 256; ++i) { - byteToHex.push((i + 256).toString(16).slice(1)); -} +const byteToHex = Array.from({ length: 256 }, (_, i) => (i + 0x100).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(); + return [ + byteToHex[arr[offset]], + 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]] + ].join('').toLowerCase(); } function stringify(arr, offset = 0) { const uuid = unsafeStringify(arr, offset); if (!isValidUUID(uuid)) { - throw TypeError("Stringified UUID is invalid"); + throw new TypeError("Stringified UUID is invalid"); } return uuid; } @@ -601,7 +547,7 @@ async function handleUDPOutBound(webSocket, protocolResponseHeader, log) { let isprotocolHeaderSent = false; const transformStream = new TransformStream({ - start(controller) { + start(_controller) { }, transform(chunk, controller) { @@ -617,7 +563,7 @@ async function handleUDPOutBound(webSocket, protocolResponseHeader, log) { controller.enqueue(udpData); } }, - flush(controller) { + flush(_controller) { } }); @@ -663,6 +609,229 @@ async function handleUDPOutBound(webSocket, protocolResponseHeader, log) { }; } +/** + * + * @param {ArrayBuffer} udpChunk + * @param {import("@cloudflare/workers-types").WebSocket} webSocket + * @param {ArrayBuffer} protocolResponseHeader + * @param {(string)=> void} log + */ +async function handleDNSQuery(udpChunk, webSocket, protocolResponseHeader, 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 = protocolResponseHeader; + /** @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, + } +} + + const at = 'QA=='; const pt = 'dmxlc3M='; const ed = 'RUR0dW5uZWw='; @@ -675,7 +844,6 @@ const ed = 'RUR0dW5uZWw='; */ function getConfig(userIDs, hostName) { const commonUrlPart = `:443?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#${hostName}`; - const hashSeparator = "################################################################"; // Split the userIDs into an array const userIDArray = userIDs.split(","); @@ -894,7 +1062,7 @@ const HttpsPort = new Set([443, 8443, 2053, 2096, 2087, 2083]); function GenSub(ไอดีผู้ใช้_เส้นทาง, ชื่อโฮสต์) { const อาร์เรย์ไอดีผู้ใช้ = ไอดีผู้ใช้_เส้นทาง.includes(',') ? ไอดีผู้ใช้_เส้นทาง.split(',') : [ไอดีผู้ใช้_เส้นทาง]; - const randomPath = () => '/' + Math.random().toString(36).substring(2, 15)+'?ed=2048'; + const randomPath = () => '/' + Math.random().toString(36).substring(2, 15) + '?ed=2048'; const ส่วนUrlทั่วไปHttp = `?encryption=none&security=none&fp=random&type=ws&host=${ชื่อโฮสต์}&path=${encodeURIComponent(randomPath())}#`; const ส่วนUrlทั่วไปHttps = `?encryption=none&security=tls&sni=${ชื่อโฮสต์}&fp=random&type=ws&host=${ชื่อโฮสต์}&path=%2F%3Fed%3D2048#`;