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

816 lines
22 KiB
JavaScript

"use strict";
"require form";
"require uci";
"require baseclass";
"require tools.widgets as widgets";
"require view.tachyon.main as main";
const UCI_PACKAGE = main.TACHYON_UCI_PACKAGE;
function isSingBoxDuration(value) {
return /^(?=.*[1-9])([0-9]+(?:\.[0-9]+)?(?:ns|us|ms|s|m|h|d))+$/.test(value);
}
function latencyTestUrlChoices() {
return Array.isArray(main.LATENCY_TEST_URL_OPTIONS)
? main.LATENCY_TEST_URL_OPTIONS
: [main.DEFAULT_LATENCY_TEST_URL || "https://www.gstatic.com/generate_204"];
}
function validateLatencyTestUrl(value) {
const validation = main.validateUrl(`${value || ""}`.trim());
return validation.valid ? true : validation.message;
}
function isDownloadSectionAction(action, capabilities) {
switch (action) {
case "connection":
case "proxy":
case "outbound":
case "vpn":
return true;
case "zapret":
return !capabilities?.loaded || Boolean(capabilities.zapretInstalled);
case "zapret2":
return !capabilities?.loaded || Boolean(capabilities.zapret2Installed);
case "byedpi":
return !capabilities?.loaded || Boolean(capabilities.byedpiInstalled);
default:
return false;
}
}
function refreshDownloadSectionChoices(option, capabilities) {
const sections = option.map?.data?.state?.values?.[UCI_PACKAGE] ?? {};
option.keylist = [];
option.vallist = [];
for (const secName in sections) {
const sec = sections[secName];
if (
sec[".type"] === "section" &&
sec.enabled !== "0" &&
isDownloadSectionAction(sec.action, capabilities)
) {
option.value(secName, sec.label || secName);
}
}
}
function configureDownloadSectionOption(option, sectionOption, capabilities) {
option.default = "";
option.rmempty = false;
option.cfgvalue = function (section_id) {
return uci.get(UCI_PACKAGE, section_id, sectionOption) || "";
};
option.load = function (section_id) {
refreshDownloadSectionChoices(this, capabilities);
return this.cfgvalue(section_id);
};
option.write = function (section_id, value) {
const normalized = value ? `${value}`.trim() : "";
if (normalized) {
uci.set(UCI_PACKAGE, section_id, sectionOption, normalized);
} else {
uci.unset(UCI_PACKAGE, section_id, sectionOption);
}
};
option.remove = function (section_id) {
uci.unset(UCI_PACKAGE, section_id, sectionOption);
};
option.validate = function (_section_id, value) {
return value ? true : _("Select a section");
};
}
function configureDownloadViaProxyFlag(option, sectionOption) {
option.default = "0";
option.rmempty = false;
option.write = function (section_id, value) {
const enabled = value === "1" || value === true;
uci.set(UCI_PACKAGE, section_id, this.option, enabled ? "1" : "0");
if (!enabled) {
uci.unset(UCI_PACKAGE, section_id, sectionOption);
}
};
}
function optionListValues(option, section_id) {
const formValue = option.formvalue(section_id);
const value = formValue != null ? formValue : option.cfgvalue(section_id);
return L.toArray(value)
.map((item) => `${item || ""}`.trim())
.filter(Boolean);
}
function configureDnsList(option, choices, defaultValue) {
Object.entries(choices).forEach(([key, label]) => {
option.value(key, _(label));
});
option.default = [defaultValue];
option.rmempty = false;
option.validate = function (_section_id, value) {
const normalized = `${value || ""}`.trim();
if (!normalized) {
return optionListValues(option, _section_id).length > 0
? true
: _("Add at least one DNS server");
}
const validation = main.validateDNS(normalized);
return validation.valid ? true : validation.message;
};
}
function configureDnsFailoverVisibility(option, dnsOption, bootstrapOption) {
option.depends("dns_server", "__tachyon_multiple_dns__");
option.depends("bootstrap_dns_server", "__tachyon_multiple_dns__");
option.retain = true;
option.checkDepends = function (section_id) {
return (
optionListValues(dnsOption, section_id).length > 1 ||
optionListValues(bootstrapOption, section_id).length > 1
);
};
}
function configureDnsDuration(
option,
defaultValue,
dnsOption,
bootstrapOption,
) {
option.default = defaultValue;
option.rmempty = false;
option.validate = function (_section_id, value) {
const normalized = `${value || ""}`.trim();
if (!normalized || !isSingBoxDuration(normalized)) {
return _("Use sing-box duration format like 10s, 1m or 2m30s");
}
return true;
};
configureDnsFailoverVisibility(option, dnsOption, bootstrapOption);
}
function createSettingsContent(section, capabilities) {
let o = section.option(
form.ListValue,
"dns_type",
_("DNS Protocol Type"),
_("Select DNS protocol to use"),
);
o.value("doh", _("DNS over HTTPS (DoH)"));
o.value("dot", _("DNS over TLS (DoT)"));
o.value("udp", _("UDP (Unprotected DNS)"));
o.default = "udp";
o.rmempty = false;
const dnsOption = section.option(
form.DynamicList,
"dns_server",
_("DNS Servers"),
_(
"Main DNS server. If multiple servers are selected, a timeout switches to a backup.",
),
);
configureDnsList(dnsOption, main.DNS_SERVER_OPTIONS, "77.88.8.8");
const bootstrapOption = section.option(
form.DynamicList,
"bootstrap_dns_server",
_("Bootstrap DNS Servers"),
_(
"DNS server used to obtain IP addresses for upstream DNS and proxies. If multiple servers are selected, a timeout switches to a backup.",
),
);
configureDnsList(
bootstrapOption,
main.BOOTSTRAP_DNS_SERVER_OPTIONS,
"77.88.8.8",
);
o = section.option(
form.Value,
"dns_check_interval",
_("DNS Check Interval"),
_("How often to check the active DNS servers."),
);
configureDnsDuration(o, "10s", dnsOption, bootstrapOption);
o = section.option(
form.Value,
"dns_recovery_check_interval",
_("Higher-priority DNS Check"),
_("How often to check whether a higher-priority DNS server has recovered."),
);
configureDnsDuration(o, "60s", dnsOption, bootstrapOption);
o = section.option(
form.Value,
"dns_check_timeout",
_("DNS Unavailability Timeout"),
_(
"Maximum time to wait for example.com to resolve during a DNS health check.",
),
);
configureDnsDuration(o, "2s", dnsOption, bootstrapOption);
o = section.option(
form.Value,
"dns_rewrite_ttl",
_("DNS Rewrite TTL"),
_("Time in seconds for DNS record caching (default: 60)"),
);
o.default = "60";
o.rmempty = false;
o.validate = function (section_id, value) {
if (!value) {
return _("TTL value cannot be empty");
}
const ttl = parseInt(value);
if (isNaN(ttl) || ttl < 0) {
return _("TTL must be a positive number");
}
return true;
};
o = section.option(form.ListValue, "dns_strategy", _("DNS Strategy"));
o.value("prefer_ipv4", _("Prefer IPv4"));
o.value("ipv4_only", _("IPv4 only"));
o.value("prefer_ipv6", _("Prefer IPv6"));
o.value("ipv6_only", _("IPv6 only"));
o.default = "prefer_ipv4";
o.rmempty = false;
o = section.option(
form.Flag,
"dns_detour_enabled",
_("DNS through proxy"),
_("Route main DNS requests through the selected section."),
);
configureDownloadViaProxyFlag(o, "dns_detour_section");
o = section.option(
form.ListValue,
"dns_detour_section",
_("DNS requests through section"),
);
o.depends("dns_detour_enabled", "1");
configureDownloadSectionOption(o, "dns_detour_section", capabilities);
o = section.option(
widgets.DeviceSelect,
"source_network_interfaces",
_("Source Network Interface"),
_("Select the network interface from which the traffic will originate"),
);
o.default = "br-lan";
o.noaliases = true;
o.nobridges = false;
o.noinactive = false;
o.multiple = true;
o.filter = function (section_id, value) {
// Block specific interface names from being selectable
const blocked = ["wan", "phy0-ap0", "phy1-ap0", "pppoe-wan"];
if (blocked.includes(value)) {
return false;
}
// Try to find the device object by its name
const device = this.devices.find((dev) => dev.getName() === value);
// If no device is found, allow the value
if (!device) {
return true;
}
// Check the type of the device
const type = device.getType();
// Consider any Wi-Fi / wireless / wlan device as invalid
const isWireless =
type === "wifi" || type === "wireless" || type.includes("wlan");
// Allow only non-wireless devices
return !isWireless;
};
o = section.option(
form.Flag,
"enable_output_network_interface",
_("Enable Output Network Interface"),
_("You can select Output Network Interface, by default autodetect"),
);
o.default = "0";
o.rmempty = false;
o = section.option(
widgets.DeviceSelect,
"output_network_interface",
_("Output Network Interface"),
_("Select the network interface to which the traffic will originate"),
);
o.noaliases = true;
o.multiple = false;
o.depends("enable_output_network_interface", "1");
o.filter = function (section_id, value) {
// Blocked interface names that should never be selectable
const blockedInterfaces = ["br-lan"];
// Reject immediately if the value matches any blocked interface
if (blockedInterfaces.includes(value)) {
return false;
}
// Reject lan*
if (value.startsWith("lan")) {
return false;
}
// Reject tun*, wg*, vpn*, awg*, oc*
if (
value.startsWith("tun") ||
value.startsWith("wg") ||
value.startsWith("vpn") ||
value.startsWith("awg") ||
value.startsWith("oc")
) {
return false;
}
// Try to find the device object with the given name
const device = this.devices.find((dev) => dev.getName() === value);
// If no device is found, allow the value
if (!device) {
return true;
}
// Get the device type (e.g., "wifi", "ethernet", etc.)
const type = device.getType();
// Reject wireless-related devices
const isWireless =
type === "wifi" || type === "wireless" || type.includes("wlan");
return !isWireless;
};
o = section.option(
form.Flag,
"enable_badwan_interface_monitoring",
_("Interface Monitoring"),
_("Interface monitoring for Bad WAN"),
);
o.default = "0";
o.rmempty = false;
o = section.option(
widgets.NetworkSelect,
"badwan_monitored_interfaces",
_("Monitored Interfaces"),
_("Select the WAN interfaces to be monitored"),
);
o.depends("enable_badwan_interface_monitoring", "1");
o.multiple = true;
o.filter = function (section_id, value) {
// Reject if the value is in the blocked list ['lan', 'loopback']
if (["lan", "loopback"].includes(value)) {
return false;
}
// Reject if the value starts with '@' (means it's an alias/reference)
if (value.startsWith("@")) {
return false;
}
// Otherwise allow it
return true;
};
o = section.option(
form.Value,
"badwan_reload_delay",
_("Interface Monitoring Delay"),
_("Delay in milliseconds before reloading Tachyon after interface UP"),
);
o.depends("enable_badwan_interface_monitoring", "1");
o.default = "2000";
o.rmempty = false;
o.validate = function (section_id, value) {
if (!value) {
return _("Delay value cannot be empty");
}
return true;
};
o = section.option(
form.Flag,
"enable_yacd",
_("Enable YACD"),
`<a href="${main.getClashUIUrl()}" target="_blank">${main.getClashUIUrl()}</a>`,
);
o.default = "0";
o.rmempty = false;
o = section.option(
form.Flag,
"enable_yacd_wan_access",
_("Enable YACD WAN Access"),
_(
"Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall.",
),
);
o.depends("enable_yacd", "1");
o.default = "0";
o.rmempty = false;
o = section.option(
form.Value,
"yacd_secret_key",
_("YACD Secret Key"),
_(
"Secret key for authenticating remote access to YACD when WAN access is enabled.",
),
);
o.depends("enable_yacd_wan_access", "1");
o.rmempty = false;
o = section.option(
form.Flag,
"disable_quic",
_("Disable QUIC"),
_(
"Disable the QUIC protocol to improve compatibility or fix issues with video streaming",
),
);
o.default = "0";
o.rmempty = false;
o = section.option(
form.Flag,
"list_update_enabled",
_("Enable list updates"),
_("Enable automatic updates for remote lists and rule sets"),
);
o.default = "1";
o.rmempty = false;
o = section.option(
form.Value,
"update_interval",
_("List Update Frequency"),
_("Use sing-box duration format like 1d, 12h or 30m"),
);
o.depends("list_update_enabled", "1");
o.placeholder = "1d";
o.default = "1d";
o.rmempty = false;
o.cfgvalue = function (section_id) {
return uci.get(UCI_PACKAGE, section_id, "update_interval") || "1d";
};
o.write = function (section_id, value) {
const normalized = value ? `${value}`.trim() : "";
if (normalized.length) {
uci.set(UCI_PACKAGE, section_id, "update_interval", normalized);
} else {
uci.set(UCI_PACKAGE, section_id, "update_interval", "1d");
}
};
o.validate = function (_section_id, value) {
const normalized = value ? `${value}`.trim() : "";
if (!normalized.length) {
return _("Use sing-box duration format like 1d, 12h or 30m");
}
if (isSingBoxDuration(normalized)) {
return true;
}
return _("Use sing-box duration format like 1d, 12h or 30m");
};
o = section.option(
form.Flag,
"component_update_check_enabled",
_("Automatic component update checks"),
_("Automatically check installed components for new versions"),
);
o.default = "0";
o.rmempty = false;
o = section.option(
form.Value,
"component_update_check_interval",
_("Component update check interval"),
_("Use sing-box duration format like 1d, 12h or 30m"),
);
o.depends("component_update_check_enabled", "1");
o.placeholder = "1d";
o.default = "1d";
o.rmempty = false;
o.cfgvalue = function (section_id) {
return (
uci.get(UCI_PACKAGE, section_id, "component_update_check_interval") ||
"1d"
);
};
o.write = function (section_id, value) {
const normalized = value ? `${value}`.trim() : "";
uci.set(
UCI_PACKAGE,
section_id,
"component_update_check_interval",
normalized.length ? normalized : "1d",
);
};
o.validate = function (_section_id, value) {
const normalized = value ? `${value}`.trim() : "";
if (normalized.length && isSingBoxDuration(normalized)) {
return true;
}
return _("Use sing-box duration format like 1d, 12h or 30m");
};
o = section.option(
form.Value,
"latency_test_url",
_("Latency test URL"),
_(
"Default address for checking server availability and latency. URLTest uses its own address.",
),
);
latencyTestUrlChoices().forEach((value) => o.value(value));
o.default =
main.DEFAULT_LATENCY_TEST_URL || "https://www.gstatic.com/generate_204";
o.rmempty = false;
o.validate = function (_section_id, value) {
return validateLatencyTestUrl(value);
};
o = section.option(
form.Flag,
"download_lists_via_proxy",
_("Download lists through a section"),
_("Download remote lists and rule sets via the selected section"),
);
configureDownloadViaProxyFlag(o, "download_lists_via_proxy_section");
o = section.option(
form.ListValue,
"download_lists_via_proxy_section",
_("Download lists through"),
);
o.depends("download_lists_via_proxy", "1");
configureDownloadSectionOption(
o,
"download_lists_via_proxy_section",
capabilities,
);
o = section.option(
form.Flag,
"download_components_via_proxy",
_("Download components through a section"),
_("Download component packages via the selected section"),
);
configureDownloadViaProxyFlag(o, "download_components_via_proxy_section");
o = section.option(
form.ListValue,
"download_components_via_proxy_section",
_("Download components through"),
);
o.depends("download_components_via_proxy", "1");
configureDownloadSectionOption(
o,
"download_components_via_proxy_section",
capabilities,
);
o = section.option(
form.Flag,
"dont_touch_dhcp",
_("Dont Touch My DHCP!"),
_("Tachyon will not modify your DHCP configuration"),
);
o.default = "0";
o.rmempty = false;
o = section.option(
form.ListValue,
"config_path",
_("Config File Path"),
_(
"Select path for sing-box config file. Change this ONLY if you know what you are doing",
),
);
o.value("/etc/sing-box/config.json", "Flash (/etc/sing-box/config.json)");
o.value("/tmp/sing-box/config.json", "RAM (/tmp/sing-box/config.json)");
o.default = "/etc/sing-box/config.json";
o.rmempty = false;
o = section.option(
form.Value,
"cache_path",
_("Cache File Path"),
_(
"Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing",
),
);
o.value("/tmp/sing-box/cache.db", "RAM (/tmp/sing-box/cache.db)");
o.value(
"/usr/share/sing-box/cache.db",
"Flash (/usr/share/sing-box/cache.db)",
);
o.default = "/tmp/sing-box/cache.db";
o.rmempty = false;
o.validate = function (section_id, value) {
if (!value) {
return _("Cache file path cannot be empty");
}
if (!value.startsWith("/")) {
return _("Path must be absolute (start with /)");
}
if (!value.endsWith("cache.db")) {
return _("Path must end with cache.db");
}
const parts = value.split("/").filter(Boolean);
if (parts.length < 2) {
return _("Path must contain at least one directory (like /tmp/cache.db)");
}
return true;
};
o = section.option(
form.ListValue,
"log_level",
_("Log Level"),
_("Select the log level for sing-box"),
);
o.value("trace", "Trace");
o.value("debug", "Debug");
o.value("info", "Info");
o.value("warn", "Warn");
o.value("error", "Error");
o.value("fatal", "Fatal");
o.value("panic", "Panic");
o.default = "warn";
o.rmempty = false;
o = section.option(
form.Flag,
"exclude_ntp",
_("Exclude NTP"),
_(
"Exclude NTP protocol traffic from the tunnel to prevent it from being routed through the proxy or VPN",
),
);
o.default = "0";
o.rmempty = false;
}
function createTelegramContent(section) {
let o = section.option(
form.Flag,
"enabled",
_("Enable Telegram Bot"),
_("Enables the background daemon that polls Telegram for commands and sends notifications."),
);
o.default = "0";
o.rmempty = false;
o = section.option(
form.Value,
"bot_token",
_("Telegram Bot Token"),
_("Enter the API token obtained from @BotFather."),
);
o.depends("enabled", "1");
o.password = true;
o.rmempty = false;
o.validate = function (section_id, value) {
if (this.section.formvalue(section_id, "enabled") === "1" && !value) {
return _("Token is required when the bot is enabled");
}
return true;
};
o = section.option(
form.Value,
"admin_ids",
_("Administrator Chat IDs"),
_("Comma-separated list of Telegram user IDs authorized to control the bot."),
);
o.depends("enabled", "1");
o.rmempty = false;
o.validate = function (section_id, value) {
if (this.section.formvalue(section_id, "enabled") === "1") {
if (!value) {
return _("At least one Admin Chat ID is required");
}
if (!/^[0-9]+(,[0-9]+)*$/.test(value)) {
return _("Must be a comma-separated list of numeric IDs");
}
}
return true;
};
o = section.option(
form.Value,
"poll_interval",
_("Polling Interval (seconds)"),
_("How often to check Telegram for new messages (default: 5)."),
);
o.depends("enabled", "1");
o.default = "5";
o.rmempty = false;
o.validate = function (section_id, value) {
const val = parseInt(value);
if (isNaN(val) || val < 1) {
return _("Polling interval must be at least 1 second");
}
return true;
};
o = section.option(
form.Flag,
"notify_crash",
_("Notify on Core Crashes"),
_("Send a Telegram notification if sing-box or nftables rules crash and get auto-restored."),
);
o.depends("enabled", "1");
o.default = "1";
o.rmempty = false;
o = section.option(
form.Flag,
"notify_restart",
_("Notify on Service Restarts"),
_("Send a Telegram notification if the Tachyon service is restarted by the watchdog."),
);
o.depends("enabled", "1");
o.default = "1";
o.rmempty = false;
o = section.option(
form.Flag,
"notify_server_switch",
_("Notify on Server Switches"),
_("Send a Telegram notification when a URLTest group switches to a new server."),
);
o.depends("enabled", "1");
o.default = "1";
o.rmempty = false;
o = section.option(
form.Flag,
"notify_subscription",
_("Notify on Subscription Updates"),
_("Send a Telegram notification when proxy subscriptions are successfully updated."),
);
o.depends("enabled", "1");
o.default = "1";
o.rmempty = false;
o = section.option(
form.Flag,
"notify_cert",
_("Notify on Certificate Warnings"),
_("Send a Telegram notification if SSL/TLS certificates are close to expiration."),
);
o.depends("enabled", "1");
o.default = "1";
o.rmempty = false;
o = section.option(
form.Flag,
"notify_dns_leak",
_("Notify on DNS Leaks"),
_("Send a Telegram notification if a potential DNS leak is detected."),
);
o.depends("enabled", "1");
o.default = "1";
o.rmempty = false;
}
const EntryPoint = {
createSettingsContent,
createTelegramContent,
};
return baseclass.extend(EntryPoint);