3525 lines
98 KiB
JavaScript
3525 lines
98 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);
|
|
|
|
if (normalized.singBoxTailscale) {
|
|
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() {
|
|
const label = _("Edit rule action");
|
|
|
|
return label === "Edit rule action" ? "Edit" : label;
|
|
}
|
|
|
|
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, singBoxTailscale) {
|
|
option.depends(
|
|
"protocol",
|
|
singBoxTailscale ? "tailscale" : "__unsupported_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.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, singBoxTailscale);
|
|
|
|
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, singBoxTailscale);
|
|
|
|
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, singBoxTailscale);
|
|
|
|
o = section.option(
|
|
form.Flag,
|
|
"tailscale_advertise_exit_node",
|
|
_("Advertise exit node"),
|
|
);
|
|
o.default = "1";
|
|
o.modalonly = true;
|
|
o.rmempty = false;
|
|
addTailscaleDepends(o, singBoxTailscale);
|
|
|
|
o = section.option(
|
|
form.DynamicList,
|
|
"tailscale_advertise_routes",
|
|
_("Advertise routes"),
|
|
);
|
|
o.modalonly = true;
|
|
o.rmempty = true;
|
|
o.validate = (_sectionId, value) => validateOptionalCidrOrIp(value);
|
|
addTailscaleDepends(o, singBoxTailscale);
|
|
|
|
o = section.option(form.Flag, "tailscale_accept_routes", _("Accept routes"));
|
|
o.default = "0";
|
|
o.modalonly = true;
|
|
o.rmempty = true;
|
|
addTailscaleDepends(o, singBoxTailscale);
|
|
|
|
// 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,
|
|
});
|