Files
NeoTachyon/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/server.js
T

3587 lines
100 KiB
JavaScript

"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 = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${viewSize} ${viewSize}" shape-rendering="crispEdges"><path fill="#fff" d="M0 0h${viewSize}v${viewSize}H0z"/><path fill="#000" d="${path.join(" ")}"/></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:560px;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;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:20px}
.fkp-server-info-modal__qr-col{width:100%;max-width:180px}
.fkp-server-info-modal__details-col{width:100%}
}
`,
),
);
}
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`;
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");
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: "",
},
];
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;
opt.depends("protocol", "awg");
});
}
return baseclass.extend({
applyServerCapabilities,
configureServerSection,
createServerContent,
preloadServerModalData,
});