import { connect } from "cloudflare:sockets";
// Variables
let serviceName = "";
let APP_DOMAIN = "";
let prxIP = "";
let cachedPrxList = [];
// IN-MEMORY CACHE STORAGE (Menyimpan hasil proxy terbaik di memori)
let memoryProxyCache = {
data: [],
lastUpdated: 0,
ttl: 10 * 60 * 1000 // Cache berlaku selama 10 menit
};
// Constant & Default Values
const horse = "dHJvamFu";
const flash = "dm1lc3M=";
const neko = "dmxlc3M=";
const v2 = "djJyYXk=";
const PORTS = [443, 80];
const PROTOCOLS = [atob(horse), atob(flash), atob(neko), "ss"];
const SUB_PAGE_URL = "https://foolvpn.web.id/nautica";
// Default Configs Sesuai Foto & Optimal (Default DoH Cloudflare)
const DEFAULT_CONFIG = {
DNS_TYPE: "DOH",
DOH_URL: "https://cloudflare-dns.com/dns-query",
DNS_SERVER_ADDRESS: "1.1.1.1",
DNS_SERVER_PORT: "53",
UDP_MODE: "RELAY",
UDP_RELAY_HOST: "udp-relay.hobihaus.space",
UDP_RELAY_PORT: "7300",
KV_PRX_URL: "https://raw.githubusercontent.com/Ddfathu/nauticamod/refs/heads/main/proxy.json",
PRX_BANK_URL: "https://raw.githubusercontent.com/Ddfathu/nauticamod/refs/heads/main/proxy.txt",
REVERSE_PRX_TARGET: "",
RAW_KV_JSON_DATA: "",
RAW_KV_TXT_DATA: "",
TCP_OPTIMIZE_MODE: "OFF", // Default MATI
ZERO_COPY_MODE: "ON",
EARLY_FLUSH_MODE: "ON",
STREAM_BUFFER_MODE: "HIGH_THROUGHPUT", // Default HIGH_THROUGHPUT
DNS_HISTORY: [
{ type: "DOH", label: "Cloudflare DoH", dohUrl: "https://cloudflare-dns.com/dns-query", isDefault: true },
{ type: "DOH", label: "Google DoH", dohUrl: "https://dns.google/dns-query", isDefault: true },
{ type: "DOH", label: "NextDNS DoH", dohUrl: "https://dns.nextdns.io/dns-query", isDefault: true },
{ type: "UDP", label: "Google DNS (8.8.8.8:53)", addr: "8.8.8.8", port: "53", isDefault: true },
{ type: "UDP", label: "Cloudflare DNS (1.1.1.1:53)", addr: "1.1.1.1", port: "53", isDefault: true }
],
RELAY_HISTORY: [
{ label: "Hobihaus Relay (udp-relay.hobihaus.space:7300)", host: "udp-relay.hobihaus.space", port: "7300", isDefault: true },
{ label: "Railway Proxy (altaria.proxy.rlwy.net:57280)", host: "altaria.proxy.rlwy.net", port: "57280", isDefault: true }
],
PRX_BANK_HISTORY: [
{ label: "Default Proxy Bank TXT", url: "https://raw.githubusercontent.com/Ddfathu/nauticamod/refs/heads/main/proxy.txt", isDefault: true }
],
KV_PRX_HISTORY: [
{ label: "Default Proxy JSON URL", url: "https://raw.githubusercontent.com/Ddfathu/nauticamod/refs/heads/main/proxy.json", isDefault: true }
],
CUSTOM_PRX_LIST: []
};
const CONVERTER_URL = "https://api.foolvpn.web.id/convert";
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-Max-Age": "86400",
};
// Encrypted Stream Constants
const SALT_A1 = atob("Vk1lc3MgSGVhZGVyIEFFQUQgS2V5X0xlbmd0aA==");
const SALT_A2 = atob("Vk1lc3MgSGVhZGVyIEFFQUQgTm9uY2VfTGVuZ3Ro");
const SALT_A3 = atob("Vk1lc3MgSGVhZGVyIEFFQUQgS2V5");
const SALT_A4 = atob("Vk1lc3MgSGVhZGVyIEFFQUQgTm9uY2U=");
const SALT_B1 = atob("QUVBRCBSZXNwIEhlYWRlciBMZW4gSVY=");
const SALT_B2 = atob("QUVBRCBSZXNwIEhlYWRlciBMZW4gSVY=");
const SALT_B3 = atob("QUVBRCBSZXNwIEhlYWRlciBLZXk=");
const SALT_B4 = atob("QUVBRCBSZXNwIEhlYWRlciBJVg==");
// HELPER PARSER PROXY: IP:PORT atau IP:PORT@USER:PASS
function parseProxy(proxyStr) {
if (!proxyStr) return null;
const atSplit = proxyStr.split("@");
const hostPort = atSplit[0];
const auth = atSplit[1] || null;
let [host, port] = hostPort.split(":");
port = parseInt(port, 10) || 443;
let user = null;
let pass = null;
if (auth && auth.includes(":")) {
const authParts = auth.split(":");
user = authParts[0];
pass = authParts.slice(1).join(":");
}
return { host, port, user, pass };
}
async function getConfig(env) {
let kvConfig = {};
if (env && env.CONFIG_KV) {
try {
const stored = await env.CONFIG_KV.get("APP_CONFIG", { type: "json" });
if (stored) kvConfig = stored;
} catch (e) {
console.error("Gagal membaca KV:", e);
}
}
return {
DNS_TYPE: kvConfig.DNS_TYPE || (env && env.DNS_TYPE) || DEFAULT_CONFIG.DNS_TYPE,
DOH_URL: kvConfig.DOH_URL || (env && env.DOH_URL) || DEFAULT_CONFIG.DOH_URL,
DNS_SERVER_ADDRESS: kvConfig.DNS_SERVER_ADDRESS || (env && env.DNS_SERVER_ADDRESS) || DEFAULT_CONFIG.DNS_SERVER_ADDRESS,
DNS_SERVER_PORT: parseInt(kvConfig.DNS_SERVER_PORT || (env && env.DNS_SERVER_PORT) || DEFAULT_CONFIG.DNS_SERVER_PORT),
UDP_MODE: kvConfig.UDP_MODE || (env && env.UDP_MODE) || DEFAULT_CONFIG.UDP_MODE,
UDP_RELAY_HOST: kvConfig.UDP_RELAY_HOST || (env && env.UDP_RELAY_HOST) || DEFAULT_CONFIG.UDP_RELAY_HOST,
UDP_RELAY_PORT: parseInt(kvConfig.UDP_RELAY_PORT || (env && env.UDP_RELAY_PORT) || DEFAULT_CONFIG.UDP_RELAY_PORT),
KV_PRX_URL: kvConfig.KV_PRX_URL || (env && env.KV_PRX_URL) || DEFAULT_CONFIG.KV_PRX_URL,
PRX_BANK_URL: kvConfig.PRX_BANK_URL || (env && env.PRX_BANK_URL) || DEFAULT_CONFIG.PRX_BANK_URL,
REVERSE_PRX_TARGET: kvConfig.REVERSE_PRX_TARGET || DEFAULT_CONFIG.REVERSE_PRX_TARGET,
RAW_KV_JSON_DATA: kvConfig.RAW_KV_JSON_DATA || DEFAULT_CONFIG.RAW_KV_JSON_DATA,
RAW_KV_TXT_DATA: kvConfig.RAW_KV_TXT_DATA || DEFAULT_CONFIG.RAW_KV_TXT_DATA,
TCP_OPTIMIZE_MODE: kvConfig.TCP_OPTIMIZE_MODE !== undefined ? kvConfig.TCP_OPTIMIZE_MODE : DEFAULT_CONFIG.TCP_OPTIMIZE_MODE,
ZERO_COPY_MODE: kvConfig.ZERO_COPY_MODE !== undefined ? kvConfig.ZERO_COPY_MODE : DEFAULT_CONFIG.ZERO_COPY_MODE,
EARLY_FLUSH_MODE: kvConfig.EARLY_FLUSH_MODE !== undefined ? kvConfig.EARLY_FLUSH_MODE : DEFAULT_CONFIG.EARLY_FLUSH_MODE,
STREAM_BUFFER_MODE: kvConfig.STREAM_BUFFER_MODE !== undefined ? kvConfig.STREAM_BUFFER_MODE : DEFAULT_CONFIG.STREAM_BUFFER_MODE,
DNS_HISTORY: kvConfig.DNS_HISTORY || DEFAULT_CONFIG.DNS_HISTORY,
RELAY_HISTORY: kvConfig.RELAY_HISTORY || DEFAULT_CONFIG.RELAY_HISTORY,
PRX_BANK_HISTORY: kvConfig.PRX_BANK_HISTORY || DEFAULT_CONFIG.PRX_BANK_HISTORY,
KV_PRX_HISTORY: kvConfig.KV_PRX_HISTORY || DEFAULT_CONFIG.KV_PRX_HISTORY,
CUSTOM_PRX_LIST: kvConfig.CUSTOM_PRX_LIST || DEFAULT_CONFIG.CUSTOM_PRX_LIST
};
}
async function getKVPrxList(config) {
if (config.RAW_KV_JSON_DATA && config.RAW_KV_JSON_DATA.trim() !== "") {
try {
return JSON.parse(config.RAW_KV_JSON_DATA);
} catch (e) {
console.error("Gagal parsing RAW_KV_JSON_DATA dari KV:", e);
}
}
if (!config.KV_PRX_URL) throw new Error("No URL Provided!");
const kvPrx = await fetch(config.KV_PRX_URL);
return kvPrx.status == 200 ? await kvPrx.json() : {};
}
async function getPrxList(config) {
let kvParsedList = [];
let urlParsedList = [];
if (config.RAW_KV_TXT_DATA && config.RAW_KV_TXT_DATA.trim() !== "") {
const lines = config.RAW_KV_TXT_DATA.split("\n").filter(Boolean);
kvParsedList = lines.map((entry) => {
const [prxIP, prxPort, country, org] = entry.split(",");
return {
prxIP: prxIP ? prxIP.trim() : "Unknown",
prxPort: prxPort ? prxPort.trim() : "Unknown",
country: country ? country.trim().toUpperCase() : "Unknown",
org: org ? org.trim() : "Unknown Org",
};
}).filter(Boolean);
}
if (config.PRX_BANK_URL) {
try {
const prxBank = await fetch(config.PRX_BANK_URL);
if (prxBank.status == 200) {
const text = (await prxBank.text()) || "";
const prxString = text.split("\n").filter(Boolean);
urlParsedList = prxString.map((entry) => {
const [prxIP, prxPort, country, org] = entry.split(",");
return {
prxIP: prxIP ? prxIP.trim() : "Unknown",
prxPort: prxPort ? prxPort.trim() : "Unknown",
country: country ? country.trim().toUpperCase() : "Unknown",
org: org ? org.trim() : "Unknown Org",
};
}).filter(Boolean);
}
} catch (e) {
console.error("Gagal mengambil Proxy Bank TXT dari URL:", e);
}
}
let customList = config.CUSTOM_PRX_LIST || [];
const existingIdentifiers = new Set([
...kvParsedList.map(p => `${p.prxIP}:${p.prxPort}`),
...customList.map(p => `${p.prxIP}:${p.prxPort}`)
]);
const filteredUrlList = urlParsedList.filter(p => !existingIdentifiers.has(`${p.prxIP}:${p.prxPort}`));
cachedPrxList = [...customList, ...kvParsedList, ...filteredUrlList];
return cachedPrxList;
}
async function checkProxyInternal(host, port, timeoutMs = 800) {
const startTime = Date.now();
try {
const socket = connect({ hostname: host, port: parseInt(port) || 443 });
await Promise.race([
socket.opened,
new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeoutMs))
]);
const latency = Date.now() - startTime;
try { socket.close(); } catch (e) {}
return { status: "active", latency: latency };
} catch (err) {
return { status: "dead", latency: 99999 };
}
}
async function getBestInMemoryProxy(config) {
const now = Date.now();
if (memoryProxyCache.data.length > 0 && (now - memoryProxyCache.lastUpdated < memoryProxyCache.ttl)) {
const randomCached = memoryProxyCache.data[Math.floor(Math.random() * memoryProxyCache.data.length)];
return `${randomCached.prxIP}:${randomCached.prxPort}`;
}
const list = await getPrxList(config);
if (list.length === 0) return "172.232.249.224:2053";
const prioritized = list.filter(p => p.country === "SG" || p.country === "ID");
const targetPool = prioritized.length > 0 ? prioritized : list;
const samples = targetPool.sort(() => 0.5 - Math.random()).slice(0, 6);
const testResults = await Promise.all(
samples.map(async (p) => {
const res = await checkProxyInternal(p.prxIP, parseInt(p.prxPort) || 443, 600);
return { ...p, latency: res.latency };
})
);
testResults.sort((a, b) => a.latency - b.latency);
const healthyOnly = testResults.filter(p => p.latency < 400);
if (healthyOnly.length > 0) {
memoryProxyCache.data = healthyOnly;
memoryProxyCache.lastUpdated = now;
return `${healthyOnly[0].prxIP}:${healthyOnly[0].prxPort}`;
}
const activeOnly = testResults.filter(p => p.latency < 99999);
if (activeOnly.length > 0) {
memoryProxyCache.data = activeOnly;
memoryProxyCache.lastUpdated = now;
return `${activeOnly[0].prxIP}:${activeOnly[0].prxPort}`;
}
return `${samples[0].prxIP}:${samples[0].prxPort}`;
}
async function getBestSgProxyInternal(config) {
const list = await getPrxList(config);
const sgList = list.filter(p => p.country && p.country.toUpperCase() === "SG");
const targetList = sgList.length > 0 ? sgList : list;
if (targetList.length === 0) return "172.232.249.224:2053";
const samples = targetList.sort(() => 0.5 - Math.random()).slice(0, 4);
const testResults = await Promise.all(
samples.map(async (p) => {
const res = await checkProxyInternal(p.prxIP, parseInt(p.prxPort) || 443, 800);
return { ipPort: `${p.prxIP}:${p.prxPort}`, latency: res.latency };
})
);
testResults.sort((a, b) => a.latency - b.latency);
return testResults[0].ipPort || `${samples[0].prxIP}:${samples[0].prxPort}`;
}
async function getBestKvJsonProxyInternal(kvPrx) {
let allProxies = [];
for (const key in kvPrx) {
if (Array.isArray(kvPrx[key])) {
allProxies.push(...kvPrx[key]);
}
}
if (allProxies.length === 0) return "172.232.249.224:2053";
const samples = allProxies.sort(() => 0.5 - Math.random()).slice(0, 4);
const testResults = await Promise.all(
samples.map(async (p) => {
const hostPort = p.split("@")[0];
const [ip, port] = hostPort.split(":");
const res = await checkProxyInternal(ip, parseInt(port) || 443, 800);
return { ipPort: p, latency: res.latency };
})
);
testResults.sort((a, b) => a.latency - b.latency);
return testResults[0].ipPort || samples[0];
}
async function reverseWeb(request, target, targetPath) {
if (!target || target.trim() === "") target = "example.com";
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 modifiedHeaders = new Headers(request.headers);
modifiedHeaders.set("Host", targetUrl.hostname);
modifiedHeaders.set("X-Forwarded-Host", request.headers.get("Host"));
const modifiedRequest = new Request(targetUrl, {
method: request.method,
headers: modifiedHeaders,
body: request.body,
redirect: "follow"
});
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;
}
export default {
async fetch(request, env, ctx) {
try {
const url = new URL(request.url);
APP_DOMAIN = url.hostname;
serviceName = APP_DOMAIN.split(".")[0];
const currentConfig = await getConfig(env);
if (url.pathname === "/api/get-proxy-list") {
const list = await getPrxList(currentConfig);
return new Response(JSON.stringify(list), {
headers: {
...CORS_HEADER_OPTIONS,
"Content-Type": "application/json",
"Cache-Control": "no-store, no-cache, must-revalidate"
}
});
}
if (url.pathname.startsWith("/api/check")) {
const target = url.searchParams.get("ip");
if (!target || !target.includes(":")) {
return new Response(JSON.stringify({ status: "error", message: "Gunakan parameter ?ip=IP:PORT" }), {
status: 400,
headers: { ...CORS_HEADER_OPTIONS, "Content-Type": "application/json" }
});
}
const hostPort = target.split("@")[0];
const [ip, port] = hostPort.split(":");
const res = await checkProxyInternal(ip, parseInt(port) || 443, 2500);
return new Response(JSON.stringify({
status: res.status,
ip: ip,
port: port,
latency: res.status === "active" ? `${res.latency}ms` : "timeout"
}), {
status: 200,
headers: {
...CORS_HEADER_OPTIONS,
"Content-Type": "application/json",
"Cache-Control": "no-store, no-cache, must-revalidate"
}
});
}
const htmlContent = `
Nautica Worker Dashboard
Full-Speed Pipeline Active (Zero-Lag)
➕ Daftarkan Custom Proxy Baru
📋 Daftar Custom Proxy Tersimpan
`;
if (url.pathname === "/ui" || url.pathname === "/ui/") {
return new Response(htmlContent, {
headers: {
"Content-Type": "text/html; charset=utf-8",
"Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0",
"Pragma": "no-cache",
"Expires": "0",
...CORS_HEADER_OPTIONS
},
});
}
const targetReversePrx = currentConfig.REVERSE_PRX_TARGET;
if (url.pathname === "/") {
if (!targetReversePrx || targetReversePrx === "example.com" || targetReversePrx.trim() === "") {
return new Response(htmlContent, {
headers: {
"Content-Type": "text/html; charset=utf-8",
"Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0",
"Pragma": "no-cache",
"Expires": "0",
...CORS_HEADER_OPTIONS
},
});
}
}
if (url.pathname === "/api/save-config" && request.method === "POST") {
if (!env || !env.CONFIG_KV) {
return new Response(
JSON.stringify({ success: false, message: "Binding CONFIG_KV belum diset di Cloudflare Worker!" }),
{ status: 400, headers: { ...CORS_HEADER_OPTIONS, "Content-Type": "application/json" } }
);
}
const body = await request.json();
let oldConfig = {};
try {
const stored = await env.CONFIG_KV.get("APP_CONFIG", { type: "json" });
if (stored) oldConfig = stored;
} catch (e) {}
const mergedConfig = { ...DEFAULT_CONFIG, ...oldConfig, ...body };
await env.CONFIG_KV.put("APP_CONFIG", JSON.stringify(mergedConfig));
return new Response(
JSON.stringify({ success: true, message: "Pengaturan berhasil disimpan ke KV." }),
{ status: 200, headers: { ...CORS_HEADER_OPTIONS, "Content-Type": "application/json" } }
);
}
const upgradeHeader = request.headers.get("Upgrade");
if (upgradeHeader === "websocket") {
const rawPath = decodeURIComponent(url.pathname);
const cleanPath = rawPath.replace(/^\//, "");
if (cleanPath.toUpperCase() === "SEMUA") {
prxIP = await getBestInMemoryProxy(currentConfig);
return await websocketHandler(request, currentConfig);
}
// Support ip:port atau ip:port@user:pass
const proxyAuthMatch = cleanPath.match(/^([a-zA-Z0-9.-]+:\d+(@[a-zA-Z0-9._~%!$&'()*+,;=-]+:[a-zA-Z0-9._~%!$&'()*+,;=-]+)?)$/);
if (proxyAuthMatch) {
prxIP = proxyAuthMatch[1];
return await websocketHandler(request, currentConfig);
}
const prxMatch = rawPath.match(/^\/(.+[:=-]\d+(@.+:.+)?)$/);
if (prxMatch) {
prxIP = prxMatch[1];
return await websocketHandler(request, currentConfig);
}
if (/^[A-Z0-9_-]+$/.test(cleanPath)) {
const prxKeys = cleanPath.split(",");
const prxKey = prxKeys[Math.floor(Math.random() * prxKeys.length)];
let selectedIp = "";
if (currentConfig.CUSTOM_PRX_LIST && currentConfig.CUSTOM_PRX_LIST.length > 0) {
const matchedCustom = currentConfig.CUSTOM_PRX_LIST.find(
p => p.org.toUpperCase() === prxKey || p.country.toUpperCase() === prxKey
);
if (matchedCustom) {
selectedIp = `${matchedCustom.prxIP}:${matchedCustom.prxPort}`;
}
}
if (!selectedIp) {
const kvPrx = await getKVPrxList(currentConfig);
if (kvPrx[prxKey] && kvPrx[prxKey].length > 0) {
selectedIp = kvPrx[prxKey][Math.floor(Math.random() * kvPrx[prxKey].length)];
} else {
selectedIp = await getBestKvJsonProxyInternal(kvPrx);
}
}
prxIP = selectedIp;
return await websocketHandler(request, currentConfig);
}
if (/[a-z]/.test(cleanPath)) {
prxIP = await getBestSgProxyInternal(currentConfig);
return await websocketHandler(request, currentConfig);
}
}
if (url.pathname.startsWith("/sub")) {
return Response.redirect(SUB_PAGE_URL + `?host=${APP_DOMAIN}`, 301);
} else if (url.pathname.startsWith("/check")) {
const target = url.searchParams.get("target").split(":");
const result = await checkPrxHealth(target[0], target[1] || "443");
return new Response(JSON.stringify(result), {
status: 200,
headers: {
...CORS_HEADER_OPTIONS,
"Content-Type": "application/json",
},
});
} else 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 prxList = await getPrxList(currentConfig)
.then((prxs) => {
if (filterCC.length) {
return prxs.filter((prx) => filterCC.includes(prx.country));
}
return prxs;
})
.then((prxs) => {
shuffleArray(prxs);
return prxs;
});
const uuid = crypto.randomUUID();
const result = [];
for (const prx of prxList) {
const uri = new URL(`${atob(horse)}://${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",
`${atob(v2)}-plugin${port == 80 ? "" : ";tls"};mux=0;mode=websocket;path=/${prx.prxIP}-${
prx.prxPort
};host=${APP_DOMAIN}`,
);
} else {
uri.username = uuid;
}
uri.searchParams.set("security", port == 443 ? "tls" : "none");
uri.searchParams.set("sni", port == 80 && protocol == atob(neko) ? "" : APP_DOMAIN);
uri.searchParams.set("path", `/${prx.prxIP}-${prx.prxPort}`);
uri.hash = `${result.length + 1} ${getFlagEmoji(prx.country)} ${prx.org} WS ${
port == 443 ? "TLS" : "NTLS"
} [${serviceName}]`;
result.push(uri.toString());
}
}
}
let finalResult = "";
switch (filterFormat) {
case "raw":
finalResult = result.join("\n");
break;
case atob(v2):
finalResult = btoa(result.join("\n"));
break;
case atob(neko):
case "sfa":
case "bfr":
const res = await fetch(CONVERTER_URL, {
method: "POST",
body: JSON.stringify({
url: result.join(","),
format: filterFormat,
template: "cf",
}),
});
if (res.status == 200) {
finalResult = await res.text();
} else {
return new Response(res.statusText, {
status: res.status,
headers: {
...CORS_HEADER_OPTIONS,
},
});
}
break;
}
return new Response(finalResult, {
status: 200,
headers: {
...CORS_HEADER_OPTIONS,
},
});
} else if (apiPath.startsWith("/myip")) {
return new Response(
JSON.stringify({
ip:
request.headers.get("cf-connecting-ipv6") ||
request.headers.get("cf-connecting-ip") ||
request.headers.get("x-real-ip"),
colo: request.headers.get("cf-ray")?.split("-")[1],
...request.cf,
}),
{
headers: {
...CORS_HEADER_OPTIONS,
},
},
);
}
}
return await reverseWeb(request, targetReversePrx);
} catch (err) {
return new Response(`An error occurred: ${err.toString()}`, {
status: 500,
headers: {
...CORS_HEADER_OPTIONS,
},
});
}
},
};
async function websocketHandler(request, config) {
const webSocketPair = new WebSocketPair();
const [client, webSocket] = Object.values(webSocketPair);
webSocket.accept();
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,
};
let isDNS = false;
readableWebSocketStream
.pipeTo(
new WritableStream({
async write(chunk, controller) {
if (isDNS) {
if (config.DNS_TYPE === "DOH") {
return handleDoHOutbound(config.DOH_URL, chunk, webSocket, null, log);
}
return handleUDPOutbound(
config.DNS_SERVER_ADDRESS,
config.DNS_SERVER_PORT,
chunk,
webSocket,
null,
log,
config
);
}
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 === atob(horse)) {
protocolHeader = readHorseHeader(chunk);
} else if (protocol === atob(flash)) {
protocolHeader = await readStreamHeader(chunk);
} else if (protocol === atob(neko)) {
protocolHeader = readNekoHeader(chunk);
} else if (protocol === "ss") {
protocolHeader = readSsHeader(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);
}
let responseHeader = protocolHeader.version;
if (protocol === atob(flash) && protocolHeader.needsResponse) {
responseHeader = await generateStreamResponseHeader(
protocolHeader.responseOptions,
protocolHeader.encKey,
protocolHeader.encIv,
);
}
if (protocolHeader.isUDP) {
if (protocolHeader.portRemote === 53) {
isDNS = true;
if (config.DNS_TYPE === "DOH") {
return handleDoHOutbound(config.DOH_URL, chunk, webSocket, responseHeader, log);
}
return handleUDPOutbound(
config.DNS_SERVER_ADDRESS,
config.DNS_SERVER_PORT,
chunk,
webSocket,
responseHeader,
log,
config
);
}
return handleUDPOutbound(
protocolHeader.addressRemote,
protocolHeader.portRemote,
chunk,
webSocket,
responseHeader,
log,
config
);
}
handleTCPOutBound(
remoteSocketWrapper,
protocolHeader.addressRemote,
protocolHeader.portRemote,
protocolHeader.rawClientData,
webSocket,
responseHeader,
log,
config,
);
},
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,
webSocket: client,
});
}
async function protocolSniffer(buffer) {
if (buffer.byteLength >= 62) {
const horseDelimiter = new Uint8Array(buffer.slice(56, 60));
if (horseDelimiter[0] === 0x0d && horseDelimiter[1] === 0x0a) {
if (horseDelimiter[2] === 0x01 || horseDelimiter[2] === 0x03 || horseDelimiter[2] === 0x7f) {
if (horseDelimiter[3] === 0x01 || horseDelimiter[3] === 0x03 || horseDelimiter[3] === 0x04) {
return atob(horse);
}
}
}
}
if (buffer.byteLength >= 18) {
const version = new Uint8Array(buffer.slice(0, 1))[0];
if (version === 0) {
const protocolUuid = new Uint8Array(buffer.slice(1, 17));
if (arrayBufferToHex(protocolUuid).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 atob(neko);
}
}
}
if (buffer.byteLength >= 42) {
const firstByte = new Uint8Array(buffer.slice(0, 1))[0];
if (firstByte === 0x01 || firstByte === 0x03 || firstByte === 0x04) {
return "ss";
}
return atob(flash);
}
return "ss";
}
async function generateStreamResponseHeader(responseOptions, encKey, encIv) {
try {
const key = (await sha256(encKey)).slice(0, 16);
const lengthKey = (await kdf(key, [SALT_B1])).slice(0, 16);
const lengthIv = (await kdf(key, [SALT_B2])).slice(0, 12);
const lengthData = new Uint8Array([0, 4]);
const encryptedLength = await aesGcmEncrypt(lengthKey, lengthIv, lengthData, new Uint8Array(0));
const headerPayload = new Uint8Array([responseOptions[0], 0x00, 0x00, 0x00]);
const payloadKey = (await kdf(key, [SALT_B3])).slice(0, 16);
const payloadIv = (await kdf(key, [SALT_B4])).slice(0, 12);
const encryptedPayload = await aesGcmEncrypt(payloadKey, payloadIv, headerPayload, new Uint8Array(0));
const response = new Uint8Array(encryptedLength.length + encryptedPayload.length);
response.set(encryptedLength, 0);
response.set(encryptedPayload, encryptedLength.length);
return response;
} catch (e) {
console.error("VMess Response Header Error:", e);
return new Uint8Array(0);
}
}
async function readStreamHeader(buffer) {
try {
const uuidString = "00000000-0000-0000-0000-000000000000";
const uuidBytes = new Uint8Array(
uuidString
.replace(/-/g, "")
.match(/.{1,2}/g)
.map((byte) => parseInt(byte, 16)),
);
const authKey = await md5(
uuidBytes,
new TextEncoder().encode(atob("YzQ4NjE5ZmUtOGYwMi00OWUwLWI5ZTktZWRmNzYzZTE3ZTIx")),
);
const authId = new Uint8Array(buffer.slice(0, 16));
const encryptedLength = new Uint8Array(buffer.slice(16, 34));
const nonce = new Uint8Array(buffer.slice(34, 42));
const lengthKey = (await kdf(authKey, [SALT_A1, authId, nonce])).slice(0, 16);
const lengthIv = (await kdf(authKey, [SALT_A2, authId, nonce])).slice(0, 12);
const lengthBytes = await aesGcmDecrypt(lengthKey, lengthIv, encryptedLength, authId);
const headerLength = (lengthBytes[0] << 8) | lengthBytes[1];
const encryptedHeader = new Uint8Array(buffer.slice(42, 42 + headerLength + 16));
const payloadKey = (await kdf(authKey, [SALT_A3, authId, nonce])).slice(0, 16);
const payloadIv = (await kdf(authKey, [SALT_A4, authId, nonce])).slice(0, 12);
const headerPayload = await aesGcmDecrypt(payloadKey, payloadIv, encryptedHeader, authId);
const view = new DataView(headerPayload.buffer);
let offset = 0;
const version = view.getUint8(offset);
offset += 1;
if (version !== 1) {
return { hasError: true, message: `Invalid protocol version: ${version}` };
}
const encIv = new Uint8Array(headerPayload.slice(offset, offset + 16));
offset += 16;
const encKey = new Uint8Array(headerPayload.slice(offset, offset + 16));
offset += 16;
const options = new Uint8Array(headerPayload.slice(offset, offset + 4));
offset += 4;
const cmd = view.getUint8(offset);
offset += 1;
const isUDP = cmd !== 0x01;
const portRemote = view.getUint16(offset, false);
offset += 2;
const addressType = view.getUint8(offset);
offset += 1;
let addressRemote = "";
switch (addressType) {
case 1:
addressRemote = `${view.getUint8(offset)}.${view.getUint8(offset + 1)}.${view.getUint8(offset + 2)}.${view.getUint8(offset + 3)}`;
offset += 4;
break;
case 2:
case 3:
const domainLength = view.getUint8(offset);
offset += 1;
addressRemote = new TextDecoder().decode(headerPayload.slice(offset, offset + domainLength));
offset += domainLength;
break;
case 4:
const ipv6Parts = [];
for (let i = 0; i < 8; i++) {
ipv6Parts.push(view.getUint16(offset + i * 2, false).toString(16));
}
addressRemote = ipv6Parts.join(":");
offset += 16;
break;
default:
return { hasError: true, message: `Invalid address type: ${addressType}` };
}
const rawDataIndex = 42 + headerLength + 16;
return {
hasError: false,
addressRemote,
addressType,
portRemote,
rawDataIndex,
rawClientData: buffer.slice(rawDataIndex),
version: new Uint8Array([options[0], 0]),
isUDP,
needsResponse: true,
responseOptions: options,
encKey: encKey,
encIv: encIv,
};
} catch (e) {
return {
hasError: true,
message: "Stream header parsing failed: " + e.message,
};
}
}
function readSsHeader(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 SS: ${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 readNekoHeader(buffer) {
const version = new Uint8Array(buffer.slice(0, 1));
let isUDP = false;
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 {
return {
hasError: true,
message: `command ${cmd} is not supported`,
};
}
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 readHorseHeader(buffer) {
const dataBuffer = buffer.slice(58);
if (dataBuffer.byteLength < 6) {
return {
hasError: true,
message: "invalid request data",
};
}
let isUDP = false;
const view = new DataView(dataBuffer);
const cmd = view.getUint8(0);
if (cmd == 3) {
isUDP = true;
} else if (cmd != 1) {
throw new Error("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(dataBuffer.slice(addressValueIndex, addressValueIndex + addressLength)).join(".");
break;
case 3:
addressLength = new Uint8Array(dataBuffer.slice(addressValueIndex, addressValueIndex + 1))[0];
addressValueIndex += 1;
addressValue = new TextDecoder().decode(dataBuffer.slice(addressValueIndex, addressValueIndex + addressLength));
break;
case 4:
addressLength = 16;
const dataView = new DataView(dataBuffer.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 = dataBuffer.slice(portIndex, portIndex + 2);
const portRemote = new DataView(portBuffer).getUint16(0);
return {
hasError: false,
addressRemote: addressValue,
addressType: addressType,
portRemote: portRemote,
rawDataIndex: portIndex + 4,
rawClientData: dataBuffer.slice(portIndex + 4),
version: null,
isUDP: isUDP,
};
}
async function handleTCPOutBound(
remoteSocket,
addressRemote,
portRemote,
rawClientData,
webSocket,
responseHeader,
log,
config,
) {
async function connectAndWrite(address, port) {
const tcpSocket = connect({
hostname: address,
port: parseInt(port, 10),
});
remoteSocket.value = tcpSocket;
log(`Connecting to ${address}:${port}`);
if (rawClientData && rawClientData.byteLength > 0) {
const writer = tcpSocket.writable.getWriter();
await writer.write(rawClientData);
writer.releaseLock();
}
return tcpSocket;
}
async function connectViaHttpProxy(parsedPrx, targetHost, targetPort) {
const tcpSocket = connect({
hostname: parsedPrx.host,
port: parsedPrx.port,
});
remoteSocket.value = tcpSocket;
log(`Tunneling via Upstream Proxy -> ${parsedPrx.host}:${parsedPrx.port} (Auth: ${parsedPrx.user ? 'YES' : 'NO'})`);
const writer = tcpSocket.writable.getWriter();
let connectReq = `CONNECT ${targetHost}:${targetPort} HTTP/1.1\r\nHost: ${targetHost}:${targetPort}\r\n`;
if (parsedPrx.user && parsedPrx.pass) {
const authBase64 = btoa(`${parsedPrx.user}:${parsedPrx.pass}`);
connectReq += `Proxy-Authorization: Basic ${authBase64}\r\n`;
}
connectReq += `User-Agent: Cloudflare-Worker\r\nProxy-Connection: Keep-Alive\r\n\r\n`;
await writer.write(new TextEncoder().encode(connectReq));
const reader = tcpSocket.readable.getReader();
const { value: resChunk } = await reader.read();
reader.releaseLock();
const responseText = new TextDecoder().decode(resChunk || new Uint8Array(0));
if (!responseText.includes(" 200 ")) {
throw new Error(`Proxy authentication/tunneling failed: ${responseText.split("\r\n")[0]}`);
}
if (rawClientData && rawClientData.byteLength > 0) {
await writer.write(rawClientData);
}
writer.releaseLock();
return tcpSocket;
}
async function retry() {
if (!prxIP) {
safeCloseWebSocket(webSocket);
return;
}
const parsed = parseProxy(prxIP);
if (!parsed) {
safeCloseWebSocket(webSocket);
return;
}
try {
let tcpSocket;
if (parsed.user && parsed.pass) {
tcpSocket = await connectViaHttpProxy(parsed, addressRemote, portRemote);
} else {
tcpSocket = await connectAndWrite(parsed.host, parsed.port);
}
tcpSocket.closed
.catch((error) => console.log("retry tcpSocket closed error", error))
.finally(() => safeCloseWebSocket(webSocket));
if (config.ZERO_COPY_MODE === "ON") {
remoteSocketToWSZeroCopy(tcpSocket, webSocket, responseHeader, null, log);
} else if (config.TCP_OPTIMIZE_MODE === "OFF") {
remoteSocketToWSStandard(tcpSocket, webSocket, responseHeader, null, log);
} else {
remoteSocketToWSOptimized(tcpSocket, webSocket, responseHeader, null, log);
}
} catch (e) {
log(`Upstream proxy error: ${e.message}`);
safeCloseWebSocket(webSocket);
}
}
// Jika format proxy mengandung user:pass, langsung lewat upstream tunnel
const parsedPrx = parseProxy(prxIP);
if (parsedPrx && parsedPrx.user && parsedPrx.pass) {
try {
const tcpSocket = await connectViaHttpProxy(parsedPrx, addressRemote, portRemote);
if (config.ZERO_COPY_MODE === "ON") {
remoteSocketToWSZeroCopy(tcpSocket, webSocket, responseHeader, null, log);
} else if (config.TCP_OPTIMIZE_MODE === "OFF") {
remoteSocketToWSStandard(tcpSocket, webSocket, responseHeader, null, log);
} else {
remoteSocketToWSOptimized(tcpSocket, webSocket, responseHeader, null, log);
}
} catch (err) {
log(`Upstream proxy direct attempt error: ${err.message}`);
safeCloseWebSocket(webSocket);
}
return;
}
// Mode Standard / Bypass
try {
const tcpSocket = await connectAndWrite(addressRemote, portRemote);
if (config.ZERO_COPY_MODE === "ON") {
remoteSocketToWSZeroCopy(tcpSocket, webSocket, responseHeader, retry, log);
} else if (config.TCP_OPTIMIZE_MODE === "OFF") {
remoteSocketToWSStandard(tcpSocket, webSocket, responseHeader, retry, log);
} else {
remoteSocketToWSOptimized(tcpSocket, webSocket, responseHeader, retry, log);
}
} catch (err) {
log(`Direct connect failed to ${addressRemote}:${portRemote}, switching to ProxyIP...`);
await retry();
}
}
// PIPELINE ZERO-COPY NATIVE DIRECT STREAM
async function remoteSocketToWSZeroCopy(remoteSocket, webSocket, responseHeader, retry, log) {
let header = responseHeader;
let hasIncomingData = false;
await remoteSocket.readable
.pipeTo(
new WritableStream({
write(chunk) {
hasIncomingData = true;
if (webSocket.readyState !== WS_READY_STATE_OPEN) return;
if (header && header.byteLength > 0) {
const combined = new Uint8Array(header.byteLength + chunk.byteLength);
combined.set(new Uint8Array(header), 0);
combined.set(new Uint8Array(chunk), header.byteLength);
webSocket.send(combined.buffer);
header = null;
} else {
webSocket.send(chunk);
}
},
close() { log(`remoteConnection!.readable closed (Zero-Copy)`); },
abort(reason) { console.error(`remoteConnection!.readable abort (Zero-Copy)`, reason); }
})
)
.catch((error) => {
safeCloseWebSocket(webSocket);
});
if (!hasIncomingData && retry) { retry(); }
}
async function remoteSocketToWSStandard(remoteSocket, webSocket, responseHeader, retry, log) {
let header = responseHeader;
let hasIncomingData = false;
await remoteSocket.readable
.pipeTo(
new WritableStream({
async write(chunk, controller) {
hasIncomingData = true;
if (webSocket.readyState !== WS_READY_STATE_OPEN) {
controller.error("webSocket.readyState is not open");
return;
}
if (header && header.byteLength > 0) {
const combined = new Uint8Array(header.byteLength + chunk.byteLength);
combined.set(new Uint8Array(header), 0);
combined.set(new Uint8Array(chunk), header.byteLength);
webSocket.send(combined.buffer);
header = null;
} else {
webSocket.send(chunk);
}
},
close() { log(`remoteConnection!.readable is close`); },
abort(reason) { console.error(`remoteConnection!.readable abort`, reason); },
})
)
.catch((error) => {
console.error(`remoteSocketToWS standard exception`, error);
safeCloseWebSocket(webSocket);
});
if (!hasIncomingData && retry) { retry(); }
}
async function remoteSocketToWSOptimized(remoteSocket, webSocket, responseHeader, retry, log) {
let header = responseHeader;
let hasIncomingData = false;
const optimizedWriter = new WritableStream({
async write(chunk, controller) {
hasIncomingData = true;
if (webSocket.readyState !== WS_READY_STATE_OPEN) {
controller.error("webSocket.readyState is not open");
return;
}
if (header && header.byteLength > 0) {
const combined = new Uint8Array(header.byteLength + chunk.byteLength);
combined.set(new Uint8Array(header), 0);
combined.set(new Uint8Array(chunk), header.byteLength);
webSocket.send(combined.buffer);
header = null;
} else {
webSocket.send(chunk);
}
},
close() { log(`remoteConnection!.readable is close (optimized)`); },
abort(reason) { console.error(`remoteConnection!.readable abort (optimized)`, reason); },
});
try {
await remoteSocket.readable.pipeTo(optimizedWriter);
} catch (error) {
console.error(`remoteSocketToWS optimized exception`, error);
safeCloseWebSocket(webSocket);
}
if (!hasIncomingData && retry) { retry(); }
}
async function handleDoHOutbound(dohUrl, dataChunk, webSocket, responseHeader, log) {
try {
let protocolHeader = responseHeader;
const dohResponse = await fetch(dohUrl, {
method: "POST",
headers: {
"Content-Type": "application/dns-message",
"Accept": "application/dns-message",
},
body: dataChunk,
});
if (dohResponse.ok && webSocket.readyState === WS_READY_STATE_OPEN) {
const dnsResponseBuffer = await dohResponse.arrayBuffer();
if (protocolHeader) {
webSocket.send(await new Blob([protocolHeader, dnsResponseBuffer]).arrayBuffer());
} else {
webSocket.send(dnsResponseBuffer);
}
}
} catch (e) {
console.error(`Error handling DoH Outbound: ${e.message}`);
}
}
async function handleUDPOutbound(targetAddress, targetPort, dataChunk, webSocket, responseHeader, log, config) {
try {
let protocolHeader = responseHeader;
let tcpSocket;
if (config.UDP_MODE === "NATIVE") {
tcpSocket = connect({
hostname: targetAddress,
port: targetPort,
});
const writer = tcpSocket.writable.getWriter();
await writer.write(dataChunk);
writer.releaseLock();
} else {
const relayHost = config.UDP_RELAY_HOST || "udp-relay.hobihaus.space";
const relayPort = config.UDP_RELAY_PORT || 7300;
tcpSocket = connect({
hostname: relayHost,
port: relayPort,
});
const header = `udp:${targetAddress}:${targetPort}`;
const headerBuffer = new TextEncoder().encode(header);
const separator = new Uint8Array([0x7c]);
const relayMessage = new Uint8Array(headerBuffer.length + separator.length + dataChunk.byteLength);
relayMessage.set(headerBuffer, 0);
relayMessage.set(separator, headerBuffer.length);
relayMessage.set(new Uint8Array(dataChunk), headerBuffer.length + separator.length);
const writer = tcpSocket.writable.getWriter();
await writer.write(relayMessage);
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 aborted due to ${reason}`);
},
}),
);
} catch (e) {
console.error(`Error while handling UDP outbound: ${e.message}`);
}
}
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);
});
webSocketServer.addEventListener("close", () => {
safeCloseWebSocket(webSocketServer);
if (readableStreamCancel) {
return;
}
controller.close();
});
webSocketServer.addEventListener("error", (err) => {
log("webSocketServer has error");
controller.error(err);
});
const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader);
if (error) {
controller.error(error);
} 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;
}
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 checkPrxHealth(prxIP, prxPort) {
const req = await fetch(`https://id1.foolvpn.web.id/api/v1/check?ip=${prxIP}:${prxPort}`);
return await req.json();
}
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]];
}
}
function getFlagEmoji(isoCode) {
const codePoints = isoCode
.toUpperCase()
.split("")
.map((char) => 127397 + char.charCodeAt(0));
return String.fromCodePoint(...codePoints);
}
// CRYPTO HELPERS
async function md5(...inputs) {
const combined = new Uint8Array(inputs.reduce((acc, input) => acc + input.length, 0));
let offset = 0;
for (const input of inputs) {
combined.set(new Uint8Array(input), offset);
offset += input.length;
}
const hashBuffer = await crypto.subtle.digest("MD5", combined);
return new Uint8Array(hashBuffer);
}
async function sha256(input) {
const hashBuffer = await crypto.subtle.digest("SHA-256", input);
return new Uint8Array(hashBuffer);
}
async function kdf(key, path) {
async function hmacSha256(key, data) {
const hmacKey = await crypto.subtle.importKey("raw", key, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
const signature = await crypto.subtle.sign("HMAC", hmacKey, data);
return new Uint8Array(signature);
}
async function recursiveHash(keyBytes, innerHashFn) {
return async (data) => {
const ipad = new Uint8Array(64);
const opad = new Uint8Array(64);
ipad.set(keyBytes.slice(0, Math.min(64, keyBytes.length)));
opad.set(keyBytes.slice(0, Math.min(64, keyBytes.length)));
for (let i = 0; i < 64; i++) {
ipad[i] ^= 0x36;
opad[i] ^= 0x5c;
}
const innerData = new Uint8Array(ipad.length + data.length);
innerData.set(ipad);
innerData.set(data, ipad.length);
const innerResult = await innerHashFn(innerData);
const outerData = new Uint8Array(opad.length + innerResult.length);
outerData.set(opad);
outerData.set(innerResult, opad.length);
return await innerHashFn(outerData);
};
}
const sha256Hash = async (data) => {
return new Uint8Array(await crypto.subtle.digest("SHA-256", data));
};
let currentHashFn = await recursiveHash(new TextEncoder().encode("VMess AEAD KDF"), sha256Hash);
for (const salt of path) {
const saltBytes = typeof salt === "string" ? new TextEncoder().encode(salt) : new Uint8Array(salt);
currentHashFn = await recursiveHash(saltBytes, currentHashFn);
}
return await currentHashFn(key);
}
async function aesGcmDecrypt(key, nonce, data, aad) {
const cryptoKey = await crypto.subtle.importKey("raw", key, { name: "AES-GCM" }, false, ["decrypt"]);
try {
const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv: nonce, additionalData: aad }, cryptoKey, data);
return new Uint8Array(decrypted);
} catch (e) {
throw new Error("AEAD decryption failed: " + e.message);
}
}
async function aesGcmEncrypt(key, nonce, data, aad) {
const cryptoKey = await crypto.subtle.importKey("raw", key, { name: "AES-GCM" }, false, ["encrypt"]);
const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv: nonce, additionalData: aad }, cryptoKey, data);
return new Uint8Array(encrypted);
}