mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-02 10:03:56 +08:00
🔥 Sync 2026-10-24 23:19:23
This commit is contained in:
1 parent
9886197604
commit
37e1475e87
59 files changed
+5167
-1483
No files matched your search
+4
-4
@@ -1,17 +1,17 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=clashoo
|
||||
PKG_SHORT_SHA:=3f27ef3
|
||||
PKG_COMMIT_DATE:=2026.10.21
|
||||
PKG_SHORT_SHA:=91e85ee
|
||||
PKG_COMMIT_DATE:=2026.10.24
|
||||
|
||||
PKG_VERSION:=$(PKG_COMMIT_DATE)~$(PKG_SHORT_SHA)
|
||||
PKG_RELEASE:=16
|
||||
PKG_RELEASE:=17
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA).tar.gz
|
||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
|
||||
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-clashoo/releases/download/mihomo-src
|
||||
PKG_SOURCE_VERSION:=c8979247b7baf46202891be29e174e3f54173cee
|
||||
PKG_SOURCE_VERSION:=413a02e9b55b94dc3892616f43822e191516e493
|
||||
PKG_HASH:=skip
|
||||
PKG_BUILD_VERSION:=alpha-$(PKG_SHORT_SHA)
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ PKG_SOURCE_URL:=https://github.com/ysurac/glorytun.git
|
||||
PKG_SOURCE_VERSION:=203c5f2223d7d5146d659b51159b2f02c2480d15
|
||||
PKG_NAME:=glorytun-udp
|
||||
PKG_VERSION:=0.3.4
|
||||
PKG_RELEASE:=5
|
||||
PKG_RELEASE:=6
|
||||
PKG_LICENSE:=BSD-2-Clause
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
|
||||
|
||||
+12
-7
@@ -31,12 +31,14 @@ validate_section() {
|
||||
'kxtimeout:string:7d' \
|
||||
'timetolerance:string:10m' \
|
||||
'keepalive:string:25s' \
|
||||
'localip:string' \
|
||||
'remoteip:string' \
|
||||
'dev:string'
|
||||
}
|
||||
|
||||
start_instance() {
|
||||
local enable key host port listener proto mtuauto chacha dev mode
|
||||
local persist kxtimeout timetolerance keepalive
|
||||
local persist kxtimeout timetolerance keepalive localip remoteip
|
||||
local key_dir="/var/run/${PROG_NAME}"
|
||||
local key_file="${key_dir}/${1}.key"
|
||||
|
||||
@@ -106,10 +108,11 @@ start_instance() {
|
||||
|
||||
procd_close_instance
|
||||
|
||||
# kxtimeout, timetolerance and keepalive are not bind arguments: only
|
||||
# the ifup hotplug set them, and a restart that keeps the device raises
|
||||
# no ifup, nor does a change of them alone (procd keeps the instance).
|
||||
# Set them once the tunnel answers; the delay leaves procd the time to
|
||||
# kxtimeout, timetolerance, keepalive and the tunnel addresses are not
|
||||
# bind arguments: only the ifup hotplug set them, and a restart that
|
||||
# keeps the device raises no ifup, nor does a change of them alone
|
||||
# (procd keeps the instance). Set them once the tunnel answers (ifconfig
|
||||
# leaves unchanged addresses alone); the delay leaves procd the time to
|
||||
# replace an instance it restarts. fd 1000 is procd.sh's service lock:
|
||||
# kept open, it would block every other glorytun-udp init call meanwhile
|
||||
(
|
||||
@@ -120,6 +123,8 @@ start_instance() {
|
||||
[ "$count" -gt 10 ] && exit 0
|
||||
sleep 1
|
||||
done
|
||||
[ -n "$localip" ] && [ -n "$remoteip" ] &&
|
||||
ifconfig "$dev" "$localip" pointopoint "$remoteip" up
|
||||
${PROG} set dev "$dev" kxtimeout "$kxtimeout" timetolerance "$timetolerance" keepalive "$keepalive"
|
||||
) </dev/null >/dev/null 2>&1 1000>&- &
|
||||
|
||||
@@ -153,8 +158,8 @@ start_service() {
|
||||
|
||||
# No reload_service: stop;start there restarted the tunnel on every network
|
||||
# config change. The default (start) lets procd restart only an instance whose
|
||||
# command or key file changed; kxtimeout, timetolerance and keepalive are
|
||||
# applied by start_instance either way.
|
||||
# command or key file changed; kxtimeout, timetolerance, keepalive and the
|
||||
# tunnel addresses are applied by start_instance either way.
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger glorytun-udp network
|
||||
}
|
||||
+2
-2
@@ -6,8 +6,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=gowebdav
|
||||
PKG_VERSION:=5.17.1
|
||||
PKG_RELEASE:=26
|
||||
PKG_VERSION:=5.17.2
|
||||
PKG_RELEASE:=27
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/hacdias/webdav/tar.gz/v$(PKG_VERSION)?
|
||||
|
||||
+2
-2
@@ -6,8 +6,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=headscale
|
||||
PKG_VERSION:=0.29.5
|
||||
PKG_RELEASE:=10
|
||||
PKG_VERSION:=0.29.6
|
||||
PKG_RELEASE:=11
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/juanfont/headscale/tar.gz/v$(PKG_VERSION)?
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
"luci-app-dsvpn": {
|
||||
"description": "Grant access to DSVPN",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci-rpc": [ "getHostHints" ]
|
||||
},
|
||||
"uci": [ "dsvpn" ]
|
||||
},
|
||||
"write": {
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
"luci-app-glorytun-tcp": {
|
||||
"description": "Grant access to glorytun TCP",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci-rpc": [ "getHostHints" ]
|
||||
},
|
||||
"uci": [ "glorytun" ]
|
||||
},
|
||||
"write": {
|
||||
|
||||
@@ -8,13 +8,6 @@
|
||||
|
||||
var callHostHints;
|
||||
|
||||
var callServiceList = rpc.declare({
|
||||
object: 'service',
|
||||
method: 'list',
|
||||
params: [ 'name' ],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
return L.view.extend({
|
||||
callHostHints: rpc.declare({
|
||||
object: 'luci-rpc',
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
"luci-app-glorytun-udp": {
|
||||
"description": "Grant access to glorytun UDP",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci-rpc": [ "getHostHints" ]
|
||||
},
|
||||
"uci": [ "glorytun-udp" ]
|
||||
},
|
||||
"write": {
|
||||
|
||||
@@ -63,17 +63,28 @@ config_account() {
|
||||
}
|
||||
|
||||
start_service() {
|
||||
local msmtp_tmp msmtp_written=0
|
||||
# The file holds the SMTP password: created 0600 next to the target so
|
||||
# the mv is a rename, never readable by others even for a moment
|
||||
msmtp_tmp="$(umask 077; mktemp /etc/msmtprc.XXXXXX)" || return 1
|
||||
local msmtp_tmp msmtp_new msmtp_written=0
|
||||
# The file holds the SMTP password: always created 0600. Built in tmpfs
|
||||
# and only written to flash when it changed, every start and reload
|
||||
# rewrote /etc/msmtprc
|
||||
msmtp_tmp="$(umask 077; mktemp /tmp/msmtprc.XXXXXX)" || return 1
|
||||
config_load mail
|
||||
config_foreach config_account smtp
|
||||
if [ "$msmtp_written" = "1" ]; then
|
||||
mv "$msmtp_tmp" /etc/msmtprc
|
||||
else
|
||||
rm -f "$msmtp_tmp"
|
||||
if [ "$msmtp_written" = "1" ] && ! cmp -s "$msmtp_tmp" /etc/msmtprc; then
|
||||
# next to the target so the mv is a rename, never readable by
|
||||
# others even for a moment; keeps the owner and mode of the old
|
||||
# file, but never its bits for others
|
||||
msmtp_new="$(umask 077; mktemp /etc/msmtprc.XXXXXX)" &&
|
||||
cat "$msmtp_tmp" > "$msmtp_new" && {
|
||||
[ -f /etc/msmtprc ] && {
|
||||
chown "$(stat -c %u:%g /etc/msmtprc)" "$msmtp_new"
|
||||
chmod "$(stat -c %a /etc/msmtprc)" "$msmtp_new"
|
||||
chmod o-rwx "$msmtp_new"
|
||||
}
|
||||
mv "$msmtp_new" /etc/msmtprc
|
||||
} || rm -f "$msmtp_new"
|
||||
fi
|
||||
rm -f "$msmtp_tmp"
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
"luci-app-mlvpn": {
|
||||
"description": "Grant UCI access for luci-app-mlvpn",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci-rpc": [ "getHostHints" ]
|
||||
},
|
||||
"uci": [ "mlvpn" ]
|
||||
},
|
||||
"write": {
|
||||
|
||||
@@ -17,6 +17,17 @@ var callSystemBoard = rpc.declare({
|
||||
});
|
||||
|
||||
|
||||
// 040-multipath-weight's reading of network.<iface>.multipath_weight:
|
||||
// not a number is unset (100), 0 is 1, over 256 is 256
|
||||
function normalizeWeightValue(value) {
|
||||
if (value == null || value === '')
|
||||
return value;
|
||||
if (!/^[0-9]+$/.test(value))
|
||||
return null;
|
||||
value = +value;
|
||||
return String(value < 1 ? 1 : (value > 256 ? 256 : value));
|
||||
}
|
||||
|
||||
return L.view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
@@ -400,6 +411,13 @@ return L.view.extend({
|
||||
// none. Unset means 100 for every reader (omr-weight-sync, mqvpn).
|
||||
o.placeholder = "100";
|
||||
o.rmempty = true;
|
||||
// A weight stored out of range (0, 300) failed the datatype and
|
||||
// blocked every save of this page: shown as 040-multipath-weight reads
|
||||
// it. A save that doesn't change the field leaves a number unwritten
|
||||
// and drops what is not one
|
||||
o.cfgvalue = function(section_id) {
|
||||
return normalizeWeightValue(uci.get('network', section_id, 'multipath_weight'));
|
||||
};
|
||||
|
||||
return m.render();
|
||||
}
|
||||
|
||||
@@ -185,6 +185,19 @@ return L.view.extend({
|
||||
// multipath_weight='100' on each WAN that had none (unset is 100)
|
||||
o.placeholder = '100';
|
||||
o.rmempty = true;
|
||||
// A weight stored out of range (0, 300) failed the datatype
|
||||
// and blocked every save: shown as 040-multipath-weight reads
|
||||
// it (not a number: unset, 0: 1, over 256: 256). A save that
|
||||
// doesn't change the field leaves a number unwritten
|
||||
o.cfgvalue = function(section_id) {
|
||||
var value = uci.get('network', section_id, 'multipath_weight');
|
||||
if (value == null || value === '')
|
||||
return value;
|
||||
if (!/^[0-9]+$/.test(value))
|
||||
return null;
|
||||
value = +value;
|
||||
return String(value < 1 ? 1 : (value > 256 ? 256 : value));
|
||||
};
|
||||
}
|
||||
|
||||
return m.render();
|
||||
|
||||
@@ -612,6 +612,52 @@ local function valid_name(v)
|
||||
return v:match("^[%w_]+$") ~= nil
|
||||
end
|
||||
|
||||
-- set_settings / set_interface_settings committed without applying: nothing
|
||||
-- changed until the next reload, and a second master was dropped by it (the
|
||||
-- init script keeps the first one in config order). As setmptcpconfig of
|
||||
-- the openmptcprouter object does: the other masters are demoted here, and
|
||||
-- mptcp reloads in the background.
|
||||
local function mptcp_reload(device)
|
||||
local cmd = "/etc/init.d/mptcp reload"
|
||||
-- embedded bare inside the sh -c single quotes
|
||||
if device and device:match("^[%w_%.%-]+$") then cmd = cmd .. " " .. device end
|
||||
os.execute("sh -c '(sleep 1; " .. cmd .. ") >/dev/null 2>/dev/null &'")
|
||||
end
|
||||
|
||||
local function set_multipath(iface, mp)
|
||||
local previous = uci:get("network", iface, "multipath") or ""
|
||||
if previous == "" then
|
||||
-- /etc/init.d/mptcp falls back to the openmptcprouter copy
|
||||
previous = uci:get("openmptcprouter", iface, "multipath") or ""
|
||||
end
|
||||
local demoted = 0
|
||||
if mp == "master" then
|
||||
uci:foreach("network", "interface", function(s)
|
||||
if s[".name"] ~= iface and s["multipath"] == "master" then
|
||||
uci:set("network", s[".name"], "multipath", "on")
|
||||
if uci:get("openmptcprouter", s[".name"]) ~= nil then
|
||||
uci:set("openmptcprouter", s[".name"], "multipath", "on")
|
||||
end
|
||||
demoted = demoted + 1
|
||||
end
|
||||
end)
|
||||
end
|
||||
uci:set("network", iface, "multipath", mp)
|
||||
-- what omr-tracker puts back on a WAN it brings up again
|
||||
if uci:get("openmptcprouter", iface) == nil then
|
||||
uci:set("openmptcprouter", iface, "interface")
|
||||
end
|
||||
uci:set("openmptcprouter", iface, "multipath", mp)
|
||||
uci:save("openmptcprouter")
|
||||
uci:commit("openmptcprouter")
|
||||
if previous == mp and demoted == 0 then return nil end
|
||||
-- master changes need the unscoped pass, which alone enforces one master
|
||||
if demoted > 0 or (previous == "master") ~= (mp == "master") then
|
||||
return ""
|
||||
end
|
||||
return get_device(iface)
|
||||
end
|
||||
|
||||
local setting_checks = {
|
||||
mptcp_checksum = function(v) return v == "0" or v == "1" end,
|
||||
mptcp_debug = function(v) return v == "0" or v == "1" end,
|
||||
@@ -751,18 +797,22 @@ local methods = {
|
||||
end
|
||||
end
|
||||
end
|
||||
local changed = false
|
||||
for _, field in ipairs(string_fields) do
|
||||
if args[field] ~= nil and args[field] ~= "" then
|
||||
changed = changed or uci:get("network", "globals", field) ~= args[field]
|
||||
uci:set("network", "globals", field, args[field])
|
||||
end
|
||||
end
|
||||
for _, field in ipairs(list_fields) do
|
||||
if args[field] ~= nil and type(args[field]) == "table" then
|
||||
changed = true
|
||||
uci:set_list("network", "globals", field, args[field])
|
||||
end
|
||||
end
|
||||
uci:save("network")
|
||||
uci:commit("network")
|
||||
if changed then mptcp_reload() end
|
||||
return { result = "ok" }
|
||||
end
|
||||
},
|
||||
@@ -822,14 +872,17 @@ local methods = {
|
||||
not valid_weight(args.multipath_weight) then
|
||||
return { error = "Invalid value for multipath_weight" }
|
||||
end
|
||||
-- the weight needs no reload: the post-tracking hooks apply it
|
||||
local reload
|
||||
if mp ~= "" then
|
||||
uci:set("network", iface, "multipath", mp)
|
||||
reload = set_multipath(iface, mp)
|
||||
end
|
||||
if args.multipath_weight ~= nil and args.multipath_weight ~= "" then
|
||||
uci:set("network", iface, "multipath_weight", args.multipath_weight)
|
||||
end
|
||||
uci:save("network")
|
||||
uci:commit("network")
|
||||
if reload then mptcp_reload(reload) end
|
||||
return { result = "ok" }
|
||||
end
|
||||
},
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-open-box
|
||||
PKG_VERSION:=0.1.356
|
||||
PKG_RELEASE:=44
|
||||
PKG_VERSION:=0.1.359
|
||||
PKG_RELEASE:=45
|
||||
|
||||
PKG_MAINTAINER:=liandu2024
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
|
||||
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall
|
||||
PKG_VERSION:=26.10.22
|
||||
PKG_RELEASE:=347
|
||||
PKG_RELEASE:=348
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@@ -0,0 +1,773 @@
|
||||
/*
|
||||
* Click-to-sort headers and a per-group filter bar for the node list.
|
||||
*
|
||||
* Sort reorders rows inside one group table and calls set_order(), the same
|
||||
* path as drag-and-drop and To Top. Nothing is written until Save & Apply.
|
||||
*
|
||||
* A filter only toggles the pw-row-hidden class. Rows stay in the form, so
|
||||
* Save & Apply still posts every node and the full group.order value.
|
||||
*
|
||||
* Translated strings are read from #pw-nodesort-i18n (the node list template).
|
||||
* The template calls init() once, then enhanceAll() after loadNodeList()
|
||||
* has built the group tables.
|
||||
*
|
||||
* auto_detection_time is a top-level let in the template, not a window
|
||||
* property. It is read by name so rows are not moved while automatic Ping
|
||||
* or TCPing is still writing results by row position.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var COLUMNS = {
|
||||
'pw-remark': 'name',
|
||||
'pw-ping': 'ping',
|
||||
'pw-tcping': 'tcping',
|
||||
'pw-urltest': 'urltest'
|
||||
};
|
||||
/* sub controls drawn inside the Remarks header: [sort key, label key or symbol, title key] */
|
||||
var NAME_EXTRA = [
|
||||
['type', 'type', 'typeHint'],
|
||||
['addr', 'addr', 'addrHint'],
|
||||
['orig', '\u21BA', 'origHint']
|
||||
];
|
||||
|
||||
var state = {
|
||||
app: 'passwall',
|
||||
root: null,
|
||||
text: {},
|
||||
bound: false
|
||||
};
|
||||
var searchTimer = null;
|
||||
|
||||
function T(key) {
|
||||
var value = state.text[key];
|
||||
return (value == null || value === '') ? key : value;
|
||||
}
|
||||
|
||||
function readText(box) {
|
||||
var text = {};
|
||||
var spans = box.querySelectorAll('[data-k]');
|
||||
for (var i = 0; i < spans.length; i++)
|
||||
text[spans[i].getAttribute('data-k')] = spans[i].textContent;
|
||||
return text;
|
||||
}
|
||||
|
||||
var collator = null;
|
||||
function prepareCollator() {
|
||||
var lang = document.documentElement.lang || (navigator.language || '');
|
||||
try {
|
||||
collator = (window.Intl && Intl.Collator) ? new Intl.Collator(lang || undefined, { numeric: true, sensitivity: 'base' }) : null;
|
||||
} catch (e) {
|
||||
collator = null;
|
||||
}
|
||||
}
|
||||
|
||||
function cmpText(a, b) {
|
||||
if (collator)
|
||||
return collator.compare(a, b);
|
||||
return a < b ? -1 : (a > b ? 1 : 0);
|
||||
}
|
||||
|
||||
/* "123 ms", "12.5 ms", "1.2 s" -> milliseconds. Timeout, Error, ---, Test stay null. */
|
||||
function parseLatency(text) {
|
||||
var t = String(text || '').replace(/\s+/g, ' ').trim();
|
||||
var m = /^(\d+(?:[.,]\d+)?)\s*(ms|s)?$/i.exec(t);
|
||||
if (!m)
|
||||
return null;
|
||||
var v = parseFloat(m[1].replace(',', '.'));
|
||||
if (isNaN(v))
|
||||
return null;
|
||||
if (m[2] && /^s$/i.test(m[2]))
|
||||
v *= 1000;
|
||||
return v;
|
||||
}
|
||||
|
||||
function hiddenVal(id, opt) {
|
||||
var el = document.getElementById('cbid.' + state.app + '.' + id + '.' + opt);
|
||||
return el && el.value != null ? String(el.value).trim() : '';
|
||||
}
|
||||
|
||||
function cellsOf(tr) {
|
||||
var out = [];
|
||||
for (var i = 0; i < tr.children.length; i++) {
|
||||
var tag = tr.children[i].tagName;
|
||||
if (tag === 'TD' || tag === 'TH')
|
||||
out.push(tr.children[i]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
var wordSplit;
|
||||
try {
|
||||
wordSplit = new RegExp('[^\\p{L}\\p{N}]+', 'u');
|
||||
} catch (e) {
|
||||
wordSplit = /[\s.,;:!?|\/\\()\[\]{}<>"'`~@#$%^&*=+_-]+/;
|
||||
}
|
||||
|
||||
function wordsOf(text) {
|
||||
var seen = {};
|
||||
var out = [];
|
||||
var parts = String(text || '').split(wordSplit);
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
var w = parts[i];
|
||||
if (w.length < 2 || /^\d+$/.test(w))
|
||||
continue;
|
||||
var k = w.toLowerCase();
|
||||
if (!seen[k]) {
|
||||
seen[k] = 1;
|
||||
out.push(w);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function infoOf(table) {
|
||||
var prefix = 'cbi-' + state.app + '-nodes-';
|
||||
var id = table && table.id ? table.id : '';
|
||||
if (id.indexOf(prefix) !== 0 || id.slice(-6) !== '-table')
|
||||
return null;
|
||||
return { group: id.slice(prefix.length, -6) };
|
||||
}
|
||||
|
||||
function rowId(tr) {
|
||||
var prefix = 'cbi-' + state.app + '-';
|
||||
return tr.id.indexOf(prefix) === 0 ? tr.id.substring(prefix.length) : tr.id;
|
||||
}
|
||||
|
||||
function bodyOf(table) {
|
||||
return table.querySelector('tbody') || table;
|
||||
}
|
||||
|
||||
function dataRows(table) {
|
||||
var body = bodyOf(table);
|
||||
var out = [];
|
||||
for (var i = 0; i < body.children.length; i++) {
|
||||
var tr = body.children[i];
|
||||
if (tr.tagName === 'TR' && tr.classList.contains('cbi-section-table-row') && !tr.classList.contains('placeholder'))
|
||||
out.push(tr);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function colIndexMap(table) {
|
||||
var rows = dataRows(table);
|
||||
var map = {};
|
||||
if (!rows.length)
|
||||
return map;
|
||||
var cells = cellsOf(rows[0]);
|
||||
for (var i = 0; i < cells.length; i++) {
|
||||
for (var cls in COLUMNS) {
|
||||
if (cells[i].classList.contains(cls))
|
||||
map[COLUMNS[cls]] = i;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function typeOf(tr, cols) {
|
||||
var cell = cellsOf(tr)[cols.name];
|
||||
var text = cell ? cell.textContent.replace(/\s+/g, ' ').trim() : '';
|
||||
var i = text.indexOf('\uFF1A');
|
||||
if (i < 0)
|
||||
i = text.indexOf(':');
|
||||
return (i < 0 ? '' : text.slice(0, i)).replace(/\s+Realm$/, '').trim();
|
||||
}
|
||||
|
||||
function sortKey(tr, key, cols) {
|
||||
var id = rowId(tr);
|
||||
var cell;
|
||||
if (key === 'name')
|
||||
return hiddenVal(id, 'remarks') || ((cellsOf(tr)[cols.name] || {}).textContent || '').trim();
|
||||
if (key === 'type')
|
||||
return typeOf(tr, cols) + '\u0000' + hiddenVal(id, 'remarks');
|
||||
if (key === 'addr') {
|
||||
var address = hiddenVal(id, 'address');
|
||||
if (!address)
|
||||
return null;
|
||||
return address + '\u0000' + ('00000' + hiddenVal(id, 'port')).slice(-5);
|
||||
}
|
||||
cell = cellsOf(tr)[cols[key]];
|
||||
return cell ? parseLatency(cell.textContent) : null;
|
||||
}
|
||||
|
||||
function detectionMode() {
|
||||
try {
|
||||
if (typeof auto_detection_time === 'undefined')
|
||||
return null;
|
||||
return auto_detection_time;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/* { inflight: requests still running, left: cells still waiting for a result } */
|
||||
function pingBusy() {
|
||||
var mode = detectionMode();
|
||||
var inflight = 0;
|
||||
var left = 0;
|
||||
if (mode !== 'icmp' && mode !== 'tcping')
|
||||
return { inflight: 0, left: 0 };
|
||||
var list = window.ajax && window.ajax.list;
|
||||
if (list && list.length) {
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
if (list[i].readyState > 0 && list[i].readyState < 4)
|
||||
inflight++;
|
||||
}
|
||||
}
|
||||
if (!state.root)
|
||||
return { inflight: inflight, left: 0 };
|
||||
var spans = state.root.getElementsByClassName(mode === 'tcping' ? 'tcping_value' : 'ping_value');
|
||||
for (var s = 0; s < spans.length; s++) {
|
||||
var id = spans[s].getAttribute('cbiid');
|
||||
if (!id || spans[s].textContent.trim() !== '---')
|
||||
continue;
|
||||
var full = null;
|
||||
if (typeof window.get_address_full === 'function')
|
||||
full = window.get_address_full(id);
|
||||
if (!full)
|
||||
full = { address: hiddenVal(id, 'address'), port: hiddenVal(id, 'port') };
|
||||
if (full.address && (mode === 'icmp' || full.port))
|
||||
left++;
|
||||
}
|
||||
return { inflight: inflight, left: left };
|
||||
}
|
||||
|
||||
function note(table, text) {
|
||||
var id = 'pw-note-' + table.id;
|
||||
var el = document.getElementById(id);
|
||||
if (!text) {
|
||||
if (el && el.parentNode)
|
||||
el.parentNode.removeChild(el);
|
||||
return;
|
||||
}
|
||||
if (!el) {
|
||||
el = document.createElement('div');
|
||||
el.id = id;
|
||||
el.className = 'pw-sort-note';
|
||||
table.parentNode.insertBefore(el, table);
|
||||
}
|
||||
el.textContent = text;
|
||||
}
|
||||
|
||||
function setIndicator(table, th, sub, key, dir) {
|
||||
var arrows = table.querySelectorAll('.pw-sort-arrow');
|
||||
var subs = table.querySelectorAll('.pw-sort-sub');
|
||||
var headers = table.querySelectorAll('th[data-pw-sort]');
|
||||
var i;
|
||||
for (i = 0; i < arrows.length; i++)
|
||||
arrows[i].textContent = '';
|
||||
for (i = 0; i < subs.length; i++)
|
||||
subs[i].classList.remove('pw-sort-active');
|
||||
for (i = 0; i < headers.length; i++) {
|
||||
if (headers[i] !== th) {
|
||||
delete headers[i].dataset.pwDir;
|
||||
delete headers[i].dataset.pwKey;
|
||||
}
|
||||
}
|
||||
var arrow = th.querySelector('.pw-sort-arrow');
|
||||
if (arrow && key !== 'orig' && !sub)
|
||||
arrow.textContent = dir === 'desc' ? '\u25BC' : '\u25B2';
|
||||
if (sub && key !== 'orig')
|
||||
sub.classList.add('pw-sort-active');
|
||||
}
|
||||
|
||||
function applyOrder(table, info, rows) {
|
||||
var body = bodyOf(table);
|
||||
var frag = document.createDocumentFragment();
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
rows[i].classList.remove('cbi-rowstyle-1', 'cbi-rowstyle-2');
|
||||
rows[i].classList.add('cbi-rowstyle-' + (i % 2 + 1));
|
||||
frag.appendChild(rows[i]);
|
||||
}
|
||||
body.appendChild(frag);
|
||||
if (typeof window.set_order === 'function')
|
||||
window.set_order(info.group);
|
||||
}
|
||||
|
||||
function doSort(table, th, sub, key) {
|
||||
var info = infoOf(table);
|
||||
var rows = dataRows(table);
|
||||
var dir = 'asc';
|
||||
if (!info || rows.length < 2)
|
||||
return;
|
||||
if (key === 'orig') {
|
||||
var pos = {};
|
||||
var orig = table.__pwOrig || [];
|
||||
for (var n = 0; n < orig.length; n++)
|
||||
pos[orig[n]] = n;
|
||||
rows.sort(function (a, b) {
|
||||
var pa = pos[rowId(a)];
|
||||
var pb = pos[rowId(b)];
|
||||
return (pa == null ? 1e9 : pa) - (pb == null ? 1e9 : pb);
|
||||
});
|
||||
delete th.dataset.pwDir;
|
||||
delete th.dataset.pwKey;
|
||||
} else {
|
||||
dir = (th.dataset.pwKey === key && th.dataset.pwDir === 'asc') ? 'desc' : 'asc';
|
||||
th.dataset.pwKey = key;
|
||||
th.dataset.pwDir = dir;
|
||||
var cols = colIndexMap(table);
|
||||
var sign = dir === 'desc' ? -1 : 1;
|
||||
var list = [];
|
||||
for (var i = 0; i < rows.length; i++)
|
||||
list.push({ tr: rows[i], i: i, k: sortKey(rows[i], key, cols) });
|
||||
list.sort(function (a, b) {
|
||||
var emptyA = (a.k === null || a.k === '');
|
||||
var emptyB = (b.k === null || b.k === '');
|
||||
if (emptyA || emptyB)
|
||||
return emptyA === emptyB ? a.i - b.i : (emptyA ? 1 : -1);
|
||||
var cmp = (typeof a.k === 'number' && typeof b.k === 'number') ? a.k - b.k : cmpText(String(a.k), String(b.k));
|
||||
return cmp !== 0 ? sign * cmp : a.i - b.i;
|
||||
});
|
||||
rows = [];
|
||||
for (var j = 0; j < list.length; j++)
|
||||
rows.push(list[j].tr);
|
||||
}
|
||||
applyOrder(table, info, rows);
|
||||
refreshFilter(table);
|
||||
setIndicator(table, th, sub, key, dir);
|
||||
note(table, key === 'orig' ? T('restored') : T('changed'));
|
||||
}
|
||||
|
||||
function requestSort(table, th, sub, key) {
|
||||
if (table.__pwWait) {
|
||||
clearInterval(table.__pwWait);
|
||||
table.__pwWait = null;
|
||||
}
|
||||
var st = pingBusy();
|
||||
if (!st.inflight && !st.left) {
|
||||
doSort(table, th, sub, key);
|
||||
return;
|
||||
}
|
||||
var started = Date.now();
|
||||
var idle = 0;
|
||||
note(table, T('wait').replace('%d', st.left || st.inflight));
|
||||
table.__pwWait = setInterval(function () {
|
||||
st = pingBusy();
|
||||
idle = st.inflight ? 0 : idle + 1;
|
||||
if ((!st.inflight && !st.left) || idle >= 6) {
|
||||
clearInterval(table.__pwWait);
|
||||
table.__pwWait = null;
|
||||
if (document.body.contains(table))
|
||||
doSort(table, th, sub, key);
|
||||
return;
|
||||
}
|
||||
if (Date.now() - started > 120000) {
|
||||
clearInterval(table.__pwWait);
|
||||
table.__pwWait = null;
|
||||
note(table, T('waitFail'));
|
||||
return;
|
||||
}
|
||||
note(table, T('wait').replace('%d', st.left || st.inflight));
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function enhance(table) {
|
||||
var info = infoOf(table);
|
||||
if (!info || table.__pwReady)
|
||||
return;
|
||||
var titles = table.querySelector('tr.cbi-section-table-titles');
|
||||
var rows = dataRows(table);
|
||||
if (!titles || !rows.length)
|
||||
return;
|
||||
var cols = colIndexMap(table);
|
||||
var headers = cellsOf(titles);
|
||||
table.__pwReady = true;
|
||||
table.__pwOrig = [];
|
||||
for (var r = 0; r < rows.length; r++)
|
||||
table.__pwOrig.push(rowId(rows[r]));
|
||||
for (var key in cols) {
|
||||
if (!Object.prototype.hasOwnProperty.call(cols, key))
|
||||
continue;
|
||||
var th = headers[cols[key]];
|
||||
if (!th || th.classList.contains('cbi-section-actions'))
|
||||
continue;
|
||||
th.setAttribute('data-pw-sort', key);
|
||||
th.title = T('hint');
|
||||
var arrow = document.createElement('span');
|
||||
arrow.className = 'pw-sort-arrow';
|
||||
th.appendChild(arrow);
|
||||
if (key !== 'name')
|
||||
continue;
|
||||
for (var s = 0; s < NAME_EXTRA.length; s++) {
|
||||
var spec = NAME_EXTRA[s];
|
||||
var sp = document.createElement('span');
|
||||
sp.className = 'pw-sort-sub';
|
||||
sp.setAttribute('data-pw-sort', spec[0]);
|
||||
sp.textContent = T(spec[1]);
|
||||
sp.title = T(spec[2]);
|
||||
th.appendChild(sp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function rowVisible(el) {
|
||||
var row = el && el.closest ? el.closest('tr') : null;
|
||||
return !row || !row.classList.contains('pw-row-hidden');
|
||||
}
|
||||
|
||||
function filterDefs(rows, cols) {
|
||||
return [
|
||||
{
|
||||
key: 'type',
|
||||
label: T('type'),
|
||||
multi: false,
|
||||
get: function (tr) { return typeOf(tr, cols); }
|
||||
},
|
||||
{
|
||||
key: 'word',
|
||||
label: T('fWord'),
|
||||
multi: true,
|
||||
minCount: 2,
|
||||
max: 40,
|
||||
get: function (tr) { return wordsOf(hiddenVal(rowId(tr), 'remarks')); }
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function searchText(tr, cols) {
|
||||
var id = rowId(tr);
|
||||
return [
|
||||
typeOf(tr, cols),
|
||||
hiddenVal(id, 'remarks'),
|
||||
hiddenVal(id, 'address') + ':' + hiddenVal(id, 'port')
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
function collect(table) {
|
||||
var rows = dataRows(table);
|
||||
var cols = colIndexMap(table);
|
||||
var defs = rows.length >= 2 ? filterDefs(rows, cols) : [];
|
||||
var shown = [];
|
||||
for (var d = 0; d < defs.length; d++) {
|
||||
var def = defs[d];
|
||||
var counts = {};
|
||||
var labels = {};
|
||||
var vals = [];
|
||||
for (var r = 0; r < rows.length; r++) {
|
||||
var raw = def.get(rows[r]);
|
||||
var list = def.multi ? (raw || []) : [raw == null ? '' : String(raw)];
|
||||
var keys = [];
|
||||
for (var n = 0; n < list.length; n++) {
|
||||
var item = list[n];
|
||||
var k = def.multi ? String(item).toLowerCase() : String(item);
|
||||
counts[k] = (counts[k] || 0) + 1;
|
||||
if (!labels[k])
|
||||
labels[k] = {};
|
||||
labels[k][item] = (labels[k][item] || 0) + 1;
|
||||
keys.push(k);
|
||||
}
|
||||
vals.push(keys);
|
||||
}
|
||||
var opts = [];
|
||||
for (var k in counts) {
|
||||
if (!Object.prototype.hasOwnProperty.call(counts, k))
|
||||
continue;
|
||||
if (def.multi && (counts[k] < (def.minCount || 1) || counts[k] >= rows.length))
|
||||
continue;
|
||||
var best = '';
|
||||
var bestN = -1;
|
||||
for (var label in labels[k]) {
|
||||
if (labels[k][label] > bestN) {
|
||||
bestN = labels[k][label];
|
||||
best = label;
|
||||
}
|
||||
}
|
||||
opts.push({ v: k, label: best === '' ? T('empty') : best, n: counts[k] });
|
||||
}
|
||||
opts.sort(function (a, b) {
|
||||
if (def.multi && a.n !== b.n)
|
||||
return b.n - a.n;
|
||||
return cmpText(a.label, b.label);
|
||||
});
|
||||
if (def.max && opts.length > def.max)
|
||||
opts = opts.slice(0, def.max);
|
||||
var enough = def.multi ? opts.length >= 1 : opts.length >= 2;
|
||||
if (!enough)
|
||||
continue;
|
||||
def.vals = vals;
|
||||
def.opts = opts;
|
||||
shown.push(def);
|
||||
}
|
||||
var search = rows.length >= 2;
|
||||
var sig = String(rows.length) + (search ? 's' : '');
|
||||
for (var i = 0; i < shown.length; i++) {
|
||||
sig += '|' + shown[i].key + '=';
|
||||
for (var o = 0; o < shown[i].opts.length; o++)
|
||||
sig += shown[i].opts[o].v + ':' + shown[i].opts[o].n + ',';
|
||||
}
|
||||
return { rows: rows, cols: cols, defs: shown, search: search, sig: sig };
|
||||
}
|
||||
|
||||
function barId(table) {
|
||||
return 'pw-filter-' + table.id;
|
||||
}
|
||||
|
||||
function buildBar(table, collected) {
|
||||
var st = table.__pwF || (table.__pwF = { sel: {}, q: '' });
|
||||
var id = barId(table);
|
||||
var bar = document.getElementById(id);
|
||||
if (!bar) {
|
||||
bar = document.createElement('div');
|
||||
bar.id = id;
|
||||
bar.className = 'pw-filter';
|
||||
bar.title = T('filterHint');
|
||||
table.parentNode.insertBefore(bar, table);
|
||||
bar.__pwTable = table;
|
||||
}
|
||||
while (bar.firstChild)
|
||||
bar.removeChild(bar.firstChild);
|
||||
var head = document.createElement('span');
|
||||
head.textContent = T('filter');
|
||||
bar.appendChild(head);
|
||||
var keep = {};
|
||||
for (var d = 0; d < collected.defs.length; d++) {
|
||||
var def = collected.defs[d];
|
||||
var lab = document.createElement('label');
|
||||
var sel = document.createElement('select');
|
||||
lab.appendChild(document.createTextNode(def.label));
|
||||
sel.className = 'cbi-input-select';
|
||||
sel.setAttribute('data-pw-filter', def.key);
|
||||
var all = document.createElement('option');
|
||||
all.value = '';
|
||||
all.textContent = T('all') + ' (' + collected.rows.length + ')';
|
||||
sel.appendChild(all);
|
||||
for (var o = 0; o < def.opts.length; o++) {
|
||||
var op = document.createElement('option');
|
||||
op.value = 'v:' + def.opts[o].v;
|
||||
op.textContent = def.opts[o].label + ' (' + def.opts[o].n + ')';
|
||||
sel.appendChild(op);
|
||||
}
|
||||
var cur = st.sel[def.key] || '';
|
||||
var known = false;
|
||||
if (cur) {
|
||||
for (var c = 0; c < def.opts.length; c++) {
|
||||
if (('v:' + def.opts[c].v) === cur) {
|
||||
known = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!known)
|
||||
cur = '';
|
||||
sel.value = cur;
|
||||
if (cur)
|
||||
keep[def.key] = cur;
|
||||
lab.appendChild(sel);
|
||||
bar.appendChild(lab);
|
||||
}
|
||||
st.sel = keep;
|
||||
if (collected.search) {
|
||||
var input = document.createElement('input');
|
||||
input.type = 'search';
|
||||
input.className = 'cbi-input-text';
|
||||
input.placeholder = T('search');
|
||||
input.title = T('searchHint');
|
||||
input.autocomplete = 'off';
|
||||
input.setAttribute('data-pw-search', '1');
|
||||
input.value = st.q;
|
||||
bar.appendChild(input);
|
||||
} else {
|
||||
st.q = '';
|
||||
}
|
||||
var count = document.createElement('span');
|
||||
count.className = 'pw-filter-count';
|
||||
bar.appendChild(count);
|
||||
var reset = document.createElement('button');
|
||||
reset.type = 'button';
|
||||
reset.className = 'btn cbi-button';
|
||||
reset.setAttribute('data-pw-reset', '1');
|
||||
reset.textContent = T('reset');
|
||||
reset.title = T('resetHint');
|
||||
bar.appendChild(reset);
|
||||
return bar;
|
||||
}
|
||||
|
||||
function listHas(list, value) {
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
if (list[i] === value)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function applyFilter(table, collected, bar) {
|
||||
var st = table.__pwF || { sel: {}, q: '' };
|
||||
var q = String(st.q || '').toLowerCase().trim();
|
||||
var active = !!q;
|
||||
var shown = 0;
|
||||
var d;
|
||||
for (d = 0; d < collected.defs.length; d++) {
|
||||
if (st.sel[collected.defs[d].key])
|
||||
active = true;
|
||||
}
|
||||
for (var i = 0; i < collected.rows.length; i++) {
|
||||
var tr = collected.rows[i];
|
||||
var ok = true;
|
||||
for (d = 0; d < collected.defs.length; d++) {
|
||||
var sel = st.sel[collected.defs[d].key];
|
||||
if (ok && sel && !listHas(collected.defs[d].vals[i], sel.slice(2)))
|
||||
ok = false;
|
||||
}
|
||||
if (ok && q && searchText(tr, collected.cols).toLowerCase().indexOf(q) < 0)
|
||||
ok = false;
|
||||
if (ok !== !tr.classList.contains('pw-row-hidden'))
|
||||
tr.classList.toggle('pw-row-hidden', !ok);
|
||||
if (ok) {
|
||||
shown++;
|
||||
var want = 'cbi-rowstyle-' + ((shown - 1) % 2 + 1);
|
||||
var other = want === 'cbi-rowstyle-1' ? 'cbi-rowstyle-2' : 'cbi-rowstyle-1';
|
||||
if (!tr.classList.contains(want))
|
||||
tr.classList.add(want);
|
||||
if (tr.classList.contains(other))
|
||||
tr.classList.remove(other);
|
||||
} else {
|
||||
var cb = tr.querySelector('input.nodes_select');
|
||||
if (cb && cb.checked) {
|
||||
cb.checked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof window.update_select_state === 'function')
|
||||
window.update_select_state();
|
||||
var count = bar.querySelector('.pw-filter-count');
|
||||
var txt = T('shown').replace('%s', shown).replace('%t', collected.rows.length);
|
||||
if (count && count.textContent !== txt)
|
||||
count.textContent = txt;
|
||||
if (bar.classList.contains('pw-filter-on') !== active)
|
||||
bar.classList.toggle('pw-filter-on', active);
|
||||
}
|
||||
|
||||
function removeBar(table) {
|
||||
var bar = document.getElementById(barId(table));
|
||||
if (bar && bar.parentNode)
|
||||
bar.parentNode.removeChild(bar);
|
||||
var rows = dataRows(table);
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
if (rows[i].classList.contains('pw-row-hidden'))
|
||||
rows[i].classList.remove('pw-row-hidden');
|
||||
}
|
||||
table.__pwSig = null;
|
||||
}
|
||||
|
||||
function refreshFilter(table) {
|
||||
if (!infoOf(table))
|
||||
return;
|
||||
var collected = collect(table);
|
||||
if (!collected.defs.length && !collected.search) {
|
||||
if (table.__pwSig)
|
||||
removeBar(table);
|
||||
return;
|
||||
}
|
||||
var bar = document.getElementById(barId(table));
|
||||
if (!bar || collected.sig !== table.__pwSig) {
|
||||
bar = buildBar(table, collected);
|
||||
table.__pwSig = collected.sig;
|
||||
}
|
||||
applyFilter(table, collected, bar);
|
||||
}
|
||||
|
||||
function barTable(el) {
|
||||
var bar = el && el.closest ? el.closest('.pw-filter') : null;
|
||||
return bar && bar.__pwTable ? bar.__pwTable : null;
|
||||
}
|
||||
|
||||
function eventTarget(ev) {
|
||||
var t = ev.target;
|
||||
if (t && t.nodeType !== 1)
|
||||
t = t.parentNode;
|
||||
return t;
|
||||
}
|
||||
|
||||
function bind(root) {
|
||||
root.addEventListener('click', function (ev) {
|
||||
var t = eventTarget(ev);
|
||||
if (t && t.getAttribute && t.getAttribute('data-pw-reset')) {
|
||||
var resetTable = barTable(t);
|
||||
if (!resetTable)
|
||||
return;
|
||||
ev.preventDefault();
|
||||
resetTable.__pwF = { sel: {}, q: '' };
|
||||
resetTable.__pwSig = null;
|
||||
refreshFilter(resetTable);
|
||||
return;
|
||||
}
|
||||
var el = t && t.closest ? t.closest('[data-pw-sort]') : null;
|
||||
if (!el || !root.contains(el))
|
||||
return;
|
||||
if (t.closest && t.closest('input, a, button, select'))
|
||||
return;
|
||||
var th = el.closest('th');
|
||||
var table = el.closest('table');
|
||||
if (!th || !table || !infoOf(table))
|
||||
return;
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
requestSort(table, th, el === th ? null : el, el.getAttribute('data-pw-sort'));
|
||||
});
|
||||
root.addEventListener('change', function (ev) {
|
||||
var t = eventTarget(ev);
|
||||
if (!t || !t.getAttribute || !t.getAttribute('data-pw-filter'))
|
||||
return;
|
||||
var table = barTable(t);
|
||||
if (!table)
|
||||
return;
|
||||
(table.__pwF = table.__pwF || { sel: {}, q: '' }).sel[t.getAttribute('data-pw-filter')] = t.value;
|
||||
refreshFilter(table);
|
||||
});
|
||||
root.addEventListener('input', function (ev) {
|
||||
var t = eventTarget(ev);
|
||||
if (!t || !t.getAttribute || !t.getAttribute('data-pw-search'))
|
||||
return;
|
||||
var table = barTable(t);
|
||||
if (!table)
|
||||
return;
|
||||
clearTimeout(searchTimer);
|
||||
searchTimer = setTimeout(function () {
|
||||
(table.__pwF = table.__pwF || { sel: {}, q: '' }).q = t.value;
|
||||
refreshFilter(table);
|
||||
}, 150);
|
||||
});
|
||||
root.addEventListener('keydown', function (ev) {
|
||||
var t = eventTarget(ev);
|
||||
if (ev.key === 'Enter' && t && t.getAttribute && t.getAttribute('data-pw-search'))
|
||||
ev.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
var box = document.getElementById('pw-nodesort-i18n');
|
||||
var root = document.getElementById('node_list');
|
||||
if (!box || !root)
|
||||
return;
|
||||
state.app = box.getAttribute('data-app') || 'passwall';
|
||||
state.text = readText(box);
|
||||
state.root = root;
|
||||
prepareCollator();
|
||||
if (!state.bound) {
|
||||
bind(root);
|
||||
state.bound = true;
|
||||
}
|
||||
}
|
||||
|
||||
function enhanceAll() {
|
||||
if (!state.root)
|
||||
return;
|
||||
var tables = state.root.querySelectorAll('table.cbi-section-table');
|
||||
for (var i = 0; i < tables.length; i++) {
|
||||
try {
|
||||
enhance(tables[i]);
|
||||
refreshFilter(tables[i]);
|
||||
} catch (e) {
|
||||
if (window.console)
|
||||
console.warn('passwall node list sort:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.pwNodeSort = {
|
||||
init: init,
|
||||
enhanceAll: enhanceAll,
|
||||
rowVisible: rowVisible
|
||||
};
|
||||
})();
|
||||
@@ -78,7 +78,7 @@ if has_xray or has_singbox then
|
||||
o.default = 0
|
||||
o.rmempty = false
|
||||
o.description = translate("Generate rule lists such as GFW, China domains, and China IP ranges based on Geo files.") .. "<br><font color='red'>" ..
|
||||
translate("When manually updating with this option enabled, rules will be regenerated from existing Geo files even if no new version is available.") .. "</font>"
|
||||
translate("GeoSite files without GFW rules cannot generate a GFW list (e.g. the original v2fly/Xray files).") .. "</font>"
|
||||
|
||||
o = s:option(Flag, "enable_geoview", translate("Enable Geo Data Parsing"))
|
||||
o.default = 0
|
||||
|
||||
@@ -1692,8 +1692,21 @@ function gen_config(var)
|
||||
local bin = api.finded_com("geoview")
|
||||
if bin then
|
||||
local geo_file = (api.uci_get_c("@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/"):match("^(.*)/") .. "/geosite.dat"
|
||||
if luci.sys.call('"' .. bin .. '" -type geosite -input "' .. geo_file .. '" | grep -q "^GFW$"') == 0 then
|
||||
domain_list = (domain_list == "") and "geosite:gfw" or domain_list .. "\ngeosite:gfw"
|
||||
local gfw_rule
|
||||
if sys.call('"' .. bin .. '" -type geosite -input "' .. geo_file .. '" 2>/dev/null | grep -q "^GFW$"') == 0 then
|
||||
gfw_rule = "geosite:gfw"
|
||||
elseif api.compare_versions(api.get_app_version("geoview"), ">=", "0.2.8") then
|
||||
local output = GEO_VAR.TO_SRS_PATH .. "geosite-gfw.srs"
|
||||
if not fs.access(output) then
|
||||
fs.mkdirr(GEO_VAR.TO_SRS_PATH)
|
||||
local cmd = string.format("%s -type geosite -action convert -input /usr/share/passwall/rules/gfwlist -list '*' -output %s -format srs",
|
||||
api.util.shellquote(bin), api.util.shellquote(output))
|
||||
if api.exec_call(cmd) ~= 0 then fs.unlink(output) end
|
||||
end
|
||||
gfw_rule = fs.access(output) and ("rule-set:local:" .. output) or nil
|
||||
end
|
||||
if gfw_rule then
|
||||
domain_list = (domain_list == "") and gfw_rule or domain_list .. "\n" .. gfw_rule
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -579,11 +579,15 @@ function gen_config_server(node)
|
||||
end
|
||||
end
|
||||
|
||||
if node.protocol == "vmess" or node.protocol == "vless" then
|
||||
if node.protocol == "vless" then
|
||||
settings = {
|
||||
users = users,
|
||||
decryption = (node.decryption and node.decryption ~= "") and node.decryption or "none"
|
||||
}
|
||||
elseif node.protocol == "vmess" then
|
||||
if users then
|
||||
settings = {
|
||||
users = users,
|
||||
decryption = (node.protocol == "vless") and ((node.decryption and node.decryption ~= "") and node.decryption or "none") or nil
|
||||
users = users
|
||||
}
|
||||
end
|
||||
elseif node.protocol == "socks" then
|
||||
@@ -1479,8 +1483,21 @@ function gen_config(var)
|
||||
local bin = api.finded_com("geoview")
|
||||
if bin then
|
||||
local geo_file = (api.uci_get_c("@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/"):match("^(.*)/") .. "/geosite.dat"
|
||||
if luci.sys.call('"' .. bin .. '" -type geosite -input "' .. geo_file .. '" | grep -q "^GFW$"') == 0 then
|
||||
domain_list = (domain_list == "") and "geosite:gfw" or domain_list .. "\ngeosite:gfw"
|
||||
local gfw_rule
|
||||
if sys.call('"' .. bin .. '" -type geosite -input "' .. geo_file .. '" 2>/dev/null | grep -q "^GFW$"') == 0 then
|
||||
gfw_rule = "geosite:gfw"
|
||||
elseif api.compare_versions(api.get_app_version("geoview"), ">=", "0.2.8") then
|
||||
local output = api.CACHE_PATH .. "/geodata/gfwlist.dat"
|
||||
if not fs.access(output) then
|
||||
fs.mkdirr(api.CACHE_PATH .. "/geodata")
|
||||
local cmd = string.format("%s -type geosite -action convert -input /usr/share/passwall/rules/gfwlist -list '*' -output %s -format geosite",
|
||||
api.util.shellquote(bin), api.util.shellquote(output))
|
||||
if api.exec_call(cmd) ~= 0 then fs.unlink(output) end
|
||||
end
|
||||
gfw_rule = fs.access(output) and "ext:gfwlist.dat:all" or nil
|
||||
end
|
||||
if gfw_rule then
|
||||
domain_list = (domain_list == "") and gfw_rule or domain_list .. "\n" .. gfw_rule
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1539,7 +1556,7 @@ function gen_config(var)
|
||||
w = api.trim(w)
|
||||
if w == "" or w:find("#") == 1 then return end
|
||||
if w:find("rule-set:", 1, true) == 1 or w:find("rs:") == 1 then return end
|
||||
if w:find("ext:", 1, true) == 1 then return end -- Rule currently not support ext:
|
||||
if w:find("ext:", 1, true) == 1 and not (e[".name"] == "GFW_Mode_List" and w == "ext:gfwlist.dat:all") then return end
|
||||
table.insert(domains, w)
|
||||
table.insert(domain_table.domain, w)
|
||||
end)
|
||||
@@ -2051,10 +2068,7 @@ function gen_config(var)
|
||||
|
||||
if inbounds or outbounds then
|
||||
local config = {
|
||||
env = (function()
|
||||
local asset_location = api.uci_get_c("@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/"
|
||||
return { XRAY_LOCATION_ASSET = asset_location }
|
||||
end)(),
|
||||
env = { XRAY_LOCATION_ASSET = api.CACHE_PATH .. "/geodata" },
|
||||
log = {
|
||||
-- error = string.format("/tmp/etc/passwall/%s.log", node[".name"]),
|
||||
loglevel = get_log_level(loglevel)
|
||||
|
||||
@@ -24,6 +24,7 @@ if node then
|
||||
end
|
||||
-%>
|
||||
<script src="<%=resource%>/view/<%=appname%>/Sortable.min.js?v=1.15.7"></script>
|
||||
<script src="<%=resource%>/view/<%=appname%>/node_list_sort.js"></script>
|
||||
|
||||
<style>
|
||||
table th, .table .th {
|
||||
@@ -385,6 +386,89 @@ table td, .table .td {
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* node list: click-to-sort headers and the per-group filter bar */
|
||||
th[data-pw-sort] {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
th[data-pw-sort]:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.pw-sort-arrow {
|
||||
display: inline-block;
|
||||
min-width: 1em;
|
||||
margin-left: 0.25em;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.pw-sort-sub {
|
||||
font-weight: normal;
|
||||
font-size: 85%;
|
||||
opacity: 0.85;
|
||||
margin-left: 0.6em;
|
||||
cursor: pointer;
|
||||
text-decoration: underline dotted;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pw-sort-sub.pw-sort-active {
|
||||
opacity: 1;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pw-sort-note {
|
||||
margin: 0.3em 0;
|
||||
padding: 0.3em 0.6em;
|
||||
border-radius: 3px;
|
||||
background: rgba(251, 154, 5, 0.15);
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
tr.pw-row-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.pw-filter {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.3em 0.8em;
|
||||
margin: 0.4em 0;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.pw-filter label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pw-filter select,
|
||||
.pw-filter input[type="search"],
|
||||
.pw-filter .cbi-button {
|
||||
width: auto !important;
|
||||
min-width: 6em;
|
||||
max-width: 22em;
|
||||
height: auto;
|
||||
padding: 0.15em 0.3em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pw-filter .pw-filter-count {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.pw-filter.pw-filter-on .pw-filter-count {
|
||||
font-weight: bold;
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
<% if is_js_luci then -%>
|
||||
var cbi_t = [];
|
||||
@@ -540,15 +624,29 @@ table td, .table .td {
|
||||
}
|
||||
}
|
||||
|
||||
function node_row_visible(el) {
|
||||
if (window.pwNodeSort && typeof pwNodeSort.rowVisible === "function")
|
||||
return pwNodeSort.rowVisible(el);
|
||||
var row = el && el.closest ? el.closest("tr") : null;
|
||||
return !row || !row.classList.contains("pw-row-hidden");
|
||||
}
|
||||
|
||||
function set_select_all_state(sectionChecked) {
|
||||
var visibleContainer = document.querySelector('#cbi-<%=appname%>-nodes > .cbi-tabcontainer[style*="display:block"], #cbi-<%=appname%>-nodes > .cbi-tabcontainer[style*="display: block"]');
|
||||
if (!visibleContainer) return;
|
||||
var nodes = visibleContainer.getElementsByClassName("nodes_select");
|
||||
var selectAllChk = visibleContainer.querySelector(".nodes_select_all");
|
||||
var selectAllBtn = document.getElementById("select_all_btn");
|
||||
var visibleCount = 0;
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
if (!node_row_visible(nodes[i])) {
|
||||
nodes[i].checked = false;
|
||||
continue;
|
||||
}
|
||||
nodes[i].checked = sectionChecked;
|
||||
visibleCount++;
|
||||
}
|
||||
sectionChecked = sectionChecked && visibleCount > 0;
|
||||
if (selectAllChk) {
|
||||
selectAllChk.checked = sectionChecked;
|
||||
selectAllChk.title = sectionChecked ? "<%:DeSelect all%>" : "<%:Select all%>";
|
||||
@@ -576,10 +674,13 @@ table td, .table .td {
|
||||
var selectAllChk = visibleContainer.querySelector(".nodes_select_all");
|
||||
var selectAllBtn = document.getElementById("select_all_btn");
|
||||
var checkedCount = 0;
|
||||
var visibleCount = 0;
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
if (!node_row_visible(nodes[i])) continue;
|
||||
visibleCount++;
|
||||
if (nodes[i].checked) checkedCount++;
|
||||
}
|
||||
var allChecked = checkedCount === nodes.length;
|
||||
var allChecked = visibleCount > 0 && checkedCount === visibleCount;
|
||||
var title = allChecked ? "<%:DeSelect all%>" : "<%:Select all%>";
|
||||
var onclickFunc = allChecked ? "dechecked_all_node(this)" : "checked_all_node(this)";
|
||||
|
||||
@@ -602,7 +703,7 @@ table td, .table .td {
|
||||
var doms = visibleContainer.getElementsByClassName("nodes_select");
|
||||
if (doms && doms.length > 0) {
|
||||
for (var i = 0 ; i < doms.length; i++) {
|
||||
if (doms[i].checked) {
|
||||
if (doms[i].checked && node_row_visible(doms[i])) {
|
||||
ids.push(doms[i].getAttribute("cbid"))
|
||||
}
|
||||
}
|
||||
@@ -1163,7 +1264,7 @@ table td, .table .td {
|
||||
const doms = visibleContainer.getElementsByClassName("nodes_select");
|
||||
if (doms && doms.length > 0) {
|
||||
for (let i = 0 ; i < doms.length; i++) {
|
||||
if (doms[i].checked) {
|
||||
if (doms[i].checked && node_row_visible(doms[i])) {
|
||||
ids.push(doms[i].getAttribute("cbid"))
|
||||
}
|
||||
}
|
||||
@@ -1195,7 +1296,7 @@ table td, .table .td {
|
||||
const doms = visibleContainer.getElementsByClassName("nodes_select");
|
||||
if (doms && doms.length > 0) {
|
||||
for (let i = 0 ; i < doms.length; i++) {
|
||||
if (doms[i].checked) {
|
||||
if (doms[i].checked && node_row_visible(doms[i])) {
|
||||
ids.push(doms[i].getAttribute("cbid"))
|
||||
}
|
||||
}
|
||||
@@ -1307,8 +1408,33 @@ table td, .table .td {
|
||||
<<%=section_tag%> class="cbi-section" id="node_list">
|
||||
</<%=section_tag%>>
|
||||
|
||||
<div id="pw-nodesort-i18n" data-app="<%=appname%>" style="display:none">
|
||||
<span data-k="hint"><%:Click to sort; click again to reverse. Press Save & Apply to keep the order.%></span>
|
||||
<span data-k="typeHint"><%:Sort by type or protocol, then by name%></span>
|
||||
<span data-k="addrHint"><%:Sort by server address and port%></span>
|
||||
<span data-k="origHint"><%:Restore the saved order%></span>
|
||||
<span data-k="changed"><%:The order changed on this page only. Press Save & Apply to store it on the router.%></span>
|
||||
<span data-k="restored"><%:Saved order restored.%></span>
|
||||
<span data-k="wait"><%:Automatic ping is still running (%d left). Sorting will run when it finishes.%></span>
|
||||
<span data-k="waitFail"><%:Automatic ping did not finish within 2 minutes. Sorting was cancelled. Reload the page and try again.%></span>
|
||||
<span data-k="filter"><%:Filter:%></span>
|
||||
<span data-k="all"><%:All%></span>
|
||||
<span data-k="empty"><%:(empty)%></span>
|
||||
<span data-k="type"><%:Type%></span>
|
||||
<span data-k="addr"><%:Address%></span>
|
||||
<span data-k="fWord"><%:Word in remarks%></span>
|
||||
<span data-k="search"><%:search...%></span>
|
||||
<span data-k="searchHint"><%:Show only rows that contain this text%></span>
|
||||
<span data-k="shown"><%:Shown %s of %t%></span>
|
||||
<span data-k="reset"><%:Reset%></span>
|
||||
<span data-k="resetHint"><%:Show all rows%></span>
|
||||
<span data-k="filterHint"><%:The filter only hides rows on this page and changes nothing on the router. Select all skips hidden rows.%></span>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
//init logic
|
||||
if (window.pwNodeSort)
|
||||
pwNodeSort.init();
|
||||
|
||||
function get_remarks_name(o) {
|
||||
let str = "";
|
||||
@@ -1511,6 +1637,8 @@ table td, .table .td {
|
||||
get_now_use_node();
|
||||
|
||||
pingAllNodes();
|
||||
if (window.pwNodeSort)
|
||||
pwNodeSort.enhanceAll();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -974,8 +974,8 @@ msgstr "从 Geo 文件生成规则"
|
||||
msgid "Generate rule lists such as GFW, China domains, and China IP ranges based on Geo files."
|
||||
msgstr "根据 Geo 文件生成规则列表,包括 GFW、中国域名和中国 IP 段等。"
|
||||
|
||||
msgid "When manually updating with this option enabled, rules will be regenerated from existing Geo files even if no new version is available."
|
||||
msgstr "启用此选项后手动更新时,即使没有新版本也会从现有 Geo 文件重新生成规则。"
|
||||
msgid "GeoSite files without GFW rules cannot generate a GFW list (e.g. the original v2fly/Xray files)."
|
||||
msgstr "不含 GFW 规则的 GeoSite 文件无法生成 GFW 列表(如 v2fly/Xray 原版)。"
|
||||
|
||||
msgid "Enable Geo Data Parsing"
|
||||
msgstr "开启 Geo 数据解析"
|
||||
@@ -2048,3 +2048,57 @@ msgstr "WARP 设备私钥"
|
||||
|
||||
msgid "WARP Endpoint Public Key"
|
||||
msgstr "WARP 服务端公钥"
|
||||
|
||||
msgid "Click to sort; click again to reverse. Press Save & Apply to keep the order."
|
||||
msgstr "点击表头排序,再次点击切换方向。按保存并应用可保留顺序。"
|
||||
|
||||
msgid "Sort by type or protocol, then by name"
|
||||
msgstr "先按类型或协议排序,再按名称排序"
|
||||
|
||||
msgid "Sort by server address and port"
|
||||
msgstr "按服务器地址和端口排序"
|
||||
|
||||
msgid "Restore the saved order"
|
||||
msgstr "恢复已保存的顺序"
|
||||
|
||||
msgid "The order changed on this page only. Press Save & Apply to store it on the router."
|
||||
msgstr "顺序仅在本页改变。按保存并应用才会写入路由器。"
|
||||
|
||||
msgid "Saved order restored."
|
||||
msgstr "已恢复保存的顺序。"
|
||||
|
||||
msgid "Automatic ping is still running (%d left). Sorting will run when it finishes."
|
||||
msgstr "自动探测仍在进行(剩余 %d)。结束后会立即排序。"
|
||||
|
||||
msgid "Automatic ping did not finish within 2 minutes. Sorting was cancelled. Reload the page and try again."
|
||||
msgstr "自动探测在 2 分钟内未结束,已取消排序。请刷新页面后重试。"
|
||||
|
||||
msgid "Filter:"
|
||||
msgstr "筛选:"
|
||||
|
||||
msgid "(empty)"
|
||||
msgstr "(空)"
|
||||
|
||||
msgid "Word in remarks"
|
||||
msgstr "备注关键词"
|
||||
|
||||
msgid "Address"
|
||||
msgstr "地址"
|
||||
|
||||
msgid "search..."
|
||||
msgstr "搜索..."
|
||||
|
||||
msgid "Show only rows that contain this text"
|
||||
msgstr "只显示包含此文本的行"
|
||||
|
||||
msgid "Shown %s of %t"
|
||||
msgstr "显示 %s / %t"
|
||||
|
||||
msgid "Reset"
|
||||
msgstr "重置"
|
||||
|
||||
msgid "Show all rows"
|
||||
msgstr "显示全部行"
|
||||
|
||||
msgid "The filter only hides rows on this page and changes nothing on the router. Select all skips hidden rows."
|
||||
msgstr "筛选只在本页隐藏行,不会改动路由器上的设置。全选会跳过隐藏行。"
|
||||
@@ -640,8 +640,8 @@ start_global() {
|
||||
}
|
||||
|
||||
[ "$protocol" = "_shunt" ] && {
|
||||
local geoip_path="${V2RAY_LOCATION_ASSET%*/}/geoip.dat"
|
||||
local geosite_path="${V2RAY_LOCATION_ASSET%*/}/geosite.dat"
|
||||
local geoip_path="${GEO_DIR%/}/geoip.dat"
|
||||
local geosite_path="${GEO_DIR%/}/geosite.dat"
|
||||
if [ ! -s "$geoip_path" ] || [ ! -s "$geosite_path" ]; then
|
||||
echolog "* 缺少Geo规则文件,TCP Sing-Box分流节点无法正常使用!"
|
||||
fi
|
||||
@@ -724,8 +724,8 @@ start_global() {
|
||||
}
|
||||
|
||||
[ "$protocol" = "_shunt" ] && {
|
||||
local geoip_path="${V2RAY_LOCATION_ASSET%*/}/geoip.dat"
|
||||
local geosite_path="${V2RAY_LOCATION_ASSET%*/}/geosite.dat"
|
||||
local geoip_path="${GEO_DIR%/}/geoip.dat"
|
||||
local geosite_path="${GEO_DIR%/}/geosite.dat"
|
||||
if [ ! -s "$geoip_path" ] || [ ! -s "$geosite_path" ]; then
|
||||
echolog "* 缺少Geo规则文件,TCP Xray分流节点无法正常使用!"
|
||||
fi
|
||||
@@ -1718,8 +1718,14 @@ start() {
|
||||
mkdir -p /tmp/etc /tmp/log $TMP_PATH $TMP_BIN_PATH $TMP_SCRIPT_FUNC_PATH $TMP_ROUTE_PATH $TMP_ACL_PATH $TMP_PATH2
|
||||
set_cache_var "LOCAL_DNS" "$LOCAL_DNS"
|
||||
get_config
|
||||
export V2RAY_LOCATION_ASSET=$(config_n_get @global_rules[0] v2ray_location_asset "/usr/share/v2ray/")
|
||||
export XRAY_LOCATION_ASSET=$V2RAY_LOCATION_ASSET
|
||||
GEO_DIR=$(config_n_get @global_rules[0] v2ray_location_asset "/usr/share/v2ray/")
|
||||
export XRAY_LOCATION_ASSET="$TMP_PATH2/geodata"
|
||||
mkdir -p "$XRAY_LOCATION_ASSET" 2>/dev/null
|
||||
local geo_file
|
||||
for geo_file in "${GEO_DIR%/}"/*.dat; do
|
||||
[ -f "$geo_file" ] || continue
|
||||
ln -sf "$geo_file" "$XRAY_LOCATION_ASSET/${geo_file##*/}" 2>/dev/null
|
||||
done
|
||||
export ENABLE_DEPRECATED_GEOSITE=true
|
||||
export ENABLE_DEPRECATED_GEOIP=true
|
||||
export SS_SYSTEM_DNS_RESOLVER_FORCE_BUILTIN=1
|
||||
@@ -1800,7 +1806,6 @@ stop() {
|
||||
busybox pgrep -af "${CONFIG}/monitor\.sh" | xargs -r kill -9 >/dev/null 2>&1
|
||||
busybox pgrep -f "sleep.*(6s|9s|58s)" | xargs -r kill -9 >/dev/null 2>&1
|
||||
busybox pgrep -af "${CONFIG}/" | awk '! /app\.sh|subscribe\.lua|rule_update\.lua|tasks\.sh|server_app\.lua|ujail/{print $1}' | xargs -r kill -9 >/dev/null 2>&1
|
||||
unset V2RAY_LOCATION_ASSET
|
||||
unset XRAY_LOCATION_ASSET
|
||||
unset SS_SYSTEM_DNS_RESOLVER_FORCE_BUILTIN
|
||||
stop_crontab "$1"
|
||||
|
||||
@@ -1597,6 +1597,7 @@ stop() {
|
||||
rm -rf $TMP_PATH2/singbox*
|
||||
rm -rf $TMP_PATH2/dnsmasq*
|
||||
rm -rf $TMP_PATH2/geo_output
|
||||
rm -rf $TMP_PATH2/geodata
|
||||
rm -rf $TMP_PATH2/user_rules
|
||||
}
|
||||
flush_include
|
||||
|
||||
@@ -1610,6 +1610,7 @@ stop() {
|
||||
rm -rf $TMP_PATH2/singbox*
|
||||
rm -rf $TMP_PATH2/dnsmasq*
|
||||
rm -rf $TMP_PATH2/geo_output
|
||||
rm -rf $TMP_PATH2/geodata
|
||||
rm -rf $TMP_PATH2/user_rules
|
||||
}
|
||||
flush_include
|
||||
|
||||
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall2
|
||||
PKG_VERSION:=26.10.20
|
||||
PKG_RELEASE:=149
|
||||
PKG_RELEASE:=150
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@@ -0,0 +1,775 @@
|
||||
/*
|
||||
* Click-to-sort headers and a per-group filter bar for the node list.
|
||||
*
|
||||
* Sort reorders rows inside one group table and calls set_order(), the same
|
||||
* path as drag-and-drop and To Top. Nothing is written until Save & Apply.
|
||||
*
|
||||
* A filter only toggles the pw-row-hidden class. Rows stay in the form, so
|
||||
* Save & Apply still posts every node and the full group.order value.
|
||||
*
|
||||
* Translated strings are read from #pw2-nodesort-i18n (the node list template).
|
||||
* The template calls init() once, then enhanceAll() after loadNodeList()
|
||||
* has built the group tables.
|
||||
*
|
||||
* auto_detection_time is a top-level let in the template, not a window
|
||||
* property. It is read by name so rows are not moved while automatic Ping
|
||||
* or TCPing is still writing results by row position.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var COLUMNS = {
|
||||
'pw-remark': 'name',
|
||||
'pw-ping': 'ping',
|
||||
'pw-tcping': 'tcping',
|
||||
'pw-urltest': 'urltest'
|
||||
};
|
||||
/* sub controls drawn inside the Remarks header: [sort key, label key or symbol, title key] */
|
||||
var NAME_EXTRA = [
|
||||
['type', 'type', 'typeHint'],
|
||||
['addr', 'addr', 'addrHint'],
|
||||
['orig', '\u21BA', 'origHint']
|
||||
];
|
||||
|
||||
var state = {
|
||||
app: 'passwall2',
|
||||
root: null,
|
||||
text: {},
|
||||
bound: false
|
||||
};
|
||||
var searchTimer = null;
|
||||
|
||||
function T(key) {
|
||||
var value = state.text[key];
|
||||
return (value == null || value === '') ? key : value;
|
||||
}
|
||||
|
||||
function readText(box) {
|
||||
var text = {};
|
||||
var spans = box.querySelectorAll('[data-k]');
|
||||
for (var i = 0; i < spans.length; i++)
|
||||
text[spans[i].getAttribute('data-k')] = spans[i].textContent;
|
||||
return text;
|
||||
}
|
||||
|
||||
var collator = null;
|
||||
function prepareCollator() {
|
||||
var lang = document.documentElement.lang || (navigator.language || '');
|
||||
try {
|
||||
collator = (window.Intl && Intl.Collator) ? new Intl.Collator(lang || undefined, { numeric: true, sensitivity: 'base' }) : null;
|
||||
} catch (e) {
|
||||
collator = null;
|
||||
}
|
||||
}
|
||||
|
||||
function cmpText(a, b) {
|
||||
if (collator)
|
||||
return collator.compare(a, b);
|
||||
return a < b ? -1 : (a > b ? 1 : 0);
|
||||
}
|
||||
|
||||
/* "123 ms", "12.5 ms", "1.2 s" -> milliseconds. Timeout, Error, ---, Test stay null. */
|
||||
function parseLatency(text) {
|
||||
var t = String(text || '').replace(/\s+/g, ' ').trim();
|
||||
var m = /^(\d+(?:[.,]\d+)?)\s*(ms|s)?$/i.exec(t);
|
||||
if (!m)
|
||||
return null;
|
||||
var v = parseFloat(m[1].replace(',', '.'));
|
||||
if (isNaN(v))
|
||||
return null;
|
||||
if (m[2] && /^s$/i.test(m[2]))
|
||||
v *= 1000;
|
||||
return v;
|
||||
}
|
||||
|
||||
function hiddenVal(id, opt) {
|
||||
var el = document.getElementById('cbid.' + state.app + '.' + id + '.' + opt);
|
||||
return el && el.value != null ? String(el.value).trim() : '';
|
||||
}
|
||||
|
||||
function cellsOf(tr) {
|
||||
var out = [];
|
||||
for (var i = 0; i < tr.children.length; i++) {
|
||||
var tag = tr.children[i].tagName;
|
||||
if (tag === 'TD' || tag === 'TH')
|
||||
out.push(tr.children[i]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
var wordSplit;
|
||||
try {
|
||||
wordSplit = new RegExp('[^\\p{L}\\p{N}]+', 'u');
|
||||
} catch (e) {
|
||||
wordSplit = /[\s.,;:!?|\/\\()\[\]{}<>"'`~@#$%^&*=+_-]+/;
|
||||
}
|
||||
|
||||
function wordsOf(text) {
|
||||
var seen = {};
|
||||
var out = [];
|
||||
var parts = String(text || '').split(wordSplit);
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
var w = parts[i];
|
||||
if (w.length < 2 || /^\d+$/.test(w))
|
||||
continue;
|
||||
var k = w.toLowerCase();
|
||||
if (!seen[k]) {
|
||||
seen[k] = 1;
|
||||
out.push(w);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function infoOf(table) {
|
||||
var prefix = 'cbi-' + state.app + '-nodes-';
|
||||
var id = table && table.id ? table.id : '';
|
||||
if (id.indexOf(prefix) !== 0 || id.slice(-6) !== '-table')
|
||||
return null;
|
||||
return { group: id.slice(prefix.length, -6) };
|
||||
}
|
||||
|
||||
function rowId(tr) {
|
||||
var prefix = 'cbi-' + state.app + '-';
|
||||
return tr.id.indexOf(prefix) === 0 ? tr.id.substring(prefix.length) : tr.id;
|
||||
}
|
||||
|
||||
function bodyOf(table) {
|
||||
return table.querySelector('tbody') || table;
|
||||
}
|
||||
|
||||
function dataRows(table) {
|
||||
var body = bodyOf(table);
|
||||
var out = [];
|
||||
for (var i = 0; i < body.children.length; i++) {
|
||||
var tr = body.children[i];
|
||||
if (tr.tagName === 'TR' && tr.classList.contains('cbi-section-table-row') && !tr.classList.contains('placeholder'))
|
||||
out.push(tr);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function colIndexMap(table) {
|
||||
var rows = dataRows(table);
|
||||
var map = {};
|
||||
if (!rows.length)
|
||||
return map;
|
||||
var cells = cellsOf(rows[0]);
|
||||
for (var i = 0; i < cells.length; i++) {
|
||||
for (var cls in COLUMNS) {
|
||||
if (cells[i].classList.contains(cls))
|
||||
map[COLUMNS[cls]] = i;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function typeOf(tr, cols) {
|
||||
var cell = cellsOf(tr)[cols.name];
|
||||
var text = cell ? cell.textContent.replace(/\s+/g, ' ').trim() : '';
|
||||
var i = text.indexOf('\uFF1A');
|
||||
if (i < 0)
|
||||
i = text.indexOf(':');
|
||||
return (i < 0 ? '' : text.slice(0, i)).replace(/\s+Realm$/, '').trim();
|
||||
}
|
||||
|
||||
function sortKey(tr, key, cols) {
|
||||
var id = rowId(tr);
|
||||
var cell;
|
||||
if (key === 'name')
|
||||
return hiddenVal(id, 'remarks') || ((cellsOf(tr)[cols.name] || {}).textContent || '').trim();
|
||||
if (key === 'type')
|
||||
return typeOf(tr, cols) + '\u0000' + hiddenVal(id, 'remarks');
|
||||
if (key === 'addr') {
|
||||
var address = hiddenVal(id, 'address');
|
||||
if (!address)
|
||||
return null;
|
||||
return address + '\u0000' + ('00000' + hiddenVal(id, 'port')).slice(-5);
|
||||
}
|
||||
cell = cellsOf(tr)[cols[key]];
|
||||
return cell ? parseLatency(cell.textContent) : null;
|
||||
}
|
||||
|
||||
function detectionMode() {
|
||||
try {
|
||||
if (typeof auto_detection_time === 'undefined')
|
||||
return null;
|
||||
return auto_detection_time;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/* { inflight: requests still running, left: cells still waiting for a result } */
|
||||
function pingBusy() {
|
||||
var mode = detectionMode();
|
||||
var inflight = 0;
|
||||
var left = 0;
|
||||
if (mode !== 'icmp' && mode !== 'tcping')
|
||||
return { inflight: 0, left: 0 };
|
||||
var list = window.ajax && window.ajax.list;
|
||||
if (list && list.length) {
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
if (list[i].readyState > 0 && list[i].readyState < 4)
|
||||
inflight++;
|
||||
}
|
||||
}
|
||||
if (!state.root)
|
||||
return { inflight: inflight, left: 0 };
|
||||
var spans = state.root.getElementsByClassName(mode === 'tcping' ? 'tcping_value' : 'ping_value');
|
||||
for (var s = 0; s < spans.length; s++) {
|
||||
var id = spans[s].getAttribute('cbiid');
|
||||
if (!id || spans[s].textContent.trim() !== '---')
|
||||
continue;
|
||||
var full = null;
|
||||
if (typeof window.get_address_full === 'function')
|
||||
full = window.get_address_full(id);
|
||||
if (!full)
|
||||
full = { address: hiddenVal(id, 'address'), port: hiddenVal(id, 'port') };
|
||||
if (full.address && (mode === 'icmp' || full.port))
|
||||
left++;
|
||||
}
|
||||
return { inflight: inflight, left: left };
|
||||
}
|
||||
|
||||
function note(table, text) {
|
||||
var id = 'pw-note-' + table.id;
|
||||
var el = document.getElementById(id);
|
||||
if (!text) {
|
||||
if (el && el.parentNode)
|
||||
el.parentNode.removeChild(el);
|
||||
return;
|
||||
}
|
||||
if (!el) {
|
||||
el = document.createElement('div');
|
||||
el.id = id;
|
||||
el.className = 'pw-sort-note';
|
||||
table.parentNode.insertBefore(el, table);
|
||||
}
|
||||
el.textContent = text;
|
||||
}
|
||||
|
||||
function setIndicator(table, th, sub, key, dir) {
|
||||
var arrows = table.querySelectorAll('.pw-sort-arrow');
|
||||
var subs = table.querySelectorAll('.pw-sort-sub');
|
||||
var headers = table.querySelectorAll('th[data-pw-sort]');
|
||||
var i;
|
||||
for (i = 0; i < arrows.length; i++)
|
||||
arrows[i].textContent = '';
|
||||
for (i = 0; i < subs.length; i++)
|
||||
subs[i].classList.remove('pw-sort-active');
|
||||
for (i = 0; i < headers.length; i++) {
|
||||
if (headers[i] !== th) {
|
||||
delete headers[i].dataset.pwDir;
|
||||
delete headers[i].dataset.pwKey;
|
||||
}
|
||||
}
|
||||
var arrow = th.querySelector('.pw-sort-arrow');
|
||||
if (arrow && key !== 'orig' && !sub)
|
||||
arrow.textContent = dir === 'desc' ? '\u25BC' : '\u25B2';
|
||||
if (sub && key !== 'orig')
|
||||
sub.classList.add('pw-sort-active');
|
||||
}
|
||||
|
||||
function applyOrder(table, info, rows) {
|
||||
var body = bodyOf(table);
|
||||
var frag = document.createDocumentFragment();
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
rows[i].classList.remove('cbi-rowstyle-1', 'cbi-rowstyle-2');
|
||||
rows[i].classList.add('cbi-rowstyle-' + (i % 2 + 1));
|
||||
frag.appendChild(rows[i]);
|
||||
}
|
||||
body.appendChild(frag);
|
||||
if (typeof window.set_order === 'function')
|
||||
window.set_order(info.group);
|
||||
}
|
||||
|
||||
function doSort(table, th, sub, key) {
|
||||
var info = infoOf(table);
|
||||
var rows = dataRows(table);
|
||||
var dir = 'asc';
|
||||
if (!info || rows.length < 2)
|
||||
return;
|
||||
if (key === 'orig') {
|
||||
var pos = {};
|
||||
var orig = table.__pwOrig || [];
|
||||
for (var n = 0; n < orig.length; n++)
|
||||
pos[orig[n]] = n;
|
||||
rows.sort(function (a, b) {
|
||||
var pa = pos[rowId(a)];
|
||||
var pb = pos[rowId(b)];
|
||||
return (pa == null ? 1e9 : pa) - (pb == null ? 1e9 : pb);
|
||||
});
|
||||
delete th.dataset.pwDir;
|
||||
delete th.dataset.pwKey;
|
||||
} else {
|
||||
dir = (th.dataset.pwKey === key && th.dataset.pwDir === 'asc') ? 'desc' : 'asc';
|
||||
th.dataset.pwKey = key;
|
||||
th.dataset.pwDir = dir;
|
||||
var cols = colIndexMap(table);
|
||||
var sign = dir === 'desc' ? -1 : 1;
|
||||
var list = [];
|
||||
for (var i = 0; i < rows.length; i++)
|
||||
list.push({ tr: rows[i], i: i, k: sortKey(rows[i], key, cols) });
|
||||
list.sort(function (a, b) {
|
||||
var emptyA = (a.k === null || a.k === '');
|
||||
var emptyB = (b.k === null || b.k === '');
|
||||
if (emptyA || emptyB)
|
||||
return emptyA === emptyB ? a.i - b.i : (emptyA ? 1 : -1);
|
||||
var cmp = (typeof a.k === 'number' && typeof b.k === 'number') ? a.k - b.k : cmpText(String(a.k), String(b.k));
|
||||
return cmp !== 0 ? sign * cmp : a.i - b.i;
|
||||
});
|
||||
rows = [];
|
||||
for (var j = 0; j < list.length; j++)
|
||||
rows.push(list[j].tr);
|
||||
}
|
||||
applyOrder(table, info, rows);
|
||||
refreshFilter(table);
|
||||
setIndicator(table, th, sub, key, dir);
|
||||
note(table, key === 'orig' ? T('restored') : T('changed'));
|
||||
}
|
||||
|
||||
function requestSort(table, th, sub, key) {
|
||||
if (table.__pwWait) {
|
||||
clearInterval(table.__pwWait);
|
||||
table.__pwWait = null;
|
||||
}
|
||||
var st = pingBusy();
|
||||
if (!st.inflight && !st.left) {
|
||||
doSort(table, th, sub, key);
|
||||
return;
|
||||
}
|
||||
var started = Date.now();
|
||||
var idle = 0;
|
||||
note(table, T('wait').replace('%d', st.left || st.inflight));
|
||||
table.__pwWait = setInterval(function () {
|
||||
st = pingBusy();
|
||||
idle = st.inflight ? 0 : idle + 1;
|
||||
if ((!st.inflight && !st.left) || idle >= 6) {
|
||||
clearInterval(table.__pwWait);
|
||||
table.__pwWait = null;
|
||||
if (document.body.contains(table))
|
||||
doSort(table, th, sub, key);
|
||||
return;
|
||||
}
|
||||
if (Date.now() - started > 120000) {
|
||||
clearInterval(table.__pwWait);
|
||||
table.__pwWait = null;
|
||||
note(table, T('waitFail'));
|
||||
return;
|
||||
}
|
||||
note(table, T('wait').replace('%d', st.left || st.inflight));
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function enhance(table) {
|
||||
var info = infoOf(table);
|
||||
if (!info || table.__pwReady)
|
||||
return;
|
||||
var titles = table.querySelector('tr.cbi-section-table-titles');
|
||||
var rows = dataRows(table);
|
||||
if (!titles || !rows.length)
|
||||
return;
|
||||
var cols = colIndexMap(table);
|
||||
var headers = cellsOf(titles);
|
||||
table.__pwReady = true;
|
||||
table.__pwOrig = [];
|
||||
for (var r = 0; r < rows.length; r++)
|
||||
table.__pwOrig.push(rowId(rows[r]));
|
||||
for (var key in cols) {
|
||||
if (!Object.prototype.hasOwnProperty.call(cols, key))
|
||||
continue;
|
||||
var th = headers[cols[key]];
|
||||
if (!th || th.classList.contains('cbi-section-actions'))
|
||||
continue;
|
||||
th.setAttribute('data-pw-sort', key);
|
||||
th.title = T('hint');
|
||||
var arrow = document.createElement('span');
|
||||
arrow.className = 'pw-sort-arrow';
|
||||
th.appendChild(arrow);
|
||||
if (key !== 'name')
|
||||
continue;
|
||||
for (var s = 0; s < NAME_EXTRA.length; s++) {
|
||||
var spec = NAME_EXTRA[s];
|
||||
var sp = document.createElement('span');
|
||||
sp.className = 'pw-sort-sub';
|
||||
sp.setAttribute('data-pw-sort', spec[0]);
|
||||
sp.textContent = T(spec[1]);
|
||||
sp.title = T(spec[2]);
|
||||
th.appendChild(sp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function rowVisible(el) {
|
||||
var row = el && el.closest ? el.closest('tr') : null;
|
||||
return !row || !row.classList.contains('pw-row-hidden');
|
||||
}
|
||||
|
||||
function filterDefs(rows, cols) {
|
||||
return [
|
||||
{
|
||||
key: 'type',
|
||||
label: T('type'),
|
||||
multi: false,
|
||||
get: function (tr) { return typeOf(tr, cols); }
|
||||
},
|
||||
{
|
||||
key: 'word',
|
||||
label: T('fWord'),
|
||||
multi: true,
|
||||
minCount: 2,
|
||||
max: 40,
|
||||
get: function (tr) { return wordsOf(hiddenVal(rowId(tr), 'remarks')); }
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function searchText(tr, cols) {
|
||||
var id = rowId(tr);
|
||||
return [
|
||||
typeOf(tr, cols),
|
||||
hiddenVal(id, 'remarks'),
|
||||
hiddenVal(id, 'address') + ':' + hiddenVal(id, 'port')
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
function collect(table) {
|
||||
var rows = dataRows(table);
|
||||
var cols = colIndexMap(table);
|
||||
var defs = rows.length >= 2 ? filterDefs(rows, cols) : [];
|
||||
var shown = [];
|
||||
for (var d = 0; d < defs.length; d++) {
|
||||
var def = defs[d];
|
||||
var counts = {};
|
||||
var labels = {};
|
||||
var vals = [];
|
||||
for (var r = 0; r < rows.length; r++) {
|
||||
var raw = def.get(rows[r]);
|
||||
var list = def.multi ? (raw || []) : [raw == null ? '' : String(raw)];
|
||||
var keys = [];
|
||||
for (var n = 0; n < list.length; n++) {
|
||||
var item = list[n];
|
||||
var k = def.multi ? String(item).toLowerCase() : String(item);
|
||||
counts[k] = (counts[k] || 0) + 1;
|
||||
if (!labels[k])
|
||||
labels[k] = {};
|
||||
labels[k][item] = (labels[k][item] || 0) + 1;
|
||||
keys.push(k);
|
||||
}
|
||||
vals.push(keys);
|
||||
}
|
||||
var opts = [];
|
||||
for (var k in counts) {
|
||||
if (!Object.prototype.hasOwnProperty.call(counts, k))
|
||||
continue;
|
||||
if (def.multi && (counts[k] < (def.minCount || 1) || counts[k] >= rows.length))
|
||||
continue;
|
||||
var best = '';
|
||||
var bestN = -1;
|
||||
for (var label in labels[k]) {
|
||||
if (labels[k][label] > bestN) {
|
||||
bestN = labels[k][label];
|
||||
best = label;
|
||||
}
|
||||
}
|
||||
opts.push({ v: k, label: best === '' ? T('empty') : best, n: counts[k] });
|
||||
}
|
||||
opts.sort(function (a, b) {
|
||||
if (def.multi && a.n !== b.n)
|
||||
return b.n - a.n;
|
||||
return cmpText(a.label, b.label);
|
||||
});
|
||||
if (def.max && opts.length > def.max)
|
||||
opts = opts.slice(0, def.max);
|
||||
var enough = def.multi ? opts.length >= 1 : opts.length >= 2;
|
||||
if (!enough)
|
||||
continue;
|
||||
def.vals = vals;
|
||||
def.opts = opts;
|
||||
shown.push(def);
|
||||
}
|
||||
var search = rows.length >= 2;
|
||||
var sig = String(rows.length) + (search ? 's' : '');
|
||||
for (var i = 0; i < shown.length; i++) {
|
||||
sig += '|' + shown[i].key + '=';
|
||||
for (var o = 0; o < shown[i].opts.length; o++)
|
||||
sig += shown[i].opts[o].v + ':' + shown[i].opts[o].n + ',';
|
||||
}
|
||||
return { rows: rows, cols: cols, defs: shown, search: search, sig: sig };
|
||||
}
|
||||
|
||||
function barId(table) {
|
||||
return 'pw-filter-' + table.id;
|
||||
}
|
||||
|
||||
function buildBar(table, collected) {
|
||||
var st = table.__pwF || (table.__pwF = { sel: {}, q: '' });
|
||||
var id = barId(table);
|
||||
var bar = document.getElementById(id);
|
||||
if (!bar) {
|
||||
bar = document.createElement('div');
|
||||
bar.id = id;
|
||||
bar.className = 'pw-filter';
|
||||
bar.title = T('filterHint');
|
||||
table.parentNode.insertBefore(bar, table);
|
||||
bar.__pwTable = table;
|
||||
}
|
||||
while (bar.firstChild)
|
||||
bar.removeChild(bar.firstChild);
|
||||
var head = document.createElement('span');
|
||||
head.textContent = T('filter');
|
||||
bar.appendChild(head);
|
||||
var keep = {};
|
||||
for (var d = 0; d < collected.defs.length; d++) {
|
||||
var def = collected.defs[d];
|
||||
var lab = document.createElement('label');
|
||||
var sel = document.createElement('select');
|
||||
lab.appendChild(document.createTextNode(def.label));
|
||||
sel.className = 'cbi-input-select';
|
||||
sel.setAttribute('data-pw-filter', def.key);
|
||||
var all = document.createElement('option');
|
||||
all.value = '';
|
||||
all.textContent = T('all') + ' (' + collected.rows.length + ')';
|
||||
sel.appendChild(all);
|
||||
for (var o = 0; o < def.opts.length; o++) {
|
||||
var op = document.createElement('option');
|
||||
op.value = 'v:' + def.opts[o].v;
|
||||
op.textContent = def.opts[o].label + ' (' + def.opts[o].n + ')';
|
||||
sel.appendChild(op);
|
||||
}
|
||||
var cur = st.sel[def.key] || '';
|
||||
var known = false;
|
||||
if (cur) {
|
||||
for (var c = 0; c < def.opts.length; c++) {
|
||||
if (('v:' + def.opts[c].v) === cur) {
|
||||
known = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!known)
|
||||
cur = '';
|
||||
sel.value = cur;
|
||||
if (cur)
|
||||
keep[def.key] = cur;
|
||||
lab.appendChild(sel);
|
||||
bar.appendChild(lab);
|
||||
}
|
||||
st.sel = keep;
|
||||
if (collected.search) {
|
||||
var input = document.createElement('input');
|
||||
input.type = 'search';
|
||||
input.className = 'cbi-input-text';
|
||||
input.placeholder = T('search');
|
||||
input.title = T('searchHint');
|
||||
input.autocomplete = 'off';
|
||||
input.setAttribute('data-pw-search', '1');
|
||||
input.value = st.q;
|
||||
bar.appendChild(input);
|
||||
} else {
|
||||
st.q = '';
|
||||
}
|
||||
var count = document.createElement('span');
|
||||
count.className = 'pw-filter-count';
|
||||
bar.appendChild(count);
|
||||
var reset = document.createElement('button');
|
||||
reset.type = 'button';
|
||||
reset.className = 'btn cbi-button';
|
||||
reset.setAttribute('data-pw-reset', '1');
|
||||
reset.textContent = T('reset');
|
||||
reset.title = T('resetHint');
|
||||
bar.appendChild(reset);
|
||||
return bar;
|
||||
}
|
||||
|
||||
function listHas(list, value) {
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
if (list[i] === value)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function applyFilter(table, collected, bar) {
|
||||
var st = table.__pwF || { sel: {}, q: '' };
|
||||
var q = String(st.q || '').toLowerCase().trim();
|
||||
var active = !!q;
|
||||
var shown = 0;
|
||||
var unchecked = false;
|
||||
var d;
|
||||
for (d = 0; d < collected.defs.length; d++) {
|
||||
if (st.sel[collected.defs[d].key])
|
||||
active = true;
|
||||
}
|
||||
for (var i = 0; i < collected.rows.length; i++) {
|
||||
var tr = collected.rows[i];
|
||||
var ok = true;
|
||||
for (d = 0; d < collected.defs.length; d++) {
|
||||
var sel = st.sel[collected.defs[d].key];
|
||||
if (ok && sel && !listHas(collected.defs[d].vals[i], sel.slice(2)))
|
||||
ok = false;
|
||||
}
|
||||
if (ok && q && searchText(tr, collected.cols).toLowerCase().indexOf(q) < 0)
|
||||
ok = false;
|
||||
if (ok !== !tr.classList.contains('pw-row-hidden'))
|
||||
tr.classList.toggle('pw-row-hidden', !ok);
|
||||
if (ok) {
|
||||
shown++;
|
||||
var want = 'cbi-rowstyle-' + ((shown - 1) % 2 + 1);
|
||||
var other = want === 'cbi-rowstyle-1' ? 'cbi-rowstyle-2' : 'cbi-rowstyle-1';
|
||||
if (!tr.classList.contains(want))
|
||||
tr.classList.add(want);
|
||||
if (tr.classList.contains(other))
|
||||
tr.classList.remove(other);
|
||||
} else {
|
||||
var cb = tr.querySelector('input.nodes_select');
|
||||
if (cb && cb.checked) {
|
||||
cb.checked = false;
|
||||
unchecked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (unchecked && typeof window.update_select_state === 'function')
|
||||
window.update_select_state();
|
||||
var count = bar.querySelector('.pw-filter-count');
|
||||
var txt = T('shown').replace('%s', shown).replace('%t', collected.rows.length);
|
||||
if (count && count.textContent !== txt)
|
||||
count.textContent = txt;
|
||||
if (bar.classList.contains('pw-filter-on') !== active)
|
||||
bar.classList.toggle('pw-filter-on', active);
|
||||
}
|
||||
|
||||
function removeBar(table) {
|
||||
var bar = document.getElementById(barId(table));
|
||||
if (bar && bar.parentNode)
|
||||
bar.parentNode.removeChild(bar);
|
||||
var rows = dataRows(table);
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
if (rows[i].classList.contains('pw-row-hidden'))
|
||||
rows[i].classList.remove('pw-row-hidden');
|
||||
}
|
||||
table.__pwSig = null;
|
||||
}
|
||||
|
||||
function refreshFilter(table) {
|
||||
if (!infoOf(table))
|
||||
return;
|
||||
var collected = collect(table);
|
||||
if (!collected.defs.length && !collected.search) {
|
||||
if (table.__pwSig)
|
||||
removeBar(table);
|
||||
return;
|
||||
}
|
||||
var bar = document.getElementById(barId(table));
|
||||
if (!bar || collected.sig !== table.__pwSig) {
|
||||
bar = buildBar(table, collected);
|
||||
table.__pwSig = collected.sig;
|
||||
}
|
||||
applyFilter(table, collected, bar);
|
||||
}
|
||||
|
||||
function barTable(el) {
|
||||
var bar = el && el.closest ? el.closest('.pw-filter') : null;
|
||||
return bar && bar.__pwTable ? bar.__pwTable : null;
|
||||
}
|
||||
|
||||
function eventTarget(ev) {
|
||||
var t = ev.target;
|
||||
if (t && t.nodeType !== 1)
|
||||
t = t.parentNode;
|
||||
return t;
|
||||
}
|
||||
|
||||
function bind(root) {
|
||||
root.addEventListener('click', function (ev) {
|
||||
var t = eventTarget(ev);
|
||||
if (t && t.getAttribute && t.getAttribute('data-pw-reset')) {
|
||||
var resetTable = barTable(t);
|
||||
if (!resetTable)
|
||||
return;
|
||||
ev.preventDefault();
|
||||
resetTable.__pwF = { sel: {}, q: '' };
|
||||
resetTable.__pwSig = null;
|
||||
refreshFilter(resetTable);
|
||||
return;
|
||||
}
|
||||
var el = t && t.closest ? t.closest('[data-pw-sort]') : null;
|
||||
if (!el || !root.contains(el))
|
||||
return;
|
||||
if (t.closest && t.closest('input, a, button, select'))
|
||||
return;
|
||||
var th = el.closest('th');
|
||||
var table = el.closest('table');
|
||||
if (!th || !table || !infoOf(table))
|
||||
return;
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
requestSort(table, th, el === th ? null : el, el.getAttribute('data-pw-sort'));
|
||||
});
|
||||
root.addEventListener('change', function (ev) {
|
||||
var t = eventTarget(ev);
|
||||
if (!t || !t.getAttribute || !t.getAttribute('data-pw-filter'))
|
||||
return;
|
||||
var table = barTable(t);
|
||||
if (!table)
|
||||
return;
|
||||
(table.__pwF = table.__pwF || { sel: {}, q: '' }).sel[t.getAttribute('data-pw-filter')] = t.value;
|
||||
refreshFilter(table);
|
||||
});
|
||||
root.addEventListener('input', function (ev) {
|
||||
var t = eventTarget(ev);
|
||||
if (!t || !t.getAttribute || !t.getAttribute('data-pw-search'))
|
||||
return;
|
||||
var table = barTable(t);
|
||||
if (!table)
|
||||
return;
|
||||
clearTimeout(searchTimer);
|
||||
searchTimer = setTimeout(function () {
|
||||
(table.__pwF = table.__pwF || { sel: {}, q: '' }).q = t.value;
|
||||
refreshFilter(table);
|
||||
}, 150);
|
||||
});
|
||||
root.addEventListener('keydown', function (ev) {
|
||||
var t = eventTarget(ev);
|
||||
if (ev.key === 'Enter' && t && t.getAttribute && t.getAttribute('data-pw-search'))
|
||||
ev.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
var box = document.getElementById('pw2-nodesort-i18n');
|
||||
var root = document.getElementById('node_list');
|
||||
if (!box || !root)
|
||||
return;
|
||||
state.app = box.getAttribute('data-app') || 'passwall2';
|
||||
state.text = readText(box);
|
||||
state.root = root;
|
||||
prepareCollator();
|
||||
if (!state.bound) {
|
||||
bind(root);
|
||||
state.bound = true;
|
||||
}
|
||||
}
|
||||
|
||||
function enhanceAll() {
|
||||
if (!state.root)
|
||||
return;
|
||||
var tables = state.root.querySelectorAll('table.cbi-section-table');
|
||||
for (var i = 0; i < tables.length; i++) {
|
||||
try {
|
||||
enhance(tables[i]);
|
||||
refreshFilter(tables[i]);
|
||||
} catch (e) {
|
||||
if (window.console)
|
||||
console.warn('passwall2 node list sort:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.pw2NodeSort = {
|
||||
init: init,
|
||||
enhanceAll: enhanceAll,
|
||||
rowVisible: rowVisible
|
||||
};
|
||||
})();
|
||||
@@ -586,11 +586,15 @@ function gen_config_server(node)
|
||||
end
|
||||
end
|
||||
|
||||
if node.protocol == "vmess" or node.protocol == "vless" then
|
||||
if node.protocol == "vless" then
|
||||
settings = {
|
||||
users = users,
|
||||
decryption = (node.decryption and node.decryption ~= "") and node.decryption or "none"
|
||||
}
|
||||
elseif node.protocol == "vmess" then
|
||||
if users then
|
||||
settings = {
|
||||
users = users,
|
||||
decryption = (node.protocol == "vless") and ((node.decryption and node.decryption ~= "") and node.decryption or "none") or nil
|
||||
users = users
|
||||
}
|
||||
end
|
||||
elseif node.protocol == "socks" then
|
||||
|
||||
@@ -22,6 +22,7 @@ if node then
|
||||
end
|
||||
-%>
|
||||
<script src="<%=resource%>/view/<%=appname%>/Sortable.min.js"></script>
|
||||
<script src="<%=resource%>/view/<%=appname%>/node_list_sort.js"></script>
|
||||
|
||||
<style>
|
||||
table th, .table .th {
|
||||
@@ -383,6 +384,89 @@ table td, .table .td {
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* node list: click-to-sort headers and the per-group filter bar */
|
||||
th[data-pw-sort] {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
th[data-pw-sort]:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.pw-sort-arrow {
|
||||
display: inline-block;
|
||||
min-width: 1em;
|
||||
margin-left: 0.25em;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.pw-sort-sub {
|
||||
font-weight: normal;
|
||||
font-size: 85%;
|
||||
opacity: 0.85;
|
||||
margin-left: 0.6em;
|
||||
cursor: pointer;
|
||||
text-decoration: underline dotted;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pw-sort-sub.pw-sort-active {
|
||||
opacity: 1;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pw-sort-note {
|
||||
margin: 0.3em 0;
|
||||
padding: 0.3em 0.6em;
|
||||
border-radius: 3px;
|
||||
background: rgba(251, 154, 5, 0.15);
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
tr.pw-row-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.pw-filter {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.3em 0.8em;
|
||||
margin: 0.4em 0;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.pw-filter label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pw-filter select,
|
||||
.pw-filter input[type="search"],
|
||||
.pw-filter .cbi-button {
|
||||
width: auto !important;
|
||||
min-width: 6em;
|
||||
max-width: 22em;
|
||||
height: auto;
|
||||
padding: 0.15em 0.3em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pw-filter .pw-filter-count {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.pw-filter.pw-filter-on .pw-filter-count {
|
||||
font-weight: bold;
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
<% if api.is_js_luci() then -%>
|
||||
var cbi_t = [];
|
||||
@@ -538,15 +622,29 @@ table td, .table .td {
|
||||
}
|
||||
}
|
||||
|
||||
function node_row_visible(el) {
|
||||
if (window.pw2NodeSort && typeof pw2NodeSort.rowVisible === "function")
|
||||
return pw2NodeSort.rowVisible(el);
|
||||
var row = el && el.closest ? el.closest("tr") : null;
|
||||
return !row || !row.classList.contains("pw-row-hidden");
|
||||
}
|
||||
|
||||
function set_select_all_state(sectionChecked) {
|
||||
var visibleContainer = document.querySelector('#cbi-<%=appname%>-nodes > .cbi-tabcontainer[style*="display:block"], #cbi-<%=appname%>-nodes > .cbi-tabcontainer[style*="display: block"]');
|
||||
if (!visibleContainer) return;
|
||||
var nodes = visibleContainer.getElementsByClassName("nodes_select");
|
||||
var selectAllChk = visibleContainer.querySelector(".nodes_select_all");
|
||||
var selectAllBtn = document.getElementById("select_all_btn");
|
||||
var visibleCount = 0;
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
if (!node_row_visible(nodes[i])) {
|
||||
nodes[i].checked = false;
|
||||
continue;
|
||||
}
|
||||
nodes[i].checked = sectionChecked;
|
||||
visibleCount++;
|
||||
}
|
||||
sectionChecked = sectionChecked && visibleCount > 0;
|
||||
if (selectAllChk) {
|
||||
selectAllChk.checked = sectionChecked;
|
||||
selectAllChk.title = sectionChecked ? "<%:DeSelect all%>" : "<%:Select all%>";
|
||||
@@ -574,10 +672,13 @@ table td, .table .td {
|
||||
var selectAllChk = visibleContainer.querySelector(".nodes_select_all");
|
||||
var selectAllBtn = document.getElementById("select_all_btn");
|
||||
var checkedCount = 0;
|
||||
var visibleCount = 0;
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
if (!node_row_visible(nodes[i])) continue;
|
||||
visibleCount++;
|
||||
if (nodes[i].checked) checkedCount++;
|
||||
}
|
||||
var allChecked = checkedCount === nodes.length;
|
||||
var allChecked = visibleCount > 0 && checkedCount === visibleCount;
|
||||
var title = allChecked ? "<%:DeSelect all%>" : "<%:Select all%>";
|
||||
var onclickFunc = allChecked ? "dechecked_all_node(this)" : "checked_all_node(this)";
|
||||
|
||||
@@ -600,7 +701,7 @@ table td, .table .td {
|
||||
var doms = visibleContainer.getElementsByClassName("nodes_select");
|
||||
if (doms && doms.length > 0) {
|
||||
for (var i = 0 ; i < doms.length; i++) {
|
||||
if (doms[i].checked) {
|
||||
if (doms[i].checked && node_row_visible(doms[i])) {
|
||||
ids.push(doms[i].getAttribute("cbid"))
|
||||
}
|
||||
}
|
||||
@@ -1158,7 +1259,7 @@ table td, .table .td {
|
||||
const doms = visibleContainer.getElementsByClassName("nodes_select");
|
||||
if (doms && doms.length > 0) {
|
||||
for (let i = 0 ; i < doms.length; i++) {
|
||||
if (doms[i].checked) {
|
||||
if (doms[i].checked && node_row_visible(doms[i])) {
|
||||
ids.push(doms[i].getAttribute("cbid"))
|
||||
}
|
||||
}
|
||||
@@ -1190,7 +1291,7 @@ table td, .table .td {
|
||||
const doms = visibleContainer.getElementsByClassName("nodes_select");
|
||||
if (doms && doms.length > 0) {
|
||||
for (let i = 0 ; i < doms.length; i++) {
|
||||
if (doms[i].checked) {
|
||||
if (doms[i].checked && node_row_visible(doms[i])) {
|
||||
ids.push(doms[i].getAttribute("cbid"))
|
||||
}
|
||||
}
|
||||
@@ -1298,8 +1399,33 @@ table td, .table .td {
|
||||
<fieldset class="cbi-section" id="node_list">
|
||||
</fieldset>
|
||||
|
||||
<div id="pw2-nodesort-i18n" data-app="<%=appname%>" style="display:none">
|
||||
<span data-k="hint"><%:Click to sort; click again to reverse. Press Save & Apply to keep the order.%></span>
|
||||
<span data-k="typeHint"><%:Sort by type or protocol, then by name%></span>
|
||||
<span data-k="addrHint"><%:Sort by server address and port%></span>
|
||||
<span data-k="origHint"><%:Restore the saved order%></span>
|
||||
<span data-k="changed"><%:The order changed on this page only. Press Save & Apply to store it on the router.%></span>
|
||||
<span data-k="restored"><%:Saved order restored.%></span>
|
||||
<span data-k="wait"><%:Automatic ping is still running (%d left). Sorting will run when it finishes.%></span>
|
||||
<span data-k="waitFail"><%:Automatic ping did not finish within 2 minutes. Sorting was cancelled. Reload the page and try again.%></span>
|
||||
<span data-k="filter"><%:Filter:%></span>
|
||||
<span data-k="all"><%:All%></span>
|
||||
<span data-k="empty"><%:(empty)%></span>
|
||||
<span data-k="type"><%:Type%></span>
|
||||
<span data-k="addr"><%:Address%></span>
|
||||
<span data-k="fWord"><%:Word in remarks%></span>
|
||||
<span data-k="search"><%:search...%></span>
|
||||
<span data-k="searchHint"><%:Show only rows that contain this text%></span>
|
||||
<span data-k="shown"><%:Shown %s of %t%></span>
|
||||
<span data-k="reset"><%:Reset%></span>
|
||||
<span data-k="resetHint"><%:Show all rows%></span>
|
||||
<span data-k="filterHint"><%:The filter only hides rows on this page and changes nothing on the router. Select all skips hidden rows.%></span>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
//init logic
|
||||
if (window.pw2NodeSort)
|
||||
pw2NodeSort.init();
|
||||
|
||||
function get_remarks_name(o) {
|
||||
let str = "";
|
||||
@@ -1499,8 +1625,10 @@ table td, .table .td {
|
||||
}
|
||||
|
||||
get_now_use_node();
|
||||
|
||||
|
||||
pingAllNodes();
|
||||
if (window.pw2NodeSort)
|
||||
pw2NodeSort.enhanceAll();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2530,3 +2530,54 @@ msgstr "بهروزرسانی ناموفق بود"
|
||||
|
||||
msgid "Unknown error"
|
||||
msgstr "خطای ناشناخته"
|
||||
|
||||
msgid "Click to sort; click again to reverse. Press Save & Apply to keep the order."
|
||||
msgstr "برای مرتبسازی کلیک کنید؛ کلیک دوباره جهت را عوض میکند. برای نگه داشتن ترتیب، ذخیره و اعمال را بزنید."
|
||||
|
||||
msgid "Sort by type or protocol, then by name"
|
||||
msgstr "مرتبسازی بر اساس نوع یا پروتکل، سپس نام"
|
||||
|
||||
msgid "Sort by server address and port"
|
||||
msgstr "مرتبسازی بر اساس آدرس سرور و درگاه"
|
||||
|
||||
msgid "Restore the saved order"
|
||||
msgstr "بازگرداندن ترتیب ذخیرهشده"
|
||||
|
||||
msgid "The order changed on this page only. Press Save & Apply to store it on the router."
|
||||
msgstr "ترتیب فقط در این صفحه عوض شده است. برای نوشتن آن روی روتر، ذخیره و اعمال را بزنید."
|
||||
|
||||
msgid "Saved order restored."
|
||||
msgstr "ترتیب ذخیرهشده بازگردانده شد."
|
||||
|
||||
msgid "Automatic ping is still running (%d left). Sorting will run when it finishes."
|
||||
msgstr "بررسی خودکار هنوز در حال اجراست (%d باقی مانده). مرتبسازی بلافاصله پس از پایان آن انجام میشود."
|
||||
|
||||
msgid "Automatic ping did not finish within 2 minutes. Sorting was cancelled. Reload the page and try again."
|
||||
msgstr "بررسی خودکار ظرف 2 دقیقه تمام نشد. مرتبسازی لغو شد. صفحه را دوباره بارگذاری کنید و دوباره تلاش کنید."
|
||||
|
||||
msgid "Filter:"
|
||||
msgstr "فیلتر:"
|
||||
|
||||
msgid "(empty)"
|
||||
msgstr "(خالی)"
|
||||
|
||||
msgid "Word in remarks"
|
||||
msgstr "کلمه در توضیحات"
|
||||
|
||||
msgid "search..."
|
||||
msgstr "جستجو..."
|
||||
|
||||
msgid "Show only rows that contain this text"
|
||||
msgstr "فقط ردیفهایی را نشان بده که این متن را دارند"
|
||||
|
||||
msgid "Shown %s of %t"
|
||||
msgstr "نمایش %s از %t"
|
||||
|
||||
msgid "Reset"
|
||||
msgstr "بازنشانی"
|
||||
|
||||
msgid "Show all rows"
|
||||
msgstr "نمایش همه ردیفها"
|
||||
|
||||
msgid "The filter only hides rows on this page and changes nothing on the router. Select all skips hidden rows."
|
||||
msgstr "فیلتر فقط ردیفها را در این صفحه پنهان میکند و چیزی را روی روتر تغییر نمیدهد. انتخاب همه، ردیفهای پنهان را رد میکند."
|
||||
@@ -2531,3 +2531,54 @@ msgstr "Ошибка обновления"
|
||||
|
||||
msgid "Unknown error"
|
||||
msgstr "Неизвестная ошибка"
|
||||
|
||||
msgid "Click to sort; click again to reverse. Press Save & Apply to keep the order."
|
||||
msgstr "Нажмите для сортировки; повторное нажатие меняет направление. Чтобы сохранить порядок, нажмите «Сохранить и применить»."
|
||||
|
||||
msgid "Sort by type or protocol, then by name"
|
||||
msgstr "Сортировать по типу или протоколу, затем по имени"
|
||||
|
||||
msgid "Sort by server address and port"
|
||||
msgstr "Сортировать по адресу сервера и порту"
|
||||
|
||||
msgid "Restore the saved order"
|
||||
msgstr "Вернуть сохранённый порядок"
|
||||
|
||||
msgid "The order changed on this page only. Press Save & Apply to store it on the router."
|
||||
msgstr "Порядок изменён только на этой странице. Чтобы записать его на роутер, нажмите «Сохранить и применить»."
|
||||
|
||||
msgid "Saved order restored."
|
||||
msgstr "Сохранённый порядок восстановлен."
|
||||
|
||||
msgid "Automatic ping is still running (%d left). Sorting will run when it finishes."
|
||||
msgstr "Автопроверка ещё выполняется (осталось %d). Сортировка запустится сразу после её окончания."
|
||||
|
||||
msgid "Automatic ping did not finish within 2 minutes. Sorting was cancelled. Reload the page and try again."
|
||||
msgstr "Автопроверка не завершилась за 2 минуты. Сортировка отменена. Обновите страницу и попробуйте снова."
|
||||
|
||||
msgid "Filter:"
|
||||
msgstr "Фильтр:"
|
||||
|
||||
msgid "(empty)"
|
||||
msgstr "(пусто)"
|
||||
|
||||
msgid "Word in remarks"
|
||||
msgstr "Слово в описании"
|
||||
|
||||
msgid "search..."
|
||||
msgstr "поиск..."
|
||||
|
||||
msgid "Show only rows that contain this text"
|
||||
msgstr "Показать только строки, содержащие этот текст"
|
||||
|
||||
msgid "Shown %s of %t"
|
||||
msgstr "Показано %s из %t"
|
||||
|
||||
msgid "Reset"
|
||||
msgstr "Сбросить"
|
||||
|
||||
msgid "Show all rows"
|
||||
msgstr "Показать все строки"
|
||||
|
||||
msgid "The filter only hides rows on this page and changes nothing on the router. Select all skips hidden rows."
|
||||
msgstr "Фильтр только скрывает строки на этой странице и ничего не меняет на роутере. «Выделить все» пропускает скрытые строки."
|
||||
@@ -2517,3 +2517,53 @@ msgstr "更新失败"
|
||||
msgid "Unknown error"
|
||||
msgstr "未知错误"
|
||||
|
||||
msgid "Click to sort; click again to reverse. Press Save & Apply to keep the order."
|
||||
msgstr "点击表头排序,再次点击切换方向。按保存并应用可保留顺序。"
|
||||
|
||||
msgid "Sort by type or protocol, then by name"
|
||||
msgstr "先按类型或协议排序,再按名称排序"
|
||||
|
||||
msgid "Sort by server address and port"
|
||||
msgstr "按服务器地址和端口排序"
|
||||
|
||||
msgid "Restore the saved order"
|
||||
msgstr "恢复已保存的顺序"
|
||||
|
||||
msgid "The order changed on this page only. Press Save & Apply to store it on the router."
|
||||
msgstr "顺序仅在本页改变。按保存并应用才会写入路由器。"
|
||||
|
||||
msgid "Saved order restored."
|
||||
msgstr "已恢复保存的顺序。"
|
||||
|
||||
msgid "Automatic ping is still running (%d left). Sorting will run when it finishes."
|
||||
msgstr "自动探测仍在进行(剩余 %d)。结束后会立即排序。"
|
||||
|
||||
msgid "Automatic ping did not finish within 2 minutes. Sorting was cancelled. Reload the page and try again."
|
||||
msgstr "自动探测在 2 分钟内未结束,已取消排序。请刷新页面后重试。"
|
||||
|
||||
msgid "Filter:"
|
||||
msgstr "筛选:"
|
||||
|
||||
msgid "(empty)"
|
||||
msgstr "(空)"
|
||||
|
||||
msgid "Word in remarks"
|
||||
msgstr "备注关键词"
|
||||
|
||||
msgid "search..."
|
||||
msgstr "搜索..."
|
||||
|
||||
msgid "Show only rows that contain this text"
|
||||
msgstr "只显示包含此文本的行"
|
||||
|
||||
msgid "Shown %s of %t"
|
||||
msgstr "显示 %s / %t"
|
||||
|
||||
msgid "Reset"
|
||||
msgstr "重置"
|
||||
|
||||
msgid "Show all rows"
|
||||
msgstr "显示全部行"
|
||||
|
||||
msgid "The filter only hides rows on this page and changes nothing on the router. Select all skips hidden rows."
|
||||
msgstr "筛选只在本页隐藏行,不会改动路由器上的设置。全选会跳过隐藏行。"
|
||||
@@ -2522,3 +2522,54 @@ msgstr "更新失敗"
|
||||
|
||||
msgid "Unknown error"
|
||||
msgstr "未知錯誤"
|
||||
|
||||
msgid "Click to sort; click again to reverse. Press Save & Apply to keep the order."
|
||||
msgstr "點擊表頭排序,再次點擊切換方向。按儲存並套用可保留順序。"
|
||||
|
||||
msgid "Sort by type or protocol, then by name"
|
||||
msgstr "先按類型或協定排序,再按名稱排序"
|
||||
|
||||
msgid "Sort by server address and port"
|
||||
msgstr "按伺服器位址和埠排序"
|
||||
|
||||
msgid "Restore the saved order"
|
||||
msgstr "還原已儲存的順序"
|
||||
|
||||
msgid "The order changed on this page only. Press Save & Apply to store it on the router."
|
||||
msgstr "順序只在本頁改變。按儲存並套用才會寫入路由器。"
|
||||
|
||||
msgid "Saved order restored."
|
||||
msgstr "已還原儲存的順序。"
|
||||
|
||||
msgid "Automatic ping is still running (%d left). Sorting will run when it finishes."
|
||||
msgstr "自動探測仍在進行(剩餘 %d)。結束後會立即排序。"
|
||||
|
||||
msgid "Automatic ping did not finish within 2 minutes. Sorting was cancelled. Reload the page and try again."
|
||||
msgstr "自動探測在 2 分鐘內未結束,已取消排序。請重新整理頁面後再試。"
|
||||
|
||||
msgid "Filter:"
|
||||
msgstr "篩選:"
|
||||
|
||||
msgid "(empty)"
|
||||
msgstr "(空)"
|
||||
|
||||
msgid "Word in remarks"
|
||||
msgstr "備註關鍵詞"
|
||||
|
||||
msgid "search..."
|
||||
msgstr "搜尋..."
|
||||
|
||||
msgid "Show only rows that contain this text"
|
||||
msgstr "只顯示包含此文字的列"
|
||||
|
||||
msgid "Shown %s of %t"
|
||||
msgstr "顯示 %s / %t"
|
||||
|
||||
msgid "Reset"
|
||||
msgstr "重設"
|
||||
|
||||
msgid "Show all rows"
|
||||
msgstr "顯示全部列"
|
||||
|
||||
msgid "The filter only hides rows on this page and changes nothing on the router. Select all skips hidden rows."
|
||||
msgstr "篩選只在本頁隱藏列,不會改動路由器上的設定。全選會跳過隱藏列。"
|
||||
@@ -4,7 +4,7 @@ LUCI_TITLE:=luci-app-ssr-plus
|
||||
LUCI_PKGARCH:=all
|
||||
PKG_NAME:=luci-app-ssr-plus
|
||||
PKG_VERSION:=198
|
||||
PKG_RELEASE:=96
|
||||
PKG_RELEASE:=97
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
|
||||
|
||||
@@ -716,6 +716,10 @@ o = s:option(DynamicList, "hysteria2_realm_stun", translate("Realm STUN"))
|
||||
o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" }
|
||||
o:depends("hysteria2_realms", true)
|
||||
|
||||
o = s:option(Flag, "hysteria2_realm_upnp", translate("Enable UPnP/NAT-PMP"), translate("Enable UPnP/NAT-PMP port mapping on your gateway to improve hole punching success."))
|
||||
o.default = "0"
|
||||
o:depends("hysteria2_realms", true)
|
||||
|
||||
o = s:option(Value, "hy2_auth", translate("Users Authentication"))
|
||||
o:depends("type", "hysteria2")
|
||||
o:depends({type = "v2ray", v2ray_protocol = "hysteria2"})
|
||||
|
||||
@@ -3900,3 +3900,9 @@ msgstr "Naive ECH 配置列表(Base64)"
|
||||
|
||||
msgid "Naive transport"
|
||||
msgstr "Naive 传输协议"
|
||||
|
||||
msgid "Enable UPnP/NAT-PMP"
|
||||
msgstr "启用 UPnP/NAT-PMP"
|
||||
|
||||
msgid "Enable UPnP/NAT-PMP port mapping on your gateway to improve hole punching success."
|
||||
msgstr "在网关上启用 UPnP/NAT-PMP 端口映射以提升打洞成功率。"
|
||||
@@ -253,13 +253,19 @@ force_dns2tcp_fallback() {
|
||||
|
||||
is_builtin_dns_active() {
|
||||
local global_server_type
|
||||
local enable_mihomo
|
||||
global_server_type="$(uci_get_by_name "$GLOBAL_SERVER" type)"
|
||||
case "$global_server_type" in
|
||||
clash|tuic|ss)
|
||||
ps_list | grep -v "grep" | grep -q "ssr-retcp"
|
||||
;;
|
||||
v2ray)
|
||||
ps_list | grep -v "grep" | grep -Eq "$TMP_PATH/.*ssr-retcp\\.(json|yaml)"
|
||||
enable_mihomo="$(uci_get_by_type server_subscribe enable_mihomo "")"
|
||||
if [ "$enable_mihomo" = "1" ]; then
|
||||
ps_list | grep -v "grep" | grep -q "ssr-retcp.*v2ray-*"
|
||||
else
|
||||
ps_list | grep -v "grep" | grep -Eq "$TMP_PATH/.*ssr-retcp\\.(json|yaml)"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
|
||||
@@ -722,21 +722,39 @@ Xray.outbounds = {
|
||||
local PT = server.v2ray_protocol
|
||||
local TP = server.transport
|
||||
if TP == "kcp" then
|
||||
local map = {none = "none", srtp = "header-srtp", utp = "header-utp", ["wechat-video"] = "header-wechat",
|
||||
dtls = "header-dtls", wireguard = "header-wireguard", dns = "header-dns"}
|
||||
local udp = {}
|
||||
if server.kcp_guise and server.kcp_guise ~= "none" then
|
||||
local g = { type = map[server.kcp_guise] }
|
||||
if server.kcp_guise == "dns" and server.kcp_domain and server.kcp_domain ~= "" then
|
||||
g.settings = { domain = server.kcp_domain }
|
||||
if xray_version_val < 260601 then
|
||||
local map = {none = "none", srtp = "header-srtp", utp = "header-utp", ["wechat-video"] = "header-wechat",
|
||||
dtls = "header-dtls", wireguard = "header-wireguard", dns = "header-dns"}
|
||||
if server.kcp_guise and server.kcp_guise ~= "none" then
|
||||
local g = { type = map[server.kcp_guise] }
|
||||
if server.kcp_guise == "dns" and server.kcp_domain and server.kcp_domain ~= "" then
|
||||
g.settings = { domain = server.kcp_domain }
|
||||
end
|
||||
udp[#udp+1] = g
|
||||
end
|
||||
udp[#udp+1] = g
|
||||
local c = { type = (server.seed and server.seed ~= "") and "mkcp-aes128gcm" or "mkcp-original" }
|
||||
if server.seed and server.seed ~= "" then
|
||||
c.settings = { password = server.seed }
|
||||
end
|
||||
udp[#udp+1] = c
|
||||
else
|
||||
local map = {none = "none", srtp = "srtp", utp = "utp", ["wechat-video"] = "wechat",
|
||||
dtls = "dtls", wireguard = "wireguard", dns = "dns"}
|
||||
if server.kcp_guise and server.kcp_guise ~= "none" then
|
||||
local g = { type = "mkcp-legacy" }
|
||||
g.settings = { header = map[server.kcp_guise] }
|
||||
if server.kcp_guise == "dns" and server.kcp_domain and server.kcp_domain ~= "" then
|
||||
g.settings.value = server.kcp_domain
|
||||
end
|
||||
udp[#udp+1] = g
|
||||
end
|
||||
local s = { type = "mkcp-legacy" }
|
||||
if server.seed and server.seed ~= "" then
|
||||
s.settings = { value = server.seed }
|
||||
end
|
||||
udp[#udp+1] = s
|
||||
end
|
||||
local c = { type = (server.seed and server.seed ~= "") and "mkcp-aes128gcm" or "mkcp-original" }
|
||||
if server.seed and server.seed ~= "" then
|
||||
c.settings = { password = server.seed }
|
||||
end
|
||||
udp[#udp+1] = c
|
||||
finalmask.udp = udp
|
||||
elseif PT == "hysteria2" then
|
||||
local udp = {}
|
||||
@@ -769,7 +787,8 @@ Xray.outbounds = {
|
||||
type = "realm",
|
||||
settings = {
|
||||
url = url,
|
||||
stunServers = stun
|
||||
stunServers = stun,
|
||||
portMapping = (node.hysteria2_realm_upnp == "1") and { enabled = true } or nil
|
||||
}
|
||||
}
|
||||
udp[#udp+1] = r
|
||||
@@ -782,7 +801,7 @@ Xray.outbounds = {
|
||||
brutalUp = up > 0 and (up .. "mbps") or nil,
|
||||
brutalDown = down > 0 and (down .. "mbps") or nil,
|
||||
udpHop = (server.flag_port_hopping == "1") and {
|
||||
ports = string.gsub(server.port_range, ":", "-"),
|
||||
ports = server.port_range and string.gsub(server.port_range, ":", "-"),
|
||||
interval = (function(v)
|
||||
if not v then return 30 end
|
||||
if v:find("-", 1, true) then
|
||||
|
||||
@@ -2,6 +2,44 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [2.7.3-r3] - 安装命令改用通配文件名,翻译包并入对应包管理器代码块(仅文档)
|
||||
|
||||
`README.md` / `README.en.md` 的安装步骤不再写死版本号:opkg 用
|
||||
`luci-app-substore_*.ipk`,apk 用 `luci-app-substore-*.apk`,避免每次升
|
||||
PKG_RELEASE 后文档里的文件名立刻过期。简体中文翻译包的安装命令移入同一代码块,
|
||||
用注释标明「中文界面需额外安装翻译包」;原先单独的翻译包安装代码块随之删除,
|
||||
只保留 `PKG_PO_VERSION` 的说明。
|
||||
|
||||
### 影响
|
||||
|
||||
**仅文档,无代码、无测试变更。** PKG_RELEASE 按版本约定 `2.7.3-r2` → `2.7.3-r3`,
|
||||
README 与 `docs/INSTALL.md` 的版本号同步更新。全套 **59 个测试文件、179 条断言、
|
||||
0 失败**(未改动被测代码,跑一遍确认无意外)。
|
||||
|
||||
## [2.7.3-r2] - README 章节重排:安装提前,构建并入「从源码编译」(仅文档)
|
||||
|
||||
`README.md` / `README.en.md` 的「安装」整体移到「功能特性」之前,让拿到包的人先看到
|
||||
安装方式、再看能力清单;「安装」末尾新增 **从源码编译** 小节,原先独立成节的
|
||||
「构建」并入其中并删除。
|
||||
|
||||
合并时修正了原「构建」段里一条**不存在的 make 目标**:
|
||||
`make package/luci-i18n-substore-zh-cn/compile` —— make 的目标按目录生成、不按包名,
|
||||
`.github/workflows/build.yml` 与 `docs/BUILD.md` 均已写明这一点。新小节改为经 CI
|
||||
验证过的做法:翻译包与主包同属 `package/luci-app-substore`,随同一条 `compile`
|
||||
命令产出;它是 HIDDEN 包(无 Kconfig prompt),编译前需显式打开语言符号:
|
||||
|
||||
```sh
|
||||
echo "CONFIG_LUCI_LANG_zh_Hans=y" >> .config
|
||||
make defconfig
|
||||
```
|
||||
|
||||
同时把 README 与 `docs/INSTALL.md` 中滞后的版本号 `2.7.2-r1` 同步为 `2.7.3-r2`。
|
||||
|
||||
### 影响
|
||||
|
||||
**仅文档,无代码、无测试变更。** PKG_RELEASE 按版本约定 `2.7.3-r1` → `-r2`。全套
|
||||
**59 个测试文件、179 条断言、0 失败**(未改动被测代码,跑一遍确认无意外)。
|
||||
|
||||
## [2.7.3-r1] - 结清「附:P2 修复范围」:改写为历史范围定义(仅文档)
|
||||
|
||||
`docs/LEGACY_ISSUES.md` 末尾的「附:P2 修复范围」一节,是 P2 **开工时**写下的范围
|
||||
|
||||
@@ -10,7 +10,7 @@ PKG_NAME:=luci-app-substore
|
||||
# 版本约定:每次提交 PKG_RELEASE +1(2.1.2-r1 ~ r9);
|
||||
# 达到 r10 时 PKG_VERSION 末位 +1(2.1.2 -> 2.1.3),PKG_RELEASE 重置为 1。
|
||||
PKG_VERSION:=2.7.3
|
||||
PKG_RELEASE:=24
|
||||
PKG_RELEASE:=25
|
||||
|
||||
LUCI_DEPENDS:=+luci-lua-runtime +luci-compat
|
||||
|
||||
|
||||
@@ -8,6 +8,67 @@ group them, then re-emit them in a format your client can consume.
|
||||
|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
> The version in the package name must match `PKG_VERSION` / `PKG_RELEASE` in the
|
||||
> [Makefile](Makefile) (currently `2.7.3-r3`).
|
||||
|
||||
**Minimum supported: OpenWrt / ImmortalWrt 23.05** (older releases are out of scope).
|
||||
|
||||
opkg (OpenWrt / ImmortalWrt 24.10 and earlier):
|
||||
|
||||
```bash
|
||||
opkg install luci-app-substore_*.ipk
|
||||
# Simplified Chinese UI needs the translation package as well
|
||||
opkg install luci-i18n-substore-zh-cn_*.ipk
|
||||
```
|
||||
|
||||
OpenWrt 25.12 and newer (apk):
|
||||
|
||||
```bash
|
||||
apk add --allow-untrusted luci-app-substore-*.apk
|
||||
# Simplified Chinese UI needs the translation package as well
|
||||
apk add --allow-untrusted luci-i18n-substore-zh-cn-*.apk
|
||||
```
|
||||
|
||||
> The main package ships no translations, so the UI stays English without the
|
||||
> translation package. Its version comes from the `po/` commit time (LuCI's
|
||||
> `PKG_PO_VERSION`), so it does not share the main package's version; install it by
|
||||
> filename wildcard. Set the LuCI language to Simplified Chinese afterwards — the
|
||||
> package's uci-defaults adds it to `luci.languages` automatically.
|
||||
|
||||
Then open LuCI: **Services → Subscriptions**.
|
||||
|
||||
### Building from source
|
||||
|
||||
In an OpenWrt / ImmortalWrt source tree matching the target firmware:
|
||||
|
||||
```sh
|
||||
cd openwrt
|
||||
./scripts/feeds update -a
|
||||
./scripts/feeds install -a
|
||||
git clone https://github.com/Arthur97172/luci-app-substore.git package/luci-app-substore
|
||||
make menuconfig # LuCI → Applications → luci-app-substore
|
||||
make package/luci-app-substore/compile V=s
|
||||
```
|
||||
|
||||
The Simplified Chinese package `luci-i18n-substore-zh-cn` is generated by `luci.mk`
|
||||
from `po/zh_Hans/` and lives in the same `package/luci-app-substore` directory, so the
|
||||
same `compile` command produces it. It is a HIDDEN package (no Kconfig prompt), though,
|
||||
so enable its language symbol before building or it will not be selected:
|
||||
|
||||
```sh
|
||||
echo "CONFIG_LUCI_LANG_zh_Hans=y" >> .config
|
||||
make defconfig
|
||||
```
|
||||
|
||||
> Note that `make package/luci-i18n-substore-zh-cn/compile` is **not a valid target** —
|
||||
> make targets are per directory, not per package name (see
|
||||
> [docs/BUILD.md](docs/BUILD.md)).
|
||||
|
||||
The `.ipk` (or `.apk` on apk builds) is produced under `bin/packages/.../`; install
|
||||
it as described above.
|
||||
|
||||
## Features
|
||||
|
||||
**Subscription management**
|
||||
@@ -85,40 +146,6 @@ group them, then re-emit them in a format your client can consume.
|
||||
**LuCI interface & i18n**
|
||||
- English by default, Simplified Chinese when the runtime language is `zh-cn`
|
||||
|
||||
## Installation
|
||||
|
||||
> The version in the package name must match `PKG_VERSION` / `PKG_RELEASE` in the
|
||||
> [Makefile](Makefile) (currently `2.7.2-r1`).
|
||||
|
||||
**Minimum supported: OpenWrt / ImmortalWrt 23.05** (older releases are out of scope).
|
||||
|
||||
opkg (OpenWrt / ImmortalWrt 24.10 and earlier):
|
||||
|
||||
```bash
|
||||
opkg install luci-app-substore-2.7.2-r1.ipk
|
||||
```
|
||||
|
||||
apk (OpenWrt / ImmortalWrt 25.12+):
|
||||
|
||||
```bash
|
||||
apk add --allow-untrusted luci-app-substore-2.7.2-r1.apk
|
||||
```
|
||||
|
||||
The Simplified Chinese UI needs a **separately installed** translation package —
|
||||
the main package ships no translations, so the UI stays English without it:
|
||||
|
||||
```bash
|
||||
opkg install luci-i18n-substore-zh-cn_*.ipk # 24.10 and earlier
|
||||
apk add --allow-untrusted luci-i18n-substore-zh-cn_*.apk # 25.12+
|
||||
```
|
||||
|
||||
> Its version comes from the `po/` commit time (LuCI's `PKG_PO_VERSION`), so it
|
||||
> does not share the main package's `2.7.2-r1`; install it by filename wildcard.
|
||||
> Set the LuCI language to Simplified Chinese afterwards — the package's
|
||||
> uci-defaults adds it to `luci.languages` automatically.
|
||||
|
||||
Then open LuCI: **Services → Subscriptions**.
|
||||
|
||||
## Usage
|
||||
|
||||
1. **Add a subscription** — paste the subscription URL; optionally set up a
|
||||
@@ -208,21 +235,6 @@ oversight; the reasoning for each is in
|
||||
- [CHANGELOG.md](CHANGELOG.md) — changelog
|
||||
- [docs/LEGACY_ISSUES.md](docs/LEGACY_ISSUES.md) — known unfixed issues (pending decision)
|
||||
|
||||
## Building
|
||||
|
||||
Place the package under an OpenWrt / ImmortalWrt SDK or source tree matching the
|
||||
target firmware, then:
|
||||
|
||||
```bash
|
||||
cp -r luci-app-substore <openwrt-tree>/package/
|
||||
make package/luci-app-substore/compile V=s
|
||||
# Simplified Chinese is a separate package (luci.mk auto-generates it from
|
||||
# po/zh_Hans/), so it must be compiled on its own:
|
||||
make package/luci-i18n-substore-zh-cn/compile V=s
|
||||
```
|
||||
|
||||
The `.ipk` (or `.apk` on apk builds) is produced under `bin/packages/.../`.
|
||||
|
||||
## Development & testing
|
||||
|
||||
The core is plain Lua 5.1 with no `luci.*` dependency, so it is unit-testable
|
||||
|
||||
+56
-45
@@ -10,6 +10,62 @@
|
||||
|
||||

|
||||
|
||||
## 安装
|
||||
|
||||
> 包名中的版本号必须与 [Makefile](Makefile) 的 `PKG_VERSION` / `PKG_RELEASE` 保持一致
|
||||
> (当前 `2.7.3-r3`)。
|
||||
|
||||
**最低支持 OpenWrt / ImmortalWrt 23.05**(更早的版本不在支持范围内)。
|
||||
|
||||
opkg(OpenWrt / ImmortalWrt 24.10 及更早):
|
||||
|
||||
```bash
|
||||
opkg install luci-app-substore_*.ipk
|
||||
# 中文界面需额外安装翻译包
|
||||
opkg install luci-i18n-substore-zh-cn_*.ipk
|
||||
```
|
||||
|
||||
OpenWrt 25.12 及更新版本(apk):
|
||||
|
||||
```bash
|
||||
apk add --allow-untrusted luci-app-substore-*.apk
|
||||
# 中文界面需额外安装翻译包
|
||||
apk add --allow-untrusted luci-i18n-substore-zh-cn-*.apk
|
||||
```
|
||||
|
||||
> 主包不含译文,未装翻译包时界面为英文。翻译包的版本号由 `po/` 目录的提交时间推导
|
||||
> (LuCI 的 `PKG_PO_VERSION`),与主包不同名,按文件名通配安装即可。装好后 LuCI
|
||||
> 语言设为「简体中文」即生效(该包的 uci-defaults 会自动把它加进 `luci.languages`)。
|
||||
|
||||
然后在 LuCI 菜单打开:**服务 → 订阅**。
|
||||
|
||||
### 从源码编译
|
||||
|
||||
在版本与目标固件匹配的 OpenWrt / ImmortalWrt 源码树中:
|
||||
|
||||
```sh
|
||||
cd openwrt
|
||||
./scripts/feeds update -a
|
||||
./scripts/feeds install -a
|
||||
git clone https://github.com/Arthur97172/luci-app-substore.git package/luci-app-substore
|
||||
make menuconfig # LuCI → Applications → luci-app-substore
|
||||
make package/luci-app-substore/compile V=s
|
||||
```
|
||||
|
||||
简体中文翻译包 `luci-i18n-substore-zh-cn` 由 `luci.mk` 按 `po/zh_Hans/` 自动生成,
|
||||
与主包同属 `package/luci-app-substore` 目录,随同一条 `compile` 命令一并产出。但它是
|
||||
HIDDEN 包(无 Kconfig prompt),编译前要显式打开语言符号,否则不会被选中:
|
||||
|
||||
```sh
|
||||
echo "CONFIG_LUCI_LANG_zh_Hans=y" >> .config
|
||||
make defconfig
|
||||
```
|
||||
|
||||
> 注意**不存在** `make package/luci-i18n-substore-zh-cn/compile` 这个目标 —— make 的
|
||||
> 目标按目录生成,不按包名(详见 [docs/BUILD.md](docs/BUILD.md))。
|
||||
|
||||
`.ipk`(或 apk 构建下的 `.apk`)生成于 `bin/packages/.../` 下,按上面的方式安装即可。
|
||||
|
||||
## 功能特性
|
||||
|
||||
**订阅管理**
|
||||
@@ -72,38 +128,6 @@
|
||||
**LuCI 界面与国际化**
|
||||
- 默认英文,运行时语言为 `zh-cn` 时自动显示简体中文
|
||||
|
||||
## 安装
|
||||
|
||||
> 包名中的版本号必须与 [Makefile](Makefile) 的 `PKG_VERSION` / `PKG_RELEASE` 保持一致
|
||||
> (当前 `2.7.2-r1`)。
|
||||
|
||||
**最低支持 OpenWrt / ImmortalWrt 23.05**(更早的版本不在支持范围内)。
|
||||
|
||||
opkg(OpenWrt / ImmortalWrt 24.10 及更早):
|
||||
|
||||
```bash
|
||||
opkg install luci-app-substore-2.7.2-r1.ipk
|
||||
```
|
||||
|
||||
apk(OpenWrt / ImmortalWrt 25.12+):
|
||||
|
||||
```bash
|
||||
apk add --allow-untrusted luci-app-substore-2.7.2-r1.apk
|
||||
```
|
||||
|
||||
简体中文界面需要**单独安装**翻译包 —— 主包不含译文,未装时界面为英文:
|
||||
|
||||
```bash
|
||||
opkg install luci-i18n-substore-zh-cn_*.ipk # 24.10 及更早
|
||||
apk add --allow-untrusted luci-i18n-substore-zh-cn_*.apk # 25.12+
|
||||
```
|
||||
|
||||
> 翻译包的版本号由 `po/` 目录的提交时间推导(LuCI 的 `PKG_PO_VERSION`),
|
||||
> 与主包的 `2.7.2-r1` 不同名,按文件名通配安装即可。装好后 LuCI 语言设为
|
||||
> 「简体中文」即生效(该包的 uci-defaults 会自动把它加进 `luci.languages`)。
|
||||
|
||||
然后在 LuCI 菜单打开:**服务 → 订阅**。
|
||||
|
||||
## 使用方法
|
||||
|
||||
1. **添加订阅** —— 粘贴订阅 URL;可选的按订阅 cron 定时、规则或下载代理。
|
||||
@@ -180,19 +204,6 @@ apk add --allow-untrusted luci-i18n-substore-zh-cn_*.apk # 25.12+
|
||||
- [CHANGELOG.md](CHANGELOG.md) — 更新日志
|
||||
- [docs/LEGACY_ISSUES.md](docs/LEGACY_ISSUES.md) — 遗留缺陷汇总(待决定修复方案)
|
||||
|
||||
## 构建
|
||||
|
||||
将本包放入与目标固件版本匹配的 OpenWrt / ImmortalWrt SDK 或源码树:
|
||||
|
||||
```bash
|
||||
cp -r luci-app-substore <openwrt-tree>/package/
|
||||
make package/luci-app-substore/compile V=s
|
||||
# 简体中文翻译是独立包(luci.mk 按 po/zh_Hans/ 自动生成),需要单独编译:
|
||||
make package/luci-i18n-substore-zh-cn/compile V=s
|
||||
```
|
||||
|
||||
`.ipk`(或 apk 构建下的 `.apk`)生成于 `bin/packages/.../` 下。
|
||||
|
||||
## 开发与测试
|
||||
|
||||
核心逻辑为纯 Lua 5.1,不依赖 `luci.*`,无需设备即可单元测试。`tests/` 下每个测试文件自包含:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
> NOTE: stage-0 skeleton has not yet been installed/verified on a target device.
|
||||
> This document will be updated once installation is validated.
|
||||
>
|
||||
> 包名中的版本号需与 Makefile 的 `PKG_VERSION` / `PKG_RELEASE` 保持同步(当前 2.7.2-r1)。
|
||||
> 包名中的版本号需与 Makefile 的 `PKG_VERSION` / `PKG_RELEASE` 保持同步(当前 2.7.3-r3)。
|
||||
|
||||
**最低支持 OpenWrt / ImmortalWrt 23.05**(更早的版本不在支持范围内 —— 23.05 起
|
||||
LuCI 使用 ucode dispatcher,ACL 的入口级校验依赖它,见
|
||||
@@ -12,13 +12,13 @@ LuCI 使用 ucode dispatcher,ACL 的入口级校验依赖它,见
|
||||
## Install (opkg — OpenWrt / ImmortalWrt 24.10 及更早)
|
||||
|
||||
```bash
|
||||
opkg install luci-app-substore-2.7.2-r1.ipk
|
||||
opkg install luci-app-substore-2.7.3-r3.ipk
|
||||
```
|
||||
|
||||
## Install (apk — OpenWrt / ImmortalWrt 25.12+)
|
||||
|
||||
```bash
|
||||
apk add --allow-untrusted luci-app-substore-2.7.2-r1.apk
|
||||
apk add --allow-untrusted luci-app-substore-2.7.3-r3.apk
|
||||
```
|
||||
|
||||
## Install the Simplified Chinese translation (optional, separate package)
|
||||
@@ -32,7 +32,7 @@ apk add --allow-untrusted luci-i18n-substore-zh-cn_*.apk # 25.12+
|
||||
```
|
||||
|
||||
该包的版本号由 `po/` 目录的提交时间推导(LuCI 的 `PKG_PO_VERSION`),
|
||||
与主包的 `2.7.2-r1` 不同名,按文件名通配安装即可。它会安装
|
||||
与主包的 `2.7.3-r3` 不同名,按文件名通配安装即可。它会安装
|
||||
`/usr/lib/lua/luci/i18n/substore.zh-cn.lmo`,并通过 uci-defaults 把
|
||||
`zh_cn` 加进 `luci.languages`;把 LuCI 语言切到「简体中文」即生效。
|
||||
|
||||
|
||||
@@ -13,9 +13,42 @@ const callDoLogout = rpc.declare({ object: 'tailscale', method: 'do_logout' });
|
||||
const callGetSubroutes = rpc.declare({ object: 'tailscale', method: 'get_subroutes' });
|
||||
const callSetupFirewall = rpc.declare({ object: 'tailscale', method: 'setup_firewall' });
|
||||
const callGetLogs = rpc.declare({ object: 'tailscale', method: 'get_logs' });
|
||||
const callSetServiceEnabled = rpc.declare({ object: 'tailscale', method: 'set_service_enabled', params: ['enabled'] });
|
||||
const callGetFirewallStatus = rpc.declare({ object: 'tailscale', method: 'get_firewall_status' });
|
||||
let map;
|
||||
let lastDevicesStatus;
|
||||
|
||||
// uci.changes() returns an object keyed by config name; count the entries so
|
||||
// the firewall button can warn about unsaved work.
|
||||
function countPending(changes) {
|
||||
if (changes == null)
|
||||
return 0;
|
||||
if (Array.isArray(changes))
|
||||
return changes.length;
|
||||
let count = 0;
|
||||
for (const key in changes)
|
||||
count += Array.isArray(changes[key]) ? changes[key].length : 1;
|
||||
return count;
|
||||
}
|
||||
|
||||
// The ucode methods report business failures as { error: ... } in a *successful*
|
||||
// ubus reply, so rpc.declare's rejection never fires for them. Every consumer of
|
||||
// a write operation has to unwrap the reply itself; without this the UI happily
|
||||
// shows "applied" for a reply that only says { error: '...' }.
|
||||
function unwrapRpc(res, action) {
|
||||
const what = action || _('Operation');
|
||||
if (res == null)
|
||||
throw new Error(_('Empty reply from server'));
|
||||
if (res.error)
|
||||
throw new Error(String(res.error));
|
||||
if (res.success === false || res.ok === false) {
|
||||
if (res.message)
|
||||
throw new Error(String(res.message));
|
||||
throw new Error(_('%s failed').format(what));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
const tailscaleSettingsConf = [
|
||||
[form.Flag, 'service_enabled', _('Enable Tailscale Service'), _('Enable or disable the Tailscale service. When disabled, the service will be stopped and the process will be killed.'), { rmempty: false }],
|
||||
[form.ListValue, 'fw_mode', _('Firewall Mode'), _('Select the firewall backend for Tailscale to use. Requires service restart to take effect.'), {values: ['nftables','iptables'],rmempty: false}],
|
||||
@@ -78,6 +111,94 @@ function getRunningStatus() {
|
||||
});
|
||||
}
|
||||
|
||||
// Poll the background firewall job until it finishes. A poll that fails
|
||||
// (network reloading, the connection dropped for a moment) is expected and is
|
||||
// simply retried on the next tick instead of being reported as an error.
|
||||
function pollFirewallJob(runId, timeoutSec) {
|
||||
const deadline = Date.now() + (timeoutSec || 30) * 1000;
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
let lastState = '';
|
||||
let finished = false;
|
||||
|
||||
function stop() {
|
||||
if (finished)
|
||||
return false;
|
||||
finished = true;
|
||||
clearTimeout(timer);
|
||||
L.Poll.remove(poll);
|
||||
return true;
|
||||
}
|
||||
|
||||
function giveUp(message) {
|
||||
if (stop())
|
||||
reject(new Error(message));
|
||||
}
|
||||
|
||||
function check() {
|
||||
if (finished)
|
||||
return Promise.resolve();
|
||||
|
||||
if (Date.now() > deadline) {
|
||||
giveUp(_('Timed out waiting for the firewall reload'));
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return callGetFirewallStatus().then(function(res) {
|
||||
if (finished)
|
||||
return;
|
||||
|
||||
// a result from an earlier run is not this run's answer
|
||||
if (runId && res?.run_id && res.run_id !== runId)
|
||||
return;
|
||||
|
||||
const state = res?.state || 'unknown';
|
||||
|
||||
if (state === 'done') {
|
||||
if (stop())
|
||||
ui.addTimeLimitedNotification(null, [ E('p', _('Firewall configuration applied.')) ], 5000, 'info');
|
||||
resolve(res);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state === 'failed') {
|
||||
giveUp(res?.message || _('Firewall reload failed'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (state === 'busy') {
|
||||
giveUp(res?.message || _('Another firewall reload is already running'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (state !== lastState) {
|
||||
lastState = state;
|
||||
if (state === 'running' && res?.step === 'firewall')
|
||||
ui.addTimeLimitedNotification(null, [ E('p', _('Reloading firewall...')) ], 3000, 'info');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function poll() {
|
||||
if (finished)
|
||||
return Promise.resolve();
|
||||
// a dropped connection during the reload is expected: leave it to
|
||||
// the next tick, the timer below still guarantees termination
|
||||
return check().catch(function() {});
|
||||
}
|
||||
|
||||
// L.Poll skips a tick while the previous call is still pending, so a
|
||||
// request that never settles would stop the polling loop entirely.
|
||||
// The timer is what actually ends the wait.
|
||||
const timer = setTimeout(function() {
|
||||
giveUp(_('Timed out waiting for the firewall reload'));
|
||||
}, (timeoutSec || 30) * 1000);
|
||||
|
||||
L.Poll.add(poll, 2);
|
||||
poll();
|
||||
});
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
const bytes_num = parseInt(bytes, 10);
|
||||
if (isNaN(bytes_num) || bytes_num === 0) return '-';
|
||||
@@ -522,21 +643,46 @@ return view.extend({
|
||||
|
||||
const fwBtn = s.taboption('general', form.Button, '_setup_firewall', _('Auto Configure Firewall'));
|
||||
fwBtn.description = _('Essential configuration for Subnet Routing (Site-to-Site) and Exit Node features.')
|
||||
+'<br>'+_('It automatically creates the tailscale interface, sets up firewall zones for LAN <-> Tailscale forwarding,')
|
||||
+'<br>'+_('and enables Masquerading and MSS Clamping (MTU fix) to ensure stable connections.');
|
||||
+'<br>'+_('It creates the tailscale interface, adds it to a firewall zone, and sets up forwarding between LAN and Tailscale, plus Tailscale to WAN.')
|
||||
+'<br>'+_('The Tailscale to WAN forwarding is what lets clients send their internet traffic through this device as an Exit Node.')
|
||||
+'<br>'+_('Masquerading and MSS Clamping (MTU fix) are enabled to keep connections stable.');
|
||||
fwBtn.inputstyle = 'action';
|
||||
fwBtn.onclick = function() {
|
||||
return callSetupFirewall().then(function(res) {
|
||||
const msg = res?.message || _('Firewall configuration applied.');
|
||||
ui.addNotification(null, E('p', {}, msg), 'info');
|
||||
}).catch(function(err) {
|
||||
ui.addNotification(null, E('p', {}, _('Failed to configure firewall: %s').format(err?.message || err || 'Unknown error')), 'error');
|
||||
}).then(function() {
|
||||
return new Promise(function(resolve) {
|
||||
window.setTimeout(resolve, 3000);
|
||||
// The button writes network/firewall UCI directly, so staged changes
|
||||
// in the form above would be left unapplied and the user would be
|
||||
// comparing the firewall against an old config. Ask at click time
|
||||
// rather than trusting a snapshot: uci.changes() is an RPC and the
|
||||
// answer goes stale as soon as the page is saved once.
|
||||
// This only warns; blocking would misfire whenever the answer is
|
||||
// stale, and the apply below is harmless either way.
|
||||
return L.resolveDefault(uci.changes(), {}).then(function(changes) {
|
||||
if (countPending(changes) > 0) {
|
||||
ui.addNotification(null, E('p', {}, _('You have unapplied changes. Save & Apply them first so the firewall matches the form.')), 'warning');
|
||||
}
|
||||
return runSetupFirewall();
|
||||
}).catch(function() {
|
||||
return runSetupFirewall();
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
// The reload runs in the background and can briefly interrupt the
|
||||
// connection carrying this page, so the reply only confirms the commit;
|
||||
// the outcome is polled separately.
|
||||
function runSetupFirewall() {
|
||||
return callSetupFirewall().then(function(res) {
|
||||
unwrapRpc(res, _('Configuring firewall'));
|
||||
|
||||
if (!res.changed_network && !res.changed_firewall) {
|
||||
ui.addNotification(null, E('p', {}, _('Firewall configuration already applied.')), 'info');
|
||||
return null;
|
||||
}
|
||||
|
||||
ui.addTimeLimitedNotification(null, [ E('p', _('Configuration committed, reloading network and firewall. This can take a moment.')) ], 5000, 'info');
|
||||
return pollFirewallJob(res.run_id);
|
||||
}).catch(function(err) {
|
||||
ui.addNotification(null, E('p', {}, _('Failed to configure firewall: %s').format(err?.message || err?.error || err || _('Unknown error'))), 'error');
|
||||
});
|
||||
}
|
||||
|
||||
const helpTitle = s.taboption('general', form.DummyValue, '_help_title');
|
||||
helpTitle.title = _('How to enable Site-to-Site?');
|
||||
@@ -552,6 +698,29 @@ return view.extend({
|
||||
]);
|
||||
};
|
||||
|
||||
// Exit Node is a different feature with a different step that LuCI cannot
|
||||
// complete on its own: the admin console approval happens in the user's
|
||||
// Tailscale account. Without it a client reaches the LAN but gets no
|
||||
// internet, which reads as "the firewall button did not work".
|
||||
const exitHelp = s.taboption('general', form.DummyValue, '_exit_node_help');
|
||||
exitHelp.title = _('How to enable Exit Node?');
|
||||
exitHelp.render = function() {
|
||||
return E('div', { 'class': 'cbi-value', 'style': 'margin-top: 1em; border-top: 1px font-weight: bold;' }, [
|
||||
E('label', { 'class': 'cbi-value-title' }, this.title),
|
||||
E('div', { 'class': 'cbi-value-field', 'style': 'line-height: 1.6em; font-size: 95%; color: #555;' }, [
|
||||
_('1. Enable "Advertise Exit Node".'), E('br'),
|
||||
_('2. Click "Auto Configure Firewall" (it forwards Tailscale traffic to WAN).'), E('br'),
|
||||
_('3. In the Tailscale admin console, edit this device under the route settings and approve it as an Exit Node.'), E('br'),
|
||||
// No quotes around the client's control on purpose: "Exit node"
|
||||
// is a menu in the Tailscale client, not a field on this page,
|
||||
// and quoting it would send the reader to the dropdown above,
|
||||
// which selects the opposite direction.
|
||||
_('4. On the client, set this device as its exit node.'), E('br'),
|
||||
E('strong', { 'style': 'color: #d9534f;' }, _('[Important] Step 3 cannot be done here: it needs your Tailscale account.'))
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
// Create the account settings
|
||||
s.tab('account', _('Account Settings'));
|
||||
defTabOpts(s, 'account', accountConf, { optional: false });
|
||||
@@ -617,7 +786,7 @@ return view.extend({
|
||||
doc.body.innerHTML =
|
||||
'<h2>' + _('Error') + '</h2>' +
|
||||
'<p>' + _('Failed to get login URL. You may close this tab.') + '</p>';
|
||||
ui.addTimeLimitedNotification(null, [ E('p', _('Failed to get login URL: Invalid response from server.')) ], 7000, 'error');
|
||||
ui.addTimeLimitedNotification(null, [ E('p', _('Failed to get login URL: %s').format(res?.error || _('Invalid response from server.'))) ], 7000, 'error');
|
||||
}
|
||||
}).catch(function(err) {
|
||||
ui.hideModal();
|
||||
@@ -643,6 +812,7 @@ return view.extend({
|
||||
ui.showModal(_('Logging out...'), E('em', {}, _('Please wait.')));
|
||||
|
||||
return callDoLogout().then(function(res) {
|
||||
unwrapRpc(res, _('Logout'));
|
||||
ui.hideModal();
|
||||
ui.addTimeLimitedNotification(null, [ E('p', _('Successfully logged out.')) ], 5000, 'info');
|
||||
}).catch(function(err) {
|
||||
@@ -680,6 +850,7 @@ return view.extend({
|
||||
display.replaceChildren(E('em', {}, _('Collecting data ...')));
|
||||
}
|
||||
return callGetLogs().then(function(res) {
|
||||
unwrapRpc(res, _('Loading logs'));
|
||||
if (display) {
|
||||
display.replaceChildren(renderLogs(res));
|
||||
}
|
||||
@@ -701,7 +872,26 @@ return view.extend({
|
||||
// standard OpenWrt apply mechanism, which triggers /etc/init.d/tailscale-settings
|
||||
// and provides automatic rollback protection if the device becomes unreachable.
|
||||
handleSaveApply(ev, mode) {
|
||||
// Read the submitted value before map.save() consumes the staged delta.
|
||||
// uci.get() prefers the staged change over the saved value, and the
|
||||
// staged delta is only populated once the form has been parsed, which
|
||||
// map.save() guarantees here.
|
||||
const wantEnabled = uci.get('tailscale', 'settings', 'service_enabled');
|
||||
|
||||
return map.save().then(function () {
|
||||
// Only records the disable request; the reload handler performs
|
||||
// the stop after the commit, so a LuCI rollback still finds a
|
||||
// running daemon. One concurrency slot on purpose: an overlapping
|
||||
// apply could let the trigger read a half-written request file.
|
||||
// Fail-open on purpose: an install whose rpcd ACL cache predates
|
||||
// this method would otherwise be unable to apply anything. The
|
||||
// worst case is that the stop is skipped for this apply.
|
||||
return callSetServiceEnabled({ enabled: wantEnabled === '0' ? '0' : '1' })
|
||||
.catch(function(err) {
|
||||
console.warn('set_service_enabled failed: %s', err);
|
||||
return null;
|
||||
});
|
||||
}).then(function () {
|
||||
return ui.changes.apply(mode == '0');
|
||||
});
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -1,563 +1,676 @@
|
||||
msgid ""
|
||||
msgstr "Content-Type: text/plain; charset=UTF-8"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:37
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:47
|
||||
msgid "%s failed"
|
||||
msgstr ""
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:71
|
||||
msgid "(Experimental) Reduce Memory Usage"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:469
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:711
|
||||
msgid "1. Enable \"Advertise Exit Node\"."
|
||||
msgstr ""
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:693
|
||||
msgid "1. Select \"Accept Routes\" (to access remote devices)."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:470
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:712
|
||||
msgid ""
|
||||
"2. Click \"Auto Configure Firewall\" (it forwards Tailscale traffic "
|
||||
"to WAN)."
|
||||
msgstr ""
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:694
|
||||
msgid ""
|
||||
"2. In \"Advertise Routes\", select your local subnet (to allow remote "
|
||||
"devices to access this LAN)."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:471
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:695
|
||||
msgid "3. Click \"Auto Configure Firewall\" (to allow traffic forwarding)."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:21
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:713
|
||||
msgid ""
|
||||
"3. In the Tailscale admin console, edit this device under the route "
|
||||
"settings and approve it as an Exit Node."
|
||||
msgstr ""
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:718
|
||||
msgid "4. On the client, set this device as its exit node."
|
||||
msgstr ""
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:55
|
||||
msgid "Accept Routes"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:478
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:725
|
||||
msgid "Account Settings"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:22
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:56
|
||||
msgid "Advertise Exit Node"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:437
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:636
|
||||
msgid "Advertise Routes"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:437
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:636
|
||||
msgid ""
|
||||
"Advertise subnet routes behind this device. Select from the detected subnets "
|
||||
"below or enter custom routes (comma-separated)."
|
||||
"Advertise subnet routes behind this device. Select from the detected "
|
||||
"subnets below or enter custom routes (comma-separated)."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:23
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:57
|
||||
msgid "Allow LAN Access"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:21
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:55
|
||||
msgid "Allow accepting routes announced by other nodes."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:27
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:61
|
||||
msgid "Allow connecting to this device through the SSH function of Tailscale."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/root/usr/share/rpcd/acl.d/luci-app-tailscale-community.json:3
|
||||
#: luci-app-tailscale-community/root/usr/share/rpcd/acl.d/luci-app-tailscale-community.json:1
|
||||
msgid "Allow user access to tailscale"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:552
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:170
|
||||
msgid "Another firewall reload is already running"
|
||||
msgstr ""
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:799
|
||||
msgid "Are you sure you want to log out?"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:445
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:644
|
||||
msgid "Auto Configure Firewall"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:559
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:806
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:503
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:750
|
||||
msgid "Click to Log out account on this device."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:482
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:729
|
||||
msgid "Click to get a login URL for this device."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:169
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:260
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:392
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:601
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:291
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:383
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:591
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:850
|
||||
msgid "Collecting data ..."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:578
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:680
|
||||
msgid ""
|
||||
"Configuration committed, reloading network and firewall. This can "
|
||||
"take a moment."
|
||||
msgstr ""
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:673
|
||||
msgid "Configuring firewall"
|
||||
msgstr ""
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:826
|
||||
msgid "Confirm Logout"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:281
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:452
|
||||
msgid "Connection Info"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:62
|
||||
msgid "Controls how Tailscale DNS is handled."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:515
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:762
|
||||
msgid ""
|
||||
"Could not open a new tab. Please check if your browser or an extension "
|
||||
"blocked the pop-up."
|
||||
"Could not open a new tab. Please check if your browser or an "
|
||||
"extension blocked the pop-up."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:487
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:734
|
||||
msgid "Custom Login Server"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:495
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:742
|
||||
msgid "Custom Login Server Auth Key"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:62
|
||||
msgid "DNS Mode"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:22
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:70
|
||||
msgid "Daemon MTU"
|
||||
msgstr ""
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:865
|
||||
msgid "Daemon Settings"
|
||||
msgstr ""
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:56
|
||||
msgid "Declare this device as an Exit Node."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:581
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:829
|
||||
msgid "Devices List"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:29
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:63
|
||||
msgid "Disable Firewall Configuration"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:25
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:59
|
||||
msgid "Disable SNAT"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:25
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:59
|
||||
msgid ""
|
||||
"Disable Source NAT (SNAT) for traffic to advertised routes. Most users "
|
||||
"should leave this unchecked."
|
||||
"Disable Source NAT (SNAT) for traffic to advertised routes. Most "
|
||||
"users should leave this unchecked."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:29
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:63
|
||||
msgid "Disable Tailscale netfilter auto-configuration (--netfilter-mode=off)."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:229
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:62
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:351
|
||||
msgid "Disabled"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:62
|
||||
msgid "Disabled: system DNS only."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:504
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:751
|
||||
msgid "Disconnect from Tailscale and expire current node key."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:30
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:41
|
||||
msgid "Empty reply from server"
|
||||
msgstr ""
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:64
|
||||
msgid "Enable Peer Relay"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:27
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:61
|
||||
msgid "Enable Tailscale SSH"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:19
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:53
|
||||
msgid "Enable Tailscale Service"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:24
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:58
|
||||
msgid "Enable Web Interface"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:19
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:53
|
||||
msgid ""
|
||||
"Enable or disable the Tailscale service. When disabled, the service will be "
|
||||
"stopped and the process will be killed."
|
||||
"Enable or disable the Tailscale service. When disabled, the service "
|
||||
"will be stopped and the process will be killed."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:30
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:64
|
||||
msgid ""
|
||||
"Enable this device as a Peer Relay server. Requires a public IP and an UDP "
|
||||
"port open on the router."
|
||||
"Enable this device as a Peer Relay server. Requires a public IP and "
|
||||
"an UDP port open on the router."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:229
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:351
|
||||
msgid "Enabled"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:37
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:71
|
||||
msgid ""
|
||||
"Enabling this option can reduce memory usage, but it may sacrifice some "
|
||||
"performance (set GOGC=10)."
|
||||
"Enabling this option can reduce memory usage, but it may sacrifice "
|
||||
"some performance (set GOGC=10)."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:540
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:787
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:150
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:272
|
||||
msgid "Error caching DERP region map: %s"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:153
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:275
|
||||
msgid "Error fetching DERP region map: %s"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:123
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:245
|
||||
msgid "Error reading cached DERP region map: %s"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:446
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:645
|
||||
msgid ""
|
||||
"Essential configuration for Subnet Routing (Site-to-Site) and Exit Node "
|
||||
"features."
|
||||
"Essential configuration for Subnet Routing (Site-to-Site) and Exit "
|
||||
"Node features."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:303
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:413
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:424
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:612
|
||||
msgid "Exit Node"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:24
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:58
|
||||
msgid ""
|
||||
"Expose a web interface on port 5252 for managing this node over Tailscale."
|
||||
"Expose a web interface on port 5252 for managing this node over "
|
||||
"Tailscale."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:455
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:683
|
||||
msgid "Failed to configure firewall: %s"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:541
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:788
|
||||
msgid "Failed to get login URL. You may close this tab."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:546
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:789
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:793
|
||||
msgid "Failed to get login URL: %s"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:542
|
||||
msgid "Failed to get login URL: Invalid response from server."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:609
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:859
|
||||
msgid "Failed to load logs: %s"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:338
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:487
|
||||
msgid "Filter peers by name, IP, or OS..."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:351
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:504
|
||||
msgid "Filter:"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:20
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:54
|
||||
msgid "Firewall Mode"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:452
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:676
|
||||
msgid "Firewall configuration already applied."
|
||||
msgstr ""
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:159
|
||||
msgid "Firewall configuration applied."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:401
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:165
|
||||
msgid "Firewall reload failed"
|
||||
msgstr ""
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:600
|
||||
msgid "General Settings"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:278
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:449
|
||||
msgid "Hostname"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:464
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:706
|
||||
msgid "How to enable Exit Node?"
|
||||
msgstr ""
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:688
|
||||
msgid "How to enable Site-to-Site?"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:483
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:730
|
||||
msgid ""
|
||||
"If the timeout is displayed, you can refresh the page and click Login again."
|
||||
"If the timeout is displayed, you can refresh the page and click Login "
|
||||
"again."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:497
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:744
|
||||
msgid ""
|
||||
"If you are using custom login server but not providing an Auth Key, will "
|
||||
"redirect to the login page without pre-filling the key."
|
||||
"If you are using custom login server but not providing an Auth Key, "
|
||||
"will redirect to the login page without pre-filling the key."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:93
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:215
|
||||
msgid "Invalid Date"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:447
|
||||
msgid ""
|
||||
"It automatically creates the tailscale interface, sets up firewall zones for "
|
||||
"LAN <-> Tailscale forwarding,"
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:789
|
||||
msgid "Invalid response from server."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:96
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:646
|
||||
msgid ""
|
||||
"It creates the tailscale interface, adds it to a firewall zone, and "
|
||||
"sets up forwarding between LAN and Tailscale, plus Tailscale to WAN."
|
||||
msgstr ""
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:218
|
||||
msgid "Just now"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:208
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:330
|
||||
msgid "LOGGED OUT"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:284
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:455
|
||||
msgid "Last Seen"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:489
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:736
|
||||
msgid "Leave blank for default Tailscale control plane."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:565
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:853
|
||||
msgid "Loading logs"
|
||||
msgstr ""
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:812
|
||||
msgid "Logging out..."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:481
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:728
|
||||
msgid "Login"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:502
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:575
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:749
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:815
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:823
|
||||
msgid "Logout"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:572
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:820
|
||||
msgid "Logout failed: %s"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:587
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:836
|
||||
msgid "Logs"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:62
|
||||
msgid "MagicDNS: Tailscale overrides resolv.conf."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:90
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:648
|
||||
msgid ""
|
||||
"Masquerading and MSS Clamping (MTU fix) are enabled to keep "
|
||||
"connections stable."
|
||||
msgstr ""
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:212
|
||||
msgid "N/A"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:219
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:341
|
||||
msgid "NOT RUNNING"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:273
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:439
|
||||
msgid "No matching peer devices found."
|
||||
msgstr ""
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:400
|
||||
msgid "No peer devices found."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:246
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:591
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:368
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:840
|
||||
msgid "No tailscale-related logs found."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:414
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:613
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:91
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:213
|
||||
msgid "Now"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:280
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:451
|
||||
msgid "OS"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:303
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:424
|
||||
msgid "Offline"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:303
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:424
|
||||
msgid "Online"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:62
|
||||
msgid "OpenWrt Forward"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:62
|
||||
msgid "OpenWrt Forward: MagicDNS via dnsmasq forwarding.(Only support ts.net)"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:488
|
||||
msgid ""
|
||||
"Optional: Specify a custom control server URL (e.g., a Headscale instance, "
|
||||
"%s)."
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:39
|
||||
msgid "Operation"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:496
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:735
|
||||
msgid ""
|
||||
"Optional: Specify an authentication key for the custom control server. Leave "
|
||||
"blank if not required."
|
||||
"Optional: Specify a custom control server URL (e.g., a Headscale "
|
||||
"instance, %s)."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:405
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:743
|
||||
msgid ""
|
||||
"Optional: Specify an authentication key for the custom control "
|
||||
"server. Leave blank if not required."
|
||||
msgstr ""
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:604
|
||||
msgid "Peer Relay Port"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:210
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:332
|
||||
msgid "Please use the login button in the settings below to authenticate."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:525
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:531
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:565
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:772
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:778
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:812
|
||||
msgid "Please wait."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:227
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:349
|
||||
msgid "RUNNING"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:282
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:453
|
||||
msgid "RX"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:596
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:845
|
||||
msgid "Refresh"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:525
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:531
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:177
|
||||
msgid "Reloading firewall..."
|
||||
msgstr ""
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:772
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:778
|
||||
msgid "Requesting Login URL..."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:522
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:769
|
||||
msgid "Requesting Tailscale login URL... Please wait."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:413
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:612
|
||||
msgid ""
|
||||
"Select an exit node from the list. If enabled, Allow LAN Access is enabled "
|
||||
"implicitly."
|
||||
"Select an exit node from the list. If enabled, Allow LAN Access is "
|
||||
"enabled implicitly."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:20
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:54
|
||||
msgid ""
|
||||
"Select the firewall backend for Tailscale to use. Requires service restart "
|
||||
"to take effect."
|
||||
"Select the firewall backend for Tailscale to use. Requires service "
|
||||
"restart to take effect."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:198
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:206
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:218
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:227
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:320
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:328
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:340
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:349
|
||||
msgid "Service Status"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:26
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:70
|
||||
msgid ""
|
||||
"Set a custom MTU for the Tailscale daemon. Leave blank to use the "
|
||||
"default value."
|
||||
msgstr ""
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:60
|
||||
msgid "Shields Up"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:277
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:448
|
||||
msgid "Status"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:569
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:817
|
||||
msgid "Successfully logged out."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:199
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:321
|
||||
msgid "TAILSCALE NOT FOUND"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:229
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:351
|
||||
msgid "TUN Mode"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:283
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:454
|
||||
msgid "TX"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:232
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:354
|
||||
msgid "Tailnet Name"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:366
|
||||
#: applications/luci-app-tailscale-community/root/usr/share/luci/menu.d/luci-app-tailscale-community.json:3
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:562
|
||||
#: luci-app-tailscale-community/root/usr/share/luci/menu.d/luci-app-tailscale-community.json:1
|
||||
msgid "Tailscale"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:175
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:297
|
||||
msgid "Tailscale Health Check: %s"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:279
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:450
|
||||
msgid "Tailscale IP"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:230
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:352
|
||||
msgid "Tailscale IPv4"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:231
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:353
|
||||
msgid "Tailscale IPv6"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:521
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:768
|
||||
msgid "Tailscale Login"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:366
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:562
|
||||
msgid ""
|
||||
"Tailscale is a mesh VPN solution that makes it easy to connect your devices "
|
||||
"securely. This configuration page allows you to manage Tailscale settings on "
|
||||
"your OpenWrt device."
|
||||
"Tailscale is a mesh VPN solution that makes it easy to connect your "
|
||||
"devices securely. This configuration page allows you to manage "
|
||||
"Tailscale settings on your OpenWrt device."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:264
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:389
|
||||
msgid "Tailscale status error"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:523
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:647
|
||||
msgid ""
|
||||
"The Tailscale to WAN forwarding is what lets clients send their "
|
||||
"internet traffic through this device as an Exit Node."
|
||||
msgstr ""
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:770
|
||||
msgid "This can take up to 30 seconds."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:553
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:800
|
||||
msgid ""
|
||||
"This will disconnect this device from your Tailnet and require you to re-"
|
||||
"authenticate."
|
||||
"This will disconnect this device from your Tailnet and require you to "
|
||||
"re-authenticate."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:406
|
||||
msgid ""
|
||||
"UDP port for the Peer Relay service. Open this port on your router firewall/"
|
||||
"NAT."
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:143
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:194
|
||||
msgid "Timed out waiting for the firewall reload"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:123
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:150
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:153
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:546
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:572
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:609
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:605
|
||||
msgid ""
|
||||
"UDP port for the Peer Relay service. Open this port on your router "
|
||||
"firewall/NAT."
|
||||
msgstr ""
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:245
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:272
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:275
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:683
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:793
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:820
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:859
|
||||
msgid "Unknown error"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:228
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:350
|
||||
msgid "Version"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:26
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:60
|
||||
msgid ""
|
||||
"When enabled, blocks all inbound connections from the Tailscale network."
|
||||
"When enabled, blocks all inbound connections from the Tailscale "
|
||||
"network."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:23
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:57
|
||||
msgid "When using the exit node, access to the local LAN is allowed."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:472
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:660
|
||||
msgid ""
|
||||
"You have unapplied changes. Save & Apply them first so the firewall "
|
||||
"matches the form."
|
||||
msgstr ""
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:696
|
||||
msgid ""
|
||||
"[Important] Log in to the Tailscale admin console and manually enable "
|
||||
"\"Subnet Routes\" for this device."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:99
|
||||
msgid "ago"
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:719
|
||||
msgid ""
|
||||
"[Important] Step 3 cannot be done here: it needs your Tailscale "
|
||||
"account."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:448
|
||||
msgid ""
|
||||
"and enables Masquerading and MSS Clamping (MTU fix) to ensure stable "
|
||||
"connections."
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:221
|
||||
msgid "ago"
|
||||
msgstr ""
|
||||
@@ -1,585 +1,693 @@
|
||||
msgid ""
|
||||
msgstr "Content-Type: text/plain; charset=UTF-8\n"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:37
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:47
|
||||
msgid "%s failed"
|
||||
msgstr "%s失败"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:71
|
||||
msgid "(Experimental) Reduce Memory Usage"
|
||||
msgstr "(实验性) 减少内存使用"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:469
|
||||
msgid "1. Select \"Accept Routes\" (to access remote devices)."
|
||||
msgstr "1. 勾选 接受路由 (接受远程设备访问)。"
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:711
|
||||
msgid "1. Enable \"Advertise Exit Node\"."
|
||||
msgstr "1. 勾选 \"通告出口节点\"。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:470
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:693
|
||||
msgid "1. Select \"Accept Routes\" (to access remote devices)."
|
||||
msgstr "1. 勾选 \"接受路由\" (以访问远程设备)。"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:712
|
||||
msgid ""
|
||||
"2. Click \"Auto Configure Firewall\" (it forwards Tailscale traffic "
|
||||
"to WAN)."
|
||||
msgstr "2. 点击 \"自动配置防火墙\" (它会将 Tailscale 流量转发到 WAN)。"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:694
|
||||
msgid ""
|
||||
"2. In \"Advertise Routes\", select your local subnet (to allow remote "
|
||||
"devices to access this LAN)."
|
||||
msgstr "2. 在 通告路由 选择本设备的局域网段 (让远程设备能访问你)。"
|
||||
msgstr "2. 在 \"通告路由\" 中选择本地子网 (让远程设备能访问本局域网)。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:471
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:695
|
||||
msgid "3. Click \"Auto Configure Firewall\" (to allow traffic forwarding)."
|
||||
msgstr "3. 点击 自动配置防火墙 (打通防火墙转发)。"
|
||||
msgstr "3. 点击 \"自动配置防火墙\" (打通防火墙转发)。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:21
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:713
|
||||
msgid ""
|
||||
"3. In the Tailscale admin console, edit this device under the route "
|
||||
"settings and approve it as an Exit Node."
|
||||
msgstr "3. 在 Tailscale 管理控制台的路由设置中编辑此设备,并批准其为出口节点。"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:718
|
||||
msgid "4. On the client, set this device as its exit node."
|
||||
msgstr "4. 在客户端上,将此设备设为出口节点。"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:55
|
||||
msgid "Accept Routes"
|
||||
msgstr "接受路由"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:478
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:725
|
||||
msgid "Account Settings"
|
||||
msgstr "账户设置"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:22
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:56
|
||||
msgid "Advertise Exit Node"
|
||||
msgstr "通告出口节点"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:437
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:636
|
||||
msgid "Advertise Routes"
|
||||
msgstr "通告路由"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:437
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:636
|
||||
msgid ""
|
||||
"Advertise subnet routes behind this device. Select from the detected subnets "
|
||||
"below or enter custom routes (comma-separated)."
|
||||
msgstr ""
|
||||
"通告此设备后的子网路由。从下面的子网中选择,或输入自定义路由 (逗号分隔)。"
|
||||
"Advertise subnet routes behind this device. Select from the detected "
|
||||
"subnets below or enter custom routes (comma-separated)."
|
||||
msgstr "通告此设备后的子网路由。从下面的子网中选择,或输入自定义路由 (逗号分隔)。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:23
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:57
|
||||
msgid "Allow LAN Access"
|
||||
msgstr "允许局域网访问"
|
||||
msgstr "允许访问局域网"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:21
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:55
|
||||
msgid "Allow accepting routes announced by other nodes."
|
||||
msgstr "允许接受由其他节点通告的路由。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:27
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:61
|
||||
msgid "Allow connecting to this device through the SSH function of Tailscale."
|
||||
msgstr "允许通过 Tailscale 的 SSH 功能连接到此设备。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/root/usr/share/rpcd/acl.d/luci-app-tailscale-community.json:3
|
||||
#: luci-app-tailscale-community/root/usr/share/rpcd/acl.d/luci-app-tailscale-community.json:1
|
||||
msgid "Allow user access to tailscale"
|
||||
msgstr "允许用户访问 Tailscale"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:552
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:170
|
||||
msgid "Another firewall reload is already running"
|
||||
msgstr "另一个防火墙重新加载任务正在进行中"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:799
|
||||
msgid "Are you sure you want to log out?"
|
||||
msgstr "您确定要登出吗?"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:445
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:644
|
||||
msgid "Auto Configure Firewall"
|
||||
msgstr "自动配置防火墙"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:559
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:806
|
||||
msgid "Cancel"
|
||||
msgstr "取消"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:503
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:750
|
||||
msgid "Click to Log out account on this device."
|
||||
msgstr "点击以登出此设备上的账户。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:482
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:729
|
||||
msgid "Click to get a login URL for this device."
|
||||
msgstr "点击获取此设备的登录 URL。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:169
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:260
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:392
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:601
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:291
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:383
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:591
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:850
|
||||
msgid "Collecting data ..."
|
||||
msgstr "正在收集数据..."
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:578
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:680
|
||||
msgid ""
|
||||
"Configuration committed, reloading network and firewall. This can "
|
||||
"take a moment."
|
||||
msgstr "配置已提交,正在重新加载网络和防火墙。这可能需要一点时间。"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:673
|
||||
msgid "Configuring firewall"
|
||||
msgstr "正在配置防火墙"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:826
|
||||
msgid "Confirm Logout"
|
||||
msgstr "确认登出"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:281
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:452
|
||||
msgid "Connection Info"
|
||||
msgstr "连接信息"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:62
|
||||
msgid "Controls how Tailscale DNS is handled."
|
||||
msgstr "控制 Tailscale DNS 的处理方式。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:515
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:762
|
||||
msgid ""
|
||||
"Could not open a new tab. Please check if your browser or an extension "
|
||||
"blocked the pop-up."
|
||||
"Could not open a new tab. Please check if your browser or an "
|
||||
"extension blocked the pop-up."
|
||||
msgstr "无法打开新标签页。请检查您的浏览器或扩展程序是否阻止了弹出窗口。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:487
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:734
|
||||
msgid "Custom Login Server"
|
||||
msgstr "自定义登录服务器"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:495
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:742
|
||||
msgid "Custom Login Server Auth Key"
|
||||
msgstr "自定义登录服务器认证密钥"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:62
|
||||
msgid "DNS Mode"
|
||||
msgstr "DNS 模式"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:22
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:70
|
||||
msgid "Daemon MTU"
|
||||
msgstr "守护进程 MTU"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:865
|
||||
msgid "Daemon Settings"
|
||||
msgstr "守护进程设置"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:56
|
||||
msgid "Declare this device as an Exit Node."
|
||||
msgstr "将此设备声明为出口节点。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:581
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:829
|
||||
msgid "Devices List"
|
||||
msgstr "设备列表"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:29
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:63
|
||||
msgid "Disable Firewall Configuration"
|
||||
msgstr "禁用防火墙配置"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:25
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:59
|
||||
msgid "Disable SNAT"
|
||||
msgstr "禁用 SNAT"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:25
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:59
|
||||
msgid ""
|
||||
"Disable Source NAT (SNAT) for traffic to advertised routes. Most users "
|
||||
"should leave this unchecked."
|
||||
"Disable Source NAT (SNAT) for traffic to advertised routes. Most "
|
||||
"users should leave this unchecked."
|
||||
msgstr "为通告路由的流量禁用源地址转换 (SNAT)。大多数用户应保持此项不勾选。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:29
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:63
|
||||
msgid "Disable Tailscale netfilter auto-configuration (--netfilter-mode=off)."
|
||||
msgstr ""
|
||||
msgstr "禁用 Tailscale netfilter 自动配置 (--netfilter-mode=off)。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:229
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:62
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:351
|
||||
msgid "Disabled"
|
||||
msgstr "已禁用"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:62
|
||||
msgid "Disabled: system DNS only."
|
||||
msgstr "已禁用:使用系统指定DNS模式"
|
||||
msgstr "已禁用:仅使用系统 DNS。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:504
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:751
|
||||
msgid "Disconnect from Tailscale and expire current node key."
|
||||
msgstr "从 Tailscale 断开连接并使当前节点密钥过期。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:30
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:41
|
||||
msgid "Empty reply from server"
|
||||
msgstr "服务器返回空响应"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:64
|
||||
msgid "Enable Peer Relay"
|
||||
msgstr "启用节点中继"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:27
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:61
|
||||
msgid "Enable Tailscale SSH"
|
||||
msgstr "启用 Tailscale SSH"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:19
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:53
|
||||
msgid "Enable Tailscale Service"
|
||||
msgstr "Tailscale服务开关"
|
||||
msgstr "启用 Tailscale 服务"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:24
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:58
|
||||
msgid "Enable Web Interface"
|
||||
msgstr "启用 Web 界面"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:19
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:53
|
||||
msgid ""
|
||||
"Enable or disable the Tailscale service. When disabled, the service will be "
|
||||
"stopped and the process will be killed."
|
||||
"Enable or disable the Tailscale service. When disabled, the service "
|
||||
"will be stopped and the process will be killed."
|
||||
msgstr "启用或禁用Tailscale服务。禁用后,服务将被停止,当前进程将被终止。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:30
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:64
|
||||
msgid ""
|
||||
"Enable this device as a Peer Relay server. Requires a public IP and an UDP "
|
||||
"port open on the router."
|
||||
msgstr ""
|
||||
"将此设备设定为节点中继服务器(DERP)。需要此路由器具备公网IP以及开放一个UDP端"
|
||||
"口"
|
||||
"Enable this device as a Peer Relay server. Requires a public IP and "
|
||||
"an UDP port open on the router."
|
||||
msgstr "将此设备设为节点中继服务器。需要此路由器具备公网 IP,并在路由器上开放一个 UDP 端口。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:229
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:351
|
||||
msgid "Enabled"
|
||||
msgstr "已启用"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:37
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:71
|
||||
msgid ""
|
||||
"Enabling this option can reduce memory usage, but it may sacrifice some "
|
||||
"performance (set GOGC=10)."
|
||||
"Enabling this option can reduce memory usage, but it may sacrifice "
|
||||
"some performance (set GOGC=10)."
|
||||
msgstr "启用此选项可以减少内存使用,但可能会牺牲一些性能 (设置 GOGC=10)。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:540
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:787
|
||||
msgid "Error"
|
||||
msgstr "错误"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:150
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:272
|
||||
msgid "Error caching DERP region map: %s"
|
||||
msgstr "缓存 DERP 区域地图时出错: %s"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:153
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:275
|
||||
msgid "Error fetching DERP region map: %s"
|
||||
msgstr "获取 DERP 区域地图时出错: %s"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:123
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:245
|
||||
msgid "Error reading cached DERP region map: %s"
|
||||
msgstr "读取缓存的 DERP 区域地图时出错: %s"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:446
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:645
|
||||
msgid ""
|
||||
"Essential configuration for Subnet Routing (Site-to-Site) and Exit Node "
|
||||
"features."
|
||||
"Essential configuration for Subnet Routing (Site-to-Site) and Exit "
|
||||
"Node features."
|
||||
msgstr "子网路由(局域网间互通)和出口节点的基本配置。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:303
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:413
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:424
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:612
|
||||
msgid "Exit Node"
|
||||
msgstr "出口节点"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:24
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:58
|
||||
msgid ""
|
||||
"Expose a web interface on port 5252 for managing this node over Tailscale."
|
||||
"Expose a web interface on port 5252 for managing this node over "
|
||||
"Tailscale."
|
||||
msgstr "在端口 5252 上暴露一个 Web 界面,用于通过 Tailscale 管理此节点。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:455
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:683
|
||||
msgid "Failed to configure firewall: %s"
|
||||
msgstr "获取防火墙设置失败: %s"
|
||||
msgstr "配置防火墙失败: %s"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:541
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:788
|
||||
msgid "Failed to get login URL. You may close this tab."
|
||||
msgstr "获取登录 URL 失败。您可以关闭此标签页。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:546
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:789
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:793
|
||||
msgid "Failed to get login URL: %s"
|
||||
msgstr "获取登录 URL 失败: %s"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:542
|
||||
msgid "Failed to get login URL: Invalid response from server."
|
||||
msgstr "获取登录 URL 失败: 服务器响应无效。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:609
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:859
|
||||
msgid "Failed to load logs: %s"
|
||||
msgstr "加载日志失败"
|
||||
msgstr "加载日志失败: %s"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:20
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:487
|
||||
msgid "Filter peers by name, IP, or OS..."
|
||||
msgstr "按名称、IP 或操作系统过滤..."
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:504
|
||||
msgid "Filter:"
|
||||
msgstr "过滤:"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:54
|
||||
msgid "Firewall Mode"
|
||||
msgstr "防火墙模式"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:452
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:676
|
||||
msgid "Firewall configuration already applied."
|
||||
msgstr "防火墙配置已应用。"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:159
|
||||
msgid "Firewall configuration applied."
|
||||
msgstr "已应用防火墙配置"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:401
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:165
|
||||
msgid "Firewall reload failed"
|
||||
msgstr "重新加载防火墙失败"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:600
|
||||
msgid "General Settings"
|
||||
msgstr "常规设置"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:278
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:449
|
||||
msgid "Hostname"
|
||||
msgstr "主机名"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:464
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:706
|
||||
msgid "How to enable Exit Node?"
|
||||
msgstr "如何启用出口节点?"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:688
|
||||
msgid "How to enable Site-to-Site?"
|
||||
msgstr "如何实现站点到站点(即多局域网互通)?"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:483
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:730
|
||||
msgid ""
|
||||
"If the timeout is displayed, you can refresh the page and click Login again."
|
||||
"If the timeout is displayed, you can refresh the page and click Login "
|
||||
"again."
|
||||
msgstr "如果显示超时,您可以刷新页面并再次点击登录。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:497
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:744
|
||||
msgid ""
|
||||
"If you are using custom login server but not providing an Auth Key, will "
|
||||
"redirect to the login page without pre-filling the key."
|
||||
msgstr ""
|
||||
"如果您使用自定义登录服务器但未提供认证密钥,将重定向到登录页面而不会预先填充"
|
||||
"密钥。"
|
||||
"If you are using custom login server but not providing an Auth Key, "
|
||||
"will redirect to the login page without pre-filling the key."
|
||||
msgstr "如果您使用自定义登录服务器但未提供认证密钥,将重定向到登录页面而不会预先填充密钥。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:93
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:215
|
||||
msgid "Invalid Date"
|
||||
msgstr "无效日期"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:447
|
||||
msgid ""
|
||||
"It automatically creates the tailscale interface, sets up firewall zones for "
|
||||
"LAN <-> Tailscale forwarding,"
|
||||
msgstr "将自动创建Tailscale接口并设置防火墙区域用于LAN <-> Tailscale转发"
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:789
|
||||
msgid "Invalid response from server."
|
||||
msgstr "服务器响应无效。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:96
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:646
|
||||
msgid ""
|
||||
"It creates the tailscale interface, adds it to a firewall zone, and "
|
||||
"sets up forwarding between LAN and Tailscale, plus Tailscale to WAN."
|
||||
msgstr ""
|
||||
"它会创建 tailscale 接口,将其加入防火墙区域,并设置 LAN 与 Tailscale 之间的转发,以及 Tailscale 到 "
|
||||
"WAN 的转发。"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:218
|
||||
msgid "Just now"
|
||||
msgstr "刚才"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:208
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:330
|
||||
msgid "LOGGED OUT"
|
||||
msgstr "已登出"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:284
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:455
|
||||
msgid "Last Seen"
|
||||
msgstr "上次在线"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:489
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:736
|
||||
msgid "Leave blank for default Tailscale control plane."
|
||||
msgstr "留空以使用默认的 Tailscale 控制平面。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:565
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:853
|
||||
msgid "Loading logs"
|
||||
msgstr "正在加载日志"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:812
|
||||
msgid "Logging out..."
|
||||
msgstr "正在登出..."
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:481
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:728
|
||||
msgid "Login"
|
||||
msgstr "登录"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:502
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:575
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:749
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:815
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:823
|
||||
msgid "Logout"
|
||||
msgstr "登出"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:572
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:820
|
||||
msgid "Logout failed: %s"
|
||||
msgstr "登出失败: %s"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:587
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:836
|
||||
msgid "Logs"
|
||||
msgstr "日志"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:62
|
||||
msgid "MagicDNS: Tailscale overrides resolv.conf."
|
||||
msgstr "MagicDNS:Tailscale将根据设置覆写系统resolv.conf配置"
|
||||
msgstr "MagicDNS:Tailscale 会覆写 resolv.conf。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:90
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:648
|
||||
msgid ""
|
||||
"Masquerading and MSS Clamping (MTU fix) are enabled to keep "
|
||||
"connections stable."
|
||||
msgstr "会启用伪装和 MSS Clamping (MTU 修复),以保持连接稳定。"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:212
|
||||
msgid "N/A"
|
||||
msgstr "不可用"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:219
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:341
|
||||
msgid "NOT RUNNING"
|
||||
msgstr "未运行"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:273
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:439
|
||||
msgid "No matching peer devices found."
|
||||
msgstr "未找到匹配的节点设备。"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:400
|
||||
msgid "No peer devices found."
|
||||
msgstr "未找到节点设备。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:246
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:591
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:368
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:840
|
||||
msgid "No tailscale-related logs found."
|
||||
msgstr "没有找到Tailscale相关日志"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:414
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:613
|
||||
msgid "None"
|
||||
msgstr "无"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:91
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:213
|
||||
msgid "Now"
|
||||
msgstr "现在"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:280
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:451
|
||||
msgid "OS"
|
||||
msgstr "操作系统"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:303
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:424
|
||||
msgid "Offline"
|
||||
msgstr "离线"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:303
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:424
|
||||
msgid "Online"
|
||||
msgstr "在线"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:62
|
||||
msgid "OpenWrt Forward"
|
||||
msgstr "OpenWrt 转发"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:62
|
||||
msgid "OpenWrt Forward: MagicDNS via dnsmasq forwarding.(Only support ts.net)"
|
||||
msgstr "OpenWrt 转发:通过dnsmasq转发MagicDNS(仅支持 ts.net域名)。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:488
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:39
|
||||
msgid "Operation"
|
||||
msgstr "操作"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:735
|
||||
msgid ""
|
||||
"Optional: Specify a custom control server URL (e.g., a Headscale instance, "
|
||||
"%s)."
|
||||
"Optional: Specify a custom control server URL (e.g., a Headscale "
|
||||
"instance, %s)."
|
||||
msgstr "可选:指定一个自定义控制服务器 URL (例如,一个 Headscale 实例, %s)。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:496
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:743
|
||||
msgid ""
|
||||
"Optional: Specify an authentication key for the custom control server. Leave "
|
||||
"blank if not required."
|
||||
"Optional: Specify an authentication key for the custom control "
|
||||
"server. Leave blank if not required."
|
||||
msgstr "可选:为自定义控制服务器指定一个认证密钥。如果不需要,请留空。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:405
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:604
|
||||
msgid "Peer Relay Port"
|
||||
msgstr "DERP端口"
|
||||
msgstr "节点中继端口"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:210
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:332
|
||||
msgid "Please use the login button in the settings below to authenticate."
|
||||
msgstr "请使用下方设置中的登录按钮进行认证。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:525
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:531
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:565
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:772
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:778
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:812
|
||||
msgid "Please wait."
|
||||
msgstr "请稍候。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:227
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:349
|
||||
msgid "RUNNING"
|
||||
msgstr "正在运行"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:282
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:453
|
||||
msgid "RX"
|
||||
msgstr "接收"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:596
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:845
|
||||
msgid "Refresh"
|
||||
msgstr "刷新"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:525
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:531
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:177
|
||||
msgid "Reloading firewall..."
|
||||
msgstr "正在重新加载防火墙..."
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:772
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:778
|
||||
msgid "Requesting Login URL..."
|
||||
msgstr "正在请求登录 URL..."
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:522
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:769
|
||||
msgid "Requesting Tailscale login URL... Please wait."
|
||||
msgstr "正在请求 Tailscale 登录 URL... 请稍候。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:413
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:612
|
||||
msgid ""
|
||||
"Select an exit node from the list. If enabled, Allow LAN Access is enabled "
|
||||
"implicitly."
|
||||
msgstr "列表中选择出口节点。一旦开启,默认局域网之间可以互访"
|
||||
"Select an exit node from the list. If enabled, Allow LAN Access is "
|
||||
"enabled implicitly."
|
||||
msgstr "从列表中选择出口节点。启用后会同时开启允许局域网访问。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:20
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:54
|
||||
msgid ""
|
||||
"Select the firewall backend for Tailscale to use. Requires service restart "
|
||||
"to take effect."
|
||||
"Select the firewall backend for Tailscale to use. Requires service "
|
||||
"restart to take effect."
|
||||
msgstr "选择 Tailscale 使用的防火墙后端。需要重启服务才能生效。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:198
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:206
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:218
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:227
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:320
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:328
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:340
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:349
|
||||
msgid "Service Status"
|
||||
msgstr "服务状态"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:26
|
||||
msgid "Shields Up"
|
||||
msgstr "开启防护 (Shields Up)"
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:70
|
||||
msgid ""
|
||||
"Set a custom MTU for the Tailscale daemon. Leave blank to use the "
|
||||
"default value."
|
||||
msgstr "为 Tailscale 守护进程设置自定义 MTU。留空则使用默认值。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:277
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:60
|
||||
msgid "Shields Up"
|
||||
msgstr "屏蔽入站连接"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:448
|
||||
msgid "Status"
|
||||
msgstr "状态"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:569
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:817
|
||||
msgid "Successfully logged out."
|
||||
msgstr "登出成功。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:199
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:321
|
||||
msgid "TAILSCALE NOT FOUND"
|
||||
msgstr "未找到 TAILSCALE 主程序"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:229
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:351
|
||||
msgid "TUN Mode"
|
||||
msgstr "TUN 模式"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:283
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:454
|
||||
msgid "TX"
|
||||
msgstr "发送"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:232
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:354
|
||||
msgid "Tailnet Name"
|
||||
msgstr "Tailnet 名称"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:366
|
||||
#: applications/luci-app-tailscale-community/root/usr/share/luci/menu.d/luci-app-tailscale-community.json:3
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:562
|
||||
#: luci-app-tailscale-community/root/usr/share/luci/menu.d/luci-app-tailscale-community.json:1
|
||||
msgid "Tailscale"
|
||||
msgstr "Tailscale"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:175
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:297
|
||||
msgid "Tailscale Health Check: %s"
|
||||
msgstr "Tailscale 健康检查: %s"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:279
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:450
|
||||
msgid "Tailscale IP"
|
||||
msgstr "Tailscale IP"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:230
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:352
|
||||
msgid "Tailscale IPv4"
|
||||
msgstr "Tailscale IPv4"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:231
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:353
|
||||
msgid "Tailscale IPv6"
|
||||
msgstr "Tailscale IPv6"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:521
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:768
|
||||
msgid "Tailscale Login"
|
||||
msgstr "Tailscale 登录"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:366
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:562
|
||||
msgid ""
|
||||
"Tailscale is a mesh VPN solution that makes it easy to connect your devices "
|
||||
"securely. This configuration page allows you to manage Tailscale settings on "
|
||||
"your OpenWrt device."
|
||||
"Tailscale is a mesh VPN solution that makes it easy to connect your "
|
||||
"devices securely. This configuration page allows you to manage "
|
||||
"Tailscale settings on your OpenWrt device."
|
||||
msgstr ""
|
||||
"Tailscale 是一个网状 VPN 解决方案,可以轻松地安全连接您的设备。此配置页面允许"
|
||||
"您在 OpenWrt 设备上管理 Tailscale 设置。"
|
||||
"Tailscale 是一个网状 VPN 解决方案,可以轻松地安全连接您的设备。此配置页面允许您在 OpenWrt 设备上管理 "
|
||||
"Tailscale 设置。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:264
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:389
|
||||
msgid "Tailscale status error"
|
||||
msgstr "Tailscale 状态异常"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:523
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:647
|
||||
msgid ""
|
||||
"The Tailscale to WAN forwarding is what lets clients send their "
|
||||
"internet traffic through this device as an Exit Node."
|
||||
msgstr "正是 Tailscale 到 WAN 的转发,让客户端可以把上网流量经由此设备作为出口节点发出。"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:770
|
||||
msgid "This can take up to 30 seconds."
|
||||
msgstr "此过程最多可能需要 30 秒。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:553
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:800
|
||||
msgid ""
|
||||
"This will disconnect this device from your Tailnet and require you to re-"
|
||||
"authenticate."
|
||||
"This will disconnect this device from your Tailnet and require you to "
|
||||
"re-authenticate."
|
||||
msgstr "这将使此设备从您的 Tailnet 断开连接,并需要您重新进行身份验证。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:406
|
||||
msgid ""
|
||||
"UDP port for the Peer Relay service. Open this port on your router firewall/"
|
||||
"NAT."
|
||||
msgstr "DERP的UDP端口号。需要在防火墙/NAT规则中开放该端口。"
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:143
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:194
|
||||
msgid "Timed out waiting for the firewall reload"
|
||||
msgstr "等待防火墙重新加载超时"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:123
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:150
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:153
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:546
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:572
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:609
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:605
|
||||
msgid ""
|
||||
"UDP port for the Peer Relay service. Open this port on your router "
|
||||
"firewall/NAT."
|
||||
msgstr "节点中继服务的 UDP 端口。需要在路由器的防火墙/NAT 上开放该端口。"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:245
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:272
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:275
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:683
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:793
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:820
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:859
|
||||
msgid "Unknown error"
|
||||
msgstr "未知错误"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:228
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:350
|
||||
msgid "Version"
|
||||
msgstr "版本"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:26
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:60
|
||||
msgid ""
|
||||
"When enabled, blocks all inbound connections from the Tailscale network."
|
||||
"When enabled, blocks all inbound connections from the Tailscale "
|
||||
"network."
|
||||
msgstr "启用后,将阻止来自 Tailscale 网络的所有入站连接。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:23
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:57
|
||||
msgid "When using the exit node, access to the local LAN is allowed."
|
||||
msgstr "使用出口节点时,允许访问本地局域网。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:472
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:660
|
||||
msgid ""
|
||||
"You have unapplied changes. Save & Apply them first so the firewall "
|
||||
"matches the form."
|
||||
msgstr "您有未应用的更改。请先保存并应用,让防火墙配置与表单保持一致。"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:696
|
||||
msgid ""
|
||||
"[Important] Log in to the Tailscale admin console and manually enable "
|
||||
"\"Subnet Routes\" for this device."
|
||||
msgstr "【重要】 登录 Tailscale 控制平面,在设备设置中手动授权子网路由。"
|
||||
msgstr "【重要】登录 Tailscale 管理控制台,手动为此设备启用“子网路由”。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:99
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:719
|
||||
msgid ""
|
||||
"[Important] Step 3 cannot be done here: it needs your Tailscale "
|
||||
"account."
|
||||
msgstr "【重要】第 3 步无法在此完成:需要您的 Tailscale 账户。"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:221
|
||||
msgid "ago"
|
||||
msgstr "前"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:448
|
||||
msgid ""
|
||||
"and enables Masquerading and MSS Clamping (MTU fix) to ensure stable "
|
||||
"connections."
|
||||
msgstr "并启用伪装和MSS钳制(MTU修复)功能,以确保连接稳定。"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "Completely disable all Tailscale firewall auto-configuration, including "
|
||||
#~ "zone creation, forwarding rules, and netfilter settings."
|
||||
#~ msgstr ""
|
||||
#~ "完全禁用所有 Tailscale 防火墙自动配置,包括区域创建、转发规则和 netfilter "
|
||||
#~ "设置。"
|
||||
#~ "Completely disable all Tailscale firewall auto-configuration, "
|
||||
#~ "including zone creation, forwarding rules, and netfilter settings."
|
||||
#~ msgstr "完全禁用所有 Tailscale 防火墙自动配置,包括区域创建、转发规则和 netfilter 设置。"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "Optional: Specify a custom control server URL (e.g., a Headscale "
|
||||
#~ "instance, https://example.com)."
|
||||
#~ msgstr ""
|
||||
#~ "可选:指定一个自定义控制服务器 URL (例如,一个 Headscale 实例,https://"
|
||||
#~ "example.com)。"
|
||||
#~ msgstr "可选:指定一个自定义控制服务器 URL (例如,一个 Headscale 实例,https://example.com)。"
|
||||
|
||||
#~ msgid "Applying changes..."
|
||||
#~ msgstr "正在应用更改..."
|
||||
@@ -601,3 +709,24 @@ msgstr "并启用伪装和MSS钳制(MTU修复)功能,以确保连接稳定
|
||||
|
||||
#~ msgid "Tailscale settings applied successfully."
|
||||
#~ msgstr "Tailscale 设置已成功应用。"
|
||||
|
||||
#~ msgid "Failed to get login URL: Invalid response from server."
|
||||
#~ msgstr "获取登录 URL 失败: 服务器响应无效。"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "It automatically creates the tailscale interface, sets up firewall "
|
||||
#~ "zones for LAN <-> Tailscale forwarding,"
|
||||
#~ msgstr "将自动创建Tailscale接口并设置防火墙区域用于LAN <-> Tailscale转发"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "and enables Masquerading and MSS Clamping (MTU fix) to ensure stable "
|
||||
#~ "connections."
|
||||
#~ msgstr "并启用伪装和MSS钳制(MTU修复)功能,以确保连接稳定。"
|
||||
|
||||
#~ msgid "4. On the client, choose this device under \"Exit node\"."
|
||||
#~ msgstr "4. 在客户端中,在 \"出口节点\" 下选择此设备。"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "It creates the tailscale interface, adds it to a firewall zone, and "
|
||||
#~ "sets up forwarding between LAN, Tailscale and WAN."
|
||||
#~ msgstr "它会创建 tailscale 接口,将其加入防火墙区域,并在 LAN、Tailscale 和 WAN 之间设置转发。"
|
||||
@@ -1,583 +1,693 @@
|
||||
msgid ""
|
||||
msgstr "Content-Type: text/plain; charset=UTF-8\n"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:37
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:47
|
||||
msgid "%s failed"
|
||||
msgstr "%s 失敗"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:71
|
||||
msgid "(Experimental) Reduce Memory Usage"
|
||||
msgstr "(實驗性) 減少記憶體使用"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:469
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:711
|
||||
msgid "1. Enable \"Advertise Exit Node\"."
|
||||
msgstr "1. 啟用「宣告出口節點」。"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:693
|
||||
msgid "1. Select \"Accept Routes\" (to access remote devices)."
|
||||
msgstr "1. 勾選「接受路由」(以存取遠端設備)。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:470
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:712
|
||||
msgid ""
|
||||
"2. Click \"Auto Configure Firewall\" (it forwards Tailscale traffic "
|
||||
"to WAN)."
|
||||
msgstr "2. 點選「自動設定防火牆」(將 Tailscale 流量轉送到 WAN)。"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:694
|
||||
msgid ""
|
||||
"2. In \"Advertise Routes\", select your local subnet (to allow remote "
|
||||
"devices to access this LAN)."
|
||||
msgstr "2. 在「宣告路由」選擇本裝置的區域網段 (讓遠端能存取這個區網)。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:471
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:695
|
||||
msgid "3. Click \"Auto Configure Firewall\" (to allow traffic forwarding)."
|
||||
msgstr "3. 點選「自動配置防火牆」(允許流量轉送)。"
|
||||
msgstr "3. 點選「自動設定防火牆」(允許流量轉送)。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:21
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:713
|
||||
msgid ""
|
||||
"3. In the Tailscale admin console, edit this device under the route "
|
||||
"settings and approve it as an Exit Node."
|
||||
msgstr "3. 在 Tailscale 管理控制台的路由設定中編輯此裝置,並核准為出口節點。"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:718
|
||||
msgid "4. On the client, set this device as its exit node."
|
||||
msgstr "4. 在用戶端上,將此裝置設為出口節點。"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:55
|
||||
msgid "Accept Routes"
|
||||
msgstr "接受路由"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:478
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:725
|
||||
msgid "Account Settings"
|
||||
msgstr "帳號設定"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:22
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:56
|
||||
msgid "Advertise Exit Node"
|
||||
msgstr "宣告出口節點"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:437
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:636
|
||||
msgid "Advertise Routes"
|
||||
msgstr "宣告路由"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:437
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:636
|
||||
msgid ""
|
||||
"Advertise subnet routes behind this device. Select from the detected subnets "
|
||||
"below or enter custom routes (comma-separated)."
|
||||
msgstr ""
|
||||
"宣告此裝置後的子網路由。從下面的子網中選擇,或輸入自定義路由 (逗號分隔)。"
|
||||
"Advertise subnet routes behind this device. Select from the detected "
|
||||
"subnets below or enter custom routes (comma-separated)."
|
||||
msgstr "宣告此裝置後方的子網路路由。從下方偵測到的子網路中選擇,或輸入自訂路由 (以逗號分隔)。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:23
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:57
|
||||
msgid "Allow LAN Access"
|
||||
msgstr "允許區網存取"
|
||||
msgstr "允許 LAN 存取"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:21
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:55
|
||||
msgid "Allow accepting routes announced by other nodes."
|
||||
msgstr "允許接受由其他節點宣告的路由。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:27
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:61
|
||||
msgid "Allow connecting to this device through the SSH function of Tailscale."
|
||||
msgstr "允許透過 Tailscale 的 SSH 功能連線到此裝置。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/root/usr/share/rpcd/acl.d/luci-app-tailscale-community.json:3
|
||||
#: luci-app-tailscale-community/root/usr/share/rpcd/acl.d/luci-app-tailscale-community.json:1
|
||||
msgid "Allow user access to tailscale"
|
||||
msgstr "允許使用者存取 Tailscale"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:552
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:170
|
||||
msgid "Another firewall reload is already running"
|
||||
msgstr "另一個防火牆重新載入作業正在執行中"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:799
|
||||
msgid "Are you sure you want to log out?"
|
||||
msgstr "您確定要登出嗎?"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:445
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:644
|
||||
msgid "Auto Configure Firewall"
|
||||
msgstr "自動設定防火牆"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:559
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:806
|
||||
msgid "Cancel"
|
||||
msgstr "取消"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:503
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:750
|
||||
msgid "Click to Log out account on this device."
|
||||
msgstr "點選以登出此裝置上的帳號。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:482
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:729
|
||||
msgid "Click to get a login URL for this device."
|
||||
msgstr "點選獲取此裝置的登入 URL。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:169
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:260
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:392
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:601
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:291
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:383
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:591
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:850
|
||||
msgid "Collecting data ..."
|
||||
msgstr "正在收集資料..."
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:578
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:680
|
||||
msgid ""
|
||||
"Configuration committed, reloading network and firewall. This can "
|
||||
"take a moment."
|
||||
msgstr "設定已提交,正在重新載入網路與防火牆。這可能需要一點時間。"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:673
|
||||
msgid "Configuring firewall"
|
||||
msgstr "正在設定防火牆"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:826
|
||||
msgid "Confirm Logout"
|
||||
msgstr "確認登出"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:281
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:452
|
||||
msgid "Connection Info"
|
||||
msgstr "連線資訊"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:62
|
||||
msgid "Controls how Tailscale DNS is handled."
|
||||
msgstr "控制 Tailscale DNS 的處理方式。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:515
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:762
|
||||
msgid ""
|
||||
"Could not open a new tab. Please check if your browser or an extension "
|
||||
"blocked the pop-up."
|
||||
"Could not open a new tab. Please check if your browser or an "
|
||||
"extension blocked the pop-up."
|
||||
msgstr "無法開啟新分頁。請檢查您的瀏覽器或擴充套件程式是否阻止了彈出式視窗。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:487
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:734
|
||||
msgid "Custom Login Server"
|
||||
msgstr "自訂登入伺服器"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:495
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:742
|
||||
msgid "Custom Login Server Auth Key"
|
||||
msgstr "自訂登入伺服器認證金鑰"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:62
|
||||
msgid "DNS Mode"
|
||||
msgstr "DNS 模式"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:22
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:70
|
||||
msgid "Daemon MTU"
|
||||
msgstr "常駐程式 MTU"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:865
|
||||
msgid "Daemon Settings"
|
||||
msgstr "常駐程式設定"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:56
|
||||
msgid "Declare this device as an Exit Node."
|
||||
msgstr "將此裝置宣告為出口節點。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:581
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:829
|
||||
msgid "Devices List"
|
||||
msgstr "裝置清單"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:29
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:63
|
||||
msgid "Disable Firewall Configuration"
|
||||
msgstr "停用防火牆設定"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:25
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:59
|
||||
msgid "Disable SNAT"
|
||||
msgstr "停用 SNAT"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:25
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:59
|
||||
msgid ""
|
||||
"Disable Source NAT (SNAT) for traffic to advertised routes. Most users "
|
||||
"should leave this unchecked."
|
||||
"Disable Source NAT (SNAT) for traffic to advertised routes. Most "
|
||||
"users should leave this unchecked."
|
||||
msgstr "為宣告路由的流量停用來源位址轉換 (SNAT)。大多數使用者不應勾選此項。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:29
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:63
|
||||
msgid "Disable Tailscale netfilter auto-configuration (--netfilter-mode=off)."
|
||||
msgstr "停用 Tailscale netfilter 自動設定 (--netfilter-mode=off)。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:229
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:62
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:351
|
||||
msgid "Disabled"
|
||||
msgstr "已停用"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:62
|
||||
msgid "Disabled: system DNS only."
|
||||
msgstr "已停用: 僅使用系統 DNS。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:504
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:751
|
||||
msgid "Disconnect from Tailscale and expire current node key."
|
||||
msgstr "中斷 Tailscale 連線並使目前節點金鑰過期。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:30
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:41
|
||||
msgid "Empty reply from server"
|
||||
msgstr "伺服器回應為空"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:64
|
||||
msgid "Enable Peer Relay"
|
||||
msgstr "啟用對等中繼"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:27
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:61
|
||||
msgid "Enable Tailscale SSH"
|
||||
msgstr "啟用 Tailscale SSH"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:19
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:53
|
||||
msgid "Enable Tailscale Service"
|
||||
msgstr "啟用 Tailscale 服務"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:24
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:58
|
||||
msgid "Enable Web Interface"
|
||||
msgstr "啟用 Web 介面"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:19
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:53
|
||||
msgid ""
|
||||
"Enable or disable the Tailscale service. When disabled, the service will be "
|
||||
"stopped and the process will be killed."
|
||||
"Enable or disable the Tailscale service. When disabled, the service "
|
||||
"will be stopped and the process will be killed."
|
||||
msgstr "啟用或停用 Tailscale 服務。停用後,該服務將停止,處理程序將被終止。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:30
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:64
|
||||
msgid ""
|
||||
"Enable this device as a Peer Relay server. Requires a public IP and an UDP "
|
||||
"port open on the router."
|
||||
"Enable this device as a Peer Relay server. Requires a public IP and "
|
||||
"an UDP port open on the router."
|
||||
msgstr "將此設備做為對等中繼伺服器。需要公開 IP 位址,且路由器上需要開啟一個 UDP 連接埠。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:229
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:351
|
||||
msgid "Enabled"
|
||||
msgstr "已啟用"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:37
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:71
|
||||
msgid ""
|
||||
"Enabling this option can reduce memory usage, but it may sacrifice some "
|
||||
"performance (set GOGC=10)."
|
||||
"Enabling this option can reduce memory usage, but it may sacrifice "
|
||||
"some performance (set GOGC=10)."
|
||||
msgstr "啟用此選項可以減少記憶體使用,但可能會犧牲一些效能 (設定 GOGC=10)。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:540
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:787
|
||||
msgid "Error"
|
||||
msgstr "錯誤"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:150
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:272
|
||||
msgid "Error caching DERP region map: %s"
|
||||
msgstr "快取 DERP 區域對照表時出錯: %s"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:153
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:275
|
||||
msgid "Error fetching DERP region map: %s"
|
||||
msgstr "獲取 DERP 區域對照表時出錯: %s"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:123
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:245
|
||||
msgid "Error reading cached DERP region map: %s"
|
||||
msgstr "讀取快取的 DERP 區域對照表時出錯: %s"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:446
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:645
|
||||
msgid ""
|
||||
"Essential configuration for Subnet Routing (Site-to-Site) and Exit Node "
|
||||
"features."
|
||||
"Essential configuration for Subnet Routing (Site-to-Site) and Exit "
|
||||
"Node features."
|
||||
msgstr "子網路由(站對站)和出口節點功能的基本設定"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:303
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:413
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:424
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:612
|
||||
msgid "Exit Node"
|
||||
msgstr "出口節點"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:24
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:58
|
||||
msgid ""
|
||||
"Expose a web interface on port 5252 for managing this node over Tailscale."
|
||||
"Expose a web interface on port 5252 for managing this node over "
|
||||
"Tailscale."
|
||||
msgstr "在埠 5252 上暴露一個 Web 介面,用於透過 Tailscale 管理此節點。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:455
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:683
|
||||
msgid "Failed to configure firewall: %s"
|
||||
msgstr "獲取防火牆設定失敗: %s"
|
||||
msgstr "設定防火牆失敗: %s"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:541
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:788
|
||||
msgid "Failed to get login URL. You may close this tab."
|
||||
msgstr "獲取登入 URL 失敗。您可以關閉此分頁。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:546
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:789
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:793
|
||||
msgid "Failed to get login URL: %s"
|
||||
msgstr "獲取登入 URL 失敗: %s"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:542
|
||||
msgid "Failed to get login URL: Invalid response from server."
|
||||
msgstr "獲取登入 URL 失敗: 伺服器回應無效。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:609
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:859
|
||||
msgid "Failed to load logs: %s"
|
||||
msgstr "載入日誌失敗:%s"
|
||||
msgstr "載入日誌失敗: %s"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:20
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:487
|
||||
msgid "Filter peers by name, IP, or OS..."
|
||||
msgstr "依名稱、IP 或作業系統篩選..."
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:504
|
||||
msgid "Filter:"
|
||||
msgstr "篩選:"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:54
|
||||
msgid "Firewall Mode"
|
||||
msgstr "防火牆模式"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:452
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:676
|
||||
msgid "Firewall configuration already applied."
|
||||
msgstr "防火牆設定已套用。"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:159
|
||||
msgid "Firewall configuration applied."
|
||||
msgstr "已套用防火牆設定"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:401
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:165
|
||||
msgid "Firewall reload failed"
|
||||
msgstr "防火牆重新載入失敗"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:600
|
||||
msgid "General Settings"
|
||||
msgstr "一般設定"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:278
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:449
|
||||
msgid "Hostname"
|
||||
msgstr "主機名"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:464
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:706
|
||||
msgid "How to enable Exit Node?"
|
||||
msgstr "如何啟用出口節點?"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:688
|
||||
msgid "How to enable Site-to-Site?"
|
||||
msgstr "如何啟用站對站通訊?"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:483
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:730
|
||||
msgid ""
|
||||
"If the timeout is displayed, you can refresh the page and click Login again."
|
||||
"If the timeout is displayed, you can refresh the page and click Login "
|
||||
"again."
|
||||
msgstr "如果顯示逾時,您可以重新整理頁面並再次點選登入。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:497
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:744
|
||||
msgid ""
|
||||
"If you are using custom login server but not providing an Auth Key, will "
|
||||
"redirect to the login page without pre-filling the key."
|
||||
msgstr ""
|
||||
"如果您使用自定義登入伺服器但未提供認證金鑰,將重定向到登入頁面而不會預先填充"
|
||||
"金鑰。"
|
||||
"If you are using custom login server but not providing an Auth Key, "
|
||||
"will redirect to the login page without pre-filling the key."
|
||||
msgstr "如果您使用自訂登入伺服器但未提供認證金鑰,將重新導向至登入頁面,且不會預先填入金鑰。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:93
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:215
|
||||
msgid "Invalid Date"
|
||||
msgstr "無效日期"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:447
|
||||
msgid ""
|
||||
"It automatically creates the tailscale interface, sets up firewall zones for "
|
||||
"LAN <-> Tailscale forwarding,"
|
||||
msgstr "它會自動建立 Tailscale 介面,並設定允許 LAN <-> Tailscale 防火牆區域轉送。"
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:789
|
||||
msgid "Invalid response from server."
|
||||
msgstr "伺服器回應無效。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:96
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:646
|
||||
msgid ""
|
||||
"It creates the tailscale interface, adds it to a firewall zone, and "
|
||||
"sets up forwarding between LAN and Tailscale, plus Tailscale to WAN."
|
||||
msgstr ""
|
||||
"它會建立 tailscale 介面、加入防火牆區域,並設定 LAN 與 Tailscale 之間的轉送,以及 Tailscale 到 "
|
||||
"WAN 的轉送。"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:218
|
||||
msgid "Just now"
|
||||
msgstr "剛才"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:208
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:330
|
||||
msgid "LOGGED OUT"
|
||||
msgstr "已登出"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:284
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:455
|
||||
msgid "Last Seen"
|
||||
msgstr "上次連線"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:489
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:736
|
||||
msgid "Leave blank for default Tailscale control plane."
|
||||
msgstr "留白則使用預設的 Tailscale 控制台。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:565
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:853
|
||||
msgid "Loading logs"
|
||||
msgstr "正在載入日誌"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:812
|
||||
msgid "Logging out..."
|
||||
msgstr "正在登出..."
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:481
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:728
|
||||
msgid "Login"
|
||||
msgstr "登入"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:502
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:575
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:749
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:815
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:823
|
||||
msgid "Logout"
|
||||
msgstr "登出"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:572
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:820
|
||||
msgid "Logout failed: %s"
|
||||
msgstr "登出失敗: %s"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:587
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:836
|
||||
msgid "Logs"
|
||||
msgstr "日誌"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:62
|
||||
msgid "MagicDNS: Tailscale overrides resolv.conf."
|
||||
msgstr "MagicDNS:Tailscale 將接管 resolv.conf。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:90
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:648
|
||||
msgid ""
|
||||
"Masquerading and MSS Clamping (MTU fix) are enabled to keep "
|
||||
"connections stable."
|
||||
msgstr "同時啟用偽裝與 MSS Clamping (MTU 修復),以維持連線穩定。"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:212
|
||||
msgid "N/A"
|
||||
msgstr "N/A"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:219
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:341
|
||||
msgid "NOT RUNNING"
|
||||
msgstr "未執行"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:273
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:439
|
||||
msgid "No matching peer devices found."
|
||||
msgstr "找不到符合的對等裝置。"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:400
|
||||
msgid "No peer devices found."
|
||||
msgstr "未找到對等裝置。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:246
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:591
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:368
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:840
|
||||
msgid "No tailscale-related logs found."
|
||||
msgstr "未找到與 tailscale 相關的日誌。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:414
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:613
|
||||
msgid "None"
|
||||
msgstr "無"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:91
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:213
|
||||
msgid "Now"
|
||||
msgstr "現在"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:280
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:451
|
||||
msgid "OS"
|
||||
msgstr "作業系統"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:303
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:424
|
||||
msgid "Offline"
|
||||
msgstr "離線"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:303
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:424
|
||||
msgid "Online"
|
||||
msgstr "上線"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:62
|
||||
msgid "OpenWrt Forward"
|
||||
msgstr "OpenWrt 轉送"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:62
|
||||
msgid "OpenWrt Forward: MagicDNS via dnsmasq forwarding.(Only support ts.net)"
|
||||
msgstr "OpenWrt 轉送:僅經由 dnsmasq 轉送至 MagicDNS。(僅支援tailxxxxxx.ts.net)"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:488
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:39
|
||||
msgid "Operation"
|
||||
msgstr "操作"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:735
|
||||
msgid ""
|
||||
"Optional: Specify a custom control server URL (e.g., a Headscale instance, "
|
||||
"%s)."
|
||||
"Optional: Specify a custom control server URL (e.g., a Headscale "
|
||||
"instance, %s)."
|
||||
msgstr "可選:指定自訂控制伺服器 URL(例如,Headscale 實例,「%s」)。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:496
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:743
|
||||
msgid ""
|
||||
"Optional: Specify an authentication key for the custom control server. Leave "
|
||||
"blank if not required."
|
||||
"Optional: Specify an authentication key for the custom control "
|
||||
"server. Leave blank if not required."
|
||||
msgstr "可選:為自訂控制伺服器指定一個認證金鑰。如果不需要,請留白。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:405
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:604
|
||||
msgid "Peer Relay Port"
|
||||
msgstr "對等中繼連接埠"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:210
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:332
|
||||
msgid "Please use the login button in the settings below to authenticate."
|
||||
msgstr "請使用下方設定中的登入按鈕進行認證。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:525
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:531
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:565
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:772
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:778
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:812
|
||||
msgid "Please wait."
|
||||
msgstr "請稍候。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:227
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:349
|
||||
msgid "RUNNING"
|
||||
msgstr "正在執行"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:282
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:453
|
||||
msgid "RX"
|
||||
msgstr "接收"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:596
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:845
|
||||
msgid "Refresh"
|
||||
msgstr "重新整理"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:525
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:531
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:177
|
||||
msgid "Reloading firewall..."
|
||||
msgstr "正在重新載入防火牆..."
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:772
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:778
|
||||
msgid "Requesting Login URL..."
|
||||
msgstr "正在請求登入 URL..."
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:522
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:769
|
||||
msgid "Requesting Tailscale login URL... Please wait."
|
||||
msgstr "正在請求 Tailscale 登入 URL... 請稍候。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:413
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:612
|
||||
msgid ""
|
||||
"Select an exit node from the list. If enabled, Allow LAN Access is enabled "
|
||||
"implicitly."
|
||||
msgstr "從清單中選擇一個出口節點。如果啟用,則會被動啟用‘允許區域網路存取’"
|
||||
"Select an exit node from the list. If enabled, Allow LAN Access is "
|
||||
"enabled implicitly."
|
||||
msgstr "從清單中選擇一個出口節點。啟用後會一併啟用「允許區網存取」。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:20
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:54
|
||||
msgid ""
|
||||
"Select the firewall backend for Tailscale to use. Requires service restart "
|
||||
"to take effect."
|
||||
"Select the firewall backend for Tailscale to use. Requires service "
|
||||
"restart to take effect."
|
||||
msgstr "選擇 Tailscale 使用的防火牆後端。需要重新啟動服務才能生效。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:198
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:206
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:218
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:227
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:320
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:328
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:340
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:349
|
||||
msgid "Service Status"
|
||||
msgstr "服務狀態"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:26
|
||||
msgid "Shields Up"
|
||||
msgstr "開啟防護 (Shields Up)"
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:70
|
||||
msgid ""
|
||||
"Set a custom MTU for the Tailscale daemon. Leave blank to use the "
|
||||
"default value."
|
||||
msgstr "為 Tailscale 常駐程式設定自訂 MTU。留白則使用預設值。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:277
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:60
|
||||
msgid "Shields Up"
|
||||
msgstr "封鎖入站連線"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:448
|
||||
msgid "Status"
|
||||
msgstr "狀態"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:569
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:817
|
||||
msgid "Successfully logged out."
|
||||
msgstr "登出成功。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:199
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:321
|
||||
msgid "TAILSCALE NOT FOUND"
|
||||
msgstr "找不到 TAILSCALE"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:229
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:351
|
||||
msgid "TUN Mode"
|
||||
msgstr "TUN 模式"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:283
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:454
|
||||
msgid "TX"
|
||||
msgstr "傳送"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:232
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:354
|
||||
msgid "Tailnet Name"
|
||||
msgstr "Tailnet 名稱"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:366
|
||||
#: applications/luci-app-tailscale-community/root/usr/share/luci/menu.d/luci-app-tailscale-community.json:3
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:562
|
||||
#: luci-app-tailscale-community/root/usr/share/luci/menu.d/luci-app-tailscale-community.json:1
|
||||
msgid "Tailscale"
|
||||
msgstr "Tailscale"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:175
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:297
|
||||
msgid "Tailscale Health Check: %s"
|
||||
msgstr "Tailscale 健康檢查: %s"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:279
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:450
|
||||
msgid "Tailscale IP"
|
||||
msgstr "Tailscale IP"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:230
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:352
|
||||
msgid "Tailscale IPv4"
|
||||
msgstr "Tailscale IPv4"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:231
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:353
|
||||
msgid "Tailscale IPv6"
|
||||
msgstr "Tailscale IPv6"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:521
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:768
|
||||
msgid "Tailscale Login"
|
||||
msgstr "Tailscale 登入"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:366
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:562
|
||||
msgid ""
|
||||
"Tailscale is a mesh VPN solution that makes it easy to connect your devices "
|
||||
"securely. This configuration page allows you to manage Tailscale settings on "
|
||||
"your OpenWrt device."
|
||||
"Tailscale is a mesh VPN solution that makes it easy to connect your "
|
||||
"devices securely. This configuration page allows you to manage "
|
||||
"Tailscale settings on your OpenWrt device."
|
||||
msgstr ""
|
||||
"Tailscale 是一個網狀 VPN 解決方案,可以輕鬆地安全連線您的裝置。此設定頁面允許"
|
||||
"您在 OpenWrt 裝置上管理 Tailscale 設定。"
|
||||
"Tailscale 是一個網狀 VPN 解決方案,可以輕鬆地安全連線您的裝置。此設定頁面允許您在 OpenWrt 裝置上管理 "
|
||||
"Tailscale 設定。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:264
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:389
|
||||
msgid "Tailscale status error"
|
||||
msgstr "Tailscale 狀態錯誤"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:523
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:647
|
||||
msgid ""
|
||||
"The Tailscale to WAN forwarding is what lets clients send their "
|
||||
"internet traffic through this device as an Exit Node."
|
||||
msgstr "Tailscale 到 WAN 的轉送,讓用戶端能透過此裝置以出口節點傳送網際網路流量。"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:770
|
||||
msgid "This can take up to 30 seconds."
|
||||
msgstr "此過程最多可能需要 30 秒。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:553
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:800
|
||||
msgid ""
|
||||
"This will disconnect this device from your Tailnet and require you to re-"
|
||||
"authenticate."
|
||||
"This will disconnect this device from your Tailnet and require you to "
|
||||
"re-authenticate."
|
||||
msgstr "這將使此裝置從您的 Tailnet 中斷連線,並需要您重新進行身份驗證。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:406
|
||||
msgid ""
|
||||
"UDP port for the Peer Relay service. Open this port on your router firewall/"
|
||||
"NAT."
|
||||
msgstr "用於對等中繼服務的 UDP 連接埠。請在路由器防火牆上開啟此連接埠以及 NAT。"
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:143
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:194
|
||||
msgid "Timed out waiting for the firewall reload"
|
||||
msgstr "等待防火牆重新載入逾時"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:123
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:150
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:153
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:546
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:572
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:609
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:605
|
||||
msgid ""
|
||||
"UDP port for the Peer Relay service. Open this port on your router "
|
||||
"firewall/NAT."
|
||||
msgstr "用於對等中繼服務的 UDP 連接埠。請在路由器的防火牆/NAT 上開放此連接埠。"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:245
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:272
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:275
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:683
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:793
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:820
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:859
|
||||
msgid "Unknown error"
|
||||
msgstr "未知錯誤"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:228
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:350
|
||||
msgid "Version"
|
||||
msgstr "版本"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:26
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:60
|
||||
msgid ""
|
||||
"When enabled, blocks all inbound connections from the Tailscale network."
|
||||
"When enabled, blocks all inbound connections from the Tailscale "
|
||||
"network."
|
||||
msgstr "啟用後,將阻止來自 Tailscale 網路的所有入站連線。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:23
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:57
|
||||
msgid "When using the exit node, access to the local LAN is allowed."
|
||||
msgstr "使用出口節點時,允許存取區域網路。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:472
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:660
|
||||
msgid ""
|
||||
"You have unapplied changes. Save & Apply them first so the firewall "
|
||||
"matches the form."
|
||||
msgstr "您有尚未套用的變更。請先儲存並套用,防火牆才會與表單內容一致。"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:696
|
||||
msgid ""
|
||||
"[Important] Log in to the Tailscale admin console and manually enable "
|
||||
"\"Subnet Routes\" for this device."
|
||||
msgstr "【重要】 登入 Tailscale 管理控制台,在裝置設定中手動授權子網路由。"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:99
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:719
|
||||
msgid ""
|
||||
"[Important] Step 3 cannot be done here: it needs your Tailscale "
|
||||
"account."
|
||||
msgstr "[重要] 步驟 3 無法在此完成:需要您的 Tailscale 帳號。"
|
||||
|
||||
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:221
|
||||
msgid "ago"
|
||||
msgstr "前"
|
||||
|
||||
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:448
|
||||
msgid ""
|
||||
"and enables Masquerading and MSS Clamping (MTU fix) to ensure stable "
|
||||
"connections."
|
||||
msgstr "並啟用IP偽裝和 MSS 箝制(MTU 修復)以確保連線穩定。"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "Completely disable all Tailscale firewall auto-configuration, including "
|
||||
#~ "zone creation, forwarding rules, and netfilter settings."
|
||||
#~ msgstr ""
|
||||
#~ "完全停用所有 Tailscale 防火牆自動設定,包括區域建立、轉送規則和 netfilter "
|
||||
#~ "設定。"
|
||||
#~ "Completely disable all Tailscale firewall auto-configuration, "
|
||||
#~ "including zone creation, forwarding rules, and netfilter settings."
|
||||
#~ msgstr "完全停用所有 Tailscale 防火牆自動設定,包括區域建立、轉送規則和 netfilter 設定。"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "Optional: Specify a custom control server URL (e.g., a Headscale "
|
||||
#~ "instance, https://example.com)."
|
||||
#~ msgstr ""
|
||||
#~ "可選:指定一個自定義控制伺服器 URL (例如,一個 Headscale 例項,https://"
|
||||
#~ "example.com)。"
|
||||
#~ msgstr "可選:指定一個自定義控制伺服器 URL (例如,一個 Headscale 例項,https://example.com)。"
|
||||
|
||||
#~ msgid "Applying changes..."
|
||||
#~ msgstr "正在套用更改..."
|
||||
@@ -599,3 +709,24 @@ msgstr "並啟用IP偽裝和 MSS 箝制(MTU 修復)以確保連線穩定。"
|
||||
|
||||
#~ msgid "Tailscale settings applied successfully."
|
||||
#~ msgstr "Tailscale 設定已成功套用。"
|
||||
|
||||
#~ msgid "Failed to get login URL: Invalid response from server."
|
||||
#~ msgstr "獲取登入 URL 失敗: 伺服器回應無效。"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "It automatically creates the tailscale interface, sets up firewall "
|
||||
#~ "zones for LAN <-> Tailscale forwarding,"
|
||||
#~ msgstr "它會自動建立 Tailscale 介面,並設定允許 LAN <-> Tailscale 防火牆區域轉送。"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "and enables Masquerading and MSS Clamping (MTU fix) to ensure stable "
|
||||
#~ "connections."
|
||||
#~ msgstr "並啟用IP偽裝和 MSS 箝制(MTU 修復)以確保連線穩定。"
|
||||
|
||||
#~ msgid "4. On the client, choose this device under \"Exit node\"."
|
||||
#~ msgstr "4. 在用戶端於「出口節點」選擇此裝置。"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "It creates the tailscale interface, adds it to a firewall zone, and "
|
||||
#~ "sets up forwarding between LAN, Tailscale and WAN."
|
||||
#~ msgstr "它會建立 tailscale 介面、加入防火牆區域,並設定 LAN、Tailscale 與 WAN 之間的轉送。"
|
||||
@@ -6,6 +6,15 @@ STOP=10
|
||||
|
||||
NAME=tailscale-settings
|
||||
|
||||
# State files live on tmpfs so they are rebuilt after every boot.
|
||||
STATE_DIR=/var/run/tailscale-settings
|
||||
DAEMON_SIG_FILE="$STATE_DIR/daemon"
|
||||
ENABLED_FILE="$STATE_DIR/enabled"
|
||||
DISABLE_REQUEST_FILE="$STATE_DIR/disable-requested"
|
||||
DNS_MODE_FILE="$STATE_DIR/dns_mode"
|
||||
|
||||
TAILSCALE_INIT=/etc/init.d/tailscale
|
||||
|
||||
# Compare version strings: return 0 (true) if $1 >= $2
|
||||
version_gte() {
|
||||
awk -v a="${1%%-*}" -v b="${2%%-*}" 'BEGIN {
|
||||
@@ -21,6 +30,31 @@ version_gte() {
|
||||
return $?
|
||||
}
|
||||
|
||||
# Path of the tailscale daemon state file, as configured for the daemon init.
|
||||
tailscale_state_file() {
|
||||
local state_file
|
||||
config_get state_file settings state_file '/etc/tailscale/tailscaled.state'
|
||||
echo "$state_file"
|
||||
}
|
||||
|
||||
# Fingerprint of the UCI options that are only read by the daemon init script
|
||||
# (they end up as procd env/command arguments, so changing them needs a restart).
|
||||
daemon_signature() {
|
||||
local fw_mode port state_file log_stdout log_stderr
|
||||
config_get fw_mode settings fw_mode 'nftables'
|
||||
config_get port settings port '41641'
|
||||
config_get state_file settings state_file '/etc/tailscale/tailscaled.state'
|
||||
config_get_bool log_stdout settings log_stdout 1
|
||||
config_get_bool log_stderr settings log_stderr 1
|
||||
echo "$fw_mode|$port|$state_file|$log_stdout|$log_stderr"
|
||||
}
|
||||
|
||||
# Returns 0 when the tailscale daemon is managed and currently running.
|
||||
tailscale_running() {
|
||||
[ -x "$TAILSCALE_INIT" ] || return 1
|
||||
"$TAILSCALE_INIT" running >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Add/remove DNS forward entry in dnsmasq for MagicDNS via OpenWrt
|
||||
setup_tailscale_dns_forward() {
|
||||
if [ ! -f /etc/config/dhcp ] || [ ! -x /etc/init.d/dnsmasq ]; then
|
||||
@@ -49,27 +83,52 @@ remove_tailscale_dns_forward() {
|
||||
fi
|
||||
|
||||
local entry="/.ts.net/100.100.100.100"
|
||||
local idx=0
|
||||
local removed=0
|
||||
while true; do
|
||||
local val
|
||||
val=$(uci -q get dhcp.@dnsmasq[0].server."${idx}" 2>/dev/null)
|
||||
[ -z "$val" ] && break
|
||||
if [ "$val" = "$entry" ]; then
|
||||
uci -q del_list dhcp.@dnsmasq[0].server="$entry"
|
||||
removed=1
|
||||
break
|
||||
fi
|
||||
idx=$((idx + 1))
|
||||
local found=0 s
|
||||
for s in $(uci -q get dhcp.@dnsmasq[0].server); do
|
||||
[ "$s" = "$entry" ] && found=1
|
||||
done
|
||||
|
||||
if [ "$removed" = "1" ]; then
|
||||
if [ "$found" = "1" ]; then
|
||||
uci -q del_list dhcp.@dnsmasq[0].server="$entry"
|
||||
uci commit dhcp
|
||||
/etc/init.d/dnsmasq reload 2>/dev/null || /etc/init.d/dnsmasq restart 2>/dev/null
|
||||
logger -t "$NAME" "Removed Tailscale DNS forward entry: $entry"
|
||||
fi
|
||||
}
|
||||
|
||||
# Keep the dnsmasq forward record in sync with dns_mode.
|
||||
#
|
||||
# Only magicdns takes resolution away from dnsmasq; both openwrt_forward and
|
||||
# disabled leave dnsmasq authoritative, so the forward entry stays in place
|
||||
# unless the user actually switches to magicdns. This mirrors what the old
|
||||
# code did for openwrt_forward but stops deleting the entry when the mode goes
|
||||
# back to disabled, which used to silently break *.ts.net resolution.
|
||||
apply_dns_forward() {
|
||||
local dns_mode prev=''
|
||||
config_get dns_mode settings dns_mode 'disabled'
|
||||
|
||||
[ -f "$DNS_MODE_FILE" ] && prev=$(cat "$DNS_MODE_FILE")
|
||||
|
||||
if [ "$dns_mode" = "$prev" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "$dns_mode" in
|
||||
magicdns)
|
||||
remove_tailscale_dns_forward
|
||||
;;
|
||||
openwrt_forward)
|
||||
setup_tailscale_dns_forward
|
||||
;;
|
||||
*)
|
||||
# disabled: leave whatever the previous mode put in place,
|
||||
# dnsmasq is authoritative either way
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "$dns_mode" > "$DNS_MODE_FILE"
|
||||
}
|
||||
|
||||
apply_settings() {
|
||||
local ts_bin
|
||||
if [ -x /usr/sbin/tailscale ]; then
|
||||
@@ -143,16 +202,10 @@ apply_settings() {
|
||||
case "$dns_mode" in
|
||||
magicdns)
|
||||
set -- "$@" --accept-dns=true
|
||||
remove_tailscale_dns_forward
|
||||
;;
|
||||
openwrt_forward)
|
||||
set -- "$@" --accept-dns=false
|
||||
setup_tailscale_dns_forward
|
||||
;;
|
||||
*)
|
||||
# disabled (default)
|
||||
# openwrt_forward and disabled both let dnsmasq own resolution
|
||||
set -- "$@" --accept-dns=false
|
||||
remove_tailscale_dns_forward
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -186,7 +239,7 @@ apply_settings() {
|
||||
# --relay-server-port (requires tailscale >= 1.90.5)
|
||||
local ts_ver
|
||||
ts_ver=$("$ts_bin" version 2>/dev/null | head -n1)
|
||||
if version_gte "$ts_ver" "1.90.5"; then
|
||||
if [ -n "$ts_ver" ] && version_gte "$ts_ver" "1.90.5"; then
|
||||
if [ "$enable_relay" = "1" ]; then
|
||||
set -- "$@" "--relay-server-port=${relay_server_port:-40000}"
|
||||
else
|
||||
@@ -208,26 +261,146 @@ apply_settings() {
|
||||
return $ret
|
||||
}
|
||||
|
||||
handle_service_state() {
|
||||
config_load tailscale
|
||||
local service_enabled
|
||||
# Enable or disable autostart and the running daemon.
|
||||
#
|
||||
# The enable direction is applied here because starting a stopped daemon is
|
||||
# harmless. The disable direction is only *armed* from the RPC (before the
|
||||
# commit), because stopping is not reversible and the UCI rollback timer can
|
||||
# still fire afterwards: rpcd commits first and arms the timer last, and a
|
||||
# rollback only restores files. Keeping the actual stop in the reload handler
|
||||
# means a rolled back config lands on service_enabled=1 while the daemon is
|
||||
# still up, and the next apply simply drops the request.
|
||||
sync_service_enabled() {
|
||||
local service_enabled=1 prev=''
|
||||
local requested=0
|
||||
config_get service_enabled settings service_enabled '1'
|
||||
|
||||
if [ "$service_enabled" = "0" ]; then
|
||||
logger -t "$NAME" "Service is disabled, stopping tailscale"
|
||||
/etc/init.d/tailscale stop 2>/dev/null
|
||||
/etc/init.d/tailscale disable 2>/dev/null
|
||||
killall tailscaled 2>/dev/null
|
||||
else
|
||||
logger -t "$NAME" "Service is enabled, ensuring tailscale is running"
|
||||
/etc/init.d/tailscale enable 2>/dev/null
|
||||
/etc/init.d/tailscale start 2>/dev/null
|
||||
apply_settings
|
||||
|
||||
prev=$(cat "$ENABLED_FILE" 2>/dev/null)
|
||||
case "$prev" in
|
||||
0|1) ;;
|
||||
*)
|
||||
# No usable record of the last applied value (first apply after
|
||||
# an upgrade, or a truncated file). Adopt the configuration as
|
||||
# the baseline instead of treating it as a transition: starting
|
||||
# a daemon here would run start_service, which arms another UCI
|
||||
# rollback window right after the one this apply is inside.
|
||||
echo "$service_enabled" > "$ENABLED_FILE"
|
||||
prev="$service_enabled"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -f "$DISABLE_REQUEST_FILE" ]; then
|
||||
requested=1
|
||||
fi
|
||||
|
||||
if [ "$service_enabled" = "0" ]; then
|
||||
if [ "$prev" = "0" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Only stop when the UI asked for it, a hand edited config is
|
||||
# respected for autostart but never kills a running tunnel.
|
||||
if [ "$requested" -eq 0 ]; then
|
||||
logger -t "$NAME" "service_enabled=0 for the first time, disabling autostart only"
|
||||
"$TAILSCALE_INIT" disable 2>/dev/null
|
||||
echo "0" > "$ENABLED_FILE"
|
||||
return 0
|
||||
fi
|
||||
|
||||
logger -t "$NAME" "Service disabled, stopping tailscale"
|
||||
"$TAILSCALE_INIT" stop 2>/dev/null
|
||||
"$TAILSCALE_INIT" disable 2>/dev/null
|
||||
rm -f "$DISABLE_REQUEST_FILE"
|
||||
echo "0" > "$ENABLED_FILE"
|
||||
return 0
|
||||
fi
|
||||
|
||||
rm -f "$DISABLE_REQUEST_FILE"
|
||||
|
||||
# The service should run. A daemon that is already up must not be touched:
|
||||
# that is the whole point of this handler. A daemon that is down is only
|
||||
# brought back when the user explicitly turned the service on in this
|
||||
# apply, not because some unrelated option happened to be saved, and not
|
||||
# because it crashed or was stopped by hand.
|
||||
if tailscale_running; then
|
||||
[ "$prev" = "1" ] || echo "1" > "$ENABLED_FILE"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$prev" = "1" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
logger -t "$NAME" "Service enabled, starting tailscale"
|
||||
"$TAILSCALE_INIT" enable 2>/dev/null
|
||||
tailscale_running || "$TAILSCALE_INIT" start 2>/dev/null
|
||||
echo "1" > "$ENABLED_FILE"
|
||||
}
|
||||
|
||||
# Wait briefly for procd to bring the daemon back after a restart. Called from
|
||||
# reload_service, which rc.common runs outside of any function, so the loop
|
||||
# counter has to live in a function of its own to stay out of the global scope.
|
||||
wait_for_daemon() {
|
||||
local waited=0
|
||||
while [ "$waited" -lt 2 ]; do
|
||||
tailscale_running && return 0
|
||||
sleep 1
|
||||
waited=$((waited + 1))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
handle_service_state() {
|
||||
if [ ! -x "$TAILSCALE_INIT" ]; then
|
||||
logger -t "$NAME" "tailscale init script not found, keeping settings only"
|
||||
apply_settings
|
||||
apply_dns_forward
|
||||
return 0
|
||||
fi
|
||||
|
||||
sync_service_enabled
|
||||
|
||||
# Nothing may touch a running daemon here. The upstream init runs
|
||||
# 'tailscaled --cleanup' before handing the instance to procd, which tears
|
||||
# down tailscale0 addresses, table 52 routes and the netfilter rules while
|
||||
# the old process is still alive. Running 'start' on a live service (or
|
||||
# 'restart' without reason) therefore leaves a node that still answers
|
||||
# 'tailscale status' and 'tailscale ping' but has no working kernel state.
|
||||
if ! tailscale_running; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local desired
|
||||
desired=$(daemon_signature)
|
||||
|
||||
if [ -f "$DAEMON_SIG_FILE" ] && [ "$(cat "$DAEMON_SIG_FILE")" != "$desired" ]; then
|
||||
logger -t "$NAME" "Daemon options changed, restarting tailscale"
|
||||
"$TAILSCALE_INIT" restart 2>/dev/null
|
||||
wait_for_daemon || logger -t "$NAME" "tailscale did not come back within 2s"
|
||||
fi
|
||||
|
||||
echo "$desired" > "$DAEMON_SIG_FILE"
|
||||
|
||||
apply_settings
|
||||
apply_dns_forward
|
||||
}
|
||||
|
||||
start_service() {
|
||||
mkdir -p "$STATE_DIR"
|
||||
config_load tailscale
|
||||
daemon_signature > "$DAEMON_SIG_FILE"
|
||||
# Record the desired flag without acting on it: at boot the service state
|
||||
# is already correct and acting here would start or stop a daemon we do
|
||||
# not own. Only seed the file when there is nothing to inherit, so a
|
||||
# manual `disable` in the shell survives until the reload handler sees a
|
||||
# real transition.
|
||||
if [ ! -f "$ENABLED_FILE" ]; then
|
||||
local service_enabled=1
|
||||
config_get service_enabled settings service_enabled '1'
|
||||
echo "$service_enabled" > "$ENABLED_FILE"
|
||||
fi
|
||||
apply_settings
|
||||
apply_dns_forward
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
@@ -235,5 +408,6 @@ service_triggers() {
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
mkdir -p "$STATE_DIR"
|
||||
handle_service_state
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/bin/sh
|
||||
# Background worker for the "Auto Configure Firewall" action.
|
||||
#
|
||||
# Kept outside the rpcd plugin on purpose: the RPC must return before the
|
||||
# network is reloaded, otherwise a request coming through the network being
|
||||
# reloaded (reverse proxy, or LuCI over Tailscale itself) dies with an XHR
|
||||
# abort and the user cannot tell whether anything happened.
|
||||
#
|
||||
# The status file is line based rather than JSON so that a message containing
|
||||
# a backslash, a quote or a tab cannot corrupt it: whoever reads it builds the
|
||||
# JSON itself. Line 4 is the run id handed over by the RPC, so a result from a
|
||||
# previous run can never be mistaken for this one.
|
||||
#
|
||||
# Usage: tailscale-setup-firewall <run-id>
|
||||
|
||||
STATE_FILE=/var/run/tailscale-fwsetup.status
|
||||
LOCK_FILE=/var/run/tailscale-fwsetup.lock
|
||||
|
||||
RUN_ID="$1"
|
||||
|
||||
write_state() {
|
||||
{
|
||||
printf '%s\n' "$1"
|
||||
printf '%s\n' "$2"
|
||||
# one line only: the reader takes fixed line numbers, so a message that
|
||||
# wrapped would shift the run id off the last line. tr cannot strip a
|
||||
# real newline, sed can.
|
||||
if [ -n "$3" ]; then
|
||||
printf '%s' "$3" | sed -e 's/\r//g' -e ':a' -e 'N' -e '$!ba' -e 's/\n/ /g'
|
||||
fi
|
||||
printf '\n'
|
||||
printf '%s\n' "$RUN_ID"
|
||||
} > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
|
||||
}
|
||||
|
||||
# Only one worker at a time: a second click while a reload is running would
|
||||
# queue another network reload on top of an unfinished one. mkdir is atomic and
|
||||
# is the only portable primitive available here; a test-then-write pair lets
|
||||
# two workers through at the same moment. Stale locks are recognised by age so
|
||||
# a recycled pid or a killed worker cannot block forever.
|
||||
LOCK_DIR="$LOCK_FILE.d"
|
||||
lock_age() {
|
||||
local ts now
|
||||
ts=$(cat "$LOCK_DIR/ts" 2>/dev/null)
|
||||
case "$ts" in ''|*[!0-9]*) echo 999999; return ;; esac
|
||||
now=$(date +%s)
|
||||
echo $((now - ts))
|
||||
}
|
||||
|
||||
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
|
||||
if [ "$(lock_age)" -gt 300 ]; then
|
||||
# the holder is gone; take the lock over
|
||||
rm -rf "$LOCK_DIR"
|
||||
mkdir "$LOCK_DIR" 2>/dev/null || exit 0
|
||||
else
|
||||
# someone else is already reloading: say so instead of leaving the
|
||||
# status file at "queued" and letting the UI time out
|
||||
write_state busy locked "Another firewall reload is already running"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
echo "$$" > "$LOCK_DIR/pid"
|
||||
date +%s > "$LOCK_DIR/ts"
|
||||
|
||||
trap 'write_state failed aborted "Worker terminated unexpectedly"; rm -rf "$LOCK_DIR"' EXIT HUP INT TERM
|
||||
|
||||
write_state running network "Reloading network"
|
||||
|
||||
if ! /etc/init.d/network reload >/dev/null 2>&1; then
|
||||
write_state failed network "Network reload failed"
|
||||
trap - EXIT HUP INT TERM
|
||||
rm -rf "$LOCK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
write_state running firewall "Reloading firewall"
|
||||
|
||||
if ! /etc/init.d/firewall reload >/dev/null 2>&1; then
|
||||
write_state failed firewall "Firewall reload failed"
|
||||
trap - EXIT HUP INT TERM
|
||||
rm -rf "$LOCK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
write_state done done "Firewall configuration applied"
|
||||
trap - EXIT HUP INT TERM
|
||||
rm -rf "$LOCK_DIR"
|
||||
+4
-2
@@ -7,7 +7,8 @@
|
||||
"get_status",
|
||||
"get_settings",
|
||||
"get_subroutes",
|
||||
"get_logs"
|
||||
"get_logs",
|
||||
"get_firewall_status"
|
||||
]
|
||||
},
|
||||
"uci": [ "tailscale" ]
|
||||
@@ -17,7 +18,8 @@
|
||||
"tailscale": [
|
||||
"do_login",
|
||||
"do_logout",
|
||||
"setup_firewall"
|
||||
"setup_firewall",
|
||||
"set_service_enabled"
|
||||
]
|
||||
},
|
||||
"uci": [ "tailscale" ]
|
||||
|
||||
@@ -149,7 +149,6 @@ methods.do_login = {
|
||||
if (status.status != 'logout') {
|
||||
return { error: 'Tailscale is already logged in and running.' };
|
||||
}
|
||||
|
||||
// --- 1. Prepare and Run Login Command (Once) ---
|
||||
const loginserver = trim(form_data.loginserver) || '';
|
||||
const loginserver_authkey = trim(form_data.loginserver_authkey) || '';
|
||||
@@ -177,7 +176,7 @@ methods.do_login = {
|
||||
let parts = split(trline, ' ');
|
||||
for (let part in parts) {
|
||||
if (index(part, 'http') != -1) {
|
||||
return { url: part };
|
||||
return { success: true, url: part };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,7 +198,7 @@ methods.do_logout = {
|
||||
if (logout_result.code != 0) {
|
||||
return { error: 'Failed to logout: ' + logout_result.stderr };
|
||||
}
|
||||
return { success: true };
|
||||
return { success: true, message: 'Logged out.' };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -241,6 +240,7 @@ methods.setup_firewall = {
|
||||
|
||||
let changed_network = false;
|
||||
let changed_firewall = false;
|
||||
let run_id = 0;
|
||||
|
||||
// 1. config Network Interface
|
||||
let net_ts = uci.get('network', 'tailscale');
|
||||
@@ -335,24 +335,57 @@ methods.setup_firewall = {
|
||||
changed_firewall = true;
|
||||
}
|
||||
|
||||
// 4. save
|
||||
// 4. save and hand the reload to a background worker
|
||||
//
|
||||
// Reloading network/firewall synchronously here was the reason the
|
||||
// button failed through a reverse proxy: the reload tears down the
|
||||
// connection carrying this very request. The worker records its
|
||||
// progress in /var/run/tailscale-fwsetup.status and the UI polls it.
|
||||
if (changed_network) {
|
||||
uci.save('network');
|
||||
uci.commit('network');
|
||||
exec('/etc/init.d/network reload');
|
||||
}
|
||||
|
||||
if (changed_firewall) {
|
||||
uci.save('firewall');
|
||||
uci.commit('firewall');
|
||||
exec('/etc/init.d/firewall reload');
|
||||
}
|
||||
|
||||
if (changed_network || changed_firewall) {
|
||||
if (!access('/usr/libexec/tailscale-setup-firewall')) {
|
||||
return { error: 'Firewall worker /usr/libexec/tailscale-setup-firewall is missing. Reinstall the package.' };
|
||||
}
|
||||
|
||||
// The run id ties the polled result to this request, and the
|
||||
// queued state replaces any previous result before the worker
|
||||
// gets a chance to overwrite it. Without both, a poll arriving
|
||||
// while the worker is still starting would read the previous
|
||||
// run's "done" and report success for work that has not run.
|
||||
run_id = time();
|
||||
let st = writefile('/var/run/tailscale-fwsetup.status.tmp',
|
||||
'queued\nstart\n\n' + run_id + '\n');
|
||||
if (st == null || rename('/var/run/tailscale-fwsetup.status.tmp', '/var/run/tailscale-fwsetup.status') == null) {
|
||||
// Without a writable status file the poll can never see a
|
||||
// result, so say so instead of letting the UI wait it out.
|
||||
return { error: 'Cannot write the firewall status file in /var/run. Configuration was not changed.' };
|
||||
}
|
||||
|
||||
// The background shell exits immediately, so its status only
|
||||
// tells us whether sh itself started. Anything the worker
|
||||
// reports afterwards lands in the status file.
|
||||
let spawn = exec('/bin/sh -c ' + shell_quote('/usr/libexec/tailscale-setup-firewall ' + run_id + ' >/dev/null 2>&1 &'));
|
||||
if (spawn.code != 0) {
|
||||
return { error: 'Failed to start the firewall worker: ' + join('\n', spawn.stdout) };
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
changed_network: changed_network,
|
||||
changed_firewall: changed_firewall,
|
||||
message: (changed_network || changed_firewall) ? 'Tailscale firewall/interface configuration applied.' : 'Tailscale firewall/interface already configured.'
|
||||
job: (changed_network || changed_firewall) ? 'running' : 'idle',
|
||||
run_id: run_id,
|
||||
message: (changed_network || changed_firewall) ? 'Configuration committed, reloading network and firewall in the background.' : 'Tailscale firewall/interface already configured.'
|
||||
};
|
||||
|
||||
} catch (e) {
|
||||
@@ -361,6 +394,75 @@ methods.setup_firewall = {
|
||||
}
|
||||
};
|
||||
|
||||
// Record that the user asked to disable the service.
|
||||
//
|
||||
// The actual stop happens in /etc/init.d/tailscale-settings on the reload
|
||||
// trigger, which runs right after rpcd commits and *before* it arms the LuCI
|
||||
// rollback timer. Stopping here would not survive a rollback: rpcd restores
|
||||
// the config files but never restarts a daemon it did not stop, so the user
|
||||
// would end up with service_enabled=1 and a dead tunnel. The request file is
|
||||
// dropped by the reload handler as soon as the config says enabled again.
|
||||
methods.set_service_enabled = {
|
||||
args: { enabled: '' },
|
||||
call: function(request) {
|
||||
let enabled = request?.args?.enabled;
|
||||
if (enabled != '0' && enabled != '1') {
|
||||
return { error: 'Invalid enabled value, expected 0 or 1.' };
|
||||
}
|
||||
|
||||
if (enabled == '1') {
|
||||
let r = exec('rm -f /var/run/tailscale-settings/disable-requested');
|
||||
if (r.code != 0) {
|
||||
return { error: 'Failed to clear the disable request: ' + join('\n', r.stdout) };
|
||||
}
|
||||
return { success: true, armed: false };
|
||||
}
|
||||
|
||||
// No daemon probe here: get_status shells out to 'tailscale status'
|
||||
// and would delay the apply for nothing. A request armed on a box
|
||||
// without the tailscale package is simply ignored by the reload
|
||||
// handler, which bails out when the init script is missing.
|
||||
let r = exec('mkdir -p /var/run/tailscale-settings && touch /var/run/tailscale-settings/disable-requested');
|
||||
if (r.code != 0) {
|
||||
return { error: 'Failed to arm the disable request: ' + join('\n', r.stdout) };
|
||||
}
|
||||
|
||||
return { success: true, armed: true };
|
||||
}
|
||||
};
|
||||
|
||||
// Result of the last background firewall setup job.
|
||||
//
|
||||
// Read here rather than in the browser so the ACL needs no file rights: the
|
||||
// ucode plugin runs as root and rpcd only enforces path ACLs on the file
|
||||
// ubus object.
|
||||
methods.get_firewall_status = {
|
||||
call: function() {
|
||||
let path = '/var/run/tailscale-fwsetup.status';
|
||||
if (!access(path)) {
|
||||
return { success: true, state: 'idle', step: '', message: '', run_id: 0 };
|
||||
}
|
||||
try {
|
||||
let raw = readfile(path);
|
||||
if (raw == null) {
|
||||
return { success: true, state: 'idle', step: '', message: '', run_id: 0 };
|
||||
}
|
||||
let lines = split(rtrim(raw, '\n'), '\n');
|
||||
return {
|
||||
success: true,
|
||||
state: lines[0] || 'unknown',
|
||||
step: lines[1] || '',
|
||||
message: lines[2] || '',
|
||||
// the run id is always the last line, so a message that somehow
|
||||
// still contains a newline cannot shift it into line 3
|
||||
run_id: int(lines[length(lines) - 1]) || 0
|
||||
};
|
||||
} catch (e) {
|
||||
return { success: true, state: 'unknown', step: '', message: 'Unreadable job state: ' + e, run_id: 0 };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
methods.get_logs = {
|
||||
args: { lines: 200 },
|
||||
call: function(request) {
|
||||
@@ -368,9 +470,9 @@ methods.get_logs = {
|
||||
let cmd = 'logread -l ' + lines + ' 2>/dev/null | grep -i -E "tailscale" || true';
|
||||
let result = exec(cmd);
|
||||
if (result.code == 0) {
|
||||
return { logs: result.stdout };
|
||||
return { success: true, logs: result.stdout };
|
||||
}
|
||||
return { logs: [], error: join(' ', result.stderr) };
|
||||
return { success: false, logs: [], error: join(' ', result.stderr) };
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=mptcp
|
||||
PKG_VERSION:=6.1
|
||||
PKG_RELEASE:=28
|
||||
PKG_RELEASE:=29
|
||||
|
||||
PKG_MAINTAINER:=Ycarus (Yannick Chabanois) <ycarus@zugaina.org>
|
||||
PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME)
|
||||
|
||||
@@ -265,28 +265,43 @@ interface_macaddr_count() {
|
||||
[ "$macaddr" = "$dmacaddr" ] && [ "${intf%.*}" = "$intf" ] && nbmac=$((nbmac+1))
|
||||
}
|
||||
|
||||
# procd reloads this init on every interface event. A "uci commit" writes
|
||||
# the whole pending delta of the config, other processes' staged changes
|
||||
# included (openmptcprouter-vps's uncommitted token went to flash on each
|
||||
# reload), so an option is only set when it differs, and a config is only
|
||||
# committed when this run changed it.
|
||||
_mptcp_uci_set() {
|
||||
[ "$(uci -q get "$1")" = "$2" ] && return 0
|
||||
uci -q set "$1=$2"
|
||||
_mptcp_uci_changed="$_mptcp_uci_changed ${1%%.*}"
|
||||
}
|
||||
|
||||
_mptcp_uci_delete() {
|
||||
uci -q get "$1" >/dev/null || return 0
|
||||
uci -q delete "$1"
|
||||
_mptcp_uci_changed="$_mptcp_uci_changed ${1%%.*}"
|
||||
}
|
||||
|
||||
_mptcp_uci_commit() {
|
||||
case " $_mptcp_uci_changed " in
|
||||
*" $1 "*) uci -q commit "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
interface_max_metric() {
|
||||
local config="$1"
|
||||
case "$1" in
|
||||
omrvpn)
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
set network.${config}.metric=1500
|
||||
set network.${config}.ip4table=1500
|
||||
set network.${config}.ip6table=61500
|
||||
commit network
|
||||
set openmptcprouter.${config}.metric=1500
|
||||
commit openmptcprouter
|
||||
EOF
|
||||
_mptcp_uci_set network.${config}.metric 1500
|
||||
_mptcp_uci_set network.${config}.ip4table 1500
|
||||
_mptcp_uci_set network.${config}.ip6table 61500
|
||||
_mptcp_uci_set openmptcprouter.${config}.metric 1500
|
||||
;;
|
||||
omr6in4)
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
set network.${config}.metric=1201
|
||||
set network.${config}.ip4table=1201
|
||||
set network.${config}.ip6table=61201
|
||||
commit network
|
||||
set openmptcprouter.${config}.metric=1201
|
||||
commit openmptcprouter
|
||||
EOF
|
||||
_mptcp_uci_set network.${config}.metric 1201
|
||||
_mptcp_uci_set network.${config}.ip4table 1201
|
||||
_mptcp_uci_set network.${config}.ip6table 61201
|
||||
_mptcp_uci_set openmptcprouter.${config}.metric 1201
|
||||
;;
|
||||
lan)
|
||||
# only when needed: this runs on every reload, sed -i writes flash
|
||||
@@ -477,10 +492,10 @@ interface_multipath_settings() {
|
||||
# metric="9998"
|
||||
fi
|
||||
config_set "$config" metric $metric
|
||||
uci -q set network.${config}.metric=$metric
|
||||
uci -q set network.${config}.ip4table=$metric
|
||||
uci -q set network.${config}.ip6table=6$metric
|
||||
uci -q set openmptcprouter.${config}.metric=$metric
|
||||
_mptcp_uci_set network.${config}.metric "$metric"
|
||||
_mptcp_uci_set network.${config}.ip4table "$metric"
|
||||
_mptcp_uci_set network.${config}.ip6table "6$metric"
|
||||
_mptcp_uci_set openmptcprouter.${config}.metric "$metric"
|
||||
config_get mode "$config" multipath
|
||||
config_get disabled "$config" disabled
|
||||
[ "$disabled" = "1" ] && mode="off"
|
||||
@@ -504,7 +519,7 @@ interface_multipath_settings() {
|
||||
mode="$(uci -q get openmptcprouter.${config}.multipath)"
|
||||
[ -n "$mode" ] && {
|
||||
logger -t "MPTCP" "No multipath setting for $config, restoring $mode from openmptcprouter config"
|
||||
uci -q set network.${config}.multipath="$mode"
|
||||
_mptcp_uci_set network.${config}.multipath "$mode"
|
||||
}
|
||||
}
|
||||
[ "$mode" = "" ] && {
|
||||
@@ -513,8 +528,8 @@ interface_multipath_settings() {
|
||||
[ "$config" = "omr6in4" ] && mode="off"
|
||||
[ "$mode" = "" ] && mode="off"
|
||||
logger -t "MPTCP" "Multipath not defined for $config set to $mode"
|
||||
uci -q set network.${config}.multipath="$mode"
|
||||
uci -q set openmptcprouter.${config}.multipath="$mode"
|
||||
_mptcp_uci_set network.${config}.multipath "$mode"
|
||||
_mptcp_uci_set openmptcprouter.${config}.multipath "$mode"
|
||||
}
|
||||
[ "$mode" != "off" ] && {
|
||||
# LRO, TSO and UFO break MPTCP packets scheduling, always disable them on multipath interfaces
|
||||
@@ -526,9 +541,9 @@ interface_multipath_settings() {
|
||||
[ -n "$mptcpintf" ] && mptcpintf="$mptcpintf $iface"
|
||||
[ -z "$mptcpintf" ] && mptcpintf="$iface"
|
||||
[ -z "$mptcpmintf" ] && mptcpmintf="$config"
|
||||
uci -q set network.${config}.defaultroute=0
|
||||
uci -q set network.${config}.peerdns=0
|
||||
uci -q delete network.${config}.dns
|
||||
_mptcp_uci_set network.${config}.defaultroute 0
|
||||
_mptcp_uci_set network.${config}.peerdns 0
|
||||
_mptcp_uci_delete network.${config}.dns
|
||||
echo '' > /etc/resolv.conf >/dev/null 2>&1
|
||||
}
|
||||
[ "$mode" = "master" ] && {
|
||||
@@ -537,14 +552,14 @@ interface_multipath_settings() {
|
||||
logger -t "MPTCP" "Multipath master already set, disable master for $config"
|
||||
mode="on"
|
||||
config_set "$config" multipath "on"
|
||||
uci -q set network.${config}.multipath="on"
|
||||
uci -q set openmptcprouter.${config}.multipath="on"
|
||||
_mptcp_uci_set network.${config}.multipath "on"
|
||||
_mptcp_uci_set openmptcprouter.${config}.multipath "on"
|
||||
else
|
||||
master="$config"
|
||||
fi
|
||||
}
|
||||
uci -q set openmptcprouter.${config}="interface"
|
||||
uci -q set openmptcprouter.${config}.multipath="$mode"
|
||||
_mptcp_uci_set openmptcprouter.${config} "interface"
|
||||
_mptcp_uci_set openmptcprouter.${config}.multipath "$mode"
|
||||
|
||||
config_get macaddr "$config" macaddr ""
|
||||
[ -n "$macaddr" ] && {
|
||||
@@ -552,7 +567,7 @@ interface_multipath_settings() {
|
||||
config_foreach interface_macaddr_count interface $macaddr
|
||||
[ "$nbmac" != "1" ] && {
|
||||
logger -t "MPTCP" "Duplicate MAC saved, removing"
|
||||
uci -q delete network.${config}.macaddr
|
||||
_mptcp_uci_delete network.${config}.macaddr
|
||||
}
|
||||
}
|
||||
|
||||
@@ -589,6 +604,13 @@ interface_multipath_settings() {
|
||||
config_get ipaddr $config ipaddr
|
||||
config_get gateway $config gateway
|
||||
config_get netmask $config netmask
|
||||
# ipaddr can be a list in CIDR notation, with no netmask: network
|
||||
# stayed empty and the WAN got no table route, rule or qdisc
|
||||
ipaddr="${ipaddr%% *}"
|
||||
case "$ipaddr" in */*)
|
||||
[ -z "$netmask" ] && netmask="${ipaddr#*/}"
|
||||
ipaddr="${ipaddr%%/*}"
|
||||
;; esac
|
||||
[ -n "$ipaddr" ] && [ -n "$netmask" ] && netmask=`ipcalc.sh $ipaddr/$netmask | sed -n '/PREFIX=/{;s/.*=//;s/ .*//;p;}'`
|
||||
[ -n "$ipaddr" ] && [ -n "$netmask" ] && network=`ipcalc.sh $ipaddr/$netmask | sed -n '/NETWORK=/{;s/.*=//;s/ .*//;p;}'`
|
||||
elif [ "$proto" != "gre" ]; then
|
||||
@@ -970,6 +992,7 @@ start_service() {
|
||||
mptcpmintf=""
|
||||
master=""
|
||||
count=0
|
||||
_mptcp_uci_changed=""
|
||||
config_load openmptcprouter
|
||||
config_foreach interface_max_metric interface
|
||||
|
||||
@@ -1000,13 +1023,13 @@ start_service() {
|
||||
intfmaster="$mptcpmintf"
|
||||
[ "$intfmaster" != "" ] && mptcp_has_master_multipath && {
|
||||
logger -t "MPTCP" "Master multipath interface disabled, setting it to $intfmaster"
|
||||
uci -q set network.${intfmaster}.multipath="master"
|
||||
uci -q set openmptcprouter.${intfmaster}.multipath="master"
|
||||
_mptcp_uci_set network.${intfmaster}.multipath "master"
|
||||
_mptcp_uci_set openmptcprouter.${intfmaster}.multipath "master"
|
||||
}
|
||||
fi
|
||||
fi
|
||||
[ -n "$(uci -q changes network)" ] && uci -q commit network
|
||||
[ -n "$(uci -q changes openmptcprouter)" ] && uci -q commit openmptcprouter
|
||||
_mptcp_uci_commit network
|
||||
_mptcp_uci_commit openmptcprouter
|
||||
case "$board_name" in
|
||||
*raspberry*) case "$board_name" in *4-model-b*|*5*) ;; *)
|
||||
ethtool --offload eth0 rx off tx off > /dev/null 2>&1
|
||||
|
||||
@@ -83,33 +83,27 @@ if [ "$(uci -q get network.globals.congestion)" = "bbr2" ] && [ -z "$(uname -a |
|
||||
commit network
|
||||
EOF
|
||||
fi
|
||||
if [ "$(uci -q get network.globals.mptcp_syn_retries)" = "1" ]; then
|
||||
# The old defaults (syn_retries 1, add_addr_accepted 1) are moved once:
|
||||
# this script runs again on every sysupgrade, and a 1 the user chose
|
||||
# afterwards was reset each time, as were the checksum and the debug level.
|
||||
# mptcp_defaults_version records which of these migrations already ran.
|
||||
if [ "$(uci -q get network.globals.mptcp_defaults_version)" != "1" ]; then
|
||||
if [ "$(uci -q get network.globals.mptcp_syn_retries)" = "1" ]; then
|
||||
uci -q set network.globals.mptcp_syn_retries=4
|
||||
fi
|
||||
if [ "$(uci -q get network.globals.mptcp_add_addr_accepted)" = "1" ]; then
|
||||
uci -q set network.globals.mptcp_add_addr_accepted=8
|
||||
fi
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
set network.globals.mptcp_syn_retries=4
|
||||
set network.globals.mptcp_defaults_version=1
|
||||
commit network
|
||||
EOF
|
||||
fi
|
||||
if [ "$(uci -q get network.globals.mptcp_checksum)" != "0" ]; then
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
set network.globals.mptcp_checksum=0
|
||||
commit network
|
||||
EOF
|
||||
fi
|
||||
if [ "$(uci -q get network.globals.mptcp_checksum)" = "enable" ]; then
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
set network.globals.mptcp_checksum=1
|
||||
EOF
|
||||
fi
|
||||
if [ "$(uci -q get network.globals.mptcp_checksum)" = "disable" ]; then
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
set network.globals.mptcp_checksum=0
|
||||
EOF
|
||||
fi
|
||||
if [ "$(uci -q get network.globals.mptcp_debug)" != "0" ]; then
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
set network.globals.mptcp_debug=0
|
||||
EOF
|
||||
fi
|
||||
# enable/disable were written by an older release: the sysctl takes 1/0
|
||||
case "$(uci -q get network.globals.mptcp_checksum)" in
|
||||
enable) uci -q set network.globals.mptcp_checksum=1; uci -q commit network ;;
|
||||
disable) uci -q set network.globals.mptcp_checksum=0; uci -q commit network ;;
|
||||
esac
|
||||
|
||||
if [ "$(uci -q show network.globals | grep mptcp_fullmesh)" = "" ]; then
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
@@ -127,12 +121,33 @@ if [ "$(uci -q get network.globals.mptcp_subflows)" = "" ]; then
|
||||
commit network
|
||||
EOF
|
||||
fi
|
||||
if [ "$(uci -q get network.globals.mptcp_add_addr_accepted)" = "1" ]; then
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
set network.globals.mptcp_add_addr_accepted=8
|
||||
commit network
|
||||
EOF
|
||||
fi
|
||||
# A multipath_weight out of 1-256 (0, 300, set by hand or by an older
|
||||
# release) failed the datatype of the MPTCP pages and blocked every save
|
||||
# there. Stored as 040-multipath-weight reads it: not a number is unset
|
||||
# (100), leading zeros dropped, 0 is 1, over 256 is 256.
|
||||
_weight_changed=""
|
||||
for _sec in $(uci -q show network | sed -n "s/^network\.\([^.]*\)\.multipath_weight=.*/\1/p"); do
|
||||
_weight="$(uci -q get "network.${_sec}.multipath_weight")"
|
||||
case "$_weight" in
|
||||
''|*[!0-9]*) _new_weight="" ;;
|
||||
*)
|
||||
_new_weight="${_weight#"${_weight%%[!0]*}"}"
|
||||
if [ -z "$_new_weight" ]; then
|
||||
_new_weight=1
|
||||
elif [ "${#_new_weight}" -gt 3 ] || [ "$_new_weight" -gt 256 ]; then
|
||||
_new_weight=256
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
[ "$_new_weight" = "$_weight" ] && continue
|
||||
if [ -z "$_new_weight" ]; then
|
||||
uci -q delete "network.${_sec}.multipath_weight"
|
||||
else
|
||||
uci -q set "network.${_sec}.multipath_weight=${_new_weight}"
|
||||
fi
|
||||
_weight_changed=1
|
||||
done
|
||||
[ -n "$_weight_changed" ] && uci -q commit network
|
||||
|
||||
# The pre-procd config-change registration was dropped from here: OMR images
|
||||
# ship no /etc/config/ucitrack, so it failed silently anyway, and the init
|
||||
|
||||
+2
-2
@@ -7,9 +7,9 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=openlist2
|
||||
PKG_VERSION:=4.2.7
|
||||
PKG_VERSION:=4.2.8
|
||||
PKG_WEB_VERSION:=4.2.6
|
||||
PKG_RELEASE:=20
|
||||
PKG_RELEASE:=21
|
||||
|
||||
PKG_SOURCE:=openlist-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/OpenListTeam/OpenList/tar.gz/v$(PKG_VERSION)?
|
||||
|
||||
+2
-2
@@ -5,8 +5,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=sing-box
|
||||
PKG_VERSION:=1.14.3
|
||||
PKG_RELEASE:=36
|
||||
PKG_VERSION:=1.14.4
|
||||
PKG_RELEASE:=37
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/SagerNet/sing-box/tar.gz/v$(PKG_VERSION)?
|
||||
|
||||
+30
-3
@@ -50,12 +50,12 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=wwand
|
||||
PKG_VERSION:=1.8.0_pre6
|
||||
PKG_RELEASE:=81
|
||||
PKG_VERSION:=1.8.0_pre14
|
||||
PKG_RELEASE:=82
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git
|
||||
PKG_SOURCE_VERSION:=7988768d58da5f0f86100b1a2d63db25bdfe87c9
|
||||
PKG_SOURCE_VERSION:=7c6f90d468a76060da225e0a2a60101b74d83bc5
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=GPL-2.0-only
|
||||
@@ -137,6 +137,32 @@ config WWAND_UCODE_SOURCE
|
||||
cmake capability probe falls back to shipping source on its own.
|
||||
endef
|
||||
|
||||
# The ucode this build links and compiles against, as a version floor for the
|
||||
# device. wwand_io.so links libucode.so.<abi> and the tree may be bytecode, so
|
||||
# a wwand built against a newer ucode cannot run on an older one — and with
|
||||
# a plain +ucode apk installed it there anyway: a feed build from after
|
||||
# OpenWrt master moved libucode 20230711 -> 20261009 (2026-10-19) went onto a
|
||||
# snapshot from before it and died on "Error loading shared library
|
||||
# libucode.so.20261009" (ddimension/wwand#67). `ucode (>=<date>)` makes apk
|
||||
# refuse instead. apk sorts the package's own "2026.10.18~d2e8b8d2-r1" above
|
||||
# "2026.10.18" (apk 3.0.5 `version --test`), so the build's own ucode passes.
|
||||
#
|
||||
# Read from ucode's Makefile because no build variable carries another
|
||||
# package's version: package/utils/ucode in a buildroot, the base feed in an
|
||||
# SDK (`feeds update` puts it under feeds/base and links package/feeds/base).
|
||||
# Not found (an unusual tree) means no floor, as before — never a guess.
|
||||
#
|
||||
# Deliberately NOT +libucode by name, though that would record the
|
||||
# ABI-versioned libucode<abi>: on a later ucode upgrade it keeps the old
|
||||
# library installed, and wwand_io.so then loads it into an interpreter running
|
||||
# the new one — two libucode ABIs in one process. Without it the library goes
|
||||
# and files/wwand.init says why the daemon cannot start.
|
||||
UCODE_MAKEFILE:=$(firstword $(wildcard \
|
||||
$(TOPDIR)/package/utils/ucode/Makefile \
|
||||
$(TOPDIR)/package/feeds/base/ucode/Makefile \
|
||||
$(TOPDIR)/feeds/base/package/utils/ucode/Makefile))
|
||||
UCODE_BUILT:=$(if $(UCODE_MAKEFILE),$(subst -,.,$(shell sed -n 's/^PKG_SOURCE_DATE:=//p' '$(UCODE_MAKEFILE)')))
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# base: daemon + framework + codec/client + shared core. No backend on its own;
|
||||
# the daemon loads whichever wwand-qmi/-mbim/-ncm package is present.
|
||||
@@ -152,6 +178,7 @@ define Package/wwand
|
||||
# wwand-private, always version-locked to the ucode side). PROVIDES keeps
|
||||
# configs/upgrades from the era of the separate ucode-mod-wwand-io working.
|
||||
PROVIDES:=ucode-mod-wwand-io
|
||||
EXTRA_DEPENDS:=$(if $(UCODE_BUILT),ucode (>=$(UCODE_BUILT)))
|
||||
endef
|
||||
|
||||
define Package/wwand/description
|
||||
|
||||
Reference in new issue
Block a user