import { connect } from "cloudflare:sockets"; // ============================================================ // DEFAULTS // ============================================================ const DEFAULT_ROOT_DOMAIN = "lifetime.workers.dev"; const DEFAULT_SERVICE_NAME = "v2"; let rootDomain = DEFAULT_ROOT_DOMAIN; let serviceName = DEFAULT_SERVICE_NAME; let APP_DOMAIN = `${serviceName}.${rootDomain}`; let PORTS = [443, 80]; let PROTOCOLS = [reverse("najort"), reverse("sselv"), reverse("ss")]; let proxyIP = "https://raw.githubusercontent.com/papapapapdelesia/Emilia/refs/heads/main/Data/Country-ALIVE.txt"; let cachedProxyList = []; let KV_PROXY_URL = "https://r2.code-hub.workers.dev/raw/t93bckle47mu6f16qd"; let PROXY_BANK_URL = "https://raw.githubusercontent.com/papapapapdelesia/Emilia/refs/heads/main/Data/Country-ALIVE.txt"; let DNS_SERVER_ADDRESS = "8.8.8.8"; let DNS_SERVER_PORT = 53; let PROXY_HEALTH_CHECK_API = "https://cprx-ku6.vercel.app/api/check"; let DONATE_LINK = "https://r9.paidweh1.workers.dev/public/1776930765841_ec1ed32c-db99-4023-aede-176bf5f2b1c0.jpeg"; let PROXY_PER_PAGE = 20; // ============================================================ // CONFIG (default + KV binding "Config_KV") // ------------------------------------------------------------ // Semua nilai di bawah adalah DEFAULT. Kalau ada data tersimpan di // KV binding Config_KV (key: "config"), nilai itu yang dipakai. // Halaman /settings dipakai untuk mengubahnya lewat browser. // ============================================================ const CONFIG_KV_KEY = "config"; const DEFAULT_CONFIG = { SERVICE_NAME: DEFAULT_SERVICE_NAME, ROOT_DOMAIN: DEFAULT_ROOT_DOMAIN, PROXY_BANK_URL: PROXY_BANK_URL, KV_PROXY_URL: KV_PROXY_URL, PROXY_HEALTH_CHECK_API: PROXY_HEALTH_CHECK_API, DONATE_LINK: DONATE_LINK, REVERSE_PROXY_TARGET: "example.com", PROXY_PER_PAGE: 20, PORTS: "443,80", DNS_SERVER_ADDRESS: "8.8.8.8", DNS_SERVER_PORT: 53, PROXYIP: "", UDP_RELAY_HOST: "cfudp.up.railway.app", UDP_RELAY_PORT: 443, DOH_URL: "https://cloudflare-dns.com/dns-query", REJECT_UDP_443: "1", UUID: "", }; let CONFIG = { ...DEFAULT_CONFIG }; function configKV(env) { return (env && (env.Config_KV || env.CONFIG_KV || env.config_kv)) || null; } async function loadConfig(env) { let stored = {}; const kv = configKV(env); if (kv) { try { const raw = await kv.get(CONFIG_KV_KEY, "json"); if (raw && typeof raw === "object") stored = raw; } catch (e) { console.error("loadConfig failed:", e.message); } } // Prioritas: KV > environment variable > default const fromEnv = {}; for (const key of Object.keys(DEFAULT_CONFIG)) { if (env && env[key] !== undefined && env[key] !== "") fromEnv[key] = env[key]; } CONFIG = { ...DEFAULT_CONFIG, ...fromEnv, ...stored }; applyConfig(); return CONFIG; } async function saveConfig(env, patch) { const kv = configKV(env); if (!kv) throw new Error("KV binding Config_KV belum dipasang di worker ini"); const clean = {}; for (const key of Object.keys(DEFAULT_CONFIG)) { if (patch && patch[key] !== undefined) clean[key] = patch[key]; } const merged = { ...DEFAULT_CONFIG, ...clean }; await kv.put(CONFIG_KV_KEY, JSON.stringify(merged)); CONFIG = merged; applyConfig(); return merged; } function applyConfig() { PROXY_BANK_URL = CONFIG.PROXY_BANK_URL || DEFAULT_CONFIG.PROXY_BANK_URL; KV_PROXY_URL = CONFIG.KV_PROXY_URL || ""; PROXY_HEALTH_CHECK_API = CONFIG.PROXY_HEALTH_CHECK_API || DEFAULT_CONFIG.PROXY_HEALTH_CHECK_API; DONATE_LINK = CONFIG.DONATE_LINK || DEFAULT_CONFIG.DONATE_LINK; DNS_SERVER_ADDRESS = CONFIG.DNS_SERVER_ADDRESS || "8.8.8.8"; DNS_SERVER_PORT = parseInt(CONFIG.DNS_SERVER_PORT, 10) || 53; PROXY_PER_PAGE = parseInt(CONFIG.PROXY_PER_PAGE, 10) || 20; if (CONFIG.PROXYIP) proxyIP = CONFIG.PROXYIP; PORTS = String(CONFIG.PORTS || "443,80") .split(",") .map((x) => parseInt(x.trim(), 10)) .filter((x) => x > 0); if (!PORTS.length) PORTS = [443, 80]; } // Konfigurasi UDP relay (dipakai modul relay hasil porting dari VLESS_CF). function relayConfig() { const port = parseInt(CONFIG.UDP_RELAY_PORT, 10); return { dohUrl: String(CONFIG.DOH_URL || DEFAULT_CONFIG.DOH_URL), dohTimeoutMs: 5000, relayHost: String(CONFIG.UDP_RELAY_HOST || "").trim(), relayPort: Number.isFinite(port) && port > 0 ? port : 443, rejectUdp443: /^(?:1|true|yes|on)$/i.test(String(CONFIG.REJECT_UDP_443 ?? "1").trim()), relayHandshakeTimeoutMs: 10000, }; } const WS_READY_STATE_OPEN = 1; const WS_READY_STATE_CLOSING = 2; const CORS_HEADER_OPTIONS = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS", "Access-Control-Allow-Headers": "*", "Access-Control-Max-Age": "86400", }; // ============================================================ // UDP RELAY MODULE (porting 1:1 dari VLESS_CF_QUIC_FILTERED.js) // ============================================================ const ATYP_IPV4 = 0x01; const ATYP_DOMAIN = 0x02; const ATYP_IPV6 = 0x03; const WS_OPEN = 1; const WS_CLOSING = 2; // Protokol handshake ke relay UDP (kompatibel dengan relay VLRLY004). const RELAY_MAGIC = new TextEncoder().encode('VLRLY004'); const RELAY_MODE_FIXED_UDP = 0x01; // satu endpoint tetap (VLESS UDP klasik) const RELAY_MODE_MUX = 0x02; // Mux.Cool / XUDP (VLESS modern) const RELAY_MODE_PACKET_UDP = 0x03; // multi endpoint per paket (Trojan UDP) // ============================================================================== // HELPER BINER // ============================================================================== function toU8(value) { if (value instanceof Uint8Array) return value; if (value instanceof ArrayBuffer) return new Uint8Array(value); if (ArrayBuffer.isView(value)) return new Uint8Array(value.buffer, value.byteOffset, value.byteLength); throw new TypeError('expected binary data'); } function concatBuffer(...parts) { const list = parts.filter((p) => p && p.byteLength !== 0).map(toU8); const total = list.reduce((n, p) => n + p.byteLength, 0); const out = new Uint8Array(total); let off = 0; for (const p of list) { out.set(p, off); off += p.byteLength; } return out; } function uint16be(value) { return new Uint8Array([(value >>> 8) & 0xff, value & 0xff]); } function utf8(text) { return new TextEncoder().encode(String(text)); } // Kenali QUIC Initial dari header, bukan hanya dari nomor port. Dengan begitu // STUN/TURN/WebRTC yang sah pada UDP/443 tidak ikut terblokir. function isQuicInitial(payload) { const b = toU8(payload); if (b.byteLength < 7 || (b[0] & 0xc0) !== 0xc0) return false; const version = ((b[1] * 0x1000000) + (b[2] << 16) + (b[3] << 8) + b[4]) >>> 0; if (version === 0) return false; // QUIC v1 memakai type 0 untuk Initial; QUIC v2 memakai type 1. const packetType = (b[0] >>> 4) & 0x03; const isV1Initial = version === 0x00000001 && packetType === 0; const isV2Initial = version === 0x6b3343cf && packetType === 1; if (!isV1Initial && !isV2Initial) return false; const dcidLength = b[5]; if (dcidLength > 20 || b.byteLength < 7 + dcidLength) return false; const scidLength = b[6 + dcidLength]; return scidLength <= 20 && b.byteLength >= 7 + dcidLength + scidLength; } function shouldDropQuic(cfg, endpoint, payload) { return Boolean(cfg.rejectUdp443 && Number(endpoint && endpoint.port) === 443 && isQuicInitial(payload)); } function bytesToUuid(buf, offset) { const b = buf.slice(offset, offset + 16); const hex = [...b].map((x) => x.toString(16).padStart(2, '0')); return `${hex[0]}${hex[1]}${hex[2]}${hex[3]}-${hex[4]}${hex[5]}-${hex[6]}${hex[7]}-${hex[8]}${hex[9]}-${hex[10]}${hex[11]}${hex[12]}${hex[13]}${hex[14]}${hex[15]}`; } function inferAddressType(address) { if (/^(?:\d{1,3}\.){3}\d{1,3}$/.test(String(address))) return ATYP_IPV4; if (String(address).includes(':')) return ATYP_IPV6; return ATYP_DOMAIN; } function ipv4ToBytes(address) { return new Uint8Array(String(address).split('.').map(Number)); } function ipv6ToBytes(address) { let input = String(address).split('%')[0].toLowerCase(); const lastColon = input.lastIndexOf(':'); if (input.includes('.') && lastColon >= 0) { const v4 = ipv4ToBytes(input.slice(lastColon + 1)); input = input.slice(0, lastColon) + ':' + ((v4[0] << 8) | v4[1]).toString(16) + ':' + ((v4[2] << 8) | v4[3]).toString(16); } const halves = input.split('::'); const left = halves[0] ? halves[0].split(':').filter(Boolean) : []; const right = halves[1] ? halves[1].split(':').filter(Boolean) : []; const words = [...left, ...Array(Math.max(0, 8 - left.length - right.length)).fill('0'), ...right]; const out = new Uint8Array(16); words.slice(0, 8).forEach((w, i) => { const n = parseInt(w || '0', 16) & 0xffff; out[i * 2] = (n >> 8) & 0xff; out[i * 2 + 1] = n & 0xff; }); return out; } function ipv6FromBytes(bytes) { const b = toU8(bytes); const words = []; for (let i = 0; i < 16; i += 2) words.push(((b[i] << 8) | b[i + 1]).toString(16)); return words.join(':'); } // Endpoint format relay: [port:2][atyp:1][address] function encodeRelayEndpoint(endpoint) { const type = endpoint.addressType || inferAddressType(endpoint.address); const head = uint16be(Number(endpoint.port)); if (type === ATYP_IPV4) return concatBuffer(head, new Uint8Array([ATYP_IPV4]), ipv4ToBytes(endpoint.address)); if (type === ATYP_IPV6) return concatBuffer(head, new Uint8Array([ATYP_IPV6]), ipv6ToBytes(endpoint.address)); const d = utf8(endpoint.address); return concatBuffer(head, new Uint8Array([ATYP_DOMAIN, d.byteLength]), d); } function parseRelayEndpoint(bytes, offset = 0) { const b = toU8(bytes); if (b.byteLength < offset + 3) return null; const port = (b[offset] << 8) | b[offset + 1]; const atyp = b[offset + 2]; let p = offset + 3; if (!port) throw new Error('zero endpoint port'); if (atyp === ATYP_IPV4) { if (b.byteLength < p + 4) return null; return { address: Array.from(b.subarray(p, p + 4)).join('.'), port, addressType: atyp, next: p + 4 }; } if (atyp === ATYP_DOMAIN) { if (b.byteLength < p + 1) return null; const len = b[p++]; if (!len || b.byteLength < p + len) return null; return { address: new TextDecoder().decode(b.subarray(p, p + len)), port, addressType: atyp, next: p + len }; } if (atyp === ATYP_IPV6) { if (b.byteLength < p + 16) return null; return { address: ipv6FromBytes(b.subarray(p, p + 16)), port, addressType: atyp, next: p + 16 }; } throw new Error('invalid endpoint address type ' + atyp); } // Alamat gaya SOCKS (dipakai frame UDP Trojan): [atyp:1][address][port:2] function encodeSocksAddress(endpoint) { const type = endpoint.addressType || inferAddressType(endpoint.address); let addr; if (type === ATYP_IPV4) addr = concatBuffer(new Uint8Array([0x01]), ipv4ToBytes(endpoint.address)); else if (type === ATYP_IPV6) addr = concatBuffer(new Uint8Array([0x04]), ipv6ToBytes(endpoint.address)); else { const d = utf8(endpoint.address); addr = concatBuffer(new Uint8Array([0x03, d.byteLength]), d); } return concatBuffer(addr, uint16be(Number(endpoint.port))); } function tryParseSocksAddress(bytes, offset = 0) { const b = toU8(bytes); if (b.byteLength < offset + 1) return null; const type = b[offset++]; let address, addressType; if (type === 0x01) { if (b.byteLength < offset + 6) return null; address = Array.from(b.subarray(offset, offset + 4)).join('.'); addressType = ATYP_IPV4; offset += 4; } else if (type === 0x03) { if (b.byteLength < offset + 1) return null; const n = b[offset++]; if (!n || b.byteLength < offset + n + 2) return null; address = new TextDecoder().decode(b.subarray(offset, offset + n)); addressType = ATYP_DOMAIN; offset += n; } else if (type === 0x04) { if (b.byteLength < offset + 18) return null; address = ipv6FromBytes(b.subarray(offset, offset + 16)); addressType = ATYP_IPV6; offset += 16; } else { throw new Error('invalid SOCKS address type ' + type); } const port = (b[offset] << 8) | b[offset + 1]; return { address, port, addressType, next: offset + 2 }; } // Frame "[2 byte panjang][payload]" berulang (VLESS UDP & relay fixed mode). class LengthPrefixedDecoder { constructor() { this.pending = new Uint8Array(0); } push(chunk) { const input = toU8(chunk); if (input.byteLength) this.pending = concatBuffer(this.pending, input); const out = []; for (;;) { if (this.pending.byteLength < 2) break; const len = (this.pending[0] << 8) | this.pending[1]; if (this.pending.byteLength < 2 + len) break; out.push(this.pending.slice(2, 2 + len)); this.pending = this.pending.slice(2 + len); } return out; } } // Frame UDP Trojan: [addr SOCKS][len:2][CRLF][payload] class TrojanUdpDecoder { constructor() { this.pending = new Uint8Array(0); } push(chunk) { this.pending = concatBuffer(this.pending, chunk); const out = []; while (this.pending.byteLength) { const ep = tryParseSocksAddress(this.pending, 0); if (!ep || this.pending.byteLength < ep.next + 4) break; const len = (this.pending[ep.next] << 8) | this.pending[ep.next + 1]; const end = ep.next + 4 + len; if (this.pending.byteLength < end) break; out.push({ endpoint: { address: ep.address, port: ep.port, addressType: ep.addressType }, payload: this.pending.slice(ep.next + 4, end), }); this.pending = this.pending.slice(end); } return out; } } function encodeTrojanUdpPacket(endpoint, payload) { const p = toU8(payload); return concatBuffer( encodeSocksAddress(endpoint), new Uint8Array([(p.byteLength >>> 8) & 0xff, p.byteLength & 0xff, 0x0d, 0x0a]), p ); } // ============================================================================== // DNS (UDP port 53) LEWAT DNS-OVER-HTTPS // ============================================================================== async function resolveDnsOverHttps(cfg, payload) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), cfg.dohTimeoutMs); try { const response = await fetch(cfg.dohUrl, { method: 'POST', headers: { 'Content-Type': 'application/dns-message', Accept: 'application/dns-message' }, body: toU8(payload), signal: controller.signal, }); if (!response.ok) throw new Error('HTTP ' + response.status); return new Uint8Array(await response.arrayBuffer()); } finally { clearTimeout(timer); } } // Endpoint DNS tetap, framing panjang 2 byte (dipakai VLESS UDP port 53). class FixedDnsOverHttps { constructor(cfg, onPacket) { this.cfg = cfg; this.decoder = new LengthPrefixedDecoder(); this.onPacket = onPacket; } async writeRaw(bytes) { for (const packet of this.decoder.push(bytes)) { try { const answer = await resolveDnsOverHttps(this.cfg, packet); this.onPacket(answer); } catch (_) { /* satu query gagal, sesi tetap jalan */ } } } close() {} } // ============================================================================== // UDP RELAY (WebSocket ke relay eksternal) โ€” diadaptasi dari test.js // ------------------------------------------------------------------------------ // Workers tidak punya raw UDP socket, jadi paket UDP selain DNS dititipkan ke // relay WebSocket. Dua mode: // FIXED -> satu endpoint tetap; frame "[len:2][payload]" // MUX -> Mux.Cool/XUDP diteruskan apa adanya // PACKET -> multi endpoint; frame "[endpoint][len:2][payload]" // ============================================================================== class FilteredFixedUdpRelay { constructor(cfg, endpoint, relay) { this.cfg = cfg; this.endpoint = endpoint; this.relay = relay; this.decoder = new LengthPrefixedDecoder(); } async writeRaw(bytes) { for (const packet of this.decoder.push(bytes)) { if (!shouldDropQuic(this.cfg, this.endpoint, packet)) this.relay.writeFixedPacket(packet); } } close() { this.relay.close(); } } class RelayConnection { constructor(ws, mode) { this.ws = ws; this.mode = mode; this.closed = false; } static async open(cfg, mode, endpoint = null) { const host = String(cfg.relayHost || '').trim(); if (!host) throw new Error('UDP relay belum dikonfigurasi (set UDP_RELAY_HOST)'); // UDP_RELAY_HOST boleh berupa host saja atau URL lengkap beserta path. let relayUrl; try { relayUrl = new URL(/^[a-z]+:\/\//i.test(host) ? host : `https://${host}`); } catch (_) { throw new Error('UDP_RELAY_HOST tidak valid'); } relayUrl.protocol = relayUrl.protocol === 'ws:' ? 'http:' : 'https:'; if (!relayUrl.port && cfg.relayPort !== 443) relayUrl.port = String(cfg.relayPort); if (!relayUrl.pathname) relayUrl.pathname = '/'; const resp = await fetch(relayUrl.toString(), { headers: { Upgrade: 'websocket' } }); const ws = resp.webSocket; if (!ws) throw new Error('Gagal upgrade WebSocket ke UDP relay'); ws.accept(); const relay = new RelayConnection(ws, mode); const parts = [RELAY_MAGIC, new Uint8Array([mode])]; if (mode === RELAY_MODE_FIXED_UDP) parts.push(encodeRelayEndpoint(endpoint)); ws.send(concatBuffer(...parts)); // Relay membalas [0] jika siap, atau [1][len:2][pesan] jika gagal. await new Promise((resolve, reject) => { let settled = false; let timer; const finish = (error) => { if (settled) return; settled = true; if (timer) clearTimeout(timer); ws.removeEventListener('message', onFirstMessage); ws.removeEventListener('error', onError); ws.removeEventListener('close', onClose); if (error) reject(error); else resolve(); }; const onFirstMessage = (event) => { let b; try { b = toU8(event.data); } catch (_) { b = new Uint8Array(0); } if (b.byteLength > 0 && b[0] === 0) return finish(); let detail = 'Relay menolak handshake'; if (b.byteLength >= 3 && b[0] === 1) { const n = (b[1] << 8) | b[2]; if (b.byteLength >= 3 + n) detail += ': ' + new TextDecoder().decode(b.subarray(3, 3 + n)); } finish(new Error(detail)); }; const onError = () => finish(new Error('Relay error')); const onClose = () => finish(new Error('Relay tertutup lebih awal')); timer = setTimeout(() => finish(new Error('Handshake relay timeout')), cfg.relayHandshakeTimeoutMs); ws.addEventListener('message', onFirstMessage); ws.addEventListener('error', onError); ws.addEventListener('close', onClose); }); return relay; } static openFixedUDP(cfg, ep) { return RelayConnection.open(cfg, RELAY_MODE_FIXED_UDP, ep); } static openMuxUDP(cfg) { return RelayConnection.open(cfg, RELAY_MODE_MUX); } static openPacketUDP(cfg) { return RelayConnection.open(cfg, RELAY_MODE_PACKET_UDP); } writeFixedPacket(payload) { if (this.closed || this.ws.readyState !== WS_OPEN) return; this.ws.send(concatBuffer(uint16be(payload.byteLength), payload)); } writePacket(endpoint, payload) { if (this.closed || this.ws.readyState !== WS_OPEN) return; this.ws.send(concatBuffer(encodeRelayEndpoint(endpoint), uint16be(payload.byteLength), payload)); } writeRaw(bytes) { if (this.closed || this.ws.readyState !== WS_OPEN) return; this.ws.send(toU8(bytes)); } pumpFixedUDP(onPacket, onClose) { const decoder = new LengthPrefixedDecoder(); this.ws.addEventListener('message', (event) => { try { for (const pkt of decoder.push(toU8(event.data))) onPacket(pkt); } catch (_) {} }); this.ws.addEventListener('close', () => { this.close(); onClose(); }); this.ws.addEventListener('error', () => { this.close(); onClose(); }); } pumpRaw(onData, onClose) { this.ws.addEventListener('message', (event) => { try { onData(toU8(event.data)); } catch (_) {} }); this.ws.addEventListener('close', () => { this.close(); onClose(); }); this.ws.addEventListener('error', () => { this.close(); onClose(); }); } pumpPacketUDP(onPacket, onClose) { let pending = new Uint8Array(0); this.ws.addEventListener('message', (event) => { try { pending = concatBuffer(pending, toU8(event.data)); while (pending.byteLength) { const ep = parseRelayEndpoint(pending, 0); if (!ep || pending.byteLength < ep.next + 2) break; const len = (pending[ep.next] << 8) | pending[ep.next + 1]; const end = ep.next + 2 + len; if (pending.byteLength < end) break; onPacket(ep, pending.slice(ep.next + 2, end)); pending = pending.slice(end); } } catch (_) { pending = new Uint8Array(0); } }); this.ws.addEventListener('close', () => { this.close(); onClose(); }); this.ws.addEventListener('error', () => { this.close(); onClose(); }); } close() { if (this.closed) return; this.closed = true; try { if (this.ws.readyState === WS_OPEN || this.ws.readyState === WS_CLOSING) this.ws.close(); } catch (_) {} } } // ============================================================ // HOST RESOLVER // ============================================================ function resolveHost(hostHeader, env) { const host = (hostHeader || "").split(":")[0].toLowerCase(); if (env?.SERVICE_NAME && env?.ROOT_DOMAIN) { return { serviceName: env.SERVICE_NAME, rootDomain: env.ROOT_DOMAIN, source: "env" }; } if (env?.CUSTOM_DOMAINS) { try { const map = JSON.parse(env.CUSTOM_DOMAINS); if (map[host] && map[host].service && map[host].root) { return { serviceName: map[host].service, rootDomain: map[host].root, source: "custom-map" }; } } catch (e) { console.error("CUSTOM_DOMAINS parse failed:", e.message); } } if (host.endsWith(".workers.dev")) { const before = host.slice(0, -".workers.dev".length); const parts = before.split("."); if (parts.length >= 2) { return { serviceName: parts[0], rootDomain: parts.slice(1).join(".") + ".workers.dev", source: "workers.dev" }; } return { serviceName: parts[0] || DEFAULT_SERVICE_NAME, rootDomain: "workers.dev", source: "workers.dev-single" }; } const parts = host.split("."); if (parts.length >= 3) { return { serviceName: parts[0], rootDomain: parts.slice(1).join("."), source: "generic-sub" }; } if (parts.length === 2) { return { serviceName: parts[0], rootDomain: parts[1], source: "generic-apex" }; } return { serviceName: DEFAULT_SERVICE_NAME, rootDomain: DEFAULT_ROOT_DOMAIN, source: "fallback" }; } function applyHost(hostHeader, env) { const r = resolveHost(hostHeader, env); serviceName = r.serviceName; rootDomain = r.rootDomain; APP_DOMAIN = `${serviceName}.${rootDomain}`; console.log(`[HOST] ${hostHeader} โ†’ svc=${serviceName} root=${rootDomain} src=${r.source}`); return r; } function rebuildConstants() { if (!Array.isArray(PORTS) || !PORTS.length) PORTS = [443, 80]; PROTOCOLS = [reverse("najort"), reverse("sselv"), reverse("ss")]; } // ============================================================ // PROXY LIST // ============================================================ async function getKVProxyList(kvProxyUrl = KV_PROXY_URL) { if (!kvProxyUrl) throw new Error("No KV Proxy URL Provided!"); try { const kvProxy = await fetch(kvProxyUrl, { cf: { cacheTtl: 300, cacheEverything: true } }); if (kvProxy.status === 200) return await kvProxy.json(); } catch (e) { console.error("getKVProxyList failed:", e.message); } return {}; } async function getProxyList(proxyBankUrl = PROXY_BANK_URL) { const url = proxyBankUrl || PROXY_BANK_URL; if (!url) throw new Error("No Proxy Bank URL Provided!"); try { const proxyBank = await fetch(url, { cf: { cacheTtl: 120, cacheEverything: true } }); if (proxyBank.status === 200) { const text = (await proxyBank.text()) || ""; const proxyString = text.split("\n").filter(Boolean); cachedProxyList = proxyString .map((entry) => { const [proxyIPx, proxyPort, country, org] = entry.split(","); return { proxyIP: (proxyIPx || "").trim(), proxyPort: (proxyPort || "").trim(), country: (country || "XX").trim(), org: (org || "Unknown Org").trim(), }; }) .filter((p) => p.proxyIP && p.proxyPort && /^\d+$/.test(p.proxyPort)); } } catch (e) { console.error("getProxyList failed:", e.message); } return cachedProxyList; } // ============================================================ // REVERSE PROXY // ============================================================ async function reverseProxy(request, target, targetPath) { const targetUrl = new URL(request.url); const targetChunk = target.split(":"); targetUrl.hostname = targetChunk[0]; targetUrl.port = targetChunk[1]?.toString() || "443"; targetUrl.pathname = targetPath || targetUrl.pathname; const modifiedRequest = new Request(targetUrl, request); modifiedRequest.headers.set("X-Forwarded-Host", request.headers.get("Host")); const response = await fetch(modifiedRequest); const newResponse = new Response(response.body, response); for (const [key, value] of Object.entries(CORS_HEADER_OPTIONS)) { newResponse.headers.set(key, value); } newResponse.headers.set("X-Proxied-By", "Cloudflare Worker"); return newResponse; } // ============================================================ // CONFIG BUILDER // ============================================================ function getAllConfig(request, hostName, proxyList, page = 0) { const startIndex = PROXY_PER_PAGE * page; try { const uuid = crypto.randomUUID(); const uri = new URL(`${reverse("najort")}://${hostName}`); uri.searchParams.set("encryption", "none"); uri.searchParams.set("type", "ws"); uri.searchParams.set("host", hostName); const document = new Document(request); document.setTitle("Welcome to Kuncung Proxy "); document.addInfo(`Total: ${proxyList.length}`); document.addInfo(`Page: ${page}/${Math.floor(proxyList.length / PROXY_PER_PAGE)}`); for (let i = startIndex; i < startIndex + PROXY_PER_PAGE; i++) { const proxy = proxyList[i]; if (!proxy) break; const { proxyIP: pIP, proxyPort, country, org } = proxy; uri.searchParams.set("path", `/${pIP}-${proxyPort}`); const proxies = []; for (const port of PORTS) { uri.port = port.toString(); uri.hash = `${i + 1} ${getFlagEmoji(country)} ${org} WS ${port == 443 ? "TLS" : "NTLS"} [${serviceName}]`; for (const protocol of PROTOCOLS) { if (protocol === "ss") { uri.username = btoa(`none:${uuid}`); uri.searchParams.set( "plugin", `v2ray-plugin${port == 80 ? "" : ";tls"};mux=0;mode=websocket;path=/${pIP}-${proxyPort};host=${hostName}` ); } else { uri.username = uuid; uri.searchParams.delete("plugin"); } uri.protocol = protocol; uri.searchParams.set("security", port == 443 ? "tls" : "none"); uri.searchParams.set("sni", port == 80 && protocol == reverse("sselv") ? "" : hostName); proxies.push(uri.toString()); } } document.registerProxies({ proxyIP: pIP, proxyPort, country, org }, proxies); } document.addPageButton("Prev", `/sub/${page > 0 ? page - 1 : 0}`, page > 0 ? false : true); document.addPageButton("Next", `/sub/${page + 1}`, page < Math.floor(proxyList.length / 10) ? false : true); return document.build(); } catch (error) { return `An error occurred while generating the ${reverse("SSELV")} configurations. ${error}`; } } // ============================================================ // MAIN FETCH HANDLER // ============================================================ export default { async fetch(request, env, ctx) { try { env = env || {}; await loadConfig(env); if (!env.PROXY_BANK_URL) env.PROXY_BANK_URL = PROXY_BANK_URL; const url = new URL(request.url); // Host tetap terdeteksi otomatis. Nilai di /settings hanya dipakai kalau // sudah diubah dari default (biar satu worker bisa dibuka dari domain apa pun). const svcOverride = CONFIG.SERVICE_NAME !== DEFAULT_SERVICE_NAME ? CONFIG.SERVICE_NAME : env.SERVICE_NAME; const rootOverride = CONFIG.ROOT_DOMAIN !== DEFAULT_ROOT_DOMAIN ? CONFIG.ROOT_DOMAIN : env.ROOT_DOMAIN; applyHost(request.headers.get("Host"), { ...env, SERVICE_NAME: svcOverride, ROOT_DOMAIN: rootOverride }); rebuildConstants(); const upgradeHeader = request.headers.get("Upgrade"); if (request.method === "OPTIONS") { return new Response(null, { status: 204, headers: CORS_HEADER_OPTIONS }); } if (url.pathname.startsWith("/check")) { const targetRaw = url.searchParams.get("target") || ""; const [ip, port] = targetRaw.split(":"); if (!ip) { return new Response(JSON.stringify({ proxyip: false, error: "missing target" }), { status: 400, headers: { ...CORS_HEADER_OPTIONS, "Content-Type": "application/json" }, }); } const result = await checkProxyHealth(ip, port || "443"); return new Response(JSON.stringify(result), { status: 200, headers: { ...CORS_HEADER_OPTIONS, "Content-Type": "application/json" }, }); } // ---- /api/v1/myip (PRIORITAS sebelum fallback) ---- if (url.pathname === "/api/v1/myip" || url.pathname === "/api/v1/myip/") { try { const clientIP = request.headers.get("cf-connecting-ip") || request.headers.get("cf-connecting-ipv6") || request.headers.get("x-real-ip") || request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "unknown"; const cf = request.cf || {}; let geo = { country: cf.country || null, city: cf.city || null, region: cf.region || null, asOrganization: cf.asOrganization || null, asn: cf.asn || null, colo: cf.colo || request.headers.get("cf-ray")?.split("-")[1] || null, }; if (!geo.country || !geo.asOrganization) { try { const geoRes = await fetch(`https://ipapi.co/${clientIP}/json/`, { headers: { "User-Agent": "cf-worker" }, }); if (geoRes.ok) { const g = await geoRes.json(); geo.country = geo.country || g.country_name || g.country || null; geo.city = geo.city || g.city || null; geo.region = geo.region || g.region || null; geo.asOrganization = geo.asOrganization || g.org || null; geo.asn = geo.asn || g.asn || null; } } catch (e) { console.error("geoip fallback failed:", e.message); } } return new Response( JSON.stringify({ ip: clientIP, ...geo }), { headers: { ...CORS_HEADER_OPTIONS, "Content-Type": "application/json" } } ); } catch (e) { return new Response(JSON.stringify({ ip: "unknown", error: e.message }), { status: 500, headers: { ...CORS_HEADER_OPTIONS, "Content-Type": "application/json" }, }); } } // ---- PROXY CLIENT ROUTING ---- if (upgradeHeader === "websocket") { const path = url.pathname; const proxyListMatch = path.match(/^\/PROXYLIST\/([A-Z]{2}(,[A-Z]{2})*)$/i); if (proxyListMatch) { const countryCodes = proxyListMatch[1].toUpperCase().split(","); const proxies = await getProxyList(env.PROXY_BANK_URL); const grouped = proxies.reduce((acc, proxy) => { if (countryCodes.includes(proxy.country)) { if (!acc[proxy.country]) acc[proxy.country] = []; acc[proxy.country].push(proxy); } return acc; }, {}); const selectedProxies = countryCodes .map((code) => { const list = grouped[code]; if (list && list.length > 0) return list[Math.floor(Math.random() * list.length)]; return null; }) .filter(Boolean); if (selectedProxies.length === 0) { return new Response(`No proxies available for requested countries: ${countryCodes.join(",")}`, { status: 404 }); } const randomProxy = selectedProxies[Math.floor(Math.random() * selectedProxies.length)]; proxyIP = `${randomProxy.proxyIP}:${randomProxy.proxyPort}`; console.log(`Selected Proxy (/PROXYLIST): ${proxyIP}`); return await websocketHandler(request); } const allMatch = path.match(/^\/ALL(\d+)?$/i); if (allMatch) { const index = allMatch[1] ? parseInt(allMatch[1], 10) - 1 : null; const proxies = await getProxyList(env.PROXY_BANK_URL); let selectedProxies; if (index === null) { selectedProxies = proxies; } else { const grouped = proxies.reduce((acc, proxy) => { if (!acc[proxy.country]) acc[proxy.country] = []; acc[proxy.country].push(proxy); return acc; }, {}); selectedProxies = []; for (const country in grouped) { const list = grouped[country]; if (index < list.length) selectedProxies.push(list[index]); } } if (selectedProxies.length === 0) { return new Response(`No proxies available for /ALL${index !== null ? index + 1 : ""}`, { status: 404 }); } const randomProxy = selectedProxies[Math.floor(Math.random() * selectedProxies.length)]; proxyIP = `${randomProxy.proxyIP}:${randomProxy.proxyPort}`; console.log(`Selected Proxy (/ALL): ${proxyIP}`); return await websocketHandler(request); } const putarMatch = path.match(/^\/PUTAR(\d+)?$/i); if (putarMatch) { const count = putarMatch[1] ? parseInt(putarMatch[1], 10) : 1; const proxies = await getProxyList(env.PROXY_BANK_URL); const grouped = proxies.reduce((acc, proxy) => { if (!acc[proxy.country]) acc[proxy.country] = []; acc[proxy.country].push(proxy); return acc; }, {}); const countries = Object.keys(grouped); if (countries.length === 0) { return new Response(`No proxies available for /PUTAR${count > 1 ? count : ""}`, { status: 404 }); } const shuffledCountries = countries.sort(() => Math.random() - 0.5); const selectedCountries = shuffledCountries.slice(0, count); const selectedProxies = selectedCountries.map((country) => { const list = grouped[country]; return list[Math.floor(Math.random() * list.length)]; }); const randomProxy = selectedProxies[Math.floor(Math.random() * selectedProxies.length)]; proxyIP = `${randomProxy.proxyIP}:${randomProxy.proxyPort}`; console.log(`Selected Proxy (/PUTAR${count}): ${proxyIP}`); return await websocketHandler(request); } const regionMap = { ASIA: ["ID", "SG", "MY", "PH", "TH", "VN", "JP", "KR", "CN", "HK", "TW"], SOUTHASIA: ["IN", "BD", "PK", "LK", "NP", "AF", "BT", "MV"], CENTRALASIA: ["KZ", "UZ", "TM", "KG", "TJ"], NORTHASIA: ["RU"], MIDDLEEAST: ["AE", "SA", "IR", "IQ", "JO", "IL", "YE", "SY", "OM", "KW", "QA", "BH", "LB"], CIS: ["RU", "UA", "BY", "KZ", "UZ", "AM", "GE", "MD", "TJ", "KG", "TM", "AZ"], WESTEUROPE: ["FR", "DE", "NL", "BE", "AT", "CH", "IE", "LU", "MC"], EASTEUROPE: ["PL", "CZ", "SK", "HU", "RO", "BG", "MD", "UA", "BY"], NORTHEUROPE: ["SE", "FI", "NO", "DK", "EE", "LV", "LT", "IS"], SOUTHEUROPE: ["IT", "ES", "PT", "GR", "HR", "SI", "MT", "AL", "BA", "RS", "ME", "MK"], EUROPE: ["FR", "DE", "NL", "BE", "AT", "CH", "IE", "LU", "MC", "PL", "CZ", "SK", "HU", "RO", "BG", "MD", "UA", "BY", "SE", "FI", "NO", "DK", "EE", "LV", "LT", "IS", "IT", "ES", "PT", "GR", "HR", "SI", "MT", "AL", "BA", "RS", "ME", "MK"], AFRICA: ["ZA", "NG", "EG", "MA", "KE", "DZ", "TN", "GH", "CI", "SN", "ET"], NORTHAMERICA: ["US", "CA", "MX"], SOUTHAMERICA: ["BR", "AR", "CL", "CO", "PE", "VE", "EC", "UY", "PY", "BO"], LATAM: ["MX", "BR", "AR", "CL", "CO", "PE", "VE", "EC", "UY", "PY", "BO", "CR", "GT", "PA", "DO", "HN", "NI", "SV"], AMERICA: ["US", "CA", "MX", "BR", "AR", "CL", "CO", "PE", "VE", "EC"], OCEANIA: ["AU", "NZ", "PG", "FJ"], GLOBAL: [], }; const regionMatch = path.match(/^\/([A-Z]+)(\d+)?$/i); if (regionMatch && regionMap[regionMatch[1].toUpperCase()]) { const regionKey = regionMatch[1].toUpperCase(); const index = regionMatch[2] ? parseInt(regionMatch[2], 10) - 1 : null; const countries = regionMap[regionKey]; const proxies = await getProxyList(env.PROXY_BANK_URL); const filteredProxies = regionKey === "GLOBAL" ? proxies : proxies.filter((p) => countries.includes(p.country)); if (filteredProxies.length === 0) { return new Response(`No proxies available for region: ${regionKey}`, { status: 404 }); } let selectedProxy; if (index === null) { selectedProxy = filteredProxies[Math.floor(Math.random() * filteredProxies.length)]; } else if (index < 0 || index >= filteredProxies.length) { return new Response(`Index ${index + 1} out of bounds.`, { status: 400 }); } else { selectedProxy = filteredProxies[index]; } proxyIP = `${selectedProxy.proxyIP}:${selectedProxy.proxyPort}`; console.log(`Selected Proxy (/${regionKey}${index !== null ? index + 1 : ""}): ${proxyIP}`); return await websocketHandler(request); } const pathMatch = path.match(/^\/([A-Z]{2})(\d+)?$/); if (pathMatch) { const countryCode = pathMatch[1]; const index = pathMatch[2] ? parseInt(pathMatch[2], 10) - 1 : null; const proxies = await getProxyList(env.PROXY_BANK_URL); const filteredProxies = proxies.filter((proxy) => proxy.country === countryCode); if (filteredProxies.length === 0) { return new Response(`No proxies available for country: ${countryCode}`, { status: 404 }); } let selectedProxy; if (index === null) { selectedProxy = filteredProxies[0]; } else if (index < 0 || index >= filteredProxies.length) { return new Response(`Index ${index + 1} out of bounds.`, { status: 400 }); } else { selectedProxy = filteredProxies[index]; } proxyIP = `${selectedProxy.proxyIP}:${selectedProxy.proxyPort}`; console.log(`Selected Proxy (/CC): ${proxyIP}`); return await websocketHandler(request); } const ipPortMatch = path.match(/^\/(.+[:=-]\d+)$/); if (ipPortMatch) { proxyIP = ipPortMatch[1].replace(/[=:-]/, ":"); console.log(`Direct Proxy IP: ${proxyIP}`); return await websocketHandler(request); } } // ---- /settings (halaman pengaturan) ---- if (url.pathname === "/settings" || url.pathname === "/settings/") { return new Response(getSettingsHTML(CONFIG, Boolean(configKV(env))), { status: 200, headers: { "Content-Type": "text/html;charset=utf-8" }, }); } // ---- /api/v1/settings (baca & simpan ke Config_KV) ---- if (url.pathname === "/api/v1/settings" || url.pathname === "/api/v1/settings/") { if (request.method === "GET") { return new Response( JSON.stringify({ config: CONFIG, defaults: DEFAULT_CONFIG, kvBound: Boolean(configKV(env)) }), { headers: { ...CORS_HEADER_OPTIONS, "Content-Type": "application/json" } } ); } if (request.method === "POST" || request.method === "PUT") { try { const body = await request.json(); const saved = await saveConfig(env, body); return new Response(JSON.stringify({ ok: true, config: saved }), { headers: { ...CORS_HEADER_OPTIONS, "Content-Type": "application/json" }, }); } catch (e) { return new Response(JSON.stringify({ ok: false, error: e.message }), { status: 400, headers: { ...CORS_HEADER_OPTIONS, "Content-Type": "application/json" }, }); } } if (request.method === "DELETE") { try { const saved = await saveConfig(env, {}); return new Response(JSON.stringify({ ok: true, config: saved }), { headers: { ...CORS_HEADER_OPTIONS, "Content-Type": "application/json" }, }); } catch (e) { return new Response(JSON.stringify({ ok: false, error: e.message }), { status: 400, headers: { ...CORS_HEADER_OPTIONS, "Content-Type": "application/json" }, }); } } return new Response("Method not allowed", { status: 405, headers: { ...CORS_HEADER_OPTIONS } }); } // ---- /sub ---- if (url.pathname.startsWith("/sub")) { const page = url.pathname.match(/^\/sub\/(\d+)$/); const pageIndex = parseInt(page ? page[1] : "0"); const hostname = request.headers.get("Host"); const countrySelect = url.searchParams.get("cc")?.split(","); const proxyBankUrl = url.searchParams.get("proxy-list") || env.PROXY_BANK_URL; let proxyList = (await getProxyList(proxyBankUrl)).filter((proxy) => { if (countrySelect) return countrySelect.includes(proxy.country); return true; }); const result = getAllConfig(request, hostname, proxyList, pageIndex); return new Response(result, { status: 200, headers: { "Content-Type": "text/html;charset=utf-8" }, }); } // ---- /api/v1 (selain myip) ---- if (url.pathname.startsWith("/api/v1")) { const apiPath = url.pathname.replace("/api/v1", ""); if (apiPath.startsWith("/sub")) { const filterCC = url.searchParams.get("cc")?.split(",") || []; const filterPort = url.searchParams.get("port")?.split(",") || PORTS; const filterVPN = url.searchParams.get("vpn")?.split(",") || PROTOCOLS; const filterLimit = parseInt(url.searchParams.get("limit")) || 10; const filterFormat = url.searchParams.get("format") || "raw"; const fillerDomain = url.searchParams.get("domain") || APP_DOMAIN; const proxyBankUrl = url.searchParams.get("proxy-list") || env.PROXY_BANK_URL; const proxyList = await getProxyList(proxyBankUrl) .then((proxies) => { if (filterCC.length) return proxies.filter((proxy) => filterCC.includes(proxy.country)); return proxies; }) .then((proxies) => { shuffleArray(proxies); return proxies; }); const uuid = crypto.randomUUID(); const result = []; for (const proxy of proxyList) { const uri = new URL(`${reverse("najort")}://${fillerDomain}`); uri.searchParams.set("encryption", "none"); uri.searchParams.set("type", "ws"); uri.searchParams.set("host", APP_DOMAIN); for (const port of filterPort) { for (const protocol of filterVPN) { if (result.length >= filterLimit) break; uri.protocol = protocol; uri.port = port.toString(); if (protocol == "ss") { uri.username = btoa(`none:${uuid}`); uri.searchParams.set( "plugin", `v2ray-plugin${port == 80 ? "" : ";tls"};mux=0;mode=websocket;path=/${proxy.proxyIP}-${proxy.proxyPort};host=${APP_DOMAIN}` ); } else { uri.username = uuid; } uri.searchParams.set("security", port == 443 ? "tls" : "none"); uri.searchParams.set("sni", port == 80 && protocol == reverse("sselv") ? "" : APP_DOMAIN); uri.searchParams.set("path", `/${proxy.proxyIP}-${proxy.proxyPort}`); uri.hash = `${result.length + 1} ${getFlagEmoji(proxy.country)} ${proxy.org} WS ${port == 443 ? "TLS" : "NTLS"} [${serviceName}]`; result.push(uri.toString()); } } } let finalResult = ""; switch (filterFormat) { case "raw": finalResult = result.join("\n"); break; case "v2ray": finalResult = btoa(result.join("\n")); break; default: return new Response("Format tidak didukung. Gunakan format=raw atau format=v2ray", { status: 400, headers: { ...CORS_HEADER_OPTIONS }, }); } return new Response(finalResult, { status: 200, headers: { ...CORS_HEADER_OPTIONS } }); } } // ---- FALLBACK ---- try { const targetReverseProxy = CONFIG.REVERSE_PROXY_TARGET || env.REVERSE_PROXY_TARGET || "example.com"; return await reverseProxy(request, targetReverseProxy); } catch (e) { return new Response(`Proxy fallback error: ${e.message}`, { status: 502, headers: { ...CORS_HEADER_OPTIONS }, }); } } catch (err) { return new Response(`An error occurred: ${err.toString()}`, { status: 500, headers: { ...CORS_HEADER_OPTIONS }, }); } }, }; // ============================================================ // WEBSOCKET HANDLER // ============================================================ async function websocketHandler(request) { const webSocketPair = new WebSocketPair(); const [client, webSocket] = Object.values(webSocketPair); webSocket.accept(); try { webSocket.binaryType = "arraybuffer"; } catch (e) {} const cfg = relayConfig(); let addressLog = ""; let portLog = ""; const log = (info, event) => console.log(`[${addressLog}:${portLog}] ${info}`, event || ""); const earlyDataHeader = request.headers.get("sec-websocket-protocol") || ""; const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log); let remoteSocketWrapper = { value: null }; // ---- Jalur UDP (hasil porting dari VLESS_CF_QUIC_FILTERED.js) ---- let udpMode = ""; // '' | 'vless' | 'vless-mux' | 'trojan' | 'ss-dns' let vlessUdpSink = null; let trojanUdpDecoder = null; let trojanRelay = null; function sendWS(bytes) { if (webSocket.readyState === WS_READY_STATE_OPEN) { try { webSocket.send(toU8(bytes)); } catch (_) {} } } function closeUdp() { if (vlessUdpSink && vlessUdpSink.close) { try { vlessUdpSink.close(); } catch (_) {} } if (trojanRelay) { try { trojanRelay.close(); } catch (_) {} } } async function ensureTrojanRelay() { if (trojanRelay && !trojanRelay.closed) return trojanRelay; trojanRelay = await RelayConnection.openPacketUDP(cfg); trojanRelay.pumpPacketUDP( (endpoint, payload) => sendWS(encodeTrojanUdpPacket(endpoint, payload)), () => { trojanRelay = null; } ); return trojanRelay; } async function handleTrojanUdp(bytes) { for (const packet of trojanUdpDecoder.push(bytes)) { const ep = packet.endpoint; if (shouldDropQuic(cfg, ep, packet.payload)) continue; try { if (Number(ep.port) === 53) { const answer = await resolveDnsOverHttps(cfg, packet.payload); sendWS(encodeTrojanUdpPacket(ep, answer)); continue; } const relay = await ensureTrojanRelay(); relay.writePacket(ep, packet.payload); } catch (_) { /* satu paket gagal, sesi tetap jalan */ } } } readableWebSocketStream .pipeTo( new WritableStream({ async write(chunk, controller) { try { // ---- lanjutan sesi UDP yang sudah terbentuk ---- if (udpMode === "vless" || udpMode === "vless-mux") { if (vlessUdpSink instanceof RelayConnection) vlessUdpSink.writeRaw(chunk); else await vlessUdpSink.writeRaw(chunk); return; } if (udpMode === "trojan") { await handleTrojanUdp(toU8(chunk)); return; } if (udpMode === "ss-dns") { return handleUDPOutbound(DNS_SERVER_ADDRESS, DNS_SERVER_PORT, chunk, webSocket, null, log); } if (remoteSocketWrapper.value) { const writer = remoteSocketWrapper.value.writable.getWriter(); await writer.write(chunk); writer.releaseLock(); return; } const protocol = await protocolSniffer(chunk); let protocolHeader; if (protocol === reverse("najorT")) protocolHeader = parseNajortHeader(chunk); else if (protocol === reverse("SSELV")) protocolHeader = parseSselvHeader(chunk); else if (protocol === reverse("skcoswodahS")) protocolHeader = parseSsHeader(chunk); else throw new Error("Unknown Protocol!"); addressLog = protocolHeader.addressRemote; portLog = `${protocolHeader.portRemote} -> ${protocolHeader.isUDP ? "UDP" : "TCP"}`; if (protocolHeader.hasError) throw new Error(protocolHeader.message); // ---- VLESS Mux.Cool / XUDP ---- if (protocolHeader.isMux) { const relay = await RelayConnection.openMuxUDP(cfg); relay.pumpRaw((payload) => sendWS(payload), () => safeCloseWebSocket(webSocket)); vlessUdpSink = relay; udpMode = "vless-mux"; sendWS(protocolHeader.version); const rest = toU8(protocolHeader.rawClientData); if (rest.byteLength) relay.writeRaw(rest); return; } // ---- UDP ---- if (protocolHeader.isUDP) { const endpoint = { address: protocolHeader.addressRemote, port: protocolHeader.portRemote }; if (protocol === reverse("najorT")) { udpMode = "trojan"; trojanUdpDecoder = new TrojanUdpDecoder(); const rest = toU8(protocolHeader.rawClientData); if (rest.byteLength) await handleTrojanUdp(rest); return; } if (protocol === reverse("SSELV")) { if (Number(protocolHeader.portRemote) === 53) { vlessUdpSink = new FixedDnsOverHttps(cfg, (answer) => sendWS(concatBuffer(uint16be(answer.byteLength), answer)) ); } else { const relay = await RelayConnection.openFixedUDP(cfg, endpoint); relay.pumpFixedUDP( (payload) => sendWS(concatBuffer(uint16be(payload.byteLength), payload)), () => safeCloseWebSocket(webSocket) ); vlessUdpSink = new FilteredFixedUdpRelay(cfg, endpoint, relay); } udpMode = "vless"; sendWS(protocolHeader.version); const rest = toU8(protocolHeader.rawClientData); if (rest.byteLength) await vlessUdpSink.writeRaw(rest); return; } // Shadowsocks: hanya DNS port 53 (lewat TCP DNS seperti semula). if (Number(protocolHeader.portRemote) !== 53) { throw new Error("UDP only support for DNS port 53 on this protocol"); } udpMode = "ss-dns"; return handleUDPOutbound(DNS_SERVER_ADDRESS, DNS_SERVER_PORT, chunk, webSocket, null, log); } await handleTCPOutBound( remoteSocketWrapper, protocolHeader.addressRemote, protocolHeader.portRemote, protocolHeader.rawClientData, webSocket, protocolHeader.version, log ); } catch (err) { log("write() error", err.message); closeUdp(); try { webSocket.close(1011, String(err.message || "internal error").slice(0, 123)); } catch {} } }, close() { closeUdp(); log(`readableWebSocketStream is close`); }, abort(reason) { closeUdp(); log(`readableWebSocketStream is abort`, JSON.stringify(reason)); }, }) ) .catch((err) => { log("readableWebSocketStream pipeTo error", err.message); closeUdp(); safeCloseWebSocket(webSocket); }); return new Response(null, { status: 101, webSocket: client, }); } // ============================================================ // PROTOCOL SNIFFER + PARSERS // ============================================================ async function protocolSniffer(buffer) { if (buffer.byteLength >= 62) { const najortDelimiter = new Uint8Array(buffer.slice(56, 60)); if (najortDelimiter[0] === 0x0d && najortDelimiter[1] === 0x0a) { if (najortDelimiter[2] === 0x01 || najortDelimiter[2] === 0x03 || najortDelimiter[2] === 0x7f) { if (najortDelimiter[3] === 0x01 || najortDelimiter[3] === 0x03 || najortDelimiter[3] === 0x04) { return reverse("najorT"); } } } } const sselvDelimiter = new Uint8Array(buffer.slice(1, 17)); if (arrayBufferToHex(sselvDelimiter).match(/^[0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12}$/i)) { return reverse("SSELV"); } return reverse("skcoswodahS"); } function parseSsHeader(ssBuffer) { const view = new DataView(ssBuffer); const addressType = view.getUint8(0); let addressLength = 0; let addressValueIndex = 1; let addressValue = ""; switch (addressType) { case 1: addressLength = 4; addressValue = new Uint8Array(ssBuffer.slice(addressValueIndex, addressValueIndex + addressLength)).join("."); break; case 3: addressLength = new Uint8Array(ssBuffer.slice(addressValueIndex, addressValueIndex + 1))[0]; addressValueIndex += 1; addressValue = new TextDecoder().decode(ssBuffer.slice(addressValueIndex, addressValueIndex + addressLength)); break; case 4: addressLength = 16; const dataView = new DataView(ssBuffer.slice(addressValueIndex, addressValueIndex + addressLength)); const ipv6 = []; for (let i = 0; i < 8; i++) ipv6.push(dataView.getUint16(i * 2).toString(16)); addressValue = ipv6.join(":"); break; default: return { hasError: true, message: `Invalid addressType for ${reverse("skcoswodahS")}: ${addressType}` }; } if (!addressValue) return { hasError: true, message: `Destination address empty, address type is: ${addressType}` }; const portIndex = addressValueIndex + addressLength; const portBuffer = ssBuffer.slice(portIndex, portIndex + 2); const portRemote = new DataView(portBuffer).getUint16(0); return { hasError: false, addressRemote: addressValue, addressType: addressType, portRemote: portRemote, rawDataIndex: portIndex + 2, rawClientData: ssBuffer.slice(portIndex + 2), version: null, isUDP: portRemote == 53, }; } function parseSselvHeader(buffer) { const version = new Uint8Array(buffer.slice(0, 1)); let isUDP = false; // Validasi UUID opsional (diatur di /settings -> UUID). Kosong = terima semua. const expectedUuid = String(CONFIG.UUID || "").trim().toLowerCase(); if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(expectedUuid)) { const got = bytesToUuid(new Uint8Array(buffer), 1); if (got !== expectedUuid) return { hasError: true, message: "UUID tidak cocok" }; } const optLength = new Uint8Array(buffer.slice(17, 18))[0]; const cmd = new Uint8Array(buffer.slice(18 + optLength, 18 + optLength + 1))[0]; if (cmd === 1) { } else if (cmd === 2) { isUDP = true; } else if (cmd === 3) { // Mux.Cool / XUDP: seluruh stream diteruskan apa adanya ke UDP relay. return { hasError: false, addressRemote: "mux", addressType: 0, portRemote: 0, rawDataIndex: 18 + optLength + 1, rawClientData: buffer.slice(18 + optLength + 1), version: new Uint8Array([version[0], 0]), isUDP: false, isMux: true, }; } else { return { hasError: true, message: `command ${cmd} is not support, command 01-tcp,02-udp,03-mux` }; } const portIndex = 18 + optLength + 1; const portBuffer = buffer.slice(portIndex, portIndex + 2); const portRemote = new DataView(portBuffer).getUint16(0); let addressIndex = portIndex + 2; const addressBuffer = new Uint8Array(buffer.slice(addressIndex, addressIndex + 1)); const addressType = addressBuffer[0]; let addressLength = 0; let addressValueIndex = addressIndex + 1; let addressValue = ""; switch (addressType) { case 1: addressLength = 4; addressValue = new Uint8Array(buffer.slice(addressValueIndex, addressValueIndex + addressLength)).join("."); break; case 2: addressLength = new Uint8Array(buffer.slice(addressValueIndex, addressValueIndex + 1))[0]; addressValueIndex += 1; addressValue = new TextDecoder().decode(buffer.slice(addressValueIndex, addressValueIndex + addressLength)); break; case 3: addressLength = 16; const dataView = new DataView(buffer.slice(addressValueIndex, addressValueIndex + addressLength)); const ipv6 = []; for (let i = 0; i < 8; i++) ipv6.push(dataView.getUint16(i * 2).toString(16)); addressValue = ipv6.join(":"); 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: addressType, portRemote: portRemote, rawDataIndex: addressValueIndex + addressLength, rawClientData: buffer.slice(addressValueIndex + addressLength), version: new Uint8Array([version[0], 0]), isUDP: isUDP, }; } function parseNajortHeader(buffer) { const socks5DataBuffer = buffer.slice(58); if (socks5DataBuffer.byteLength < 6) return { hasError: true, message: "invalid SOCKS5 request data" }; let isUDP = false; const view = new DataView(socks5DataBuffer); const cmd = view.getUint8(0); if (cmd == 3) isUDP = true; else if (cmd != 1) return { hasError: true, message: "Unsupported command type!" }; let addressType = view.getUint8(1); let addressLength = 0; let addressValueIndex = 2; let addressValue = ""; switch (addressType) { case 1: addressLength = 4; addressValue = new Uint8Array(socks5DataBuffer.slice(addressValueIndex, addressValueIndex + addressLength)).join("."); break; case 3: addressLength = new Uint8Array(socks5DataBuffer.slice(addressValueIndex, addressValueIndex + 1))[0]; addressValueIndex += 1; addressValue = new TextDecoder().decode( socks5DataBuffer.slice(addressValueIndex, addressValueIndex + addressLength) ); break; case 4: addressLength = 16; const dataView = new DataView(socks5DataBuffer.slice(addressValueIndex, addressValueIndex + addressLength)); const ipv6 = []; for (let i = 0; i < 8; i++) ipv6.push(dataView.getUint16(i * 2).toString(16)); addressValue = ipv6.join(":"); break; default: return { hasError: true, message: `invalid addressType is ${addressType}` }; } if (!addressValue) return { hasError: true, message: `address is empty, addressType is ${addressType}` }; const portIndex = addressValueIndex + addressLength; const portBuffer = socks5DataBuffer.slice(portIndex, portIndex + 2); const portRemote = new DataView(portBuffer).getUint16(0); return { hasError: false, addressRemote: addressValue, addressType: addressType, portRemote: portRemote, rawDataIndex: portIndex + 4, rawClientData: socks5DataBuffer.slice(portIndex + 4), version: null, isUDP: isUDP, }; } // ============================================================ // TCP / UDP OUTBOUND // ============================================================ async function handleTCPOutBound(remoteSocket, addressRemote, portRemote, rawClientData, webSocket, responseHeader, log) { async function connectAndWrite(address, port) { if (!address || !port) throw new Error(`Invalid target ${address}:${port}`); const tcpSocket = connect({ hostname: address, port: Number(port) }); remoteSocket.value = tcpSocket; log(`connected to ${address}:${port}`); const writer = tcpSocket.writable.getWriter(); await writer.write(rawClientData); writer.releaseLock(); return tcpSocket; } async function retry() { try { const [rAddr, rPort] = proxyIP.split(/[:=-]/); const tcpSocket = await connectAndWrite(rAddr || addressRemote, rPort || portRemote); tcpSocket.closed .catch((error) => console.log("retry tcpSocket closed error", error)) .finally(() => safeCloseWebSocket(webSocket)); remoteSocketToWS(tcpSocket, webSocket, responseHeader, null, log); } catch (e) { log("retry() failed", e.message); safeCloseWebSocket(webSocket); } } try { const tcpSocket = await connectAndWrite(addressRemote, portRemote); remoteSocketToWS(tcpSocket, webSocket, responseHeader, retry, log); } catch (err) { log("initial connect failed", err.message); return retry(); } } async function handleUDPOutbound(targetAddress, targetPort, udpChunk, webSocket, responseHeader, log) { try { let protocolHeader = responseHeader; const tcpSocket = connect({ hostname: targetAddress, port: targetPort }); log(`Connected to ${targetAddress}:${targetPort}`); const writer = tcpSocket.writable.getWriter(); await writer.write(udpChunk); writer.releaseLock(); await tcpSocket.readable.pipeTo( new WritableStream({ async write(chunk) { if (webSocket.readyState === WS_READY_STATE_OPEN) { if (protocolHeader) { webSocket.send(await new Blob([protocolHeader, chunk]).arrayBuffer()); protocolHeader = null; } else { webSocket.send(chunk); } } }, close() { log(`UDP connection to ${targetAddress} closed`); }, abort(reason) { console.error(`UDP connection to ${targetPort} aborted due to ${reason}`); }, }) ); } catch (e) { console.error(`Error while handling UDP outbound, error ${e.message}`); } } // ============================================================ // WS STREAM // ============================================================ function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) { let readableStreamCancel = false; const stream = new ReadableStream({ start(controller) { webSocketServer.addEventListener("message", (event) => { if (readableStreamCancel) return; controller.enqueue(event.data); }); webSocketServer.addEventListener("close", () => { safeCloseWebSocket(webSocketServer); if (readableStreamCancel) return; try { controller.close(); } catch {} }); webSocketServer.addEventListener("error", (err) => { log("webSocketServer has error"); try { controller.error(err); } catch {} }); const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader); if (error) { try { controller.error(error); } catch {} } else if (earlyData) { controller.enqueue(earlyData); } }, pull(controller) {}, cancel(reason) { if (readableStreamCancel) return; log(`ReadableStream was canceled, due to ${reason}`); readableStreamCancel = true; safeCloseWebSocket(webSocketServer); }, }); return stream; } async function remoteSocketToWS(remoteSocket, webSocket, responseHeader, retry, log) { let header = responseHeader; let hasIncomingData = false; await remoteSocket.readable .pipeTo( new WritableStream({ start() {}, async write(chunk, controller) { hasIncomingData = true; if (webSocket.readyState !== WS_READY_STATE_OPEN) { controller.error("webSocket.readyState is not open, maybe close"); } if (header) { webSocket.send(await new Blob([header, chunk]).arrayBuffer()); header = null; } else { webSocket.send(chunk); } }, close() { log(`remoteConnection!.readable is close with hasIncomingData is ${hasIncomingData}`); }, abort(reason) { console.error(`remoteConnection!.readable abort`, reason); }, }) ) .catch((error) => { console.error(`remoteSocketToWS has exception `, error.stack || error); safeCloseWebSocket(webSocket); }); if (hasIncomingData === false && retry) { log(`retry`); retry(); } } // ============================================================ // HELPERS // ============================================================ 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); } } async function checkProxyHealth(proxyIPx, proxyPort) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 12000); const res = await fetch(`${PROXY_HEALTH_CHECK_API}?ip=${proxyIPx}:${proxyPort}`, { signal: controller.signal, }); clearTimeout(timeoutId); if (!res.ok) { return { proxyip: false, error: `HTTP ${res.status}` }; } const data = await res.json(); const coloIata = data?.colo?.iata || (typeof data?.colo === "string" ? data.colo : ""); return { proxyip: data.proxyip === true, ip: data.ip, delay: data.delay, hostname: data.hostname, httpProtocol: data.httpProtocol, asn: data.asn, asOrganization: data.asOrganization, country: data.country, city: data.city, region: data.region, colo: coloIata, }; } catch (e) { return { proxyip: false, error: e.name === "AbortError" ? "timeout 12s" : e.message }; } } function base64ToArrayBuffer(base64Str) { if (!base64Str) return { error: null }; try { 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 { error }; } } function arrayBufferToHex(buffer) { return [...new Uint8Array(buffer)].map((x) => x.toString(16).padStart(2, "0")).join(""); } function shuffleArray(array) { let currentIndex = array.length; while (currentIndex != 0) { let randomIndex = Math.floor(Math.random() * currentIndex); currentIndex--; [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]]; } } async function generateHashFromText(text) { const msgUint8 = new TextEncoder().encode(text); const hashBuffer = await crypto.subtle.digest("SHA-256", msgUint8); return [...new Uint8Array(hashBuffer)].map((b) => b.toString(16).padStart(2, "0")).join(""); } function reverse(s) { return s.split("").reverse().join(""); } function getFlagEmoji(isoCode) { if (!isoCode || isoCode === "XX") return "๐ŸŒ"; const codePoints = isoCode .toUpperCase() .split("") .map((char) => 127397 + char.charCodeAt(0)); return String.fromCodePoint(...codePoints); } // ============================================================ // HTML PAGE // ============================================================ function buildBaseHTML() { return ` Nautica Reborn โ€” Proxy Command Center
NR

Nautica Reborn

EDGE PROXY COMMAND CENTER

NETWORK ONLINE
IP...
CC...
ISP...

PROXY DIRECTORY / LIVE NODES

PLACEHOLDER_JUDUL

PLACEHOLDER_INFO
PLACEHOLDER_PROXY_GROUP
โœ“Konfigurasi berhasil disalin
`; } function escapeHTML(value) { return String(value ?? "").replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[char])); } function escapeAttribute(value) { return escapeHTML(value).replace(/`/g, "`"); } class Document { proxies = []; constructor(request) { this.html = buildBaseHTML(); this.request = request; this.url = new URL(request.url); } setTitle(title) { this.html = this.html.replaceAll("PLACEHOLDER_JUDUL", title); } addInfo(text) { const item = `${escapeHTML(text)}`; this.html = this.html.replaceAll("PLACEHOLDER_INFO", `${item}\nPLACEHOLDER_INFO`); } registerProxies(data, proxies) { this.proxies.push({ ...data, list: proxies }); } buildProxyGroup() { let output = ""; const names = [`${reverse("NAJORT")} TLS`, `${reverse("SSELV")} TLS`, `${reverse("SS")} TLS`, `${reverse("NAJORT")} NTLS`, `${reverse("SSELV")} NTLS`, `${reverse("SS")} NTLS`]; this.proxies.forEach((item, index) => { const country = escapeAttribute((item.country || "XX").toLowerCase()); const ip = escapeHTML(item.proxyIP); const port = escapeHTML(item.proxyPort); output += `
${escapeAttribute(item.country ||
NODE.${String(index + 1).padStart(2, "0")}${escapeHTML(item.country || "XX")}
IDLE
${escapeHTML(item.org || "Unknown network")}
${ip}${port}
AVAILABLE PROTOCOLS
`; item.list.forEach((proxy, protocolIndex) => { output += ``; }); output += `
`; }); this.html = this.html.replaceAll("PLACEHOLDER_PROXY_GROUP", output || `
Tidak ada node proxy tersedia.
`); } buildCountryFlag() { const proxyBankUrl = this.url.searchParams.get("proxy-list"); const flags = [...new Set(cachedProxyList.map((proxy) => proxy.country).filter(Boolean))]; const output = flags.map((flag) => `${escapeAttribute(flag)}`).join(""); this.html = this.html.replaceAll("PLACEHOLDER_BENDERA_NEGARA", output); } addPageButton(text, link, isDisabled) { const button = ``; this.html = this.html.replaceAll("PLACEHOLDER_PAGE_BUTTON", `${button}\nPLACEHOLDER_PAGE_BUTTON`); } build() { this.buildProxyGroup(); this.buildCountryFlag(); this.html = this.html.replaceAll("PLACEHOLDER_API_READY", ""); return this.html.replaceAll(/PLACEHOLDER_\w+/gim, ""); } } // ============================================================ // SETTINGS PAGE (semua pengaturan, tersimpan di KV: Config_KV) // ============================================================ const SETTINGS_FIELDS = [ { group: "IDENTITAS WORKER", items: [ { key: "SERVICE_NAME", label: "Service name", hint: "Nama worker, dipakai untuk nama config" }, { key: "ROOT_DOMAIN", label: "Root domain", hint: "Contoh: lifetime.workers.dev" }, { key: "REVERSE_PROXY_TARGET", label: "Reverse proxy target", hint: "Situs yang ditampilkan di halaman lain" }, ]}, { group: "SUMBER PROXY", items: [ { key: "PROXY_BANK_URL", label: "Proxy bank URL", hint: "Daftar proxy format ip,port,cc,org" }, { key: "KV_PROXY_URL", label: "KV proxy URL", hint: "Opsional" }, { key: "PROXY_HEALTH_CHECK_API", label: "Health check API" }, { key: "PROXY_PER_PAGE", label: "Proxy per halaman", type: "number" }, { key: "PORTS", label: "Ports", hint: "Pisahkan dengan koma, contoh: 443,80" }, ]}, { group: "UDP RELAY & DNS", items: [ { key: "UDP_RELAY_HOST", label: "UDP relay host", hint: "Host relay WebSocket (protokol VLRLY004)" }, { key: "UDP_RELAY_PORT", label: "UDP relay port", type: "number" }, { key: "DOH_URL", label: "DNS over HTTPS URL" }, { key: "REJECT_UDP_443", label: "Buang QUIC di UDP/443", hint: "1 = aktif, 0 = izinkan QUIC" }, { key: "DNS_SERVER_ADDRESS", label: "DNS server (fallback TCP)" }, { key: "DNS_SERVER_PORT", label: "DNS port", type: "number" }, ]}, { group: "TUNNEL", items: [ { key: "PROXYIP", label: "Proxy IP cadangan", hint: "ip:port, boleh beberapa dipisah koma" }, { key: "UUID", label: "UUID VLESS", hint: "Kosong = terima semua UUID" }, ]}, { group: "TAMPILAN", items: [ { key: "DONATE_LINK", label: "Link donasi" }, ]}, ]; function getSettingsHTML(cfg, kvBound) { const groups = SETTINGS_FIELDS.map((group) => { const fields = group.items .map((field) => { const value = escapeAttribute(cfg[field.key] ?? ""); const hint = field.hint ? `${escapeHTML(field.hint)}` : ""; return ``; }) .join(""); return `

${escapeHTML(group.group)}

${fields}
`; }).join(""); return ` Nautica Reborn โ€” Settings

Nautica Settings

CONFIG STORE / KV: Config_KV

${kvBound ? "KV binding Config_KV terpasang โ€” perubahan akan tersimpan permanen." : "KV binding Config_KV belum dipasang. Tambahkan binding bernama Config_KV di worker agar pengaturan bisa disimpan."}
${groups}
`; }