diff --git a/src/authentication/auth.js b/src/authentication/auth.js new file mode 100644 index 000000000..a814afb41 --- /dev/null +++ b/src/authentication/auth.js @@ -0,0 +1,36 @@ +import { SignJWT, jwtVerify } from 'jose'; + +export async function generateJWTToken (secretKey) { + const secret = new TextEncoder().encode(secretKey); + return await new SignJWT({ userID }) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime('24h') + .sign(secret); +} + +export function generateSecretKey () { + const key = nacl.randomBytes(32); + return Array.from(key, byte => byte.toString(16).padStart(2, '0')).join(''); +} + +export async function Authenticate (request, env) { + try { + const secretKey = await env.bpb.get('secretKey'); + const secret = new TextEncoder().encode(secretKey); + const cookie = request.headers.get('Cookie')?.match(/(^|;\s*)jwtToken=([^;]*)/); + const token = cookie ? cookie[2] : null; + + if (!token) { + console.log('Unauthorized: Token not available!'); + return false; + } + + const { payload } = await jwtVerify(token, secret); + console.log(`Successfully authenticated, User ID: ${payload.userID}`); + return true; + } catch (error) { + console.log(error); + return false; + } +} \ No newline at end of file diff --git a/src/cores/clash.js b/src/cores/clash.js new file mode 100644 index 000000000..ba7cd93aa --- /dev/null +++ b/src/cores/clash.js @@ -0,0 +1,495 @@ +import { getConfigAddresses, extractWireguardParams, generateRemark, randomUpperCase, getRandomPath, isIPv6 } from './helpers.js'; +import { configs } from '../helpers/config.js'; +import { isValidUUID } from '../helpers/helpers.js'; +let userID = configs.userID; +let trojanPassword = configs.userID; +const defaultHttpsPorts = configs.defaultHttpsPorts; + +async function buildClashDNS (proxySettings, isWarp) { + const { + remoteDNS, + resolvedRemoteDNS, + localDNS, + vlessTrojanFakeDNS, + enableIPv6, + warpFakeDNS, + warpEnableIPv6, + bypassIran, + bypassChina, + bypassRussia + } = proxySettings; + + const warpRemoteDNS = warpEnableIPv6 + ? ["1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001"] + : ["1.1.1.1", "1.0.0.1"]; + const isFakeDNS = (vlessTrojanFakeDNS && !isWarp) || (warpFakeDNS && isWarp); + const isIPv6 = (enableIPv6 && !isWarp) || (warpEnableIPv6 && isWarp); + const isBypass = bypassIran || bypassChina || bypassRussia; + const bypassRules = [ + { rule: bypassIran, geosite: "category-ir" }, + { rule: bypassChina, geosite: "cn" }, + { rule: bypassRussia, geosite: "category-ru" } + ]; + + let dns = { + "enable": true, + "listen": "0.0.0.0:1053", + "ipv6": isIPv6, + "respect-rules": true, + "nameserver": isWarp ? warpRemoteDNS : [remoteDNS], + "proxy-server-nameserver": [localDNS] + }; + + if (resolvedRemoteDNS.server && !isWarp) { + dns["hosts"] = { + [resolvedRemoteDNS.server]: resolvedRemoteDNS.staticIPs + }; + } + + if (isBypass) { + let geosites = []; + bypassRules.forEach(({ rule, geosite }) => { + rule && geosites.push(geosite) + }); + + dns["nameserver-policy"] = { + [`geosite:${geosites.join(',')}`]: [localDNS], + "www.gstatic.com": [localDNS] + }; + } + + if (isFakeDNS) Object.assign(dns, { + "enhanced-mode": "fake-ip", + "fake-ip-range": "198.18.0.1/16", + "fake-ip-filter": ["geosite:private"] + }); + + return dns; +} + +function buildClashRoutingRules (proxySettings) { + const { + localDNS, + bypassLAN, + bypassIran, + bypassChina, + bypassRussia, + blockAds, + blockPorn, + blockUDP443 + } = proxySettings; + + const isBypass = bypassIran || bypassChina || bypassLAN || bypassRussia; + const isBlock = blockAds || blockPorn; + let geositeDirectRules = [], geoipDirectRules = [], geositeBlockRules = []; + const geoRules = [ + { rule: bypassLAN, type: 'direct', geosite: "private", geoip: "private" }, + { rule: bypassIran, type: 'direct', geosite: "category-ir", geoip: "ir" }, + { rule: bypassChina, type: 'direct', geosite: "cn", geoip: "cn" }, + { rule: bypassRussia, type: 'direct', geosite: "category-ru", geoip: "ru" }, + { rule: blockAds, type: 'block', geosite: "category-ads-all" }, + { rule: blockAds, type: 'block', geosite: "category-ads-ir" }, + { rule: blockPorn, type: 'block', geosite: "category-porn" } + ]; + + if (isBypass || isBlock) { + geoRules.forEach(({ rule, type, geosite, geoip }) => { + if (rule) { + if (type === 'direct') { + geositeDirectRules.push(`GEOSITE,${geosite},DIRECT`); + geoipDirectRules.push(`GEOIP,${geoip},DIRECT,no-resolve`); + } else { + geositeBlockRules.push(`GEOSITE,${geosite},REJECT`); + } + } + }); + } + + let rules = [ + `AND,((IP-CIDR,${localDNS}/32),(DST-PORT,53)),DIRECT`, + ...geositeDirectRules, + ...geoipDirectRules, + ...geositeBlockRules + ]; + + blockUDP443 && rules.push("AND,((NETWORK,udp),(DST-PORT,443)),REJECT"); + rules.push("MATCH,✅ Selector"); + return rules; +} + +function buildClashVLESSOutbound (remark, address, port, host, sni, path, allowInsecure) { + const tls = defaultHttpsPorts.includes(port) ? true : false; + const addr = isIPv6(address) ? address.replace(/\[|\]/g, '') : address; + let outbound = { + "name": remark, + "type": "vless", + "server": addr, + "port": +port, + "uuid": userID, + "tls": tls, + "network": "ws", + "udp": true, + "ws-opts": { + "path": path, + "headers": { "host": host }, + "max-early-data": 2560, + "early-data-header-name": "Sec-WebSocket-Protocol" + } + }; + + if (tls) { + Object.assign(outbound, { + "servername": sni, + "alpn": ["h2", "http/1.1"], + "client-fingerprint": "random", + "skip-cert-verify": allowInsecure + }); + } + + return outbound; +} + +function buildClashTrojanOutbound (remark, address, port, host, sni, path, allowInsecure) { + const addr = isIPv6(address) ? address.replace(/\[|\]/g, '') : address; + return { + "name": remark, + "type": "trojan", + "server": addr, + "port": +port, + "password": trojanPassword, + "network": "ws", + "udp": true, + "ws-opts": { + "path": path, + "headers": { "host": host }, + "max-early-data": 2560, + "early-data-header-name": "Sec-WebSocket-Protocol" + }, + "sni": sni, + "alpn": ["h2", "http/1.1"], + "client-fingerprint": "random", + "skip-cert-verify": allowInsecure + }; +} + +function buildClashWarpOutbound (warpConfigs, remark, endpoint, chain) { + const ipv6Regex = /\[(.*?)\]/; + const portRegex = /[^:]*$/; + const endpointServer = endpoint.includes('[') ? endpoint.match(ipv6Regex)[1] : endpoint.split(':')[0]; + const endpointPort = endpoint.includes('[') ? +endpoint.match(portRegex)[0] : +endpoint.split(':')[1]; + const { + warpIPv6, + reserved, + publicKey, + privateKey + } = extractWireguardParams(warpConfigs, chain); + + return { + "name": remark, + "type": "wireguard", + "ip": "172.16.0.2/32", + "ipv6": warpIPv6, + "private-key": privateKey, + "server": endpointServer, + "port": endpointPort, + "public-key": publicKey, + "allowed-ips": ["0.0.0.0/0", "::/0"], + "reserved": reserved, + "udp": true, + "mtu": 1280, + "dialer-proxy": chain, + "remote-dns-resolve": true, + "dns": [ "1.1.1.1", "1.0.0.1" ] + }; +} + +function buildClashChainOutbound(chainProxyParams) { + if (["socks", "http"].includes(chainProxyParams.protocol)) { + const { protocol, host, port, user, pass } = chainProxyParams; + const proxyType = protocol === 'socks' ? 'socks5' : protocol; + return { + "name": "", + "type": proxyType, + "server": host, + "port": +port, + "dialer-proxy": "", + "username": user, + "password": pass + }; + } + + const { hostName, port, uuid, flow, security, type, sni, fp, alpn, pbk, sid, headerType, host, path, serviceName } = chainProxyParams; + let chainOutbound = { + "name": "💦 Chain Best Ping 💥", + "type": "vless", + "server": hostName, + "port": +port, + "udp": true, + "uuid": uuid, + "flow": flow, + "network": type, + "dialer-proxy": "💦 Best Ping 💥" + }; + + if (security === 'tls') { + const tlsAlpns = alpn ? alpn?.split(',') : []; + Object.assign(chainOutbound, { + "tls": true, + "servername": sni, + "alpn": tlsAlpns, + "client-fingerprint": fp + }); + } + + if (security === 'reality') Object.assign(chainOutbound, { + "tls": true, + "servername": sni, + "client-fingerprint": fp, + "reality-opts": { + "public-key": pbk, + "short-id": sid + } + }); + + if (headerType === 'http') { + const httpPaths = path?.split(','); + chainOutbound["http-opts"] = { + "method": "GET", + "path": httpPaths, + "headers": { + "Connection": ["keep-alive"], + "Content-Type": ["application/octet-stream"] + } + }; + } + + if (type === 'ws') { + const wsPath = path?.split('?ed=')[0]; + const earlyData = +path?.split('?ed=')[1]; + chainOutbound["ws-opts"] = { + "path": wsPath, + "headers": { + "Host": host + }, + "max-early-data": earlyData, + "early-data-header-name": "Sec-WebSocket-Protocol" + }; + } + + if (type === 'grpc') chainOutbound["grpc-opts"] = { + "grpc-service-name": serviceName + }; + + return chainOutbound; +} + +export async function getClashWarpConfig(proxySettings, warpConfigs) { + const { warpEndpoints, warpEnableIPv6 } = proxySettings; + let config = structuredClone(clashConfigTemp); + config.ipv6 = warpEnableIPv6; + config.dns = await buildClashDNS(proxySettings, true); + config.rules = buildClashRoutingRules(proxySettings); + const selector = config['proxy-groups'][0]; + const warpUrlTest = config['proxy-groups'][1]; + selector.proxies = ['💦 Warp - Best Ping 🚀', '💦 WoW - Best Ping 🚀']; + warpUrlTest.name = '💦 Warp - Best Ping 🚀'; + warpUrlTest.interval = +proxySettings.bestWarpInterval; + config['proxy-groups'].push(structuredClone(warpUrlTest)); + const WoWUrlTest = config['proxy-groups'][2]; + WoWUrlTest.name = '💦 WoW - Best Ping 🚀'; + let warpRemarks = [], WoWRemarks = []; + + warpEndpoints.split(',').forEach( (endpoint, index) => { + const warpRemark = `💦 ${index + 1} - Warp 🇮🇷`; + const WoWRemark = `💦 ${index + 1} - WoW 🌍`; + const warpOutbound = buildClashWarpOutbound(warpConfigs, warpRemark, endpoint, ''); + const WoWOutbound = buildClashWarpOutbound(warpConfigs, WoWRemark, endpoint, warpRemark); + config.proxies.push(WoWOutbound, warpOutbound); + warpRemarks.push(warpRemark); + WoWRemarks.push(WoWRemark); + warpUrlTest.proxies.push(warpRemark); + WoWUrlTest.proxies.push(WoWRemark); + }); + + selector.proxies.push(...warpRemarks, ...WoWRemarks); + return config; +} + +export async function getClashNormalConfig (env, proxySettings) { + let chainProxy; + userID = env.UUID || userID; + if (!isValidUUID(userID)) throw new Error(`Invalid UUID: ${userID}`); + trojanPassword = env.TROJAN_PASS || trojanPassword; + const hostName = globalThis.hostName; + const { + cleanIPs, + proxyIP, + ports, + vlessConfigs, + trojanConfigs, + outProxy, + outProxyParams, + customCdnAddrs, + customCdnHost, + customCdnSni, + bestVLESSTrojanInterval, + enableIPv6 + } = proxySettings; + + if (outProxy) { + const proxyParams = JSON.parse(outProxyParams); + try { + chainProxy = buildClashChainOutbound(proxyParams); + } catch (error) { + console.log('An error occured while parsing chain proxy: ', error); + chainProxy = undefined; + await env.bpb.put("proxySettings", JSON.stringify({ + ...proxySettings, + outProxy: '', + outProxyParams: {} + })); + } + } + + let config = structuredClone(clashConfigTemp); + config.ipv6 = enableIPv6; + config.dns = await buildClashDNS(proxySettings, false); + config.rules = buildClashRoutingRules(proxySettings); + const selector = config['proxy-groups'][0]; + const urlTest = config['proxy-groups'][1]; + selector.proxies = ['💦 Best Ping 💥']; + urlTest.name = '💦 Best Ping 💥'; + urlTest.interval = +bestVLESSTrojanInterval; + const Addresses = await getConfigAddresses(hostName, cleanIPs, enableIPv6); + const customCdnAddresses = customCdnAddrs ? customCdnAddrs.split(',') : []; + const totalAddresses = [...Addresses, ...customCdnAddresses]; + let proxyIndex = 1, path; + const protocols = [ + ...(vlessConfigs ? ['VLESS'] : []), + ...(trojanConfigs ? ['Trojan'] : []) + ]; + + protocols.forEach ( protocol => { + let protocolIndex = 1; + ports.forEach ( port => { + totalAddresses.forEach( addr => { + let VLESSOutbound, TrojanOutbound; + const isCustomAddr = customCdnAddresses.includes(addr); + const configType = isCustomAddr ? 'C' : ''; + const sni = isCustomAddr ? customCdnSni : randomUpperCase(hostName); + const host = isCustomAddr ? customCdnHost : hostName; + const remark = generateRemark(protocolIndex, port, addr, cleanIPs, protocol, configType).replace(' : ', ' - '); + + if (protocol === 'VLESS') { + path = `/${getRandomPath(16)}${proxyIP ? `/${btoa(proxyIP)}` : ''}`; + VLESSOutbound = buildClashVLESSOutbound( + chainProxy ? `proxy-${proxyIndex}` : remark, + addr, + port, + host, + sni, + path, + isCustomAddr + ); + config.proxies.push(VLESSOutbound); + selector.proxies.push(remark); + urlTest.proxies.push(remark); + } + + if (protocol === 'Trojan' && defaultHttpsPorts.includes(port)) { + path = `/tr${getRandomPath(16)}${proxyIP ? `/${btoa(proxyIP)}` : ''}`; + TrojanOutbound = buildClashTrojanOutbound( + chainProxy ? `proxy-${proxyIndex}` : remark, + addr, + port, + host, + sni, + path, + isCustomAddr + ); + config.proxies.push(TrojanOutbound); + selector.proxies.push(remark); + urlTest.proxies.push(remark); + } + + if (chainProxy) { + let chain = structuredClone(chainProxy); + chain['name'] = remark; + chain['dialer-proxy'] = `proxy-${proxyIndex}`; + config.proxies.push(chain); + } + + proxyIndex++; + protocolIndex++; + }); + }); + }); + + return config; +} + +const clashConfigTemp = { + "mixed-port": 7890, + "ipv6": true, + "allow-lan": true, + "mode": "rule", + "log-level": "info", + "keep-alive-interval": 30, + "unified-delay": false, + "geo-auto-update": true, + "geo-update-interval": 168, + "external-controller": "127.0.0.1:9090", + "external-ui-url": "https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip", + "external-ui": "ui", + "profile": { + "store-selected": true, + "store-fake-ip": true + }, + "dns": {}, + "tun": { + "enable": true, + "stack": "mixed", + "auto-route": true, + "strict-route": true, + "auto-detect-interface": true, + "dns-hijack": ["any:53"], + "mtu": 9000 + }, + "sniffer": { + "enable": true, + "force-dns-mapping": true, + "parse-pure-ip": true, + "override-destination": false, + "sniff": { + "HTTP": { + "ports": [80, 8080, 8880, 2052, 2082, 2086, 2095] + }, + "TLS": { + "ports": [443, 8443, 2053, 2083, 2087, 2096] + } + } + }, + "proxies": [], + "proxy-groups": [ + { + "name": "✅ Selector", + "type": "select", + "proxies": [] + }, + { + "name": "", + "type": "url-test", + "url": "https://www.gstatic.com/generate_204", + "interval": 30, + "tolerance": 50, + "proxies": [] + } + ], + "rules": [], + "ntp": { + "enable": true, + "server": "time.apple.com", + "port": 123, + "interval": 30 + } +}; \ No newline at end of file diff --git a/src/cores/helpers.js b/src/cores/helpers.js new file mode 100644 index 000000000..b6a6ce06d --- /dev/null +++ b/src/cores/helpers.js @@ -0,0 +1,70 @@ +import { resolveDNS, isDomain } from '../helpers/helpers.js'; + +export async function getConfigAddresses(hostName, cleanIPs, enableIPv6) { + const resolved = await resolveDNS(hostName); + const defaultIPv6 = enableIPv6 ? resolved.ipv6.map((ip) => `[${ip}]`) : [] + return [ + hostName, + 'www.speedtest.net', + ...resolved.ipv4, + ...defaultIPv6, + ...(cleanIPs ? cleanIPs.split(',') : []) + ]; +} + +export function extractWireguardParams(warpConfigs, isWoW) { + const index = isWoW ? 1 : 0; + const warpConfig = warpConfigs[index].account.config; + return { + warpIPv6: `${warpConfig.interface.addresses.v6}/128`, + reserved: warpConfig.client_id, + publicKey: warpConfig.peers[0].public_key, + privateKey: warpConfigs[index].privateKey, + }; +} + +export function generateRemark(index, port, address, cleanIPs, protocol, configType) { + let addressType; + const type = configType ? ` ${configType}` : ''; + + cleanIPs.includes(address) + ? addressType = 'Clean IP' + : addressType = isDomain(address) ? 'Domain': isIPv4(address) ? 'IPv4' : isIPv6(address) ? 'IPv6' : ''; + + return `💦 ${index} - ${protocol}${type} - ${addressType} : ${port}`; +} + +export function randomUpperCase (str) { + let result = ''; + for (let i = 0; i < str.length; i++) { + result += Math.random() < 0.5 ? str[i].toUpperCase() : str[i]; + } + return result; +} + +export function getRandomPath (length) { + let result = ''; + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const charactersLength = characters.length; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; +} + +export function base64ToDecimal (base64) { + const binaryString = atob(base64); + const hexString = Array.from(binaryString).map(char => char.charCodeAt(0).toString(16).padStart(2, '0')).join(''); + const decimalArray = hexString.match(/.{2}/g).map(hex => parseInt(hex, 16)); + return decimalArray; +} + +export function isIPv4(address) { + const ipv4Pattern = /^(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + return ipv4Pattern.test(address); +} + +export function isIPv6(address) { + const ipv6Pattern = /^\[(?:(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}|(?:[a-fA-F0-9]{1,4}:){1,7}:|::(?:[a-fA-F0-9]{1,4}:){0,7}|(?:[a-fA-F0-9]{1,4}:){1,6}:[a-fA-F0-9]{1,4}|(?:[a-fA-F0-9]{1,4}:){1,5}(?::[a-fA-F0-9]{1,4}){1,2}|(?:[a-fA-F0-9]{1,4}:){1,4}(?::[a-fA-F0-9]{1,4}){1,3}|(?:[a-fA-F0-9]{1,4}:){1,3}(?::[a-fA-F0-9]{1,4}){1,4}|(?:[a-fA-F0-9]{1,4}:){1,2}(?::[a-fA-F0-9]{1,4}){1,5}|[a-fA-F0-9]{1,4}:(?::[a-fA-F0-9]{1,4}){1,6})\]$/; + return ipv6Pattern.test(address); +} \ No newline at end of file diff --git a/src/cores/normalConfigs.js b/src/cores/normalConfigs.js new file mode 100644 index 000000000..cfa1c0785 --- /dev/null +++ b/src/cores/normalConfigs.js @@ -0,0 +1,77 @@ +import { getConfigAddresses, generateRemark, randomUpperCase, getRandomPath } from './helpers.js'; +import { configs } from '../helpers/config.js'; +import { isValidUUID } from '../helpers/helpers.js'; +let userID = configs.userID; +let trojanPassword = configs.userID; +const defaultHttpsPorts = configs.defaultHttpsPorts; + +export async function getNormalConfigs(env, proxySettings, client) { + userID = env.UUID || userID; + if (!isValidUUID(userID)) throw new Error(`Invalid UUID: ${userID}`); + trojanPassword = env.TROJAN_PASS || trojanPassword; + const hostName = globalThis.hostName; + const { + cleanIPs, + proxyIP, + ports, + vlessConfigs, + trojanConfigs , + outProxy, + customCdnAddrs, + customCdnHost, + customCdnSni, + enableIPv6 + } = proxySettings; + + let vlessConfs = '', trojanConfs = '', chainProxy = ''; + let proxyIndex = 1; + const Addresses = await getConfigAddresses(hostName, cleanIPs, enableIPv6); + const customCdnAddresses = customCdnAddrs ? customCdnAddrs.split(',') : []; + const totalAddresses = [...Addresses, ...customCdnAddresses]; + const alpn = client === 'singbox' ? 'http/1.1' : 'h2,http/1.1'; + const trojanPass = encodeURIComponent(trojanPassword); + const earlyData = client === 'singbox' + ? '&eh=Sec-WebSocket-Protocol&ed=2560' + : encodeURIComponent('?ed=2560'); + + ports.forEach(port => { + totalAddresses.forEach((addr, index) => { + const isCustomAddr = index > Addresses.length - 1; + const configType = isCustomAddr ? 'C' : ''; + const sni = isCustomAddr ? customCdnSni : randomUpperCase(hostName); + const host = isCustomAddr ? customCdnHost : hostName; + const path = `${getRandomPath(16)}${proxyIP ? `/${encodeURIComponent(btoa(proxyIP))}` : ''}${earlyData}`; + const vlessRemark = encodeURIComponent(generateRemark(proxyIndex, port, addr, cleanIPs, 'VLESS', configType)); + const trojanRemark = encodeURIComponent(generateRemark(proxyIndex, port, addr, cleanIPs, 'Trojan', configType)); + const tlsFields = defaultHttpsPorts.includes(port) + ? `&security=tls&sni=${sni}&fp=randomized&alpn=${alpn}` + : '&security=none'; + + if (vlessConfigs) { + vlessConfs += `${atob('dmxlc3M')}://${userID}@${addr}:${port}?path=/${path}&encryption=none&host=${host}&type=ws${tlsFields}#${vlessRemark}\n`; + } + + if (trojanConfigs) { + trojanConfs += `${atob('dHJvamFu')}://${trojanPass}@${addr}:${port}?path=/tr${path}&host=${host}&type=ws${tlsFields}#${trojanRemark}\n`; + } + + proxyIndex++; + }); + }); + + if (outProxy) { + let chainRemark = `#${encodeURIComponent('💦 Chain proxy 🔗')}`; + if (outProxy.startsWith('socks') || outProxy.startsWith('http')) { + const regex = /^(?:socks|http):\/\/([^@]+)@/; + const isUserPass = outProxy.match(regex); + const userPass = isUserPass ? isUserPass[1] : false; + chainProxy = userPass + ? outProxy.replace(userPass, btoa(userPass)) + chainRemark + : outProxy + chainRemark; + } else { + chainProxy = outProxy.split('#')[0] + chainRemark; + } + } + + return btoa(vlessConfs + trojanConfs + chainProxy); +} \ No newline at end of file diff --git a/src/cores/sing-box.js b/src/cores/sing-box.js new file mode 100644 index 000000000..f6818fba7 --- /dev/null +++ b/src/cores/sing-box.js @@ -0,0 +1,763 @@ +import { getConfigAddresses, extractWireguardParams, generateRemark, randomUpperCase, getRandomPath } from './helpers.js'; +import { configs } from '../helpers/config.js'; +import { isValidUUID } from '../helpers/helpers.js'; +let userID = configs.userID; +let trojanPassword = configs.userID; +const defaultHttpsPorts = configs.defaultHttpsPorts; + +function buildSingBoxDNS (proxySettings, isChain, isWarp) { + const { + remoteDNS, + localDNS, + vlessTrojanFakeDNS, + enableIPv6, + warpFakeDNS, + warpEnableIPv6, + bypassIran, + bypassChina, + bypassRussia, + blockAds, + blockPorn + } = proxySettings; + + let fakeip; + const isFakeDNS = (vlessTrojanFakeDNS && !isWarp) || (warpFakeDNS && isWarp); + const isIPv6 = (enableIPv6 && !isWarp) || (warpEnableIPv6 && isWarp); + const isBypass = bypassIran || bypassChina || bypassRussia; + const geoRules = [ + { rule: bypassIran, type: 'direct', ruleSet: "geosite-ir" }, + { rule: bypassChina, type: 'direct', ruleSet: "geosite-cn" }, + { rule: bypassRussia, type: 'direct', ruleSet: "geosite-category-ru" }, + { rule: true, type: 'block', ruleSet: "geosite-malware" }, + { rule: true, type: 'block', ruleSet: "geosite-phishing" }, + { rule: true, type: 'block', ruleSet: "geosite-cryptominers" }, + { rule: blockAds, type: 'block', ruleSet: "geosite-category-ads-all" }, + { rule: blockPorn, type: 'block', ruleSet: "geosite-nsfw" } + ]; + const servers = [ + { + address: isWarp ? "1.1.1.1" : remoteDNS, + address_resolver: "dns-direct", + strategy: isIPv6 ? "prefer_ipv4" : "ipv4_only", + detour: isChain ? 'proxy-1' : "proxy", + tag: "dns-remote" + }, + { + address: localDNS, + strategy: isIPv6 ? "prefer_ipv4" : "ipv4_only", + detour: "direct", + tag: "dns-direct" + }, + { + address: "rcode://success", + tag: "dns-block" + } + ]; + + let rules = [ + { + outbound: "any", + server: "dns-direct" + }, + { + domain: "www.gstatic.com", + server: "dns-direct" + }, + { + clash_mode: "block", + server: "dns-block" + }, + { + clash_mode: "direct", + server: "dns-direct" + }, + { + clash_mode: "global", + server: "dns-remote" + } + ]; + + let bypassRule = { + rule_set: [], + server: "dns-direct" + }; + + let blockRule = { + disable_cache: true, + rule_set: [], + server: "dns-block" + }; + + geoRules.forEach(({ rule, type, ruleSet }) => { + rule && type === 'direct' && bypassRule.rule_set.push(ruleSet); + rule && type === 'block' && blockRule.rule_set.push(ruleSet); + }); + + isBypass && rules.push(bypassRule); + rules.push(blockRule); + if (isFakeDNS) { + servers.push({ + address: "fakeip", + tag: "dns-fake" + }); + + rules.push({ + disable_cache: true, + inbound: "tun-in", + query_type: [ + "A", + "AAAA" + ], + server: "dns-fake" + }); + + fakeip = { + enabled: true, + inet4_range: "198.18.0.0/15" + }; + + if (isIPv6) fakeip.inet6_range = "fc00::/18"; + } + + return {servers, rules, fakeip}; +} + +function buildSingBoxRoutingRules (proxySettings) { + const { + bypassLAN, + bypassIran, + bypassChina, + bypassRussia, + blockAds, + blockPorn, + blockUDP443 + } = proxySettings; + + const isBypass = bypassIran || bypassChina || bypassRussia; + let rules = [ + { + inbound: "dns-in", + outbound: "dns-out" + }, + { + network: "udp", + port: 53, + outbound: "dns-out" + }, + { + clash_mode: "direct", + outbound: "direct" + }, + { + clash_mode: "block", + outbound: "block" + }, + { + clash_mode: "global", + outbound: "proxy" + } + ]; + + const geoRules = [ + { + rule: bypassIran, + type: 'direct', + ruleSet: { + geosite: "geosite-ir", + geoip: "geoip-ir", + geositeURL: "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-ir.srs", + geoipURL: "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geoip-ir.srs" + } + }, + { + rule: bypassChina, + type: 'direct', + ruleSet: { + geosite: "geosite-cn", + geoip: "geoip-cn", + geositeURL: "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs", + geoipURL: "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs" + } + }, + { + rule: bypassRussia, + type: 'direct', + ruleSet: { + geosite: "geosite-category-ru", + geoip: "geoip-ru", + geositeURL: "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-ru.srs", + geoipURL: "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-ru.srs" + } + }, + { + rule: true, + type: 'block', + ruleSet: { + geosite: "geosite-malware", + geoip: "geoip-malware", + geositeURL: "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-malware.srs", + geoipURL: "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geoip-malware.srs" + } + }, + { + rule: true, + type: 'block', + ruleSet: { + geosite: "geosite-phishing", + geoip: "geoip-phishing", + geositeURL: "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-phishing.srs", + geoipURL: "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geoip-phishing.srs" + } + }, + { + rule: true, + type: 'block', + ruleSet: { + geosite: "geosite-cryptominers", + geositeURL: "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-cryptominers.srs", + } + }, + { + rule: blockAds, + type: 'block', + ruleSet: { + geosite: "geosite-category-ads-all", + geositeURL: "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-category-ads-all.srs", + } + }, + { + rule: blockPorn, + type: 'block', + ruleSet: { + geosite: "geosite-nsfw", + geositeURL: "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-nsfw.srs", + } + }, + ]; + + bypassLAN && rules.push({ + ip_is_private: true, + outbound: "direct" + }); + + const createRule = (outbound) => ({ + rule_set: [], + outbound + }); + + const routingRuleSet = { + type: "remote", + tag: "", + format: "binary", + url: "", + download_detour: "direct" + }; + + let directRule = createRule('direct');; + let blockRule = createRule('block'); + let ruleSets = []; + + geoRules.forEach(({ rule, type, ruleSet }) => { + const { geosite, geoip, geositeURL, geoipURL } = ruleSet; + if (rule) { + if (type === 'direct') { + directRule.rule_set.unshift(geosite); + directRule.rule_set.push(geoip); + } else { + blockRule.rule_set.unshift(geosite); + geoip && blockRule.rule_set.push(geoip); + } + ruleSets.push({...routingRuleSet, tag: geosite, url: geositeURL}); + geoip && ruleSets.push({...routingRuleSet, tag: geoip, url: geoipURL}); + } + }); + + isBypass && rules.push(directRule); + rules.push(blockRule); + + blockUDP443 && rules.push({ + network: "udp", + port: 443, + protocol: "quic", + outbound: "block" + }); + + rules.push({ + ip_cidr: ["224.0.0.0/3", "ff00::/8"], + source_ip_cidr: ["224.0.0.0/3", "ff00::/8"], + outbound: "block" + }); + + return {rules: rules, rule_set: ruleSets}; +} + +function buildSingBoxVLESSOutbound (proxySettings, remark, address, port, host, sni, allowInsecure, isFragment) { + const { enableIPv6, lengthMin, lengthMax, intervalMin, intervalMax, proxyIP } = proxySettings; + const path = `/${getRandomPath(16)}${proxyIP ? `/${btoa(proxyIP)}` : ''}`; + const tls = defaultHttpsPorts.includes(port) ? true : false; + let outbound = { + type: "vless", + server: address, + server_port: +port, + domain_strategy: enableIPv6 ? "prefer_ipv4" : "ipv4_only", + uuid: userID, + tls: { + alpn: "http/1.1", + enabled: true, + insecure: allowInsecure, + server_name: sni, + utls: { + enabled: true, + fingerprint: "randomized" + } + }, + transport: { + early_data_header_name: "Sec-WebSocket-Protocol", + max_early_data: 2560, + headers: { + Host: host + }, + path: path, + type: "ws" + }, + tag: remark + }; + + if (!tls) delete outbound.tls; + if (isFragment) outbound.tls_fragment = { + enabled: true, + size: `${lengthMin}-${lengthMax}`, + sleep: `${intervalMin}-${intervalMax}` + }; + + return outbound; +} + +function buildSingBoxTrojanOutbound (proxySettings, remark, address, port, host, sni, allowInsecure, isFragment) { + const { enableIPv6, lengthMin, lengthMax, intervalMin, intervalMax, proxyIP } = proxySettings; + const path = `/tr${getRandomPath(16)}${proxyIP ? `/${btoa(proxyIP)}` : ''}`; + const tls = defaultHttpsPorts.includes(port) ? true : false; + let outbound = { + type: "trojan", + password: trojanPassword, + server: address, + server_port: +port, + domain_strategy: enableIPv6 ? "prefer_ipv4" : "ipv4_only", + tls: { + alpn: "http/1.1", + enabled: true, + insecure: allowInsecure, + server_name: sni, + utls: { + enabled: true, + fingerprint: "randomized" + } + }, + transport: { + early_data_header_name: "Sec-WebSocket-Protocol", + max_early_data: 2560, + headers: { + Host: host + }, + path: path, + type: "ws" + }, + tag: remark + } + + if (!tls) delete outbound.tls; + if (isFragment) outbound.tls_fragment = { + enabled: true, + size: `${lengthMin}-${lengthMax}`, + sleep: `${intervalMin}-${intervalMax}` + }; + + return outbound; +} + +function buildSingBoxWarpOutbound (proxySettings, warpConfigs, remark, endpoint, chain, client) { + const ipv6Regex = /\[(.*?)\]/; + const portRegex = /[^:]*$/; + const endpointServer = endpoint.includes('[') ? endpoint.match(ipv6Regex)[1] : endpoint.split(':')[0]; + const endpointPort = endpoint.includes('[') ? +endpoint.match(portRegex)[0] : +endpoint.split(':')[1]; + const { + warpEnableIPv6, + hiddifyNoiseMode, + noiseCountMin, + noiseCountMax, + noiseSizeMin, + noiseSizeMax, + noiseDelayMin, + noiseDelayMax + } = proxySettings; + + const { + warpIPv6, + reserved, + publicKey, + privateKey + } = extractWireguardParams(warpConfigs, chain); + + let outbound = { + local_address: [ + "172.16.0.2/32", + warpIPv6 + ], + mtu: 1280, + peer_public_key: publicKey, + private_key: privateKey, + reserved: reserved, + server: endpointServer, + server_port: endpointPort, + domain_strategy: warpEnableIPv6 ? "prefer_ipv4" : "ipv4_only", + type: "wireguard", + detour: chain, + tag: remark + }; + + client === 'hiddify' && Object.assign(outbound, { + fake_packets_mode: hiddifyNoiseMode, + fake_packets: noiseCountMin === noiseCountMax ? noiseCountMin : `${noiseCountMin}-${noiseCountMax}`, + fake_packets_size: noiseSizeMin === noiseSizeMax ? noiseSizeMin : `${noiseSizeMin}-${noiseSizeMax}`, + fake_packets_delay: noiseDelayMin === noiseDelayMax ? noiseDelayMin : `${noiseDelayMin}-${noiseDelayMax}` + }); + + return outbound; +} + +function buildSingBoxChainOutbound (chainProxyParams) { + if (["socks", "http"].includes(chainProxyParams.protocol)) { + const { protocol, host, port, user, pass } = chainProxyParams; + + let chainOutbound = { + type: protocol, + tag: "", + server: host, + server_port: +port, + username: user, + password: pass, + detour: "" + }; + + if (protocol === 'socks') chainOutbound.version = "5"; + return chainOutbound; + } + + const { hostName, port, uuid, flow, security, type, sni, fp, alpn, pbk, sid, headerType, host, path, serviceName } = chainProxyParams; + let chainOutbound = { + type: "vless", + tag: "", + server: hostName, + server_port: +port, + uuid: uuid, + flow: flow, + detour: "" + }; + + if (security === 'tls' || security === 'reality') { + const tlsAlpns = alpn ? alpn?.split(',').filter(value => value !== 'h2') : []; + chainOutbound.tls = { + enabled: true, + server_name: sni, + insecure: false, + alpn: tlsAlpns, + utls: { + enabled: true, + fingerprint: fp + } + }; + + if (security === 'reality') { + chainOutbound.tls.reality = { + enabled: true, + public_key: pbk, + short_id: sid + }; + + delete chainOutbound.tls.alpn; + } + } + + if (headerType === 'http') { + const httpHosts = host?.split(','); + chainOutbound.transport = { + type: "http", + host: httpHosts, + path: path, + method: "GET", + headers: { + "Connection": ["keep-alive"], + "Content-Type": ["application/octet-stream"] + }, + }; + } + + if (type === 'ws') { + const wsPath = path?.split('?ed=')[0]; + const earlyData = +path?.split('?ed=')[1] || 0; + chainOutbound.transport = { + type: "ws", + path: wsPath, + headers: { Host: host }, + max_early_data: earlyData, + early_data_header_name: "Sec-WebSocket-Protocol" + }; + } + + if (type === 'grpc') chainOutbound.transport = { + type: "grpc", + service_name: serviceName + }; + + return chainOutbound; +} + +export async function getSingBoxWarpConfig (proxySettings, warpConfigs, client) { + const { warpEndpoints } = proxySettings; + let config = structuredClone(singboxConfigTemp); + const dnsObject = buildSingBoxDNS(proxySettings, false, true); + const {rules, rule_set} = buildSingBoxRoutingRules(proxySettings); + config.dns.servers = dnsObject.servers; + config.dns.rules = dnsObject.rules; + if (dnsObject.fakeip) config.dns.fakeip = dnsObject.fakeip; + config.route.rules = rules; + config.route.rule_set = rule_set; + const selector = config.outbounds[0]; + const warpUrlTest = config.outbounds[1]; + const proIndicator = client === 'hiddify' ? ' Pro ' : ' '; + selector.outbounds = [`💦 Warp${proIndicator}- Best Ping 🚀`, `💦 WoW${proIndicator}- Best Ping 🚀`]; + config.outbounds.splice(2, 0, structuredClone(warpUrlTest)); + const WoWUrlTest = config.outbounds[2]; + warpUrlTest.tag = `💦 Warp${proIndicator}- Best Ping 🚀`; + warpUrlTest.interval = `${proxySettings.bestWarpInterval}s`; + WoWUrlTest.tag = `💦 WoW${proIndicator}- Best Ping 🚀`; + WoWUrlTest.interval = `${proxySettings.bestWarpInterval}s`; + let warpRemarks = [], WoWRemarks = []; + + warpEndpoints.split(',').forEach( (endpoint, index) => { + const warpRemark = `💦 ${index + 1} - Warp 🇮🇷`; + const WoWRemark = `💦 ${index + 1} - WoW 🌍`; + const warpOutbound = buildSingBoxWarpOutbound(proxySettings, warpConfigs, warpRemark, endpoint, '', client); + const WoWOutbound = buildSingBoxWarpOutbound(proxySettings, warpConfigs, WoWRemark, endpoint, warpRemark, client); + config.outbounds.push(WoWOutbound, warpOutbound); + warpRemarks.push(warpRemark); + WoWRemarks.push(WoWRemark); + warpUrlTest.outbounds.push(warpRemark); + WoWUrlTest.outbounds.push(WoWRemark); + }); + + selector.outbounds.push(...warpRemarks, ...WoWRemarks); + return config; +} + +export async function getSingBoxCustomConfig(env, proxySettings, isFragment) { + let chainProxyOutbound; + userID = env.UUID || userID; + if (!isValidUUID(userID)) throw new Error(`Invalid UUID: ${userID}`); + trojanPassword = env.TROJAN_PASS || trojanPassword; + const hostName = globalThis.hostName; + const { + cleanIPs, + ports, + vlessConfigs, + trojanConfigs, + outProxy, + outProxyParams, + customCdnAddrs, + customCdnHost, + customCdnSni, + bestVLESSTrojanInterval, + enableIPv6 + } = proxySettings; + + if (outProxy) { + const proxyParams = JSON.parse(outProxyParams); + try { + chainProxyOutbound = buildSingBoxChainOutbound(proxyParams); + } catch (error) { + console.log('An error occured while parsing chain proxy: ', error); + chainProxyOutbound = undefined; + await env.bpb.put("proxySettings", JSON.stringify({ + ...proxySettings, + outProxy: '', + outProxyParams: {} + })); + } + } + + let config = structuredClone(singboxConfigTemp); + const dnsObject = buildSingBoxDNS(proxySettings, chainProxyOutbound, false); + const {rules, rule_set} = buildSingBoxRoutingRules(proxySettings); + config.dns.servers = dnsObject.servers; + config.dns.rules = dnsObject.rules; + if (dnsObject.fakeip) config.dns.fakeip = dnsObject.fakeip; + config.route.rules = rules; + config.route.rule_set = rule_set; + const selector = config.outbounds[0]; + const urlTest = config.outbounds[1]; + selector.outbounds = ['💦 Best Ping 💥']; + urlTest.interval = `${bestVLESSTrojanInterval}s`; + urlTest.tag = '💦 Best Ping 💥'; + const Addresses = await getConfigAddresses(hostName, cleanIPs, enableIPv6); + const customCdnAddresses = customCdnAddrs ? customCdnAddrs.split(',') : []; + const totalAddresses = [...Addresses, ...customCdnAddresses]; + const totalPorts = ports.filter(port => isFragment ? defaultHttpsPorts.includes(port) : true); + let proxyIndex = 1; + const protocols = [ + ...(vlessConfigs ? ['VLESS'] : []), + ...(trojanConfigs ? ['Trojan'] : []) + ]; + + protocols.forEach ( protocol => { + let protocolIndex = 1; + totalPorts.forEach ( port => { + totalAddresses.forEach ( addr => { + let VLESSOutbound, TrojanOutbound; + const isCustomAddr = customCdnAddresses.includes(addr); + const configType = isCustomAddr ? 'C' : isFragment ? 'F' : ''; + const sni = isCustomAddr ? customCdnSni : randomUpperCase(hostName); + const host = isCustomAddr ? customCdnHost : hostName; + const remark = generateRemark(protocolIndex, port, addr, cleanIPs, protocol, configType); + + if (protocol === 'VLESS') { + VLESSOutbound = buildSingBoxVLESSOutbound ( + proxySettings, + chainProxyOutbound ? `proxy-${proxyIndex}` : remark, + addr, + port, + host, + sni, + isCustomAddr, + isFragment + ); + config.outbounds.push(VLESSOutbound); + } + + if (protocol === 'Trojan') { + TrojanOutbound = buildSingBoxTrojanOutbound ( + proxySettings, + chainProxyOutbound ? `proxy-${proxyIndex}` : remark, + addr, + port, + host, + sni, + isCustomAddr, + isFragment + ); + config.outbounds.push(TrojanOutbound); + } + + if (chainProxyOutbound) { + let chain = structuredClone(chainProxyOutbound); + chain.tag = remark; + chain.detour = `proxy-${proxyIndex}`; + config.outbounds.push(chain); + } + + selector.outbounds.push(remark); + urlTest.outbounds.push(remark); + proxyIndex++; + protocolIndex++; + }); + }); + }); + + return config; +} + +const singboxConfigTemp = { + log: { + level: "warn", + timestamp: true + }, + dns: { + servers: [], + rules: [], + independent_cache: true + }, + inbounds: [ + { + type: "direct", + tag: "dns-in", + listen: "0.0.0.0", + listen_port: 6450, + override_address: "8.8.8.8", + override_port: 53 + }, + { + type: "tun", + tag: "tun-in", + address: [ + "172.18.0.1/28", + "fdfe:dcba:9876::1/126" + ], + mtu: 9000, + auto_route: true, + strict_route: true, + stack: "mixed", + sniff: true, + sniff_override_destination: true + }, + { + type: "mixed", + tag: "mixed-in", + listen: "0.0.0.0", + listen_port: 2080, + sniff: true, + sniff_override_destination: false + } + ], + outbounds: [ + { + type: "selector", + tag: "proxy", + outbounds: [] + }, + { + type: "urltest", + tag: "", + outbounds: [], + url: "https://www.gstatic.com/generate_204", + interval: "" + }, + { + type: "direct", + tag: "direct" + }, + { + type: "block", + tag: "block" + }, + { + type: "dns", + tag: "dns-out" + } + ], + route: { + rules: [], + rule_set: [], + auto_detect_interface: true, + override_android_vpn: true, + final: "proxy" + }, + ntp: { + enabled: true, + server: "time.apple.com", + server_port: 123, + detour: "direct", + interval: "30m", + }, + experimental: { + cache_file: { + enabled: true, + store_fakeip: true + }, + clash_api: { + external_controller: "127.0.0.1:9090", + external_ui: "yacd", + external_ui_download_url: "https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip", + external_ui_download_detour: "direct", + default_mode: "rule" + } + } +}; \ No newline at end of file diff --git a/src/cores/xray.js b/src/cores/xray.js new file mode 100644 index 000000000..f7085905f --- /dev/null +++ b/src/cores/xray.js @@ -0,0 +1,903 @@ +import { resolveDNS, isDomain, isValidUUID } from '../helpers/helpers.js'; +import { getConfigAddresses, extractWireguardParams, base64ToDecimal, generateRemark, randomUpperCase, getRandomPath } from './helpers.js'; +import { configs } from '../helpers/config.js'; +let userID = configs.userID; +let trojanPassword = configs.userID; +const defaultHttpsPorts = configs.defaultHttpsPorts; + +async function buildXrayDNS (proxySettings, outboundAddrs, domainToStaticIPs, isWorkerLess, isBalancer, isWarp) { + const { + remoteDNS, + resolvedRemoteDNS, + localDNS, + vlessTrojanFakeDNS, + enableIPv6, + warpFakeDNS, + warpEnableIPv6, + blockAds, + bypassIran, + bypassChina, + blockPorn, + bypassRussia + } = proxySettings; + + const isBypass = bypassIran || bypassChina || bypassRussia; + const isBlock = blockAds || blockPorn; + const bypassRules = [ + { rule: bypassIran, domain: "geosite:category-ir", ip: "geoip:ir" }, + { rule: bypassChina, domain: "geosite:cn", ip: "geoip:cn" }, + { rule: bypassRussia, domain: "geosite:category-ru", ip: "geoip:ru" } + ]; + + const blockRules = [ + { rule: blockAds, host: "geosite:category-ads-all", address: ["127.0.0.1"] }, + { rule: blockAds, host: "geosite:category-ads-ir", address: ["127.0.0.1"] }, + { rule: blockPorn, host: "geosite:category-porn", address: ["127.0.0.1"] } + ]; + + const isFakeDNS = (vlessTrojanFakeDNS && !isWarp) || (warpFakeDNS && isWarp); + const isIPv6 = (enableIPv6 && !isWarp) || (warpEnableIPv6 && isWarp); + const outboundDomains = outboundAddrs.filter(address => isDomain(address)); + const isOutboundRule = outboundDomains.length > 0; + const outboundRules = outboundDomains.map(domain => `full:${domain}`); + isBalancer && outboundRules.push("full:www.gstatic.com"); + const finalRemoteDNS = isWorkerLess + ? ["https://cloudflare-dns.com/dns-query"] + : isWarp + ? warpEnableIPv6 + ? ["1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001"] + : ["1.1.1.1", "1.0.0.1"] + : [remoteDNS]; + + const dnsHost = {}; + isBlock && blockRules.forEach( ({ rule, host, address}) => { + if (rule) dnsHost[host] = address; + }); + + const staticIPs = domainToStaticIPs ? await resolveDNS(domainToStaticIPs) : undefined; + if (staticIPs) dnsHost[domainToStaticIPs] = enableIPv6 ? [...staticIPs.ipv4, ...staticIPs.ipv6] : staticIPs.ipv4; + if (resolvedRemoteDNS.server && !isWorkerLess && !isWarp) dnsHost[resolvedRemoteDNS.server] = resolvedRemoteDNS.staticIPs; + if (isWorkerLess) { + const domains = ["cloudflare-dns.com", "cloudflare.com", "dash.cloudflare.com"]; + const resolved = await Promise.all(domains.map(resolveDNS)); + const hostIPv4 = resolved.flatMap(r => r.ipv4); + const hostIPv6 = enableIPv6 ? resolved.flatMap(r => r.ipv6) : []; + dnsHost["cloudflare-dns.com"] = [ + ...hostIPv4, + ...hostIPv6 + ]; + } + + const hosts = Object.keys(dnsHost).length ? { hosts: dnsHost } : {}; + let dnsObject = { + ...hosts, + servers: finalRemoteDNS, + queryStrategy: isIPv6 ? "UseIP" : "UseIPv4", + tag: "dns", + }; + + isOutboundRule && dnsObject.servers.push({ + address: localDNS, + domains: outboundRules, + skipFallback: true + }); + + let localDNSServer = { + address: localDNS, + domains: [], + expectIPs: [], + skipFallback: true + }; + + if (!isWorkerLess && isBypass) { + bypassRules.forEach(({ rule, domain, ip }) => { + if (rule) { + localDNSServer.domains.push(domain); + localDNSServer.expectIPs.push(ip); + } + }); + + dnsObject.servers.push(localDNSServer); + } + + if (isFakeDNS) { + const fakeDNSServer = isBypass && !isWorkerLess + ? { address: "fakedns", domains: localDNSServer.domains } + : "fakedns"; + dnsObject.servers.unshift(fakeDNSServer); + } + + return dnsObject; +} + +function buildXrayRoutingRules (proxySettings, outboundAddrs, isChain, isBalancer, isWorkerLess) { + const { + localDNS, + bypassLAN, + bypassIran, + bypassChina, + bypassRussia, + blockAds, + blockPorn, + blockUDP443 + } = proxySettings; + + const isBlock = blockAds || blockPorn; + const isBypass = bypassIran || bypassChina || bypassRussia; + const geoRules = [ + { rule: bypassLAN, type: 'direct', domain: "geosite:private", ip: "geoip:private" }, + { rule: bypassIran, type: 'direct', domain: "geosite:category-ir", ip: "geoip:ir" }, + { rule: bypassChina, type: 'direct', domain: "geosite:cn", ip: "geoip:cn" }, + { rule: blockAds, type: 'block', domain: "geosite:category-ads-all" }, + { rule: blockAds, type: 'block', domain: "geosite:category-ads-ir" }, + { rule: blockPorn, type: 'block', domain: "geosite:category-porn" } + ]; + const outboundDomains = outboundAddrs.filter(address => isDomain(address)); + const isOutboundRule = outboundDomains.length > 0; + let rules = [ + { + inboundTag: [ + "dns-in" + ], + outboundTag: "dns-out", + type: "field" + }, + { + inboundTag: [ + "socks-in", + "http-in" + ], + port: "53", + outboundTag: "dns-out", + type: "field" + } + ]; + + if (!isWorkerLess && (isOutboundRule || isBypass)) rules.push({ + ip: [localDNS], + port: "53", + outboundTag: "direct", + type: "field" + }); + + if (isBypass || isBlock) { + const createRule = (type, outbound) => ({ + [type]: [], + outboundTag: outbound, + type: "field" + }); + + let geositeDirectRule, geoipDirectRule; + if (!isWorkerLess) { + geositeDirectRule = createRule("domain", "direct"); + geoipDirectRule = createRule("ip", "direct"); + } + + let geositeBlockRule = createRule("domain", "block"); + geoRules.forEach(({ rule, type, domain, ip }) => { + if (rule) { + if (type === 'direct') { + geositeDirectRule?.domain.push(domain); + geoipDirectRule?.ip?.push(ip); + } else { + geositeBlockRule.domain.push(domain); + } + } + }); + + !isWorkerLess && isBypass && rules.push(geositeDirectRule, geoipDirectRule); + isBlock && rules.push(geositeBlockRule); + } + + blockUDP443 && rules.push({ + network: "udp", + port: "443", + outboundTag: "block", + type: "field", + }); + + if (isBalancer) { + rules.push({ + network: "tcp,udp", + balancerTag: "all", + type: "field" + }); + } else { + rules.push({ + network: "tcp,udp", + outboundTag: isChain ? "chain" : isWorkerLess ? "fragment" : "proxy", + type: "field" + }); + } + + return rules; +} + +function buildXrayVLESSOutbound (tag, address, port, host, sni, proxyIP, isFragment, allowInsecure) { + let outbound = { + protocol: "vless", + settings: { + vnext: [ + { + address: address, + port: +port, + users: [ + { + id: userID, + encryption: "none", + level: 8 + } + ] + } + ] + }, + streamSettings: { + network: "ws", + security: "none", + sockopt: {}, + wsSettings: { + headers: { + Host: host, + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36" + }, + path: `/${getRandomPath(16)}${proxyIP ? `/${btoa(proxyIP)}` : ''}?ed=2560` + } + }, + tag: tag + }; + + if (defaultHttpsPorts.includes(port)) { + outbound.streamSettings.security = "tls"; + outbound.streamSettings.tlsSettings = { + allowInsecure: allowInsecure, + fingerprint: "randomized", + alpn: ["h2", "http/1.1"], + serverName: sni + }; + } + + if (isFragment) { + outbound.streamSettings.sockopt.dialerProxy = "fragment"; + } else { + outbound.streamSettings.sockopt.tcpKeepAliveIdle = 100; + outbound.streamSettings.sockopt.tcpNoDelay = true; + } + + return outbound; +} + +function buildXrayTrojanOutbound (tag, address, port, host, sni, proxyIP, isFragment, allowInsecure) { + let outbound = { + protocol: "trojan", + settings: { + servers: [ + { + address: address, + port: +port, + password: trojanPassword, + level: 8 + } + ] + }, + streamSettings: { + network: "ws", + security: "none", + sockopt: {}, + wsSettings: { + headers: { + Host: host + }, + path: `/tr${getRandomPath(16)}${proxyIP ? `/${btoa(proxyIP)}` : ''}?ed=2560` + } + }, + tag: tag + }; + + if (defaultHttpsPorts.includes(port)) { + outbound.streamSettings.security = "tls"; + outbound.streamSettings.tlsSettings = { + allowInsecure: allowInsecure, + fingerprint: "randomized", + alpn: ["h2", "http/1.1"], + serverName: sni + }; + } + + if (isFragment) { + outbound.streamSettings.sockopt.dialerProxy = "fragment"; + } else { + outbound.streamSettings.sockopt.tcpKeepAliveIdle = 100; + outbound.streamSettings.sockopt.tcpNoDelay = true; + } + + return outbound; +} + +function buildXrayWarpOutbound (proxySettings, warpConfigs, endpoint, isChain, client) { + const { + nikaNGNoiseMode, + noiseCountMin, + noiseCountMax, + noiseSizeMin, + noiseSizeMax, + noiseDelayMin, + noiseDelayMax + } = proxySettings; + + const { + warpIPv6, + reserved, + publicKey, + privateKey + } = extractWireguardParams(warpConfigs, isChain); + + let outbound = { + protocol: "wireguard", + settings: { + address: [ + "172.16.0.2/32", + warpIPv6 + ], + mtu: 1280, + peers: [ + { + endpoint: endpoint, + publicKey: publicKey, + keepAlive: 5 + } + ], + reserved: base64ToDecimal(reserved), + secretKey: privateKey + }, + streamSettings: { + sockopt: { + dialerProxy: "proxy", + tcpKeepAliveIdle: 100, + tcpNoDelay: true, + } + }, + tag: isChain ? "chain" : "proxy" + }; + + !isChain && delete outbound.streamSettings; + client === 'nikang' && !isChain && Object.assign(outbound.settings, { + wnoise: nikaNGNoiseMode, + wnoisecount: noiseCountMin === noiseCountMax ? noiseCountMin : `${noiseCountMin}-${noiseCountMax}`, + wpayloadsize: noiseSizeMin === noiseSizeMax ? noiseSizeMin : `${noiseSizeMin}-${noiseSizeMax}`, + wnoisedelay: noiseDelayMin === noiseDelayMax ? noiseDelayMin : `${noiseDelayMin}-${noiseDelayMax}` + }); + + return outbound; +} + +function buildXrayChainOutbound(chainProxyParams) { + if (['socks', 'http'].includes(chainProxyParams.protocol)) { + const { protocol, host, port, user, pass } = chainProxyParams; + return { + protocol: protocol, + settings: { + servers: [ + { + address: host, + port: +port, + users: [ + { + user: user, + pass: pass, + level: 8 + } + ] + } + ] + }, + streamSettings: { + network: "tcp", + sockopt: { + dialerProxy: "proxy", + tcpNoDelay: true + } + }, + mux: { + enabled: true, + concurrency: 8, + xudpConcurrency: 16, + xudpProxyUDP443: "reject" + }, + tag: "chain" + }; + } + + const { + hostName, + port, + uuid, + flow, + security, + type, + sni, + fp, + alpn, + pbk, + sid, + spx, + headerType, + host, + path, + authority, + serviceName, + mode + } = chainProxyParams; + + let proxyOutbound = { + mux: { + concurrency: 8, + enabled: true, + xudpConcurrency: 16, + xudpProxyUDP443: "reject" + }, + protocol: "vless", + settings: { + vnext: [ + { + address: hostName, + port: +port, + users: [ + { + encryption: "none", + flow: flow, + id: uuid, + level: 8, + security: "auto" + } + ] + } + ] + }, + streamSettings: { + network: type, + security: security, + sockopt: { + dialerProxy: "proxy", + tcpNoDelay: true + } + }, + tag: "chain" + }; + + if (security === 'tls') { + const tlsAlpns = alpn ? alpn?.split(',') : []; + proxyOutbound.streamSettings.tlsSettings = { + allowInsecure: false, + fingerprint: fp, + alpn: tlsAlpns, + serverName: sni + }; + } + + if (security === 'reality') { + delete proxyOutbound.mux; + proxyOutbound.streamSettings.realitySettings = { + fingerprint: fp, + publicKey: pbk, + serverName: sni, + shortId: sid, + spiderX: spx + }; + } + + if (headerType === 'http') { + const httpPaths = path?.split(','); + const httpHosts = host?.split(','); + proxyOutbound.streamSettings.tcpSettings = { + header: { + request: { + headers: { Host: httpHosts }, + method: "GET", + path: httpPaths, + version: "1.1" + }, + response: { + headers: { "Content-Type": ["application/octet-stream"] }, + reason: "OK", + status: "200", + version: "1.1" + }, + type: "http" + } + }; + } + + if (type === 'tcp' && security !== 'reality' && !headerType) proxyOutbound.streamSettings.tcpSettings = { + header: { + type: "none" + } + }; + + if (type === 'ws') proxyOutbound.streamSettings.wsSettings = { + headers: { Host: host }, + path: path + }; + + if (type === 'grpc') { + delete proxyOutbound.mux; + proxyOutbound.streamSettings.grpcSettings = { + authority: authority, + multiMode: mode === 'multi', + serviceName: serviceName + }; + } + + return proxyOutbound; +} + +function buildXrayConfig (proxySettings, remark, isFragment, isBalancer, isChain, balancerFallback, isWarp) { + const { + vlessTrojanFakeDNS, + enableIPv6, + warpFakeDNS, + warpEnableIPv6, + bestVLESSTrojanInterval, + bestWarpInterval, + lengthMin, + lengthMax, + intervalMin, + intervalMax, + fragmentPackets + } = proxySettings; + + const isFakeDNS = (vlessTrojanFakeDNS && !isWarp) || (warpFakeDNS && isWarp); + const isIPv6 = (enableIPv6 && !isWarp) || (warpEnableIPv6 && isWarp); + let config = structuredClone(xrayConfigTemp); + config.remarks = remark; + if (isFakeDNS) { + config.inbounds[0].sniffing.destOverride.push("fakedns"); + config.inbounds[1].sniffing.destOverride.push("fakedns"); + !isIPv6 && config.fakedns.pop(); + } else { + delete config.fakedns; + } + + if (isFragment) { + const fragment = config.outbounds[0].settings.fragment; + fragment.length = `${lengthMin}-${lengthMax}`; + fragment.interval = `${intervalMin}-${intervalMax}`; + fragment.packets = fragmentPackets; + } else { + config.outbounds.shift(); + } + + if (isBalancer) { + const interval = isWarp ? bestWarpInterval : bestVLESSTrojanInterval; + config.observatory.probeInterval = `${interval}s`; + config.observatory.subjectSelector = [isChain ? 'chain' : 'prox']; + config.routing.balancers[0].selector = [isChain ? 'chain' : 'prox']; + if (balancerFallback) config.routing.balancers[0].fallbackTag = balancerFallback; + } else { + delete config.observatory; + delete config.routing.balancers; + } + + return config; +} + +async function buildXrayBestPingConfig(proxySettings, totalAddresses, chainProxy, outbounds, isFragment) { + const remark = isFragment ? '💦 BPB F - Best Ping 💥' : '💦 BPB - Best Ping 💥'; + let config = buildXrayConfig(proxySettings, remark, isFragment, true, chainProxy, chainProxy ? 'chain-2' : 'prox-2'); + config.dns = await buildXrayDNS(proxySettings, totalAddresses, undefined, false, true, false); + config.routing.rules = buildXrayRoutingRules(proxySettings, totalAddresses, chainProxy, true, false); + config.outbounds.unshift(...outbounds); + + return config; +} + +async function buildXrayBestFragmentConfig(proxySettings, hostName, chainProxy, outbounds) { + const bestFragValues = ['10-20', '20-30', '30-40', '40-50', '50-60', '60-70', + '70-80', '80-90', '90-100', '10-30', '20-40', '30-50', + '40-60', '50-70', '60-80', '70-90', '80-100', '100-200']; + + let config = buildXrayConfig(proxySettings, '💦 BPB F - Best Fragment 😎', true, true, chainProxy, undefined, false); + config.dns = await buildXrayDNS(proxySettings, [], hostName, false, true, false); + config.routing.rules = buildXrayRoutingRules(proxySettings, [], chainProxy, true, false); + const fragment = config.outbounds.shift(); + let bestFragOutbounds = []; + + bestFragValues.forEach( (fragLength, index) => { + if (chainProxy) { + let chainOutbound = structuredClone(chainProxy); + chainOutbound.tag = `chain-${index + 1}`; + chainOutbound.streamSettings.sockopt.dialerProxy = `prox-${index + 1}`; + bestFragOutbounds.push(chainOutbound); + } + + let proxyOutbound = structuredClone(outbounds[chainProxy ? 1 : 0]); + proxyOutbound.tag = `prox-${index + 1}`; + proxyOutbound.streamSettings.sockopt.dialerProxy = `frag-${index + 1}`; + let fragmentOutbound = structuredClone(fragment); + fragmentOutbound.tag = `frag-${index + 1}`; + fragmentOutbound.settings.fragment.length = fragLength; + fragmentOutbound.settings.fragment.interval = '1-1'; + bestFragOutbounds.push(proxyOutbound, fragmentOutbound); + }); + + config.outbounds.unshift(...bestFragOutbounds); + return config; +} + +async function buildXrayWorkerLessConfig(proxySettings) { + let config = buildXrayConfig(proxySettings, '💦 BPB F - WorkerLess ⭐', true, false, false, undefined, false); + config.dns = await buildXrayDNS(proxySettings, [], undefined, true); + config.routing.rules = buildXrayRoutingRules(proxySettings, [], false, false, true); + let fakeOutbound = buildXrayVLESSOutbound('fake-outbound', 'google.com', '443', userID, 'google.com', 'google.com', '', true, false); + delete fakeOutbound.streamSettings.sockopt; + fakeOutbound.streamSettings.wsSettings.path = '/'; + config.outbounds.push(fakeOutbound); + return config; +} + +export async function getXrayCustomConfigs(env, proxySettings, isFragment) { + userID = env.UUID || userID; + if (!isValidUUID(userID)) throw new Error(`Invalid UUID: ${userID}`); + trojanPassword = env.TROJAN_PASS || trojanPassword; + const hostName = globalThis.hostName; + let configs = []; + let outbounds = []; + let protocols = []; + let chainProxy; + const { + proxyIP, + outProxy, + outProxyParams, + cleanIPs, + enableIPv6, + customCdnAddrs, + customCdnHost, + customCdnSni, + vlessConfigs, + trojanConfigs, + ports + } = proxySettings; + + if (outProxy) { + const proxyParams = JSON.parse(outProxyParams); + try { + chainProxy = buildXrayChainOutbound(proxyParams); + } catch (error) { + console.log('An error occured while parsing chain proxy: ', error); + chainProxy = undefined; + await env.bpb.put("proxySettings", JSON.stringify({ + ...proxySettings, + outProxy: '', + outProxyParams: {} + })); + } + } + + const Addresses = await getConfigAddresses(hostName, cleanIPs, enableIPv6); + const customCdnAddresses = customCdnAddrs ? customCdnAddrs.split(',') : []; + const totalAddresses = isFragment ? [...Addresses] : [...Addresses, ...customCdnAddresses]; + const totalPorts = ports.filter(port => isFragment ? defaultHttpsPorts.includes(port): true); + vlessConfigs && protocols.push('VLESS'); + trojanConfigs && protocols.push('Trojan'); + let proxyIndex = 1; + + for (const protocol of protocols) { + let protocolIndex = 1; + for (const port of totalPorts) { + for (const addr of totalAddresses) { + const isCustomAddr = customCdnAddresses.includes(addr); + const configType = isCustomAddr ? 'C' : isFragment ? 'F' : ''; + const sni = isCustomAddr ? customCdnSni : randomUpperCase(hostName); + const host = isCustomAddr ? customCdnHost : hostName; + const remark = generateRemark(protocolIndex, port, addr, cleanIPs, protocol, configType); + let customConfig = buildXrayConfig(proxySettings, remark, isFragment, false, chainProxy, undefined, false); + customConfig.dns = await buildXrayDNS(proxySettings, [addr], undefined); + customConfig.routing.rules = buildXrayRoutingRules(proxySettings, [addr], chainProxy, false, false); + let outbound = protocol === 'VLESS' + ? buildXrayVLESSOutbound('proxy', addr, port, host, sni, proxyIP, isFragment, isCustomAddr) + : buildXrayTrojanOutbound('proxy', addr, port, host, sni, proxyIP, isFragment, isCustomAddr); + + customConfig.outbounds.unshift({...outbound}); + outbound.tag = `prox-${proxyIndex}`; + + if (chainProxy) { + customConfig.outbounds.unshift(chainProxy); + let chainOutbound = structuredClone(chainProxy); + chainOutbound.tag = `chain-${proxyIndex}`; + chainOutbound.streamSettings.sockopt.dialerProxy = `prox-${proxyIndex}`; + outbounds.push(chainOutbound); + } + + outbounds.push(outbound); + configs.push(customConfig); + proxyIndex++; + protocolIndex++; + } + } + } + + const bestPing = await buildXrayBestPingConfig(proxySettings, totalAddresses, chainProxy, outbounds, isFragment); + if (!isFragment) return [...configs, bestPing]; + const bestFragment = await buildXrayBestFragmentConfig(proxySettings, hostName, chainProxy, outbounds); + const workerLessConfig = await buildXrayWorkerLessConfig(proxySettings); + configs.push(bestPing, bestFragment, workerLessConfig); + + return configs; +} + +export async function getXrayWarpConfigs (proxySettings, warpConfigs, client) { + let xrayWarpConfigs = []; + let xrayWoWConfigs = []; + let xrayWarpOutbounds = []; + let xrayWoWOutbounds = []; + const { warpEndpoints } = proxySettings; + const outboundDomains = warpEndpoints.split(',').map(endpoint => endpoint.split(':')[0]).filter(address => isDomain(address)); + const proIndicator = client === 'nikang' ? ' Pro ' : ' '; + + for (const [index, endpoint] of warpEndpoints.split(',').entries()) { + const endpointHost = endpoint.split(':')[0]; + let warpConfig = buildXrayConfig(proxySettings, `💦 ${index + 1} - Warp${proIndicator}🇮🇷`, false, false, false, undefined, true); + let WoWConfig = buildXrayConfig(proxySettings, `💦 ${index + 1} - WoW${proIndicator}🌍`, false, false, true, undefined, true); + warpConfig.dns = WoWConfig.dns = await buildXrayDNS(proxySettings, [endpointHost], undefined, false, true); + warpConfig.routing.rules = buildXrayRoutingRules(proxySettings, [endpointHost], false, false, false); + WoWConfig.routing.rules = buildXrayRoutingRules(proxySettings, [endpointHost], true, false, false); + const warpOutbound = buildXrayWarpOutbound(proxySettings, warpConfigs, endpoint, false, client); + const WoWOutbound = buildXrayWarpOutbound(proxySettings, warpConfigs, endpoint, true, client); + warpOutbound.settings.peers[0].endpoint = endpoint; + WoWOutbound.settings.peers[0].endpoint = endpoint; + warpConfig.outbounds.unshift(warpOutbound); + WoWConfig.outbounds.unshift(WoWOutbound, warpOutbound); + xrayWarpConfigs.push(warpConfig); + xrayWoWConfigs.push(WoWConfig); + const proxyOutbound = structuredClone(warpOutbound); + proxyOutbound.tag = `prox-${index + 1}`; + const chainOutbound = structuredClone(WoWOutbound); + chainOutbound.tag = `chain-${index + 1}`; + chainOutbound.streamSettings.sockopt.dialerProxy = `prox-${index + 1}`; + xrayWarpOutbounds.push(proxyOutbound); + xrayWoWOutbounds.push(chainOutbound); + } + + const dnsObject = await buildXrayDNS(proxySettings, outboundDomains, undefined, false, true, true); + let xrayWarpBestPing = buildXrayConfig(proxySettings, `💦 Warp${proIndicator}- Best Ping 🚀`, false, true, false, undefined, true); + xrayWarpBestPing.dns = dnsObject; + xrayWarpBestPing.routing.rules = buildXrayRoutingRules(proxySettings, outboundDomains, false, true, false); + xrayWarpBestPing.outbounds.unshift(...xrayWarpOutbounds); + let xrayWoWBestPing = buildXrayConfig(proxySettings, `💦 WoW${proIndicator}- Best Ping 🚀`, false, true, true, undefined, true); + xrayWoWBestPing.dns = dnsObject; + xrayWoWBestPing.routing.rules = buildXrayRoutingRules(proxySettings, outboundDomains, true, true, false); + xrayWoWBestPing.outbounds.unshift(...xrayWoWOutbounds, ...xrayWarpOutbounds); + return [...xrayWarpConfigs, ...xrayWoWConfigs, xrayWarpBestPing, xrayWoWBestPing]; +} + +const xrayConfigTemp = { + remarks: "", + log: { + loglevel: "warning", + }, + dns: {}, + fakedns: [ + { + ipPool: "198.18.0.0/15", + poolSize: 32768 + }, + { + ipPool: "fc00::/18", + poolSize: 32768 + } + ], + inbounds: [ + { + port: 10808, + protocol: "socks", + settings: { + auth: "noauth", + udp: true, + userLevel: 8, + }, + sniffing: { + destOverride: ["http", "tls"], + enabled: true, + routeOnly: true + }, + tag: "socks-in", + }, + { + port: 10809, + protocol: "http", + settings: { + auth: "noauth", + udp: true, + userLevel: 8, + }, + sniffing: { + destOverride: ["http", "tls"], + enabled: true, + routeOnly: true + }, + tag: "http-in", + }, + { + listen: "127.0.0.1", + port: 10853, + protocol: "dokodemo-door", + settings: { + address: "1.1.1.1", + network: "tcp,udp", + port: 53 + }, + tag: "dns-in" + } + ], + outbounds: [ + { + tag: "fragment", + protocol: "freedom", + settings: { + fragment: { + packets: "tlshello", + length: "", + interval: "", + }, + domainStrategy: "UseIP" + }, + streamSettings: { + sockopt: { + tcpKeepAliveIdle: 100, + tcpNoDelay: true + }, + }, + }, + { + protocol: "dns", + tag: "dns-out" + }, + { + protocol: "freedom", + settings: {}, + tag: "direct", + }, + { + protocol: "blackhole", + settings: { + response: { + type: "http", + }, + }, + tag: "block", + }, + ], + policy: { + levels: { + 8: { + connIdle: 300, + downlinkOnly: 1, + handshake: 4, + uplinkOnly: 1, + } + }, + system: { + statsOutboundUplink: true, + statsOutboundDownlink: true, + } + }, + routing: { + domainStrategy: "IPIfNonMatch", + rules: [], + balancers: [ + { + tag: "all", + selector: ["prox"], + strategy: { + type: "leastPing", + }, + } + ] + }, + observatory: { + probeInterval: "30s", + probeURL: "https://www.gstatic.com/generate_204", + subjectSelector: ["prox"], + EnableConcurrency: true, + }, + stats: {} +}; \ No newline at end of file diff --git a/src/helpers/config.js b/src/helpers/config.js new file mode 100644 index 000000000..bc97bd1f5 --- /dev/null +++ b/src/helpers/config.js @@ -0,0 +1,10 @@ +export const configs = { + userID: '89b3cbba-e6ac-485a-9481-976a0415eab9', + dohURL: 'https://cloudflare-dns.com/dns-query', + proxyIPs: ['bpb.yousef.isegaro.com'], + proxyIP: proxyIPs[Math.floor(Math.random() * proxyIPs.length)], + trojanPassword: 'bpb-trojan', + defaultHttpPorts: ['80', '8080', '2052', '2082', '2086', '2095', '8880'], + defaultHttpsPorts: ['443', '8443', '2053', '2083', '2087', '2096'], + panelVersion: '2.7.2' +}; diff --git a/src/helpers/helpers.js b/src/helpers/helpers.js new file mode 100644 index 000000000..102286f0d --- /dev/null +++ b/src/helpers/helpers.js @@ -0,0 +1,41 @@ +import { configs } from './config.js'; +const { dohURL } = configs; + +export function isValidUUID(uuid) { + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + return uuidRegex.test(uuid); +} + +export async function resolveDNS (domain) { + const dohURLv4 = `${dohURL}?name=${encodeURIComponent(domain)}&type=A`; + const dohURLv6 = `${dohURL}?name=${encodeURIComponent(domain)}&type=AAAA`; + + try { + const [ipv4Response, ipv6Response] = await Promise.all([ + fetch(dohURLv4, { headers: { accept: 'application/dns-json' } }), + fetch(dohURLv6, { headers: { accept: 'application/dns-json' } }) + ]); + + const ipv4Addresses = await ipv4Response.json(); + const ipv6Addresses = await ipv6Response.json(); + + const ipv4 = ipv4Addresses.Answer + ? ipv4Addresses.Answer.map((record) => record.data) + : []; + const ipv6 = ipv6Addresses.Answer + ? ipv6Addresses.Answer.map((record) => record.data) + : []; + + return { ipv4, ipv6 }; + } catch (error) { + console.error('Error resolving DNS:', error); + throw new Error(`An error occurred while resolving DNS - ${error}`); + } +} + +export function isDomain(address) { + const domainPattern = /^(?!\-)(?:[A-Za-z0-9\-]{1,63}\.)+[A-Za-z]{2,}$/; + return domainPattern.test(address); +} + + diff --git a/src/kv/handlers.js b/src/kv/handlers.js new file mode 100644 index 000000000..6d0cfa497 --- /dev/null +++ b/src/kv/handlers.js @@ -0,0 +1,152 @@ +import { fetchWgConfig } from '../protocols/warp.js'; +import { isDomain, resolveDNS } from '../helpers/helpers.js'; + +export async function getDataset(env) { + let proxySettings, warpConfigs; + if (typeof env.bpb !== 'object') { + return {kvNotFound: true, proxySettings: null, warpConfigs: null} + } + + try { + proxySettings = await env.bpb.get("proxySettings", {type: 'json'}); + warpConfigs = await env.bpb.get('warpConfigs', {type: 'json'}); + } catch (error) { + console.log(error); + throw new Error(`An error occurred while getting KV - ${error}`); + } + + if (!proxySettings) { + proxySettings = await updateDataset(env); + const { error, configs } = await fetchWgConfig(env, proxySettings); + if (error) throw new Error(`An error occurred while getting Warp configs - ${error}`); + warpConfigs = configs; + } + + if (panelVersion !== proxySettings.panelVersion) proxySettings = await updateDataset(env); + return {kvNotFound: false, proxySettings, warpConfigs} +} + +export async function updateDataset (env, newSettings, resetSettings) { + let currentSettings; + if (!resetSettings) { + try { + currentSettings = await env.bpb.get("proxySettings", {type: 'json'}); + } catch (error) { + console.log(error); + throw new Error(`An error occurred while getting current KV settings - ${error}`); + } + } else { + await env.bpb.delete('warpConfigs'); + } + + const validateField = (field) => { + const fieldValue = newSettings?.get(field); + if (fieldValue === undefined) return null; + if (fieldValue === 'true') return true; + if (fieldValue === 'false') return false; + return fieldValue; + } + + const remoteDNS = validateField('remoteDNS') ?? currentSettings?.remoteDNS ?? 'https://8.8.8.8/dns-query'; + const enableIPv6 = validateField('enableIPv6') ?? currentSettings?.enableIPv6 ?? true; + const url = new URL(remoteDNS); + const remoteDNSServer = url.hostname; + const isServerDomain = isDomain(remoteDNSServer); + let resolvedRemoteDNS = {}; + if (isServerDomain) { + try { + const resolvedDomain = await resolveDNS(remoteDNSServer); + resolvedRemoteDNS = { + server: remoteDNSServer, + staticIPs: enableIPv6 ? [...resolvedDomain.ipv4, ...resolvedDomain.ipv6] : resolvedDomain.ipv4 + }; + } catch (error) { + console.log(error); + throw new Error(`An error occurred while resolving remote DNS server, please try agian! - ${error}`); + } + } + + const proxySettings = { + remoteDNS: remoteDNS, + resolvedRemoteDNS: resolvedRemoteDNS, + localDNS: validateField('localDNS') ?? currentSettings?.localDNS ?? '8.8.8.8', + vlessTrojanFakeDNS: validateField('vlessTrojanFakeDNS') ?? currentSettings?.vlessTrojanFakeDNS ?? false, + proxyIP: validateField('proxyIP')?.trim() ?? currentSettings?.proxyIP ?? '', + outProxy: validateField('outProxy') ?? currentSettings?.outProxy ?? '', + outProxyParams: extractChainProxyParams(validateField('outProxy')) ?? currentSettings?.outProxyParams ?? {}, + cleanIPs: validateField('cleanIPs')?.replaceAll(' ', '') ?? currentSettings?.cleanIPs ?? '', + enableIPv6: enableIPv6, + customCdnAddrs: validateField('customCdnAddrs')?.replaceAll(' ', '') ?? currentSettings?.customCdnAddrs ?? '', + customCdnHost: validateField('customCdnHost')?.trim() ?? currentSettings?.customCdnHost ?? '', + customCdnSni: validateField('customCdnSni')?.trim() ?? currentSettings?.customCdnSni ?? '', + bestVLESSTrojanInterval: validateField('bestVLESSTrojanInterval') ?? currentSettings?.bestVLESSTrojanInterval ?? '30', + vlessConfigs: validateField('vlessConfigs') ?? currentSettings?.vlessConfigs ?? true, + trojanConfigs: validateField('trojanConfigs') ?? currentSettings?.trojanConfigs ?? false, + ports: validateField('ports')?.split(',') ?? currentSettings?.ports ?? ['443'], + lengthMin: validateField('fragmentLengthMin') ?? currentSettings?.lengthMin ?? '100', + lengthMax: validateField('fragmentLengthMax') ?? currentSettings?.lengthMax ?? '200', + intervalMin: validateField('fragmentIntervalMin') ?? currentSettings?.intervalMin ?? '1', + intervalMax: validateField('fragmentIntervalMax') ?? currentSettings?.intervalMax ?? '1', + fragmentPackets: validateField('fragmentPackets') ?? currentSettings?.fragmentPackets ?? 'tlshello', + bypassLAN: validateField('bypass-lan') ?? currentSettings?.bypassLAN ?? false, + bypassIran: validateField('bypass-iran') ?? currentSettings?.bypassIran ?? false, + bypassChina: validateField('bypass-china') ?? currentSettings?.bypassChina ?? false, + bypassRussia: validateField('bypass-russia') ?? currentSettings?.bypassRussia ?? false, + blockAds: validateField('block-ads') ?? currentSettings?.blockAds ?? false, + blockPorn: validateField('block-porn') ?? currentSettings?.blockPorn ?? false, + blockUDP443: validateField('block-udp-443') ?? currentSettings?.blockUDP443 ?? false, + warpEndpoints: validateField('warpEndpoints')?.replaceAll(' ', '') ?? currentSettings?.warpEndpoints ?? 'engage.cloudflareclient.com:2408', + warpFakeDNS: validateField('warpFakeDNS') ?? currentSettings?.warpFakeDNS ?? false, + warpEnableIPv6: validateField('warpEnableIPv6') ?? currentSettings?.warpEnableIPv6 ?? true, + warpPlusLicense: validateField('warpPlusLicense') ?? currentSettings?.warpPlusLicense ?? '', + bestWarpInterval: validateField('bestWarpInterval') ?? currentSettings?.bestWarpInterval ?? '30', + hiddifyNoiseMode: validateField('hiddifyNoiseMode') ?? currentSettings?.hiddifyNoiseMode ?? 'm4', + nikaNGNoiseMode: validateField('nikaNGNoiseMode') ?? currentSettings?.nikaNGNoiseMode ?? 'quic', + noiseCountMin: validateField('noiseCountMin') ?? currentSettings?.noiseCountMin ?? '10', + noiseCountMax: validateField('noiseCountMax') ?? currentSettings?.noiseCountMax ?? '15', + noiseSizeMin: validateField('noiseSizeMin') ?? currentSettings?.noiseSizeMin ?? '5', + noiseSizeMax: validateField('noiseSizeMax') ?? currentSettings?.noiseSizeMax ?? '10', + noiseDelayMin: validateField('noiseDelayMin') ?? currentSettings?.noiseDelayMin ?? '1', + noiseDelayMax: validateField('noiseDelayMax') ?? currentSettings?.noiseDelayMax ?? '1', + panelVersion: panelVersion + }; + + try { + await env.bpb.put("proxySettings", JSON.stringify(proxySettings)); + } catch (error) { + console.log(error); + throw new Error(`An error occurred while updating KV - ${error}`); + } + + return proxySettings; +} + +function extractChainProxyParams(chainProxy) { + let configParams = {}; + if (!chainProxy) return {}; + let url = new URL(chainProxy); + const protocol = url.protocol.slice(0, -1); + if (protocol === 'vless') { + const params = new URLSearchParams(url.search); + configParams = { + protocol: protocol, + uuid : url.username, + hostName : url.hostname, + port : url.port + }; + + params.forEach( (value, key) => { + configParams[key] = value; + }); + } else { + configParams = { + protocol: protocol, + user : url.username, + pass : url.password, + host : url.host, + port : url.port + }; + } + + return JSON.stringify(configParams); +} \ No newline at end of file diff --git a/src/pages/errorPage.js b/src/pages/errorPage.js new file mode 100644 index 000000000..4b39aed09 --- /dev/null +++ b/src/pages/errorPage.js @@ -0,0 +1,57 @@ +import { configs } from '../helpers/config.js'; +const panelVersion = configs.panelVersion; + +export function renderErrorPage (message, error, refer) { + return ` + + + + + + Error Page + + + +
+

BPB Panel ${panelVersion} 💦

+
+

${message} ${refer + ? 'Please try again or refer to documents' + : ''} +

+

${error ? `⚠️ ${error.stack.toString()}` : ''}

+
+
+ + + `; +} \ No newline at end of file diff --git a/src/pages/homePage.js b/src/pages/homePage.js new file mode 100644 index 000000000..30749ded3 --- /dev/null +++ b/src/pages/homePage.js @@ -0,0 +1,1519 @@ +import { configs } from '../helpers/config.js'; +import { isValidUUID } from '../helpers/helpers.js'; +const { defaultHttpPorts, defaultHttpsPorts, panelVersion } = configs; +let userID = configs.userID; + +export function renderHomePage (request, env, proxySettings, isPassSet) { + const hostName = globalThis.hostName; + userID = env.UUID || userID; + if (!isValidUUID(userID)) throw new Error(`Invalid UUID: ${userID}`); + const { + remoteDNS, + localDNS, + vlessTrojanFakeDNS, + proxyIP, + outProxy, + cleanIPs, + enableIPv6, + customCdnAddrs, + customCdnHost, + customCdnSni, + bestVLESSTrojanInterval, + vlessConfigs, + trojanConfigs, + ports, + lengthMin, + lengthMax, + intervalMin, + intervalMax, + fragmentPackets, + warpEndpoints, + warpFakeDNS, + warpEnableIPv6, + warpPlusLicense, + bestWarpInterval, + hiddifyNoiseMode, + nikaNGNoiseMode, + noiseCountMin, + noiseCountMax, + noiseSizeMin, + noiseSizeMax, + noiseDelayMin, + noiseDelayMax, + bypassLAN, + bypassIran, + bypassChina, + bypassRussia, + blockAds, + blockPorn, + blockUDP443 + } = proxySettings; + + const isWarpPlus = warpPlusLicense ? true : false; + let activeProtocols = (vlessConfigs ? 1 : 0) + (trojanConfigs ? 1 : 0); + let httpPortsBlock = '', httpsPortsBlock = ''; + const allPorts = [...(hostName.includes('workers.dev') ? defaultHttpPorts : []), ...defaultHttpsPorts]; + let regionNames = new Intl.DisplayNames(['en'], {type: 'region'}); + const cfCountry = regionNames.of(request.cf.country); + + allPorts.forEach(port => { + const id = `port-${port}`; + const isChecked = ports.includes(port) ? 'checked' : ''; + const portBlock = ` +
+ + +
`; + defaultHttpsPorts.includes(port) ? httpsPortsBlock += portBlock : httpPortsBlock += portBlock; + }); + + return ` + + + + + + BPB Panel ${panelVersion} + + + Collapsible Sections + + + +

BPB Panel ${panelVersion} 💦

+
+
+
+

VLESS / TROJAN ⚙️

+
+ + +
+
+ + +
+
+ +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + + + +
+
+ +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+
+
+ + + + + + + + + + ${!httpPortsBlock ? '' : ` + + + `} +
Config typePorts
TLS +
${httpsPortsBlock}
+
Non TLS +
${httpPortsBlock}
+
+
+
+
+

FRAGMENT ⚙️

+
+ +
+ + - + +
+
+
+ +
+ + - + +
+
+
+ +
+ +
+
+
+
+

WARP GENERAL ⚙️

+
+ + +
+
+ + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+

WARP PRO ⚙️

+
+ + +
+
+ + +
+
+ +
+ + - + +
+
+
+ +
+ + - + +
+
+
+ +
+ + - + +
+
+
+
+

ROUTING RULES ⚙️

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ + +
+
+
+
+

NORMAL SUB 🔗

+
+ + + + + + + + + + + + + +
ApplicationSubscription
+
+ verified + v2rayNG +
+
+ verified + NikaNG +
+
+ verified + MahsaNG +
+
+ verified + v2rayN +
+
+ verified + v2rayN-PRO +
+
+ verified + Shadowrocket +
+
+ verified + Streisand +
+
+ verified + Hiddify +
+
+ verified + Nekoray (Xray) +
+
+ + +
+
+ verified + Nekobox +
+
+ verified + Nekoray (Sing-Box) +
+
+ verified + Karing +
+
+ +
+
+

FULL NORMAL SUB 🔗

+
+ + + + + + + + + + + + + + + + + +
ApplicationSubscription
+
+ verified + v2rayNG +
+
+ verified + NikaNG +
+
+ verified + MahsaNG +
+
+ verified + v2rayN +
+
+ verified + v2rayN-PRO +
+
+ verified + Streisand +
+
+ + +
+
+ verified + Sing-box +
+
+ + +
+
+ verified + Clash Meta +
+
+ verified + Clash Verge +
+
+ verified + v2rayN +
+
+ verified + FlClash +
+
+ verified + Stash +
+
+ + +
+
+

FRAGMENT SUB ⛓️

+
+ + + + + + + + + + + + + +
ApplicationSubscription
+
+ verified + v2rayNG +
+
+ verified + NikaNG +
+
+ verified + MahsaNG +
+
+ verified + v2rayN +
+
+ verified + v2rayN-PRO +
+
+ verified + Streisand +
+
+ + +
+
+ verified + Hiddify +
+
+ + +
+
+

WARP SUB 🔗

+
+ + + + + + + + + + + + + + + + + +
ApplicationSubscription
+
+ verified + v2rayNG +
+
+ verified + v2rayN +
+
+ verified + Streisand +
+
+ + +
+
+ verified + Hiddify +
+
+ verified + Singbox +
+
+ + +
+
+ verified + Clash Meta +
+
+ verified + Clash Verge +
+
+ verified + v2rayN +
+
+ verified + FlClash +
+
+ verified + Stash +
+
+ + +
+
+

WARP PRO SUB 🔗

+
+ + + + + + + + + + + + + +
ApplicationSubscription
+
+ verified + NikaNG +
+
+ verified + MahsaNG +
+
+ verified + v2rayN-PRO +
+
+ + +
+
+ verified + Hiddify +
+
+ + +
+
+ +
+ +
+
+

YOUR IP 💡

+
+ + + + + + + + + + + + + + + + + + + + + + +
TargetYour IPCountryCityISP
Cloudflare CDN${request.headers.get('cf-connecting-ip') || '-'}${cfCountry || '-'}${request.cf.city || '-'}${request.cf.asOrganization.toUpperCase() || '-'}
Others
+
+
+ +
+ + + + + `; +} \ No newline at end of file diff --git a/src/pages/loginPage.js b/src/pages/loginPage.js new file mode 100644 index 000000000..a24bd3812 --- /dev/null +++ b/src/pages/loginPage.js @@ -0,0 +1,149 @@ +import { configs } from '../helpers/config.js'; +const panelVersion = configs.panelVersion; + +export function renderLoginPage () { + return ` + + + + + + User Login + + + +
+

BPB Panel ${panelVersion} 💦

+
+

User Login

+
+
+ + +
+
+ +
+
+
+ + + `; +} \ No newline at end of file diff --git a/src/protocols/trojan.js b/src/protocols/trojan.js new file mode 100644 index 000000000..d8303fc3c --- /dev/null +++ b/src/protocols/trojan.js @@ -0,0 +1,364 @@ +import { connect } from 'cloudflare:sockets'; +import { configs } from '../helpers/config.js'; +import sha256 from 'js-sha256'; +// https://www.nslookup.io/domains/bpb.yousef.isegaro.com/dns-records/ +let proxyIP = configs.proxyIP +let trojanPassword = configs.trojanPassword; + +export async function trojanOverWSHandler(request, env) { + proxyIP = env.PROXYIP || proxyIP; + trojanPassword = env.TROJAN_PASS || trojanPassword; + const webSocketPair = new WebSocketPair(); + const [client, webSocket] = Object.values(webSocketPair); + webSocket.accept(); + let address = ""; + let portWithRandomLog = ""; + const log = (info, event) => { + console.log(`[${address}:${portWithRandomLog}] ${info}`, event || ""); + }; + const earlyDataHeader = request.headers.get("sec-websocket-protocol") || ""; + const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log); + let remoteSocketWapper = { + value: null, + }; + let udpStreamWrite = null; + + readableWebSocketStream + .pipeTo( + new WritableStream({ + async write(chunk, controller) { + if (udpStreamWrite) { + return udpStreamWrite(chunk); + } + + if (remoteSocketWapper.value) { + const writer = remoteSocketWapper.value.writable.getWriter(); + await writer.write(chunk); + writer.releaseLock(); + return; + } + + const { + hasError, + message, + portRemote = 443, + addressRemote = "", + rawClientData, + } = await parseTrojanHeader(chunk); + + address = addressRemote; + portWithRandomLog = `${portRemote}--${Math.random()} tcp`; + + if (hasError) { + throw new Error(message); + return; + } + + handleTCPOutBound(request, remoteSocketWapper, addressRemote, portRemote, rawClientData, webSocket, log); + }, + close() { + log(`readableWebSocketStream is closed`); + }, + abort(reason) { + log(`readableWebSocketStream is aborted`, JSON.stringify(reason)); + }, + }) + ) + .catch((err) => { + log("readableWebSocketStream pipeTo error", err); + }); + + return new Response(null, { + status: 101, + // @ts-ignore + webSocket: client, + }); +} + +async function parseTrojanHeader(buffer) { + if (buffer.byteLength < 56) { + return { + hasError: true, + message: "invalid data", + }; + } + + let crLfIndex = 56; + if (new Uint8Array(buffer.slice(56, 57))[0] !== 0x0d || new Uint8Array(buffer.slice(57, 58))[0] !== 0x0a) { + return { + hasError: true, + message: "invalid header format (missing CR LF)", + }; + } + + const password = new TextDecoder().decode(buffer.slice(0, crLfIndex)); + if (password !== sha256.sha224(trojanPassword)) { + return { + hasError: true, + message: "invalid password", + }; + } + + const socks5DataBuffer = buffer.slice(crLfIndex + 2); + if (socks5DataBuffer.byteLength < 6) { + return { + hasError: true, + message: "invalid SOCKS5 request data", + }; + } + + const view = new DataView(socks5DataBuffer); + const cmd = view.getUint8(0); + if (cmd !== 1) { + return { + hasError: true, + message: "unsupported command, only TCP (CONNECT) is allowed", + }; + } + + const atype = view.getUint8(1); + // 0x01: IPv4 address + // 0x03: Domain name + // 0x04: IPv6 address + let addressLength = 0; + let addressIndex = 2; + let address = ""; + switch (atype) { + case 1: + addressLength = 4; + address = new Uint8Array(socks5DataBuffer.slice(addressIndex, addressIndex + addressLength)).join("."); + break; + case 3: + addressLength = new Uint8Array(socks5DataBuffer.slice(addressIndex, addressIndex + 1))[0]; + addressIndex += 1; + address = new TextDecoder().decode(socks5DataBuffer.slice(addressIndex, addressIndex + addressLength)); + break; + case 4: + addressLength = 16; + const dataView = new DataView(socks5DataBuffer.slice(addressIndex, addressIndex + addressLength)); + const ipv6 = []; + for (let i = 0; i < 8; i++) { + ipv6.push(dataView.getUint16(i * 2).toString(16)); + } + address = ipv6.join(":"); + break; + default: + return { + hasError: true, + message: `invalid addressType is ${atype}`, + }; + } + + if (!address) { + return { + hasError: true, + message: `address is empty, addressType is ${atype}`, + }; + } + + const portIndex = addressIndex + addressLength; + const portBuffer = socks5DataBuffer.slice(portIndex, portIndex + 2); + const portRemote = new DataView(portBuffer).getUint16(0); + return { + hasError: false, + addressRemote: address, + portRemote, + rawClientData: socks5DataBuffer.slice(portIndex + 4), + }; +} + +/** + * Handles outbound TCP connections. + * + * @param {any} remoteSocket + * @param {string} addressRemote The remote address to connect to. + * @param {number} portRemote The remote port to connect to. + * @param {Uint8Array} rawClientData The raw client data to write. + * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to pass the remote socket to. + * @param {function} log The logging function. + * @returns {Promise} The remote socket. + */ +async function handleTCPOutBound( + request, + remoteSocket, + addressRemote, + portRemote, + rawClientData, + webSocket, + log +) { + async function connectAndWrite(address, port) { + if (/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(address)) address = `${atob('d3d3Lg==')}${address}${atob('LnNzbGlwLmlv')}`; + /** @type {import("@cloudflare/workers-types").Socket} */ + const tcpSocket = connect({ + hostname: address, + port: port, + }); + remoteSocket.value = tcpSocket; + log(`connected to ${address}:${port}`); + const writer = tcpSocket.writable.getWriter(); + await writer.write(rawClientData); // first write, nomal is tls client hello + writer.releaseLock(); + return tcpSocket; + } + + // if the cf connect tcp socket have no incoming data, we retry to redirect ip + async function retry() { + const { pathname } = new URL(request.url); + let panelProxyIP = pathname.split('/')[2]; + panelProxyIP = panelProxyIP ? atob(panelProxyIP) : undefined; + const tcpSocket = await connectAndWrite(panelProxyIP || 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); + }); + + trojanRemoteSocketToWS(tcpSocket, webSocket, null, log); + } + + const tcpSocket = await connectAndWrite(addressRemote, portRemote); + + // when remoteSocket is ready, pass to websocket + // remote--> ws + trojanRemoteSocketToWS(tcpSocket, webSocket, retry, log); +} + +/** + * Creates a readable stream from a WebSocket server, allowing for data to be read from the WebSocket. + * @param {import("@cloudflare/workers-types").WebSocket} webSocketServer The WebSocket server to create the readable stream from. + * @param {string} earlyDataHeader The header containing early data for WebSocket 0-RTT. + * @param {(info: string)=> void} log The logging function. + * @returns {ReadableStream} A readable stream that can be used to read data from the WebSocket. + */ +function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) { + let readableStreamCancel = false; + const stream = new ReadableStream({ + start(controller) { + webSocketServer.addEventListener("message", (event) => { + if (readableStreamCancel) { + return; + } + const message = event.data; + controller.enqueue(message); + }); + + // The event means that the client closed the client -> server stream. + // However, the server -> client stream is still open until you call close() on the server side. + // The WebSocket protocol says that a separate close message must be sent in each direction to fully close the socket. + webSocketServer.addEventListener("close", () => { + // client send close, need close server + // if stream is cancel, skip controller.close + safeCloseWebSocket(webSocketServer); + if (readableStreamCancel) { + return; + } + controller.close(); + }); + webSocketServer.addEventListener("error", (err) => { + log("webSocketServer has error"); + controller.error(err); + }); + // for ws 0rtt + const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader); + if (error) { + controller.error(error); + } else if (earlyData) { + controller.enqueue(earlyData); + } + }, + pull(controller) { + // if ws can stop read if stream is full, we can implement backpressure + // https://streams.spec.whatwg.org/#example-rs-push-backpressure + }, + cancel(reason) { + // 1. pipe WritableStream has error, this cancel will called, so ws handle server close into here + // 2. if readableStream is cancel, all controller.close/enqueue need skip, + // 3. but from testing controller.error still work even if readableStream is cancel + if (readableStreamCancel) { + return; + } + log(`ReadableStream was canceled, due to ${reason}`); + readableStreamCancel = true; + safeCloseWebSocket(webSocketServer); + }, + }); + + return stream; +} + +async function trojanRemoteSocketToWS(remoteSocket, webSocket, retry, log) { + let hasIncomingData = false; + await remoteSocket.readable + .pipeTo( + new WritableStream({ + start() {}, + /** + * + * @param {Uint8Array} chunk + * @param {*} controller + */ + async write(chunk, controller) { + hasIncomingData = true; + if (webSocket.readyState !== WS_READY_STATE_OPEN) { + controller.error("webSocket connection is not open"); + } + webSocket.send(chunk); + }, + close() { + log(`remoteSocket.readable is closed, hasIncomingData: ${hasIncomingData}`); + }, + abort(reason) { + console.error("remoteSocket.readable abort", reason); + }, + }) + ) + .catch((error) => { + console.error(`trojanRemoteSocketToWS error:`, error.stack || error); + safeCloseWebSocket(webSocket); + }); + + if (hasIncomingData === false && retry) { + log(`retry`); + retry(); + } +} + +/** + * Decodes a base64 string into an ArrayBuffer. + * @param {string} base64Str The base64 string to decode. + * @returns {{earlyData: ArrayBuffer|null, error: Error|null}} An object containing the decoded ArrayBuffer or null if there was an error, and any error that occurred during decoding or null if there was no error. + */ +function base64ToArrayBuffer(base64Str) { + if (!base64Str) { + return { earlyData: null, error: null }; + } + try { + // go use modified Base64 for URL rfc4648 which js atob not support + base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/'); + const decode = atob(base64Str); + const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0)); + return { earlyData: arryBuffer.buffer, error: null }; + } catch (error) { + return { earlyData: null, error }; + } +} + +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. + */ +function safeCloseWebSocket(socket) { + try { + if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) { + socket.close(); + } + } catch (error) { + console.error('safeCloseWebSocket error', error); + } +} \ No newline at end of file diff --git a/src/protocols/vless.js b/src/protocols/vless.js new file mode 100644 index 000000000..4ebcb7c25 --- /dev/null +++ b/src/protocols/vless.js @@ -0,0 +1,603 @@ +import { connect } from 'cloudflare:sockets'; +import { configs } from '../helpers/config.js'; +import { isValidUUID } from '../helpers/helpers.js'; +let proxyIP = configs.proxyIP; +let userID = configs.userID; +const dohURL = configs.dohURL; +/** + * Handles VLESS over WebSocket requests by creating a WebSocket pair, accepting the WebSocket connection, and processing the VLESS header. + * @param {import("@cloudflare/workers-types").Request} request The incoming request object. + * @returns {Promise} A Promise that resolves to a WebSocket response object. + */ +export async function vlessOverWSHandler(request, env) { + /** @type {import("@cloudflare/workers-types").WebSocket[]} */ + // @ts-ignore + userID = env.UUID || userID; + if (!isValidUUID(userID)) throw new Error(`Invalid UUID: ${userID}`); + proxyIP = env.PROXYIP || proxyIP; + const webSocketPair = new WebSocketPair(); + const [client, webSocket] = Object.values(webSocketPair); + + webSocket.accept(); + + let address = ""; + let portWithRandomLog = ""; + const log = (/** @type {string} */ info, /** @type {string | undefined} */ event) => { + console.log(`[${address}:${portWithRandomLog}] ${info}`, event || ""); + }; + const earlyDataHeader = request.headers.get("sec-websocket-protocol") || ""; + + const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log); + + /** @type {{ value: import("@cloudflare/workers-types").Socket | null}}*/ + let remoteSocketWapper = { + value: null, + }; + let udpStreamWrite = null; + let isDns = false; + + // ws --> remote + readableWebSocketStream + .pipeTo( + new WritableStream({ + async write(chunk, controller) { + if (isDns && udpStreamWrite) { + return udpStreamWrite(chunk); + } + if (remoteSocketWapper.value) { + const writer = remoteSocketWapper.value.writable.getWriter(); + await writer.write(chunk); + writer.releaseLock(); + return; + } + + const { + hasError, + message, + portRemote = 443, + addressRemote = "", + rawDataIndex, + vlessVersion = new Uint8Array([0, 0]), + isUDP, + } = await processVlessHeader(chunk, userID); + address = addressRemote; + portWithRandomLog = `${portRemote}--${Math.random()} ${isUDP ? "udp " : "tcp "} `; + if (hasError) { + // controller.error(message); + throw new Error(message); // cf seems has bug, controller.error will not end stream + // webSocket.close(1000, message); + return; + } + // if UDP but port not DNS port, close it + if (isUDP) { + if (portRemote === 53) { + isDns = true; + } else { + // controller.error('UDP proxy only enable for DNS which is port 53'); + throw new Error("UDP proxy only enable for DNS which is port 53"); // cf seems has bug, controller.error will not end stream + return; + } + } + // ["version", "附加信息长度 N"] + const vlessResponseHeader = new Uint8Array([vlessVersion[0], 0]); + const rawClientData = chunk.slice(rawDataIndex); + + // TODO: support udp here when cf runtime has udp support + if (isDns) { + const { write } = await handleUDPOutBound(webSocket, vlessResponseHeader, log); + udpStreamWrite = write; + udpStreamWrite(rawClientData); + return; + } + + handleTCPOutBound( + request, + remoteSocketWapper, + addressRemote, + portRemote, + rawClientData, + webSocket, + vlessResponseHeader, + log + ); + }, + close() { + log(`readableWebSocketStream is close`); + }, + abort(reason) { + log(`readableWebSocketStream is abort`, JSON.stringify(reason)); + }, + }) + ) + .catch((err) => { + log("readableWebSocketStream pipeTo error", err); + }); + + return new Response(null, { + status: 101, + // @ts-ignore + webSocket: client, + }); +} + +/** + * Checks if a given UUID is present in the API response. + * @param {string} targetUuid The UUID to search for. + * @returns {Promise} A Promise that resolves to true if the UUID is present in the API response, false otherwise. + */ +async function checkUuidInApiResponse(targetUuid) { + // Check if any of the environment variables are empty + + try { + const apiResponse = await getApiResponse(); + if (!apiResponse) { + return false; + } + const isUuidInResponse = apiResponse.users.some((user) => user.uuid === targetUuid); + return isUuidInResponse; + } catch (error) { + console.error("Error:", error); + return false; + } +} + +/** + * Handles outbound TCP connections. + * + * @param {any} remoteSocket + * @param {string} addressRemote The remote address to connect to. + * @param {number} portRemote The remote port to connect to. + * @param {Uint8Array} rawClientData The raw client data to write. + * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to pass the remote socket to. + * @param {Uint8Array} vlessResponseHeader The VLESS response header. + * @param {function} log The logging function. + * @returns {Promise} The remote socket. + */ +async function handleTCPOutBound( + request, + remoteSocket, + addressRemote, + portRemote, + rawClientData, + webSocket, + vlessResponseHeader, + log +) { + async function connectAndWrite(address, port) { + if (/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(address)) address = `${atob('d3d3Lg==')}${address}${atob('LnNzbGlwLmlv')}`; + /** @type {import("@cloudflare/workers-types").Socket} */ + const tcpSocket = connect({ + hostname: address, + port: port, + }); + remoteSocket.value = tcpSocket; + log(`connected to ${address}:${port}`); + const writer = tcpSocket.writable.getWriter(); + await writer.write(rawClientData); // first write, nomal is tls client hello + writer.releaseLock(); + return tcpSocket; + } + + // if the cf connect tcp socket have no incoming data, we retry to redirect ip + async function retry() { + const { pathname } = new URL(request.url); + let panelProxyIP = pathname.split('/')[2]; + panelProxyIP = panelProxyIP ? atob(panelProxyIP) : undefined; + const tcpSocket = await connectAndWrite(panelProxyIP || 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); + }); + + vlessRemoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, null, log); + } + + const tcpSocket = await connectAndWrite(addressRemote, portRemote); + + // when remoteSocket is ready, pass to websocket + // remote--> ws + vlessRemoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, retry, log); +} + +/** + * Creates a readable stream from a WebSocket server, allowing for data to be read from the WebSocket. + * @param {import("@cloudflare/workers-types").WebSocket} webSocketServer The WebSocket server to create the readable stream from. + * @param {string} earlyDataHeader The header containing early data for WebSocket 0-RTT. + * @param {(info: string)=> void} log The logging function. + * @returns {ReadableStream} A readable stream that can be used to read data from the WebSocket. + */ +function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) { + let readableStreamCancel = false; + const stream = new ReadableStream({ + start(controller) { + webSocketServer.addEventListener("message", (event) => { + if (readableStreamCancel) { + return; + } + const message = event.data; + controller.enqueue(message); + }); + + // The event means that the client closed the client -> server stream. + // However, the server -> client stream is still open until you call close() on the server side. + // The WebSocket protocol says that a separate close message must be sent in each direction to fully close the socket. + webSocketServer.addEventListener("close", () => { + // client send close, need close server + // if stream is cancel, skip controller.close + safeCloseWebSocket(webSocketServer); + if (readableStreamCancel) { + return; + } + controller.close(); + }); + webSocketServer.addEventListener("error", (err) => { + log("webSocketServer has error"); + controller.error(err); + }); + // for ws 0rtt + const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader); + if (error) { + controller.error(error); + } else if (earlyData) { + controller.enqueue(earlyData); + } + }, + pull(controller) { + // if ws can stop read if stream is full, we can implement backpressure + // https://streams.spec.whatwg.org/#example-rs-push-backpressure + }, + cancel(reason) { + // 1. pipe WritableStream has error, this cancel will called, so ws handle server close into here + // 2. if readableStream is cancel, all controller.close/enqueue need skip, + // 3. but from testing controller.error still work even if readableStream is cancel + if (readableStreamCancel) { + return; + } + log(`ReadableStream was canceled, due to ${reason}`); + readableStreamCancel = true; + safeCloseWebSocket(webSocketServer); + }, + }); + + return stream; +} + +// https://xtls.github.io/development/protocols/vless.html +// https://github.com/zizifn/excalidraw-backup/blob/main/v2ray-protocol.excalidraw + +/** + * Processes the VLESS header buffer and returns an object with the relevant information. + * @param {ArrayBuffer} vlessBuffer The VLESS header buffer to process. + * @param {string} userID The user ID to validate against the UUID in the VLESS header. + * @returns {{ + * hasError: boolean, + * message?: string, + * addressRemote?: string, + * addressType?: number, + * portRemote?: number, + * rawDataIndex?: number, + * vlessVersion?: Uint8Array, + * isUDP?: boolean + * }} An object with the relevant information extracted from the VLESS header buffer. + */ +async function processVlessHeader(vlessBuffer, userID) { + if (vlessBuffer.byteLength < 24) { + return { + hasError: true, + message: "invalid data", + }; + } + const version = new Uint8Array(vlessBuffer.slice(0, 1)); + let isValidUser = false; + let isUDP = false; + const slicedBuffer = new Uint8Array(vlessBuffer.slice(1, 17)); + const slicedBufferString = stringify(slicedBuffer); + + const uuids = userID.includes(",") ? userID.split(",") : [userID]; + + const checkUuidInApi = await checkUuidInApiResponse(slicedBufferString); + isValidUser = uuids.some((userUuid) => checkUuidInApi || slicedBufferString === userUuid.trim()); + + console.log(`checkUuidInApi: ${await checkUuidInApiResponse(slicedBufferString)}, userID: ${slicedBufferString}`); + + if (!isValidUser) { + return { + hasError: true, + message: "invalid user", + }; + } + + const optLength = new Uint8Array(vlessBuffer.slice(17, 18))[0]; + //skip opt for now + + const command = new Uint8Array(vlessBuffer.slice(18 + optLength, 18 + optLength + 1))[0]; + + // 0x01 TCP + // 0x02 UDP + // 0x03 MUX + if (command === 1) { + } else if (command === 2) { + isUDP = true; + } else { + return { + hasError: true, + message: `command ${command} is not support, command 01-tcp,02-udp,03-mux`, + }; + } + const portIndex = 18 + optLength + 1; + const portBuffer = vlessBuffer.slice(portIndex, portIndex + 2); + // port is big-Endian in raw data etc 80 == 0x005d + const portRemote = new DataView(portBuffer).getUint16(0); + + let addressIndex = portIndex + 2; + const addressBuffer = new Uint8Array(vlessBuffer.slice(addressIndex, addressIndex + 1)); + + // 1--> ipv4 addressLength =4 + // 2--> domain name addressLength=addressBuffer[1] + // 3--> ipv6 addressLength =16 + const addressType = addressBuffer[0]; + let addressLength = 0; + let addressValueIndex = addressIndex + 1; + let addressValue = ""; + switch (addressType) { + case 1: + addressLength = 4; + addressValue = new Uint8Array(vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength)).join("."); + break; + case 2: + addressLength = new Uint8Array(vlessBuffer.slice(addressValueIndex, addressValueIndex + 1))[0]; + addressValueIndex += 1; + addressValue = new TextDecoder().decode(vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength)); + break; + case 3: + addressLength = 16; + const dataView = new DataView(vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength)); + // 2001:0db8:85a3:0000:0000:8a2e:0370:7334 + const ipv6 = []; + for (let i = 0; i < 8; i++) { + ipv6.push(dataView.getUint16(i * 2).toString(16)); + } + addressValue = ipv6.join(":"); + // seems no need add [] for ipv6 + break; + default: + return { + hasError: true, + message: `invild addressType is ${addressType}`, + }; + } + if (!addressValue) { + return { + hasError: true, + message: `addressValue is empty, addressType is ${addressType}`, + }; + } + + return { + hasError: false, + addressRemote: addressValue, + addressType, + portRemote, + rawDataIndex: addressValueIndex + addressLength, + vlessVersion: version, + isUDP, + }; +} + +/** + * Converts a remote socket to a WebSocket connection. + * @param {import("@cloudflare/workers-types").Socket} remoteSocket The remote socket to convert. + * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to connect to. + * @param {ArrayBuffer | null} vlessResponseHeader The VLESS response header. + * @param {(() => Promise) | null} retry The function to retry the connection if it fails. + * @param {(info: string) => void} log The logging function. + * @returns {Promise} A Promise that resolves when the conversion is complete. + */ +async function vlessRemoteSocketToWS(remoteSocket, webSocket, vlessResponseHeader, retry, log) { + // remote--> ws + let remoteChunkCount = 0; + let chunks = []; + /** @type {ArrayBuffer | null} */ + let vlessHeader = vlessResponseHeader; + let hasIncomingData = false; // check if remoteSocket has incoming data + await remoteSocket.readable + .pipeTo( + new WritableStream({ + start() {}, + /** + * + * @param {Uint8Array} chunk + * @param {*} controller + */ + async write(chunk, controller) { + hasIncomingData = true; + // remoteChunkCount++; + if (webSocket.readyState !== WS_READY_STATE_OPEN) { + controller.error("webSocket.readyState is not open, maybe close"); + } + if (vlessHeader) { + webSocket.send(await new Blob([vlessHeader, chunk]).arrayBuffer()); + vlessHeader = null; + } else { + // seems no need rate limit this, CF seems fix this??.. + // if (remoteChunkCount > 20000) { + // // cf one package is 4096 byte(4kb), 4096 * 20000 = 80M + // await delay(1); + // } + webSocket.send(chunk); + } + }, + close() { + log(`remoteConnection!.readable is close with hasIncomingData is ${hasIncomingData}`); + // safeCloseWebSocket(webSocket); // no need server close websocket frist for some case will casue HTTP ERR_CONTENT_LENGTH_MISMATCH issue, client will send close event anyway. + }, + abort(reason) { + console.error(`remoteConnection!.readable abort`, reason); + }, + }) + ) + .catch((error) => { + console.error(`vlessRemoteSocketToWS has exception `, error.stack || error); + safeCloseWebSocket(webSocket); + }); + + // seems is cf connect socket have error, + // 1. Socket.closed will have error + // 2. Socket.readable will be close without any data coming + if (hasIncomingData === false && retry) { + log(`retry`); + retry(); + } +} + +/** + * Decodes a base64 string into an ArrayBuffer. + * @param {string} base64Str The base64 string to decode. + * @returns {{earlyData: ArrayBuffer|null, error: Error|null}} An object containing the decoded ArrayBuffer or null if there was an error, and any error that occurred during decoding or null if there was no error. + */ +function base64ToArrayBuffer(base64Str) { + if (!base64Str) { + return { earlyData: null, error: null }; + } + try { + // go use modified Base64 for URL rfc4648 which js atob not support + base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/'); + const decode = atob(base64Str); + const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0)); + return { earlyData: arryBuffer.buffer, error: null }; + } catch (error) { + return { earlyData: null, error }; + } +} + +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. + */ +function safeCloseWebSocket(socket) { + try { + if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) { + socket.close(); + } + } catch (error) { + console.error('safeCloseWebSocket error', error); + } +} + +const byteToHex = []; + +for (let i = 0; i < 256; ++i) { + byteToHex.push((i + 256).toString(16).slice(1)); +} + +function unsafeStringify(arr, offset = 0) { + return ( + byteToHex[arr[offset + 0]] + + byteToHex[arr[offset + 1]] + + byteToHex[arr[offset + 2]] + + byteToHex[arr[offset + 3]] + + "-" + + byteToHex[arr[offset + 4]] + + byteToHex[arr[offset + 5]] + + "-" + + byteToHex[arr[offset + 6]] + + byteToHex[arr[offset + 7]] + + "-" + + byteToHex[arr[offset + 8]] + + byteToHex[arr[offset + 9]] + + "-" + + byteToHex[arr[offset + 10]] + + byteToHex[arr[offset + 11]] + + byteToHex[arr[offset + 12]] + + byteToHex[arr[offset + 13]] + + byteToHex[arr[offset + 14]] + + byteToHex[arr[offset + 15]] + ).toLowerCase(); +} + +function stringify(arr, offset = 0) { + const uuid = unsafeStringify(arr, offset); + if (!isValidUUID(uuid)) { + throw TypeError("Stringified UUID is invalid"); + } + return uuid; +} + +/** + * Handles outbound UDP traffic by transforming the data into DNS queries and sending them over a WebSocket connection. + * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket connection to send the DNS queries over. + * @param {ArrayBuffer} vlessResponseHeader The VLESS response header. + * @param {(string) => void} log The logging function. + * @returns {{write: (chunk: Uint8Array) => void}} An object with a write method that accepts a Uint8Array chunk to write to the transform stream. + */ +async function handleUDPOutBound(webSocket, vlessResponseHeader, log) { + let isVlessHeaderSent = false; + const transformStream = new TransformStream({ + start(controller) {}, + transform(chunk, controller) { + // udp message 2 byte is the the length of udp data + // TODO: this should have bug, beacsue maybe udp chunk can be in two websocket message + for (let index = 0; index < chunk.byteLength; ) { + const lengthBuffer = chunk.slice(index, index + 2); + const udpPakcetLength = new DataView(lengthBuffer).getUint16(0); + const udpData = new Uint8Array(chunk.slice(index + 2, index + 2 + udpPakcetLength)); + index = index + 2 + udpPakcetLength; + controller.enqueue(udpData); + } + }, + flush(controller) {}, + }); + + // only handle dns udp for now + transformStream.readable + .pipeTo( + new WritableStream({ + async write(chunk) { + const resp = await fetch( + dohURL, // dns server url + { + method: "POST", + headers: { + "content-type": "application/dns-message", + }, + body: chunk, + } + ); + const dnsQueryResult = await resp.arrayBuffer(); + const udpSize = dnsQueryResult.byteLength; + // console.log([...new Uint8Array(dnsQueryResult)].map((x) => x.toString(16))); + const udpSizeBuffer = new Uint8Array([(udpSize >> 8) & 0xff, udpSize & 0xff]); + if (webSocket.readyState === WS_READY_STATE_OPEN) { + log(`doh success and dns message length is ${udpSize}`); + if (isVlessHeaderSent) { + webSocket.send(await new Blob([udpSizeBuffer, dnsQueryResult]).arrayBuffer()); + } else { + webSocket.send(await new Blob([vlessResponseHeader, udpSizeBuffer, dnsQueryResult]).arrayBuffer()); + isVlessHeaderSent = true; + } + } + }, + }) + ) + .catch((error) => { + log("dns udp has error" + error); + }); + + const writer = transformStream.writable.getWriter(); + + return { + /** + * + * @param {Uint8Array} chunk + */ + write(chunk) { + writer.write(chunk); + }, + }; +} \ No newline at end of file diff --git a/src/protocols/warp.js b/src/protocols/warp.js new file mode 100644 index 000000000..81fce3aa0 --- /dev/null +++ b/src/protocols/warp.js @@ -0,0 +1,77 @@ +import nacl from 'tweetnacl'; + +export async function fetchWgConfig (env, proxySettings) { + let warpConfigs = []; + const apiBaseUrl = 'https://api.cloudflareclient.com/v0a4005/reg'; + + const { warpPlusLicense } = proxySettings; + const warpKeys = [generateKeyPair(), generateKeyPair()]; + + for(let i = 0; i < 2; i++) { + const accountResponse = await fetch(apiBaseUrl, { + method: 'POST', + headers: { + 'User-Agent': 'insomnia/8.6.1', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + key: warpKeys[i].publicKey, + install_id: "", + fcm_token: "", + tos: new Date().toISOString(), + type: "Android", + model: 'PC', + locale: 'en_US', + warp_enabled: true + }) + }); + + const accountData = await accountResponse.json(); + warpConfigs.push ({ + privateKey: warpKeys[i].privateKey, + account: accountData + }); + + if (warpPlusLicense) { + const response = await fetch(`${apiBaseUrl}/${accountData.id}/account`, { + method: 'PUT', + headers: { + 'User-Agent': 'insomnia/8.6.1', + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${accountData.token}` + }, + body: JSON.stringify({ + key: warpKeys[i].publicKey, + install_id: "", + fcm_token: "", + tos: new Date().toISOString(), + type: "Android", + model: 'PC', + locale: 'en_US', + warp_enabled: true, + license: warpPlusLicense + }) + }); + + const responseData = await response.json(); + if(response.status !== 200 && !responseData.success) return { error: responseData.errors[0]?.message, configs: null} + } + } + + const configs = JSON.stringify(warpConfigs) + await env.bpb.put('warpConfigs', configs); + return { error: null, configs }; +} + +const generateKeyPair = () => { + const base64Encode = (array) => btoa(String.fromCharCode.apply(null, array)); + let privateKey = nacl.randomBytes(32); + privateKey[0] &= 248; + privateKey[31] &= 127; + privateKey[31] |= 64; + let publicKey = nacl.scalarMult.base(privateKey); + const publicKeyBase64 = base64Encode(publicKey); + const privateKeyBase64 = base64Encode(privateKey); + + return { publicKey: publicKeyBase64, privateKey: privateKeyBase64 }; +}; \ No newline at end of file diff --git a/src/worker.js b/src/worker.js index a5f658744..42c32662c 100644 --- a/src/worker.js +++ b/src/worker.js @@ -1,45 +1,26 @@ // https://github.com/bia-pain-bache/BPB-Worker-Panel -import { connect } from 'cloudflare:sockets'; -import nacl from 'tweetnacl'; -import sha256 from 'js-sha256'; -import { SignJWT, jwtVerify } from 'jose'; - -// How to generate your own UUID: -// https://www.uuidgenerator.net/ -let userID = '89b3cbba-e6ac-485a-9481-976a0415eab9'; -let trojanPassword = `bpb-trojan`; - -// https://www.nslookup.io/domains/bpb.yousef.isegaro.com/dns-records/ -const proxyIPs= ['bpb.yousef.isegaro.com']; -const defaultHttpPorts = ['80', '8080', '2052', '2082', '2086', '2095', '8880']; -const defaultHttpsPorts = ['443', '8443', '2053', '2083', '2087', '2096']; -let proxyIP = proxyIPs[Math.floor(Math.random() * proxyIPs.length)]; -let dohURL = 'https://cloudflare-dns.com/dns-query'; -let hashPassword; -let panelVersion = '2.7.2'; +import { vlessOverWSHandler } from './protocols/vless.js'; +import { trojanOverWSHandler } from './protocols/trojan.js'; +import { fetchWgConfig } from './protocols/warp.js'; +import { getDataset, updateDataset } from './kv/handlers.js'; +import { generateJWTToken, generateSecretKey, Authenticate } from './authentication/auth.js'; +import { renderHomePage } from './pages/homePage.js'; +import { renderLoginPage } from './pages/loginPage.js'; +import { renderErrorPage } from './pages/errorPage.js'; +import { getXrayCustomConfigs, getXrayWarpConfigs } from './cores/xray.js'; +import { getSingBoxCustomConfig, getSingBoxWarpConfig } from './cores/sing-box.js'; +import { getClashNormalConfig, getClashWarpConfig } from './cores/clash.js'; +import { getNormalConfigs } from './cores/normalConfigs.js'; +globalThis.hostName = ''; export default { - /** - * @param {import("@cloudflare/workers-types").Request} request - * @param {{UUID: string, PROXYIP: string, DNS_RESOLVER_URL: string}} env - * @param {import("@cloudflare/workers-types").ExecutionContext} ctx - * @returns {Promise} - */ async fetch(request, env) { try { - userID = env.UUID || userID; - proxyIP = env.PROXYIP || proxyIP; - dohURL = env.DNS_RESOLVER_URL || dohURL; - trojanPassword = env.TROJAN_PASS || trojanPassword; - hashPassword = sha256.sha224(trojanPassword); - if (!isValidUUID(userID)) throw new Error(`Invalid UUID: ${userID}`); const upgradeHeader = request.headers.get('Upgrade'); const url = new URL(request.url); - if (!upgradeHeader || upgradeHeader !== 'websocket') { - + globalThis.hostName = request.headers.get('Host'); const searchParams = new URLSearchParams(url.search); - const host = request.headers.get('Host'); const client = searchParams.get('app'); const { kvNotFound, proxySettings: settings, warpConfigs } = await getDataset(env); if (kvNotFound) { @@ -71,7 +52,7 @@ export default { case `/sub/${userID}`: if (client === 'sfa') { - const BestPingSFA = await getSingBoxCustomConfig(env, settings, host, client, false); + const BestPingSFA = await getSingBoxCustomConfig(env, settings, false); return new Response(JSON.stringify(BestPingSFA, null, 4), { status: 200, headers: { @@ -83,7 +64,7 @@ export default { } if (client === 'clash') { - const BestPingClash = await getClashNormalConfig(env, settings, host); + const BestPingClash = await getClashNormalConfig(env, settings); return new Response(JSON.stringify(BestPingClash, null, 4), { status: 200, headers: { @@ -95,7 +76,7 @@ export default { } if (client === 'xray') { - const xrayFullConfigs = await getXrayCustomConfigs(env, settings, host, false); + const xrayFullConfigs = await getXrayCustomConfigs(env, settings, false); return new Response(JSON.stringify(xrayFullConfigs, null, 4), { status: 200, headers: { @@ -106,7 +87,7 @@ export default { }); } - const normalConfigs = await getNormalConfigs(settings, host, client); + const normalConfigs = await getNormalConfigs(env, settings, client); return new Response(normalConfigs, { status: 200, headers: { @@ -118,8 +99,8 @@ export default { case `/fragsub/${userID}`: let fragConfigs = client === 'hiddify' - ? await getSingBoxCustomConfig(env, settings, host, client, true) - : await getXrayCustomConfigs(env, settings, host, true); + ? await getSingBoxCustomConfig(env, settings, true) + : await getXrayCustomConfigs(env, settings, true); return new Response(JSON.stringify(fragConfigs, null, 4), { status: 200, @@ -181,7 +162,7 @@ export default { const pwd = await env.bpb.get('pwd'); if (pwd && !isAuth) return Response.redirect(`${url.origin}/login`, 302); const isPassSet = pwd?.length >= 8; - const homePage = renderHomePage(request, settings, host, isPassSet); + const homePage = renderHomePage(request, env, settings, isPassSet); return new Response(homePage, { status: 200, headers: { @@ -277,5119 +258,12 @@ export default { } } else { return url.pathname.startsWith('/tr') - ? await trojanOverWSHandler(request) - : await vlessOverWSHandler(request); + ? await trojanOverWSHandler(request, env) + : await vlessOverWSHandler(request, env); } } catch (err) { const errorPage = renderErrorPage('Something went wrong!', err, false); return new Response(errorPage, { status: 200, headers: {'Content-Type': 'text/html'}}); } } -}; - -/** - * Handles VLESS over WebSocket requests by creating a WebSocket pair, accepting the WebSocket connection, and processing the VLESS 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 vlessOverWSHandler(request) { - /** @type {import("@cloudflare/workers-types").WebSocket[]} */ - // @ts-ignore - const webSocketPair = new WebSocketPair(); - const [client, webSocket] = Object.values(webSocketPair); - - webSocket.accept(); - - let address = ""; - let portWithRandomLog = ""; - const log = (/** @type {string} */ info, /** @type {string | undefined} */ event) => { - console.log(`[${address}:${portWithRandomLog}] ${info}`, event || ""); - }; - const earlyDataHeader = request.headers.get("sec-websocket-protocol") || ""; - - const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log); - - /** @type {{ value: import("@cloudflare/workers-types").Socket | null}}*/ - let remoteSocketWapper = { - value: null, - }; - let udpStreamWrite = null; - let isDns = false; - - // ws --> remote - readableWebSocketStream - .pipeTo( - new WritableStream({ - async write(chunk, controller) { - if (isDns && udpStreamWrite) { - return udpStreamWrite(chunk); - } - if (remoteSocketWapper.value) { - const writer = remoteSocketWapper.value.writable.getWriter(); - await writer.write(chunk); - writer.releaseLock(); - return; - } - - const { - hasError, - message, - portRemote = 443, - addressRemote = "", - rawDataIndex, - vlessVersion = new Uint8Array([0, 0]), - isUDP, - } = await processVlessHeader(chunk, userID); - address = addressRemote; - portWithRandomLog = `${portRemote}--${Math.random()} ${isUDP ? "udp " : "tcp "} `; - if (hasError) { - // controller.error(message); - throw new Error(message); // cf seems has bug, controller.error will not end stream - // webSocket.close(1000, message); - return; - } - // if UDP but port not DNS port, close it - if (isUDP) { - if (portRemote === 53) { - isDns = true; - } else { - // controller.error('UDP proxy only enable for DNS which is port 53'); - throw new Error("UDP proxy only enable for DNS which is port 53"); // cf seems has bug, controller.error will not end stream - return; - } - } - // ["version", "附加信息长度 N"] - const vlessResponseHeader = new Uint8Array([vlessVersion[0], 0]); - const rawClientData = chunk.slice(rawDataIndex); - - // TODO: support udp here when cf runtime has udp support - if (isDns) { - const { write } = await handleUDPOutBound(webSocket, vlessResponseHeader, log); - udpStreamWrite = write; - udpStreamWrite(rawClientData); - return; - } - - handleTCPOutBound( - request, - remoteSocketWapper, - addressRemote, - portRemote, - rawClientData, - webSocket, - vlessResponseHeader, - log - ); - }, - close() { - log(`readableWebSocketStream is close`); - }, - abort(reason) { - log(`readableWebSocketStream is abort`, JSON.stringify(reason)); - }, - }) - ) - .catch((err) => { - log("readableWebSocketStream pipeTo error", err); - }); - - return new Response(null, { - status: 101, - // @ts-ignore - webSocket: client, - }); -} - -/** - * Checks if a given UUID is present in the API response. - * @param {string} targetUuid The UUID to search for. - * @returns {Promise} A Promise that resolves to true if the UUID is present in the API response, false otherwise. - */ -async function checkUuidInApiResponse(targetUuid) { - // Check if any of the environment variables are empty - - try { - const apiResponse = await getApiResponse(); - if (!apiResponse) { - return false; - } - const isUuidInResponse = apiResponse.users.some((user) => user.uuid === targetUuid); - return isUuidInResponse; - } catch (error) { - console.error("Error:", error); - return false; - } -} - -async function trojanOverWSHandler(request) { - const webSocketPair = new WebSocketPair(); - const [client, webSocket] = Object.values(webSocketPair); - webSocket.accept(); - let address = ""; - let portWithRandomLog = ""; - const log = (info, event) => { - console.log(`[${address}:${portWithRandomLog}] ${info}`, event || ""); - }; - const earlyDataHeader = request.headers.get("sec-websocket-protocol") || ""; - const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log); - let remoteSocketWapper = { - value: null, - }; - let udpStreamWrite = null; - - readableWebSocketStream - .pipeTo( - new WritableStream({ - async write(chunk, controller) { - if (udpStreamWrite) { - return udpStreamWrite(chunk); - } - - if (remoteSocketWapper.value) { - const writer = remoteSocketWapper.value.writable.getWriter(); - await writer.write(chunk); - writer.releaseLock(); - return; - } - - const { - hasError, - message, - portRemote = 443, - addressRemote = "", - rawClientData, - } = await parseTrojanHeader(chunk); - - address = addressRemote; - portWithRandomLog = `${portRemote}--${Math.random()} tcp`; - - if (hasError) { - throw new Error(message); - return; - } - - handleTCPOutBound(request, remoteSocketWapper, addressRemote, portRemote, rawClientData, webSocket, false, log); - }, - close() { - log(`readableWebSocketStream is closed`); - }, - abort(reason) { - log(`readableWebSocketStream is aborted`, JSON.stringify(reason)); - }, - }) - ) - .catch((err) => { - log("readableWebSocketStream pipeTo error", err); - }); - - return new Response(null, { - status: 101, - // @ts-ignore - webSocket: client, - }); -} - -async function parseTrojanHeader(buffer) { - if (buffer.byteLength < 56) { - return { - hasError: true, - message: "invalid data", - }; - } - - let crLfIndex = 56; - if (new Uint8Array(buffer.slice(56, 57))[0] !== 0x0d || new Uint8Array(buffer.slice(57, 58))[0] !== 0x0a) { - return { - hasError: true, - message: "invalid header format (missing CR LF)", - }; - } - - const password = new TextDecoder().decode(buffer.slice(0, crLfIndex)); - if (password !== hashPassword) { - return { - hasError: true, - message: "invalid password", - }; - } - - const socks5DataBuffer = buffer.slice(crLfIndex + 2); - if (socks5DataBuffer.byteLength < 6) { - return { - hasError: true, - message: "invalid SOCKS5 request data", - }; - } - - const view = new DataView(socks5DataBuffer); - const cmd = view.getUint8(0); - if (cmd !== 1) { - return { - hasError: true, - message: "unsupported command, only TCP (CONNECT) is allowed", - }; - } - - const atype = view.getUint8(1); - // 0x01: IPv4 address - // 0x03: Domain name - // 0x04: IPv6 address - let addressLength = 0; - let addressIndex = 2; - let address = ""; - switch (atype) { - case 1: - addressLength = 4; - address = new Uint8Array(socks5DataBuffer.slice(addressIndex, addressIndex + addressLength)).join("."); - break; - case 3: - addressLength = new Uint8Array(socks5DataBuffer.slice(addressIndex, addressIndex + 1))[0]; - addressIndex += 1; - address = new TextDecoder().decode(socks5DataBuffer.slice(addressIndex, addressIndex + addressLength)); - break; - case 4: - addressLength = 16; - const dataView = new DataView(socks5DataBuffer.slice(addressIndex, addressIndex + addressLength)); - const ipv6 = []; - for (let i = 0; i < 8; i++) { - ipv6.push(dataView.getUint16(i * 2).toString(16)); - } - address = ipv6.join(":"); - break; - default: - return { - hasError: true, - message: `invalid addressType is ${atype}`, - }; - } - - if (!address) { - return { - hasError: true, - message: `address is empty, addressType is ${atype}`, - }; - } - - const portIndex = addressIndex + addressLength; - const portBuffer = socks5DataBuffer.slice(portIndex, portIndex + 2); - const portRemote = new DataView(portBuffer).getUint16(0); - return { - hasError: false, - addressRemote: address, - portRemote, - rawClientData: socks5DataBuffer.slice(portIndex + 4), - }; -} - -/** - * Handles outbound TCP connections. - * - * @param {any} remoteSocket - * @param {string} addressRemote The remote address to connect to. - * @param {number} portRemote The remote port to connect to. - * @param {Uint8Array} rawClientData The raw client data to write. - * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to pass the remote socket to. - * @param {Uint8Array} vlessResponseHeader The VLESS response header. - * @param {function} log The logging function. - * @returns {Promise} The remote socket. - */ -async function handleTCPOutBound( - request, - remoteSocket, - addressRemote, - portRemote, - rawClientData, - webSocket, - vlessResponseHeader, - log -) { - async function connectAndWrite(address, port) { - if (/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(address)) address = `${atob('d3d3Lg==')}${address}${atob('LnNzbGlwLmlv')}`; - /** @type {import("@cloudflare/workers-types").Socket} */ - const tcpSocket = connect({ - hostname: address, - port: port, - }); - remoteSocket.value = tcpSocket; - log(`connected to ${address}:${port}`); - const writer = tcpSocket.writable.getWriter(); - await writer.write(rawClientData); // first write, nomal is tls client hello - writer.releaseLock(); - return tcpSocket; - } - - // if the cf connect tcp socket have no incoming data, we retry to redirect ip - async function retry() { - const { pathname } = new URL(request.url); - let panelProxyIP = pathname.split('/')[2]; - panelProxyIP = panelProxyIP ? atob(panelProxyIP) : undefined; - const tcpSocket = await connectAndWrite(panelProxyIP || 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); - }); - - vlessResponseHeader - ? vlessRemoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, null, log) - : trojanRemoteSocketToWS(tcpSocket, webSocket, null, log); - } - - const tcpSocket = await connectAndWrite(addressRemote, portRemote); - - // when remoteSocket is ready, pass to websocket - // remote--> ws - vlessResponseHeader - ? vlessRemoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, retry, log) - : trojanRemoteSocketToWS(tcpSocket, webSocket, retry, log); -} - -/** - * Creates a readable stream from a WebSocket server, allowing for data to be read from the WebSocket. - * @param {import("@cloudflare/workers-types").WebSocket} webSocketServer The WebSocket server to create the readable stream from. - * @param {string} earlyDataHeader The header containing early data for WebSocket 0-RTT. - * @param {(info: string)=> void} log The logging function. - * @returns {ReadableStream} A readable stream that can be used to read data from the WebSocket. - */ -function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) { - let readableStreamCancel = false; - const stream = new ReadableStream({ - start(controller) { - webSocketServer.addEventListener("message", (event) => { - if (readableStreamCancel) { - return; - } - const message = event.data; - controller.enqueue(message); - }); - - // The event means that the client closed the client -> server stream. - // However, the server -> client stream is still open until you call close() on the server side. - // The WebSocket protocol says that a separate close message must be sent in each direction to fully close the socket. - webSocketServer.addEventListener("close", () => { - // client send close, need close server - // if stream is cancel, skip controller.close - safeCloseWebSocket(webSocketServer); - if (readableStreamCancel) { - return; - } - controller.close(); - }); - webSocketServer.addEventListener("error", (err) => { - log("webSocketServer has error"); - controller.error(err); - }); - // for ws 0rtt - const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader); - if (error) { - controller.error(error); - } else if (earlyData) { - controller.enqueue(earlyData); - } - }, - pull(controller) { - // if ws can stop read if stream is full, we can implement backpressure - // https://streams.spec.whatwg.org/#example-rs-push-backpressure - }, - cancel(reason) { - // 1. pipe WritableStream has error, this cancel will called, so ws handle server close into here - // 2. if readableStream is cancel, all controller.close/enqueue need skip, - // 3. but from testing controller.error still work even if readableStream is cancel - if (readableStreamCancel) { - return; - } - log(`ReadableStream was canceled, due to ${reason}`); - readableStreamCancel = true; - safeCloseWebSocket(webSocketServer); - }, - }); - - return stream; -} - -// https://xtls.github.io/development/protocols/vless.html -// https://github.com/zizifn/excalidraw-backup/blob/main/v2ray-protocol.excalidraw - -/** - * Processes the VLESS header buffer and returns an object with the relevant information. - * @param {ArrayBuffer} vlessBuffer The VLESS header buffer to process. - * @param {string} userID The user ID to validate against the UUID in the VLESS header. - * @returns {{ - * hasError: boolean, - * message?: string, - * addressRemote?: string, - * addressType?: number, - * portRemote?: number, - * rawDataIndex?: number, - * vlessVersion?: Uint8Array, - * isUDP?: boolean - * }} An object with the relevant information extracted from the VLESS header buffer. - */ -async function processVlessHeader(vlessBuffer, userID) { - if (vlessBuffer.byteLength < 24) { - return { - hasError: true, - message: "invalid data", - }; - } - const version = new Uint8Array(vlessBuffer.slice(0, 1)); - let isValidUser = false; - let isUDP = false; - const slicedBuffer = new Uint8Array(vlessBuffer.slice(1, 17)); - const slicedBufferString = stringify(slicedBuffer); - - const uuids = userID.includes(",") ? userID.split(",") : [userID]; - - const checkUuidInApi = await checkUuidInApiResponse(slicedBufferString); - isValidUser = uuids.some((userUuid) => checkUuidInApi || slicedBufferString === userUuid.trim()); - - console.log(`checkUuidInApi: ${await checkUuidInApiResponse(slicedBufferString)}, userID: ${slicedBufferString}`); - - if (!isValidUser) { - return { - hasError: true, - message: "invalid user", - }; - } - - const optLength = new Uint8Array(vlessBuffer.slice(17, 18))[0]; - //skip opt for now - - const command = new Uint8Array(vlessBuffer.slice(18 + optLength, 18 + optLength + 1))[0]; - - // 0x01 TCP - // 0x02 UDP - // 0x03 MUX - if (command === 1) { - } else if (command === 2) { - isUDP = true; - } else { - return { - hasError: true, - message: `command ${command} is not support, command 01-tcp,02-udp,03-mux`, - }; - } - const portIndex = 18 + optLength + 1; - const portBuffer = vlessBuffer.slice(portIndex, portIndex + 2); - // port is big-Endian in raw data etc 80 == 0x005d - const portRemote = new DataView(portBuffer).getUint16(0); - - let addressIndex = portIndex + 2; - const addressBuffer = new Uint8Array(vlessBuffer.slice(addressIndex, addressIndex + 1)); - - // 1--> ipv4 addressLength =4 - // 2--> domain name addressLength=addressBuffer[1] - // 3--> ipv6 addressLength =16 - const addressType = addressBuffer[0]; - let addressLength = 0; - let addressValueIndex = addressIndex + 1; - let addressValue = ""; - switch (addressType) { - case 1: - addressLength = 4; - addressValue = new Uint8Array(vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength)).join("."); - break; - case 2: - addressLength = new Uint8Array(vlessBuffer.slice(addressValueIndex, addressValueIndex + 1))[0]; - addressValueIndex += 1; - addressValue = new TextDecoder().decode(vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength)); - break; - case 3: - addressLength = 16; - const dataView = new DataView(vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength)); - // 2001:0db8:85a3:0000:0000:8a2e:0370:7334 - const ipv6 = []; - for (let i = 0; i < 8; i++) { - ipv6.push(dataView.getUint16(i * 2).toString(16)); - } - addressValue = ipv6.join(":"); - // seems no need add [] for ipv6 - break; - default: - return { - hasError: true, - message: `invild addressType is ${addressType}`, - }; - } - if (!addressValue) { - return { - hasError: true, - message: `addressValue is empty, addressType is ${addressType}`, - }; - } - - return { - hasError: false, - addressRemote: addressValue, - addressType, - portRemote, - rawDataIndex: addressValueIndex + addressLength, - vlessVersion: version, - isUDP, - }; -} - -/** - * Converts a remote socket to a WebSocket connection. - * @param {import("@cloudflare/workers-types").Socket} remoteSocket The remote socket to convert. - * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to connect to. - * @param {ArrayBuffer | null} vlessResponseHeader The VLESS response header. - * @param {(() => Promise) | null} retry The function to retry the connection if it fails. - * @param {(info: string) => void} log The logging function. - * @returns {Promise} A Promise that resolves when the conversion is complete. - */ -async function vlessRemoteSocketToWS(remoteSocket, webSocket, vlessResponseHeader, retry, log) { - // remote--> ws - let remoteChunkCount = 0; - let chunks = []; - /** @type {ArrayBuffer | null} */ - let vlessHeader = vlessResponseHeader; - let hasIncomingData = false; // check if remoteSocket has incoming data - await remoteSocket.readable - .pipeTo( - new WritableStream({ - start() {}, - /** - * - * @param {Uint8Array} chunk - * @param {*} controller - */ - async write(chunk, controller) { - hasIncomingData = true; - // remoteChunkCount++; - if (webSocket.readyState !== WS_READY_STATE_OPEN) { - controller.error("webSocket.readyState is not open, maybe close"); - } - if (vlessHeader) { - webSocket.send(await new Blob([vlessHeader, chunk]).arrayBuffer()); - vlessHeader = null; - } else { - // seems no need rate limit this, CF seems fix this??.. - // if (remoteChunkCount > 20000) { - // // cf one package is 4096 byte(4kb), 4096 * 20000 = 80M - // await delay(1); - // } - webSocket.send(chunk); - } - }, - close() { - log(`remoteConnection!.readable is close with hasIncomingData is ${hasIncomingData}`); - // safeCloseWebSocket(webSocket); // no need server close websocket frist for some case will casue HTTP ERR_CONTENT_LENGTH_MISMATCH issue, client will send close event anyway. - }, - abort(reason) { - console.error(`remoteConnection!.readable abort`, reason); - }, - }) - ) - .catch((error) => { - console.error(`vlessRemoteSocketToWS has exception `, error.stack || error); - safeCloseWebSocket(webSocket); - }); - - // seems is cf connect socket have error, - // 1. Socket.closed will have error - // 2. Socket.readable will be close without any data coming - if (hasIncomingData === false && retry) { - log(`retry`); - retry(); - } -} - -async function trojanRemoteSocketToWS(remoteSocket, webSocket, retry, log) { - let hasIncomingData = false; - await remoteSocket.readable - .pipeTo( - new WritableStream({ - start() {}, - /** - * - * @param {Uint8Array} chunk - * @param {*} controller - */ - async write(chunk, controller) { - hasIncomingData = true; - if (webSocket.readyState !== WS_READY_STATE_OPEN) { - controller.error("webSocket connection is not open"); - } - webSocket.send(chunk); - }, - close() { - log(`remoteSocket.readable is closed, hasIncomingData: ${hasIncomingData}`); - }, - abort(reason) { - console.error("remoteSocket.readable abort", reason); - }, - }) - ) - .catch((error) => { - console.error(`trojanRemoteSocketToWS error:`, error.stack || error); - safeCloseWebSocket(webSocket); - }); - - if (hasIncomingData === false && retry) { - log(`retry`); - retry(); - } -} - -/** - * Decodes a base64 string into an ArrayBuffer. - * @param {string} base64Str The base64 string to decode. - * @returns {{earlyData: ArrayBuffer|null, error: Error|null}} An object containing the decoded ArrayBuffer or null if there was an error, and any error that occurred during decoding or null if there was no error. - */ -function base64ToArrayBuffer(base64Str) { - if (!base64Str) { - return { earlyData: null, error: null }; - } - try { - // go use modified Base64 for URL rfc4648 which js atob not support - base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/'); - const decode = atob(base64Str); - const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0)); - return { earlyData: arryBuffer.buffer, error: null }; - } catch (error) { - return { earlyData: null, error }; - } -} - -/** - * 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; - 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. - */ -function safeCloseWebSocket(socket) { - try { - if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) { - socket.close(); - } - } catch (error) { - console.error('safeCloseWebSocket error', error); - } -} - -const byteToHex = []; - -for (let i = 0; i < 256; ++i) { - byteToHex.push((i + 256).toString(16).slice(1)); -} - -function unsafeStringify(arr, offset = 0) { - return ( - byteToHex[arr[offset + 0]] + - byteToHex[arr[offset + 1]] + - byteToHex[arr[offset + 2]] + - byteToHex[arr[offset + 3]] + - "-" + - byteToHex[arr[offset + 4]] + - byteToHex[arr[offset + 5]] + - "-" + - byteToHex[arr[offset + 6]] + - byteToHex[arr[offset + 7]] + - "-" + - byteToHex[arr[offset + 8]] + - byteToHex[arr[offset + 9]] + - "-" + - byteToHex[arr[offset + 10]] + - byteToHex[arr[offset + 11]] + - byteToHex[arr[offset + 12]] + - byteToHex[arr[offset + 13]] + - byteToHex[arr[offset + 14]] + - byteToHex[arr[offset + 15]] - ).toLowerCase(); -} - -function stringify(arr, offset = 0) { - const uuid = unsafeStringify(arr, offset); - if (!isValidUUID(uuid)) { - throw TypeError("Stringified UUID is invalid"); - } - return uuid; -} - -/** - * Handles outbound UDP traffic by transforming the data into DNS queries and sending them over a WebSocket connection. - * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket connection to send the DNS queries over. - * @param {ArrayBuffer} vlessResponseHeader The VLESS response header. - * @param {(string) => void} log The logging function. - * @returns {{write: (chunk: Uint8Array) => void}} An object with a write method that accepts a Uint8Array chunk to write to the transform stream. - */ -async function handleUDPOutBound(webSocket, vlessResponseHeader, log) { - let isVlessHeaderSent = false; - const transformStream = new TransformStream({ - start(controller) {}, - transform(chunk, controller) { - // udp message 2 byte is the the length of udp data - // TODO: this should have bug, beacsue maybe udp chunk can be in two websocket message - for (let index = 0; index < chunk.byteLength; ) { - const lengthBuffer = chunk.slice(index, index + 2); - const udpPakcetLength = new DataView(lengthBuffer).getUint16(0); - const udpData = new Uint8Array(chunk.slice(index + 2, index + 2 + udpPakcetLength)); - index = index + 2 + udpPakcetLength; - controller.enqueue(udpData); - } - }, - flush(controller) {}, - }); - - // only handle dns udp for now - transformStream.readable - .pipeTo( - new WritableStream({ - async write(chunk) { - const resp = await fetch( - dohURL, // dns server url - { - method: "POST", - headers: { - "content-type": "application/dns-message", - }, - body: chunk, - } - ); - const dnsQueryResult = await resp.arrayBuffer(); - const udpSize = dnsQueryResult.byteLength; - // console.log([...new Uint8Array(dnsQueryResult)].map((x) => x.toString(16))); - const udpSizeBuffer = new Uint8Array([(udpSize >> 8) & 0xff, udpSize & 0xff]); - if (webSocket.readyState === WS_READY_STATE_OPEN) { - log(`doh success and dns message length is ${udpSize}`); - if (isVlessHeaderSent) { - webSocket.send(await new Blob([udpSizeBuffer, dnsQueryResult]).arrayBuffer()); - } else { - webSocket.send(await new Blob([vlessResponseHeader, udpSizeBuffer, dnsQueryResult]).arrayBuffer()); - isVlessHeaderSent = true; - } - } - }, - }) - ) - .catch((error) => { - log("dns udp has error" + error); - }); - - const writer = transformStream.writable.getWriter(); - - return { - /** - * - * @param {Uint8Array} chunk - */ - write(chunk) { - writer.write(chunk); - }, - }; -} - -/** - * - * @param {string} userID - * @param {string | null} hostName - * @returns {string} - */ - -const generateKeyPair = () => { - const base64Encode = (array) => btoa(String.fromCharCode.apply(null, array)); - let privateKey = nacl.randomBytes(32); - privateKey[0] &= 248; - privateKey[31] &= 127; - privateKey[31] |= 64; - let publicKey = nacl.scalarMult.base(privateKey); - const publicKeyBase64 = base64Encode(publicKey); - const privateKeyBase64 = base64Encode(privateKey); - - return { publicKey: publicKeyBase64, privateKey: privateKeyBase64 }; -}; - -function generateRemark(index, port, address, cleanIPs, protocol, configType) { - let remark = ''; - let addressType; - const type = configType ? ` ${configType}` : ''; - - cleanIPs.includes(address) - ? addressType = 'Clean IP' - : addressType = isDomain(address) ? 'Domain': isIPv4(address) ? 'IPv4' : isIPv6(address) ? 'IPv6' : ''; - - return `💦 ${index} - ${protocol}${type} - ${addressType} : ${port}`; -} - -function isDomain(address) { - const domainPattern = /^(?!\-)(?:[A-Za-z0-9\-]{1,63}\.)+[A-Za-z]{2,}$/; - return domainPattern.test(address); -} - -function isIPv4(address) { - const ipv4Pattern = /^(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; - return ipv4Pattern.test(address); -} - -function isIPv6(address) { - const ipv6Pattern = /^\[(?:(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}|(?:[a-fA-F0-9]{1,4}:){1,7}:|::(?:[a-fA-F0-9]{1,4}:){0,7}|(?:[a-fA-F0-9]{1,4}:){1,6}:[a-fA-F0-9]{1,4}|(?:[a-fA-F0-9]{1,4}:){1,5}(?::[a-fA-F0-9]{1,4}){1,2}|(?:[a-fA-F0-9]{1,4}:){1,4}(?::[a-fA-F0-9]{1,4}){1,3}|(?:[a-fA-F0-9]{1,4}:){1,3}(?::[a-fA-F0-9]{1,4}){1,4}|(?:[a-fA-F0-9]{1,4}:){1,2}(?::[a-fA-F0-9]{1,4}){1,5}|[a-fA-F0-9]{1,4}:(?::[a-fA-F0-9]{1,4}){1,6})\]$/; - return ipv6Pattern.test(address); -} - -function base64ToDecimal (base64) { - const binaryString = atob(base64); - const hexString = Array.from(binaryString).map(char => char.charCodeAt(0).toString(16).padStart(2, '0')).join(''); - const decimalArray = hexString.match(/.{2}/g).map(hex => parseInt(hex, 16)); - return decimalArray; -} - -async function getDataset(env) { - let proxySettings, warpConfigs; - if (typeof env.bpb !== 'object') { - return {kvNotFound: true, proxySettings: null, warpConfigs: null} - } - - try { - proxySettings = await env.bpb.get("proxySettings", {type: 'json'}); - warpConfigs = await env.bpb.get('warpConfigs', {type: 'json'}); - } catch (error) { - console.log(error); - throw new Error(`An error occurred while getting KV - ${error}`); - } - - if (!proxySettings) { - proxySettings = await updateDataset(env); - const { error, configs } = await fetchWgConfig(env, proxySettings); - if (error) throw new Error(`An error occurred while getting Warp configs - ${error}`); - warpConfigs = configs; - } - - if (panelVersion !== proxySettings.panelVersion) proxySettings = await updateDataset(env); - return {kvNotFound: false, proxySettings, warpConfigs} -} - -async function updateDataset (env, newSettings, resetSettings) { - let currentSettings; - if (!resetSettings) { - try { - currentSettings = await env.bpb.get("proxySettings", {type: 'json'}); - } catch (error) { - console.log(error); - throw new Error(`An error occurred while getting current KV settings - ${error}`); - } - } else { - await env.bpb.delete('warpConfigs'); - } - - const validateField = (field) => { - const fieldValue = newSettings?.get(field); - if (fieldValue === undefined) return null; - if (fieldValue === 'true') return true; - if (fieldValue === 'false') return false; - return fieldValue; - } - - const remoteDNS = validateField('remoteDNS') ?? currentSettings?.remoteDNS ?? 'https://8.8.8.8/dns-query'; - const enableIPv6 = validateField('enableIPv6') ?? currentSettings?.enableIPv6 ?? true; - const url = new URL(remoteDNS); - const remoteDNSServer = url.hostname; - const isServerDomain = isDomain(remoteDNSServer); - let resolvedRemoteDNS = {}; - if (isServerDomain) { - try { - const resolvedDomain = await resolveDNS(remoteDNSServer); - resolvedRemoteDNS = { - server: remoteDNSServer, - staticIPs: enableIPv6 ? [...resolvedDomain.ipv4, ...resolvedDomain.ipv6] : resolvedDomain.ipv4 - }; - } catch (error) { - console.log(error); - throw new Error(`An error occurred while resolving remote DNS server, please try agian! - ${error}`); - } - } - - const proxySettings = { - remoteDNS: remoteDNS, - resolvedRemoteDNS: resolvedRemoteDNS, - localDNS: validateField('localDNS') ?? currentSettings?.localDNS ?? '8.8.8.8', - vlessTrojanFakeDNS: validateField('vlessTrojanFakeDNS') ?? currentSettings?.vlessTrojanFakeDNS ?? false, - proxyIP: validateField('proxyIP')?.trim() ?? currentSettings?.proxyIP ?? '', - outProxy: validateField('outProxy') ?? currentSettings?.outProxy ?? '', - outProxyParams: extractChainProxyParams(validateField('outProxy')) ?? currentSettings?.outProxyParams ?? {}, - cleanIPs: validateField('cleanIPs')?.replaceAll(' ', '') ?? currentSettings?.cleanIPs ?? '', - enableIPv6: enableIPv6, - customCdnAddrs: validateField('customCdnAddrs')?.replaceAll(' ', '') ?? currentSettings?.customCdnAddrs ?? '', - customCdnHost: validateField('customCdnHost')?.trim() ?? currentSettings?.customCdnHost ?? '', - customCdnSni: validateField('customCdnSni')?.trim() ?? currentSettings?.customCdnSni ?? '', - bestVLESSTrojanInterval: validateField('bestVLESSTrojanInterval') ?? currentSettings?.bestVLESSTrojanInterval ?? '30', - vlessConfigs: validateField('vlessConfigs') ?? currentSettings?.vlessConfigs ?? true, - trojanConfigs: validateField('trojanConfigs') ?? currentSettings?.trojanConfigs ?? false, - ports: validateField('ports')?.split(',') ?? currentSettings?.ports ?? ['443'], - lengthMin: validateField('fragmentLengthMin') ?? currentSettings?.lengthMin ?? '100', - lengthMax: validateField('fragmentLengthMax') ?? currentSettings?.lengthMax ?? '200', - intervalMin: validateField('fragmentIntervalMin') ?? currentSettings?.intervalMin ?? '1', - intervalMax: validateField('fragmentIntervalMax') ?? currentSettings?.intervalMax ?? '1', - fragmentPackets: validateField('fragmentPackets') ?? currentSettings?.fragmentPackets ?? 'tlshello', - bypassLAN: validateField('bypass-lan') ?? currentSettings?.bypassLAN ?? false, - bypassIran: validateField('bypass-iran') ?? currentSettings?.bypassIran ?? false, - bypassChina: validateField('bypass-china') ?? currentSettings?.bypassChina ?? false, - bypassRussia: validateField('bypass-russia') ?? currentSettings?.bypassRussia ?? false, - blockAds: validateField('block-ads') ?? currentSettings?.blockAds ?? false, - blockPorn: validateField('block-porn') ?? currentSettings?.blockPorn ?? false, - blockUDP443: validateField('block-udp-443') ?? currentSettings?.blockUDP443 ?? false, - warpEndpoints: validateField('warpEndpoints')?.replaceAll(' ', '') ?? currentSettings?.warpEndpoints ?? 'engage.cloudflareclient.com:2408', - warpFakeDNS: validateField('warpFakeDNS') ?? currentSettings?.warpFakeDNS ?? false, - warpEnableIPv6: validateField('warpEnableIPv6') ?? currentSettings?.warpEnableIPv6 ?? true, - warpPlusLicense: validateField('warpPlusLicense') ?? currentSettings?.warpPlusLicense ?? '', - bestWarpInterval: validateField('bestWarpInterval') ?? currentSettings?.bestWarpInterval ?? '30', - hiddifyNoiseMode: validateField('hiddifyNoiseMode') ?? currentSettings?.hiddifyNoiseMode ?? 'm4', - nikaNGNoiseMode: validateField('nikaNGNoiseMode') ?? currentSettings?.nikaNGNoiseMode ?? 'quic', - noiseCountMin: validateField('noiseCountMin') ?? currentSettings?.noiseCountMin ?? '10', - noiseCountMax: validateField('noiseCountMax') ?? currentSettings?.noiseCountMax ?? '15', - noiseSizeMin: validateField('noiseSizeMin') ?? currentSettings?.noiseSizeMin ?? '5', - noiseSizeMax: validateField('noiseSizeMax') ?? currentSettings?.noiseSizeMax ?? '10', - noiseDelayMin: validateField('noiseDelayMin') ?? currentSettings?.noiseDelayMin ?? '1', - noiseDelayMax: validateField('noiseDelayMax') ?? currentSettings?.noiseDelayMax ?? '1', - panelVersion: panelVersion - }; - - try { - await env.bpb.put("proxySettings", JSON.stringify(proxySettings)); - } catch (error) { - console.log(error); - throw new Error(`An error occurred while updating KV - ${error}`); - } - - return proxySettings; -} - -function randomUpperCase (str) { - let result = ''; - for (let i = 0; i < str.length; i++) { - result += Math.random() < 0.5 ? str[i].toUpperCase() : str[i]; - } - return result; -} - -function getRandomPath (length) { - let result = ''; - const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - const charactersLength = characters.length; - for (let i = 0; i < length; i++) { - result += characters.charAt(Math.floor(Math.random() * charactersLength)); - } - return result; -} - -async function resolveDNS (domain) { - const dohURLv4 = `${dohURL}?name=${encodeURIComponent(domain)}&type=A`; - const dohURLv6 = `${dohURL}?name=${encodeURIComponent(domain)}&type=AAAA`; - - try { - const [ipv4Response, ipv6Response] = await Promise.all([ - fetch(dohURLv4, { headers: { accept: 'application/dns-json' } }), - fetch(dohURLv6, { headers: { accept: 'application/dns-json' } }) - ]); - - const ipv4Addresses = await ipv4Response.json(); - const ipv6Addresses = await ipv6Response.json(); - - const ipv4 = ipv4Addresses.Answer - ? ipv4Addresses.Answer.map((record) => record.data) - : []; - const ipv6 = ipv6Addresses.Answer - ? ipv6Addresses.Answer.map((record) => record.data) - : []; - - return { ipv4, ipv6 }; - } catch (error) { - console.error('Error resolving DNS:', error); - throw new Error(`An error occurred while resolving DNS - ${error}`); - } -} - -async function getConfigAddresses(hostName, cleanIPs, enableIPv6) { - const resolved = await resolveDNS(hostName); - const defaultIPv6 = enableIPv6 ? resolved.ipv6.map((ip) => `[${ip}]`) : [] - return [ - hostName, - 'www.speedtest.net', - ...resolved.ipv4, - ...defaultIPv6, - ...(cleanIPs ? cleanIPs.split(',') : []) - ]; -} - -async function generateJWTToken (secretKey) { - const secret = new TextEncoder().encode(secretKey); - return await new SignJWT({ userID }) - .setProtectedHeader({ alg: 'HS256' }) - .setIssuedAt() - .setExpirationTime('24h') - .sign(secret); -} - -function generateSecretKey () { - const key = nacl.randomBytes(32); - return Array.from(key, byte => byte.toString(16).padStart(2, '0')).join(''); -} - -async function Authenticate (request, env) { - try { - const secretKey = await env.bpb.get('secretKey'); - const secret = new TextEncoder().encode(secretKey); - const cookie = request.headers.get('Cookie')?.match(/(^|;\s*)jwtToken=([^;]*)/); - const token = cookie ? cookie[2] : null; - - if (!token) { - console.log('Unauthorized: Token not available!'); - return false; - } - - const { payload } = await jwtVerify(token, secret); - console.log(`Successfully authenticated, User ID: ${payload.userID}`); - return true; - } catch (error) { - console.log(error); - return false; - } -} - -function renderHomePage (request, proxySettings, hostName, isPassSet) { - const { - remoteDNS, - localDNS, - vlessTrojanFakeDNS, - proxyIP, - outProxy, - cleanIPs, - enableIPv6, - customCdnAddrs, - customCdnHost, - customCdnSni, - bestVLESSTrojanInterval, - vlessConfigs, - trojanConfigs, - ports, - lengthMin, - lengthMax, - intervalMin, - intervalMax, - fragmentPackets, - warpEndpoints, - warpFakeDNS, - warpEnableIPv6, - warpPlusLicense, - bestWarpInterval, - hiddifyNoiseMode, - nikaNGNoiseMode, - noiseCountMin, - noiseCountMax, - noiseSizeMin, - noiseSizeMax, - noiseDelayMin, - noiseDelayMax, - bypassLAN, - bypassIran, - bypassChina, - bypassRussia, - blockAds, - blockPorn, - blockUDP443 - } = proxySettings; - - const isWarpPlus = warpPlusLicense ? true : false; - let activeProtocols = (vlessConfigs ? 1 : 0) + (trojanConfigs ? 1 : 0); - let httpPortsBlock = '', httpsPortsBlock = ''; - const allPorts = [...(hostName.includes('workers.dev') ? defaultHttpPorts : []), ...defaultHttpsPorts]; - let regionNames = new Intl.DisplayNames(['en'], {type: 'region'}); - const cfCountry = regionNames.of(request.cf.country); - - allPorts.forEach(port => { - const id = `port-${port}`; - const isChecked = ports.includes(port) ? 'checked' : ''; - const portBlock = ` -
- - -
`; - defaultHttpsPorts.includes(port) ? httpsPortsBlock += portBlock : httpPortsBlock += portBlock; - }); - - return ` - - - - - - BPB Panel ${panelVersion} - - - Collapsible Sections - - - -

BPB Panel ${panelVersion} 💦

-
-
-
-

VLESS / TROJAN ⚙️

-
- - -
-
- - -
-
- -
- -
-
-
- - -
-
- - -
-
- - -
-
- - - - -
-
- -
- -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
-
- - -
-
- - -
-
-
-
- - - - - - - - - - ${!httpPortsBlock ? '' : ` - - - `} -
Config typePorts
TLS -
${httpsPortsBlock}
-
Non TLS -
${httpPortsBlock}
-
-
-
-
-

FRAGMENT ⚙️

-
- -
- - - - -
-
-
- -
- - - - -
-
-
- -
- -
-
-
-
-

WARP GENERAL ⚙️

-
- - -
-
- - -
-
- -
- -
-
-
- -
- -
-
-
- - -
-
- - -
-
- - -
-
-
-

WARP PRO ⚙️

-
- - -
-
- - -
-
- -
- - - - -
-
-
- -
- - - - -
-
-
- -
- - - - -
-
-
-
-

ROUTING RULES ⚙️

-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
-
- - -
-
-
-
-

NORMAL SUB 🔗

-
- - - - - - - - - - - - - -
ApplicationSubscription
-
- verified - v2rayNG -
-
- verified - NikaNG -
-
- verified - MahsaNG -
-
- verified - v2rayN -
-
- verified - v2rayN-PRO -
-
- verified - Shadowrocket -
-
- verified - Streisand -
-
- verified - Hiddify -
-
- verified - Nekoray (Xray) -
-
- - -
-
- verified - Nekobox -
-
- verified - Nekoray (Sing-Box) -
-
- verified - Karing -
-
- -
-
-

FULL NORMAL SUB 🔗

-
- - - - - - - - - - - - - - - - - -
ApplicationSubscription
-
- verified - v2rayNG -
-
- verified - NikaNG -
-
- verified - MahsaNG -
-
- verified - v2rayN -
-
- verified - v2rayN-PRO -
-
- verified - Streisand -
-
- - -
-
- verified - Sing-box -
-
- - -
-
- verified - Clash Meta -
-
- verified - Clash Verge -
-
- verified - v2rayN -
-
- verified - FlClash -
-
- verified - Stash -
-
- - -
-
-

FRAGMENT SUB ⛓️

-
- - - - - - - - - - - - - -
ApplicationSubscription
-
- verified - v2rayNG -
-
- verified - NikaNG -
-
- verified - MahsaNG -
-
- verified - v2rayN -
-
- verified - v2rayN-PRO -
-
- verified - Streisand -
-
- - -
-
- verified - Hiddify -
-
- - -
-
-

WARP SUB 🔗

-
- - - - - - - - - - - - - - - - - -
ApplicationSubscription
-
- verified - v2rayNG -
-
- verified - v2rayN -
-
- verified - Streisand -
-
- - -
-
- verified - Hiddify -
-
- verified - Singbox -
-
- - -
-
- verified - Clash Meta -
-
- verified - Clash Verge -
-
- verified - v2rayN -
-
- verified - FlClash -
-
- verified - Stash -
-
- - -
-
-

WARP PRO SUB 🔗

-
- - - - - - - - - - - - - -
ApplicationSubscription
-
- verified - NikaNG -
-
- verified - MahsaNG -
-
- verified - v2rayN-PRO -
-
- - -
-
- verified - Hiddify -
-
- - -
-
- -
- -
-
-

YOUR IP 💡

-
- - - - - - - - - - - - - - - - - - - - - - -
TargetYour IPCountryCityISP
Cloudflare CDN${request.headers.get('cf-connecting-ip') || '-'}${cfCountry || '-'}${request.cf.city || '-'}${request.cf.asOrganization.toUpperCase() || '-'}
Others
-
-
- -
- - - - - `; -} - -function renderLoginPage () { - return ` - - - - - - User Login - - - -
-

BPB Panel ${panelVersion} 💦

-
-

User Login

-
-
- - -
-
- -
-
-
- - - `; -} - -function renderErrorPage (message, error, refer) { - return ` - - - - - - Error Page - - - -
-

BPB Panel ${panelVersion} 💦

-
-

${message} ${refer - ? 'Please try again or refer to documents' - : ''} -

-

${error ? `⚠️ ${error.stack.toString()}` : ''}

-
-
- - - `; -} - -function extractChainProxyParams(chainProxy) { - let configParams = {}; - if (!chainProxy) return {}; - let url = new URL(chainProxy); - const protocol = url.protocol.slice(0, -1); - if (protocol === 'vless') { - const params = new URLSearchParams(url.search); - configParams = { - protocol: protocol, - uuid : url.username, - hostName : url.hostname, - port : url.port - }; - - params.forEach( (value, key) => { - configParams[key] = value; - }); - } else { - configParams = { - protocol: protocol, - user : url.username, - pass : url.password, - host : url.host, - port : url.port - }; - } - - return JSON.stringify(configParams); -} - -async function fetchWgConfig (env, proxySettings) { - let warpConfigs = []; - const apiBaseUrl = 'https://api.cloudflareclient.com/v0a4005/reg'; - - const { warpPlusLicense } = proxySettings; - const warpKeys = [generateKeyPair(), generateKeyPair()]; - - for(let i = 0; i < 2; i++) { - const accountResponse = await fetch(apiBaseUrl, { - method: 'POST', - headers: { - 'User-Agent': 'insomnia/8.6.1', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - key: warpKeys[i].publicKey, - install_id: "", - fcm_token: "", - tos: new Date().toISOString(), - type: "Android", - model: 'PC', - locale: 'en_US', - warp_enabled: true - }) - }); - - const accountData = await accountResponse.json(); - warpConfigs.push ({ - privateKey: warpKeys[i].privateKey, - account: accountData - }); - - if (warpPlusLicense) { - const response = await fetch(`${apiBaseUrl}/${accountData.id}/account`, { - method: 'PUT', - headers: { - 'User-Agent': 'insomnia/8.6.1', - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${accountData.token}` - }, - body: JSON.stringify({ - key: warpKeys[i].publicKey, - install_id: "", - fcm_token: "", - tos: new Date().toISOString(), - type: "Android", - model: 'PC', - locale: 'en_US', - warp_enabled: true, - license: warpPlusLicense - }) - }); - - const responseData = await response.json(); - if(response.status !== 200 && !responseData.success) return { error: responseData.errors[0]?.message, configs: null} - } - } - - const configs = JSON.stringify(warpConfigs) - await env.bpb.put('warpConfigs', configs); - return { error: null, configs }; -} - -function extractWireguardParams(warpConfigs, isWoW) { - const index = isWoW ? 1 : 0; - const warpConfig = warpConfigs[index].account.config; - return { - warpIPv6: `${warpConfig.interface.addresses.v6}/128`, - reserved: warpConfig.client_id, - publicKey: warpConfig.peers[0].public_key, - privateKey: warpConfigs[index].privateKey, - }; -} - -async function buildXrayDNS (proxySettings, outboundAddrs, domainToStaticIPs, isWorkerLess, isBalancer, isWarp) { - const { - remoteDNS, - resolvedRemoteDNS, - localDNS, - vlessTrojanFakeDNS, - enableIPv6, - warpFakeDNS, - warpEnableIPv6, - blockAds, - bypassIran, - bypassChina, - blockPorn, - bypassRussia - } = proxySettings; - - const isBypass = bypassIran || bypassChina || bypassRussia; - const isBlock = blockAds || blockPorn; - const bypassRules = [ - { rule: bypassIran, domain: "geosite:category-ir", ip: "geoip:ir" }, - { rule: bypassChina, domain: "geosite:cn", ip: "geoip:cn" }, - { rule: bypassRussia, domain: "geosite:category-ru", ip: "geoip:ru" } - ]; - - const blockRules = [ - { rule: blockAds, host: "geosite:category-ads-all", address: ["127.0.0.1"] }, - { rule: blockAds, host: "geosite:category-ads-ir", address: ["127.0.0.1"] }, - { rule: blockPorn, host: "geosite:category-porn", address: ["127.0.0.1"] } - ]; - - const isFakeDNS = (vlessTrojanFakeDNS && !isWarp) || (warpFakeDNS && isWarp); - const isIPv6 = (enableIPv6 && !isWarp) || (warpEnableIPv6 && isWarp); - const outboundDomains = outboundAddrs.filter(address => isDomain(address)); - const isOutboundRule = outboundDomains.length > 0; - const outboundRules = outboundDomains.map(domain => `full:${domain}`); - isBalancer && outboundRules.push("full:www.gstatic.com"); - const finalRemoteDNS = isWorkerLess - ? ["https://cloudflare-dns.com/dns-query"] - : isWarp - ? warpEnableIPv6 - ? ["1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001"] - : ["1.1.1.1", "1.0.0.1"] - : [remoteDNS]; - - const dnsHost = {}; - isBlock && blockRules.forEach( ({ rule, host, address}) => { - if (rule) dnsHost[host] = address; - }); - - const staticIPs = domainToStaticIPs ? await resolveDNS(domainToStaticIPs) : undefined; - if (staticIPs) dnsHost[domainToStaticIPs] = enableIPv6 ? [...staticIPs.ipv4, ...staticIPs.ipv6] : staticIPs.ipv4; - if (resolvedRemoteDNS.server && !isWorkerLess && !isWarp) dnsHost[resolvedRemoteDNS.server] = resolvedRemoteDNS.staticIPs; - if (isWorkerLess) { - const domains = ["cloudflare-dns.com", "cloudflare.com", "dash.cloudflare.com"]; - const resolved = await Promise.all(domains.map(resolveDNS)); - const hostIPv4 = resolved.flatMap(r => r.ipv4); - const hostIPv6 = enableIPv6 ? resolved.flatMap(r => r.ipv6) : []; - dnsHost["cloudflare-dns.com"] = [ - ...hostIPv4, - ...hostIPv6 - ]; - } - - const hosts = Object.keys(dnsHost).length ? { hosts: dnsHost } : {}; - let dnsObject = { - ...hosts, - servers: finalRemoteDNS, - queryStrategy: isIPv6 ? "UseIP" : "UseIPv4", - tag: "dns", - }; - - isOutboundRule && dnsObject.servers.push({ - address: localDNS, - domains: outboundRules, - skipFallback: true - }); - - let localDNSServer = { - address: localDNS, - domains: [], - expectIPs: [], - skipFallback: true - }; - - if (!isWorkerLess && isBypass) { - bypassRules.forEach(({ rule, domain, ip }) => { - if (rule) { - localDNSServer.domains.push(domain); - localDNSServer.expectIPs.push(ip); - } - }); - - dnsObject.servers.push(localDNSServer); - } - - if (isFakeDNS) { - const fakeDNSServer = isBypass && !isWorkerLess - ? { address: "fakedns", domains: localDNSServer.domains } - : "fakedns"; - dnsObject.servers.unshift(fakeDNSServer); - } - - return dnsObject; -} - -function buildXrayRoutingRules (proxySettings, outboundAddrs, isChain, isBalancer, isWorkerLess) { - const { - localDNS, - bypassLAN, - bypassIran, - bypassChina, - bypassRussia, - blockAds, - blockPorn, - blockUDP443 - } = proxySettings; - - const isBlock = blockAds || blockPorn; - const isBypass = bypassIran || bypassChina || bypassRussia; - const geoRules = [ - { rule: bypassLAN, type: 'direct', domain: "geosite:private", ip: "geoip:private" }, - { rule: bypassIran, type: 'direct', domain: "geosite:category-ir", ip: "geoip:ir" }, - { rule: bypassChina, type: 'direct', domain: "geosite:cn", ip: "geoip:cn" }, - { rule: blockAds, type: 'block', domain: "geosite:category-ads-all" }, - { rule: blockAds, type: 'block', domain: "geosite:category-ads-ir" }, - { rule: blockPorn, type: 'block', domain: "geosite:category-porn" } - ]; - const outboundDomains = outboundAddrs.filter(address => isDomain(address)); - const isOutboundRule = outboundDomains.length > 0; - let rules = [ - { - inboundTag: [ - "dns-in" - ], - outboundTag: "dns-out", - type: "field" - }, - { - inboundTag: [ - "socks-in", - "http-in" - ], - port: "53", - outboundTag: "dns-out", - type: "field" - } - ]; - - if (!isWorkerLess && (isOutboundRule || isBypass)) rules.push({ - ip: [localDNS], - port: "53", - outboundTag: "direct", - type: "field" - }); - - if (isBypass || isBlock) { - const createRule = (type, outbound) => ({ - [type]: [], - outboundTag: outbound, - type: "field" - }); - - let geositeDirectRule, geoipDirectRule; - if (!isWorkerLess) { - geositeDirectRule = createRule("domain", "direct"); - geoipDirectRule = createRule("ip", "direct"); - } - - let geositeBlockRule = createRule("domain", "block"); - geoRules.forEach(({ rule, type, domain, ip }) => { - if (rule) { - if (type === 'direct') { - geositeDirectRule?.domain.push(domain); - geoipDirectRule?.ip?.push(ip); - } else { - geositeBlockRule.domain.push(domain); - } - } - }); - - !isWorkerLess && isBypass && rules.push(geositeDirectRule, geoipDirectRule); - isBlock && rules.push(geositeBlockRule); - } - - blockUDP443 && rules.push({ - network: "udp", - port: "443", - outboundTag: "block", - type: "field", - }); - - if (isBalancer) { - rules.push({ - network: "tcp,udp", - balancerTag: "all", - type: "field" - }); - } else { - rules.push({ - network: "tcp,udp", - outboundTag: isChain ? "chain" : isWorkerLess ? "fragment" : "proxy", - type: "field" - }); - } - - return rules; -} - -function buildXrayVLESSOutbound (tag, address, port, host, sni, proxyIP, isFragment, allowInsecure) { - let outbound = { - protocol: "vless", - settings: { - vnext: [ - { - address: address, - port: +port, - users: [ - { - id: userID, - encryption: "none", - level: 8 - } - ] - } - ] - }, - streamSettings: { - network: "ws", - security: "none", - sockopt: {}, - wsSettings: { - headers: { - Host: host, - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36" - }, - path: `/${getRandomPath(16)}${proxyIP ? `/${btoa(proxyIP)}` : ''}?ed=2560` - } - }, - tag: tag - }; - - if (defaultHttpsPorts.includes(port)) { - outbound.streamSettings.security = "tls"; - outbound.streamSettings.tlsSettings = { - allowInsecure: allowInsecure, - fingerprint: "randomized", - alpn: ["h2", "http/1.1"], - serverName: sni - }; - } - - if (isFragment) { - outbound.streamSettings.sockopt.dialerProxy = "fragment"; - } else { - outbound.streamSettings.sockopt.tcpKeepAliveIdle = 100; - outbound.streamSettings.sockopt.tcpNoDelay = true; - } - - return outbound; -} - -function buildXrayTrojanOutbound (tag, address, port, host, sni, proxyIP, isFragment, allowInsecure) { - let outbound = { - protocol: "trojan", - settings: { - servers: [ - { - address: address, - port: +port, - password: trojanPassword, - level: 8 - } - ] - }, - streamSettings: { - network: "ws", - security: "none", - sockopt: {}, - wsSettings: { - headers: { - Host: host - }, - path: `/tr${getRandomPath(16)}${proxyIP ? `/${btoa(proxyIP)}` : ''}?ed=2560` - } - }, - tag: tag - }; - - if (defaultHttpsPorts.includes(port)) { - outbound.streamSettings.security = "tls"; - outbound.streamSettings.tlsSettings = { - allowInsecure: allowInsecure, - fingerprint: "randomized", - alpn: ["h2", "http/1.1"], - serverName: sni - }; - } - - if (isFragment) { - outbound.streamSettings.sockopt.dialerProxy = "fragment"; - } else { - outbound.streamSettings.sockopt.tcpKeepAliveIdle = 100; - outbound.streamSettings.sockopt.tcpNoDelay = true; - } - - return outbound; -} - -function buildXrayWarpOutbound (proxySettings, warpConfigs, endpoint, isChain, client) { - const { - nikaNGNoiseMode, - noiseCountMin, - noiseCountMax, - noiseSizeMin, - noiseSizeMax, - noiseDelayMin, - noiseDelayMax - } = proxySettings; - - const { - warpIPv6, - reserved, - publicKey, - privateKey - } = extractWireguardParams(warpConfigs, isChain); - - let outbound = { - protocol: "wireguard", - settings: { - address: [ - "172.16.0.2/32", - warpIPv6 - ], - mtu: 1280, - peers: [ - { - endpoint: endpoint, - publicKey: publicKey, - keepAlive: 5 - } - ], - reserved: base64ToDecimal(reserved), - secretKey: privateKey - }, - streamSettings: { - sockopt: { - dialerProxy: "proxy", - tcpKeepAliveIdle: 100, - tcpNoDelay: true, - } - }, - tag: isChain ? "chain" : "proxy" - }; - - !isChain && delete outbound.streamSettings; - client === 'nikang' && !isChain && Object.assign(outbound.settings, { - wnoise: nikaNGNoiseMode, - wnoisecount: noiseCountMin === noiseCountMax ? noiseCountMin : `${noiseCountMin}-${noiseCountMax}`, - wpayloadsize: noiseSizeMin === noiseSizeMax ? noiseSizeMin : `${noiseSizeMin}-${noiseSizeMax}`, - wnoisedelay: noiseDelayMin === noiseDelayMax ? noiseDelayMin : `${noiseDelayMin}-${noiseDelayMax}` - }); - - return outbound; -} - -function buildXrayChainOutbound(chainProxyParams) { - if (['socks', 'http'].includes(chainProxyParams.protocol)) { - const { protocol, host, port, user, pass } = chainProxyParams; - return { - protocol: protocol, - settings: { - servers: [ - { - address: host, - port: +port, - users: [ - { - user: user, - pass: pass, - level: 8 - } - ] - } - ] - }, - streamSettings: { - network: "tcp", - sockopt: { - dialerProxy: "proxy", - tcpNoDelay: true - } - }, - mux: { - enabled: true, - concurrency: 8, - xudpConcurrency: 16, - xudpProxyUDP443: "reject" - }, - tag: "chain" - }; - } - - const { - hostName, - port, - uuid, - flow, - security, - type, - sni, - fp, - alpn, - pbk, - sid, - spx, - headerType, - host, - path, - authority, - serviceName, - mode - } = chainProxyParams; - - let proxyOutbound = { - mux: { - concurrency: 8, - enabled: true, - xudpConcurrency: 16, - xudpProxyUDP443: "reject" - }, - protocol: "vless", - settings: { - vnext: [ - { - address: hostName, - port: +port, - users: [ - { - encryption: "none", - flow: flow, - id: uuid, - level: 8, - security: "auto" - } - ] - } - ] - }, - streamSettings: { - network: type, - security: security, - sockopt: { - dialerProxy: "proxy", - tcpNoDelay: true - } - }, - tag: "chain" - }; - - if (security === 'tls') { - const tlsAlpns = alpn ? alpn?.split(',') : []; - proxyOutbound.streamSettings.tlsSettings = { - allowInsecure: false, - fingerprint: fp, - alpn: tlsAlpns, - serverName: sni - }; - } - - if (security === 'reality') { - delete proxyOutbound.mux; - proxyOutbound.streamSettings.realitySettings = { - fingerprint: fp, - publicKey: pbk, - serverName: sni, - shortId: sid, - spiderX: spx - }; - } - - if (headerType === 'http') { - const httpPaths = path?.split(','); - const httpHosts = host?.split(','); - proxyOutbound.streamSettings.tcpSettings = { - header: { - request: { - headers: { Host: httpHosts }, - method: "GET", - path: httpPaths, - version: "1.1" - }, - response: { - headers: { "Content-Type": ["application/octet-stream"] }, - reason: "OK", - status: "200", - version: "1.1" - }, - type: "http" - } - }; - } - - if (type === 'tcp' && security !== 'reality' && !headerType) proxyOutbound.streamSettings.tcpSettings = { - header: { - type: "none" - } - }; - - if (type === 'ws') proxyOutbound.streamSettings.wsSettings = { - headers: { Host: host }, - path: path - }; - - if (type === 'grpc') { - delete proxyOutbound.mux; - proxyOutbound.streamSettings.grpcSettings = { - authority: authority, - multiMode: mode === 'multi', - serviceName: serviceName - }; - } - - return proxyOutbound; -} - -function buildXrayConfig (proxySettings, remark, isFragment, isBalancer, isChain, balancerFallback, isWarp) { - const { - vlessTrojanFakeDNS, - enableIPv6, - warpFakeDNS, - warpEnableIPv6, - bestVLESSTrojanInterval, - bestWarpInterval, - lengthMin, - lengthMax, - intervalMin, - intervalMax, - fragmentPackets - } = proxySettings; - - const isFakeDNS = (vlessTrojanFakeDNS && !isWarp) || (warpFakeDNS && isWarp); - const isIPv6 = (enableIPv6 && !isWarp) || (warpEnableIPv6 && isWarp); - let config = structuredClone(xrayConfigTemp); - config.remarks = remark; - if (isFakeDNS) { - config.inbounds[0].sniffing.destOverride.push("fakedns"); - config.inbounds[1].sniffing.destOverride.push("fakedns"); - !isIPv6 && config.fakedns.pop(); - } else { - delete config.fakedns; - } - - if (isFragment) { - const fragment = config.outbounds[0].settings.fragment; - fragment.length = `${lengthMin}-${lengthMax}`; - fragment.interval = `${intervalMin}-${intervalMax}`; - fragment.packets = fragmentPackets; - } else { - config.outbounds.shift(); - } - - if (isBalancer) { - const interval = isWarp ? bestWarpInterval : bestVLESSTrojanInterval; - config.observatory.probeInterval = `${interval}s`; - config.observatory.subjectSelector = [isChain ? 'chain' : 'prox']; - config.routing.balancers[0].selector = [isChain ? 'chain' : 'prox']; - if (balancerFallback) config.routing.balancers[0].fallbackTag = balancerFallback; - } else { - delete config.observatory; - delete config.routing.balancers; - } - - return config; -} - -async function buildXrayBestPingConfig(proxySettings, totalAddresses, chainProxy, outbounds, isFragment) { - const remark = isFragment ? '💦 BPB F - Best Ping 💥' : '💦 BPB - Best Ping 💥'; - let config = buildXrayConfig(proxySettings, remark, isFragment, true, chainProxy, chainProxy ? 'chain-2' : 'prox-2'); - config.dns = await buildXrayDNS(proxySettings, totalAddresses, undefined, false, true, false); - config.routing.rules = buildXrayRoutingRules(proxySettings, totalAddresses, chainProxy, true, false); - config.outbounds.unshift(...outbounds); - - return config; -} - -async function buildXrayBestFragmentConfig(proxySettings, hostName, chainProxy, outbounds) { - const bestFragValues = ['10-20', '20-30', '30-40', '40-50', '50-60', '60-70', - '70-80', '80-90', '90-100', '10-30', '20-40', '30-50', - '40-60', '50-70', '60-80', '70-90', '80-100', '100-200']; - - let config = buildXrayConfig(proxySettings, '💦 BPB F - Best Fragment 😎', true, true, chainProxy, undefined, false); - config.dns = await buildXrayDNS(proxySettings, [], hostName, false, true, false); - config.routing.rules = buildXrayRoutingRules(proxySettings, [], chainProxy, true, false); - const fragment = config.outbounds.shift(); - let bestFragOutbounds = []; - - bestFragValues.forEach( (fragLength, index) => { - if (chainProxy) { - let chainOutbound = structuredClone(chainProxy); - chainOutbound.tag = `chain-${index + 1}`; - chainOutbound.streamSettings.sockopt.dialerProxy = `prox-${index + 1}`; - bestFragOutbounds.push(chainOutbound); - } - - let proxyOutbound = structuredClone(outbounds[chainProxy ? 1 : 0]); - proxyOutbound.tag = `prox-${index + 1}`; - proxyOutbound.streamSettings.sockopt.dialerProxy = `frag-${index + 1}`; - let fragmentOutbound = structuredClone(fragment); - fragmentOutbound.tag = `frag-${index + 1}`; - fragmentOutbound.settings.fragment.length = fragLength; - fragmentOutbound.settings.fragment.interval = '1-1'; - bestFragOutbounds.push(proxyOutbound, fragmentOutbound); - }); - - config.outbounds.unshift(...bestFragOutbounds); - return config; -} - -async function buildXrayWorkerLessConfig(proxySettings) { - let config = buildXrayConfig(proxySettings, '💦 BPB F - WorkerLess ⭐', true, false, false, undefined, false); - config.dns = await buildXrayDNS(proxySettings, [], undefined, true); - config.routing.rules = buildXrayRoutingRules(proxySettings, [], false, false, true); - let fakeOutbound = buildXrayVLESSOutbound('fake-outbound', 'google.com', '443', userID, 'google.com', 'google.com', '', true, false); - delete fakeOutbound.streamSettings.sockopt; - fakeOutbound.streamSettings.wsSettings.path = '/'; - config.outbounds.push(fakeOutbound); - return config; -} - -async function getXrayCustomConfigs(env, proxySettings, hostName, isFragment) { - let configs = []; - let outbounds = []; - let protocols = []; - let chainProxy; - const { - proxyIP, - outProxy, - outProxyParams, - cleanIPs, - enableIPv6, - customCdnAddrs, - customCdnHost, - customCdnSni, - vlessConfigs, - trojanConfigs, - ports - } = proxySettings; - - if (outProxy) { - const proxyParams = JSON.parse(outProxyParams); - try { - chainProxy = buildXrayChainOutbound(proxyParams); - } catch (error) { - console.log('An error occured while parsing chain proxy: ', error); - chainProxy = undefined; - await env.bpb.put("proxySettings", JSON.stringify({ - ...proxySettings, - outProxy: '', - outProxyParams: {} - })); - } - } - - const Addresses = await getConfigAddresses(hostName, cleanIPs, enableIPv6); - const customCdnAddresses = customCdnAddrs ? customCdnAddrs.split(',') : []; - const totalAddresses = isFragment ? [...Addresses] : [...Addresses, ...customCdnAddresses]; - const totalPorts = ports.filter(port => isFragment ? defaultHttpsPorts.includes(port): true); - vlessConfigs && protocols.push('VLESS'); - trojanConfigs && protocols.push('Trojan'); - let proxyIndex = 1; - - for (const protocol of protocols) { - let protocolIndex = 1; - for (const port of totalPorts) { - for (const addr of totalAddresses) { - const isCustomAddr = customCdnAddresses.includes(addr); - const configType = isCustomAddr ? 'C' : isFragment ? 'F' : ''; - const sni = isCustomAddr ? customCdnSni : randomUpperCase(hostName); - const host = isCustomAddr ? customCdnHost : hostName; - const remark = generateRemark(protocolIndex, port, addr, cleanIPs, protocol, configType); - let customConfig = buildXrayConfig(proxySettings, remark, isFragment, false, chainProxy, undefined, false); - customConfig.dns = await buildXrayDNS(proxySettings, [addr], undefined); - customConfig.routing.rules = buildXrayRoutingRules(proxySettings, [addr], chainProxy, false, false); - let outbound = protocol === 'VLESS' - ? buildXrayVLESSOutbound('proxy', addr, port, host, sni, proxyIP, isFragment, isCustomAddr) - : buildXrayTrojanOutbound('proxy', addr, port, host, sni, proxyIP, isFragment, isCustomAddr); - - customConfig.outbounds.unshift({...outbound}); - outbound.tag = `prox-${proxyIndex}`; - - if (chainProxy) { - customConfig.outbounds.unshift(chainProxy); - let chainOutbound = structuredClone(chainProxy); - chainOutbound.tag = `chain-${proxyIndex}`; - chainOutbound.streamSettings.sockopt.dialerProxy = `prox-${proxyIndex}`; - outbounds.push(chainOutbound); - } - - outbounds.push(outbound); - configs.push(customConfig); - proxyIndex++; - protocolIndex++; - } - } - } - - const bestPing = await buildXrayBestPingConfig(proxySettings, totalAddresses, chainProxy, outbounds, isFragment); - if (!isFragment) return [...configs, bestPing]; - const bestFragment = await buildXrayBestFragmentConfig(proxySettings, hostName, chainProxy, outbounds); - const workerLessConfig = await buildXrayWorkerLessConfig(proxySettings); - configs.push(bestPing, bestFragment, workerLessConfig); - - return configs; -} - -async function getXrayWarpConfigs (proxySettings, warpConfigs, client) { - let xrayWarpConfigs = []; - let xrayWoWConfigs = []; - let xrayWarpOutbounds = []; - let xrayWoWOutbounds = []; - const { warpEndpoints } = proxySettings; - const outboundDomains = warpEndpoints.split(',').map(endpoint => endpoint.split(':')[0]).filter(address => isDomain(address)); - const proIndicator = client === 'nikang' ? ' Pro ' : ' '; - - for (const [index, endpoint] of warpEndpoints.split(',').entries()) { - const endpointHost = endpoint.split(':')[0]; - let warpConfig = buildXrayConfig(proxySettings, `💦 ${index + 1} - Warp${proIndicator}🇮🇷`, false, false, false, undefined, true); - let WoWConfig = buildXrayConfig(proxySettings, `💦 ${index + 1} - WoW${proIndicator}🌍`, false, false, true, undefined, true); - warpConfig.dns = WoWConfig.dns = await buildXrayDNS(proxySettings, [endpointHost], undefined, false, true); - warpConfig.routing.rules = buildXrayRoutingRules(proxySettings, [endpointHost], false, false, false); - WoWConfig.routing.rules = buildXrayRoutingRules(proxySettings, [endpointHost], true, false, false); - const warpOutbound = buildXrayWarpOutbound(proxySettings, warpConfigs, endpoint, false, client); - const WoWOutbound = buildXrayWarpOutbound(proxySettings, warpConfigs, endpoint, true, client); - warpOutbound.settings.peers[0].endpoint = endpoint; - WoWOutbound.settings.peers[0].endpoint = endpoint; - warpConfig.outbounds.unshift(warpOutbound); - WoWConfig.outbounds.unshift(WoWOutbound, warpOutbound); - xrayWarpConfigs.push(warpConfig); - xrayWoWConfigs.push(WoWConfig); - const proxyOutbound = structuredClone(warpOutbound); - proxyOutbound.tag = `prox-${index + 1}`; - const chainOutbound = structuredClone(WoWOutbound); - chainOutbound.tag = `chain-${index + 1}`; - chainOutbound.streamSettings.sockopt.dialerProxy = `prox-${index + 1}`; - xrayWarpOutbounds.push(proxyOutbound); - xrayWoWOutbounds.push(chainOutbound); - } - - const dnsObject = await buildXrayDNS(proxySettings, outboundDomains, undefined, false, true, true); - let xrayWarpBestPing = buildXrayConfig(proxySettings, `💦 Warp${proIndicator}- Best Ping 🚀`, false, true, false, undefined, true); - xrayWarpBestPing.dns = dnsObject; - xrayWarpBestPing.routing.rules = buildXrayRoutingRules(proxySettings, outboundDomains, false, true, false); - xrayWarpBestPing.outbounds.unshift(...xrayWarpOutbounds); - let xrayWoWBestPing = buildXrayConfig(proxySettings, `💦 WoW${proIndicator}- Best Ping 🚀`, false, true, true, undefined, true); - xrayWoWBestPing.dns = dnsObject; - xrayWoWBestPing.routing.rules = buildXrayRoutingRules(proxySettings, outboundDomains, true, true, false); - xrayWoWBestPing.outbounds.unshift(...xrayWoWOutbounds, ...xrayWarpOutbounds); - return [...xrayWarpConfigs, ...xrayWoWConfigs, xrayWarpBestPing, xrayWoWBestPing]; -} - -async function buildClashDNS (proxySettings, isWarp) { - const { - remoteDNS, - resolvedRemoteDNS, - localDNS, - vlessTrojanFakeDNS, - enableIPv6, - warpFakeDNS, - warpEnableIPv6, - bypassIran, - bypassChina, - bypassRussia - } = proxySettings; - - const warpRemoteDNS = warpEnableIPv6 - ? ["1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001"] - : ["1.1.1.1", "1.0.0.1"]; - const isFakeDNS = (vlessTrojanFakeDNS && !isWarp) || (warpFakeDNS && isWarp); - const isIPv6 = (enableIPv6 && !isWarp) || (warpEnableIPv6 && isWarp); - const isBypass = bypassIran || bypassChina || bypassRussia; - const bypassRules = [ - { rule: bypassIran, geosite: "category-ir" }, - { rule: bypassChina, geosite: "cn" }, - { rule: bypassRussia, geosite: "category-ru" } - ]; - - let dns = { - "enable": true, - "listen": "0.0.0.0:1053", - "ipv6": isIPv6, - "respect-rules": true, - "nameserver": isWarp ? warpRemoteDNS : [remoteDNS], - "proxy-server-nameserver": [localDNS] - }; - - if (resolvedRemoteDNS.server && !isWarp) { - dns["hosts"] = { - [resolvedRemoteDNS.server]: resolvedRemoteDNS.staticIPs - }; - } - - if (isBypass) { - let geosites = []; - bypassRules.forEach(({ rule, geosite }) => { - rule && geosites.push(geosite) - }); - - dns["nameserver-policy"] = { - [`geosite:${geosites.join(',')}`]: [localDNS], - "www.gstatic.com": [localDNS] - }; - } - - if (isFakeDNS) Object.assign(dns, { - "enhanced-mode": "fake-ip", - "fake-ip-range": "198.18.0.1/16", - "fake-ip-filter": ["geosite:private"] - }); - - return dns; -} - -function buildClashRoutingRules (proxySettings) { - const { - localDNS, - bypassLAN, - bypassIran, - bypassChina, - bypassRussia, - blockAds, - blockPorn, - blockUDP443 - } = proxySettings; - - const isBypass = bypassIran || bypassChina || bypassLAN || bypassRussia; - const isBlock = blockAds || blockPorn; - let geositeDirectRules = [], geoipDirectRules = [], geositeBlockRules = []; - const geoRules = [ - { rule: bypassLAN, type: 'direct', geosite: "private", geoip: "private" }, - { rule: bypassIran, type: 'direct', geosite: "category-ir", geoip: "ir" }, - { rule: bypassChina, type: 'direct', geosite: "cn", geoip: "cn" }, - { rule: bypassRussia, type: 'direct', geosite: "category-ru", geoip: "ru" }, - { rule: blockAds, type: 'block', geosite: "category-ads-all" }, - { rule: blockAds, type: 'block', geosite: "category-ads-ir" }, - { rule: blockPorn, type: 'block', geosite: "category-porn" } - ]; - - if (isBypass || isBlock) { - geoRules.forEach(({ rule, type, geosite, geoip }) => { - if (rule) { - if (type === 'direct') { - geositeDirectRules.push(`GEOSITE,${geosite},DIRECT`); - geoipDirectRules.push(`GEOIP,${geoip},DIRECT,no-resolve`); - } else { - geositeBlockRules.push(`GEOSITE,${geosite},REJECT`); - } - } - }); - } - - let rules = [ - `AND,((IN-NAME,mixed-in),(DST-PORT,53)),dns-out`, - `AND,((IP-CIDR,${localDNS}/32),(DST-PORT,53)),DIRECT`, - ...geositeDirectRules, - ...geoipDirectRules, - ...geositeBlockRules - ]; - - blockUDP443 && rules.push("AND,((NETWORK,udp),(DST-PORT,443)),REJECT"); - rules.push("MATCH,✅ Selector"); - return rules; -} - -function buildClashVLESSOutbound (remark, address, port, host, sni, path, allowInsecure) { - const tls = defaultHttpsPorts.includes(port) ? true : false; - const addr = isIPv6(address) ? address.replace(/\[|\]/g, '') : address; - let outbound = { - "name": remark, - "type": "vless", - "server": addr, - "port": +port, - "uuid": userID, - "tls": tls, - "network": "ws", - "udp": true, - "ws-opts": { - "path": path, - "headers": { "host": host }, - "max-early-data": 2560, - "early-data-header-name": "Sec-WebSocket-Protocol" - } - }; - - if (tls) { - Object.assign(outbound, { - "servername": sni, - "alpn": ["h2", "http/1.1"], - "client-fingerprint": "random", - "skip-cert-verify": allowInsecure - }); - } - - return outbound; -} - -function buildClashTrojanOutbound (remark, address, port, host, sni, path, allowInsecure) { - const addr = isIPv6(address) ? address.replace(/\[|\]/g, '') : address; - return { - "name": remark, - "type": "trojan", - "server": addr, - "port": +port, - "password": trojanPassword, - "network": "ws", - "udp": true, - "ws-opts": { - "path": path, - "headers": { "host": host }, - "max-early-data": 2560, - "early-data-header-name": "Sec-WebSocket-Protocol" - }, - "sni": sni, - "alpn": ["h2", "http/1.1"], - "client-fingerprint": "random", - "skip-cert-verify": allowInsecure - }; -} - -function buildClashWarpOutbound (warpConfigs, remark, endpoint, chain) { - const ipv6Regex = /\[(.*?)\]/; - const portRegex = /[^:]*$/; - const endpointServer = endpoint.includes('[') ? endpoint.match(ipv6Regex)[1] : endpoint.split(':')[0]; - const endpointPort = endpoint.includes('[') ? +endpoint.match(portRegex)[0] : +endpoint.split(':')[1]; - const { - warpIPv6, - reserved, - publicKey, - privateKey - } = extractWireguardParams(warpConfigs, chain); - - return { - "name": remark, - "type": "wireguard", - "ip": "172.16.0.2/32", - "ipv6": warpIPv6, - "private-key": privateKey, - "server": endpointServer, - "port": endpointPort, - "public-key": publicKey, - "allowed-ips": ["0.0.0.0/0", "::/0"], - "reserved": reserved, - "udp": true, - "mtu": 1280, - "dialer-proxy": chain, - "remote-dns-resolve": true, - "dns": [ "1.1.1.1", "1.0.0.1" ] - }; -} - -function buildClashChainOutbound(chainProxyParams) { - if (["socks", "http"].includes(chainProxyParams.protocol)) { - const { protocol, host, port, user, pass } = chainProxyParams; - const proxyType = protocol === 'socks' ? 'socks5' : protocol; - return { - "name": "", - "type": proxyType, - "server": host, - "port": +port, - "dialer-proxy": "", - "username": user, - "password": pass - }; - } - - const { hostName, port, uuid, flow, security, type, sni, fp, alpn, pbk, sid, headerType, host, path, serviceName } = chainProxyParams; - let chainOutbound = { - "name": "💦 Chain Best Ping 💥", - "type": "vless", - "server": hostName, - "port": +port, - "udp": true, - "uuid": uuid, - "flow": flow, - "network": type, - "dialer-proxy": "💦 Best Ping 💥" - }; - - if (security === 'tls') { - const tlsAlpns = alpn ? alpn?.split(',') : []; - Object.assign(chainOutbound, { - "tls": true, - "servername": sni, - "alpn": tlsAlpns, - "client-fingerprint": fp - }); - } - - if (security === 'reality') Object.assign(chainOutbound, { - "tls": true, - "servername": sni, - "client-fingerprint": fp, - "reality-opts": { - "public-key": pbk, - "short-id": sid - } - }); - - if (headerType === 'http') { - const httpPaths = path?.split(','); - chainOutbound["http-opts"] = { - "method": "GET", - "path": httpPaths, - "headers": { - "Connection": ["keep-alive"], - "Content-Type": ["application/octet-stream"] - } - }; - } - - if (type === 'ws') { - const wsPath = path?.split('?ed=')[0]; - const earlyData = +path?.split('?ed=')[1]; - chainOutbound["ws-opts"] = { - "path": wsPath, - "headers": { - "Host": host - }, - "max-early-data": earlyData, - "early-data-header-name": "Sec-WebSocket-Protocol" - }; - } - - if (type === 'grpc') chainOutbound["grpc-opts"] = { - "grpc-service-name": serviceName - }; - - return chainOutbound; -} - -async function getClashWarpConfig(proxySettings, warpConfigs) { - const { warpEndpoints, warpEnableIPv6 } = proxySettings; - let config = structuredClone(clashConfigTemp); - config.ipv6 = warpEnableIPv6; - config.dns = await buildClashDNS(proxySettings, true); - config.rules = buildClashRoutingRules(proxySettings); - const selector = config['proxy-groups'][0]; - const warpUrlTest = config['proxy-groups'][1]; - selector.proxies = ['💦 Warp - Best Ping 🚀', '💦 WoW - Best Ping 🚀']; - warpUrlTest.name = '💦 Warp - Best Ping 🚀'; - warpUrlTest.interval = +proxySettings.bestWarpInterval; - config['proxy-groups'].push(structuredClone(warpUrlTest)); - const WoWUrlTest = config['proxy-groups'][2]; - WoWUrlTest.name = '💦 WoW - Best Ping 🚀'; - let warpRemarks = [], WoWRemarks = []; - - warpEndpoints.split(',').forEach( (endpoint, index) => { - const warpRemark = `💦 ${index + 1} - Warp 🇮🇷`; - const WoWRemark = `💦 ${index + 1} - WoW 🌍`; - const warpOutbound = buildClashWarpOutbound(warpConfigs, warpRemark, endpoint, ''); - const WoWOutbound = buildClashWarpOutbound(warpConfigs, WoWRemark, endpoint, warpRemark); - config.proxies.push(WoWOutbound, warpOutbound); - warpRemarks.push(warpRemark); - WoWRemarks.push(WoWRemark); - warpUrlTest.proxies.push(warpRemark); - WoWUrlTest.proxies.push(WoWRemark); - }); - - selector.proxies.push(...warpRemarks, ...WoWRemarks); - return config; -} - -async function getClashNormalConfig (env, proxySettings, hostName) { - let chainProxy; - const { - cleanIPs, - proxyIP, - ports, - vlessConfigs, - trojanConfigs, - outProxy, - outProxyParams, - customCdnAddrs, - customCdnHost, - customCdnSni, - bestVLESSTrojanInterval, - enableIPv6 - } = proxySettings; - - if (outProxy) { - const proxyParams = JSON.parse(outProxyParams); - try { - chainProxy = buildClashChainOutbound(proxyParams); - } catch (error) { - console.log('An error occured while parsing chain proxy: ', error); - chainProxy = undefined; - await env.bpb.put("proxySettings", JSON.stringify({ - ...proxySettings, - outProxy: '', - outProxyParams: {} - })); - } - } - - let config = structuredClone(clashConfigTemp); - config.ipv6 = enableIPv6; - config.dns = await buildClashDNS(proxySettings, false); - config.rules = buildClashRoutingRules(proxySettings); - const selector = config['proxy-groups'][0]; - const urlTest = config['proxy-groups'][1]; - selector.proxies = ['💦 Best Ping 💥']; - urlTest.name = '💦 Best Ping 💥'; - urlTest.interval = +bestVLESSTrojanInterval; - const Addresses = await getConfigAddresses(hostName, cleanIPs, enableIPv6); - const customCdnAddresses = customCdnAddrs ? customCdnAddrs.split(',') : []; - const totalAddresses = [...Addresses, ...customCdnAddresses]; - let proxyIndex = 1, path; - const protocols = [ - ...(vlessConfigs ? ['VLESS'] : []), - ...(trojanConfigs ? ['Trojan'] : []) - ]; - - protocols.forEach ( protocol => { - let protocolIndex = 1; - ports.forEach ( port => { - totalAddresses.forEach( addr => { - let VLESSOutbound, TrojanOutbound; - const isCustomAddr = customCdnAddresses.includes(addr); - const configType = isCustomAddr ? 'C' : ''; - const sni = isCustomAddr ? customCdnSni : randomUpperCase(hostName); - const host = isCustomAddr ? customCdnHost : hostName; - const remark = generateRemark(protocolIndex, port, addr, cleanIPs, protocol, configType).replace(' : ', ' - '); - - if (protocol === 'VLESS') { - path = `/${getRandomPath(16)}${proxyIP ? `/${btoa(proxyIP)}` : ''}`; - VLESSOutbound = buildClashVLESSOutbound( - chainProxy ? `proxy-${proxyIndex}` : remark, - addr, - port, - host, - sni, - path, - isCustomAddr - ); - config.proxies.push(VLESSOutbound); - selector.proxies.push(remark); - urlTest.proxies.push(remark); - } - - if (protocol === 'Trojan' && defaultHttpsPorts.includes(port)) { - path = `/tr${getRandomPath(16)}${proxyIP ? `/${btoa(proxyIP)}` : ''}`; - TrojanOutbound = buildClashTrojanOutbound( - chainProxy ? `proxy-${proxyIndex}` : remark, - addr, - port, - host, - sni, - path, - isCustomAddr - ); - config.proxies.push(TrojanOutbound); - selector.proxies.push(remark); - urlTest.proxies.push(remark); - } - - if (chainProxy) { - let chain = structuredClone(chainProxy); - chain['name'] = remark; - chain['dialer-proxy'] = `proxy-${proxyIndex}`; - config.proxies.push(chain); - } - - proxyIndex++; - protocolIndex++; - }); - }); - }); - - return config; -} - -function buildSingBoxDNS (proxySettings, isChain, isWarp) { - const { - remoteDNS, - localDNS, - vlessTrojanFakeDNS, - enableIPv6, - warpFakeDNS, - warpEnableIPv6, - bypassIran, - bypassChina, - bypassRussia, - blockAds, - blockPorn - } = proxySettings; - - let fakeip; - const isFakeDNS = (vlessTrojanFakeDNS && !isWarp) || (warpFakeDNS && isWarp); - const isIPv6 = (enableIPv6 && !isWarp) || (warpEnableIPv6 && isWarp); - const isBypass = bypassIran || bypassChina || bypassRussia; - const geoRules = [ - { rule: bypassIran, type: 'direct', ruleSet: "geosite-ir" }, - { rule: bypassChina, type: 'direct', ruleSet: "geosite-cn" }, - { rule: bypassRussia, type: 'direct', ruleSet: "geosite-category-ru" }, - { rule: true, type: 'block', ruleSet: "geosite-malware" }, - { rule: true, type: 'block', ruleSet: "geosite-phishing" }, - { rule: true, type: 'block', ruleSet: "geosite-cryptominers" }, - { rule: blockAds, type: 'block', ruleSet: "geosite-category-ads-all" }, - { rule: blockPorn, type: 'block', ruleSet: "geosite-nsfw" } - ]; - const servers = [ - { - address: isWarp ? "1.1.1.1" : remoteDNS, - address_resolver: "dns-direct", - strategy: isIPv6 ? "prefer_ipv4" : "ipv4_only", - detour: isChain ? 'proxy-1' : "proxy", - tag: "dns-remote" - }, - { - address: localDNS, - strategy: isIPv6 ? "prefer_ipv4" : "ipv4_only", - detour: "direct", - tag: "dns-direct" - }, - { - address: "rcode://success", - tag: "dns-block" - } - ]; - - let rules = [ - { - outbound: "any", - server: "dns-direct" - }, - { - domain: "www.gstatic.com", - server: "dns-direct" - }, - { - clash_mode: "block", - server: "dns-block" - }, - { - clash_mode: "direct", - server: "dns-direct" - }, - { - clash_mode: "global", - server: "dns-remote" - } - ]; - - let bypassRule = { - rule_set: [], - server: "dns-direct" - }; - - let blockRule = { - disable_cache: true, - rule_set: [], - server: "dns-block" - }; - - geoRules.forEach(({ rule, type, ruleSet }) => { - rule && type === 'direct' && bypassRule.rule_set.push(ruleSet); - rule && type === 'block' && blockRule.rule_set.push(ruleSet); - }); - - isBypass && rules.push(bypassRule); - rules.push(bypassRule, blockRule); - if (isFakeDNS) { - servers.push({ - address: "fakeip", - tag: "dns-fake" - }); - - rules.push({ - disable_cache: true, - inbound: "tun-in", - query_type: [ - "A", - "AAAA" - ], - server: "dns-fake" - }); - - fakeip = { - enabled: true, - inet4_range: "198.18.0.0/15" - }; - - if (isIPv6) fakeip.inet6_range = "fc00::/18"; - } - - return {servers, rules, fakeip}; -} - -function buildSingBoxRoutingRules (proxySettings) { - const { - bypassLAN, - bypassIran, - bypassChina, - bypassRussia, - blockAds, - blockPorn, - blockUDP443 - } = proxySettings; - - const isBypass = bypassIran || bypassChina || bypassRussia; - let rules = [ - { - inbound: "dns-in", - outbound: "dns-out" - }, - { - network: "udp", - port: 53, - outbound: "dns-out" - }, - { - clash_mode: "direct", - outbound: "direct" - }, - { - clash_mode: "block", - outbound: "block" - }, - { - clash_mode: "global", - outbound: "proxy" - } - ]; - - const geoRules = [ - { - rule: bypassIran, - type: 'direct', - ruleSet: { - geosite: "geosite-ir", - geoip: "geoip-ir", - geositeURL: "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-ir.srs", - geoipURL: "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geoip-ir.srs" - } - }, - { - rule: bypassChina, - type: 'direct', - ruleSet: { - geosite: "geosite-cn", - geoip: "geoip-cn", - geositeURL: "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs", - geoipURL: "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs" - } - }, - { - rule: bypassRussia, - type: 'direct', - ruleSet: { - geosite: "geosite-category-ru", - geoip: "geoip-ru", - geositeURL: "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-ru.srs", - geoipURL: "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-ru.srs" - } - }, - { - rule: true, - type: 'block', - ruleSet: { - geosite: "geosite-malware", - geoip: "geoip-malware", - geositeURL: "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-malware.srs", - geoipURL: "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geoip-malware.srs" - } - }, - { - rule: true, - type: 'block', - ruleSet: { - geosite: "geosite-phishing", - geoip: "geoip-phishing", - geositeURL: "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-phishing.srs", - geoipURL: "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geoip-phishing.srs" - } - }, - { - rule: true, - type: 'block', - ruleSet: { - geosite: "geosite-cryptominers", - geositeURL: "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-cryptominers.srs", - } - }, - { - rule: blockAds, - type: 'block', - ruleSet: { - geosite: "geosite-category-ads-all", - geositeURL: "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-category-ads-all.srs", - } - }, - { - rule: blockPorn, - type: 'block', - ruleSet: { - geosite: "geosite-nsfw", - geositeURL: "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-nsfw.srs", - } - }, - ]; - - bypassLAN && rules.push({ - ip_is_private: true, - outbound: "direct" - }); - - const createRule = (outbound) => ({ - rule_set: [], - outbound - }); - - const routingRuleSet = { - type: "remote", - tag: "", - format: "binary", - url: "", - download_detour: "direct" - }; - - let directRule = createRule('direct');; - let blockRule = createRule('block'); - let ruleSets = []; - - geoRules.forEach(({ rule, type, ruleSet }) => { - const { geosite, geoip, geositeURL, geoipURL } = ruleSet; - if (rule) { - if (type === 'direct') { - directRule.rule_set.unshift(geosite); - directRule.rule_set.push(geoip); - } else { - blockRule.rule_set.unshift(geosite); - geoip && blockRule.rule_set.push(geoip); - } - ruleSets.push({...routingRuleSet, tag: geosite, url: geositeURL}); - geoip && ruleSets.push({...routingRuleSet, tag: geoip, url: geoipURL}); - } - }); - - isBypass && rules.push(directRule); - rules.push(blockRule); - - blockUDP443 && rules.push({ - network: "udp", - port: 443, - protocol: "quic", - outbound: "block" - }); - - rules.push({ - ip_cidr: ["224.0.0.0/3", "ff00::/8"], - source_ip_cidr: ["224.0.0.0/3", "ff00::/8"], - outbound: "block" - }); - - return {rules: rules, rule_set: ruleSets}; -} - -function buildSingBoxVLESSOutbound (proxySettings, remark, address, port, host, sni, allowInsecure, isFragment) { - const { enableIPv6, lengthMin, lengthMax, intervalMin, intervalMax, proxyIP } = proxySettings; - const path = `/${getRandomPath(16)}${proxyIP ? `/${btoa(proxyIP)}` : ''}`; - const tls = defaultHttpsPorts.includes(port) ? true : false; - let outbound = { - type: "vless", - server: address, - server_port: +port, - domain_strategy: enableIPv6 ? "prefer_ipv4" : "ipv4_only", - uuid: userID, - tls: { - alpn: "http/1.1", - enabled: true, - insecure: allowInsecure, - server_name: sni, - utls: { - enabled: true, - fingerprint: "randomized" - } - }, - transport: { - early_data_header_name: "Sec-WebSocket-Protocol", - max_early_data: 2560, - headers: { - Host: host - }, - path: path, - type: "ws" - }, - tag: remark - }; - - if (!tls) delete outbound.tls; - if (isFragment) outbound.tls_fragment = { - enabled: true, - size: `${lengthMin}-${lengthMax}`, - sleep: `${intervalMin}-${intervalMax}` - }; - - return outbound; -} - -function buildSingBoxTrojanOutbound (proxySettings, remark, address, port, host, sni, allowInsecure, isFragment) { - const { enableIPv6, lengthMin, lengthMax, intervalMin, intervalMax, proxyIP } = proxySettings; - const path = `/tr${getRandomPath(16)}${proxyIP ? `/${btoa(proxyIP)}` : ''}`; - const tls = defaultHttpsPorts.includes(port) ? true : false; - let outbound = { - type: "trojan", - password: trojanPassword, - server: address, - server_port: +port, - domain_strategy: enableIPv6 ? "prefer_ipv4" : "ipv4_only", - tls: { - alpn: "http/1.1", - enabled: true, - insecure: allowInsecure, - server_name: sni, - utls: { - enabled: true, - fingerprint: "randomized" - } - }, - transport: { - early_data_header_name: "Sec-WebSocket-Protocol", - max_early_data: 2560, - headers: { - Host: host - }, - path: path, - type: "ws" - }, - tag: remark - } - - if (!tls) delete outbound.tls; - if (isFragment) outbound.tls_fragment = { - enabled: true, - size: `${lengthMin}-${lengthMax}`, - sleep: `${intervalMin}-${intervalMax}` - }; - - return outbound; -} - -function buildSingBoxWarpOutbound (proxySettings, warpConfigs, remark, endpoint, chain, client) { - const ipv6Regex = /\[(.*?)\]/; - const portRegex = /[^:]*$/; - const endpointServer = endpoint.includes('[') ? endpoint.match(ipv6Regex)[1] : endpoint.split(':')[0]; - const endpointPort = endpoint.includes('[') ? +endpoint.match(portRegex)[0] : +endpoint.split(':')[1]; - const { - warpEnableIPv6, - hiddifyNoiseMode, - noiseCountMin, - noiseCountMax, - noiseSizeMin, - noiseSizeMax, - noiseDelayMin, - noiseDelayMax - } = proxySettings; - - const { - warpIPv6, - reserved, - publicKey, - privateKey - } = extractWireguardParams(warpConfigs, chain); - - let outbound = { - local_address: [ - "172.16.0.2/32", - warpIPv6 - ], - mtu: 1280, - peer_public_key: publicKey, - private_key: privateKey, - reserved: reserved, - server: endpointServer, - server_port: endpointPort, - domain_strategy: warpEnableIPv6 ? "prefer_ipv4" : "ipv4_only", - type: "wireguard", - detour: chain, - tag: remark - }; - - client === 'hiddify' && Object.assign(outbound, { - fake_packets_mode: hiddifyNoiseMode, - fake_packets: noiseCountMin === noiseCountMax ? noiseCountMin : `${noiseCountMin}-${noiseCountMax}`, - fake_packets_size: noiseSizeMin === noiseSizeMax ? noiseSizeMin : `${noiseSizeMin}-${noiseSizeMax}`, - fake_packets_delay: noiseDelayMin === noiseDelayMax ? noiseDelayMin : `${noiseDelayMin}-${noiseDelayMax}` - }); - - return outbound; -} - -function buildSingBoxChainOutbound (chainProxyParams) { - if (["socks", "http"].includes(chainProxyParams.protocol)) { - const { protocol, host, port, user, pass } = chainProxyParams; - - let chainOutbound = { - type: protocol, - tag: "", - server: host, - server_port: +port, - username: user, - password: pass, - detour: "" - }; - - if (protocol === 'socks') chainOutbound.version = "5"; - return chainOutbound; - } - - const { hostName, port, uuid, flow, security, type, sni, fp, alpn, pbk, sid, headerType, host, path, serviceName } = chainProxyParams; - let chainOutbound = { - type: "vless", - tag: "", - server: hostName, - server_port: +port, - uuid: uuid, - flow: flow, - detour: "" - }; - - if (security === 'tls' || security === 'reality') { - const tlsAlpns = alpn ? alpn?.split(',').filter(value => value !== 'h2') : []; - chainOutbound.tls = { - enabled: true, - server_name: sni, - insecure: false, - alpn: tlsAlpns, - utls: { - enabled: true, - fingerprint: fp - } - }; - - if (security === 'reality') { - chainOutbound.tls.reality = { - enabled: true, - public_key: pbk, - short_id: sid - }; - - delete chainOutbound.tls.alpn; - } - } - - if (headerType === 'http') { - const httpHosts = host?.split(','); - chainOutbound.transport = { - type: "http", - host: httpHosts, - path: path, - method: "GET", - headers: { - "Connection": ["keep-alive"], - "Content-Type": ["application/octet-stream"] - }, - }; - } - - if (type === 'ws') { - const wsPath = path?.split('?ed=')[0]; - const earlyData = +path?.split('?ed=')[1] || 0; - chainOutbound.transport = { - type: "ws", - path: wsPath, - headers: { Host: host }, - max_early_data: earlyData, - early_data_header_name: "Sec-WebSocket-Protocol" - }; - } - - if (type === 'grpc') chainOutbound.transport = { - type: "grpc", - service_name: serviceName - }; - - return chainOutbound; -} - -async function getSingBoxWarpConfig (proxySettings, warpConfigs, client) { - const { warpEndpoints } = proxySettings; - let config = structuredClone(singboxConfigTemp); - const dnsObject = buildSingBoxDNS(proxySettings, false, true); - const {rules, rule_set} = buildSingBoxRoutingRules(proxySettings); - config.dns.servers = dnsObject.servers; - config.dns.rules = dnsObject.rules; - if (dnsObject.fakeip) config.dns.fakeip = dnsObject.fakeip; - config.route.rules = rules; - config.route.rule_set = rule_set; - const selector = config.outbounds[0]; - const warpUrlTest = config.outbounds[1]; - const proIndicator = client === 'hiddify' ? ' Pro ' : ' '; - selector.outbounds = [`💦 Warp${proIndicator}- Best Ping 🚀`, `💦 WoW${proIndicator}- Best Ping 🚀`]; - config.outbounds.splice(2, 0, structuredClone(warpUrlTest)); - const WoWUrlTest = config.outbounds[2]; - warpUrlTest.tag = `💦 Warp${proIndicator}- Best Ping 🚀`; - warpUrlTest.interval = `${proxySettings.bestWarpInterval}s`; - WoWUrlTest.tag = `💦 WoW${proIndicator}- Best Ping 🚀`; - WoWUrlTest.interval = `${proxySettings.bestWarpInterval}s`; - let warpRemarks = [], WoWRemarks = []; - - warpEndpoints.split(',').forEach( (endpoint, index) => { - const warpRemark = `💦 ${index + 1} - Warp 🇮🇷`; - const WoWRemark = `💦 ${index + 1} - WoW 🌍`; - const warpOutbound = buildSingBoxWarpOutbound(proxySettings, warpConfigs, warpRemark, endpoint, '', client); - const WoWOutbound = buildSingBoxWarpOutbound(proxySettings, warpConfigs, WoWRemark, endpoint, warpRemark, client); - config.outbounds.push(WoWOutbound, warpOutbound); - warpRemarks.push(warpRemark); - WoWRemarks.push(WoWRemark); - warpUrlTest.outbounds.push(warpRemark); - WoWUrlTest.outbounds.push(WoWRemark); - }); - - selector.outbounds.push(...warpRemarks, ...WoWRemarks); - return config; -} - -async function getSingBoxCustomConfig(env, proxySettings, hostName, client, isFragment) { - let chainProxyOutbound; - const { - cleanIPs, - ports, - vlessConfigs, - trojanConfigs, - outProxy, - outProxyParams, - customCdnAddrs, - customCdnHost, - customCdnSni, - bestVLESSTrojanInterval, - enableIPv6 - } = proxySettings; - - if (outProxy) { - const proxyParams = JSON.parse(outProxyParams); - try { - chainProxyOutbound = buildSingBoxChainOutbound(proxyParams); - } catch (error) { - console.log('An error occured while parsing chain proxy: ', error); - chainProxyOutbound = undefined; - await env.bpb.put("proxySettings", JSON.stringify({ - ...proxySettings, - outProxy: '', - outProxyParams: {} - })); - } - } - - let config = structuredClone(singboxConfigTemp); - const dnsObject = buildSingBoxDNS(proxySettings, chainProxyOutbound, false); - const {rules, rule_set} = buildSingBoxRoutingRules(proxySettings); - config.dns.servers = dnsObject.servers; - config.dns.rules = dnsObject.rules; - if (dnsObject.fakeip) config.dns.fakeip = dnsObject.fakeip; - config.route.rules = rules; - config.route.rule_set = rule_set; - const selector = config.outbounds[0]; - const urlTest = config.outbounds[1]; - selector.outbounds = ['💦 Best Ping 💥']; - urlTest.interval = `${bestVLESSTrojanInterval}s`; - urlTest.tag = '💦 Best Ping 💥'; - const Addresses = await getConfigAddresses(hostName, cleanIPs, enableIPv6); - const customCdnAddresses = customCdnAddrs ? customCdnAddrs.split(',') : []; - const totalAddresses = [...Addresses, ...customCdnAddresses]; - const totalPorts = ports.filter(port => isFragment ? defaultHttpsPorts.includes(port) : true); - let proxyIndex = 1; - const protocols = [ - ...(vlessConfigs ? ['VLESS'] : []), - ...(trojanConfigs ? ['Trojan'] : []) - ]; - - protocols.forEach ( protocol => { - let protocolIndex = 1; - totalPorts.forEach ( port => { - totalAddresses.forEach ( addr => { - let VLESSOutbound, TrojanOutbound; - const isCustomAddr = customCdnAddresses.includes(addr); - const configType = isCustomAddr ? 'C' : isFragment ? 'F' : ''; - const sni = isCustomAddr ? customCdnSni : randomUpperCase(hostName); - const host = isCustomAddr ? customCdnHost : hostName; - const remark = generateRemark(protocolIndex, port, addr, cleanIPs, protocol, configType); - - if (protocol === 'VLESS') { - VLESSOutbound = buildSingBoxVLESSOutbound ( - proxySettings, - chainProxyOutbound ? `proxy-${proxyIndex}` : remark, - addr, - port, - host, - sni, - isCustomAddr, - isFragment - ); - config.outbounds.push(VLESSOutbound); - } - - if (protocol === 'Trojan') { - TrojanOutbound = buildSingBoxTrojanOutbound ( - proxySettings, - chainProxyOutbound ? `proxy-${proxyIndex}` : remark, - addr, - port, - host, - sni, - isCustomAddr, - isFragment - ); - config.outbounds.push(TrojanOutbound); - } - - if (chainProxyOutbound) { - let chain = structuredClone(chainProxyOutbound); - chain.tag = remark; - chain.detour = `proxy-${proxyIndex}`; - config.outbounds.push(chain); - } - - selector.outbounds.push(remark); - urlTest.outbounds.push(remark); - proxyIndex++; - protocolIndex++; - }); - }); - }); - - return config; -} - -async function getNormalConfigs(proxySettings, hostName, client) { - const { - cleanIPs, - proxyIP, - ports, - vlessConfigs, - trojanConfigs , - outProxy, - customCdnAddrs, - customCdnHost, - customCdnSni, - enableIPv6 - } = proxySettings; - - let vlessConfs = '', trojanConfs = '', chainProxy = ''; - let proxyIndex = 1; - const Addresses = await getConfigAddresses(hostName, cleanIPs, enableIPv6); - const customCdnAddresses = customCdnAddrs ? customCdnAddrs.split(',') : []; - const totalAddresses = [...Addresses, ...customCdnAddresses]; - const alpn = client === 'singbox' ? 'http/1.1' : 'h2,http/1.1'; - const trojanPass = encodeURIComponent(trojanPassword); - const earlyData = client === 'singbox' - ? '&eh=Sec-WebSocket-Protocol&ed=2560' - : encodeURIComponent('?ed=2560'); - - ports.forEach(port => { - totalAddresses.forEach((addr, index) => { - const isCustomAddr = index > Addresses.length - 1; - const configType = isCustomAddr ? 'C' : ''; - const sni = isCustomAddr ? customCdnSni : randomUpperCase(hostName); - const host = isCustomAddr ? customCdnHost : hostName; - const path = `${getRandomPath(16)}${proxyIP ? `/${encodeURIComponent(btoa(proxyIP))}` : ''}${earlyData}`; - const vlessRemark = encodeURIComponent(generateRemark(proxyIndex, port, addr, cleanIPs, 'VLESS', configType)); - const trojanRemark = encodeURIComponent(generateRemark(proxyIndex, port, addr, cleanIPs, 'Trojan', configType)); - const tlsFields = defaultHttpsPorts.includes(port) - ? `&security=tls&sni=${sni}&fp=randomized&alpn=${alpn}` - : '&security=none'; - - if (vlessConfigs) { - vlessConfs += `${atob('dmxlc3M')}://${userID}@${addr}:${port}?path=/${path}&encryption=none&host=${host}&type=ws${tlsFields}#${vlessRemark}\n`; - } - - if (trojanConfigs) { - trojanConfs += `${atob('dHJvamFu')}://${trojanPass}@${addr}:${port}?path=/tr${path}&host=${host}&type=ws${tlsFields}#${trojanRemark}\n`; - } - - proxyIndex++; - }); - }); - - if (outProxy) { - let chainRemark = `#${encodeURIComponent('💦 Chain proxy 🔗')}`; - if (outProxy.startsWith('socks') || outProxy.startsWith('http')) { - const regex = /^(?:socks|http):\/\/([^@]+)@/; - const isUserPass = outProxy.match(regex); - const userPass = isUserPass ? isUserPass[1] : false; - chainProxy = userPass - ? outProxy.replace(userPass, btoa(userPass)) + chainRemark - : outProxy + chainRemark; - } else { - chainProxy = outProxy.split('#')[0] + chainRemark; - } - } - - return btoa(vlessConfs + trojanConfs + chainProxy); -} - -const xrayConfigTemp = { - remarks: "", - log: { - loglevel: "warning", - }, - dns: {}, - fakedns: [ - { - ipPool: "198.18.0.0/15", - poolSize: 32768 - }, - { - ipPool: "fc00::/18", - poolSize: 32768 - } - ], - inbounds: [ - { - port: 10808, - protocol: "socks", - settings: { - auth: "noauth", - udp: true, - userLevel: 8, - }, - sniffing: { - destOverride: ["http", "tls"], - enabled: true, - routeOnly: true - }, - tag: "socks-in", - }, - { - port: 10809, - protocol: "http", - settings: { - auth: "noauth", - udp: true, - userLevel: 8, - }, - sniffing: { - destOverride: ["http", "tls"], - enabled: true, - routeOnly: true - }, - tag: "http-in", - }, - { - listen: "127.0.0.1", - port: 10853, - protocol: "dokodemo-door", - settings: { - address: "1.1.1.1", - network: "tcp,udp", - port: 53 - }, - tag: "dns-in" - } - ], - outbounds: [ - { - tag: "fragment", - protocol: "freedom", - settings: { - fragment: { - packets: "tlshello", - length: "", - interval: "", - }, - domainStrategy: "UseIP" - }, - streamSettings: { - sockopt: { - tcpKeepAliveIdle: 100, - tcpNoDelay: true - }, - }, - }, - { - protocol: "dns", - tag: "dns-out" - }, - { - protocol: "freedom", - settings: {}, - tag: "direct", - }, - { - protocol: "blackhole", - settings: { - response: { - type: "http", - }, - }, - tag: "block", - }, - ], - policy: { - levels: { - 8: { - connIdle: 300, - downlinkOnly: 1, - handshake: 4, - uplinkOnly: 1, - } - }, - system: { - statsOutboundUplink: true, - statsOutboundDownlink: true, - } - }, - routing: { - domainStrategy: "IPIfNonMatch", - rules: [], - balancers: [ - { - tag: "all", - selector: ["prox"], - strategy: { - type: "leastPing", - }, - } - ] - }, - observatory: { - probeInterval: "30s", - probeURL: "https://www.gstatic.com/generate_204", - subjectSelector: ["prox"], - EnableConcurrency: true, - }, - stats: {} -}; - -const singboxConfigTemp = { - log: { - level: "warn", - timestamp: true - }, - dns: { - servers: [], - rules: [], - independent_cache: true - }, - inbounds: [ - { - type: "direct", - tag: "dns-in", - listen: "0.0.0.0", - listen_port: 6450, - override_address: "8.8.8.8", - override_port: 53 - }, - { - type: "tun", - tag: "tun-in", - address: [ - "172.18.0.1/28", - "fdfe:dcba:9876::1/126" - ], - mtu: 9000, - auto_route: true, - strict_route: true, - stack: "mixed", - sniff: true, - sniff_override_destination: true - }, - { - type: "mixed", - tag: "mixed-in", - listen: "0.0.0.0", - listen_port: 2080, - sniff: true, - sniff_override_destination: false - } - ], - outbounds: [ - { - type: "selector", - tag: "proxy", - outbounds: [] - }, - { - type: "urltest", - tag: "", - outbounds: [], - url: "https://www.gstatic.com/generate_204", - interval: "" - }, - { - type: "direct", - tag: "direct" - }, - { - type: "block", - tag: "block" - }, - { - type: "dns", - tag: "dns-out" - } - ], - route: { - rules: [], - rule_set: [], - auto_detect_interface: true, - override_android_vpn: true, - final: "proxy" - }, - ntp: { - enabled: true, - server: "time.apple.com", - server_port: 123, - detour: "direct", - interval: "30m", - }, - experimental: { - cache_file: { - enabled: true, - store_fakeip: true - }, - clash_api: { - external_controller: "127.0.0.1:9090", - external_ui: "yacd", - external_ui_download_url: "https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip", - external_ui_download_detour: "direct", - default_mode: "rule" - } - } -}; - -const clashConfigTemp = { - "ipv6": true, - "allow-lan": true, - "mode": "rule", - "log-level": "info", - "keep-alive-interval": 30, - "unified-delay": false, - "external-controller": "127.0.0.1:9090", - "external-ui-url": "https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip", - "external-ui": "ui", - "profile": { - "store-selected": true, - "store-fake-ip": true - }, - "dns": {}, - "listeners": [ - { - "name": "mixed-in", - "type": "mixed", - "port": 7892, - "listen": "0.0.0.0", - "udp": true - } - ], - "tun": { - "enable": true, - "stack": "mixed", - "auto-route": true, - "auto-redirect": true, - "auto-detect-interface": true, - "dns-hijack": [ - "any:53", - "198.18.0.2:53" - ], - "device": "utun0", - "mtu": 9000, - "strict-route": true - }, - "sniffer": { - "enable": true, - "force-dns-mapping": true, - "parse-pure-ip": true, - "sniff": { - "HTTP": { - "ports": [80, 8080, 8880, 2052, 2082, 2086, 2095], - "override-destination": false - }, - "TLS": { - "ports": [443, 8443, 2053, 2083, 2087, 2096], - "override-destination": false - } - } - }, - "proxies": [ - { - "name": "dns-out", - "type": "dns" - } - ], - "proxy-groups": [ - { - "name": "✅ Selector", - "type": "select", - "proxies": [] - }, - { - "name": "", - "type": "url-test", - "url": "https://www.gstatic.com/generate_204", - "interval": 30, - "tolerance": 50, - "proxies": [] - } - ], - "rules": [], - "ntp": { - "enable": true, - "server": "time.apple.com", - "port": 123, - "interval": 30 - } }; \ No newline at end of file