🔥 Sync 2026-10-24 23:19:23

This commit is contained in:
github-actions[bot] committed 2026-10-24 23:19:23 +08:00
1 parent 9886197604
commit 37e1475e87
59 files changed
+5167 -1483

No files matched your search

+4 -4
View File
@@ -1,17 +1,17 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=clashoo PKG_NAME:=clashoo
PKG_SHORT_SHA:=3f27ef3 PKG_SHORT_SHA:=91e85ee
PKG_COMMIT_DATE:=2026.10.21 PKG_COMMIT_DATE:=2026.10.24
PKG_VERSION:=$(PKG_COMMIT_DATE)~$(PKG_SHORT_SHA) 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:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA).tar.gz
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA) PKG_SOURCE_SUBDIR:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
PKG_BUILD_DIR:=$(BUILD_DIR)/$(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_URL:=https://github.com/kenzok8/openwrt-clashoo/releases/download/mihomo-src
PKG_SOURCE_VERSION:=c8979247b7baf46202891be29e174e3f54173cee PKG_SOURCE_VERSION:=413a02e9b55b94dc3892616f43822e191516e493
PKG_HASH:=skip PKG_HASH:=skip
PKG_BUILD_VERSION:=alpha-$(PKG_SHORT_SHA) PKG_BUILD_VERSION:=alpha-$(PKG_SHORT_SHA)
+1 -1
View File
@@ -13,7 +13,7 @@ PKG_SOURCE_URL:=https://github.com/ysurac/glorytun.git
PKG_SOURCE_VERSION:=203c5f2223d7d5146d659b51159b2f02c2480d15 PKG_SOURCE_VERSION:=203c5f2223d7d5146d659b51159b2f02c2480d15
PKG_NAME:=glorytun-udp PKG_NAME:=glorytun-udp
PKG_VERSION:=0.3.4 PKG_VERSION:=0.3.4
PKG_RELEASE:=5 PKG_RELEASE:=6
PKG_LICENSE:=BSD-2-Clause PKG_LICENSE:=BSD-2-Clause
PKG_LICENSE_FILES:=LICENSE PKG_LICENSE_FILES:=LICENSE
+12 -7
View File
@@ -31,12 +31,14 @@ validate_section() {
'kxtimeout:string:7d' \ 'kxtimeout:string:7d' \
'timetolerance:string:10m' \ 'timetolerance:string:10m' \
'keepalive:string:25s' \ 'keepalive:string:25s' \
'localip:string' \
'remoteip:string' \
'dev:string' 'dev:string'
} }
start_instance() { start_instance() {
local enable key host port listener proto mtuauto chacha dev mode 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_dir="/var/run/${PROG_NAME}"
local key_file="${key_dir}/${1}.key" local key_file="${key_dir}/${1}.key"
@@ -106,10 +108,11 @@ start_instance() {
procd_close_instance procd_close_instance
# kxtimeout, timetolerance and keepalive are not bind arguments: only # kxtimeout, timetolerance, keepalive and the tunnel addresses are not
# the ifup hotplug set them, and a restart that keeps the device raises # bind arguments: only the ifup hotplug set them, and a restart that
# no ifup, nor does a change of them alone (procd keeps the instance). # keeps the device raises no ifup, nor does a change of them alone
# Set them once the tunnel answers; the delay leaves procd the time to # (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: # 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 # kept open, it would block every other glorytun-udp init call meanwhile
( (
@@ -120,6 +123,8 @@ start_instance() {
[ "$count" -gt 10 ] && exit 0 [ "$count" -gt 10 ] && exit 0
sleep 1 sleep 1
done done
[ -n "$localip" ] && [ -n "$remoteip" ] &&
ifconfig "$dev" "$localip" pointopoint "$remoteip" up
${PROG} set dev "$dev" kxtimeout "$kxtimeout" timetolerance "$timetolerance" keepalive "$keepalive" ${PROG} set dev "$dev" kxtimeout "$kxtimeout" timetolerance "$timetolerance" keepalive "$keepalive"
) </dev/null >/dev/null 2>&1 1000>&- & ) </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 # No reload_service: stop;start there restarted the tunnel on every network
# config change. The default (start) lets procd restart only an instance whose # config change. The default (start) lets procd restart only an instance whose
# command or key file changed; kxtimeout, timetolerance and keepalive are # command or key file changed; kxtimeout, timetolerance, keepalive and the
# applied by start_instance either way. # tunnel addresses are applied by start_instance either way.
service_triggers() { service_triggers() {
procd_add_reload_trigger glorytun-udp network procd_add_reload_trigger glorytun-udp network
} }
+2 -2
View File
@@ -6,8 +6,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=gowebdav PKG_NAME:=gowebdav
PKG_VERSION:=5.17.1 PKG_VERSION:=5.17.2
PKG_RELEASE:=26 PKG_RELEASE:=27
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/hacdias/webdav/tar.gz/v$(PKG_VERSION)? PKG_SOURCE_URL:=https://codeload.github.com/hacdias/webdav/tar.gz/v$(PKG_VERSION)?
+2 -2
View File
@@ -6,8 +6,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=headscale PKG_NAME:=headscale
PKG_VERSION:=0.29.5 PKG_VERSION:=0.29.6
PKG_RELEASE:=10 PKG_RELEASE:=11
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/juanfont/headscale/tar.gz/v$(PKG_VERSION)? PKG_SOURCE_URL:=https://codeload.github.com/juanfont/headscale/tar.gz/v$(PKG_VERSION)?
@@ -2,6 +2,9 @@
"luci-app-dsvpn": { "luci-app-dsvpn": {
"description": "Grant access to DSVPN", "description": "Grant access to DSVPN",
"read": { "read": {
"ubus": {
"luci-rpc": [ "getHostHints" ]
},
"uci": [ "dsvpn" ] "uci": [ "dsvpn" ]
}, },
"write": { "write": {
@@ -2,6 +2,9 @@
"luci-app-glorytun-tcp": { "luci-app-glorytun-tcp": {
"description": "Grant access to glorytun TCP", "description": "Grant access to glorytun TCP",
"read": { "read": {
"ubus": {
"luci-rpc": [ "getHostHints" ]
},
"uci": [ "glorytun" ] "uci": [ "glorytun" ]
}, },
"write": { "write": {
@@ -8,13 +8,6 @@
var callHostHints; var callHostHints;
var callServiceList = rpc.declare({
object: 'service',
method: 'list',
params: [ 'name' ],
expect: { '': {} }
});
return L.view.extend({ return L.view.extend({
callHostHints: rpc.declare({ callHostHints: rpc.declare({
object: 'luci-rpc', object: 'luci-rpc',
@@ -2,6 +2,9 @@
"luci-app-glorytun-udp": { "luci-app-glorytun-udp": {
"description": "Grant access to glorytun UDP", "description": "Grant access to glorytun UDP",
"read": { "read": {
"ubus": {
"luci-rpc": [ "getHostHints" ]
},
"uci": [ "glorytun-udp" ] "uci": [ "glorytun-udp" ]
}, },
"write": { "write": {
+19 -8
View File
@@ -63,17 +63,28 @@ config_account() {
} }
start_service() { start_service() {
local msmtp_tmp msmtp_written=0 local msmtp_tmp msmtp_new msmtp_written=0
# The file holds the SMTP password: created 0600 next to the target so # The file holds the SMTP password: always created 0600. Built in tmpfs
# the mv is a rename, never readable by others even for a moment # and only written to flash when it changed, every start and reload
msmtp_tmp="$(umask 077; mktemp /etc/msmtprc.XXXXXX)" || return 1 # rewrote /etc/msmtprc
msmtp_tmp="$(umask 077; mktemp /tmp/msmtprc.XXXXXX)" || return 1
config_load mail config_load mail
config_foreach config_account smtp config_foreach config_account smtp
if [ "$msmtp_written" = "1" ]; then if [ "$msmtp_written" = "1" ] && ! cmp -s "$msmtp_tmp" /etc/msmtprc; then
mv "$msmtp_tmp" /etc/msmtprc # next to the target so the mv is a rename, never readable by
else # others even for a moment; keeps the owner and mode of the old
rm -f "$msmtp_tmp" # 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 fi
rm -f "$msmtp_tmp"
} }
service_triggers() { service_triggers() {
@@ -2,6 +2,9 @@
"luci-app-mlvpn": { "luci-app-mlvpn": {
"description": "Grant UCI access for luci-app-mlvpn", "description": "Grant UCI access for luci-app-mlvpn",
"read": { "read": {
"ubus": {
"luci-rpc": [ "getHostHints" ]
},
"uci": [ "mlvpn" ] "uci": [ "mlvpn" ]
}, },
"write": { "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({ return L.view.extend({
load: function() { load: function() {
return Promise.all([ return Promise.all([
@@ -400,6 +411,13 @@ return L.view.extend({
// none. Unset means 100 for every reader (omr-weight-sync, mqvpn). // none. Unset means 100 for every reader (omr-weight-sync, mqvpn).
o.placeholder = "100"; o.placeholder = "100";
o.rmempty = true; 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(); return m.render();
} }
@@ -185,6 +185,19 @@ return L.view.extend({
// multipath_weight='100' on each WAN that had none (unset is 100) // multipath_weight='100' on each WAN that had none (unset is 100)
o.placeholder = '100'; o.placeholder = '100';
o.rmempty = true; 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(); return m.render();
@@ -612,6 +612,52 @@ local function valid_name(v)
return v:match("^[%w_]+$") ~= nil return v:match("^[%w_]+$") ~= nil
end 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 = { local setting_checks = {
mptcp_checksum = function(v) return v == "0" or v == "1" end, mptcp_checksum = function(v) return v == "0" or v == "1" end,
mptcp_debug = 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 end
end end
local changed = false
for _, field in ipairs(string_fields) do for _, field in ipairs(string_fields) do
if args[field] ~= nil and args[field] ~= "" then 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]) uci:set("network", "globals", field, args[field])
end end
end end
for _, field in ipairs(list_fields) do for _, field in ipairs(list_fields) do
if args[field] ~= nil and type(args[field]) == "table" then if args[field] ~= nil and type(args[field]) == "table" then
changed = true
uci:set_list("network", "globals", field, args[field]) uci:set_list("network", "globals", field, args[field])
end end
end end
uci:save("network") uci:save("network")
uci:commit("network") uci:commit("network")
if changed then mptcp_reload() end
return { result = "ok" } return { result = "ok" }
end end
}, },
@@ -822,14 +872,17 @@ local methods = {
not valid_weight(args.multipath_weight) then not valid_weight(args.multipath_weight) then
return { error = "Invalid value for multipath_weight" } return { error = "Invalid value for multipath_weight" }
end end
-- the weight needs no reload: the post-tracking hooks apply it
local reload
if mp ~= "" then if mp ~= "" then
uci:set("network", iface, "multipath", mp) reload = set_multipath(iface, mp)
end end
if args.multipath_weight ~= nil and args.multipath_weight ~= "" then if args.multipath_weight ~= nil and args.multipath_weight ~= "" then
uci:set("network", iface, "multipath_weight", args.multipath_weight) uci:set("network", iface, "multipath_weight", args.multipath_weight)
end end
uci:save("network") uci:save("network")
uci:commit("network") uci:commit("network")
if reload then mptcp_reload(reload) end
return { result = "ok" } return { result = "ok" }
end end
}, },
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-open-box PKG_NAME:=luci-app-open-box
PKG_VERSION:=0.1.356 PKG_VERSION:=0.1.359
PKG_RELEASE:=44 PKG_RELEASE:=45
PKG_MAINTAINER:=liandu2024 PKG_MAINTAINER:=liandu2024
PKG_LICENSE:=GPL-3.0-or-later PKG_LICENSE:=GPL-3.0-or-later
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.10.22 PKG_VERSION:=26.10.22
PKG_RELEASE:=347 PKG_RELEASE:=348
PKG_PO_VERSION:=$(PKG_VERSION) PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \ 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.default = 0
o.rmempty = false 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'>" .. 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 = s:option(Flag, "enable_geoview", translate("Enable Geo Data Parsing"))
o.default = 0 o.default = 0
@@ -1692,8 +1692,21 @@ function gen_config(var)
local bin = api.finded_com("geoview") local bin = api.finded_com("geoview")
if bin then if bin then
local geo_file = (api.uci_get_c("@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/"):match("^(.*)/") .. "/geosite.dat" 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 local gfw_rule
domain_list = (domain_list == "") and "geosite:gfw" or domain_list .. "\ngeosite:gfw" 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
end end
+24 -10
View File
@@ -579,11 +579,15 @@ function gen_config_server(node)
end end
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 if users then
settings = { settings = {
users = users, users = users
decryption = (node.protocol == "vless") and ((node.decryption and node.decryption ~= "") and node.decryption or "none") or nil
} }
end end
elseif node.protocol == "socks" then elseif node.protocol == "socks" then
@@ -1479,8 +1483,21 @@ function gen_config(var)
local bin = api.finded_com("geoview") local bin = api.finded_com("geoview")
if bin then if bin then
local geo_file = (api.uci_get_c("@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/"):match("^(.*)/") .. "/geosite.dat" 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 local gfw_rule
domain_list = (domain_list == "") and "geosite:gfw" or domain_list .. "\ngeosite:gfw" 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
end end
@@ -1539,7 +1556,7 @@ function gen_config(var)
w = api.trim(w) w = api.trim(w)
if w == "" or w:find("#") == 1 then return end 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("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(domains, w)
table.insert(domain_table.domain, w) table.insert(domain_table.domain, w)
end) end)
@@ -2051,10 +2068,7 @@ function gen_config(var)
if inbounds or outbounds then if inbounds or outbounds then
local config = { local config = {
env = (function() env = { XRAY_LOCATION_ASSET = api.CACHE_PATH .. "/geodata" },
local asset_location = api.uci_get_c("@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/"
return { XRAY_LOCATION_ASSET = asset_location }
end)(),
log = { log = {
-- error = string.format("/tmp/etc/passwall/%s.log", node[".name"]), -- error = string.format("/tmp/etc/passwall/%s.log", node[".name"]),
loglevel = get_log_level(loglevel) loglevel = get_log_level(loglevel)
@@ -24,6 +24,7 @@ if node then
end end
-%> -%>
<script src="<%=resource%>/view/<%=appname%>/Sortable.min.js?v=1.15.7"></script> <script src="<%=resource%>/view/<%=appname%>/Sortable.min.js?v=1.15.7"></script>
<script src="<%=resource%>/view/<%=appname%>/node_list_sort.js"></script>
<style> <style>
table th, .table .th { table th, .table .th {
@@ -385,6 +386,89 @@ table td, .table .td {
} }
</style> </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"> <script type="text/javascript">
<% if is_js_luci then -%> <% if is_js_luci then -%>
var cbi_t = []; 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) { 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"]'); var visibleContainer = document.querySelector('#cbi-<%=appname%>-nodes > .cbi-tabcontainer[style*="display:block"], #cbi-<%=appname%>-nodes > .cbi-tabcontainer[style*="display: block"]');
if (!visibleContainer) return; if (!visibleContainer) return;
var nodes = visibleContainer.getElementsByClassName("nodes_select"); var nodes = visibleContainer.getElementsByClassName("nodes_select");
var selectAllChk = visibleContainer.querySelector(".nodes_select_all"); var selectAllChk = visibleContainer.querySelector(".nodes_select_all");
var selectAllBtn = document.getElementById("select_all_btn"); var selectAllBtn = document.getElementById("select_all_btn");
var visibleCount = 0;
for (var i = 0; i < nodes.length; i++) { for (var i = 0; i < nodes.length; i++) {
if (!node_row_visible(nodes[i])) {
nodes[i].checked = false;
continue;
}
nodes[i].checked = sectionChecked; nodes[i].checked = sectionChecked;
visibleCount++;
} }
sectionChecked = sectionChecked && visibleCount > 0;
if (selectAllChk) { if (selectAllChk) {
selectAllChk.checked = sectionChecked; selectAllChk.checked = sectionChecked;
selectAllChk.title = sectionChecked ? "<%:DeSelect all%>" : "<%:Select all%>"; selectAllChk.title = sectionChecked ? "<%:DeSelect all%>" : "<%:Select all%>";
@@ -576,10 +674,13 @@ table td, .table .td {
var selectAllChk = visibleContainer.querySelector(".nodes_select_all"); var selectAllChk = visibleContainer.querySelector(".nodes_select_all");
var selectAllBtn = document.getElementById("select_all_btn"); var selectAllBtn = document.getElementById("select_all_btn");
var checkedCount = 0; var checkedCount = 0;
var visibleCount = 0;
for (var i = 0; i < nodes.length; i++) { for (var i = 0; i < nodes.length; i++) {
if (!node_row_visible(nodes[i])) continue;
visibleCount++;
if (nodes[i].checked) checkedCount++; if (nodes[i].checked) checkedCount++;
} }
var allChecked = checkedCount === nodes.length; var allChecked = visibleCount > 0 && checkedCount === visibleCount;
var title = allChecked ? "<%:DeSelect all%>" : "<%:Select all%>"; var title = allChecked ? "<%:DeSelect all%>" : "<%:Select all%>";
var onclickFunc = allChecked ? "dechecked_all_node(this)" : "checked_all_node(this)"; 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"); var doms = visibleContainer.getElementsByClassName("nodes_select");
if (doms && doms.length > 0) { if (doms && doms.length > 0) {
for (var i = 0 ; i < doms.length; i++) { 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")) ids.push(doms[i].getAttribute("cbid"))
} }
} }
@@ -1163,7 +1264,7 @@ table td, .table .td {
const doms = visibleContainer.getElementsByClassName("nodes_select"); const doms = visibleContainer.getElementsByClassName("nodes_select");
if (doms && doms.length > 0) { if (doms && doms.length > 0) {
for (let i = 0 ; i < doms.length; i++) { 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")) ids.push(doms[i].getAttribute("cbid"))
} }
} }
@@ -1195,7 +1296,7 @@ table td, .table .td {
const doms = visibleContainer.getElementsByClassName("nodes_select"); const doms = visibleContainer.getElementsByClassName("nodes_select");
if (doms && doms.length > 0) { if (doms && doms.length > 0) {
for (let i = 0 ; i < doms.length; i++) { 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")) ids.push(doms[i].getAttribute("cbid"))
} }
} }
@@ -1307,8 +1408,33 @@ table td, .table .td {
<<%=section_tag%> class="cbi-section" id="node_list"> <<%=section_tag%> class="cbi-section" id="node_list">
</<%=section_tag%>> </<%=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"> <script type="text/javascript">
//init logic //init logic
if (window.pwNodeSort)
pwNodeSort.init();
function get_remarks_name(o) { function get_remarks_name(o) {
let str = ""; let str = "";
@@ -1511,6 +1637,8 @@ table td, .table .td {
get_now_use_node(); get_now_use_node();
pingAllNodes(); pingAllNodes();
if (window.pwNodeSort)
pwNodeSort.enhanceAll();
}); });
} }
+56 -2
View File
@@ -974,8 +974,8 @@ msgstr "从 Geo 文件生成规则"
msgid "Generate rule lists such as GFW, China domains, and China IP ranges based on Geo files." msgid "Generate rule lists such as GFW, China domains, and China IP ranges based on Geo files."
msgstr "根据 Geo 文件生成规则列表,包括 GFW、中国域名和中国 IP 段等。" 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." msgid "GeoSite files without GFW rules cannot generate a GFW list (e.g. the original v2fly/Xray files)."
msgstr "启用此选项后手动更新时,即使没有新版本也会从现有 Geo 文件重新生成规则。" msgstr "不含 GFW 规则的 GeoSite 文件无法生成 GFW 列表(如 v2fly/Xray 原版)。"
msgid "Enable Geo Data Parsing" msgid "Enable Geo Data Parsing"
msgstr "开启 Geo 数据解析" msgstr "开启 Geo 数据解析"
@@ -2048,3 +2048,57 @@ msgstr "WARP 设备私钥"
msgid "WARP Endpoint Public Key" msgid "WARP Endpoint Public Key"
msgstr "WARP 服务端公钥" 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" ] && { [ "$protocol" = "_shunt" ] && {
local geoip_path="${V2RAY_LOCATION_ASSET%*/}/geoip.dat" local geoip_path="${GEO_DIR%/}/geoip.dat"
local geosite_path="${V2RAY_LOCATION_ASSET%*/}/geosite.dat" local geosite_path="${GEO_DIR%/}/geosite.dat"
if [ ! -s "$geoip_path" ] || [ ! -s "$geosite_path" ]; then if [ ! -s "$geoip_path" ] || [ ! -s "$geosite_path" ]; then
echolog "* 缺少Geo规则文件,TCP Sing-Box分流节点无法正常使用!" echolog "* 缺少Geo规则文件,TCP Sing-Box分流节点无法正常使用!"
fi fi
@@ -724,8 +724,8 @@ start_global() {
} }
[ "$protocol" = "_shunt" ] && { [ "$protocol" = "_shunt" ] && {
local geoip_path="${V2RAY_LOCATION_ASSET%*/}/geoip.dat" local geoip_path="${GEO_DIR%/}/geoip.dat"
local geosite_path="${V2RAY_LOCATION_ASSET%*/}/geosite.dat" local geosite_path="${GEO_DIR%/}/geosite.dat"
if [ ! -s "$geoip_path" ] || [ ! -s "$geosite_path" ]; then if [ ! -s "$geoip_path" ] || [ ! -s "$geosite_path" ]; then
echolog "* 缺少Geo规则文件,TCP Xray分流节点无法正常使用!" echolog "* 缺少Geo规则文件,TCP Xray分流节点无法正常使用!"
fi 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 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" set_cache_var "LOCAL_DNS" "$LOCAL_DNS"
get_config get_config
export V2RAY_LOCATION_ASSET=$(config_n_get @global_rules[0] v2ray_location_asset "/usr/share/v2ray/") GEO_DIR=$(config_n_get @global_rules[0] v2ray_location_asset "/usr/share/v2ray/")
export XRAY_LOCATION_ASSET=$V2RAY_LOCATION_ASSET 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_GEOSITE=true
export ENABLE_DEPRECATED_GEOIP=true export ENABLE_DEPRECATED_GEOIP=true
export SS_SYSTEM_DNS_RESOLVER_FORCE_BUILTIN=1 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 -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 -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 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 XRAY_LOCATION_ASSET
unset SS_SYSTEM_DNS_RESOLVER_FORCE_BUILTIN unset SS_SYSTEM_DNS_RESOLVER_FORCE_BUILTIN
stop_crontab "$1" stop_crontab "$1"
@@ -1597,6 +1597,7 @@ stop() {
rm -rf $TMP_PATH2/singbox* rm -rf $TMP_PATH2/singbox*
rm -rf $TMP_PATH2/dnsmasq* rm -rf $TMP_PATH2/dnsmasq*
rm -rf $TMP_PATH2/geo_output rm -rf $TMP_PATH2/geo_output
rm -rf $TMP_PATH2/geodata
rm -rf $TMP_PATH2/user_rules rm -rf $TMP_PATH2/user_rules
} }
flush_include flush_include
@@ -1610,6 +1610,7 @@ stop() {
rm -rf $TMP_PATH2/singbox* rm -rf $TMP_PATH2/singbox*
rm -rf $TMP_PATH2/dnsmasq* rm -rf $TMP_PATH2/dnsmasq*
rm -rf $TMP_PATH2/geo_output rm -rf $TMP_PATH2/geo_output
rm -rf $TMP_PATH2/geodata
rm -rf $TMP_PATH2/user_rules rm -rf $TMP_PATH2/user_rules
} }
flush_include flush_include
+1 -1
View File
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2 PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.10.20 PKG_VERSION:=26.10.20
PKG_RELEASE:=149 PKG_RELEASE:=150
PKG_PO_VERSION:=$(PKG_VERSION) PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \ 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
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 if users then
settings = { settings = {
users = users, users = users
decryption = (node.protocol == "vless") and ((node.decryption and node.decryption ~= "") and node.decryption or "none") or nil
} }
end end
elseif node.protocol == "socks" then elseif node.protocol == "socks" then
@@ -22,6 +22,7 @@ if node then
end end
-%> -%>
<script src="<%=resource%>/view/<%=appname%>/Sortable.min.js"></script> <script src="<%=resource%>/view/<%=appname%>/Sortable.min.js"></script>
<script src="<%=resource%>/view/<%=appname%>/node_list_sort.js"></script>
<style> <style>
table th, .table .th { table th, .table .th {
@@ -383,6 +384,89 @@ table td, .table .td {
} }
</style> </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"> <script type="text/javascript">
<% if api.is_js_luci() then -%> <% if api.is_js_luci() then -%>
var cbi_t = []; 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) { 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"]'); var visibleContainer = document.querySelector('#cbi-<%=appname%>-nodes > .cbi-tabcontainer[style*="display:block"], #cbi-<%=appname%>-nodes > .cbi-tabcontainer[style*="display: block"]');
if (!visibleContainer) return; if (!visibleContainer) return;
var nodes = visibleContainer.getElementsByClassName("nodes_select"); var nodes = visibleContainer.getElementsByClassName("nodes_select");
var selectAllChk = visibleContainer.querySelector(".nodes_select_all"); var selectAllChk = visibleContainer.querySelector(".nodes_select_all");
var selectAllBtn = document.getElementById("select_all_btn"); var selectAllBtn = document.getElementById("select_all_btn");
var visibleCount = 0;
for (var i = 0; i < nodes.length; i++) { for (var i = 0; i < nodes.length; i++) {
if (!node_row_visible(nodes[i])) {
nodes[i].checked = false;
continue;
}
nodes[i].checked = sectionChecked; nodes[i].checked = sectionChecked;
visibleCount++;
} }
sectionChecked = sectionChecked && visibleCount > 0;
if (selectAllChk) { if (selectAllChk) {
selectAllChk.checked = sectionChecked; selectAllChk.checked = sectionChecked;
selectAllChk.title = sectionChecked ? "<%:DeSelect all%>" : "<%:Select all%>"; selectAllChk.title = sectionChecked ? "<%:DeSelect all%>" : "<%:Select all%>";
@@ -574,10 +672,13 @@ table td, .table .td {
var selectAllChk = visibleContainer.querySelector(".nodes_select_all"); var selectAllChk = visibleContainer.querySelector(".nodes_select_all");
var selectAllBtn = document.getElementById("select_all_btn"); var selectAllBtn = document.getElementById("select_all_btn");
var checkedCount = 0; var checkedCount = 0;
var visibleCount = 0;
for (var i = 0; i < nodes.length; i++) { for (var i = 0; i < nodes.length; i++) {
if (!node_row_visible(nodes[i])) continue;
visibleCount++;
if (nodes[i].checked) checkedCount++; if (nodes[i].checked) checkedCount++;
} }
var allChecked = checkedCount === nodes.length; var allChecked = visibleCount > 0 && checkedCount === visibleCount;
var title = allChecked ? "<%:DeSelect all%>" : "<%:Select all%>"; var title = allChecked ? "<%:DeSelect all%>" : "<%:Select all%>";
var onclickFunc = allChecked ? "dechecked_all_node(this)" : "checked_all_node(this)"; 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"); var doms = visibleContainer.getElementsByClassName("nodes_select");
if (doms && doms.length > 0) { if (doms && doms.length > 0) {
for (var i = 0 ; i < doms.length; i++) { 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")) ids.push(doms[i].getAttribute("cbid"))
} }
} }
@@ -1158,7 +1259,7 @@ table td, .table .td {
const doms = visibleContainer.getElementsByClassName("nodes_select"); const doms = visibleContainer.getElementsByClassName("nodes_select");
if (doms && doms.length > 0) { if (doms && doms.length > 0) {
for (let i = 0 ; i < doms.length; i++) { 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")) ids.push(doms[i].getAttribute("cbid"))
} }
} }
@@ -1190,7 +1291,7 @@ table td, .table .td {
const doms = visibleContainer.getElementsByClassName("nodes_select"); const doms = visibleContainer.getElementsByClassName("nodes_select");
if (doms && doms.length > 0) { if (doms && doms.length > 0) {
for (let i = 0 ; i < doms.length; i++) { 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")) ids.push(doms[i].getAttribute("cbid"))
} }
} }
@@ -1298,8 +1399,33 @@ table td, .table .td {
<fieldset class="cbi-section" id="node_list"> <fieldset class="cbi-section" id="node_list">
</fieldset> </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"> <script type="text/javascript">
//init logic //init logic
if (window.pw2NodeSort)
pw2NodeSort.init();
function get_remarks_name(o) { function get_remarks_name(o) {
let str = ""; let str = "";
@@ -1499,8 +1625,10 @@ table td, .table .td {
} }
get_now_use_node(); get_now_use_node();
pingAllNodes(); pingAllNodes();
if (window.pw2NodeSort)
pw2NodeSort.enhanceAll();
}); });
} }
+51
View File
@@ -2530,3 +2530,54 @@ msgstr "به‌روزرسانی ناموفق بود"
msgid "Unknown error" msgid "Unknown error"
msgstr "خطای ناشناخته" 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 "فیلتر فقط ردیف‌ها را در این صفحه پنهان می‌کند و چیزی را روی روتر تغییر نمی‌دهد. انتخاب همه، ردیف‌های پنهان را رد می‌کند."
+51
View File
@@ -2531,3 +2531,54 @@ msgstr "Ошибка обновления"
msgid "Unknown error" msgid "Unknown error"
msgstr "Неизвестная ошибка" 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 "Фильтр только скрывает строки на этой странице и ничего не меняет на роутере. «Выделить все» пропускает скрытые строки."
+50
View File
@@ -2517,3 +2517,53 @@ msgstr "更新失败"
msgid "Unknown error" msgid "Unknown error"
msgstr "未知错误" 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 "筛选只在本页隐藏行,不会改动路由器上的设置。全选会跳过隐藏行。"
+51
View File
@@ -2522,3 +2522,54 @@ msgstr "更新失敗"
msgid "Unknown error" msgid "Unknown error"
msgstr "未知錯誤" 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 "篩選只在本頁隱藏列,不會改動路由器上的設定。全選會跳過隱藏列。"
+1 -1
View File
@@ -4,7 +4,7 @@ LUCI_TITLE:=luci-app-ssr-plus
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
PKG_NAME:=luci-app-ssr-plus PKG_NAME:=luci-app-ssr-plus
PKG_VERSION:=198 PKG_VERSION:=198
PKG_RELEASE:=96 PKG_RELEASE:=97
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \ 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.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" }
o:depends("hysteria2_realms", true) 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 = s:option(Value, "hy2_auth", translate("Users Authentication"))
o:depends("type", "hysteria2") o:depends("type", "hysteria2")
o:depends({type = "v2ray", v2ray_protocol = "hysteria2"}) o:depends({type = "v2ray", v2ray_protocol = "hysteria2"})
+6
View File
@@ -3900,3 +3900,9 @@ msgstr "Naive ECH 配置列表(Base64)"
msgid "Naive transport" msgid "Naive transport"
msgstr "Naive 传输协议" 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() { is_builtin_dns_active() {
local global_server_type local global_server_type
local enable_mihomo
global_server_type="$(uci_get_by_name "$GLOBAL_SERVER" type)" global_server_type="$(uci_get_by_name "$GLOBAL_SERVER" type)"
case "$global_server_type" in case "$global_server_type" in
clash|tuic|ss) clash|tuic|ss)
ps_list | grep -v "grep" | grep -q "ssr-retcp" ps_list | grep -v "grep" | grep -q "ssr-retcp"
;; ;;
v2ray) 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 return 1
@@ -722,21 +722,39 @@ Xray.outbounds = {
local PT = server.v2ray_protocol local PT = server.v2ray_protocol
local TP = server.transport local TP = server.transport
if TP == "kcp" then 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 = {} local udp = {}
if server.kcp_guise and server.kcp_guise ~= "none" then if xray_version_val < 260601 then
local g = { type = map[server.kcp_guise] } local map = {none = "none", srtp = "header-srtp", utp = "header-utp", ["wechat-video"] = "header-wechat",
if server.kcp_guise == "dns" and server.kcp_domain and server.kcp_domain ~= "" then dtls = "header-dtls", wireguard = "header-wireguard", dns = "header-dns"}
g.settings = { domain = server.kcp_domain } 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 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 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 finalmask.udp = udp
elseif PT == "hysteria2" then elseif PT == "hysteria2" then
local udp = {} local udp = {}
@@ -769,7 +787,8 @@ Xray.outbounds = {
type = "realm", type = "realm",
settings = { settings = {
url = url, url = url,
stunServers = stun stunServers = stun,
portMapping = (node.hysteria2_realm_upnp == "1") and { enabled = true } or nil
} }
} }
udp[#udp+1] = r udp[#udp+1] = r
@@ -782,7 +801,7 @@ Xray.outbounds = {
brutalUp = up > 0 and (up .. "mbps") or nil, brutalUp = up > 0 and (up .. "mbps") or nil,
brutalDown = down > 0 and (down .. "mbps") or nil, brutalDown = down > 0 and (down .. "mbps") or nil,
udpHop = (server.flag_port_hopping == "1") and { 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) interval = (function(v)
if not v then return 30 end if not v then return 30 end
if v:find("-", 1, true) then if v:find("-", 1, true) then
+38
View File
@@ -2,6 +2,44 @@
All notable changes to this project will be documented in this file. 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 修复范围」:改写为历史范围定义(仅文档) ## [2.7.3-r1] - 结清「附:P2 修复范围」:改写为历史范围定义(仅文档)
`docs/LEGACY_ISSUES.md` 末尾的「附:P2 修复范围」一节,是 P2 **开工时**写下的范围 `docs/LEGACY_ISSUES.md` 末尾的「附:P2 修复范围」一节,是 P2 **开工时**写下的范围
+1 -1
View File
@@ -10,7 +10,7 @@ PKG_NAME:=luci-app-substore
# 版本约定:每次提交 PKG_RELEASE +1(2.1.2-r1 ~ r9); # 版本约定:每次提交 PKG_RELEASE +1(2.1.2-r1 ~ r9);
# 达到 r10 时 PKG_VERSION 末位 +1(2.1.2 -> 2.1.3),PKG_RELEASE 重置为 1。 # 达到 r10 时 PKG_VERSION 末位 +1(2.1.2 -> 2.1.3),PKG_RELEASE 重置为 1。
PKG_VERSION:=2.7.3 PKG_VERSION:=2.7.3
PKG_RELEASE:=24 PKG_RELEASE:=25
LUCI_DEPENDS:=+luci-lua-runtime +luci-compat LUCI_DEPENDS:=+luci-lua-runtime +luci-compat
+61 -49
View File
@@ -8,6 +8,67 @@ group them, then re-emit them in a format your client can consume.
![Screenshot](screenshot.png) ![Screenshot](screenshot.png)
## 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 ## Features
**Subscription management** **Subscription management**
@@ -85,40 +146,6 @@ group them, then re-emit them in a format your client can consume.
**LuCI interface & i18n** **LuCI interface & i18n**
- English by default, Simplified Chinese when the runtime language is `zh-cn` - 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 ## Usage
1. **Add a subscription** — paste the subscription URL; optionally set up a 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 - [CHANGELOG.md](CHANGELOG.md) — changelog
- [docs/LEGACY_ISSUES.md](docs/LEGACY_ISSUES.md) — known unfixed issues (pending decision) - [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 ## Development & testing
The core is plain Lua 5.1 with no `luci.*` dependency, so it is unit-testable The core is plain Lua 5.1 with no `luci.*` dependency, so it is unit-testable
+56 -45
View File
@@ -10,6 +10,62 @@
![Screenshot](screenshot.png) ![Screenshot](screenshot.png)
## 安装
> 包名中的版本号必须与 [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 界面与国际化** **LuCI 界面与国际化**
- 默认英文,运行时语言为 `zh-cn` 时自动显示简体中文 - 默认英文,运行时语言为 `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 定时、规则或下载代理。 1. **添加订阅** —— 粘贴订阅 URL;可选的按订阅 cron 定时、规则或下载代理。
@@ -180,19 +204,6 @@ apk add --allow-untrusted luci-i18n-substore-zh-cn_*.apk # 25.12+
- [CHANGELOG.md](CHANGELOG.md) — 更新日志 - [CHANGELOG.md](CHANGELOG.md) — 更新日志
- [docs/LEGACY_ISSUES.md](docs/LEGACY_ISSUES.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/` 下每个测试文件自包含: 核心逻辑为纯 Lua 5.1,不依赖 `luci.*`,无需设备即可单元测试。`tests/` 下每个测试文件自包含:
+4 -4
View File
@@ -3,7 +3,7 @@
> NOTE: stage-0 skeleton has not yet been installed/verified on a target device. > NOTE: stage-0 skeleton has not yet been installed/verified on a target device.
> This document will be updated once installation is validated. > 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 起 **最低支持 OpenWrt / ImmortalWrt 23.05**(更早的版本不在支持范围内 —— 23.05 起
LuCI 使用 ucode dispatcher,ACL 的入口级校验依赖它,见 LuCI 使用 ucode dispatcher,ACL 的入口级校验依赖它,见
@@ -12,13 +12,13 @@ LuCI 使用 ucode dispatcher,ACL 的入口级校验依赖它,见
## Install (opkg — OpenWrt / ImmortalWrt 24.10 及更早) ## Install (opkg — OpenWrt / ImmortalWrt 24.10 及更早)
```bash ```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+) ## Install (apk — OpenWrt / ImmortalWrt 25.12+)
```bash ```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) ## 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`), 该包的版本号由 `po/` 目录的提交时间推导(LuCI 的 `PKG_PO_VERSION`),
与主包的 `2.7.2-r1` 不同名,按文件名通配安装即可。它会安装 与主包的 `2.7.3-r3` 不同名,按文件名通配安装即可。它会安装
`/usr/lib/lua/luci/i18n/substore.zh-cn.lmo`,并通过 uci-defaults 把 `/usr/lib/lua/luci/i18n/substore.zh-cn.lmo`,并通过 uci-defaults 把
`zh_cn` 加进 `luci.languages`;把 LuCI 语言切到「简体中文」即生效。 `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 callGetSubroutes = rpc.declare({ object: 'tailscale', method: 'get_subroutes' });
const callSetupFirewall = rpc.declare({ object: 'tailscale', method: 'setup_firewall' }); const callSetupFirewall = rpc.declare({ object: 'tailscale', method: 'setup_firewall' });
const callGetLogs = rpc.declare({ object: 'tailscale', method: 'get_logs' }); 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 map;
let lastDevicesStatus; 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 = [ 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.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}], [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) { function formatBytes(bytes) {
const bytes_num = parseInt(bytes, 10); const bytes_num = parseInt(bytes, 10);
if (isNaN(bytes_num) || bytes_num === 0) return '-'; 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')); 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.') 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>'+_('It creates the tailscale interface, adds it to a firewall zone, and sets up forwarding between LAN and Tailscale, plus Tailscale to WAN.')
+'<br>'+_('and enables Masquerading and MSS Clamping (MTU fix) to ensure stable connections.'); +'<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.inputstyle = 'action';
fwBtn.onclick = function() { fwBtn.onclick = function() {
return callSetupFirewall().then(function(res) { // The button writes network/firewall UCI directly, so staged changes
const msg = res?.message || _('Firewall configuration applied.'); // in the form above would be left unapplied and the user would be
ui.addNotification(null, E('p', {}, msg), 'info'); // comparing the firewall against an old config. Ask at click time
}).catch(function(err) { // rather than trusting a snapshot: uci.changes() is an RPC and the
ui.addNotification(null, E('p', {}, _('Failed to configure firewall: %s').format(err?.message || err || 'Unknown error')), 'error'); // answer goes stale as soon as the page is saved once.
}).then(function() { // This only warns; blocking would misfire whenever the answer is
return new Promise(function(resolve) { // stale, and the apply below is harmless either way.
window.setTimeout(resolve, 3000); 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'); const helpTitle = s.taboption('general', form.DummyValue, '_help_title');
helpTitle.title = _('How to enable Site-to-Site?'); 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 // Create the account settings
s.tab('account', _('Account Settings')); s.tab('account', _('Account Settings'));
defTabOpts(s, 'account', accountConf, { optional: false }); defTabOpts(s, 'account', accountConf, { optional: false });
@@ -617,7 +786,7 @@ return view.extend({
doc.body.innerHTML = doc.body.innerHTML =
'<h2>' + _('Error') + '</h2>' + '<h2>' + _('Error') + '</h2>' +
'<p>' + _('Failed to get login URL. You may close this tab.') + '</p>'; '<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) { }).catch(function(err) {
ui.hideModal(); ui.hideModal();
@@ -643,6 +812,7 @@ return view.extend({
ui.showModal(_('Logging out...'), E('em', {}, _('Please wait.'))); ui.showModal(_('Logging out...'), E('em', {}, _('Please wait.')));
return callDoLogout().then(function(res) { return callDoLogout().then(function(res) {
unwrapRpc(res, _('Logout'));
ui.hideModal(); ui.hideModal();
ui.addTimeLimitedNotification(null, [ E('p', _('Successfully logged out.')) ], 5000, 'info'); ui.addTimeLimitedNotification(null, [ E('p', _('Successfully logged out.')) ], 5000, 'info');
}).catch(function(err) { }).catch(function(err) {
@@ -680,6 +850,7 @@ return view.extend({
display.replaceChildren(E('em', {}, _('Collecting data ...'))); display.replaceChildren(E('em', {}, _('Collecting data ...')));
} }
return callGetLogs().then(function(res) { return callGetLogs().then(function(res) {
unwrapRpc(res, _('Loading logs'));
if (display) { if (display) {
display.replaceChildren(renderLogs(res)); display.replaceChildren(renderLogs(res));
} }
@@ -701,7 +872,26 @@ return view.extend({
// standard OpenWrt apply mechanism, which triggers /etc/init.d/tailscale-settings // standard OpenWrt apply mechanism, which triggers /etc/init.d/tailscale-settings
// and provides automatic rollback protection if the device becomes unreachable. // and provides automatic rollback protection if the device becomes unreachable.
handleSaveApply(ev, mode) { 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 () { 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'); 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 "" msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8" 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" msgid "(Experimental) Reduce Memory Usage"
msgstr "" 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)." msgid "1. Select \"Accept Routes\" (to access remote devices)."
msgstr "" 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 "" msgid ""
"2. In \"Advertise Routes\", select your local subnet (to allow remote " "2. In \"Advertise Routes\", select your local subnet (to allow remote "
"devices to access this LAN)." "devices to access this LAN)."
msgstr "" 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)." msgid "3. Click \"Auto Configure Firewall\" (to allow traffic forwarding)."
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: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" msgid "Accept Routes"
msgstr "" 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" msgid "Account Settings"
msgstr "" 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" msgid "Advertise Exit Node"
msgstr "" 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" msgid "Advertise Routes"
msgstr "" 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 "" msgid ""
"Advertise subnet routes behind this device. Select from the detected subnets " "Advertise subnet routes behind this device. Select from the detected "
"below or enter custom routes (comma-separated)." "subnets below or enter custom routes (comma-separated)."
msgstr "" 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" 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." msgid "Allow accepting routes announced by other nodes."
msgstr "" 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." msgid "Allow connecting to this device through the SSH function of Tailscale."
msgstr "" 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" msgid "Allow user access to tailscale"
msgstr "" 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?" msgid "Are you sure you want to log out?"
msgstr "" 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" msgid "Auto Configure Firewall"
msgstr "" 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" msgid "Cancel"
msgstr "" 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." msgid "Click to Log out account on this device."
msgstr "" 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." msgid "Click to get a login URL for this device."
msgstr "" msgstr ""
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:169 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:291
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:260 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:383
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:392 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:591
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:601 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:850
msgid "Collecting data ..." msgid "Collecting data ..."
msgstr "" 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" msgid "Confirm Logout"
msgstr "" 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" msgid "Connection Info"
msgstr "" 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." msgid "Controls how Tailscale DNS is handled."
msgstr "" 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 "" msgid ""
"Could not open a new tab. Please check if your browser or an extension " "Could not open a new tab. Please check if your browser or an "
"blocked the pop-up." "extension blocked the pop-up."
msgstr "" 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" msgid "Custom Login Server"
msgstr "" 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" msgid "Custom Login Server Auth Key"
msgstr "" 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" msgid "DNS Mode"
msgstr "" 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." msgid "Declare this device as an Exit Node."
msgstr "" 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" msgid "Devices List"
msgstr "" 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" msgid "Disable Firewall Configuration"
msgstr "" 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" msgid "Disable SNAT"
msgstr "" 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 "" msgid ""
"Disable Source NAT (SNAT) for traffic to advertised routes. Most users " "Disable Source NAT (SNAT) for traffic to advertised routes. Most "
"should leave this unchecked." "users should leave this unchecked."
msgstr "" 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)." msgid "Disable Tailscale netfilter auto-configuration (--netfilter-mode=off)."
msgstr "" 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
#: 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 "Disabled" msgid "Disabled"
msgstr "" 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." msgid "Disabled: system DNS only."
msgstr "" 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." msgid "Disconnect from Tailscale and expire current node key."
msgstr "" 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" msgid "Enable Peer Relay"
msgstr "" 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" msgid "Enable Tailscale SSH"
msgstr "" 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" msgid "Enable Tailscale Service"
msgstr "" 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" msgid "Enable Web Interface"
msgstr "" 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 "" msgid ""
"Enable or disable the Tailscale service. When disabled, the service will be " "Enable or disable the Tailscale service. When disabled, the service "
"stopped and the process will be killed." "will be stopped and the process will be killed."
msgstr "" 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 "" msgid ""
"Enable this device as a Peer Relay server. Requires a public IP and an UDP " "Enable this device as a Peer Relay server. Requires a public IP and "
"port open on the router." "an UDP port open on the router."
msgstr "" 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" msgid "Enabled"
msgstr "" 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 "" msgid ""
"Enabling this option can reduce memory usage, but it may sacrifice some " "Enabling this option can reduce memory usage, but it may sacrifice "
"performance (set GOGC=10)." "some performance (set GOGC=10)."
msgstr "" 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" msgid "Error"
msgstr "" 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" msgid "Error caching DERP region map: %s"
msgstr "" 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" msgid "Error fetching DERP region map: %s"
msgstr "" 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" msgid "Error reading cached DERP region map: %s"
msgstr "" 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 "" msgid ""
"Essential configuration for Subnet Routing (Site-to-Site) and Exit Node " "Essential configuration for Subnet Routing (Site-to-Site) and Exit "
"features." "Node features."
msgstr "" 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
#: 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 "Exit Node" msgid "Exit Node"
msgstr "" 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 "" 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 "" 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" msgid "Failed to configure firewall: %s"
msgstr "" 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." msgid "Failed to get login URL. You may close this tab."
msgstr "" 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" msgid "Failed to get login URL: %s"
msgstr "" msgstr ""
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:542 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:859
msgid "Failed to get login URL: Invalid response from server."
msgstr ""
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:609
msgid "Failed to load logs: %s" msgid "Failed to load logs: %s"
msgstr "" 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..." msgid "Filter peers by name, IP, or OS..."
msgstr "" 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:" msgid "Filter:"
msgstr "" 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" msgid "Firewall Mode"
msgstr "" 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." msgid "Firewall configuration applied."
msgstr "" 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" msgid "General Settings"
msgstr "" 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" msgid "Hostname"
msgstr "" 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?" msgid "How to enable Site-to-Site?"
msgstr "" 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 "" 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 "" 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 "" msgid ""
"If you are using custom login server but not providing an Auth Key, will " "If you are using custom login server but not providing an Auth Key, "
"redirect to the login page without pre-filling the key." "will redirect to the login page without pre-filling the key."
msgstr "" 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" msgid "Invalid Date"
msgstr "" msgstr ""
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:447 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:789
msgid "" msgid "Invalid response from server."
"It automatically creates the tailscale interface, sets up firewall zones for "
"LAN <-> Tailscale forwarding,"
msgstr "" 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" msgid "Just now"
msgstr "" 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" msgid "LOGGED OUT"
msgstr "" 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" msgid "Last Seen"
msgstr "" 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." msgid "Leave blank for default Tailscale control plane."
msgstr "" 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..." msgid "Logging out..."
msgstr "" 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" msgid "Login"
msgstr "" msgstr ""
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:502 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:749
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:575 #: 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" msgid "Logout"
msgstr "" 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" msgid "Logout failed: %s"
msgstr "" 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" msgid "Logs"
msgstr "" 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." msgid "MagicDNS: Tailscale overrides resolv.conf."
msgstr "" 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" msgid "N/A"
msgstr "" 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" msgid "NOT RUNNING"
msgstr "" 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." msgid "No peer devices found."
msgstr "" msgstr ""
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:246 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:368
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:591 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:840
msgid "No tailscale-related logs found." msgid "No tailscale-related logs found."
msgstr "" 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" msgid "None"
msgstr "" 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" msgid "Now"
msgstr "" 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" msgid "OS"
msgstr "" 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" msgid "Offline"
msgstr "" 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" msgid "Online"
msgstr "" 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" msgid "OpenWrt Forward"
msgstr "" 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)" msgid "OpenWrt Forward: MagicDNS via dnsmasq forwarding.(Only support ts.net)"
msgstr "" msgstr ""
#: 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 "" msgid "Operation"
"Optional: Specify a custom control server URL (e.g., a Headscale instance, "
"%s)."
msgstr "" 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 "" msgid ""
"Optional: Specify an authentication key for the custom control server. Leave " "Optional: Specify a custom control server URL (e.g., a Headscale "
"blank if not required." "instance, %s)."
msgstr "" 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" msgid "Peer Relay Port"
msgstr "" 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." msgid "Please use the login button in the settings below to authenticate."
msgstr "" msgstr ""
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:525 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:772
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:531 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:778
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:565 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:812
msgid "Please wait." msgid "Please wait."
msgstr "" 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" msgid "RUNNING"
msgstr "" 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" msgid "RX"
msgstr "" 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" msgid "Refresh"
msgstr "" msgstr ""
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:525 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:177
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:531 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..." msgid "Requesting Login URL..."
msgstr "" 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." msgid "Requesting Tailscale login URL... Please wait."
msgstr "" 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 "" msgid ""
"Select an exit node from the list. If enabled, Allow LAN Access is enabled " "Select an exit node from the list. If enabled, Allow LAN Access is "
"implicitly." "enabled implicitly."
msgstr "" 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 "" msgid ""
"Select the firewall backend for Tailscale to use. Requires service restart " "Select the firewall backend for Tailscale to use. Requires service "
"to take effect." "restart to take effect."
msgstr "" msgstr ""
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:198 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:320
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:206 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:328
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:218 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:340
#: 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 "Service Status" msgid "Service Status"
msgstr "" 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" msgid "Shields Up"
msgstr "" 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" msgid "Status"
msgstr "" 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." msgid "Successfully logged out."
msgstr "" 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" msgid "TAILSCALE NOT FOUND"
msgstr "" 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" msgid "TUN Mode"
msgstr "" 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" msgid "TX"
msgstr "" 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" msgid "Tailnet Name"
msgstr "" 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
#: applications/luci-app-tailscale-community/root/usr/share/luci/menu.d/luci-app-tailscale-community.json:3 #: luci-app-tailscale-community/root/usr/share/luci/menu.d/luci-app-tailscale-community.json:1
msgid "Tailscale" msgid "Tailscale"
msgstr "" 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" msgid "Tailscale Health Check: %s"
msgstr "" 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" msgid "Tailscale IP"
msgstr "" 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" msgid "Tailscale IPv4"
msgstr "" 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" msgid "Tailscale IPv6"
msgstr "" 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" msgid "Tailscale Login"
msgstr "" 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 "" msgid ""
"Tailscale is a mesh VPN solution that makes it easy to connect your devices " "Tailscale is a mesh VPN solution that makes it easy to connect your "
"securely. This configuration page allows you to manage Tailscale settings on " "devices securely. This configuration page allows you to manage "
"your OpenWrt device." "Tailscale settings on your OpenWrt device."
msgstr "" 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" msgid "Tailscale status error"
msgstr "" 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." msgid "This can take up to 30 seconds."
msgstr "" 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 "" msgid ""
"This will disconnect this device from your Tailnet and require you to re-" "This will disconnect this device from your Tailnet and require you to "
"authenticate." "re-authenticate."
msgstr "" msgstr ""
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:406 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:143
msgid "" #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:194
"UDP port for the Peer Relay service. Open this port on your router firewall/" msgid "Timed out waiting for the firewall reload"
"NAT."
msgstr "" 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:605
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:150 msgid ""
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:153 "UDP port for the Peer Relay service. Open this port on your router "
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:546 "firewall/NAT."
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:572 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: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" msgid "Unknown error"
msgstr "" 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" msgid "Version"
msgstr "" 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 "" msgid ""
"When enabled, blocks all inbound connections from the Tailscale network." "When enabled, blocks all inbound connections from the Tailscale "
"network."
msgstr "" 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." msgid "When using the exit node, access to the local LAN is allowed."
msgstr "" 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 "" msgid ""
"[Important] Log in to the Tailscale admin console and manually enable " "[Important] Log in to the Tailscale admin console and manually enable "
"\"Subnet Routes\" for this device." "\"Subnet Routes\" for this device."
msgstr "" msgstr ""
#: 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 "ago" msgid ""
"[Important] Step 3 cannot be done here: it needs your Tailscale "
"account."
msgstr "" msgstr ""
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:448 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:221
msgid "" msgid "ago"
"and enables Masquerading and MSS Clamping (MTU fix) to ensure stable "
"connections."
msgstr "" msgstr ""
@@ -1,585 +1,693 @@
msgid "" msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8\n" 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" msgid "(Experimental) Reduce Memory Usage"
msgstr "(实验性) 减少内存使用" 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. Select \"Accept Routes\" (to access remote devices)." msgid "1. Enable \"Advertise Exit Node\"."
msgstr "1. 勾选 接受路由 (接受远程设备访问)。" 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 "" msgid ""
"2. In \"Advertise Routes\", select your local subnet (to allow remote " "2. In \"Advertise Routes\", select your local subnet (to allow remote "
"devices to access this LAN)." "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)." 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" msgid "Accept Routes"
msgstr "接受路由" 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" msgid "Account Settings"
msgstr "账户设置" 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" msgid "Advertise Exit Node"
msgstr "通告出口节点" 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" msgid "Advertise Routes"
msgstr "通告路由" 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 "" msgid ""
"Advertise subnet routes behind this device. Select from the detected subnets " "Advertise subnet routes behind this device. Select from the detected "
"below or enter custom routes (comma-separated)." "subnets below or enter custom routes (comma-separated)."
msgstr "" 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" 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." msgid "Allow accepting routes announced by other nodes."
msgstr "允许接受由其他节点通告的路由。" 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." msgid "Allow connecting to this device through the SSH function of Tailscale."
msgstr "允许通过 Tailscale 的 SSH 功能连接到此设备。" 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" msgid "Allow user access to tailscale"
msgstr "允许用户访问 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?" msgid "Are you sure you want to log out?"
msgstr "您确定要登出吗?" 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" msgid "Auto Configure Firewall"
msgstr "自动配置防火墙" 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" msgid "Cancel"
msgstr "取消" 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." msgid "Click to Log out account on this device."
msgstr "点击以登出此设备上的账户。" 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." msgid "Click to get a login URL for this device."
msgstr "点击获取此设备的登录 URL。" msgstr "点击获取此设备的登录 URL。"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:169 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:291
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:260 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:383
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:392 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:591
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:601 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:850
msgid "Collecting data ..." msgid "Collecting data ..."
msgstr "正在收集数据..." 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" msgid "Confirm Logout"
msgstr "确认登出" 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" msgid "Connection Info"
msgstr "连接信息" 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." msgid "Controls how Tailscale DNS is handled."
msgstr "控制 Tailscale DNS 的处理方式。" 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 "" msgid ""
"Could not open a new tab. Please check if your browser or an extension " "Could not open a new tab. Please check if your browser or an "
"blocked the pop-up." "extension blocked the pop-up."
msgstr "无法打开新标签页。请检查您的浏览器或扩展程序是否阻止了弹出窗口。" 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" msgid "Custom Login Server"
msgstr "自定义登录服务器" 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" msgid "Custom Login Server Auth Key"
msgstr "自定义登录服务器认证密钥" 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" msgid "DNS Mode"
msgstr "DNS 模式" 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." msgid "Declare this device as an Exit Node."
msgstr "将此设备声明为出口节点。" 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" msgid "Devices List"
msgstr "设备列表" 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" msgid "Disable Firewall Configuration"
msgstr "禁用防火墙配置" 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" msgid "Disable SNAT"
msgstr "禁用 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 "" msgid ""
"Disable Source NAT (SNAT) for traffic to advertised routes. Most users " "Disable Source NAT (SNAT) for traffic to advertised routes. Most "
"should leave this unchecked." "users should leave this unchecked."
msgstr "为通告路由的流量禁用源地址转换 (SNAT)。大多数用户应保持此项不勾选。" 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)." 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 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:62
#: 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 "Disabled" msgid "Disabled"
msgstr "已禁用" 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." 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." msgid "Disconnect from Tailscale and expire current node key."
msgstr "从 Tailscale 断开连接并使当前节点密钥过期。" 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" msgid "Enable Peer Relay"
msgstr "启用节点中继" 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" msgid "Enable Tailscale SSH"
msgstr "启用 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" 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" msgid "Enable Web Interface"
msgstr "启用 Web 界面" 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 "" msgid ""
"Enable or disable the Tailscale service. When disabled, the service will be " "Enable or disable the Tailscale service. When disabled, the service "
"stopped and the process will be killed." "will be stopped and the process will be killed."
msgstr "启用或禁用Tailscale服务。禁用后,服务将被停止,当前进程将被终止。" 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 "" msgid ""
"Enable this device as a Peer Relay server. Requires a public IP and an UDP " "Enable this device as a Peer Relay server. Requires a public IP and "
"port open on the router." "an UDP port open on the router."
msgstr "" msgstr "将此设备设为节点中继服务器。需要此路由器具备公网 IP,并在路由器上开放一个 UDP 端口。"
"将此设备设定为节点中继服务器(DERP)。需要此路由器具备公网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" msgid "Enabled"
msgstr "已启用" 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 "" msgid ""
"Enabling this option can reduce memory usage, but it may sacrifice some " "Enabling this option can reduce memory usage, but it may sacrifice "
"performance (set GOGC=10)." "some performance (set GOGC=10)."
msgstr "启用此选项可以减少内存使用,但可能会牺牲一些性能 (设置 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" msgid "Error"
msgstr "错误" 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" msgid "Error caching DERP region map: %s"
msgstr "缓存 DERP 区域地图时出错: %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" msgid "Error fetching DERP region map: %s"
msgstr "获取 DERP 区域地图时出错: %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" msgid "Error reading cached DERP region map: %s"
msgstr "读取缓存的 DERP 区域地图时出错: %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 "" msgid ""
"Essential configuration for Subnet Routing (Site-to-Site) and Exit Node " "Essential configuration for Subnet Routing (Site-to-Site) and Exit "
"features." "Node features."
msgstr "子网路由(局域网间互通)和出口节点的基本配置。" 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
#: 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 "Exit Node" msgid "Exit Node"
msgstr "出口节点" 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 "" 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 管理此节点。" 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" 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." msgid "Failed to get login URL. You may close this tab."
msgstr "获取登录 URL 失败。您可以关闭此标签页。" 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" msgid "Failed to get login URL: %s"
msgstr "获取登录 URL 失败: %s" msgstr "获取登录 URL 失败: %s"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:542 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:859
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
msgid "Failed to load logs: %s" 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..." msgid "Filter peers by name, IP, or OS..."
msgstr "按名称、IP 或操作系统过滤..." msgstr "按名称、IP 或操作系统过滤..."
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:504
msgid "Filter:" msgid "Filter:"
msgstr "过滤:" msgstr "过滤:"
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:54
msgid "Firewall Mode" msgid "Firewall Mode"
msgstr "防火墙模式" 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." msgid "Firewall configuration applied."
msgstr "已应用防火墙配置" 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" msgid "General Settings"
msgstr "常规设置" 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" msgid "Hostname"
msgstr "主机名" 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?" msgid "How to enable Site-to-Site?"
msgstr "如何实现站点到站点(即多局域网互通)?" 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 "" 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 "如果显示超时,您可以刷新页面并再次点击登录。" 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 "" msgid ""
"If you are using custom login server but not providing an Auth Key, will " "If you are using custom login server but not providing an Auth Key, "
"redirect to the login page without pre-filling the key." "will redirect to the login page without pre-filling the key."
msgstr "" 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" msgid "Invalid Date"
msgstr "无效日期" msgstr "无效日期"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:447 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:789
msgid "" msgid "Invalid response from server."
"It automatically creates the tailscale interface, sets up firewall zones for " msgstr "服务器响应无效。"
"LAN <-> Tailscale forwarding,"
msgstr "将自动创建Tailscale接口并设置防火墙区域用于LAN <-> Tailscale转发"
#: 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" msgid "Just now"
msgstr "刚才" 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" msgid "LOGGED OUT"
msgstr "已登出" 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" msgid "Last Seen"
msgstr "上次在线" 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." msgid "Leave blank for default Tailscale control plane."
msgstr "留空以使用默认的 Tailscale 控制平面。" 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..." msgid "Logging out..."
msgstr "正在登出..." 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" msgid "Login"
msgstr "登录" msgstr "登录"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:502 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:749
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:575 #: 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" msgid "Logout"
msgstr "登出" 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" msgid "Logout failed: %s"
msgstr "登出失败: %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" msgid "Logs"
msgstr "日志" 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." 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" msgid "N/A"
msgstr "不可用" 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" msgid "NOT RUNNING"
msgstr "未运行" 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." msgid "No peer devices found."
msgstr "未找到节点设备。" msgstr "未找到节点设备。"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:246 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:368
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:591 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:840
msgid "No tailscale-related logs found." msgid "No tailscale-related logs found."
msgstr "没有找到Tailscale相关日志" 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" msgid "None"
msgstr "无" 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" msgid "Now"
msgstr "现在" 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" msgid "OS"
msgstr "操作系统" 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" msgid "Offline"
msgstr "离线" 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" msgid "Online"
msgstr "在线" 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" msgid "OpenWrt Forward"
msgstr "OpenWrt 转发" 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)" msgid "OpenWrt Forward: MagicDNS via dnsmasq forwarding.(Only support ts.net)"
msgstr "OpenWrt 转发:通过dnsmasq转发MagicDNS(仅支持 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 "" msgid ""
"Optional: Specify a custom control server URL (e.g., a Headscale instance, " "Optional: Specify a custom control server URL (e.g., a Headscale "
"%s)." "instance, %s)."
msgstr "可选:指定一个自定义控制服务器 URL (例如,一个 Headscale 实例, %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 "" msgid ""
"Optional: Specify an authentication key for the custom control server. Leave " "Optional: Specify an authentication key for the custom control "
"blank if not required." "server. Leave blank if not required."
msgstr "可选:为自定义控制服务器指定一个认证密钥。如果不需要,请留空。" 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" 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." msgid "Please use the login button in the settings below to authenticate."
msgstr "请使用下方设置中的登录按钮进行认证。" msgstr "请使用下方设置中的登录按钮进行认证。"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:525 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:772
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:531 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:778
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:565 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:812
msgid "Please wait." msgid "Please wait."
msgstr "请稍候。" 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" msgid "RUNNING"
msgstr "正在运行" 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" msgid "RX"
msgstr "接收" 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" msgid "Refresh"
msgstr "刷新" msgstr "刷新"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:525 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:177
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:531 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..." msgid "Requesting Login URL..."
msgstr "正在请求登录 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." msgid "Requesting Tailscale login URL... Please wait."
msgstr "正在请求 Tailscale 登录 URL... 请稍候。" 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 "" msgid ""
"Select an exit node from the list. If enabled, Allow LAN Access is enabled " "Select an exit node from the list. If enabled, Allow LAN Access is "
"implicitly." "enabled implicitly."
msgstr "列表中选择出口节点。一旦开启,默认局域网之间可以互访" 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 "" msgid ""
"Select the firewall backend for Tailscale to use. Requires service restart " "Select the firewall backend for Tailscale to use. Requires service "
"to take effect." "restart to take effect."
msgstr "选择 Tailscale 使用的防火墙后端。需要重启服务才能生效。" msgstr "选择 Tailscale 使用的防火墙后端。需要重启服务才能生效。"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:198 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:320
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:206 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:328
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:218 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:340
#: 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 "Service Status" msgid "Service Status"
msgstr "服务状态" 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 "Shields Up" msgid ""
msgstr "开启防护 (Shields Up)" "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" msgid "Status"
msgstr "状态" 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." msgid "Successfully logged out."
msgstr "登出成功。" 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" msgid "TAILSCALE NOT FOUND"
msgstr "未找到 TAILSCALE 主程序" 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" msgid "TUN Mode"
msgstr "TUN 模式" 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" msgid "TX"
msgstr "发送" 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" msgid "Tailnet Name"
msgstr "Tailnet 名称" msgstr "Tailnet 名称"
#: 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
#: applications/luci-app-tailscale-community/root/usr/share/luci/menu.d/luci-app-tailscale-community.json:3 #: luci-app-tailscale-community/root/usr/share/luci/menu.d/luci-app-tailscale-community.json:1
msgid "Tailscale" msgid "Tailscale"
msgstr "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" msgid "Tailscale Health Check: %s"
msgstr "Tailscale 健康检查: %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" msgid "Tailscale IP"
msgstr "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" msgid "Tailscale IPv4"
msgstr "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" msgid "Tailscale IPv6"
msgstr "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" msgid "Tailscale Login"
msgstr "Tailscale 登录" 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 "" msgid ""
"Tailscale is a mesh VPN solution that makes it easy to connect your devices " "Tailscale is a mesh VPN solution that makes it easy to connect your "
"securely. This configuration page allows you to manage Tailscale settings on " "devices securely. This configuration page allows you to manage "
"your OpenWrt device." "Tailscale settings on your OpenWrt device."
msgstr "" msgstr ""
"Tailscale 是一个网状 VPN 解决方案,可以轻松地安全连接您的设备。此配置页面允许" "Tailscale 是一个网状 VPN 解决方案,可以轻松地安全连接您的设备。此配置页面允许您在 OpenWrt 设备上管理 "
"您在 OpenWrt 设备上管理 Tailscale 设置。" "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" msgid "Tailscale status error"
msgstr "Tailscale 状态异常" 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." msgid "This can take up to 30 seconds."
msgstr "此过程最多可能需要 30 秒。" 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 "" msgid ""
"This will disconnect this device from your Tailnet and require you to re-" "This will disconnect this device from your Tailnet and require you to "
"authenticate." "re-authenticate."
msgstr "这将使此设备从您的 Tailnet 断开连接,并需要您重新进行身份验证。" msgstr "这将使此设备从您的 Tailnet 断开连接,并需要您重新进行身份验证。"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:406 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:143
msgid "" #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:194
"UDP port for the Peer Relay service. Open this port on your router firewall/" msgid "Timed out waiting for the firewall reload"
"NAT." msgstr "等待防火墙重新加载超时"
msgstr "DERP的UDP端口号。需要在防火墙/NAT规则中开放该端口。"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:123 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:605
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:150 msgid ""
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:153 "UDP port for the Peer Relay service. Open this port on your router "
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:546 "firewall/NAT."
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:572 msgstr "节点中继服务的 UDP 端口。需要在路由器的防火墙/NAT 上开放该端口。"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:609
#: 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" msgid "Unknown error"
msgstr "未知错误" 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" msgid "Version"
msgstr "版本" 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 "" msgid ""
"When enabled, blocks all inbound connections from the Tailscale network." "When enabled, blocks all inbound connections from the Tailscale "
"network."
msgstr "启用后,将阻止来自 Tailscale 网络的所有入站连接。" 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." msgid "When using the exit node, access to the local LAN is allowed."
msgstr "使用出口节点时,允许访问本地局域网。" 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 "" msgid ""
"[Important] Log in to the Tailscale admin console and manually enable " "[Important] Log in to the Tailscale admin console and manually enable "
"\"Subnet Routes\" for this device." "\"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" msgid "ago"
msgstr "前" 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 "" #~ msgid ""
#~ "Completely disable all Tailscale firewall auto-configuration, including " #~ "Completely disable all Tailscale firewall auto-configuration, "
#~ "zone creation, forwarding rules, and netfilter settings." #~ "including zone creation, forwarding rules, and netfilter settings."
#~ msgstr "" #~ msgstr "完全禁用所有 Tailscale 防火墙自动配置,包括区域创建、转发规则和 netfilter 设置。"
#~ "完全禁用所有 Tailscale 防火墙自动配置,包括区域创建、转发规则和 netfilter "
#~ "设置。"
#~ msgid "" #~ msgid ""
#~ "Optional: Specify a custom control server URL (e.g., a Headscale " #~ "Optional: Specify a custom control server URL (e.g., a Headscale "
#~ "instance, https://example.com)." #~ "instance, https://example.com)."
#~ msgstr "" #~ msgstr "可选:指定一个自定义控制服务器 URL (例如,一个 Headscale 实例,https://example.com)。"
#~ "可选:指定一个自定义控制服务器 URL (例如,一个 Headscale 实例,https://"
#~ "example.com)。"
#~ msgid "Applying changes..." #~ msgid "Applying changes..."
#~ msgstr "正在应用更改..." #~ msgstr "正在应用更改..."
@@ -601,3 +709,24 @@ msgstr "并启用伪装和MSS钳制(MTU修复)功能,以确保连接稳定
#~ msgid "Tailscale settings applied successfully." #~ msgid "Tailscale settings applied successfully."
#~ msgstr "Tailscale 设置已成功应用。" #~ 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 "" msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8\n" 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" msgid "(Experimental) Reduce Memory Usage"
msgstr "(實驗性) 減少記憶體使用" 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)." msgid "1. Select \"Accept Routes\" (to access remote devices)."
msgstr "1. 勾選「接受路由」(以存取遠端設備)。" 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 "" msgid ""
"2. In \"Advertise Routes\", select your local subnet (to allow remote " "2. In \"Advertise Routes\", select your local subnet (to allow remote "
"devices to access this LAN)." "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)." 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" msgid "Accept Routes"
msgstr "接受路由" 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" msgid "Account Settings"
msgstr "帳號設定" 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" msgid "Advertise Exit Node"
msgstr "宣告出口節點" 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" msgid "Advertise Routes"
msgstr "宣告路由" 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 "" msgid ""
"Advertise subnet routes behind this device. Select from the detected subnets " "Advertise subnet routes behind this device. Select from the detected "
"below or enter custom routes (comma-separated)." "subnets below or enter custom routes (comma-separated)."
msgstr "" 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" 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." msgid "Allow accepting routes announced by other nodes."
msgstr "允許接受由其他節點宣告的路由。" 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." msgid "Allow connecting to this device through the SSH function of Tailscale."
msgstr "允許透過 Tailscale 的 SSH 功能連線到此裝置。" 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" msgid "Allow user access to tailscale"
msgstr "允許使用者存取 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?" msgid "Are you sure you want to log out?"
msgstr "您確定要登出嗎?" 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" msgid "Auto Configure Firewall"
msgstr "自動設定防火牆" 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" msgid "Cancel"
msgstr "取消" 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." msgid "Click to Log out account on this device."
msgstr "點選以登出此裝置上的帳號。" 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." msgid "Click to get a login URL for this device."
msgstr "點選獲取此裝置的登入 URL。" msgstr "點選獲取此裝置的登入 URL。"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:169 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:291
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:260 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:383
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:392 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:591
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:601 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:850
msgid "Collecting data ..." msgid "Collecting data ..."
msgstr "正在收集資料..." 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" msgid "Confirm Logout"
msgstr "確認登出" 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" msgid "Connection Info"
msgstr "連線資訊" 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." msgid "Controls how Tailscale DNS is handled."
msgstr "控制 Tailscale DNS 的處理方式。" 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 "" msgid ""
"Could not open a new tab. Please check if your browser or an extension " "Could not open a new tab. Please check if your browser or an "
"blocked the pop-up." "extension blocked the pop-up."
msgstr "無法開啟新分頁。請檢查您的瀏覽器或擴充套件程式是否阻止了彈出式視窗。" 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" msgid "Custom Login Server"
msgstr "自訂登入伺服器" 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" msgid "Custom Login Server Auth Key"
msgstr "自訂登入伺服器認證金鑰" 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" msgid "DNS Mode"
msgstr "DNS 模式" 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." msgid "Declare this device as an Exit Node."
msgstr "將此裝置宣告為出口節點。" 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" msgid "Devices List"
msgstr "裝置清單" 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" msgid "Disable Firewall Configuration"
msgstr "停用防火牆設定" 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" msgid "Disable SNAT"
msgstr "停用 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 "" msgid ""
"Disable Source NAT (SNAT) for traffic to advertised routes. Most users " "Disable Source NAT (SNAT) for traffic to advertised routes. Most "
"should leave this unchecked." "users should leave this unchecked."
msgstr "為宣告路由的流量停用來源位址轉換 (SNAT)。大多數使用者不應勾選此項。" 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)." msgid "Disable Tailscale netfilter auto-configuration (--netfilter-mode=off)."
msgstr "停用 Tailscale netfilter 自動設定 (--netfilter-mode=off)。" msgstr "停用 Tailscale netfilter 自動設定 (--netfilter-mode=off)。"
#: 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
#: 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 "Disabled" msgid "Disabled"
msgstr "已停用" 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." 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." msgid "Disconnect from Tailscale and expire current node key."
msgstr "中斷 Tailscale 連線並使目前節點金鑰過期。" 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" msgid "Enable Peer Relay"
msgstr "啟用對等中繼" 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" msgid "Enable Tailscale SSH"
msgstr "啟用 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" 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" msgid "Enable Web Interface"
msgstr "啟用 Web 介面" 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 "" msgid ""
"Enable or disable the Tailscale service. When disabled, the service will be " "Enable or disable the Tailscale service. When disabled, the service "
"stopped and the process will be killed." "will be stopped and the process will be killed."
msgstr "啟用或停用 Tailscale 服務。停用後,該服務將停止,處理程序將被終止。" 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 "" msgid ""
"Enable this device as a Peer Relay server. Requires a public IP and an UDP " "Enable this device as a Peer Relay server. Requires a public IP and "
"port open on the router." "an UDP port open on the router."
msgstr "將此設備做為對等中繼伺服器。需要公開 IP 位址,且路由器上需要開啟一個 UDP 連接埠。" 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" msgid "Enabled"
msgstr "已啟用" 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 "" msgid ""
"Enabling this option can reduce memory usage, but it may sacrifice some " "Enabling this option can reduce memory usage, but it may sacrifice "
"performance (set GOGC=10)." "some performance (set GOGC=10)."
msgstr "啟用此選項可以減少記憶體使用,但可能會犧牲一些效能 (設定 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" msgid "Error"
msgstr "錯誤" 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" msgid "Error caching DERP region map: %s"
msgstr "快取 DERP 區域對照表時出錯: %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" msgid "Error fetching DERP region map: %s"
msgstr "獲取 DERP 區域對照表時出錯: %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" msgid "Error reading cached DERP region map: %s"
msgstr "讀取快取的 DERP 區域對照表時出錯: %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 "" msgid ""
"Essential configuration for Subnet Routing (Site-to-Site) and Exit Node " "Essential configuration for Subnet Routing (Site-to-Site) and Exit "
"features." "Node features."
msgstr "子網路由(站對站)和出口節點功能的基本設定" 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
#: 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 "Exit Node" msgid "Exit Node"
msgstr "出口節點" 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 "" 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 管理此節點。" 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" 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." msgid "Failed to get login URL. You may close this tab."
msgstr "獲取登入 URL 失敗。您可以關閉此分頁。" 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" msgid "Failed to get login URL: %s"
msgstr "獲取登入 URL 失敗: %s" msgstr "獲取登入 URL 失敗: %s"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:542 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:859
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
msgid "Failed to load logs: %s" 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..." msgid "Filter peers by name, IP, or OS..."
msgstr "依名稱、IP 或作業系統篩選..." msgstr "依名稱、IP 或作業系統篩選..."
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:504
msgid "Filter:" msgid "Filter:"
msgstr "篩選:" msgstr "篩選:"
#: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:54
msgid "Firewall Mode" msgid "Firewall Mode"
msgstr "防火牆模式" 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." msgid "Firewall configuration applied."
msgstr "已套用防火牆設定" 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" msgid "General Settings"
msgstr "一般設定" 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" msgid "Hostname"
msgstr "主機名" 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?" msgid "How to enable Site-to-Site?"
msgstr "如何啟用站對站通訊?" 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 "" 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 "如果顯示逾時,您可以重新整理頁面並再次點選登入。" 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 "" msgid ""
"If you are using custom login server but not providing an Auth Key, will " "If you are using custom login server but not providing an Auth Key, "
"redirect to the login page without pre-filling the key." "will redirect to the login page without pre-filling the key."
msgstr "" 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" msgid "Invalid Date"
msgstr "無效日期" msgstr "無效日期"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:447 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:789
msgid "" msgid "Invalid response from server."
"It automatically creates the tailscale interface, sets up firewall zones for " msgstr "伺服器回應無效。"
"LAN <-> Tailscale forwarding,"
msgstr "它會自動建立 Tailscale 介面,並設定允許 LAN <-> Tailscale 防火牆區域轉送。"
#: 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" msgid "Just now"
msgstr "剛才" 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" msgid "LOGGED OUT"
msgstr "已登出" 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" msgid "Last Seen"
msgstr "上次連線" 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." msgid "Leave blank for default Tailscale control plane."
msgstr "留白則使用預設的 Tailscale 控制台。" 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..." msgid "Logging out..."
msgstr "正在登出..." 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" msgid "Login"
msgstr "登入" msgstr "登入"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:502 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:749
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:575 #: 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" msgid "Logout"
msgstr "登出" 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" msgid "Logout failed: %s"
msgstr "登出失敗: %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" msgid "Logs"
msgstr "日誌" 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." 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" msgid "N/A"
msgstr "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" msgid "NOT RUNNING"
msgstr "未執行" 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." msgid "No peer devices found."
msgstr "未找到對等裝置。" msgstr "未找到對等裝置。"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:246 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:368
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:591 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:840
msgid "No tailscale-related logs found." msgid "No tailscale-related logs found."
msgstr "未找到與 tailscale 相關的日誌。" 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" msgid "None"
msgstr "無" 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" msgid "Now"
msgstr "現在" 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" msgid "OS"
msgstr "作業系統" 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" msgid "Offline"
msgstr "離線" 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" msgid "Online"
msgstr "上線" 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" msgid "OpenWrt Forward"
msgstr "OpenWrt 轉送" 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)" msgid "OpenWrt Forward: MagicDNS via dnsmasq forwarding.(Only support ts.net)"
msgstr "OpenWrt 轉送:僅經由 dnsmasq 轉送至 MagicDNS。(僅支援tailxxxxxx.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 "" msgid ""
"Optional: Specify a custom control server URL (e.g., a Headscale instance, " "Optional: Specify a custom control server URL (e.g., a Headscale "
"%s)." "instance, %s)."
msgstr "可選:指定自訂控制伺服器 URL(例如,Headscale 實例,「%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 "" msgid ""
"Optional: Specify an authentication key for the custom control server. Leave " "Optional: Specify an authentication key for the custom control "
"blank if not required." "server. Leave blank if not required."
msgstr "可選:為自訂控制伺服器指定一個認證金鑰。如果不需要,請留白。" 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" msgid "Peer Relay Port"
msgstr "對等中繼連接埠" 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." msgid "Please use the login button in the settings below to authenticate."
msgstr "請使用下方設定中的登入按鈕進行認證。" msgstr "請使用下方設定中的登入按鈕進行認證。"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:525 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:772
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:531 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:778
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:565 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:812
msgid "Please wait." msgid "Please wait."
msgstr "請稍候。" 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" msgid "RUNNING"
msgstr "正在執行" 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" msgid "RX"
msgstr "接收" 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" msgid "Refresh"
msgstr "重新整理" msgstr "重新整理"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:525 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:177
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:531 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..." msgid "Requesting Login URL..."
msgstr "正在請求登入 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." msgid "Requesting Tailscale login URL... Please wait."
msgstr "正在請求 Tailscale 登入 URL... 請稍候。" 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 "" msgid ""
"Select an exit node from the list. If enabled, Allow LAN Access is enabled " "Select an exit node from the list. If enabled, Allow LAN Access is "
"implicitly." "enabled implicitly."
msgstr "從清單中選擇一個出口節點。如果啟用,則會被動啟用‘允許區域網路存取’" 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 "" msgid ""
"Select the firewall backend for Tailscale to use. Requires service restart " "Select the firewall backend for Tailscale to use. Requires service "
"to take effect." "restart to take effect."
msgstr "選擇 Tailscale 使用的防火牆後端。需要重新啟動服務才能生效。" msgstr "選擇 Tailscale 使用的防火牆後端。需要重新啟動服務才能生效。"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:198 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:320
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:206 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:328
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:218 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:340
#: 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 "Service Status" msgid "Service Status"
msgstr "服務狀態" 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 "Shields Up" msgid ""
msgstr "開啟防護 (Shields Up)" "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" msgid "Status"
msgstr "狀態" 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." msgid "Successfully logged out."
msgstr "登出成功。" 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" msgid "TAILSCALE NOT FOUND"
msgstr "找不到 TAILSCALE" 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" msgid "TUN Mode"
msgstr "TUN 模式" 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" msgid "TX"
msgstr "傳送" 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" msgid "Tailnet Name"
msgstr "Tailnet 名稱" msgstr "Tailnet 名稱"
#: 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
#: applications/luci-app-tailscale-community/root/usr/share/luci/menu.d/luci-app-tailscale-community.json:3 #: luci-app-tailscale-community/root/usr/share/luci/menu.d/luci-app-tailscale-community.json:1
msgid "Tailscale" msgid "Tailscale"
msgstr "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" msgid "Tailscale Health Check: %s"
msgstr "Tailscale 健康檢查: %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" msgid "Tailscale IP"
msgstr "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" msgid "Tailscale IPv4"
msgstr "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" msgid "Tailscale IPv6"
msgstr "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" msgid "Tailscale Login"
msgstr "Tailscale 登入" 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 "" msgid ""
"Tailscale is a mesh VPN solution that makes it easy to connect your devices " "Tailscale is a mesh VPN solution that makes it easy to connect your "
"securely. This configuration page allows you to manage Tailscale settings on " "devices securely. This configuration page allows you to manage "
"your OpenWrt device." "Tailscale settings on your OpenWrt device."
msgstr "" msgstr ""
"Tailscale 是一個網狀 VPN 解決方案,可以輕鬆地安全連線您的裝置。此設定頁面允許" "Tailscale 是一個網狀 VPN 解決方案,可以輕鬆地安全連線您的裝置。此設定頁面允許您在 OpenWrt 裝置上管理 "
"您在 OpenWrt 裝置上管理 Tailscale 設定。" "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" msgid "Tailscale status error"
msgstr "Tailscale 狀態錯誤" 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." msgid "This can take up to 30 seconds."
msgstr "此過程最多可能需要 30 秒。" 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 "" msgid ""
"This will disconnect this device from your Tailnet and require you to re-" "This will disconnect this device from your Tailnet and require you to "
"authenticate." "re-authenticate."
msgstr "這將使此裝置從您的 Tailnet 中斷連線,並需要您重新進行身份驗證。" msgstr "這將使此裝置從您的 Tailnet 中斷連線,並需要您重新進行身份驗證。"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:406 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:143
msgid "" #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:194
"UDP port for the Peer Relay service. Open this port on your router firewall/" msgid "Timed out waiting for the firewall reload"
"NAT." msgstr "等待防火牆重新載入逾時"
msgstr "用於對等中繼服務的 UDP 連接埠。請在路由器防火牆上開啟此連接埠以及 NAT。"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:123 #: luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:605
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:150 msgid ""
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:153 "UDP port for the Peer Relay service. Open this port on your router "
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:546 "firewall/NAT."
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:572 msgstr "用於對等中繼服務的 UDP 連接埠。請在路由器的防火牆/NAT 上開放此連接埠。"
#: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:609
#: 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" msgid "Unknown error"
msgstr "未知錯誤" 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" msgid "Version"
msgstr "版本" 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 "" msgid ""
"When enabled, blocks all inbound connections from the Tailscale network." "When enabled, blocks all inbound connections from the Tailscale "
"network."
msgstr "啟用後,將阻止來自 Tailscale 網路的所有入站連線。" 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." msgid "When using the exit node, access to the local LAN is allowed."
msgstr "使用出口節點時,允許存取區域網路。" 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 "" msgid ""
"[Important] Log in to the Tailscale admin console and manually enable " "[Important] Log in to the Tailscale admin console and manually enable "
"\"Subnet Routes\" for this device." "\"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" msgid "ago"
msgstr "前" 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 "" #~ msgid ""
#~ "Completely disable all Tailscale firewall auto-configuration, including " #~ "Completely disable all Tailscale firewall auto-configuration, "
#~ "zone creation, forwarding rules, and netfilter settings." #~ "including zone creation, forwarding rules, and netfilter settings."
#~ msgstr "" #~ msgstr "完全停用所有 Tailscale 防火牆自動設定,包括區域建立、轉送規則和 netfilter 設定。"
#~ "完全停用所有 Tailscale 防火牆自動設定,包括區域建立、轉送規則和 netfilter "
#~ "設定。"
#~ msgid "" #~ msgid ""
#~ "Optional: Specify a custom control server URL (e.g., a Headscale " #~ "Optional: Specify a custom control server URL (e.g., a Headscale "
#~ "instance, https://example.com)." #~ "instance, https://example.com)."
#~ msgstr "" #~ msgstr "可選:指定一個自定義控制伺服器 URL (例如,一個 Headscale 例項,https://example.com)。"
#~ "可選:指定一個自定義控制伺服器 URL (例如,一個 Headscale 例項,https://"
#~ "example.com)。"
#~ msgid "Applying changes..." #~ msgid "Applying changes..."
#~ msgstr "正在套用更改..." #~ msgstr "正在套用更改..."
@@ -599,3 +709,24 @@ msgstr "並啟用IP偽裝和 MSS 箝制(MTU 修復)以確保連線穩定。"
#~ msgid "Tailscale settings applied successfully." #~ msgid "Tailscale settings applied successfully."
#~ msgstr "Tailscale 設定已成功套用。" #~ 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 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 # Compare version strings: return 0 (true) if $1 >= $2
version_gte() { version_gte() {
awk -v a="${1%%-*}" -v b="${2%%-*}" 'BEGIN { awk -v a="${1%%-*}" -v b="${2%%-*}" 'BEGIN {
@@ -21,6 +30,31 @@ version_gte() {
return $? 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 # Add/remove DNS forward entry in dnsmasq for MagicDNS via OpenWrt
setup_tailscale_dns_forward() { setup_tailscale_dns_forward() {
if [ ! -f /etc/config/dhcp ] || [ ! -x /etc/init.d/dnsmasq ]; then if [ ! -f /etc/config/dhcp ] || [ ! -x /etc/init.d/dnsmasq ]; then
@@ -49,27 +83,52 @@ remove_tailscale_dns_forward() {
fi fi
local entry="/.ts.net/100.100.100.100" local entry="/.ts.net/100.100.100.100"
local idx=0 local found=0 s
local removed=0 for s in $(uci -q get dhcp.@dnsmasq[0].server); do
while true; do [ "$s" = "$entry" ] && found=1
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))
done done
if [ "$removed" = "1" ]; then if [ "$found" = "1" ]; then
uci -q del_list dhcp.@dnsmasq[0].server="$entry"
uci commit dhcp uci commit dhcp
/etc/init.d/dnsmasq reload 2>/dev/null || /etc/init.d/dnsmasq restart 2>/dev/null /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" logger -t "$NAME" "Removed Tailscale DNS forward entry: $entry"
fi 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() { apply_settings() {
local ts_bin local ts_bin
if [ -x /usr/sbin/tailscale ]; then if [ -x /usr/sbin/tailscale ]; then
@@ -143,16 +202,10 @@ apply_settings() {
case "$dns_mode" in case "$dns_mode" in
magicdns) magicdns)
set -- "$@" --accept-dns=true 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 set -- "$@" --accept-dns=false
remove_tailscale_dns_forward
;; ;;
esac esac
@@ -186,7 +239,7 @@ apply_settings() {
# --relay-server-port (requires tailscale >= 1.90.5) # --relay-server-port (requires tailscale >= 1.90.5)
local ts_ver local ts_ver
ts_ver=$("$ts_bin" version 2>/dev/null | head -n1) 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 if [ "$enable_relay" = "1" ]; then
set -- "$@" "--relay-server-port=${relay_server_port:-40000}" set -- "$@" "--relay-server-port=${relay_server_port:-40000}"
else else
@@ -208,26 +261,146 @@ apply_settings() {
return $ret return $ret
} }
handle_service_state() { # Enable or disable autostart and the running daemon.
config_load tailscale #
local service_enabled # 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' config_get service_enabled settings service_enabled '1'
if [ "$service_enabled" = "0" ]; then prev=$(cat "$ENABLED_FILE" 2>/dev/null)
logger -t "$NAME" "Service is disabled, stopping tailscale" case "$prev" in
/etc/init.d/tailscale stop 2>/dev/null 0|1) ;;
/etc/init.d/tailscale disable 2>/dev/null *)
killall tailscaled 2>/dev/null # No usable record of the last applied value (first apply after
else # an upgrade, or a truncated file). Adopt the configuration as
logger -t "$NAME" "Service is enabled, ensuring tailscale is running" # the baseline instead of treating it as a transition: starting
/etc/init.d/tailscale enable 2>/dev/null # a daemon here would run start_service, which arms another UCI
/etc/init.d/tailscale start 2>/dev/null # rollback window right after the one this apply is inside.
apply_settings echo "$service_enabled" > "$ENABLED_FILE"
prev="$service_enabled"
;;
esac
if [ -f "$DISABLE_REQUEST_FILE" ]; then
requested=1
fi 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() { 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_settings
apply_dns_forward
} }
service_triggers() { service_triggers() {
@@ -235,5 +408,6 @@ service_triggers() {
} }
reload_service() { reload_service() {
mkdir -p "$STATE_DIR"
handle_service_state 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"
@@ -7,7 +7,8 @@
"get_status", "get_status",
"get_settings", "get_settings",
"get_subroutes", "get_subroutes",
"get_logs" "get_logs",
"get_firewall_status"
] ]
}, },
"uci": [ "tailscale" ] "uci": [ "tailscale" ]
@@ -17,7 +18,8 @@
"tailscale": [ "tailscale": [
"do_login", "do_login",
"do_logout", "do_logout",
"setup_firewall" "setup_firewall",
"set_service_enabled"
] ]
}, },
"uci": [ "tailscale" ] "uci": [ "tailscale" ]
@@ -149,7 +149,6 @@ methods.do_login = {
if (status.status != 'logout') { if (status.status != 'logout') {
return { error: 'Tailscale is already logged in and running.' }; return { error: 'Tailscale is already logged in and running.' };
} }
// --- 1. Prepare and Run Login Command (Once) --- // --- 1. Prepare and Run Login Command (Once) ---
const loginserver = trim(form_data.loginserver) || ''; const loginserver = trim(form_data.loginserver) || '';
const loginserver_authkey = trim(form_data.loginserver_authkey) || ''; const loginserver_authkey = trim(form_data.loginserver_authkey) || '';
@@ -177,7 +176,7 @@ methods.do_login = {
let parts = split(trline, ' '); let parts = split(trline, ' ');
for (let part in parts) { for (let part in parts) {
if (index(part, 'http') != -1) { 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) { if (logout_result.code != 0) {
return { error: 'Failed to logout: ' + logout_result.stderr }; 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_network = false;
let changed_firewall = false; let changed_firewall = false;
let run_id = 0;
// 1. config Network Interface // 1. config Network Interface
let net_ts = uci.get('network', 'tailscale'); let net_ts = uci.get('network', 'tailscale');
@@ -335,24 +335,57 @@ methods.setup_firewall = {
changed_firewall = true; 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) { if (changed_network) {
uci.save('network'); uci.save('network');
uci.commit('network'); uci.commit('network');
exec('/etc/init.d/network reload');
} }
if (changed_firewall) { if (changed_firewall) {
uci.save('firewall'); uci.save('firewall');
uci.commit('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 { return {
success: true, success: true,
changed_network: changed_network, changed_network: changed_network,
changed_firewall: changed_firewall, 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) { } 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 = { methods.get_logs = {
args: { lines: 200 }, args: { lines: 200 },
call: function(request) { call: function(request) {
@@ -368,9 +470,9 @@ methods.get_logs = {
let cmd = 'logread -l ' + lines + ' 2>/dev/null | grep -i -E "tailscale" || true'; let cmd = 'logread -l ' + lines + ' 2>/dev/null | grep -i -E "tailscale" || true';
let result = exec(cmd); let result = exec(cmd);
if (result.code == 0) { 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
View File
@@ -10,7 +10,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=mptcp PKG_NAME:=mptcp
PKG_VERSION:=6.1 PKG_VERSION:=6.1
PKG_RELEASE:=28 PKG_RELEASE:=29
PKG_MAINTAINER:=Ycarus (Yannick Chabanois) <ycarus@zugaina.org> PKG_MAINTAINER:=Ycarus (Yannick Chabanois) <ycarus@zugaina.org>
PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME) PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME)
+58 -35
View File
@@ -265,28 +265,43 @@ interface_macaddr_count() {
[ "$macaddr" = "$dmacaddr" ] && [ "${intf%.*}" = "$intf" ] && nbmac=$((nbmac+1)) [ "$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() { interface_max_metric() {
local config="$1" local config="$1"
case "$1" in case "$1" in
omrvpn) omrvpn)
uci -q batch <<-EOF >/dev/null _mptcp_uci_set network.${config}.metric 1500
set network.${config}.metric=1500 _mptcp_uci_set network.${config}.ip4table 1500
set network.${config}.ip4table=1500 _mptcp_uci_set network.${config}.ip6table 61500
set network.${config}.ip6table=61500 _mptcp_uci_set openmptcprouter.${config}.metric 1500
commit network
set openmptcprouter.${config}.metric=1500
commit openmptcprouter
EOF
;; ;;
omr6in4) omr6in4)
uci -q batch <<-EOF >/dev/null _mptcp_uci_set network.${config}.metric 1201
set network.${config}.metric=1201 _mptcp_uci_set network.${config}.ip4table 1201
set network.${config}.ip4table=1201 _mptcp_uci_set network.${config}.ip6table 61201
set network.${config}.ip6table=61201 _mptcp_uci_set openmptcprouter.${config}.metric 1201
commit network
set openmptcprouter.${config}.metric=1201
commit openmptcprouter
EOF
;; ;;
lan) lan)
# only when needed: this runs on every reload, sed -i writes flash # only when needed: this runs on every reload, sed -i writes flash
@@ -477,10 +492,10 @@ interface_multipath_settings() {
# metric="9998" # metric="9998"
fi fi
config_set "$config" metric $metric config_set "$config" metric $metric
uci -q set network.${config}.metric=$metric _mptcp_uci_set network.${config}.metric "$metric"
uci -q set network.${config}.ip4table=$metric _mptcp_uci_set network.${config}.ip4table "$metric"
uci -q set network.${config}.ip6table=6$metric _mptcp_uci_set network.${config}.ip6table "6$metric"
uci -q set openmptcprouter.${config}.metric=$metric _mptcp_uci_set openmptcprouter.${config}.metric "$metric"
config_get mode "$config" multipath config_get mode "$config" multipath
config_get disabled "$config" disabled config_get disabled "$config" disabled
[ "$disabled" = "1" ] && mode="off" [ "$disabled" = "1" ] && mode="off"
@@ -504,7 +519,7 @@ interface_multipath_settings() {
mode="$(uci -q get openmptcprouter.${config}.multipath)" mode="$(uci -q get openmptcprouter.${config}.multipath)"
[ -n "$mode" ] && { [ -n "$mode" ] && {
logger -t "MPTCP" "No multipath setting for $config, restoring $mode from openmptcprouter config" 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" = "" ] && { [ "$mode" = "" ] && {
@@ -513,8 +528,8 @@ interface_multipath_settings() {
[ "$config" = "omr6in4" ] && mode="off" [ "$config" = "omr6in4" ] && mode="off"
[ "$mode" = "" ] && mode="off" [ "$mode" = "" ] && mode="off"
logger -t "MPTCP" "Multipath not defined for $config set to $mode" logger -t "MPTCP" "Multipath not defined for $config set to $mode"
uci -q set network.${config}.multipath="$mode" _mptcp_uci_set network.${config}.multipath "$mode"
uci -q set openmptcprouter.${config}.multipath="$mode" _mptcp_uci_set openmptcprouter.${config}.multipath "$mode"
} }
[ "$mode" != "off" ] && { [ "$mode" != "off" ] && {
# LRO, TSO and UFO break MPTCP packets scheduling, always disable them on multipath interfaces # 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" [ -n "$mptcpintf" ] && mptcpintf="$mptcpintf $iface"
[ -z "$mptcpintf" ] && mptcpintf="$iface" [ -z "$mptcpintf" ] && mptcpintf="$iface"
[ -z "$mptcpmintf" ] && mptcpmintf="$config" [ -z "$mptcpmintf" ] && mptcpmintf="$config"
uci -q set network.${config}.defaultroute=0 _mptcp_uci_set network.${config}.defaultroute 0
uci -q set network.${config}.peerdns=0 _mptcp_uci_set network.${config}.peerdns 0
uci -q delete network.${config}.dns _mptcp_uci_delete network.${config}.dns
echo '' > /etc/resolv.conf >/dev/null 2>&1 echo '' > /etc/resolv.conf >/dev/null 2>&1
} }
[ "$mode" = "master" ] && { [ "$mode" = "master" ] && {
@@ -537,14 +552,14 @@ interface_multipath_settings() {
logger -t "MPTCP" "Multipath master already set, disable master for $config" logger -t "MPTCP" "Multipath master already set, disable master for $config"
mode="on" mode="on"
config_set "$config" multipath "on" config_set "$config" multipath "on"
uci -q set network.${config}.multipath="on" _mptcp_uci_set network.${config}.multipath "on"
uci -q set openmptcprouter.${config}.multipath="on" _mptcp_uci_set openmptcprouter.${config}.multipath "on"
else else
master="$config" master="$config"
fi fi
} }
uci -q set openmptcprouter.${config}="interface" _mptcp_uci_set openmptcprouter.${config} "interface"
uci -q set openmptcprouter.${config}.multipath="$mode" _mptcp_uci_set openmptcprouter.${config}.multipath "$mode"
config_get macaddr "$config" macaddr "" config_get macaddr "$config" macaddr ""
[ -n "$macaddr" ] && { [ -n "$macaddr" ] && {
@@ -552,7 +567,7 @@ interface_multipath_settings() {
config_foreach interface_macaddr_count interface $macaddr config_foreach interface_macaddr_count interface $macaddr
[ "$nbmac" != "1" ] && { [ "$nbmac" != "1" ] && {
logger -t "MPTCP" "Duplicate MAC saved, removing" 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 ipaddr $config ipaddr
config_get gateway $config gateway config_get gateway $config gateway
config_get netmask $config netmask 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" ] && 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;}'` [ -n "$ipaddr" ] && [ -n "$netmask" ] && network=`ipcalc.sh $ipaddr/$netmask | sed -n '/NETWORK=/{;s/.*=//;s/ .*//;p;}'`
elif [ "$proto" != "gre" ]; then elif [ "$proto" != "gre" ]; then
@@ -970,6 +992,7 @@ start_service() {
mptcpmintf="" mptcpmintf=""
master="" master=""
count=0 count=0
_mptcp_uci_changed=""
config_load openmptcprouter config_load openmptcprouter
config_foreach interface_max_metric interface config_foreach interface_max_metric interface
@@ -1000,13 +1023,13 @@ start_service() {
intfmaster="$mptcpmintf" intfmaster="$mptcpmintf"
[ "$intfmaster" != "" ] && mptcp_has_master_multipath && { [ "$intfmaster" != "" ] && mptcp_has_master_multipath && {
logger -t "MPTCP" "Master multipath interface disabled, setting it to $intfmaster" logger -t "MPTCP" "Master multipath interface disabled, setting it to $intfmaster"
uci -q set network.${intfmaster}.multipath="master" _mptcp_uci_set network.${intfmaster}.multipath "master"
uci -q set openmptcprouter.${intfmaster}.multipath="master" _mptcp_uci_set openmptcprouter.${intfmaster}.multipath "master"
} }
fi fi
fi fi
[ -n "$(uci -q changes network)" ] && uci -q commit network _mptcp_uci_commit network
[ -n "$(uci -q changes openmptcprouter)" ] && uci -q commit openmptcprouter _mptcp_uci_commit openmptcprouter
case "$board_name" in case "$board_name" in
*raspberry*) case "$board_name" in *4-model-b*|*5*) ;; *) *raspberry*) case "$board_name" in *4-model-b*|*5*) ;; *)
ethtool --offload eth0 rx off tx off > /dev/null 2>&1 ethtool --offload eth0 rx off tx off > /dev/null 2>&1
+44 -29
View File
@@ -83,33 +83,27 @@ if [ "$(uci -q get network.globals.congestion)" = "bbr2" ] && [ -z "$(uname -a |
commit network commit network
EOF EOF
fi 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 uci -q batch <<-EOF >/dev/null
set network.globals.mptcp_syn_retries=4 set network.globals.mptcp_defaults_version=1
commit network commit network
EOF EOF
fi fi
if [ "$(uci -q get network.globals.mptcp_checksum)" != "0" ]; then # enable/disable were written by an older release: the sysctl takes 1/0
uci -q batch <<-EOF >/dev/null case "$(uci -q get network.globals.mptcp_checksum)" in
set network.globals.mptcp_checksum=0 enable) uci -q set network.globals.mptcp_checksum=1; uci -q commit network ;;
commit network disable) uci -q set network.globals.mptcp_checksum=0; uci -q commit network ;;
EOF esac
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
if [ "$(uci -q show network.globals | grep mptcp_fullmesh)" = "" ]; then if [ "$(uci -q show network.globals | grep mptcp_fullmesh)" = "" ]; then
uci -q batch <<-EOF >/dev/null uci -q batch <<-EOF >/dev/null
@@ -127,12 +121,33 @@ if [ "$(uci -q get network.globals.mptcp_subflows)" = "" ]; then
commit network commit network
EOF EOF
fi fi
if [ "$(uci -q get network.globals.mptcp_add_addr_accepted)" = "1" ]; then # A multipath_weight out of 1-256 (0, 300, set by hand or by an older
uci -q batch <<-EOF >/dev/null # release) failed the datatype of the MPTCP pages and blocked every save
set network.globals.mptcp_add_addr_accepted=8 # there. Stored as 040-multipath-weight reads it: not a number is unset
commit network # (100), leading zeros dropped, 0 is 1, over 256 is 256.
EOF _weight_changed=""
fi 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 # 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 # ship no /etc/config/ucitrack, so it failed silently anyway, and the init
+2 -2
View File
@@ -7,9 +7,9 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=openlist2 PKG_NAME:=openlist2
PKG_VERSION:=4.2.7 PKG_VERSION:=4.2.8
PKG_WEB_VERSION:=4.2.6 PKG_WEB_VERSION:=4.2.6
PKG_RELEASE:=20 PKG_RELEASE:=21
PKG_SOURCE:=openlist-$(PKG_VERSION).tar.gz PKG_SOURCE:=openlist-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/OpenListTeam/OpenList/tar.gz/v$(PKG_VERSION)? PKG_SOURCE_URL:=https://codeload.github.com/OpenListTeam/OpenList/tar.gz/v$(PKG_VERSION)?
+2 -2
View File
@@ -5,8 +5,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=sing-box PKG_NAME:=sing-box
PKG_VERSION:=1.14.3 PKG_VERSION:=1.14.4
PKG_RELEASE:=36 PKG_RELEASE:=37
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/SagerNet/sing-box/tar.gz/v$(PKG_VERSION)? PKG_SOURCE_URL:=https://codeload.github.com/SagerNet/sing-box/tar.gz/v$(PKG_VERSION)?
+30 -3
View File
@@ -50,12 +50,12 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=wwand PKG_NAME:=wwand
PKG_VERSION:=1.8.0_pre6 PKG_VERSION:=1.8.0_pre14
PKG_RELEASE:=81 PKG_RELEASE:=82
PKG_SOURCE_PROTO:=git PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git
PKG_SOURCE_VERSION:=7988768d58da5f0f86100b1a2d63db25bdfe87c9 PKG_SOURCE_VERSION:=7c6f90d468a76060da225e0a2a60101b74d83bc5
PKG_MIRROR_HASH:=skip PKG_MIRROR_HASH:=skip
PKG_LICENSE:=GPL-2.0-only 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. cmake capability probe falls back to shipping source on its own.
endef 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; # base: daemon + framework + codec/client + shared core. No backend on its own;
# the daemon loads whichever wwand-qmi/-mbim/-ncm package is present. # 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 # wwand-private, always version-locked to the ucode side). PROVIDES keeps
# configs/upgrades from the era of the separate ucode-mod-wwand-io working. # configs/upgrades from the era of the separate ucode-mod-wwand-io working.
PROVIDES:=ucode-mod-wwand-io PROVIDES:=ucode-mod-wwand-io
EXTRA_DEPENDS:=$(if $(UCODE_BUILT),ucode (>=$(UCODE_BUILT)))
endef endef
define Package/wwand/description define Package/wwand/description