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} 💦
+
+
+
+
+
+ `;
+}
\ 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} 💦
+
+
+
+
+ `;
+}
\ 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} 💦
-
-
-
-
-
- `;
-}
-
-function renderLoginPage () {
- return `
-
-
-
-
-
- User Login
-
-
-
-
-
BPB Panel ${panelVersion} 💦
-
-
-
-
- `;
-}
-
-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