"use strict"; "require form"; "require baseclass"; "require dom"; "require fs"; "require rpc"; "require uci"; "require ui"; "require view.tachyon.main as main"; const UCI_PACKAGE = main.TACHYON_UCI_PACKAGE; const ROUTING_SECTION_ACTIONS = [ "connection", "proxy", "outbound", "vpn", "byedpi", "zapret", "zapret2", ]; const callNetworkInterfaceDump = rpc.declare({ object: "network.interface", method: "dump", expect: { interface: [] }, }); let defaultPublicHostPromise = null; let defaultPublicHostCache = null; function asList(value) { if (!value) { return []; } return Array.isArray(value) ? value.filter(Boolean) : [`${value}`]; } const QR_EC_CODEWORDS_PER_BLOCK_LOW = [ -1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, ]; const QR_NUM_ERROR_CORRECTION_BLOCKS_LOW = [ -1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25, ]; function qrGetBit(value, index) { return ((value >>> index) & 1) !== 0; } function qrAppendBits(buffer, value, length) { for (let i = length - 1; i >= 0; i -= 1) { buffer.push(qrGetBit(value, i)); } } function qrRawDataModules(version) { let result = (16 * version + 128) * version + 64; if (version >= 2) { const numAlign = Math.floor(version / 7) + 2; result -= (25 * numAlign - 10) * numAlign - 55; if (version >= 7) { result -= 36; } } return result; } function qrRawCodewords(version) { return Math.floor(qrRawDataModules(version) / 8); } function qrDataCodewords(version) { return ( qrRawCodewords(version) - QR_EC_CODEWORDS_PER_BLOCK_LOW[version] * QR_NUM_ERROR_CORRECTION_BLOCKS_LOW[version] ); } function qrEncodeText(text) { return Array.from(new TextEncoder().encode(text)); } function qrBuildDataCodewords(bytes, version) { const capacity = qrDataCodewords(version); const bits = []; const countBits = version <= 9 ? 8 : 16; qrAppendBits(bits, 4, 4); qrAppendBits(bits, bytes.length, countBits); bytes.forEach((value) => qrAppendBits(bits, value, 8)); qrAppendBits(bits, 0, Math.min(4, capacity * 8 - bits.length)); while (bits.length % 8 !== 0) { bits.push(false); } const result = []; for (let i = 0; i < bits.length; i += 8) { let value = 0; for (let j = 0; j < 8; j += 1) { value = (value << 1) | (bits[i + j] ? 1 : 0); } result.push(value); } for (let pad = 0xec; result.length < capacity; pad ^= 0xfd) { result.push(pad); } return result; } function qrMultiply(x, y) { let z = 0; for (let i = 7; i >= 0; i -= 1) { z = (z << 1) ^ ((z >>> 7) * 0x11d); z ^= ((y >>> i) & 1) * x; } return z; } function qrReedSolomonDivisor(degree) { const result = Array(degree).fill(0); result[degree - 1] = 1; let root = 1; for (let i = 0; i < degree; i += 1) { for (let j = 0; j < degree; j += 1) { result[j] = qrMultiply(result[j], root); if (j + 1 < degree) { result[j] ^= result[j + 1]; } } root = qrMultiply(root, 2); } return result; } function qrReedSolomonRemainder(data, divisor) { const result = Array(divisor.length).fill(0); data.forEach((value) => { const factor = value ^ result.shift(); result.push(0); for (let i = 0; i < result.length; i += 1) { result[i] ^= qrMultiply(divisor[i], factor); } }); return result; } function qrAddErrorCorrection(data, version) { const numBlocks = QR_NUM_ERROR_CORRECTION_BLOCKS_LOW[version]; const blockEccLen = QR_EC_CODEWORDS_PER_BLOCK_LOW[version]; const rawCodewords = qrRawCodewords(version); const numShortBlocks = numBlocks - (rawCodewords % numBlocks); const shortBlockDataLen = Math.floor(rawCodewords / numBlocks) - blockEccLen; const divisor = qrReedSolomonDivisor(blockEccLen); const blocks = []; for (let i = 0, offset = 0; i < numBlocks; i += 1) { const dataLen = shortBlockDataLen + (i < numShortBlocks ? 0 : 1); const blockData = data.slice(offset, offset + dataLen); offset += dataLen; blocks.push({ data: blockData, ecc: qrReedSolomonRemainder(blockData, divisor), }); } const result = []; const maxDataLen = Math.max(...blocks.map((block) => block.data.length)); for (let i = 0; i < maxDataLen; i += 1) { blocks.forEach((block) => { if (i < block.data.length) { result.push(block.data[i]); } }); } for (let i = 0; i < blockEccLen; i += 1) { blocks.forEach((block) => result.push(block.ecc[i])); } return result; } function qrAlignmentPatternPositions(version) { if (version === 1) { return []; } const size = version * 4 + 17; const numAlign = Math.floor(version / 7) + 2; const step = version === 32 ? 26 : Math.ceil((version * 4 + 4) / (numAlign * 2 - 2)) * 2; const result = [6]; for (let pos = size - 7; result.length < numAlign; pos -= step) { result.splice(1, 0, pos); } return result; } function qrMakeMatrix(version, dataCodewords) { const size = version * 4 + 17; const modules = Array.from({ length: size }, () => Array(size).fill(false)); const isFunction = Array.from({ length: size }, () => Array(size).fill(false), ); function setFunction(x, y, dark) { modules[y][x] = dark; isFunction[y][x] = true; } function drawFinder(cx, cy) { for (let dy = -4; dy <= 4; dy += 1) { for (let dx = -4; dx <= 4; dx += 1) { const x = cx + dx; const y = cy + dy; if (x < 0 || x >= size || y < 0 || y >= size) { continue; } const dist = Math.max(Math.abs(dx), Math.abs(dy)); setFunction(x, y, dist !== 2 && dist !== 4); } } } function drawAlignment(cx, cy) { for (let dy = -2; dy <= 2; dy += 1) { for (let dx = -2; dx <= 2; dx += 1) { setFunction( cx + dx, cy + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1, ); } } } drawFinder(3, 3); drawFinder(size - 4, 3); drawFinder(3, size - 4); for (let i = 8; i < size - 8; i += 1) { setFunction(6, i, i % 2 === 0); setFunction(i, 6, i % 2 === 0); } const alignPositions = qrAlignmentPatternPositions(version); alignPositions.forEach((x) => { alignPositions.forEach((y) => { const overlapsFinder = (x === 6 && y === 6) || (x === 6 && y === size - 7) || (x === size - 7 && y === 6); if (!overlapsFinder) { drawAlignment(x, y); } }); }); if (version >= 7) { let rem = version; for (let i = 0; i < 12; i += 1) { rem = (rem << 1) ^ ((rem >>> 11) * 0x1f25); } const bits = (version << 12) | rem; for (let i = 0; i < 18; i += 1) { const bit = qrGetBit(bits, i); const a = size - 11 + (i % 3); const b = Math.floor(i / 3); setFunction(a, b, bit); setFunction(b, a, bit); } } function drawFormatBits(mask) { const formatData = (1 << 3) | mask; let rem = formatData; for (let i = 0; i < 10; i += 1) { rem = (rem << 1) ^ ((rem >>> 9) * 0x537); } const formatBits = ((formatData << 10) | rem) ^ 0x5412; for (let i = 0; i <= 5; i += 1) { setFunction(8, i, qrGetBit(formatBits, i)); } setFunction(8, 7, qrGetBit(formatBits, 6)); setFunction(8, 8, qrGetBit(formatBits, 7)); setFunction(7, 8, qrGetBit(formatBits, 8)); for (let i = 9; i < 15; i += 1) { setFunction(14 - i, 8, qrGetBit(formatBits, i)); } for (let i = 0; i < 8; i += 1) { setFunction(size - 1 - i, 8, qrGetBit(formatBits, i)); } for (let i = 8; i < 15; i += 1) { setFunction(8, size - 15 + i, qrGetBit(formatBits, i)); } setFunction(8, size - 8, true); } drawFormatBits(0); const codewords = qrAddErrorCorrection(dataCodewords, version); let bitIndex = 0; for (let right = size - 1; right >= 1; right -= 2) { if (right === 6) { right = 5; } for (let vert = 0; vert < size; vert += 1) { for (let j = 0; j < 2; j += 1) { const x = right - j; const upward = ((right + 1) & 2) === 0; const y = upward ? size - 1 - vert : vert; if (!isFunction[y][x] && bitIndex < codewords.length * 8) { modules[y][x] = qrGetBit( codewords[Math.floor(bitIndex / 8)], 7 - (bitIndex % 8), ); bitIndex += 1; } } } } for (let y = 0; y < size; y += 1) { for (let x = 0; x < size; x += 1) { if (!isFunction[y][x] && (x + y) % 2 === 0) { modules[y][x] = !modules[y][x]; } } } drawFormatBits(0); return modules; } function qrSvgDataUri(text) { const bytes = qrEncodeText(text); let version = 1; for (; version <= 40; version += 1) { const countBits = version <= 9 ? 8 : 16; if (4 + countBits + bytes.length * 8 <= qrDataCodewords(version) * 8) { break; } } if (version > 40) { return ""; } const matrix = qrMakeMatrix(version, qrBuildDataCodewords(bytes, version)); const border = 4; const size = matrix.length; const viewSize = size + border * 2; const path = []; matrix.forEach((row, y) => { row.forEach((dark, x) => { if (dark) { path.push(`M${x + border},${y + border}h1v1h-1z`); } }); }); const svg = ``; return `data:image/svg+xml,${encodeURIComponent(svg)}`; } const SERVER_STYLE_ID = "tachyon-server-styles"; const DEFAULT_SERVER_PROTOCOL = "tailscale"; const FALLBACK_SERVER_PROTOCOL = "vless"; const STREAM_PROTOCOLS = ["vless", "vmess", "trojan"]; const PORT_PROTOCOLS = [ "shadowsocks", "socks", "vmess", "vless", "trojan", "hysteria2", "awg", ]; const EXTENDED_PORT_PROTOCOLS = ["mtproto"]; const PASSWORD_PROTOCOLS = ["shadowsocks", "socks", "trojan", "hysteria2"]; const BASE_PROTOCOL_LABELS = { vless: "VLESS", shadowsocks: "Shadowsocks", socks: "SOCKS", vmess: "VMess", trojan: "Trojan", hysteria2: "Hysteria2", awg: "AmneziaWG", }; const EXTENDED_PROTOCOL_LABELS = { mtproto: "MTProto", }; const CUSTOM_PROTOCOL_LABELS = { json_inbound: "JSON Inbound", }; const TAILSCALE_PROTOCOL_LABELS = { tailscale: "Tailscale", }; const PROTOCOL_LABELS = { ...BASE_PROTOCOL_LABELS, ...EXTENDED_PROTOCOL_LABELS, ...CUSTOM_PROTOCOL_LABELS, ...TAILSCALE_PROTOCOL_LABELS, }; const SECURITY_BY_PROTOCOL = { vless: ["reality", "tls", "none"], vmess: ["tls", "none"], trojan: ["tls", "none"], hysteria2: ["tls"], shadowsocks: ["none"], socks: ["none"], mtproto: ["none"], tailscale: ["none"], json_inbound: ["none"], awg: ["none"], }; function getSecurityLabel(security) { if (security === "reality") { return "Reality"; } if (security === "tls") { return "TLS"; } return _("None"); } function injectServerStyles() { if (document.getElementById(SERVER_STYLE_ID)) { return; } document.head.appendChild( E( "style", { id: SERVER_STYLE_ID }, ` .fkp-server-icon-button{box-sizing:border-box;vertical-align:middle} .fkp-server-icon-button__glyph{display:inline-block;line-height:1;transform:scale(1.55);transform-origin:center} .fkp-server-icon-button--done{outline:2px solid var(--success-color,#37a969);outline-offset:1px} .fkp-server-validation-summary{margin:0 0 12px} #cbi-${UCI_PACKAGE}-server > h3:nth-child(1){display:none} #cbi-${UCI_PACKAGE}-server > .cbi-section-remove{margin-bottom:-32px} #cbi-${UCI_PACKAGE}-server .cbi-section-actions > div{display:inline-flex;align-items:center;gap:4px} #cbi-${UCI_PACKAGE}-server .cbi-section-actions{text-align:right} .fkp-server-info-modal{display:flex;flex-direction:column;gap:14px;width:min(560px,calc(100vw - 32px));max-width:100%;box-sizing:border-box} .fkp-server-info-modal__container{display:flex;gap:24px;align-items:flex-start;width:100%;box-sizing:border-box} .fkp-server-info-modal__qr-col{display:flex;flex-direction:column;align-items:center;gap:12px;flex-shrink:0;width:180px} .fkp-server-info-modal__qr-image{width:180px;height:180px;max-width:min(180px,60vw);max-height:min(180px,60vw);display:block;image-rendering:pixelated} .fkp-server-info-modal__details-col{display:flex;flex-direction:column;gap:16px;flex-grow:1;min-width:0;box-sizing:border-box} .fkp-server-info-modal__details-grid{display:flex;flex-direction:column;gap:10px;background:rgba(128,128,128,0.08);padding:14px;border:1px solid rgba(128,128,128,0.15);box-sizing:border-box} .fkp-server-info-modal__detail-item{display:flex;justify-content:space-between;align-items:center;gap:16px} .fkp-server-info-modal__detail-label{font-size:11px;opacity:0.65;text-transform:uppercase;letter-spacing:0.05em;font-weight:700} .fkp-server-info-modal__detail-value{font-size:13px;font-weight:500} .fkp-server-info-modal__detail-value--title{font-weight:600} .fkp-server-info-modal__detail-value--mono{font-family:monospace;font-size:12px;background:rgba(128,128,128,0.12);padding:2px 6px;border-radius:4px;word-break:break-all} .fkp-server-info-modal__badge{display:inline-flex;align-items:center;padding:2px 8px;font-size:11px;font-weight:600;border-radius:4px;border:1px solid var(--border-color,rgba(128,128,128,0.25));background:rgba(128,128,128,0.08);line-height:1.2} .fkp-server-info-modal__link-section{display:flex;flex-direction:column;gap:8px;box-sizing:border-box} .fkp-server-info-modal__link{width:100% !important;max-width:100% !important;font-family:monospace;font-size:11px;padding:8px 10px;resize:none;min-height:54px;box-sizing:border-box;margin:0 !important;line-height:1.4;word-break:break-all;float:none !important;position:static !important;border-radius:0 !important;background-color:transparent !important} .fkp-server-info-modal__copy-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;align-self:flex-start;padding:3px 10px !important;font-size:11px !important;height:auto !important;min-height:24px !important;float:none !important;position:static !important;margin:0 !important} @media (max-width:640px){ .fkp-server-info-modal{width:100%} .fkp-server-info-modal__container{flex-direction:column;align-items:center;gap:16px} .fkp-server-info-modal__qr-col{width:100%;max-width:180px;align-items:center} .fkp-server-info-modal__details-col{width:100%;min-width:0} .fkp-server-info-modal__details-grid{padding:10px} .fkp-server-info-modal__detail-item{flex-wrap:wrap;gap:4px 8px} } `, ), ); } function getFirstListValue(sectionId, option) { return asList(uci.get(UCI_PACKAGE, sectionId, option))[0] || ""; } function getProtocol(sectionId) { return uci.get(UCI_PACKAGE, sectionId, "protocol") || DEFAULT_SERVER_PROTOCOL; } function getProtocolLabel(protocol) { return PROTOCOL_LABELS[protocol] || protocol || ""; } function resetOptionValues(option) { option.keylist = []; option.vallist = []; } function addOptionValue(option, value, label) { if (option.keylist && option.keylist.indexOf(value) >= 0) { return; } option.value(value, label); } function normalizeServerCapabilities(capabilities) { return { singBoxExtended: Boolean(capabilities?.singBoxExtended), singBoxTailscale: capabilities?.singBoxTailscale !== false, }; } function getDefaultProtocolForCapabilities(capabilities) { return normalizeServerCapabilities(capabilities).singBoxTailscale ? DEFAULT_SERVER_PROTOCOL : FALLBACK_SERVER_PROTOCOL; } function populateProtocolValues(option, capabilities) { const normalized = normalizeServerCapabilities(capabilities); resetOptionValues(option); // Tailscale is offered unconditionally: the native tailscaled mode does not // depend on the sing-box build, only on the installable Tailscale component. Object.entries(TAILSCALE_PROTOCOL_LABELS).forEach(([value, label]) => { addOptionValue(option, value, label); }); Object.entries(BASE_PROTOCOL_LABELS).forEach(([value, label]) => { addOptionValue(option, value, label); }); if (normalized.singBoxExtended) { Object.entries(EXTENDED_PROTOCOL_LABELS).forEach(([value, label]) => { addOptionValue(option, value, label); }); } Object.entries(CUSTOM_PROTOCOL_LABELS).forEach(([value, label]) => { addOptionValue(option, value, label); }); } function populateTransportValues(option, singBoxExtended) { resetOptionValues(option); addOptionValue(option, "tcp", "TCP"); addOptionValue(option, "ws", "WebSocket"); addOptionValue(option, "grpc", "gRPC"); addOptionValue(option, "http", "HTTP"); addOptionValue(option, "httpupgrade", "HTTPUpgrade"); if (singBoxExtended) { addOptionValue(option, "xhttp", "XHTTP"); } } function applyServerCapabilities(sectionRef, capabilities) { const options = sectionRef.serverCapabilityOptions; const normalized = normalizeServerCapabilities(capabilities); sectionRef.serverCapabilities = normalized; if (!options) { return; } if (options.protocol) { populateProtocolValues(options.protocol, normalized); options.protocol.default = getDefaultProtocolForCapabilities(normalized); } if (options.transport) { populateTransportValues(options.transport, normalized.singBoxExtended); } } function getServerName(sectionId) { return uci.get(UCI_PACKAGE, sectionId, "label") || sectionId; } function getSocksUsername(sectionId) { return ( uci.get(UCI_PACKAGE, sectionId, "server_username") || getServerName(sectionId) || sectionId ); } function isSocksAuthenticationEnabled(sectionId) { const value = `${ uci.get(UCI_PACKAGE, sectionId, "socks_auth_enabled") ?? "1" }`.toLowerCase(); return !["0", "false", "no", "off"].includes(value); } function getDefaultSecurity(protocol) { if (protocol === "vless") { return "reality"; } if (protocol === "trojan" || protocol === "hysteria2") { return "tls"; } return "none"; } function normalizeVlessFlow(flow) { return flow === "xtls-rprx-vision" ? flow : ""; } function getEffectiveSecurity(sectionId) { const protocol = getProtocol(sectionId); const security = uci.get(UCI_PACKAGE, sectionId, "security"); if ( protocol === "shadowsocks" || protocol === "socks" || protocol === "mtproto" || protocol === "tailscale" || protocol === "json_inbound" ) { return "none"; } if (protocol === "hysteria2") { return "tls"; } if (security === "reality" && protocol !== "vless") { return getDefaultSecurity(protocol); } return security || getDefaultSecurity(protocol); } function normalizeHost(host) { return `${host || ""}`.trim().replace(/^\[|\]$/g, ""); } function getBrowserHost() { return normalizeHost(window.location.hostname || ""); } function isIpv4(value) { return /^(25[0-5]|2[0-4][0-9]|1?[0-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1?[0-9]?[0-9])){3}$/.test( value, ); } function isIpv6(value) { const normalized = normalizeHost(value); if (!normalized.includes(":")) { return false; } try { new URL(`http://[${normalized}]/`); return true; } catch (_e) { return false; } } function formatHostForUri(host) { const normalized = normalizeHost(host); return isIpv6(normalized) ? `[${normalized}]` : normalized; } function isHostname(value) { const hostname = normalizeHost(value); if (hostname.length < 1 || hostname.length > 253 || hostname.endsWith(".")) { return false; } return hostname .split(".") .every((part) => /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/.test(part), ); } function isHost(value) { const host = normalizeHost(value); return isIpv4(host) || isIpv6(host) || isHostname(host); } function isPrivateOrReservedIpv4(value) { if (!isIpv4(value)) { return true; } const octets = value.split(".").map((part) => parseInt(part, 10)); const [a, b] = octets; return ( a === 0 || a === 10 || a === 127 || a >= 224 || (a === 100 && b >= 64 && b <= 127) || (a === 169 && b === 254) || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 192 && b === 0) || (a === 192 && b === 2) || (a === 198 && (b === 18 || b === 19 || b === 51)) || (a === 203 && b === 0) ); } function isPrivateOrReservedIp(value) { const normalized = normalizeHost(value).toLowerCase(); if (isIpv4(normalized)) { return isPrivateOrReservedIpv4(normalized); } if (!isIpv6(normalized)) { return true; } return ( normalized === "::" || normalized === "::1" || normalized.startsWith("fe80") || normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("ff") || normalized.startsWith("2001:db8") ); } function getInterfaceIpAddresses(networkInterface) { const ipv4Values = networkInterface && typeof networkInterface === "object" && Array.isArray(networkInterface["ipv4-address"]) ? networkInterface["ipv4-address"] : []; const ipv6Values = networkInterface && typeof networkInterface === "object" && Array.isArray(networkInterface["ipv6-address"]) ? networkInterface["ipv6-address"] : []; return [...ipv4Values, ...ipv6Values] .map((address) => normalizeHost( address && typeof address === "object" ? address.address : address, ), ) .filter((address) => isIpv4(address) || isIpv6(address)); } function chooseDefaultPublicHost(networkInterfaces) { const interfaces = Array.isArray(networkInterfaces) ? networkInterfaces : []; const upInterfaces = interfaces.filter( (networkInterface) => networkInterface && networkInterface.up !== false, ); const publicIp = upInterfaces .flatMap(getInterfaceIpAddresses) .find((ip) => !isPrivateOrReservedIp(ip)); if (publicIp) { return publicIp; } const lanInterface = upInterfaces.find((networkInterface) => { const name = `${networkInterface.interface || ""}`; const device = `${networkInterface.device || networkInterface.l3_device || ""}`; return name === "lan" || device === "br-lan"; }) || {}; const lanIp = getInterfaceIpAddresses(lanInterface).find( (ip) => ip !== "::1" && !ip.startsWith("127."), ); if (lanIp) { return lanIp; } return ( upInterfaces .flatMap(getInterfaceIpAddresses) .find((ip) => ip !== "::1" && !ip.startsWith("127.")) || getBrowserHost() ); } function loadDefaultPublicHost() { if (defaultPublicHostCache != null) { return Promise.resolve(defaultPublicHostCache); } if (defaultPublicHostPromise) { return defaultPublicHostPromise; } defaultPublicHostPromise = callNetworkInterfaceDump() .then((networkInterfaces) => { defaultPublicHostCache = chooseDefaultPublicHost(networkInterfaces); return defaultPublicHostCache; }) .catch(() => getBrowserHost()) .finally(() => { defaultPublicHostPromise = null; }); return defaultPublicHostPromise; } function preloadServerModalData() { return loadDefaultPublicHost().catch(() => null); } function applyDefaultPublicHost(sectionId, host) { const normalizedHost = normalizeHost(host); const currentHost = normalizeHost( uci.get(UCI_PACKAGE, sectionId, "public_host"), ); const browserHost = getBrowserHost(); if ( normalizedHost && (!currentHost || currentHost === browserHost || currentHost === "127.0.0.1") ) { uci.set(UCI_PACKAGE, sectionId, "public_host", normalizedHost); } } function randomBytes(length) { const bytes = new Uint8Array(length); if (window.crypto && window.crypto.getRandomValues) { window.crypto.getRandomValues(bytes); } else { for (let i = 0; i < length; i += 1) { bytes[i] = Math.floor(Math.random() * 256); } } return bytes; } function randomHex(byteLength) { return Array.from(randomBytes(byteLength)) .map((byte) => byte.toString(16).padStart(2, "0")) .join(""); } function stringToHex(value) { return Array.from(unescape(encodeURIComponent(`${value || ""}`))) .map((char) => char.charCodeAt(0).toString(16).padStart(2, "0")) .join(""); } function hexToString(value) { const hex = `${value || ""}`; if (!/^(?:[0-9a-fA-F]{2})+$/.test(hex)) { return ""; } const bytes = hex.match(/../g).map((byte) => parseInt(byte, 16)); try { return decodeURIComponent( escape(bytes.map((byte) => String.fromCharCode(byte)).join("")), ); } catch (_error) { return ""; } } function generateUuid() { if (window.crypto && window.crypto.randomUUID) { return window.crypto.randomUUID(); } const bytes = randomBytes(16); bytes[6] = (bytes[6] & 0x0f) | 0x40; bytes[8] = (bytes[8] & 0x3f) | 0x80; const hex = Array.from(bytes) .map((byte) => byte.toString(16).padStart(2, "0")) .join(""); return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; } function base64Encode(value) { return btoa(unescape(encodeURIComponent(value))); } function base64UrlEncode(value) { return base64Encode(value) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/g, ""); } function generatePassword() { return base64UrlEncode( Array.from(randomBytes(18)) .map((byte) => String.fromCharCode(byte)) .join(""), ); } function generateMtprotoSecret() { const secret = randomHex(16); return secret === "00000000000000000000000000000000" ? "11111111111111111111111111111111" : secret; } function randomPort() { const bytes = randomBytes(2); const value = (bytes[0] << 8) | bytes[1]; return `${20000 + (value % 30000)}`; } function serverSafeName(sectionId) { return `${sectionId || "server"}`.replace(/[^A-Za-z0-9_.-]/g, "_"); } function defaultTlsCertificatePath(sectionId) { return `/etc/tachyon/server-certs/${serverSafeName(sectionId)}.crt`; } function defaultTlsKeyPath(sectionId) { return `/etc/tachyon/server-certs/${serverSafeName(sectionId)}.key`; } function defaultTailscaleHostname(sectionId) { return `tachyon-${serverSafeName(sectionId)}`; } function setDefault(sectionId, key, value) { const current = uci.get(UCI_PACKAGE, sectionId, key); if (current == null || current === "") { uci.set(UCI_PACKAGE, sectionId, key, value); } } function setValue(sectionId, key, value) { uci.set(UCI_PACKAGE, sectionId, key, value); } function cloneUciState(value) { return value == null ? null : JSON.parse(JSON.stringify(value)); } function isEmptyObject(value) { return value && Object.keys(value).length === 0; } function setUciSectionState(bucketName, sectionId, value) { const state = uci.state; if (!state) { return; } state[bucketName] ??= {}; const bucket = state[bucketName]; if (value == null) { delete bucket[UCI_PACKAGE]?.[sectionId]; if (isEmptyObject(bucket[UCI_PACKAGE])) { delete bucket[UCI_PACKAGE]; } return; } bucket[UCI_PACKAGE] ??= {}; bucket[UCI_PACKAGE][sectionId] = cloneUciState(value); } function captureServerEditState(sectionId) { const state = uci.state; if (!state || state.creates?.[UCI_PACKAGE]?.[sectionId]) { return null; } return { sectionId, changes: cloneUciState(state.changes?.[UCI_PACKAGE]?.[sectionId]), deletes: cloneUciState(state.deletes?.[UCI_PACKAGE]?.[sectionId]), }; } function restoreServerEditState(snapshot) { if (!snapshot) { return; } setUciSectionState("changes", snapshot.sectionId, snapshot.changes); setUciSectionState("deletes", snapshot.sectionId, snapshot.deletes); } function ensureProtocolDefaults(sectionId, protocol, forceProtocolDefaults) { setValue(sectionId, "protocol", protocol); setDefault(sectionId, "enabled", "1"); setDefault(sectionId, "label", sectionId); setDefault(sectionId, "routing_mode", "rules"); if (protocol === "json_inbound") { setValue(sectionId, "security", getDefaultSecurity(protocol)); return; } setDefault(sectionId, "listen", "0.0.0.0"); setDefault(sectionId, "listen_port", randomPort()); setDefault(sectionId, "public_host", getBrowserHost()); setDefault( sectionId, "tls_certificate_path", defaultTlsCertificatePath(sectionId), ); setDefault(sectionId, "tls_key_path", defaultTlsKeyPath(sectionId)); if (forceProtocolDefaults) { setValue(sectionId, "security", getDefaultSecurity(protocol)); setValue(sectionId, "transport", "tcp"); } else { setDefault(sectionId, "security", getDefaultSecurity(protocol)); setDefault(sectionId, "transport", "tcp"); } if (protocol === "vless" || protocol === "vmess") { setDefault(sectionId, "server_uuid", generateUuid()); } if (PASSWORD_PROTOCOLS.includes(protocol)) { setDefault(sectionId, "server_password", generatePassword()); } if (protocol === "socks") { setDefault(sectionId, "socks_auth_enabled", "1"); setDefault(sectionId, "server_username", getServerName(sectionId)); } if (protocol === "vless") { setDefault(sectionId, "vless_flow", "none"); setDefault(sectionId, "tls_server_name", "www.microsoft.com"); setDefault(sectionId, "client_fingerprint", "chrome"); setDefault(sectionId, "reality_handshake_server", "www.microsoft.com"); setDefault(sectionId, "reality_handshake_server_port", "443"); setDefault(sectionId, "reality_short_id", randomHex(4)); setDefault(sectionId, "reality_max_time_difference", "1m"); } if (protocol === "vmess") { setDefault(sectionId, "vmess_alter_id", "0"); } if (protocol === "shadowsocks") { setDefault(sectionId, "shadowsocks_method", "aes-128-gcm"); } if (protocol === "tailscale") { setDefault( sectionId, "tailscale_control_url", "https://controlplane.tailscale.com", ); setDefault( sectionId, "tailscale_hostname", defaultTailscaleHostname(sectionId), ); setDefault(sectionId, "tailscale_advertise_exit_node", "1"); } if (protocol === "hysteria2") { setDefault(sectionId, "hysteria2_obfs_password", generatePassword()); } if (protocol === "mtproto") { setDefault(sectionId, "mtproto_secret", generateMtprotoSecret()); setDefault(sectionId, "mtproto_faketls", "google.com"); setDefault(sectionId, "mtproto_padding", "1"); setDefault(sectionId, "mtproto_domain_fronting_port", "443"); setDefault(sectionId, "mtproto_prefer_ip", "prefer-ipv4"); setDefault(sectionId, "mtproto_tolerate_time_skewness", "3s"); setDefault(sectionId, "mtproto_idle_timeout", "5m"); setDefault(sectionId, "mtproto_handshake_timeout", "10s"); } } function parseLegacyServerUser(sectionId, protocol) { const entry = asList(uci.get(UCI_PACKAGE, sectionId, "server_users"))[0]; if (!entry) { return null; } const parts = `${entry}`.split("|"); return { name: parts.length > 1 ? parts[0].trim() : "client", credential: (parts.length > 1 ? parts[1] : parts[0] || "").trim(), extra: (parts[2] || "").trim(), protocol, }; } function normalizeMtprotoBaseSecret(value) { const secret = `${value || ""}`.trim().toLowerCase(); if (/^[0-9a-f]{32}$/.test(secret)) { return secret; } if (/^ee[0-9a-f]{32}(?:[0-9a-f]{2})+$/.test(secret)) { return secret.slice(2, 34); } return ""; } function getMtprotoFaketlsFromSecret(value) { const secret = `${value || ""}`.trim().toLowerCase(); if (!/^ee[0-9a-f]{32}(?:[0-9a-f]{2})+$/.test(secret)) { return ""; } return hexToString(secret.slice(34)); } function getMtprotoFaketls(sectionId) { return ( uci.get(UCI_PACKAGE, sectionId, "mtproto_faketls") || getMtprotoFaketlsFromSecret( uci.get(UCI_PACKAGE, sectionId, "mtproto_secret"), ) || "google.com" ); } function isMtprotoPaddingEnabled(sectionId) { return uci.get(UCI_PACKAGE, sectionId, "mtproto_padding") !== "0"; } function buildMtprotoFullSecret(baseSecret, faketls, padding) { if (!padding) { return baseSecret; } return `ee${baseSecret}${stringToHex(faketls || "google.com")}`; } function getServerIdentity(sectionId) { const protocol = getProtocol(sectionId); const legacy = parseLegacyServerUser(sectionId, protocol); const name = getServerName(sectionId); const uuid = uci.get(UCI_PACKAGE, sectionId, "server_uuid") || legacy?.credential || ""; const password = uci.get(UCI_PACKAGE, sectionId, "server_password") || (PASSWORD_PROTOCOLS.includes(protocol) ? legacy?.credential : "") || ""; const mtprotoSecret = normalizeMtprotoBaseSecret( uci.get(UCI_PACKAGE, sectionId, "mtproto_secret"), ) || normalizeMtprotoBaseSecret( protocol === "mtproto" ? legacy?.credential : "", ) || ""; return { name, username: getSocksUsername(sectionId), uuid, password, mtprotoSecret, mtprotoFaketls: getMtprotoFaketls(sectionId), mtprotoPadding: isMtprotoPaddingEnabled(sectionId), flow: normalizeVlessFlow( uci.get(UCI_PACKAGE, sectionId, "vless_flow") || (protocol === "vless" ? legacy?.extra : "") || "", ), }; } function encodeQuery(params) { return Object.entries(params) .filter(([, value]) => value != null && value !== "") .map( ([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(`${value}`)}`, ) .join("&"); } function getPublicHost(sectionId) { return ( uci.get(UCI_PACKAGE, sectionId, "public_host") || window.location.hostname || "" ); } function getTransportParams(sectionId, params) { const transport = uci.get(UCI_PACKAGE, sectionId, "transport") || "tcp"; params.type = transport === "raw" ? "tcp" : transport; if (transport === "ws" || transport === "httpupgrade") { params.path = uci.get(UCI_PACKAGE, sectionId, "transport_path") || ""; params.host = uci.get(UCI_PACKAGE, sectionId, "transport_host") || ""; } else if (transport === "xhttp") { params.path = uci.get(UCI_PACKAGE, sectionId, "transport_path") || "/"; params.host = uci.get(UCI_PACKAGE, sectionId, "transport_host") || ""; params.mode = uci.get(UCI_PACKAGE, sectionId, "transport_xhttp_mode") || "auto"; } else if (transport === "grpc") { params.serviceName = uci.get(UCI_PACKAGE, sectionId, "transport_service_name") || ""; } else if (transport === "http") { params.path = uci.get(UCI_PACKAGE, sectionId, "transport_path") || ""; params.host = asList( uci.get(UCI_PACKAGE, sectionId, "transport_hosts"), ).join(","); } return transport; } function buildVlessTrojanLink(sectionId, identity) { const protocol = getProtocol(sectionId); const host = getPublicHost(sectionId); const uriHost = formatHostForUri(host); const port = uci.get(UCI_PACKAGE, sectionId, "listen_port") || ""; const security = getEffectiveSecurity(sectionId); const tlsServerName = uci.get(UCI_PACKAGE, sectionId, "tls_server_name") || uci.get(UCI_PACKAGE, sectionId, "reality_handshake_server") || ""; const params = { security }; const credential = protocol === "vless" ? identity.uuid : identity.password; getTransportParams(sectionId, params); if (protocol === "vless") { params.encryption = "none"; } if (security === "tls" || security === "reality") { params.sni = tlsServerName; params.alpn = asList(uci.get(UCI_PACKAGE, sectionId, "tls_alpn")).join(","); } if (security === "reality") { params.pbk = uci.get(UCI_PACKAGE, sectionId, "reality_public_key") || ""; params.sid = getFirstListValue(sectionId, "reality_short_id"); params.fp = uci.get(UCI_PACKAGE, sectionId, "client_fingerprint") || "chrome"; } if (protocol === "vless" && identity.flow) { params.flow = identity.flow; } const query = encodeQuery(params); return `${protocol}://${encodeURIComponent(credential)}@${uriHost}:${port}${query ? `?${query}` : ""}#${encodeURIComponent(identity.name || sectionId)}`; } function buildVmessLink(sectionId, identity) { const host = getPublicHost(sectionId); const port = uci.get(UCI_PACKAGE, sectionId, "listen_port") || ""; const security = getEffectiveSecurity(sectionId); const transportParams = {}; const transport = getTransportParams(sectionId, transportParams); if ( security === "reality" && (!uci.get(UCI_PACKAGE, sectionId, "reality_public_key") || !getFirstListValue(sectionId, "reality_short_id")) ) { return ""; } const payload = { v: "2", ps: identity.name || sectionId, add: host, port, id: identity.uuid, aid: uci.get(UCI_PACKAGE, sectionId, "vmess_alter_id") || "0", scy: "auto", net: transport === "tcp" ? "tcp" : transport, type: "none", host: transportParams.host || "", path: transportParams.path || transportParams.serviceName || "", tls: security === "tls" ? "tls" : "", sni: uci.get(UCI_PACKAGE, sectionId, "tls_server_name") || "", alpn: asList(uci.get(UCI_PACKAGE, sectionId, "tls_alpn")).join(","), }; return `vmess://${base64Encode(JSON.stringify(payload))}`; } function buildShadowsocksLink(sectionId, identity) { const method = uci.get(UCI_PACKAGE, sectionId, "shadowsocks_method") || "aes-128-gcm"; const host = getPublicHost(sectionId); const uriHost = formatHostForUri(host); const port = uci.get(UCI_PACKAGE, sectionId, "listen_port") || ""; const userInfo = base64UrlEncode(`${method}:${identity.password}`); return `ss://${userInfo}@${uriHost}:${port}#${encodeURIComponent(identity.name || sectionId)}`; } function buildSocksLink(sectionId, identity) { const host = getPublicHost(sectionId); const uriHost = formatHostForUri(host); const port = uci.get(UCI_PACKAGE, sectionId, "listen_port") || ""; const username = identity.username || identity.name || sectionId; const userInfo = isSocksAuthenticationEnabled(sectionId) ? `${encodeURIComponent(username)}:${encodeURIComponent(identity.password)}@` : ""; return `socks5://${userInfo}${uriHost}:${port}#${encodeURIComponent(identity.name || sectionId)}`; } function normalizeSha256(value) { const normalized = `${value || ""}`.trim().replace(/:/g, "").toLowerCase(); return /^[0-9a-f]{64}$/.test(normalized) ? normalized : ""; } function shouldUseGeneratedTlsCertificatePin(sectionId) { const certificatePath = uci.get(UCI_PACKAGE, sectionId, "tls_certificate_path") || defaultTlsCertificatePath(sectionId); return `${certificatePath}` === defaultTlsCertificatePath(sectionId); } function loadGeneratedTlsCertificatePin(sectionId) { if (!shouldUseGeneratedTlsCertificatePin(sectionId)) { return Promise.resolve(""); } return fs .exec("/usr/bin/tachyon", ["get_tls_certificate_sha256", sectionId]) .then((response) => { if ((response.code ?? 0) !== 0 || !response.stdout) { throw new Error(response.stderr || response.stdout || ""); } const data = JSON.parse(response.stdout); if (!data.success) { throw new Error(data.message || ""); } return normalizeSha256(data.sha256); }) .catch((error) => { console.warn( error?.message || "Failed to read generated TLS certificate pin", error, ); return ""; }); } function buildHysteria2Link(sectionId, identity, options = {}) { const host = getPublicHost(sectionId); const uriHost = formatHostForUri(host); const port = uci.get(UCI_PACKAGE, sectionId, "listen_port") || ""; const certificatePin = normalizeSha256(options.tlsCertificateSha256); const obfsType = uci.get(UCI_PACKAGE, sectionId, "hysteria2_obfs_type") || ""; const params = { sni: uci.get(UCI_PACKAGE, sectionId, "tls_server_name") || "", pcs: certificatePin, }; if (obfsType === "salamander") { params.obfs = obfsType; params["obfs-password"] = uci.get(UCI_PACKAGE, sectionId, "hysteria2_obfs_password") || ""; } const query = encodeQuery(params); return `hysteria2://${encodeURIComponent(identity.password)}@${uriHost}:${port}${query ? `?${query}` : ""}#${encodeURIComponent(identity.name || sectionId)}`; } function buildMtprotoLink(sectionId, identity) { const host = getPublicHost(sectionId); const port = uci.get(UCI_PACKAGE, sectionId, "listen_port") || ""; const secret = buildMtprotoFullSecret( identity.mtprotoSecret, identity.mtprotoFaketls, identity.mtprotoPadding, ); const query = encodeQuery({ server: host, port, secret, }); return `https://t.me/proxy?${query}`; } function buildAwgClientConfig(sectionId) { const host = getPublicHost(sectionId); const port = uci.get(UCI_PACKAGE, sectionId, "listen_port") || "51820"; const clientPrivateKey = uci.get(UCI_PACKAGE, sectionId, "awg_client_private_key") || ""; const serverPublicKey = uci.get(UCI_PACKAGE, sectionId, "awg_server_public_key") || uci.get(UCI_PACKAGE, sectionId, "awg_peer_public_key") || ""; const presharedKey = uci.get(UCI_PACKAGE, sectionId, "awg_preshared_key") || ""; const clientAddress = uci.get(UCI_PACKAGE, sectionId, "awg_client_address") || "10.0.0.2/32"; const mtu = uci.get(UCI_PACKAGE, sectionId, "awg_mtu") || "1280"; const keepalive = uci.get(UCI_PACKAGE, sectionId, "awg_keepalive") || "25"; const dns = uci.get(UCI_PACKAGE, sectionId, "awg_dns") || "10.0.0.1"; const jc = uci.get(UCI_PACKAGE, sectionId, "awg_jc") || "4"; const jmin = uci.get(UCI_PACKAGE, sectionId, "awg_jmin") || "40"; const jmax = uci.get(UCI_PACKAGE, sectionId, "awg_jmax") || "70"; const s1 = uci.get(UCI_PACKAGE, sectionId, "awg_s1") || "0"; const s2 = uci.get(UCI_PACKAGE, sectionId, "awg_s2") || "0"; const s3 = uci.get(UCI_PACKAGE, sectionId, "awg_s3") || "0"; const s4 = uci.get(UCI_PACKAGE, sectionId, "awg_s4") || "0"; const h1 = uci.get(UCI_PACKAGE, sectionId, "awg_h1") || "1"; const h2 = uci.get(UCI_PACKAGE, sectionId, "awg_h2") || "2"; const h3 = uci.get(UCI_PACKAGE, sectionId, "awg_h3") || "3"; const h4 = uci.get(UCI_PACKAGE, sectionId, "awg_h4") || "4"; const i1 = uci.get(UCI_PACKAGE, sectionId, "awg_i1") || ""; const i2 = uci.get(UCI_PACKAGE, sectionId, "awg_i2") || ""; const i3 = uci.get(UCI_PACKAGE, sectionId, "awg_i3") || ""; const i4 = uci.get(UCI_PACKAGE, sectionId, "awg_i4") || ""; const i5 = uci.get(UCI_PACKAGE, sectionId, "awg_i5") || ""; const j1 = uci.get(UCI_PACKAGE, sectionId, "awg_j1") || ""; const j2 = uci.get(UCI_PACKAGE, sectionId, "awg_j2") || ""; const j3 = uci.get(UCI_PACKAGE, sectionId, "awg_j3") || ""; const itime = uci.get(UCI_PACKAGE, sectionId, "awg_itime") || ""; let conf = `[Interface]\nAddress = ${clientAddress}\nPrivateKey = ${clientPrivateKey}\nDNS = ${dns}\nMTU = ${mtu}\n`; conf += `Jc = ${jc}\nJmin = ${jmin}\nJmax = ${jmax}\nS1 = ${s1}\nS2 = ${s2}\nS3 = ${s3}\nS4 = ${s4}\nH1 = ${h1}\nH2 = ${h2}\nH3 = ${h3}\nH4 = ${h4}\n`; if (i1) conf += `I1 = ${i1}\n`; if (i2) conf += `I2 = ${i2}\n`; if (i3) conf += `I3 = ${i3}\n`; if (i4) conf += `I4 = ${i4}\n`; if (i5) conf += `I5 = ${i5}\n`; if (j1) conf += `J1 = ${j1}\n`; if (j2) conf += `J2 = ${j2}\n`; if (j3) conf += `J3 = ${j3}\n`; if (itime) conf += `Itime = ${itime}\n`; const awgVer = uci.get(UCI_PACKAGE, sectionId, "awg_version") || "2.0"; if (awgVer === "3.0" || awgVer === "3.1") { const hpk = uci.get(UCI_PACKAGE, sectionId, "awg_header_protection_key") || ""; const cpa = uci.get(UCI_PACKAGE, sectionId, "awg_content_padding_addition") || ""; if (hpk) conf += `HeaderProtectionKey = ${hpk}\n`; if (cpa) conf += `ContentPaddingAddition = ${cpa}\n`; } if (awgVer === "3.1") { const rka = uci.get(UCI_PACKAGE, sectionId, "awg_rekey_after_time") || ""; const rkt = uci.get(UCI_PACKAGE, sectionId, "awg_rekey_timeout") || ""; const rja = uci.get(UCI_PACKAGE, sectionId, "awg_reject_after_time") || ""; const kpt = uci.get(UCI_PACKAGE, sectionId, "awg_keepalive_timeout") || ""; const mha = uci.get(UCI_PACKAGE, sectionId, "awg_max_handshake_attempts") || ""; if (rka) conf += `RekeyAfterTime = ${rka}\n`; if (rkt) conf += `RekeyTimeout = ${rkt}\n`; if (rja) conf += `RejectAfterTime = ${rja}\n`; if (kpt) conf += `KeepaliveTimeout = ${kpt}\n`; if (mha) conf += `MaxHandshakeAttempts = ${mha}\n`; } conf += `\n[Peer]\nPublicKey = ${serverPublicKey}\nEndpoint = ${host}:${port}\nAllowedIPs = 0.0.0.0/0, ::/0\nPersistentKeepalive = ${keepalive}\n`; if (presharedKey) conf += `PresharedKey = ${presharedKey}\n`; return conf; } function buildClientLink(sectionId, options = {}) { const protocol = getProtocol(sectionId); const identity = getServerIdentity(sectionId); if (protocol === "tailscale" || protocol === "json_inbound") { return ""; } if (protocol === "awg") { return buildAwgClientConfig(sectionId); } if ( ((protocol === "vless" || protocol === "vmess") && !identity.uuid) || (PASSWORD_PROTOCOLS.includes(protocol) && !identity.password) || (protocol === "mtproto" && !identity.mtprotoSecret) ) { return ""; } if ( protocol === "vless" && getEffectiveSecurity(sectionId) === "reality" && (!uci.get(UCI_PACKAGE, sectionId, "reality_public_key") || !getFirstListValue(sectionId, "reality_short_id")) ) { return ""; } switch (protocol) { case "shadowsocks": return buildShadowsocksLink(sectionId, identity); case "socks": return buildSocksLink(sectionId, identity); case "vmess": return buildVmessLink(sectionId, identity); case "trojan": return buildVlessTrojanLink(sectionId, identity); case "hysteria2": return buildHysteria2Link(sectionId, identity, options); case "mtproto": return buildMtprotoLink(sectionId, identity); case "awg": return buildAwgClientConfig(sectionId); case "vless": default: return buildVlessTrojanLink(sectionId, identity); } } function buildClientLinkContext(sectionId) { const certificatePinExpected = getProtocol(sectionId) === "hysteria2" && shouldUseGeneratedTlsCertificatePin(sectionId); if (!certificatePinExpected) { return Promise.resolve({ link: buildClientLink(sectionId), certificatePinExpected, tlsCertificateSha256: "", }); } return loadGeneratedTlsCertificatePin(sectionId).then( (tlsCertificateSha256) => ({ link: buildClientLink(sectionId, { tlsCertificateSha256 }), certificatePinExpected, tlsCertificateSha256, }), ); } function svgEl(tag, attrs = {}, children = []) { const element = document.createElementNS("http://www.w3.org/2000/svg", tag); Object.entries(attrs).forEach(([key, value]) => { if (value != null) { element.setAttribute(key, `${value}`); } }); (Array.isArray(children) ? children : [children]) .filter(Boolean) .forEach((child) => element.appendChild(child)); return element; } function iconSvg(name) { const attrs = { viewBox: "0 0 24 24", width: "16", height: "16", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", focusable: "false", }; if (name === "copy") { return svgEl("svg", attrs, [ svgEl("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2", }), svgEl("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1", }), ]); } if (name === "check") { return svgEl("svg", attrs, [ svgEl("polyline", { points: "20 6 9 17 4 12" }), ]); } if (name === "info") { return svgEl("svg", attrs, [ svgEl("circle", { cx: "12", cy: "12", r: "10" }), svgEl("path", { d: "M12 16v-4" }), svgEl("path", { d: "M12 8h.01" }), ]); } return svgEl("svg", attrs, [ svgEl("rect", { x: "3", y: "3", width: "5", height: "5" }), svgEl("rect", { x: "16", y: "3", width: "5", height: "5" }), svgEl("rect", { x: "3", y: "16", width: "5", height: "5" }), svgEl("path", { d: "M16 16h1v1h-1zM20 16h1v1h-1zM16 20h1v1h-1zM20 20h1v1h-1zM18 18h1v1h-1z", }), ]); } function copyText(text) { const textarea = document.createElement("textarea"); textarea.value = text; textarea.style.position = "fixed"; textarea.style.top = "0"; textarea.style.left = "0"; textarea.style.opacity = "0"; document.body.appendChild(textarea); textarea.select(); try { document.execCommand("copy"); main.showToast(_("Copied"), "success"); } catch (error) { main.showToast(_("Failed to copy!"), "error"); console.warn("Failed to copy server client link", error); } document.body.removeChild(textarea); } function renderInfoClientLink(sectionId, linkContext = {}) { const link = linkContext.link || buildClientLink(sectionId); if (!link) { return E("em", {}, _("Client link is not available yet")); } const qr = qrSvgDataUri(link); const protocol = getProtocol(sectionId); const security = getEffectiveSecurity(sectionId); const host = getPublicHost(sectionId); const port = uci.get(UCI_PACKAGE, sectionId, "listen_port") || ""; const name = getServerName(sectionId); const securitySuffix = security && security !== "none" ? ` + ${getSecurityLabel(security)}` : ""; const protocolText = `${getProtocolLabel(protocol)}${securitySuffix}`; const certificatePinWarning = protocol === "hysteria2" && linkContext.certificatePinExpected && !linkContext.tlsCertificateSha256 ? E( "div", { class: "alert-message warning" }, _( "Certificate pin is unavailable. Apply or restart the server to generate the default certificate, then open this dialog again.", ), ) : ""; return E("div", { class: "fkp-server-info-modal__container" }, [ // Left column: QR code E("div", { class: "fkp-server-info-modal__qr-col" }, [ qr ? E("img", { class: "fkp-server-info-modal__qr-image", src: qr, alt: _("Client link QR code"), }) : E("em", {}, _("QR code is too large")), ]), // Right column: Details and Copy Box E("div", { class: "fkp-server-info-modal__details-col" }, [ E("div", { class: "fkp-server-info-modal__details-grid" }, [ E("div", { class: "fkp-server-info-modal__detail-item" }, [ E( "span", { class: "fkp-server-info-modal__detail-label" }, _("Server name"), ), E( "span", { class: "fkp-server-info-modal__detail-value fkp-server-info-modal__detail-value--title", }, name, ), ]), E("div", { class: "fkp-server-info-modal__detail-item" }, [ E( "span", { class: "fkp-server-info-modal__detail-label" }, _("Type"), ), E( "span", { class: "fkp-server-info-modal__detail-value fkp-server-info-modal__badge", }, protocolText, ), ]), E("div", { class: "fkp-server-info-modal__detail-item" }, [ E( "span", { class: "fkp-server-info-modal__detail-label" }, _("Address"), ), E( "span", { class: "fkp-server-info-modal__detail-value fkp-server-info-modal__detail-value--mono", }, `${host}:${port}`, ), ]), ]), E("div", { class: "fkp-server-info-modal__link-section" }, [ E( "span", { class: "fkp-server-info-modal__detail-label" }, _("Client link"), ), certificatePinWarning, E( "textarea", { class: "cbi-input-textarea fkp-server-info-modal__link", readonly: "readonly", rows: 3, click: (ev) => ev.currentTarget.select(), }, link, ), E("div", { style: "display:flex;gap:6px;" }, [ E( "button", { class: "btn cbi-button cbi-button-neutral fkp-server-info-modal__copy-btn", type: "button", click: (ev) => { ev.preventDefault(); copyText(link); }, }, [iconSvg("copy"), " ", E("span", {}, _("Copy"))], ), protocol === "awg" ? E( "button", { class: "btn cbi-button cbi-button-action fkp-server-info-modal__copy-btn", type: "button", click: (ev) => { ev.preventDefault(); const blob = new Blob([link], { type: "text/plain;charset=utf-8", }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${name || "amneziawg"}.conf`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }, }, [E("span", {}, _("Download .conf"))], ) : "", ]), ]), ]), ]); } function showServerInfoModal(sectionId) { const modalContent = E("div", { class: "fkp-server-info-modal" }, [ E("em", {}, _("Loading client link...")), ]); ui.showModal( _("Server information"), [ modalContent, E("div", { class: "button-row" }, [ E( "button", { class: "btn cbi-button cbi-button-neutral", type: "button", click: () => ui.hideModal(), }, _("Close"), ), ]), ], "cbi-modal", ); buildClientLinkContext(sectionId) .then((linkContext) => { modalContent.innerHTML = ""; modalContent.appendChild(renderInfoClientLink(sectionId, linkContext)); }) .catch((error) => { console.warn(error?.message || "Failed to build client link", error); modalContent.innerHTML = ""; modalContent.appendChild(renderInfoClientLink(sectionId)); }); } function getServerEditButtonText() { return _("Edit"); } function renderServerRowActions(sectionRef, sectionId) { const editLabel = getServerEditButtonText(); const deleteLabel = _("Delete"); const infoLabel = _("Information"); const tdEl = E( "td", { class: "td cbi-section-table-cell nowrap cbi-section-actions" }, E("div"), ); const actionsEl = tdEl.lastElementChild; const actionButtons = []; if (!["tailscale", "json_inbound"].includes(getProtocol(sectionId))) { actionButtons.push( E( "button", { title: infoLabel, "aria-label": infoLabel, class: "cbi-button center fkp-server-icon-button", style: "display:inline-block;", click: (ev) => { ev.preventDefault(); ev.stopPropagation(); showServerInfoModal(sectionId); }, }, E("span", { class: "fkp-server-icon-button__glyph" }, "\u24D8"), ), ); } dom.append(actionsEl, [ ...actionButtons, E( "button", { title: editLabel, class: "btn cbi-button cbi-button-edit", click: ui.createHandlerFn( sectionRef, "renderMoreOptionsModal", sectionId, ), }, editLabel, ), ]); if (sectionRef.addremove) { dom.append( actionsEl, E( "button", { title: deleteLabel, class: "btn cbi-button cbi-button-remove", click: ui.createHandlerFn(sectionRef, "handleRemove", sectionId), disabled: sectionRef.map.readonly || null, }, deleteLabel, ), ); } return tdEl; } function validatePort(_sectionId, value) { const port = parseInt(value, 10); if (!/^[0-9]+$/.test(value) || port < 1 || port > 65535) { return _("Port must be between 1 and 65535"); } return true; } function validatePositiveInteger(_sectionId, value) { if (isEmptyValue(value)) { return _("This field is required"); } const n = parseInt(value, 10); if (!/^[0-9]+$/.test(`${value}`) || isNaN(n) || n <= 0) { return _("Value must be a positive integer"); } return true; } function isEmptyValue(value) { return ( value == null || value === "" || (Array.isArray(value) && value.filter((item) => item != null && item !== "").length === 0) ); } function validateRequired(_sectionId, value) { return isEmptyValue(value) ? _("This field is required") : true; } function validateRequiredText(_sectionId, value) { if (isEmptyValue(value)) { return _("This field is required"); } return /[\u0000-\u001F\u007F]/.test(`${value}`) ? _("Value must not contain control characters") : true; } function isRoutingSectionAction(action) { return ROUTING_SECTION_ACTIONS.indexOf(`${action || ""}`) !== -1; } function loadRoutingSectionChoices(option) { const sections = option.map?.data?.state?.values?.[UCI_PACKAGE] ?? {}; option.keylist = []; option.vallist = []; for (const sectionName in sections) { const section = sections[sectionName]; if ( section[".type"] === "section" && section.enabled !== "0" && isRoutingSectionAction(section.action) ) { option.keylist.push(sectionName); option.vallist.push(section.label || sectionName); } } return Promise.resolve(); } function loadServerTableOptions(sectionRef) { const sectionIds = sectionRef.cfgsections(); const tasks = []; for (let i = 0; i < sectionIds.length; i += 1) { const sectionId = sectionIds[i]; for (let j = 0; j < sectionRef.children.length; j += 1) { const option = sectionRef.children[j]; if (option.disable || option.modalonly) { continue; } tasks.push( Promise.resolve(option.load.call(option, sectionId)).then((value) => { option.cfgvalue(sectionId, value); }), ); } } return Promise.all(tasks); } function validateHost(_sectionId, value) { return isHost(value) ? true : _("Use a domain name or IP address"); } function validateOptionalHost(_sectionId, value) { return isEmptyValue(value) || isHost(value) ? true : _("Use a domain name or IP address"); } function validateListenAddress(_sectionId, value) { const address = normalizeHost(value); return address === "0.0.0.0" || address === "::" || isIpv4(address) || isIpv6(address) ? true : _("Use an IP listen address"); } function validateOptionalIp(_sectionId, value) { const address = normalizeHost(value); return isEmptyValue(value) || isIpv4(address) || isIpv6(address) ? true : _("Use an IP address"); } function validateFilePath(_sectionId, value) { const path = `${value || ""}`; if (isEmptyValue(path)) { return _("This field is required"); } const validation = main.validatePath(path); return validation.valid && path !== "/" && !path.endsWith("/") ? true : _("Specify a file path"); } function validateTlsCertificatePath(sectionId, value) { const result = validateFilePath(sectionId, value); if (result !== true) { return result; } return `${value}` === `${uci.get(UCI_PACKAGE, sectionId, "tls_key_path") || ""}` ? _("Specify different files") : true; } function validateTlsKeyPath(sectionId, value) { const result = validateFilePath(sectionId, value); if (result !== true) { return result; } return `${value}` === `${uci.get(UCI_PACKAGE, sectionId, "tls_certificate_path") || ""}` ? _("Specify different files") : true; } function validateHttpUrl(_sectionId, value) { try { const url = new URL(`${value || ""}`); return (url.protocol === "http:" || url.protocol === "https:") && isHost(url.hostname) ? true : _("Use an HTTP or HTTPS URL"); } catch (_err) { return _("Use an HTTP or HTTPS URL"); } } function validateInboundJson(_sectionId, value) { if (isEmptyValue(value)) { return _("Inbound JSON cannot be empty"); } try { const parsed = JSON.parse(`${value}`); return parsed && typeof parsed === "object" && !Array.isArray(parsed) && typeof parsed.type === "string" && parsed.type ? true : _("Inbound JSON must be an object with a type field"); } catch (_err) { return _("Invalid JSON format"); } } function validateDurationValue(value) { return /^([0-9]+(ns|us|ms|s|m|h))+$/.test(`${value || ""}`); } function validateRequiredDuration(_sectionId, value) { return validateDurationValue(value) ? true : _("Use a duration like 30s, 5m, or 1h30m"); } function validateOptionalNonNegativeInteger(_sectionId, value) { return isEmptyValue(value) || /^[0-9]+$/.test(`${value}`) ? true : _("Use a non-negative integer"); } function validateTransportPath(_sectionId, value) { return isEmptyValue(value) || `${value}`.startsWith("/") ? true : _("Transport path must start with /"); } function validateOptionalHostList(_sectionId, value) { const values = Array.isArray(value) ? value : [value]; const invalid = values .filter((item) => item != null && item !== "") .some((item) => !isHost(item)); return invalid ? _("Use a domain name or IP address") : true; } function validateGrpcServiceName(_sectionId, value) { return isEmptyValue(value) || /^[A-Za-z0-9_.-]+$/.test(`${value}`) ? true : _("Use letters, digits, dots, underscores, or hyphens"); } function isIpv6Literal(value) { return isIpv6(value); } function normalizeCidrOrIp(value) { const normalized = `${value || ""}`.trim(); if (!normalized.includes("/") && isIpv4(normalized)) { return `${normalized}/32`; } if (!normalized.includes("/") && isIpv6Literal(normalized)) { return `${normalized}/128`; } return normalized; } function validateOptionalCidr(value) { if (isEmptyValue(value)) { return true; } const values = Array.isArray(value) ? value : [value]; const invalid = values .filter((item) => item != null && item !== "") .some((item) => { const [address, prefix, extra] = `${item}`.split("/"); if (extra != null || prefix == null || !/^[0-9]+$/.test(prefix)) { return true; } const prefixNumber = parseInt(prefix, 10); if (isIpv4(address)) { return prefixNumber < 0 || prefixNumber > 32; } if (isIpv6Literal(address)) { return prefixNumber < 0 || prefixNumber > 128; } return true; }); return invalid ? _("Use CIDR prefixes like 192.168.1.0/24 or 2001:db8::/32") : true; } function validateOptionalCidrOrIp(value) { if (isEmptyValue(value)) { return true; } const values = Array.isArray(value) ? value : [value]; const invalid = values .filter((item) => item != null && item !== "") .some((item) => validateOptionalCidr(normalizeCidrOrIp(item)) !== true); return invalid ? _( "Use CIDR prefixes or IP addresses like 192.168.1.0/24, 192.168.1.10, or 2001:db8::1", ) : true; } function validateShortId(_sectionId, value) { if (isEmptyValue(value)) { return _("This field is required"); } const values = Array.isArray(value) ? value : [value]; const invalid = values .filter((item) => item != null && item !== "") .some((item) => !/^[0-9a-fA-F]{1,8}$/.test(`${item}`)); if (invalid) { return _("Reality short ID must contain 1-8 hex digits"); } return true; } function validateMtprotoSecret(_sectionId, value) { const secret = normalizeMtprotoBaseSecret(value); if (!secret) { return _("Use 32 hex characters"); } if (secret === "00000000000000000000000000000000") { return _("Use a non-zero secret"); } return true; } function validateSecurity(sectionId, value) { const protocol = getProtocol(sectionId); const selectedSecurity = value || getDefaultSecurity(protocol); const supportedSecurity = SECURITY_BY_PROTOCOL[protocol] || ["none"]; return supportedSecurity.includes(selectedSecurity) ? true : _("Unsupported security mode for this protocol"); } function getWidget(sectionId, option) { return document.getElementById( `widget.cbid.${UCI_PACKAGE}.${sectionId}.${option}`, ); } function getControlWidget(sectionId, option) { const widget = getWidget(sectionId, option); if (!widget) { return null; } if (/^(INPUT|SELECT|TEXTAREA)$/.test(widget.tagName)) { return widget; } return typeof widget.querySelector === "function" ? widget.querySelector("input:not([type='hidden']), select, textarea") : null; } function getSelectWidget(sectionId, option) { const widget = getControlWidget(sectionId, option); return widget && widget.tagName === "SELECT" ? widget : null; } function setWidgetValue(sectionId, option, value) { const widget = getControlWidget(sectionId, option); if (!widget) { uci.set(UCI_PACKAGE, sectionId, option, value); return; } widget.value = value; uci.set(UCI_PACKAGE, sectionId, option, value); widget.dispatchEvent(new Event("input", { bubbles: true })); widget.dispatchEvent(new Event("change", { bubbles: true })); } function renderSecurityChoices(select, protocol, selectedSecurity) { select.replaceChildren( ...(SECURITY_BY_PROTOCOL[protocol] || ["none"]).map((security) => E( "option", { value: security, selected: security === selectedSecurity ? "" : null, }, getSecurityLabel(security), ), ), ); } function syncSecurityChoices(sectionId) { const protocol = getProtocol(sectionId); const select = getSelectWidget(sectionId, "security"); const selectedSecurity = uci.get(UCI_PACKAGE, sectionId, "security") || getDefaultSecurity(protocol); const nextSecurity = validateSecurity(sectionId, selectedSecurity) === true ? selectedSecurity : getDefaultSecurity(protocol); if (select) { renderSecurityChoices(select, protocol, nextSecurity); if ( Array.from(select.options).some((option) => option.value === nextSecurity) ) { select.value = nextSecurity; } } if (selectedSecurity !== nextSecurity) { uci.set(UCI_PACKAGE, sectionId, "security", nextSecurity); } if (select) { select.dispatchEvent(new Event("input", { bubbles: true })); select.dispatchEvent(new Event("change", { bubbles: true })); } } function getModalNode() { return document.querySelector( "body.modal-overlay-active > #modal_overlay > .modal.cbi-modal", ); } function clearServerValidationMessage() { const modal = getModalNode(); if (modal) { modal .querySelectorAll(".fkp-server-validation-summary") .forEach((node) => node.remove()); } } function showServerValidationMessage(error) { const modal = getModalNode(); if (!modal) { return; } clearServerValidationMessage(); const message = error?.message || ""; const summary = E( "div", { class: "alert-message warning fkp-server-validation-summary", }, [ E("strong", {}, _("Cannot save server")), E("div", {}, _("Fix the highlighted fields and save again.")), message ? E("small", {}, message) : "", ], ); const buttonRow = modal.querySelector(".button-row"); modal.insertBefore(summary, buttonRow || modal.firstChild); const invalidInput = modal.querySelector(".cbi-input-invalid"); if (invalidInput) { invalidInput.scrollIntoView({ block: "center", behavior: "smooth" }); invalidInput.focus({ preventScroll: true }); } } function hasRealityKeypair(sectionId) { return ( !!uci.get(UCI_PACKAGE, sectionId, "reality_private_key") && !!uci.get(UCI_PACKAGE, sectionId, "reality_public_key") ); } function generateRealityKeypair(sectionId) { return fs .exec("/usr/bin/tachyon", ["generate_reality_keypair"]) .then((response) => { if ((response.code ?? 0) !== 0 || !response.stdout) { throw new Error(response.stderr || response.stdout || ""); } const data = JSON.parse(response.stdout); if (!data.success || !data.private_key || !data.public_key) { throw new Error(data.message || ""); } setWidgetValue(sectionId, "reality_private_key", data.private_key); setWidgetValue(sectionId, "reality_public_key", data.public_key); }) .catch((error) => { console.warn( error?.message || "Failed to generate Reality key pair", error, ); }); } function generateRealityKeypairIfMissing(sectionId) { return hasRealityKeypair(sectionId) ? Promise.resolve() : generateRealityKeypair(sectionId); } function shouldGenerateRealityKeypair(sectionId) { return ( getProtocol(sectionId) === "vless" && getEffectiveSecurity(sectionId) === "reality" ); } function generateAwgKeypair(sectionId) { return fs .exec("/usr/bin/tachyon", ["generate_wg_keypair"]) .then((response) => { const text = response?.stdout || ""; const matchPriv = text.match(/PrivateKey:\s*([^\s]+)/); const matchPub = text.match(/PublicKey:\s*([^\s]+)/); if (matchPriv && matchPub) { return { private_key: matchPriv[1], public_key: matchPub[1], }; } return null; }) .catch(() => null); } function generateAwgKeypairsIfMissing(sectionId) { const currentServerPriv = uci.get(UCI_PACKAGE, sectionId, "awg_private_key"); const currentPeerPub = uci.get(UCI_PACKAGE, sectionId, "awg_peer_public_key"); if (currentServerPriv && currentPeerPub) { return Promise.resolve(); } return Promise.all([ generateAwgKeypair(sectionId), generateAwgKeypair(sectionId), ]).then(([srvPair, cliPair]) => { if (srvPair) { uci.set(UCI_PACKAGE, sectionId, "awg_private_key", srvPair.private_key); uci.set( UCI_PACKAGE, sectionId, "awg_server_public_key", srvPair.public_key, ); setWidgetValue(sectionId, "awg_private_key", srvPair.private_key); setWidgetValue(sectionId, "awg_server_public_key", srvPair.public_key); } if (cliPair) { uci.set( UCI_PACKAGE, sectionId, "awg_client_private_key", cliPair.private_key, ); uci.set( UCI_PACKAGE, sectionId, "awg_peer_public_key", cliPair.public_key, ); setWidgetValue(sectionId, "awg_client_private_key", cliPair.private_key); setWidgetValue(sectionId, "awg_peer_public_key", cliPair.public_key); } }); } function prepareServerModal(sectionId) { if (shouldGenerateRealityKeypair(sectionId)) { return generateRealityKeypairIfMissing(sectionId); } if (getProtocol(sectionId) === "awg") { return generateAwgKeypairsIfMissing(sectionId); } return Promise.resolve(); } function addStreamDepends(option) { STREAM_PROTOCOLS.forEach((protocol) => option.depends("protocol", protocol)); } function addPortProtocolDepends(option, singBoxExtended) { PORT_PROTOCOLS.forEach((protocol) => option.depends("protocol", protocol)); if (singBoxExtended) { EXTENDED_PORT_PROTOCOLS.forEach((protocol) => option.depends("protocol", protocol), ); } } function addTlsDepends(option) { option.depends({ protocol: "vless", security: "tls" }); option.depends({ protocol: "vmess", security: "tls" }); option.depends({ protocol: "trojan", security: "tls" }); option.depends("protocol", "hysteria2"); } function addRealityDepends(option) { option.depends({ protocol: "vless", security: "reality" }); } function addTailscaleDepends(option) { option.depends("protocol", "tailscale"); } function addMtprotoDepends(option) { option.depends("protocol", "mtproto"); } function configureServerTextareaOption(option) { const originalRenderWidget = option.renderWidget; option.renderWidget = function (sectionId, optionIndex, cfgvalue) { const node = originalRenderWidget.call( this, sectionId, optionIndex, cfgvalue, ); const textarea = node && typeof node.querySelector === "function" ? node.querySelector("textarea") : node; if (textarea) { textarea.setAttribute("spellcheck", "false"); textarea.setAttribute("autocomplete", "off"); textarea.setAttribute("autocorrect", "off"); textarea.setAttribute("autocapitalize", "off"); textarea.setAttribute("data-gramm", "false"); textarea.setAttribute("data-gramm_editor", "false"); textarea.setAttribute("data-enable-grammarly", "false"); textarea.style.resize = "vertical"; textarea.style.maxWidth = "100%"; } return node; }; } function configureServerSection(sectionRef, options = {}) { sectionRef.sortable = false; sectionRef.nodescriptions = true; sectionRef.load = function () { // The main page table renders only non-modal fields; full server defaults // are loaded by LuCI's cloned modal map when the user opens Add/Edit. return loadServerTableOptions(this); }; sectionRef.renderMoreOptionsModal = function (sectionId, ev) { this.serverEditSnapshots ??= {}; this.serverEditSnapshots[sectionId] = captureServerEditState(sectionId); const capabilitiesTask = typeof options.loadCapabilities === "function" ? options.loadCapabilities() : Promise.resolve(); return Promise.resolve(capabilitiesTask) .then(() => prepareServerModal(sectionId)) .then(() => form.GridSection.prototype.renderMoreOptionsModal.call( this, sectionId, ev, ), ); }; sectionRef.handleAdd = function (_ev, name) { const configName = this.uciconfig ?? this.map.config; const sectionId = this.map.data.add(configName, this.sectiontype, name); const mapNode = this.getPreviousModalMap(); const prevMap = mapNode ? dom.findClassInstance(mapNode) : this.map; prevMap.addedSection = sectionId; const capabilitiesTask = typeof options.loadCapabilities === "function" ? options.loadCapabilities() : Promise.resolve(this.serverCapabilities); return Promise.resolve(capabilitiesTask).then((capabilities) => { if (capabilities) { this.serverCapabilities = normalizeServerCapabilities(capabilities); } ensureProtocolDefaults( sectionId, getDefaultProtocolForCapabilities(this.serverCapabilities), true, ); loadDefaultPublicHost() .then((host) => applyDefaultPublicHost(sectionId, host)) .catch(() => null); return this.renderMoreOptionsModal(sectionId); }); }; sectionRef.handleModalCancel = function (modalMap, ev, isSaving) { const sectionId = modalMap?.section; const snapshot = sectionId ? this.serverEditSnapshots?.[sectionId] : null; if (sectionId && this.serverEditSnapshots) { delete this.serverEditSnapshots[sectionId]; } if (!isSaving) { restoreServerEditState(snapshot); } return form.GridSection.prototype.handleModalCancel.call( this, modalMap, ev, isSaving, ); }; sectionRef.handleModalSave = function (modalMap, ev) { clearServerValidationMessage(); const mapNode = this.getActiveModalMap(); const activeMap = dom.findClassInstance(mapNode); return activeMap .parse() .then(() => form.GridSection.prototype.handleModalSave.call(this, modalMap, ev), ) .catch((error) => showServerValidationMessage(error)); }; sectionRef.renderRowActions = function (sectionId) { return renderServerRowActions(this, sectionId); }; } function createServerContent(section, options = {}) { injectServerStyles(); const capabilities = normalizeServerCapabilities(options); const singBoxExtended = capabilities.singBoxExtended; const singBoxTailscale = capabilities.singBoxTailscale; section.serverCapabilityOptions = {}; section.serverCapabilities = capabilities; let o = section.option(form.Flag, "enabled", _("Enable")); o.default = "1"; o.rmempty = false; o.editable = true; o.width = "6rem"; o = section.option(form.DummyValue, "_protocol_display", _("Protocol")); o.rawhtml = true; o.modalonly = false; o.cfgvalue = function (sectionId) { return getProtocolLabel(getProtocol(sectionId)); }; o.textvalue = function (sectionId) { return getProtocolLabel(getProtocol(sectionId)); }; o.width = "7rem"; o = section.option(form.Value, "label", _("Server name")); o.modalonly = true; o.rmempty = false; o.validate = validateRequiredText; o.onchange = function (_ev, sectionId, value) { const previousLabel = getServerName(sectionId); const currentUsername = uci.get(UCI_PACKAGE, sectionId, "server_username") || ""; uci.set(UCI_PACKAGE, sectionId, "label", value); if ( getProtocol(sectionId) === "socks" && (!currentUsername || currentUsername === previousLabel || currentUsername === sectionId) ) { const nextUsername = value || sectionId; uci.set(UCI_PACKAGE, sectionId, "server_username", nextUsername); setWidgetValue(sectionId, "server_username", nextUsername); } }; o = section.option(form.ListValue, "protocol", _("Protocol")); section.serverCapabilityOptions.protocol = o; populateProtocolValues(o, capabilities); o.default = getDefaultProtocolForCapabilities(capabilities); o.rmempty = false; o.modalonly = true; o.validate = validateRequired; o.onchange = function (_ev, sectionId, value) { ensureProtocolDefaults(sectionId, value, true); syncSecurityChoices(sectionId); if (value === "vless" && getEffectiveSecurity(sectionId) === "reality") { generateRealityKeypairIfMissing(sectionId); } if (value === "awg") { generateAwgKeypairsIfMissing(sectionId); } }; o = section.option(form.Value, "listen", _("Listen address")); o.default = "0.0.0.0"; o.rmempty = false; o.modalonly = true; o.validate = validateListenAddress; addPortProtocolDepends(o, true); o = section.option(form.Value, "listen_port", _("Listen port")); o.default = "443"; o.rmempty = false; o.modalonly = true; o.validate = validatePort; addPortProtocolDepends(o, true); o = section.option(form.Value, "public_host", _("Public host")); o.modalonly = true; o.rmempty = false; o.validate = validateHost; addPortProtocolDepends(o, true); o.load = function (sectionId) { return loadDefaultPublicHost().then((host) => { applyDefaultPublicHost(sectionId, host); return uci.get(UCI_PACKAGE, sectionId, "public_host") || host || ""; }); }; o = section.option(form.ListValue, "routing_mode", _("Routing mode")); o.value("rules", _("Rules")); o.value("direct", _("Direct")); o.value("section", _("Selected section")); o.default = "rules"; o.rmempty = false; o.modalonly = true; o.validate = validateRequired; o = section.option(form.ListValue, "routing_section", _("Routing section")); o.depends("routing_mode", "section"); o.modalonly = true; o.rmempty = false; o.validate = validateRequired; o.cfgvalue = function (sectionId) { return uci.get(UCI_PACKAGE, sectionId, "routing_section"); }; o.load = function () { return loadRoutingSectionChoices(this); }; o = section.option( form.Flag, "isolate_lan_for_users", _("Restrict LAN access for specific users"), _( "Blocks access to private/LAN IP addresses (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, etc.) for selected users before applying routing mode.", ), ); o.modalonly = true; o.rmempty = true; o = section.option( form.DynamicList, "isolated_users", _("Restricted users (auth_user)"), _( "User names (e.g. from VLESS/Trojan/SS/JSON inbound users) that are forbidden from reaching LAN subnets. Leave empty to restrict all users on this inbound.", ), ); o.depends("isolate_lan_for_users", "1"); o.modalonly = true; o.rmempty = true; o = section.option( form.TextValue, "inbound_json", _("Inbound JSON"), _("Enter a complete sing-box inbound object"), ); o.depends("protocol", "json_inbound"); o.rows = 10; o.wrap = "soft"; o.textarea = true; o.modalonly = true; o.rmempty = false; o.validate = validateInboundJson; configureServerTextareaOption(o); o = section.option(form.ListValue, "security", _("Security")); o.value("reality", "Reality"); o.value("tls", "TLS"); o.value("none", _("None")); o.default = "reality"; o.rmempty = false; o.modalonly = true; addStreamDepends(o); o.depends("protocol", "hysteria2"); o.validate = validateSecurity; o.onchange = function (_ev, sectionId, value) { if (value === "reality") { generateRealityKeypairIfMissing(sectionId); } }; { const originalRenderWidget = o.renderWidget; o.renderWidget = function (sectionId, optionIndex, cfgvalue) { const node = originalRenderWidget.call( this, sectionId, optionIndex, cfgvalue, ); window.setTimeout(() => syncSecurityChoices(sectionId), 0); return node; }; } o = section.option(form.Value, "tls_server_name", _("Server name / SNI")); o.modalonly = true; o.rmempty = false; o.validate = validateHost; o.load = function (sectionId) { const current = normalizeHost( uci.get(UCI_PACKAGE, sectionId, "tls_server_name"), ); if (current) { return current; } const fallback = "www.microsoft.com"; uci.set(UCI_PACKAGE, sectionId, "tls_server_name", fallback); return fallback; }; addTlsDepends(o); addRealityDepends(o); o = section.option(form.MultiValue, "tls_alpn", _("ALPN")); o.value("h3", "h3"); o.value("h2", "h2"); o.value("http/1.1", "http/1.1"); o.create = false; o.rmempty = true; o.modalonly = true; addTlsDepends(o); addRealityDepends(o); o = section.option(form.Value, "tls_certificate_path", _("Certificate path")); o.modalonly = true; o.rmempty = false; o.validate = validateTlsCertificatePath; o.load = function (sectionId) { const current = uci.get(UCI_PACKAGE, sectionId, "tls_certificate_path"); if (current) { return current; } const value = defaultTlsCertificatePath(sectionId); uci.set(UCI_PACKAGE, sectionId, "tls_certificate_path", value); return value; }; addTlsDepends(o); o = section.option(form.Value, "tls_key_path", _("Key path")); o.modalonly = true; o.rmempty = false; o.validate = validateTlsKeyPath; o.load = function (sectionId) { const current = uci.get(UCI_PACKAGE, sectionId, "tls_key_path"); if (current) { return current; } const value = defaultTlsKeyPath(sectionId); uci.set(UCI_PACKAGE, sectionId, "tls_key_path", value); return value; }; addTlsDepends(o); o = section.option( form.Value, "reality_handshake_server", _("Reality fake site"), ); o.default = "www.microsoft.com"; o.modalonly = true; o.rmempty = false; o.validate = validateHost; addRealityDepends(o); o = section.option( form.Value, "reality_handshake_server_port", _("Reality fake site port"), ); o.default = "443"; o.modalonly = true; o.rmempty = false; o.validate = validatePort; addRealityDepends(o); o = section.option( form.Value, "reality_private_key", _("Reality private key"), ); o.modalonly = true; o.rmempty = false; o.validate = validateRequired; addRealityDepends(o); o = section.option(form.Value, "reality_public_key", _("Reality public key")); o.modalonly = true; o.rmempty = false; o.validate = validateRequired; addRealityDepends(o); o = section.option(form.Value, "reality_short_id", _("Reality short ID")); o.modalonly = true; o.rmempty = false; o.validate = validateShortId; o.load = function (sectionId) { const current = getFirstListValue(sectionId, "reality_short_id"); if (current) { uci.set(UCI_PACKAGE, sectionId, "reality_short_id", current); return current; } const value = randomHex(4); uci.set(UCI_PACKAGE, sectionId, "reality_short_id", value); return value; }; addRealityDepends(o); o = section.option( form.Value, "reality_max_time_difference", _("Reality max time difference"), ); o.default = "1m"; o.modalonly = true; o.rmempty = false; o.validate = validateRequiredDuration; addRealityDepends(o); o = section.option( form.ListValue, "client_fingerprint", _("Client fingerprint"), ); ["chrome", "firefox", "edge", "safari", "ios", "android", "random"].forEach( (value) => o.value(value, value), ); o.default = "chrome"; o.rmempty = false; o.modalonly = true; o.validate = validateRequired; addRealityDepends(o); o = section.option(form.ListValue, "transport", _("Transport")); section.serverCapabilityOptions.transport = o; populateTransportValues(o, singBoxExtended); o.default = "tcp"; o.rmempty = false; o.modalonly = true; addStreamDepends(o); o = section.option(form.Value, "transport_path", _("Transport path")); o.modalonly = true; o.validate = validateTransportPath; o.depends({ protocol: "vless", transport: "ws" }); o.depends({ protocol: "vmess", transport: "ws" }); o.depends({ protocol: "trojan", transport: "ws" }); o.depends({ protocol: "vless", transport: "http" }); o.depends({ protocol: "vmess", transport: "http" }); o.depends({ protocol: "trojan", transport: "http" }); o.depends({ protocol: "vless", transport: "httpupgrade" }); o.depends({ protocol: "vmess", transport: "httpupgrade" }); o.depends({ protocol: "trojan", transport: "httpupgrade" }); o.depends({ protocol: "vless", transport: "xhttp" }); o.depends({ protocol: "vmess", transport: "xhttp" }); o.depends({ protocol: "trojan", transport: "xhttp" }); o = section.option(form.Value, "transport_host", _("Transport host")); o.modalonly = true; o.validate = validateOptionalHost; o.depends({ protocol: "vless", transport: "ws" }); o.depends({ protocol: "vmess", transport: "ws" }); o.depends({ protocol: "trojan", transport: "ws" }); o.depends({ protocol: "vless", transport: "httpupgrade" }); o.depends({ protocol: "vmess", transport: "httpupgrade" }); o.depends({ protocol: "trojan", transport: "httpupgrade" }); o.depends({ protocol: "vless", transport: "xhttp" }); o.depends({ protocol: "vmess", transport: "xhttp" }); o.depends({ protocol: "trojan", transport: "xhttp" }); o = section.option(form.ListValue, "transport_xhttp_mode", _("XHTTP mode")); o.value("auto", "auto"); o.value("packet-up", "packet-up"); o.value("stream-up", "stream-up"); o.value("stream-one", "stream-one"); o.default = "auto"; o.rmempty = false; o.modalonly = true; o.depends({ protocol: "vless", transport: "xhttp" }); o.depends({ protocol: "vmess", transport: "xhttp" }); o.depends({ protocol: "trojan", transport: "xhttp" }); o = section.option(form.DynamicList, "transport_hosts", _("HTTP hosts")); o.modalonly = true; o.validate = validateOptionalHostList; o.depends({ protocol: "vless", transport: "http" }); o.depends({ protocol: "vmess", transport: "http" }); o.depends({ protocol: "trojan", transport: "http" }); o = section.option(form.Value, "transport_service_name", _("gRPC service")); o.modalonly = true; o.validate = validateGrpcServiceName; o.depends({ protocol: "vless", transport: "grpc" }); o.depends({ protocol: "vmess", transport: "grpc" }); o.depends({ protocol: "trojan", transport: "grpc" }); o = section.option(form.ListValue, "shadowsocks_method", _("Method")); o.value("aes-128-gcm", "aes-128-gcm"); o.value("aes-256-gcm", "aes-256-gcm"); o.value("chacha20-ietf-poly1305", "chacha20-ietf-poly1305"); o.default = "aes-128-gcm"; o.rmempty = false; o.modalonly = true; o.validate = validateRequired; o.depends("protocol", "shadowsocks"); o = section.option( form.Flag, "socks_auth_enabled", _("Enable authentication"), ); o.default = "1"; o.rmempty = false; o.modalonly = true; o.depends("protocol", "socks"); o = section.option(form.Value, "server_username", _("Username")); o.modalonly = true; o.rmempty = false; o.validate = validateRequiredText; o.load = function (sectionId) { const current = uci.get(UCI_PACKAGE, sectionId, "server_username"); if (current) { return current; } const value = getServerName(sectionId); uci.set(UCI_PACKAGE, sectionId, "server_username", value); return value; }; o.depends({ protocol: "socks", socks_auth_enabled: "1" }); o = section.option(form.Value, "server_password", _("Password")); o.modalonly = true; o.rmempty = false; o.validate = validateRequiredText; o.load = function (sectionId) { const current = uci.get(UCI_PACKAGE, sectionId, "server_password"); if (current) { return current; } const value = generatePassword(); uci.set(UCI_PACKAGE, sectionId, "server_password", value); return value; }; o.depends({ protocol: "socks", socks_auth_enabled: "1" }); o = section.option(form.Value, "vmess_alter_id", _("Alter ID")); o.default = "0"; o.rmempty = false; o.modalonly = true; o.depends("protocol", "vmess"); o.validate = function (_sectionId, value) { if (value == null || value === "") { return _("This field is required"); } return /^[0-9]+$/.test(value) ? true : _("Use a non-negative integer"); }; o = section.option(form.ListValue, "vless_flow", _("VLESS flow")); o.value("none", _("None")); o.value("xtls-rprx-vision", "xtls-rprx-vision"); o.default = "none"; o.rmempty = false; o.modalonly = true; o.validate = validateRequired; o.depends("protocol", "vless"); o = section.option(form.ListValue, "hysteria2_obfs_type", _("Obfuscation")); o.value("", _("None")); o.value("salamander", "salamander"); o.default = ""; o.modalonly = true; o.depends("protocol", "hysteria2"); o.onchange = function (_ev, sectionId, value) { if (value === "salamander") { setDefault(sectionId, "hysteria2_obfs_password", generatePassword()); const widget = getControlWidget(sectionId, "hysteria2_obfs_password"); if (widget && !widget.value) { setWidgetValue( sectionId, "hysteria2_obfs_password", uci.get(UCI_PACKAGE, sectionId, "hysteria2_obfs_password"), ); } } }; o = section.option( form.Value, "hysteria2_obfs_password", _("Obfuscation password"), ); o.modalonly = true; o.rmempty = false; o.validate = validateRequired; o.load = function (sectionId) { const current = uci.get(UCI_PACKAGE, sectionId, "hysteria2_obfs_password"); if (current) { return current; } const value = generatePassword(); uci.set(UCI_PACKAGE, sectionId, "hysteria2_obfs_password", value); return value; }; o.depends({ protocol: "hysteria2", hysteria2_obfs_type: "salamander" }); o = section.option(form.Value, "mtproto_secret", _("Secret")); o.modalonly = true; o.rmempty = false; o.validate = validateMtprotoSecret; o.load = function (sectionId) { const current = uci.get(UCI_PACKAGE, sectionId, "mtproto_secret"); const baseSecret = normalizeMtprotoBaseSecret(current); const faketls = getMtprotoFaketlsFromSecret(current); if (baseSecret) { uci.set(UCI_PACKAGE, sectionId, "mtproto_secret", baseSecret); if (faketls && !uci.get(UCI_PACKAGE, sectionId, "mtproto_faketls")) { uci.set(UCI_PACKAGE, sectionId, "mtproto_faketls", faketls); } return baseSecret; } if (current) { return current; } const value = generateMtprotoSecret(); uci.set(UCI_PACKAGE, sectionId, "mtproto_secret", value); return value; }; addMtprotoDepends(o); o = section.option(form.Value, "mtproto_faketls", _("FakeTLS host")); o.default = "google.com"; o.modalonly = true; o.rmempty = false; o.validate = validateHost; o.load = function (sectionId) { const current = uci.get(UCI_PACKAGE, sectionId, "mtproto_faketls"); if (current) { return current; } const faketls = getMtprotoFaketlsFromSecret( uci.get(UCI_PACKAGE, sectionId, "mtproto_secret"), ) || "google.com"; uci.set(UCI_PACKAGE, sectionId, "mtproto_faketls", faketls); return faketls; }; addMtprotoDepends(o); o = section.option(form.Flag, "mtproto_padding", _("Padding")); o.default = "1"; o.modalonly = true; o.rmempty = false; addMtprotoDepends(o); o = section.option(form.Value, "mtproto_concurrency", _("Concurrency")); o.modalonly = true; o.rmempty = true; o.validate = validateOptionalNonNegativeInteger; addMtprotoDepends(o); o = section.option( form.Value, "mtproto_domain_fronting_port", _("Fronting port"), ); o.default = "443"; o.modalonly = true; o.rmempty = false; o.validate = validatePort; addMtprotoDepends(o); o = section.option( form.Value, "mtproto_domain_fronting_ip", _("Fronting IP"), ); o.modalonly = true; o.rmempty = true; o.validate = validateOptionalIp; addMtprotoDepends(o); o = section.option( form.Flag, "mtproto_domain_fronting_proxy_protocol", _("Fronting proxy protocol"), ); o.default = "0"; o.modalonly = true; o.rmempty = true; addMtprotoDepends(o); o = section.option(form.ListValue, "mtproto_prefer_ip", _("Preferred IP")); o.value("prefer-ipv4", "prefer-ipv4"); o.value("prefer-ipv6", "prefer-ipv6"); o.value("only-ipv4", "only-ipv4"); o.value("only-ipv6", "only-ipv6"); o.default = "prefer-ipv4"; o.rmempty = false; o.modalonly = true; o.validate = validateRequired; addMtprotoDepends(o); o = section.option(form.Flag, "mtproto_auto_update", _("Auto update")); o.default = "0"; o.modalonly = true; o.rmempty = true; addMtprotoDepends(o); o = section.option( form.Flag, "mtproto_allow_fallback_on_unknown_dc", _("Fallback on unknown DC"), ); o.default = "0"; o.modalonly = true; o.rmempty = true; addMtprotoDepends(o); o = section.option( form.Value, "mtproto_tolerate_time_skewness", _("Time skew tolerance"), ); o.default = "3s"; o.modalonly = true; o.rmempty = false; o.validate = validateRequiredDuration; addMtprotoDepends(o); o = section.option(form.Value, "mtproto_idle_timeout", _("Idle timeout")); o.default = "5m"; o.modalonly = true; o.rmempty = false; o.validate = validateRequiredDuration; addMtprotoDepends(o); o = section.option( form.Value, "mtproto_handshake_timeout", _("Handshake timeout"), ); o.default = "10s"; o.modalonly = true; o.rmempty = false; o.validate = validateRequiredDuration; addMtprotoDepends(o); o = section.option(form.Value, "tailscale_auth_key", _("Tailscale auth key")); o.modalonly = true; o.rmempty = false; o.validate = validateRequired; addTailscaleDepends(o); o = section.option( form.Value, "tailscale_control_url", _("Tailscale control URL"), ); o.default = "https://controlplane.tailscale.com"; o.modalonly = true; o.rmempty = false; o.validate = validateHttpUrl; addTailscaleDepends(o); o = section.option(form.Value, "tailscale_hostname", _("Tailscale hostname")); o.modalonly = true; o.rmempty = false; o.validate = validateRequiredText; o.load = function (sectionId) { const current = uci.get(UCI_PACKAGE, sectionId, "tailscale_hostname"); if (current) { return current; } const value = defaultTailscaleHostname(sectionId); uci.set(UCI_PACKAGE, sectionId, "tailscale_hostname", value); return value; }; addTailscaleDepends(o); o = section.option( form.Flag, "tailscale_advertise_exit_node", _("Advertise exit node"), ); o.default = "1"; o.modalonly = true; o.rmempty = false; addTailscaleDepends(o); o = section.option( form.DynamicList, "tailscale_advertise_routes", _("Advertise routes"), ); o.modalonly = true; o.rmempty = true; o.validate = (_sectionId, value) => validateOptionalCidrOrIp(value); addTailscaleDepends(o); o = section.option(form.Flag, "tailscale_accept_routes", _("Accept routes")); o.default = "0"; o.modalonly = true; o.rmempty = true; addTailscaleDepends(o); o = section.option(form.ListValue, "tailscale_mode", _("Tailscale mode")); o.value("singbox", _("sing-box endpoint (userspace, default)")); o.value( "native", _("Native tailscaled (tailnet reachable from LAN without client)"), ); o.default = "singbox"; o.modalonly = true; o.rmempty = false; addTailscaleDepends(o); o = section.option( form.Flag, "tailscale_masquerade", _("Masquerade LAN traffic into tailnet"), _( "SNAT LAN sources when leaving through tailscale0 so peers can answer. Required unless every peer accepts routes.", ), ); o.default = "1"; o.modalonly = true; o.rmempty = false; o.depends({ protocol: "tailscale", tailscale_mode: "native" }); o = section.option( form.Flag, "tailscale_use_exit_node", _("Use tailnet exit node for LAN"), _( "Route all non-local LAN traffic through the tailnet exit node instead of the proxy engines.", ), ); o.default = "0"; o.modalonly = true; o.rmempty = false; o.depends({ protocol: "tailscale", tailscale_mode: "native" }); // AWG Server Options o = section.option( form.Value, "awg_local_address", _("Server Address"), _("Server VPN IP address and subnet (e.g. 10.0.0.1/24)"), ); o.default = "10.0.0.1/24"; o.modalonly = true; o.rmempty = false; o.validate = validateRequired; o.depends("protocol", "awg"); o = section.option( form.Value, "awg_client_address", _("Client Address"), _("Client VPN IP address (e.g. 10.0.0.2/32)"), ); o.default = "10.0.0.2/32"; o.modalonly = true; o.rmempty = false; o.validate = validateRequired; o.depends("protocol", "awg"); o = section.option( form.Value, "awg_dns", _("Client DNS"), _("DNS server pushed to the client configuration"), ); o.default = "10.0.0.1"; o.modalonly = true; o.rmempty = false; o.depends("protocol", "awg"); o = section.option(form.Value, "awg_mtu", _("MTU")); o.default = "1280"; o.modalonly = true; o.rmempty = false; o.validate = validatePositiveInteger; o.depends("protocol", "awg"); o = section.option(form.Value, "awg_keepalive", _("Persistent Keepalive")); o.default = "25"; o.modalonly = true; o.rmempty = false; o.validate = validatePositiveInteger; o.depends("protocol", "awg"); o = section.option(form.Value, "awg_private_key", _("Server Private Key")); o.modalonly = true; o.rmempty = false; o.validate = validateRequired; o.depends("protocol", "awg"); o = section.option( form.Value, "awg_server_public_key", _("Server Public Key"), ); o.modalonly = true; o.rmempty = true; o.depends("protocol", "awg"); o = section.option( form.Value, "awg_client_private_key", _("Client Private Key"), ); o.modalonly = true; o.rmempty = false; o.validate = validateRequired; o.depends("protocol", "awg"); o = section.option(form.Value, "awg_peer_public_key", _("Client Public Key")); o.modalonly = true; o.rmempty = false; o.validate = validateRequired; o.depends("protocol", "awg"); o = section.option( form.Value, "awg_preshared_key", _("Pre-shared Key (PSK)"), ); o.modalonly = true; o.rmempty = true; o.depends("protocol", "awg"); o = section.option( form.ListValue, "awg_version", _("AmneziaWG Version"), _( "Protocol version: 2.0 (standard integer headers) or 3.1 (header protection key, padding ranges, advanced timers)", ), ); o.modalonly = true; o.rmempty = false; o.default = "2.0"; o.value("2.0", "AmneziaWG 2.0"); o.value("3.0", "AmneziaWG 3.0"); o.value("3.1", "AmneziaWG 3.1"); o.depends("protocol", "awg"); o.write = function (section_id, formvalue) { if (formvalue === "2.0") { uci.unset(UCI_PACKAGE, section_id, "awg_header_protection_key"); uci.unset(UCI_PACKAGE, section_id, "awg_content_padding_addition"); uci.unset(UCI_PACKAGE, section_id, "awg_rekey_after_time"); uci.unset(UCI_PACKAGE, section_id, "awg_rekey_timeout"); uci.unset(UCI_PACKAGE, section_id, "awg_reject_after_time"); uci.unset(UCI_PACKAGE, section_id, "awg_keepalive_timeout"); uci.unset(UCI_PACKAGE, section_id, "awg_max_handshake_attempts"); } else if (formvalue === "3.0") { uci.unset(UCI_PACKAGE, section_id, "awg_rekey_after_time"); uci.unset(UCI_PACKAGE, section_id, "awg_rekey_timeout"); uci.unset(UCI_PACKAGE, section_id, "awg_reject_after_time"); uci.unset(UCI_PACKAGE, section_id, "awg_keepalive_timeout"); uci.unset(UCI_PACKAGE, section_id, "awg_max_handshake_attempts"); } return uci.set(UCI_PACKAGE, section_id, "awg_version", formvalue); }; const awgV3ParamNames = [ "awg_header_protection_key", "awg_content_padding_addition", ]; const awgV31TimerNames = [ "awg_rekey_after_time", "awg_rekey_timeout", "awg_reject_after_time", "awg_keepalive_timeout", "awg_max_handshake_attempts", ]; const awgParams = [ { name: "awg_jc", title: _("Junk packet count (Jc)"), desc: _("Number of junk packets before handshake (1-10)"), default: "4", }, { name: "awg_jmin", title: _("Junk packet min size (Jmin)"), desc: _("Minimum size of junk packets"), default: "40", }, { name: "awg_jmax", title: _("Junk packet max size (Jmax)"), desc: _("Maximum size of junk packets"), default: "70", }, { name: "awg_s1", title: _("Init packet junk size (S1)"), desc: _("Junk size for initiation packet"), default: "0", }, { name: "awg_s2", title: _("Response packet junk size (S2)"), desc: _("Junk size for response packet"), default: "0", }, { name: "awg_s3", title: _("Cookie packet junk size (S3)"), desc: _("Junk size for cookie packet"), default: "0", }, { name: "awg_s4", title: _("Transport packet junk size (S4)"), desc: _("Junk size for transport packet"), default: "0", }, { name: "awg_h1", title: _("Init packet header (H1)"), desc: _("Custom header for handshake initiation"), default: "1", }, { name: "awg_h2", title: _("Response packet header (H2)"), desc: _("Custom header for handshake response"), default: "2", }, { name: "awg_h3", title: _("Underload packet header (H3)"), desc: _("Custom header for cookie packet"), default: "3", }, { name: "awg_h4", title: _("Transport packet header (H4)"), desc: _("Custom header for data packet"), default: "4", }, { name: "awg_i1", title: _("Init payload (I1)"), desc: _("Hex or binary payload for init"), default: "", }, { name: "awg_i2", title: _("Response payload (I2)"), desc: _("Hex or binary payload for response"), default: "", }, { name: "awg_i3", title: _("Payload (I3)"), desc: _("Hex or binary payload"), default: "", }, { name: "awg_i4", title: _("Payload (I4)"), desc: _("Hex or binary payload"), default: "", }, { name: "awg_i5", title: _("Payload (I5)"), desc: _("Hex or binary payload"), default: "", }, { name: "awg_j1", title: _("Junk payload (J1)"), desc: _("Hex or binary junk payload"), default: "", }, { name: "awg_j2", title: _("Junk payload (J2)"), desc: _("Hex or binary junk payload"), default: "", }, { name: "awg_j3", title: _("Junk payload (J3)"), desc: _("Hex or binary junk payload"), default: "", }, { name: "awg_itime", title: _("Init timeout (Itime)"), desc: _("Init handshake timeout"), default: "", }, { name: "awg_header_protection_key", title: _("Header Protection Key"), desc: _("AmneziaWG 3.1 header protection key"), default: "", }, { name: "awg_content_padding_addition", title: _("Content Padding Addition"), desc: _("AmneziaWG 3.1 content padding range, e.g. 38-104"), default: "", }, { name: "awg_rekey_after_time", title: _("Rekey After Time"), desc: _("AmneziaWG 3.1 rekey after time"), default: "", }, { name: "awg_rekey_timeout", title: _("Rekey Timeout"), desc: _("AmneziaWG 3.1 rekey timeout"), default: "", }, { name: "awg_reject_after_time", title: _("Reject After Time"), desc: _("AmneziaWG 3.1 reject after time"), default: "", }, { name: "awg_keepalive_timeout", title: _("Keepalive Timeout"), desc: _("AmneziaWG 3.1 keepalive timeout"), default: "", }, { name: "awg_max_handshake_attempts", title: _("Max Handshake Attempts"), desc: _("AmneziaWG 3.1 max handshake attempts"), default: "", }, ]; awgParams.forEach((param) => { let opt = section.option(form.Value, param.name, param.title, param.desc); opt.modalonly = true; opt.rmempty = true; if (param.default !== "") opt.default = param.default; if (awgV31TimerNames.includes(param.name)) { opt.depends({ protocol: "awg", awg_version: "3.1" }); } else if (awgV3ParamNames.includes(param.name)) { opt.depends({ protocol: "awg", awg_version: "3.0" }); opt.depends({ protocol: "awg", awg_version: "3.1" }); } else { opt.depends("protocol", "awg"); } }); } return baseclass.extend({ applyServerCapabilities, configureServerSection, createServerContent, preloadServerModalData, });