import { connect } from "cloudflare:sockets";
let serviceName = "";
let APP_DOMAIN = "";
let prxIP = "";
let cachedPrxList = [];
let memoryProxyCache = {
data: [],
lastUpdated: 0,
ttl: 10 * 60 * 1000
};
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";
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: "wsudprelay.up.railway.app",
UDP_RELAY_PORT: "443",
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",
ZERO_COPY_MODE: "ON",
EARLY_FLUSH_MODE: "ON",
STREAM_BUFFER_MODE: "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: "Railway WS Relay (wsudprelay.up.railway.app:443)", host: "wsudprelay.up.railway.app", port: "443", 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",
};
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==");
const RELAY_MAGIC = new TextEncoder().encode("VLRLY004");
const RELAY_MODE_PACKET_UDP = 0x03;
const ATYP_IPV4 = 0x01;
const ATYP_DOMAIN = 0x02;
const ATYP_IPV6 = 0x03;
function concatBytes(...parts) {
let len = 0;
for (const p of parts) if (p) len += p.byteLength;
const out = new Uint8Array(len);
let off = 0;
for (const p of parts) {
if (p && p.byteLength) {
out.set(new Uint8Array(p), off);
off += p.byteLength;
}
}
return out;
}
function parseRelayEndpoint(bytes, offset = 0) {
const b = new Uint8Array(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 (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 (b.byteLength < p + len) return null;
return { address: new TextDecoder("utf-8").decode(b.subarray(p, p + len)), port, addressType: atyp, next: p + len };
}
if (atyp === ATYP_IPV6) {
if (b.byteLength < p + 16) return null;
const parts = [];
for (let i = 0; i < 16; i += 2) parts.push(((b[p + i] << 8) | b[p + i + 1]).toString(16));
return { address: parts.join(":"), port, addressType: atyp, next: p + 16 };
}
return null;
}
function encodeRelayEndpoint(address, port) {
const p = Number(port);
const head = new Uint8Array([(p >>> 8) & 0xff, p & 0xff]);
if (/^(?:\d{1,3}\.){3}\d{1,3}$/.test(String(address))) {
const ipBytes = new Uint8Array(String(address).split(".").map(Number));
const out = new Uint8Array(head.length + 1 + ipBytes.length);
out.set(head, 0);
out[head.length] = ATYP_IPV4;
out.set(ipBytes, head.length + 1);
return out;
}
if (String(address).includes(":")) {
const parts = String(address).split(":");
const out = new Uint8Array(head.length + 1 + 16);
out.set(head, 0);
out[head.length] = ATYP_IPV6;
for (let i = 0; i < 8; i++) {
const val = parseInt(parts[i] || "0", 16);
out[head.length + 1 + (i * 2)] = (val >>> 8) & 0xff;
out[head.length + 1 + (i * 2) + 1] = val & 0xff;
}
return out;
}
const d = new TextEncoder().encode(String(address));
const out = new Uint8Array(head.length + 2 + d.length);
out.set(head, 0);
out[head.length] = ATYP_DOMAIN;
out[head.length + 1] = d.length;
out.set(d, head.length + 2);
return out;
}
function parseSocksAddress(bytes, cursor = 0) {
const b = new Uint8Array(bytes);
if (b.byteLength < cursor + 1) return null;
const atyp = b[cursor++];
let address = "";
if (atyp === 0x01) {
if (b.byteLength < cursor + 6) return null;
address = Array.from(b.subarray(cursor, cursor + 4)).join(".");
cursor += 4;
} else if (atyp === 0x03) {
if (b.byteLength < cursor + 1) return null;
const len = b[cursor++];
if (!len || b.byteLength < cursor + len + 2) return null;
address = new TextDecoder("utf-8").decode(b.subarray(cursor, cursor + len));
cursor += len;
} else if (atyp === 0x04) {
if (b.byteLength < cursor + 18) return null;
const parts = [];
for (let i = 0; i < 16; i += 2) parts.push(((b[cursor + i] << 8) | b[cursor + i + 1]).toString(16));
address = parts.join(":");
cursor += 16;
} else return null;
const port = (b[cursor] << 8) | b[cursor + 1];
cursor += 2;
return { address, port, addressType: atyp, next: cursor };
}
function encodeSocksAddress(endpoint) {
const address = endpoint.address;
const port = Number(endpoint.port);
let addr;
if (/^(?:\d{1,3}\.){3}\d{1,3}$/.test(String(address))) {
addr = concatBytes(new Uint8Array([0x01]), new Uint8Array(String(address).split(".").map(Number)));
} else if (String(address).includes(":")) {
const parts = String(address).split(":");
const b = new Uint8Array(16);
for (let i = 0; i < 8; i++) {
const val = parseInt(parts[i] || "0", 16);
b[i * 2] = (val >>> 8) & 0xff;
b[i * 2 + 1] = val & 0xff;
}
addr = concatBytes(new Uint8Array([0x04]), b);
} else {
const d = new TextEncoder().encode(String(address));
addr = concatBytes(new Uint8Array([0x03, d.byteLength]), d);
}
return concatBytes(addr, new Uint8Array([(port >>> 8) & 0xff, port & 0xff]));
}
function encodeTrojanUdpPacket(endpoint, payload) {
const p = new Uint8Array(payload);
return concatBytes(
encodeSocksAddress(endpoint),
new Uint8Array([(p.byteLength >>> 8) & 0xff, p.byteLength & 0xff, 0x0d, 0x0a]),
p
);
}
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) {}
}
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) {}
}
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) {}
}
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 };
} 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) {
try {
const kvPrx = await getKVPrxList(config);
if (kvPrx && kvPrx["SG"] && Array.isArray(kvPrx["SG"]) && kvPrx["SG"].length > 0) {
const sgPool = kvPrx["SG"];
return sgPool[Math.floor(Math.random() * sgPool.length)];
}
} catch (e) {}
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";
return `${targetList[0].prxIP}:${targetList[0].prxPort}`;
}
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,
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"
}
});
}
// FULL HTML UI NAUTICA (DENGAN DYNAMIC CURRENT BROWSER URL)
const htmlContent = `
Nautica Worker Dashboard
Smart Routing (/MULTI) & Browser-Host Sync Active
➕ Daftarkan Custom Proxy Baru
📋 Daftar Custom Proxy Tersimpan
`;
if (url.pathname === "/ui" || url.pathname === "/ui/" || url.pathname === "/") {
return new Response(htmlContent, {
headers: {
"Content-Type": "text/html; charset=utf-8",
"Cache-Control": "no-store, no-cache, must-revalidate",
...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() === "MULTI") return await websocketHandler(request, currentConfig, "MULTI", "");
if (cleanPath.toUpperCase() === "DIRECT") return await websocketHandler(request, currentConfig, "DIRECT", "");
if (cleanPath.toUpperCase() === "SEMUA") {
const picked = await getBestInMemoryProxy(currentConfig);
return await websocketHandler(request, currentConfig, "PROXY", picked);
}
const proxyAuthMatch = cleanPath.match(/^([a-zA-Z0-9.-]+:\d+(@[a-zA-Z0-9._~%!$&'()*+,;=-]+:[a-zA-Z0-9._~%!$&'()*+,;=-]+)?)$/);
if (proxyAuthMatch) return await websocketHandler(request, currentConfig, "PROXY", proxyAuthMatch[1]);
const prxMatch = rawPath.match(/^\/(.+[:=-]\d+(@.+:.+)?)$/);
if (prxMatch) return await websocketHandler(request, currentConfig, "PROXY", prxMatch[1]);
if (/[a-z]/.test(cleanPath)) {
const picked = await getBestSgProxyInternal(currentConfig);
return await websocketHandler(request, currentConfig, "PROXY", picked);
}
}
if (url.pathname.startsWith("/sub")) return Response.redirect(SUB_PAGE_URL + `?host=${APP_DOMAIN}`, 301);
return await reverseWeb(request, currentConfig.REVERSE_PRX_TARGET);
} catch (err) {
return new Response(`Error: ${err.toString()}`, { status: 500, headers: CORS_HEADER_OPTIONS });
}
}
};
async function websocketHandler(request, config, routingMode = "PROXY", assignedProxy = "") {
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 udpRelaySession = null;
let isDNS = false;
readableWebSocketStream.pipeTo(
new WritableStream({
async write(chunk, controller) {
if (isDNS && config.DNS_TYPE === "DOH") {
return handleDoHOutbound(config.DOH_URL, chunk, webSocket, null, log);
}
if (remoteSocketWrapper.value) {
const writer = remoteSocketWrapper.value.writable.getWriter();
await writer.write(chunk);
writer.releaseLock();
return;
}
if (udpRelaySession) {
await udpRelaySession.write(chunk);
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 && config.DNS_TYPE === "DOH") {
isDNS = true;
return handleDoHOutbound(config.DOH_URL, chunk, webSocket, responseHeader, log);
}
udpRelaySession = new ExactPacketUdpRelay(
config,
protocol,
protocolHeader.addressRemote,
protocolHeader.portRemote,
webSocket,
responseHeader
);
await udpRelaySession.init(protocolHeader.rawClientData);
return;
}
handleTCPOutBound(
remoteSocketWrapper,
protocolHeader.addressRemote,
protocolHeader.portRemote,
protocolHeader.rawClientData,
webSocket,
responseHeader,
log,
config,
routingMode,
assignedProxy
);
},
close() {
if (udpRelaySession) udpRelaySession.close();
log(`readableWebSocketStream is closed`);
},
abort(reason) {
if (udpRelaySession) udpRelaySession.close();
log(`readableWebSocketStream is aborted`, reason);
}
})
).catch(err => log("Stream pipe error", err));
return new Response(null, { status: 101, webSocket: client });
}
// 1:1 PACKET-UDP ENGINE
class ExactPacketUdpRelay {
constructor(config, protocol, defaultAddr, defaultPort, clientWs, responseHeader) {
this.config = config;
this.protocol = protocol;
this.defaultAddr = defaultAddr;
this.defaultPort = defaultPort;
this.clientWs = clientWs;
this.responseHeader = responseHeader;
this.relayWs = null;
this.isReady = false;
this.queue = [];
this.closed = false;
this.inBuffer = new Uint8Array(0);
this.outBuffer = new Uint8Array(0);
}
async init(initialData) {
try {
const relayHost = (this.config.UDP_RELAY_HOST || "wsudprelay.up.railway.app")
.replace(/^https?:\/\//i, "")
.replace(/^wss?:\/\//i, "")
.replace(/\/+$/, "");
const wsUrl = `https://${relayHost}/`;
const resp = await fetch(wsUrl, { headers: { Upgrade: "websocket" } });
const rWs = resp.webSocket;
if (!rWs) throw new Error("Failed to connect to Railway relay");
rWs.accept();
this.relayWs = rWs;
rWs.send(concatBytes(RELAY_MAGIC, new Uint8Array([RELAY_MODE_PACKET_UDP])));
let gotAck = false;
rWs.addEventListener("message", async (event) => {
const chunk = new Uint8Array(event.data);
if (!gotAck) {
if (chunk.byteLength > 0 && chunk[0] === 0) {
gotAck = true;
this.isReady = true;
this.flush();
if (chunk.byteLength > 1) {
this.outBuffer = concatBytes(this.outBuffer, chunk.subarray(1));
}
} else {
this.close();
return;
}
} else {
this.outBuffer = concatBytes(this.outBuffer, chunk);
}
while (this.outBuffer.byteLength > 0) {
const ep = parseRelayEndpoint(this.outBuffer, 0);
if (!ep || this.outBuffer.byteLength < ep.next + 2) break;
const payloadLen = (this.outBuffer[ep.next] << 8) | this.outBuffer[ep.next + 1];
const totalEnd = ep.next + 2 + payloadLen;
if (this.outBuffer.byteLength < totalEnd) break;
const payload = this.outBuffer.slice(ep.next + 2, totalEnd);
this.outBuffer = this.outBuffer.slice(totalEnd);
this.emitToClient(ep, payload);
}
});
rWs.addEventListener("close", () => this.close());
rWs.addEventListener("error", () => this.close());
if (initialData && initialData.byteLength > 0) {
await this.write(initialData);
}
} catch (e) {
this.close();
}
}
async write(chunk) {
if (this.closed) return;
this.inBuffer = concatBytes(this.inBuffer, chunk);
while (this.inBuffer.byteLength > 0) {
let targetAddr = this.defaultAddr;
let targetPort = this.defaultPort;
let payload = null;
let consumed = 0;
if (this.protocol === atob(horse)) {
const ep = parseSocksAddress(this.inBuffer, 0);
if (!ep || this.inBuffer.byteLength < ep.next + 4) break;
const len = (this.inBuffer[ep.next] << 8) | this.inBuffer[ep.next + 1];
const end = ep.next + 4 + len;
if (this.inBuffer.byteLength < end) break;
targetAddr = ep.address;
targetPort = ep.port;
payload = this.inBuffer.slice(ep.next + 4, end);
consumed = end;
} else {
if (this.inBuffer.byteLength < 2) break;
const len = (this.inBuffer[0] << 8) | this.inBuffer[1];
if (this.inBuffer.byteLength < 2 + len) break;
payload = this.inBuffer.slice(2, 2 + len);
consumed = 2 + len;
}
this.inBuffer = this.inBuffer.slice(consumed);
const epBytes = encodeRelayEndpoint(targetAddr, targetPort);
const lenBytes = new Uint8Array([(payload.byteLength >>> 8) & 0xff, payload.byteLength & 0xff]);
const packet = concatBytes(epBytes, lenBytes, payload);
if (this.isReady && this.relayWs && this.relayWs.readyState === WS_READY_STATE_OPEN) {
this.relayWs.send(packet);
} else {
this.queue.push(packet);
}
}
}
emitToClient(ep, payload) {
if (this.clientWs.readyState !== WS_READY_STATE_OPEN) return;
let packetForClient;
if (this.protocol === atob(horse)) {
packetForClient = encodeTrojanUdpPacket(ep, payload);
} else {
const lenBytes = new Uint8Array([(payload.byteLength >>> 8) & 0xff, payload.byteLength & 0xff]);
packetForClient = concatBytes(lenBytes, payload);
}
if (this.responseHeader) {
this.clientWs.send(concatBytes(this.responseHeader, packetForClient).buffer);
this.responseHeader = null;
} else {
this.clientWs.send(packetForClient.buffer || packetForClient);
}
}
flush() {
while (this.queue.length > 0) {
const p = this.queue.shift();
if (this.relayWs && this.relayWs.readyState === WS_READY_STATE_OPEN) {
this.relayWs.send(p);
}
}
}
close() {
if (this.closed) return;
this.closed = true;
try { this.relayWs?.close(); } catch (e) {}
try { safeCloseWebSocket(this.clientWs); } catch (e) {}
}
}
async function handleTCPOutBound(
remoteSocket, addressRemote, portRemote, rawClientData, webSocket, responseHeader, log, config, routingMode, assignedProxy
) {
async function connectAndWrite(address, port, timeoutMs = 450) {
const tcpSocket = connect({ hostname: address, port: parseInt(port, 10) });
remoteSocket.value = tcpSocket;
if (timeoutMs > 0) {
await Promise.race([
tcpSocket.opened,
new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeoutMs))
]);
}
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;
const writer = tcpSocket.writable.getWriter();
let connectReq = `CONNECT ${targetHost}:${targetPort} HTTP/1.1\r\nHost: ${targetHost}:${targetPort}\r\n`;
if (parsedPrx.user && parsedPrx.pass) {
connectReq += `Proxy-Authorization: Basic ${btoa(`${parsedPrx.user}:${parsedPrx.pass}`)}\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();
if (!new TextDecoder().decode(resChunk || new Uint8Array(0)).includes(" 200 ")) {
throw new Error("Proxy tunnel failed");
}
if (rawClientData && rawClientData.byteLength > 0) await writer.write(rawClientData);
writer.releaseLock();
return tcpSocket;
}
async function retryProxyFallback() {
try {
const backupPrx = assignedProxy || (await getBestSgProxyInternal(config));
const parsed = parseProxy(backupPrx);
if (!parsed) return safeCloseWebSocket(webSocket);
let tcpSocket = (parsed.user && parsed.pass)
? await connectViaHttpProxy(parsed, addressRemote, portRemote)
: await connectAndWrite(parsed.host, parsed.port, 0);
tcpSocket.closed.catch(() => {}).finally(() => safeCloseWebSocket(webSocket));
remoteSocketToWSZeroCopy(tcpSocket, webSocket, responseHeader, null, log);
} catch (e) {
safeCloseWebSocket(webSocket);
}
}
if (routingMode === "PROXY" && assignedProxy) {
const parsedPrx = parseProxy(assignedProxy);
if (parsedPrx && parsedPrx.user && parsedPrx.pass) {
try {
const tcpSocket = await connectViaHttpProxy(parsedPrx, addressRemote, portRemote);
remoteSocketToWSZeroCopy(tcpSocket, webSocket, responseHeader, null, log);
return;
} catch (e) {
safeCloseWebSocket(webSocket);
return;
}
}
}
try {
const timeoutLimit = (routingMode === "MULTI") ? 450 : 0;
const tcpSocket = await connectAndWrite(addressRemote, portRemote, timeoutLimit);
remoteSocketToWSZeroCopy(tcpSocket, webSocket, responseHeader, (routingMode === "MULTI" || routingMode === "PROXY") ? retryProxyFallback : null, log);
} catch (err) {
if (routingMode === "MULTI" || routingMode === "PROXY") await retryProxyFallback();
else safeCloseWebSocket(webSocket);
}
}
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() {},
abort() {}
})
).catch(() => safeCloseWebSocket(webSocket));
if (!hasIncomingData && retry) retry();
}
async function handleDoHOutbound(dohUrl, dataChunk, webSocket, responseHeader, log) {
try {
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 (responseHeader) webSocket.send(await new Blob([responseHeader, dnsResponseBuffer]).arrayBuffer());
else webSocket.send(dnsResponseBuffer);
}
} catch (e) {}
}
function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) {
let readableStreamCancel = false;
return new ReadableStream({
start(controller) {
webSocketServer.addEventListener("message", (event) => {
if (readableStreamCancel) return;
controller.enqueue(event.data);
});
webSocketServer.addEventListener("close", () => {
safeCloseWebSocket(webSocketServer);
if (readableStreamCancel) return;
controller.close();
});
webSocketServer.addEventListener("error", (err) => 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;
readableStreamCancel = true;
safeCloseWebSocket(webSocketServer);
},
});
}
function safeCloseWebSocket(socket) {
try {
if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) socket.close();
} catch (error) {}
}
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) {
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++);
if (version !== 1) return { hasError: true, message: `Invalid protocol: ${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++);
const isUDP = cmd !== 0x01;
const portRemote = view.getUint16(offset, false); offset += 2;
const addressType = view.getUint8(offset++);
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++);
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,
encIv,
};
} catch (e) {
return { hasError: true, message: "Stream 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: ${addressType}` };
}
const portIndex = addressValueIndex + addressLength;
const portRemote = new DataView(ssBuffer.slice(portIndex, portIndex + 2)).getUint16(0);
return {
hasError: false,
addressRemote: addressValue,
addressType,
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 === 2) isUDP = true;
else if (cmd !== 1) return { hasError: true, message: `cmd ${cmd} unsupported` };
const portIndex = 18 + optLength + 1;
const portRemote = new DataView(buffer.slice(portIndex, portIndex + 2)).getUint16(0);
let addressIndex = portIndex + 2;
const addressType = new Uint8Array(buffer.slice(addressIndex, addressIndex + 1))[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: `invalid addressType ${addressType}` };
}
return {
hasError: false,
addressRemote: addressValue,
addressType,
portRemote,
rawDataIndex: addressValueIndex + addressLength,
rawClientData: buffer.slice(addressValueIndex + addressLength),
version: new Uint8Array([version[0], 0]),
isUDP,
};
}
function readHorseHeader(buffer) {
const dataBuffer = buffer.slice(58);
if (dataBuffer.byteLength < 6) return { hasError: true, message: "invalid 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 cmd");
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 ${addressType}` };
}
const portIndex = addressValueIndex + addressLength;
const portRemote = new DataView(dataBuffer.slice(portIndex, portIndex + 2)).getUint16(0);
return {
hasError: false,
addressRemote: addressValue,
addressType,
portRemote,
rawDataIndex: portIndex + 4,
rawClientData: dataBuffer.slice(portIndex + 4),
version: null,
isUDP,
};
}
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("");
}
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 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) => 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 decrypt error: " + 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);
}