🔥 Sync 2026-09-20 08:04:58

This commit is contained in:
github-actions[bot] committed 2026-09-20 08:04:58 +08:00
1 parent e4a2d7800e
commit 4b4d482791
39 files changed
+755 -216

No files matched your search

+1 -1
View File
@@ -12,7 +12,7 @@ LUCI_DEPENDS:=+luci-base +logd +jsonfilter
LUCI_PKGARCH:=all
PKG_VERSION:=0.1.44
PKG_RELEASE:=25
PKG_RELEASE:=26
# Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI).
ifdef SOURCE_DATE_EPOCH
@@ -33,7 +33,7 @@ const CONSENT_STORAGE_KEY = 'fwlive-logging-consent-v1';
function consentDismissedPermanent() {
try {
return localStorage.getItem(CONSENT_STORAGE_KEY) === '1';
} catch (e) {
} catch (_e) {
return false;
}
}
@@ -41,7 +41,7 @@ function consentDismissedPermanent() {
function persistConsentDismissed() {
try {
localStorage.setItem(CONSENT_STORAGE_KEY, '1');
} catch (e) {
} catch (_e) {
/* private mode / no storage */
}
}
@@ -72,6 +72,21 @@ function blockerCode(state) {
return '';
}
function wanZoneCandidateNames(st) {
return Array.isArray(st && st.wan_zone_candidates)
? st.wan_zone_candidates.filter(function (name) {
return typeof name === 'string';
})
: [];
}
function labelWithZoneCandidates(label, st) {
const children = [label];
const candidates = wanZoneCandidateNames(st);
if (candidates.length) children.push(': ', E('code', {}, [candidates.join(', ')]));
return children;
}
function renderToolbar(host, state, callbacks) {
host.innerHTML = '';
const st = state.loggingStatus;
@@ -85,9 +100,11 @@ function renderToolbar(host, state, callbacks) {
if (blocker === 'no_wan_zone') {
host.appendChild(
E('span', { 'class': 'fwlive-logging-status' }, [
_('WAN logging unavailable: no WAN zone')
])
E(
'span',
{ 'class': 'fwlive-logging-status' },
labelWithZoneCandidates(_('WAN logging unavailable: no WAN zone'), st)
)
);
host.appendChild(links.firewallZonesLink());
return;
@@ -218,7 +235,13 @@ function buildEmptyStateNodes(state, callbacks) {
}
if (blocker === 'no_wan_zone') {
nodes.push(E('p', { 'class': 'fwlive-empty-title' }, [_('No WAN zone found')]));
nodes.push(
E(
'p',
{ 'class': 'fwlive-empty-title' },
labelWithZoneCandidates(_('No WAN zone found'), st)
)
);
nodes.push(
E('p', {}, [
_('No WAN firewall zone found in /etc/config/firewall. Configure zones under '),
@@ -48,11 +48,13 @@ const callFwliveLoggingStatus = rpc.declare({
expect: {
'': {
wan_zone: null,
wan_zone_candidates: [],
wan_log: false,
wan_log_limit: null,
nf_log_ipv4: false,
nf_log_ipv6: false,
ready: false,
weak_device: false,
blockers: [],
warnings: []
}
@@ -62,20 +64,20 @@ const callFwliveLoggingStatus = rpc.declare({
const callFwliveEnableLogging = rpc.declare({
object: 'fwlive',
method: 'enable_wan_logging',
expect: { '': { ok: false, changed: false, wan_zone: null } }
expect: { '': { ok: false, changed: false, wan_zone: null, wan_zone_candidates: [] } }
});
const callFwliveDisableLogging = rpc.declare({
object: 'fwlive',
method: 'disable_wan_logging',
expect: { '': { ok: false, changed: false, wan_zone: null } }
expect: { '': { ok: false, changed: false, wan_zone: null, wan_zone_candidates: [] } }
});
function storedValue(key, fallback) {
try {
const v = localStorage.getItem(key);
return v === null ? fallback : v;
} catch (e) {
} catch (_e) {
return fallback;
}
}
@@ -83,7 +85,7 @@ function storedValue(key, fallback) {
function storeValue(key, value) {
try {
localStorage.setItem(key, value);
} catch (e) {
} catch (_e) {
/* private mode / no storage */
}
}
@@ -214,7 +216,7 @@ return view.extend({
try {
key = decodeURIComponent(kv[0]);
val = decodeURIComponent(kv[1]);
} catch (e) {
} catch (_e) {
continue;
}
result.push({ key: key, val: val });
@@ -623,7 +625,7 @@ return view.extend({
/* Bounds / mktemp failures are reply.error — same idea as poll. */
this.lastRulesError = (res && res.error) || null;
if (this.lastRulesError) console.warn('fwlive rules map error:', this.lastRulesError);
} catch (e) {
} catch (_e) {
if (this.viewDisposed) return;
this.rulesMap = {};
this.firewallBackend = 'nft';
@@ -676,7 +678,7 @@ return view.extend({
if (this.viewDisposed) return;
this.loggingStatus = status;
this.weakDevice = !!(this.loggingStatus && this.loggingStatus.weak_device === true);
} catch (e) {
} catch (_e) {
if (this.viewDisposed) return;
this.loggingStatus = null;
}
@@ -705,7 +707,7 @@ return view.extend({
this.loggingNotice = opts.successNotice(res);
if (opts.onSuccess) opts.onSuccess(res);
await this.loadLoggingStatus();
} catch (e) {
} catch (_e) {
this.loggingNotice = opts.catchNotice();
await this.loadLoggingStatus();
} finally {
@@ -791,10 +793,13 @@ return view.extend({
const st = this.loggingStatus;
/* Sort blockers so unstable backend order does not force a rebuild. */
const blockers = st && st.blockers ? st.blockers.slice().sort().join(',') : '';
const candidates =
st && Array.isArray(st.wan_zone_candidates) ? st.wan_zone_candidates.join(',') : '';
return [
st ? (st.wan_log ? '1' : '0') : 'x',
st ? String(st.wan_log_limit || '') : '',
blockers,
candidates,
this.loggingBusy ? '1' : '0',
this.loggingNotice || '',
this.shouldShowLoggingConsent() ? 'c1' : 'c0'
@@ -876,7 +881,7 @@ return view.extend({
reply = await callFwlivePoll({
addresses: [String(fetchLines)]
});
} catch (e) {
} catch (_e) {
reply = null;
}
return {
@@ -1678,7 +1683,7 @@ return view.extend({
this.updateAdaptiveBanner();
if (updated) this.scheduleRenderRows(true);
} catch (e) {
} catch (_e) {
/* resolve unavailable — show IPs */
} finally {
if (gen === this.resolveGeneration) this.resolveInFlight = false;
@@ -1964,7 +1969,7 @@ return view.extend({
try {
try {
await this.fetchEntries();
} catch (e) {
} catch (_e) {
/* fetchEntries already accounts the poll RTT for every rpc
* outcome; a throw here is a local normalize/buffer bug, not
* network slowness, so count nothing further. */
@@ -1982,10 +1987,10 @@ return view.extend({
try {
await this.resolveHostnamesForEntries(this.filteredRows());
} catch (e) {
} catch (_e) {
/* resolve unavailable — show IPs */
}
} catch (e) {
} catch (_e) {
/* Keep the coordinator promise settling so a queued refresh cannot
* be stranded by an unexpected local rendering failure. */
if (epoch === this.currentPollEpoch()) this.lastPollError = true;
@@ -21,6 +21,8 @@ if ! command -v jsonfilter >/dev/null 2>&1; then
fi
FILTER_DIR="$(cd "$(dirname "$0")" && pwd)"
FILTER_TMP_DIR='/tmp'
[ "$#" -gt 0 ] && FILTER_TMP_DIR="$1"
# shellcheck disable=SC1091 # classifier is a sibling file next to this script
. "$FILTER_DIR/fwlive-is-firewall-event.sh"
@@ -40,14 +42,15 @@ fi
# jsonfilter output in a secure temporary file before classification so a
# partial pipeline cannot produce malformed JSON on failure.
# This script runs from rpcd as root and reopens the path for classification.
# Match _fwlive_mktemp: only use the verified, sticky system /tmp; honoring
# TMPDIR would permit a non-sticky attacker-controlled directory here.
# Match _fwlive_mktemp: only use a verified, sticky dump directory. The
# production rpcd caller passes /tmp; the explicit argument lets host tests
# exercise the directory guard without changing the host's /tmp.
# shellcheck disable=SC3065 # OpenWrt BusyBox test supports -k; match rpcd helper.
if [ ! -d /tmp ] || [ -L /tmp ] || ! [ -k /tmp ]; then
if [ ! -d "$FILTER_TMP_DIR" ] || [ -L "$FILTER_TMP_DIR" ] || ! [ -k "$FILTER_TMP_DIR" ]; then
printf '%s' '{"log":[],"error":"filter_tempfile_failed"}'
exit 1
fi
_filter_tmp=$(mktemp /tmp/fwlive-filter.XXXXXX) || {
_filter_tmp=$(mktemp "$FILTER_TMP_DIR/fwlive-filter.XXXXXX") || {
printf '%s' '{"log":[],"error":"filter_tempfile_failed"}'
exit 1
}
@@ -9,8 +9,19 @@
# expect soft failures. The rpcd entry point enables strict mode; critical
# paths use explicit `|| return 1` / `|| true`.
NF_LOG_IPV4='/proc/sys/net/netfilter/nf_log/2'
NF_LOG_IPV6='/proc/sys/net/netfilter/nf_log/10'
NF_LOG_IPV4="${FWLIVE_NF_LOG_IPV4_PATH:-/proc/sys/net/netfilter/nf_log/2}"
NF_LOG_IPV6="${FWLIVE_NF_LOG_IPV6_PATH:-/proc/sys/net/netfilter/nf_log/10}"
# /proc/sys/net/netfilter/nf_log/10 is a backend selector, not an IPv6
# availability probe. A missing or empty /proc/net/if_inet6 means the IPv6
# stack is absent (compiled out or ipv6.disable=1), so an IPv6 backend is
# not required. Any content, including loopback ::1, means the IPv6 stack
# is present and the IPv6 logger is required. This is not a WAN-address
# probe: stock OpenWrt with CONFIG_IPV6 still has lo ::1 on an IPv4-only WAN.
NF_LOG_IPV6_AVAILABLE_PATH="${FWLIVE_IPV6_AVAILABLE_PATH:-/proc/net/if_inet6}"
NF_LOG_STATE_COMPUTED=0
NF_LOG_IPV4_READY=false
NF_LOG_IPV6_READY=false
# Serialize the WAN logging read->compute->set->commit window across
# concurrent ubus write-ACL callers: each toggle re-reads the current
@@ -26,6 +37,8 @@ NF_LOG_IPV6='/proc/sys/net/netfilter/nf_log/10'
# Overridable for tests/containers (default is root-only /etc/fwlive).
WAN_LOG_LOCK_FILE="${FWLIVE_WAN_LOG_LOCK_FILE:-/etc/fwlive/logging.lock}"
WAN_LOG_BASELINE_FILE="${FWLIVE_WAN_LOG_BASELINE_FILE:-/etc/fwlive/wan-log-baseline}"
WAN_ZONE_DIAGNOSTIC_JSON=''
WAN_ZONE_FOUND=''
weak_device_detected() {
# Path overrides are test hooks; production defaults stay in procfs.
@@ -170,26 +183,78 @@ release_wan_log_lock() {
exec 9>&-
}
find_wan_zone_section() {
# Match anonymous (@zone[N]) and named (e.g. wan) sections whose
# name option is 'wan'. Prefer the first section whose type is
# zone; skip non-zone sections that happen to share name='wan'.
find_wan_zone_section_state() {
# Match the first zone whose name is 'wan' or whose effective network list
# contains 'wan'/'wan6'. UCI defaults an omitted network option to the
# section name, so a renamed zone such as internet with network=wan is
# supported while a fully renamed zone remains an explicit no-WAN result.
# The diagnostic list is reset on every lookup and records zone names for
# no_wan_zone callers; it is JSON-escaped before it reaches a reply.
# uci missing / no wan zone is empty, not fatal. pipefail + set -e
# cannot apply to this pipeline.
_zones=$(uci -q show firewall 2>/dev/null \
| sed -n "s/^firewall\.\([^.]*\)\.name='wan'$/\1/p") || true
WAN_ZONE_DIAGNOSTIC_JSON=''
WAN_ZONE_FOUND=''
_firewall_show=$(uci -q show firewall 2>/dev/null || true)
_zones=$(printf '%s\n' "$_firewall_show" \
| sed -n 's/^firewall\.\([^.=]*\)=zone$/\1/p')
for zone in $_zones; do
[ -n "$zone" ] || continue
_name=$(uci -q get "firewall.${zone}.name" 2>/dev/null || true)
if [ -z "$_name" ]; then
_name=$(printf '%s\n' "$_firewall_show" \
| awk -v key="firewall.${zone}.name=" \
'index($0, key) == 1 {
value = substr($0, length(key) + 1)
sub(/^'\''/, "", value)
sub(/'\''$/, "", value)
print value
exit
}')
fi
# UCI permits a zone section without an explicit name. In that case
# the section id is the effective zone name for omitted network lists.
[ -n "$_name" ] || _name="$zone"
[ -n "$_name" ] && _zone_label="$_name" || _zone_label="$zone"
_esc=$(printf '%s' "$_zone_label" | json_escape)
if [ -n "$WAN_ZONE_DIAGNOSTIC_JSON" ]; then
WAN_ZONE_DIAGNOSTIC_JSON="${WAN_ZONE_DIAGNOSTIC_JSON},"
fi
WAN_ZONE_DIAGNOSTIC_JSON="${WAN_ZONE_DIAGNOSTIC_JSON}\"${_esc}\""
# uci -q get exits 1 on a missing section. Capture with || true so
# set -e cannot abort inside "$(…)" before || continue.
_type=$(uci -q get "firewall.${zone}" 2>/dev/null || true)
[ "$_type" = "zone" ] || continue
printf '%s' "$zone"
return 0
_network=$(uci -q get "firewall.${zone}.network" 2>/dev/null || true)
[ -n "$_network" ] || _network="$_name"
_is_wan=0
[ "$_name" = wan ] && _is_wan=1
case " ${_network} " in
*' wan '*|*' wan6 '*) _is_wan=1 ;;
esac
if [ "$_is_wan" = 1 ]; then
WAN_ZONE_FOUND="$zone"
return 0
fi
done
return 0
}
find_wan_zone_section() {
find_wan_zone_section_state
printf '%s' "$WAN_ZONE_FOUND"
}
wan_zone_diagnostic_json() {
printf '[%s]' "${WAN_ZONE_DIAGNOSTIC_JSON:-}"
}
no_wan_zone_error_json() {
_candidates=$(wan_zone_diagnostic_json)
printf '{"ok":false,"changed":false,"wan_zone":null,"error":"no_wan_zone","wan_zone_candidates":%s}' \
"$_candidates"
}
firewall_changes_pending() {
# uci miss is "no pending changes". set -e cannot apply.
pending="$(uci -q changes firewall 2>/dev/null || true)"
@@ -346,7 +411,8 @@ restore_wan_log_baseline() {
[ -f "$path" ] || return 0
# Empty file is a valid "option was unset" baseline.
baseline=$(cat "$path" 2>/dev/null || true)
zone=$(find_wan_zone_section)
find_wan_zone_section_state
zone=$WAN_ZONE_FOUND
if [ -z "$zone" ]; then
logger -t fwlive "WAN log baseline restore skipped: no WAN zone" 2>/dev/null || true
return 1
@@ -425,7 +491,40 @@ read_nf_log_backend() {
path="$1"
[ -f "$path" ] || return 1
val=$(cat "$path" 2>/dev/null) || return 1
[ -n "$val" ] && [ "$val" != 'none' ]
# Sysctl values are single-line, but trim surrounding whitespace before
# interpreting the no-backend sentinel. This keeps a malformed `NONE `
# value from becoming a fail-open logger.
while case "$val" in [[:space:]]*) true ;; *) false ;; esac; do
val=${val#?}
done
while case "$val" in *[[:space:]]) true ;; *) false ;; esac; do
val=${val%?}
done
[ -n "$val" ] || return 1
case "$val" in
[Nn][Oo][Nn][Ee]) return 1 ;;
esac
return 0
}
nf_log_family_available() {
family="$1"
case "$family" in
ipv4)
# Supported fwlive deployments require IPv4 for the WAN path.
return 0
;;
ipv6)
# Content probe: missing/empty => stack absent; any row
# (including lo ::1) => stack present. Not a WAN check.
path="${FWLIVE_IPV6_AVAILABLE_PATH:-$NF_LOG_IPV6_AVAILABLE_PATH}"
[ -r "$path" ] || return 1
grep -q '[^[:space:]]' "$path" 2>/dev/null
;;
*)
return 1
;;
esac
}
check_nf_log_ipv4() {
@@ -433,9 +532,20 @@ check_nf_log_ipv4() {
}
check_nf_log_ipv6() {
# Effective readiness: an absent IPv6 stack is not a blocker. When
# if_inet6 has any address, including lo, the IPv6 logger is required.
nf_log_family_available ipv6 || return 0
read_nf_log_backend "$NF_LOG_IPV6"
}
compute_nf_log_state() {
NF_LOG_IPV4_READY=false
check_nf_log_ipv4 && NF_LOG_IPV4_READY=true
NF_LOG_IPV6_READY=false
check_nf_log_ipv6 && NF_LOG_IPV6_READY=true
NF_LOG_STATE_COMPUTED=1
}
logging_blockers_append() {
blocker="$1"
[ -n "$blocker" ] || return 0
@@ -459,16 +569,28 @@ logging_warnings_append() {
collect_logging_blockers() {
zone="$1"
LOGGING_BLOCKERS=''
[ "$NF_LOG_STATE_COMPUTED" = 1 ] || compute_nf_log_state
[ -n "$zone" ] || logging_blockers_append 'no_wan_zone'
check_nf_log_ipv4 || logging_blockers_append 'nf_log_ipv4_missing'
check_nf_log_ipv6 || logging_blockers_append 'nf_log_ipv6_missing'
[ "$NF_LOG_IPV4_READY" = true ] || logging_blockers_append 'nf_log_ipv4_missing'
[ "$NF_LOG_IPV6_READY" = true ] || logging_blockers_append 'nf_log_ipv6_missing'
# Report via LOGGING_BLOCKERS, not exit status: return 1 would abort
# build_logging_status_json under set -e.
return 0
}
legacy_iptables_active() {
for _path in \
"${FWLIVE_IP_TABLES_NAMES_PATH:-/proc/net/ip_tables_names}" \
"${FWLIVE_IP6_TABLES_NAMES_PATH:-/proc/net/ip6_tables_names}"; do
if [ -r "$_path" ] && grep -q '[^[:space:]]' "$_path" 2>/dev/null; then
return 0
fi
done
return 1
}
collect_logging_warnings() {
LOGGING_WARNINGS=''
@@ -476,6 +598,12 @@ collect_logging_warnings() {
# Warnings are diagnostics only — do not gate the enable-logging CTA.
command -v timeout >/dev/null 2>&1 || logging_warnings_append 'timeout_missing'
# Diagnostic only: supported releases use nftables, but a registered legacy
# iptables table can still exist in the network namespace visible to rpcd.
# Keep this ubus-only for now; it must not affect readiness or the enable
# gate, and the UI has no remediation flow for legacy tables yet.
legacy_iptables_active && logging_warnings_append 'legacy_iptables_detected'
# Report via LOGGING_WARNINGS, not exit status.
return 0
}
@@ -491,7 +619,11 @@ json_null_or_string() {
}
build_logging_status_json() {
zone=$(find_wan_zone_section)
find_wan_zone_section_state
zone=$WAN_ZONE_FOUND
candidates=$(wan_zone_diagnostic_json)
NF_LOG_STATE_COMPUTED=0
compute_nf_log_state
# Empty zone / unset log bit are valid; set -e cannot apply.
log_val=$(wan_zone_log_value "$zone") || log_val=
# Unset log_limit is a valid empty value; uci -q get exits 1.
@@ -504,10 +636,8 @@ build_logging_status_json() {
wan_log=true
fi
nf4=false
check_nf_log_ipv4 && nf4=true
nf6=false
check_nf_log_ipv6 && nf6=true
nf4=$NF_LOG_IPV4_READY
nf6=$NF_LOG_IPV6_READY
collect_logging_blockers "$zone"
blockers="[${LOGGING_BLOCKERS:-}]"
@@ -524,8 +654,8 @@ build_logging_status_json() {
zone_json=$(json_null_or_string "$zone")
limit_json=$(json_null_or_string "$limit_val")
printf '{"wan_zone":%s,"wan_log":%s,"wan_log_limit":%s,"nf_log_ipv4":%s,"nf_log_ipv6":%s,"ready":%s,"weak_device":%s,"blockers":%s,"warnings":%s}' \
"$zone_json" "$wan_log" "$limit_json" "$nf4" "$nf6" "$ready" "$weak_device" "$blockers" "$warnings"
printf '{"wan_zone":%s,"wan_zone_candidates":%s,"wan_log":%s,"wan_log_limit":%s,"nf_log_ipv4":%s,"nf_log_ipv6":%s,"ready":%s,"weak_device":%s,"blockers":%s,"warnings":%s}' \
"$zone_json" "$candidates" "$wan_log" "$limit_json" "$nf4" "$nf6" "$ready" "$weak_device" "$blockers" "$warnings"
}
reload_firewall() {
@@ -719,15 +849,18 @@ reload_and_report_wan_log() {
}
enable_wan_logging() {
zone=$(find_wan_zone_section)
find_wan_zone_section_state
zone=$WAN_ZONE_FOUND
if [ -z "$zone" ]; then
printf '{"ok":false,"changed":false,"wan_zone":null,"error":"no_wan_zone"}'
no_wan_zone_error_json
return 0
fi
zone_json=$(json_null_or_string "$zone")
if ! check_nf_log_ipv4 || ! check_nf_log_ipv6; then
NF_LOG_STATE_COMPUTED=0
compute_nf_log_state
if [ "$NF_LOG_IPV4_READY" != true ] || [ "$NF_LOG_IPV6_READY" != true ]; then
printf '{"ok":false,"changed":false,"wan_zone":%s,"error":"nf_log_missing"}' "$zone_json"
return 0
fi
@@ -778,9 +911,10 @@ enable_wan_logging() {
}
disable_wan_logging() {
zone=$(find_wan_zone_section)
find_wan_zone_section_state
zone=$WAN_ZONE_FOUND
if [ -z "$zone" ]; then
printf '{"ok":false,"changed":false,"wan_zone":null,"error":"no_wan_zone"}'
no_wan_zone_error_json
return 0
fi
+11 -1
View File
@@ -482,7 +482,7 @@ fetch_firewall_logs() {
# Empty success is not valid poll JSON — the shipped filter always
# prints {"log":[...]}, but a stub/regression must not emit a blank body.
_filter_ok=1
if ! out=$(printf '%s' "$raw" | "$FILTER_SH"); then
if ! out=$(printf '%s' "$raw" | "$FILTER_SH" /tmp); then
_filter_ok=0
if [ -z "$out" ]; then
out='{"log":[],"error":"filter_failed"}'
@@ -862,6 +862,16 @@ Address: 127.0.0.1:53
Address 1: 8.8.8.8 dns.google.")
check_eq parse_nslookup_name "$got" "dns.google" "busybox Address N expected dns.google got '$got'" || return 1
got=$(parse_nslookup_name "Server: 2001:db8::53
Address: 2001:db8::53#53
2001:db8::1.ip6.arpa name = v6.example.")
check_eq parse_nslookup_name "$got" "v6.example" "IPv6 bind-style expected v6.example got '$got'" || return 1
got=$(parse_nslookup_name "Server: 2001:db8::53
Address: 2001:db8::53#53
Address 1: 2001:db8::1 v6.example.")
check_eq parse_nslookup_name "$got" "v6.example" "IPv6 busybox Address N expected v6.example got '$got'" || return 1
got=$(parse_nslookup_name "Server: 192.168.1.1
Address 1: 192.168.1.1 router.lan
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.9.16
PKG_RELEASE:=282
PKG_RELEASE:=283
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -83,6 +83,21 @@ o = s:option(DynamicList, "users", translate("User"))
for i, v in ipairs(user_list) do
o:value(v[".name"], v.username)
end
o.validate = function(self, value, t)
if type(value) == "string" then value = {value} end
if type(value) == "table" then
for _, u in ipairs(value) do
local found = false
for _, v in ipairs(user_list) do
if u == v[".name"] then found = true; break end
end
if not found then
return nil, translate("Please configure user info on the User Management page.")
end
end
end
return value
end
o:depends({ protocol = "mixed" })
o:depends({ protocol = "socks" })
o:depends({ protocol = "http" })
@@ -76,6 +76,21 @@ o = s:option(DynamicList, "users", translate("User"))
for i, v in ipairs(user_list) do
o:value(v[".name"], v.username)
end
o.validate = function(self, value, t)
if type(value) == "string" then value = {value} end
if type(value) == "table" then
for _, u in ipairs(value) do
local found = false
for _, v in ipairs(user_list) do
if u == v[".name"] then found = true; break end
end
if not found then
return nil, translate("Please configure user info on the User Management page.")
end
end
end
return value
end
o:depends({ protocol = "http" })
o:depends({ protocol = "socks" })
o:depends({ protocol = "shadowsocks" })
@@ -50,6 +50,21 @@ for i, v in ipairs(user_list) do
o:value(v[".name"], v.username)
end
o:depends({ custom = false })
o.validate = function(self, value, t)
if type(value) == "string" then value = {value} end
if type(value) == "table" then
for _, u in ipairs(value) do
local found = false
for _, v in ipairs(user_list) do
if u == v[".name"] then found = true; break end
end
if not found then
return nil, translate("Please configure user info on the User Management page.")
end
end
end
return value
end
o = s:option(Flag, "realms", translate("Realms"))
o.default = "0"
@@ -217,8 +217,8 @@ function gen_outbound(flag, node, tag, proxy_table)
tti = 50,
uplinkCapacity = 12,
downlinkCapacity = 100,
CwndMultiplier = 1,
MaxSendingWindow = 2 * 1024 * 1024
cwndMultiplier = 1,
maxSendingWindow = 2 * 1024 * 1024
} or nil,
wsSettings = (node.transport == "ws") and {
path = node.ws_path or "/",
@@ -727,8 +727,8 @@ function gen_config_server(node)
tti = 50,
uplinkCapacity = 12,
downlinkCapacity = 100,
CwndMultiplier = 1,
MaxSendingWindow = 2 * 1024 * 1024
cwndMultiplier = 1,
maxSendingWindow = 2 * 1024 * 1024
} or nil,
wsSettings = (node.transport == "ws") and {
host = node.ws_host or nil,
@@ -3,6 +3,13 @@ local map = self.map
local api = map.api
local section = self.section
-%>
<style>
[id*="_users"] .cbi-dropdown li[data-value="-"],
[id*="_users"] .cbi-dropdown input[type="text"],
[id*="_users"] .cbi-dropdown .preview li[data-value="-"] {
display: none !important;
}
</style>
<script type="text/javascript">
//<![CDATA[
function server_update_config(cfgid, data, redirect_url) {
+3
View File
@@ -1682,6 +1682,9 @@ msgstr "用户管理"
msgid "User Config"
msgstr "用户配置"
msgid "Please configure user info on the User Management page."
msgstr "请在用户管理页设置用户信息。"
msgid "Logs"
msgstr "日志"
@@ -1831,15 +1831,14 @@ get_config() {
DNSMASQ_CONF_DIR=/tmp/dnsmasq.d
DEFAULT_DNSMASQ_CFGID="$(uci -q show "dhcp.@dnsmasq[0]" | awk 'NR==1 {split($0, conf, /[.=]/); print conf[2]}')"
if [ -f "/tmp/etc/dnsmasq.conf.$DEFAULT_DNSMASQ_CFGID" ]; then
DNSMASQ_CONF_DIR="$(awk -F '=' '/^conf-dir=/ {print $2}' "/tmp/etc/dnsmasq.conf.$DEFAULT_DNSMASQ_CFGID")"
if [ -f "/var/etc/dnsmasq.conf.$DEFAULT_DNSMASQ_CFGID" ]; then
DNSMASQ_CONF_DIR="$(awk -F '=' '/^conf-dir=/ {print $2}' "/var/etc/dnsmasq.conf.$DEFAULT_DNSMASQ_CFGID")"
if [ -n "$DNSMASQ_CONF_DIR" ]; then
DNSMASQ_CONF_DIR=${DNSMASQ_CONF_DIR%*/}
else
DNSMASQ_CONF_DIR="/tmp/dnsmasq.d"
fi
fi
[ -d "$DNSMASQ_CONF_DIR" ] || mkdir -p "$DNSMASQ_CONF_DIR"
set_cache_var GLOBAL_DNSMASQ_CONF ${DNSMASQ_CONF_DIR}/dnsmasq-${CONFIG}.conf
set_cache_var GLOBAL_DNSMASQ_CONF_PATH ${GLOBAL_ACL_PATH}/dnsmasq.d
@@ -118,24 +118,26 @@ function copy_instance(var)
local TMP_DNSMASQ_PATH = var["-TMP_DNSMASQ_PATH"]
local conf_lines = {}
local DEFAULT_DNSMASQ_CFGID = sys.exec("echo -n $(uci -q show dhcp.@dnsmasq[0] | awk 'NR==1 {split($0, conf, /[.=]/); print conf[2]}')")
for line in io.lines("/tmp/etc/dnsmasq.conf." .. DEFAULT_DNSMASQ_CFGID) do
local filter
if line:find("passwall") then filter = true end
if line:find("ubus") then filter = true end
if line:find("dhcp") then filter = true end
if line:find("server=") == 1 then filter = true end
if line:find("port=") == 1 then filter = true end
if line:find("min%-cache%-ttl") == 1 then filter = true end
if line:find("conf%-dir=") == 1 then
filter = true
if TMP_DNSMASQ_PATH then
local tmp_path = line:sub(1 + #"conf-dir=")
sys.call(string.format("cp -r %s/* %s/ 2>/dev/null", tmp_path, TMP_DNSMASQ_PATH))
if fs.access("/var/etc/dnsmasq.conf." .. DEFAULT_DNSMASQ_CFGID) then
for line in io.lines("/var/etc/dnsmasq.conf." .. DEFAULT_DNSMASQ_CFGID) do
local filter
if line:find("passwall") then filter = true end
if line:find("ubus") then filter = true end
if line:find("dhcp") then filter = true end
if line:find("server=") == 1 then filter = true end
if line:find("port=") == 1 then filter = true end
if line:find("min%-cache%-ttl") == 1 then filter = true end
if line:find("conf%-dir=") == 1 then
filter = true
if TMP_DNSMASQ_PATH then
local tmp_path = line:sub(1 + #"conf-dir=")
sys.call(string.format("cp -r %s/* %s/ 2>/dev/null", tmp_path, TMP_DNSMASQ_PATH))
end
end
if line:find("address=") == 1 or (line:find("server=") == 1 and line:find("/")) then filter = nil end
if not filter then
tinsert(conf_lines, line)
end
end
if line:find("address=") == 1 or (line:find("server=") == 1 and line:find("/")) then filter = nil end
if not filter then
tinsert(conf_lines, line)
end
end
tinsert(conf_lines, "port=" .. LISTEN_PORT)
+1 -1
View File
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.9.16
PKG_RELEASE:=129
PKG_RELEASE:=130
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -598,9 +598,12 @@ o.default = ""
o.rows = 5
o.wrap = "off"
o:depends({ tls_certificate = true })
o.cfgvalue = function(self, section)
return (m:get(section, "tls_certificate_pem") or ""):gsub("\\n", "\n")
end
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
return value
value = api.trim(value):gsub("\r\n", "\n"):gsub("\r", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
return value:gsub("\n", "\\n")
end
o = s:option(DynamicList, "cipherSuites", translate("Cipher Suites"), '<a href="https://go.dev/src/crypto/tls/cipher_suites.go#L44" target="_blank">***</a>' .. " " .. translate("Configures the list of supported cipher suites."))
@@ -632,9 +635,12 @@ o.default = ""
o.rows = 5
o.wrap = "off"
o:depends({ ech = true })
o.cfgvalue = function(self, section)
return (m:get(section, "ech_config") or ""):gsub("\\n", "\n")
end
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
return value
value = api.trim(value):gsub("\r\n", "\n"):gsub("\r", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
return value:gsub("\n", "\\n")
end
o = s:option(Value, "ech_query_server_name", translate("ECH Query Domain"), translate("Overrides the domain name used for ECH HTTPS record queries."))
@@ -451,9 +451,12 @@ o.default = ""
o.rows = 5
o.wrap = "off"
o:depends({ tls_certificate = true })
o.cfgvalue = function(self, section)
return (m:get(section, "tls_certificate_pem") or ""):gsub("\\n", "\n")
end
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
return value
value = api.trim(value):gsub("\r\n", "\n"):gsub("\r", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
return value:gsub("\n", "\\n")
end
o = s:option(Flag, "ech", translate("ECH"))
@@ -50,7 +50,7 @@ o.validate = function(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.cfgvalue = function(self, section, value)
o.cfgvalue = function(self, section)
local config_str = m:get(section, "config_str")
if config_str then
return api.base64Decode(config_str)
@@ -77,7 +77,8 @@ o = s:option(Value, "port", translate("Listen Port"))
o.datatype = "port"
o:depends({ custom = false })
o = s:option(DynamicList, "users", translate("User"))
o = s:option(MultiValue, "users", translate("User"))
o.cast = "table"
for i, v in ipairs(user_list) do
o:value(v[".name"], v.username)
end
@@ -250,12 +251,19 @@ o.default = 0
o.validate = function(self, value, t)
if value then
local reality = s.fields["reality"] and s.fields["reality"]:formvalue(t) or nil
local use_pem = s.fields["tls_use_pem"] and s.fields["tls_use_pem"]:formvalue(t) or nil
if reality and reality == "1" then return value end
if value == "1" then
if value == "1" and use_pem ~= "1" then
local ca = s.fields["tls_certificateFile"] and s.fields["tls_certificateFile"]:formvalue(t) or ""
local key = s.fields["tls_keyFile"] and s.fields["tls_keyFile"]:formvalue(t) or ""
if ca == "" or key == "" then
return nil, translate("Public key and Private key path can not be empty!")
return nil, translate("Certificate and Private key path can not be empty!")
end
elseif value == "1" and use_pem == "1" then
local ca = s.fields["tls_certificate"] and s.fields["tls_certificate"]:formvalue(t) or ""
local key = s.fields["tls_key"] and s.fields["tls_key"]:formvalue(t) or ""
if ca == "" or key == "" then
return nil, translate("Certificate and Private key PEM can not be empty!")
end
end
return value
@@ -308,14 +316,21 @@ o:depends({ protocol = "hysteria" })
-- [[ TLS ]] --
o = s:option(FileUpload, "tls_certificateFile", translate("Public key absolute path"), translate("as:") .. "/etc/ssl/fullchain.pem")
o.default = m:get(s.section, "tls_certificateFile") or "/etc/config/ssl/" .. s.section .. ".pem"
if o and o:formvalue(s.section) then o.default = o:formvalue(s.section) end
o = s:option(Flag, "tls_use_pem", translate("Use PEM"), translate("Use certificate and private key PEM content."))
o:depends({ tls = true, reality = false })
o:depends({ protocol = "naive" })
o:depends({ protocol = "hysteria" })
o:depends({ protocol = "tuic" })
o:depends({ protocol = "hysteria2" })
o = s:option(FileUpload, "tls_certificateFile", translate("Path to the certificate file"), translate("as:") .. "/etc/ssl/fullchain.crt")
o.default = m:get(s.section, "tls_certificateFile") or "/etc/config/ssl/" .. s.section .. ".crt"
if o and o:formvalue(s.section) then o.default = o:formvalue(s.section) end
o:depends({ tls = true, reality = false, tls_use_pem = false })
o:depends({ protocol = "naive", tls_use_pem = false })
o:depends({ protocol = "hysteria", tls_use_pem = false })
o:depends({ protocol = "tuic", tls_use_pem = false })
o:depends({ protocol = "hysteria2", tls_use_pem = false })
o.validate = function(self, value, t)
if value and value ~= "" then
if not api.fs.access(value) then
@@ -327,14 +342,14 @@ o.validate = function(self, value, t)
return nil
end
o = s:option(FileUpload, "tls_keyFile", translate("Private key absolute path"), translate("as:") .. "/etc/ssl/private.key")
o = s:option(FileUpload, "tls_keyFile", translate("Path to the private key file"), translate("as:") .. "/etc/ssl/private.key")
o.default = m:get(s.section, "tls_keyFile") or "/etc/config/ssl/" .. s.section .. ".key"
if o and o:formvalue(s.section) then o.default = o:formvalue(s.section) end
o:depends({ tls = true, reality = false })
o:depends({ protocol = "naive" })
o:depends({ protocol = "hysteria" })
o:depends({ protocol = "tuic" })
o:depends({ protocol = "hysteria2" })
o:depends({ tls = true, reality = false, tls_use_pem = false })
o:depends({ protocol = "naive", tls_use_pem = false })
o:depends({ protocol = "hysteria", tls_use_pem = false })
o:depends({ protocol = "tuic", tls_use_pem = false })
o:depends({ protocol = "hysteria2", tls_use_pem = false })
o.validate = function(self, value, t)
if value and value ~= "" then
if not api.fs.access(value) then
@@ -346,6 +361,32 @@ o.validate = function(self, value, t)
return nil
end
o = s:option(TextValue, "tls_certificate", "TLS Certificate (PEM)", translate("Full certificate (chain), PEM format."))
o.default = ""
o.rows = 5
o.wrap = "off"
o:depends({ tls_use_pem = true })
o.cfgvalue = function(self, section)
return (m:get(section, "tls_certificate") or ""):gsub("\\n", "\n")
end
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("\r", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
return value:gsub("\n", "\\n")
end
o = s:option(TextValue, "tls_key", "TLS Private Key (PEM)", translate("Private key in PEM format."))
o.default = ""
o.rows = 5
o.wrap = "off"
o:depends({ tls_use_pem = true })
o.cfgvalue = function(self, section)
return (m:get(section, "tls_key") or ""):gsub("\\n", "\n")
end
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("\r", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
return value:gsub("\n", "\\n")
end
o = s:option(Flag, "ech", translate("ECH"))
o.default = "0"
o:depends({ tls = true, flow = "", reality = false })
@@ -359,13 +400,12 @@ o.default = ""
o.rows = 5
o.wrap = "off"
o:depends({ ech = true })
o.cfgvalue = function(self, section)
return (m:get(section, "ech_key") or ""):gsub("\\n", "\n")
end
o.validate = function(self, value)
value = value:gsub("^%s+", ""):gsub("%s+$","\n"):gsub("\r\n","\n"):gsub("[ \t]*\n[ \t]*", "\n")
value = value:gsub("^%s*\n", "")
if value:sub(-1) == "\n" then
value = value:sub(1, -2)
end
return value
value = api.trim(value):gsub("\r\n", "\n"):gsub("\r", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
return value:gsub("\n", "\\n")
end
o = s:option(ListValue, "transport", translate("Transport"))
@@ -489,7 +529,7 @@ o.default = "0"
o:depends({ custom = false })
o = s:option(Value, "firewall_allow_src", translate("Source zone"))
o.rmempty = false
o.rmempty = not m.is_js_luci
o.nocreate = true
o.allowany = true
o.default = "wan"
@@ -46,7 +46,7 @@ o.validate = function(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.cfgvalue = function(self, section, value)
o.cfgvalue = function(self, section)
local config_str = m:get(section, "config_str")
if config_str then
return api.base64Decode(config_str)
@@ -72,7 +72,8 @@ o = s:option(Value, "port", translate("Listen Port"))
o.datatype = "port"
o:depends({ custom = false })
o = s:option(DynamicList, "users", translate("User"))
o = s:option(MultiValue, "users", translate("User"))
o.cast = "table"
for i, v in ipairs(user_list) do
o:value(v[".name"], v.username)
end
@@ -192,12 +193,19 @@ o.default = 0
o.validate = function(self, value, t)
if value then
local reality = s.fields["reality"] and s.fields["reality"]:formvalue(t) or nil
local use_pem = s.fields["tls_use_pem"] and s.fields["tls_use_pem"]:formvalue(t) or nil
if reality and reality == "1" then return value end
if value == "1" then
if value == "1" and use_pem ~= "1" then
local ca = s.fields["tls_certificateFile"] and s.fields["tls_certificateFile"]:formvalue(t) or ""
local key = s.fields["tls_keyFile"] and s.fields["tls_keyFile"]:formvalue(t) or ""
if ca == "" or key == "" then
return nil, translate("Public key and Private key path can not be empty!")
return nil, translate("Certificate and Private key path can not be empty!")
end
elseif value == "1" and use_pem == "1" then
local ca = s.fields["tls_certificate"] and s.fields["tls_certificate"]:formvalue(t) or ""
local key = s.fields["tls_key"] and s.fields["tls_key"]:formvalue(t) or ""
if ca == "" or key == "" then
return nil, translate("Certificate and Private key PEM can not be empty!")
end
end
return value
@@ -262,11 +270,16 @@ end
--o:depends({ tls = true })
-- [[ TLS ]] --
o = s:option(FileUpload, "tls_certificateFile", translate("Public key absolute path"), translate("as:") .. "/etc/ssl/fullchain.pem")
o.default = m:get(s.section, "tls_certificateFile") or "/etc/config/ssl/" .. s.section .. ".pem"
if o and o:formvalue(s.section) then o.default = o:formvalue(s.section) end
o = s:option(Flag, "tls_use_pem", translate("Use PEM"), translate("Use certificate and private key PEM content."))
o:depends({ tls = true, reality = false })
o:depends({ protocol = "hysteria2"})
o:depends({ protocol = "hysteria2" })
o = s:option(FileUpload, "tls_certificateFile", translate("Path to the certificate file"), translate("as:") .. "/etc/ssl/fullchain.crt")
o.default = m:get(s.section, "tls_certificateFile") or "/etc/config/ssl/" .. s.section .. ".crt"
if o and o:formvalue(s.section) then o.default = o:formvalue(s.section) end
o:depends({ tls = true, reality = false, tls_use_pem = false })
o:depends({ protocol = "hysteria2", tls_use_pem = false })
o.validate = function(self, value, t)
if value and value ~= "" then
if not api.fs.access(value) then
@@ -278,11 +291,11 @@ o.validate = function(self, value, t)
return nil
end
o = s:option(FileUpload, "tls_keyFile", translate("Private key absolute path"), translate("as:") .. "/etc/ssl/private.key")
o = s:option(FileUpload, "tls_keyFile", translate("Path to the private key file"), translate("as:") .. "/etc/ssl/private.key")
o.default = m:get(s.section, "tls_keyFile") or "/etc/config/ssl/" .. s.section .. ".key"
if o and o:formvalue(s.section) then o.default = o:formvalue(s.section) end
o:depends({ tls = true, reality = false })
o:depends({ protocol = "hysteria2"})
o:depends({ tls = true, reality = false, tls_use_pem = false })
o:depends({ protocol = "hysteria2", tls_use_pem = false })
o.validate = function(self, value, t)
if value and value ~= "" then
if not api.fs.access(value) then
@@ -294,6 +307,32 @@ o.validate = function(self, value, t)
return nil
end
o = s:option(TextValue, "tls_certificate", "TLS Certificate (PEM)", translate("Full certificate (chain), PEM format."))
o.default = ""
o.rows = 5
o.wrap = "off"
o:depends({ tls_use_pem = true })
o.cfgvalue = function(self, section)
return (m:get(section, "tls_certificate") or ""):gsub("\\n", "\n")
end
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("\r", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
return value:gsub("\n", "\\n")
end
o = s:option(TextValue, "tls_key", "TLS Private Key (PEM)", translate("Private key in PEM format."))
o.default = ""
o.rows = 5
o.wrap = "off"
o:depends({ tls_use_pem = true })
o.cfgvalue = function(self, section)
return (m:get(section, "tls_key") or ""):gsub("\\n", "\n")
end
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("\r", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
return value:gsub("\n", "\\n")
end
o = s:option(Flag, "ech", translate("ECH"))
o.default = "0"
o:depends({ tls = true, reality = false })
@@ -407,7 +446,7 @@ o:depends({ use_finalmask = true })
o.rows = 10
o.wrap = "off"
o.datatype = "json"
o.cfgvalue = function(self, section, value)
o.cfgvalue = function(self, section)
local raw = m:get(section, "finalmask")
if raw then
return api.base64Decode(raw)
@@ -488,7 +527,7 @@ o.default = "0"
o:depends({ custom = false })
o = s:option(Value, "firewall_allow_src", translate("Source zone"))
o.rmempty = false
o.rmempty = not m.is_js_luci
o.nocreate = true
o.allowany = true
o.default = "wan"
@@ -41,7 +41,7 @@ o.validate = function(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.cfgvalue = function(self, section, value)
o.cfgvalue = function(self, section)
local config_str = m:get(section, "config_str")
if config_str then
return api.base64Decode(config_str)
@@ -79,7 +79,7 @@ o.default = "0"
o:depends({ custom = false })
o = s:option(Value, "firewall_allow_src", translate("Source zone"))
o.rmempty = false
o.rmempty = not m.is_js_luci
o.nocreate = true
o.allowany = true
o.default = "wan"
@@ -54,7 +54,7 @@ o.validate = function(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.cfgvalue = function(self, section, value)
o.cfgvalue = function(self, section)
local config_str = m:get(section, "config_str")
if config_str then
return api.base64Decode(config_str)
@@ -110,7 +110,7 @@ o.default = "0"
o:depends({ custom = false })
o = s:option(Value, "firewall_allow_src", translate("Source zone"))
o.rmempty = false
o.rmempty = not m.is_js_luci
o.nocreate = true
o.allowany = true
o.default = "wan"
+23 -1
View File
@@ -1565,7 +1565,13 @@ function set_type_cbi(s)
obj.option_prefix = s_self.option_prefix
obj.option = s_self.option_prefix .. option
obj.cfgvalue = function(self, section)
return self.map:get(section, self.config_option)
local v = self.map:get(section, self.config_option)
if util.instanceof(self, cbi.MultiValue) then
if v and self.cast == "table" then
return table.concat(v, " ")
end
end
return v
end
obj.write = function(self, section, value)
if s1.fields["type"]:formvalue(s_self.section) == s_self.type_name then
@@ -1583,6 +1589,21 @@ function set_type_cbi(s)
new_val = new_t
end
end
if util.instanceof(self, cbi.MultiValue) then
local new_t = {}
if type(value) == "table" then
new_t = table_remove_duplicates(value)
else
string.gsub(value, '[^' .. " " .. ']+', function(v)
new_t[#new_t + 1] = v
end)
end
if self.cast == "string" then
new_val = table.concat(new_t, " ")
else
new_val = new_t
end
end
self.map:set(section, self.config_option, new_val)
end
end
@@ -1630,6 +1651,7 @@ end
function type_cbi_section(s, s2)
for i, v in ipairs(s2.children) do
local o = s2.children[i]
o.section = s
s:append(o)
s.fields[o.option] = o
end
@@ -253,8 +253,8 @@ function gen_outbound(flag, node, tag, proxy_table)
--max_version = "1.3",
fragment = fragment,
record_fragment = record_fragment,
certificate = (node.tls_certificate == "1" and node.tls_certificate_pem ~= "") and split(node.tls_certificate_pem, "\n") or nil,
cipher_suites = (node.cipherSuites and #node.cipherSuites > 0) and node.cipherSuites or nil,
certificate = (node.tls_certificate == "1" and node.tls_certificate_pem ~= "") and api.split(node.tls_certificate_pem:gsub("\\n", "\n"), "\n") or nil,
cipher_suites = (node.cipherSuites and node.cipherSuites ~= "") and api.split(node.cipherSuites, ":") or nil,
ech = (node.ech == "1") and (function()
local function get_ech_domain(s) --兼容xray "域名+DNS" 格式ech
local domain, dns = s:match("^([^+]+)%+(.+)$")
@@ -278,7 +278,7 @@ function gen_outbound(flag, node, tag, proxy_table)
ech.query_server_name = qname
ech_domain[qname] = true
elseif config then
ech.config = { config }
ech.config = api.split(config:gsub("\\n", "\n"), "\n")
elseif node.tls_serverName and node.tls_serverName ~= "" then
ech_domain[node.tls_serverName] = true
end
@@ -714,8 +714,10 @@ function gen_config_server(node)
local tls = {
enabled = true,
certificate_path = node.tls_certificateFile,
key_path = node.tls_keyFile,
certificate_path = (node.tls_use_pem ~= "1") and node.tls_certificateFile or nil,
key_path = (node.tls_use_pem ~= "1") and node.tls_keyFile or nil,
certificate = (node.tls_use_pem == "1" and node.tls_certificate) and api.split(node.tls_certificate:gsub("\\n", "\n"), "\n") or nil,
key = (node.tls_use_pem == "1" and node.tls_key) and api.split(node.tls_key:gsub("\\n", "\n"), "\n") or nil,
alpn = (node.alpn and node.alpn ~= "default") and (function()
local alpn = {}
string.gsub(node.alpn, '[^,]+', function(w)
@@ -729,6 +731,8 @@ function gen_config_server(node)
if node.tls == "1" and node.reality == "1" then
tls.certificate_path = nil
tls.key_path = nil
tls.certificate = nil
tls.key = nil
tls.server_name = node.reality_handshake_server
tls.reality = {
enabled = true,
@@ -746,7 +750,7 @@ function gen_config_server(node)
if node.tls == "1" and node.ech == "1" then
tls.ech = {
enabled = true,
key = node.ech_key and { node.ech_key } or nil
key = node.ech_key and api.split(node.ech_key:gsub("\\n", "\n"), "\n") or nil
}
end
@@ -190,7 +190,7 @@ function gen_outbound(flag, node, tag, proxy_table)
verifyPeerCertByName = node.tls_CertByName or "",
echConfigList = (node.ech == "1") and node.ech_config or nil,
certificates = (node.tls_certificate == "1" and node.tls_certificate_pem ~= "") and {
certificate = api.split(node.tls_certificate_pem, "\n"),
certificate = api.split(node.tls_certificate_pem:gsub("\\n", "\n"), "\n"),
usage = "verify"
} or nil,
cipherSuites = (node.cipherSuites and #node.cipherSuites > 0) and table.concat(node.cipherSuites, ":") or nil,
@@ -226,8 +226,8 @@ function gen_outbound(flag, node, tag, proxy_table)
tti = 50,
uplinkCapacity = 12,
downlinkCapacity = 100,
CwndMultiplier = 1,
MaxSendingWindow = 2 * 1024 * 1024
cwndMultiplier = 1,
maxSendingWindow = 2 * 1024 * 1024
} or nil,
wsSettings = (node.transport == "ws") and {
path = node.ws_path or "/",
@@ -702,8 +702,10 @@ function gen_config_server(node)
disableSystemRoot = false,
certificates = {
{
certificateFile = node.tls_certificateFile,
keyFile = node.tls_keyFile
certificateFile = (node.tls_use_pem ~= "1") and node.tls_certificateFile or nil,
keyFile = (node.tls_use_pem ~= "1") and node.tls_keyFile or nil,
certificate = (node.tls_use_pem == "1" and node.tls_certificate) and api.split(node.tls_certificate:gsub("\\n", "\n"), "\n") or nil,
key = (node.tls_use_pem == "1" and node.tls_key) and api.split(node.tls_key:gsub("\\n", "\n"), "\n") or nil
}
},
echServerKeys = (node.ech == "1") and node.ech_key or nil
@@ -730,8 +732,8 @@ function gen_config_server(node)
tti = 50,
uplinkCapacity = 12,
downlinkCapacity = 100,
CwndMultiplier = 1,
MaxSendingWindow = 2 * 1024 * 1024
cwndMultiplier = 1,
maxSendingWindow = 2 * 1024 * 1024
} or nil,
wsSettings = (node.transport == "ws") and {
host = node.ws_host or nil,
@@ -688,12 +688,40 @@ local current_node = map:get(section)
params += opt.query("congestion_control", dom_prefix + "naive_congestion_control");
}
params += "#" + encodeURI(v_alias.value);
if (params[0] == "&") {
params = params.substring(1);
}
url += params;
} else if (v_type === "sing-box" && opt.get(dom_prefix + "protocol").value === "snell") {
protocol = "snell";
var v_userkey = opt.get(dom_prefix + "password");
var v_psk = opt.get(dom_prefix + "snell_psk");
var v_port = opt.get(dom_prefix + "port");
url = encodeURIComponent(v_psk.value) +
"@" + _address +
":" + v_port.value + "?";
var params = "psk=" + encodeURIComponent(v_psk.value);
if (v_userkey.value !== "") {
params += "&userkey=" + encodeURIComponent(v_userkey.value);
}
params += opt.query("version", dom_prefix + "snell_version");
if (opt.get(dom_prefix + "snell_version").value === '4') {
params += opt.query("obfs", dom_prefix + "snell_obfs_mode");
params += opt.query("obfs-host", dom_prefix + "snell_obfs_host");
} else {
params += opt.query("mode", dom_prefix + "snell_mode");
}
params += opt.query("reuse", dom_prefix + "snell_reuse");
params += "#" + encodeURI(v_alias.value);
if (params[0] == "&") {
params = params.substring(1);
}
url += params;
}
if (url) {
url = protocol.toLowerCase() + "://" + url;
return url;
@@ -1760,6 +1788,37 @@ local current_node = map:get(section)
opt.set('remarks', decodeURIComponent(hash.substr(1)));
}
}
if (link_type === "snell") {
if (has_singbox) {
dom_prefix = "singbox_";
opt.set('type', "sing-box");
}
opt.set(dom_prefix + 'protocol', "snell");
var m = parseNodeUrl(link);
opt.set(dom_prefix + 'address', unbracketIP(m.hostname));
opt.set(dom_prefix + 'port', m.port || "443");
var queryParam = {};
if (m.search.length > 1) {
var query = m.search.replace('/?', '?').split('?')
var queryParams = query[1];
var queryArray = queryParams.split('&');
var params;
for (i = 0; i < queryArray.length; i++) {
params = queryArray[i].split('=');
queryParam[decodeURIComponent(params[0]).toLowerCase()] = decodeURIComponent(params[1] || '');
}
}
opt.set(dom_prefix + 'password', queryParam.userkey || '');
opt.set(dom_prefix + 'snell_psk', queryParam.psk || m.passwd || '');
opt.set(dom_prefix + 'snell_version', queryParam.version || '4');
opt.set(dom_prefix + 'snell_reuse', queryParam.reuse === '1');
opt.set(dom_prefix + 'snell_obfs_mode', queryParam.obfs || 'none');
opt.set(dom_prefix + 'snell_obfs_host', queryParam['obfs-host'] || queryParam.obfs_host || '');
opt.set(dom_prefix + 'snell_mode', queryParam.mode || 'default');
if (m.hash) {
opt.set('remarks', decodeURIComponent(m.hash.substr(1)));
}
}
if (switch_type) {
sessionStorage.setItem("fromUrl", JSON.stringify({
+21 -6
View File
@@ -1453,17 +1453,26 @@ msgstr "پورت راه دور"
msgid "as:"
msgstr "به عنوان:"
msgid "Public key absolute path"
msgstr "مسیر مطلق کلید عمومی"
msgid "Path to the certificate file"
msgstr "مسیر فایل گواهی‌نامه"
msgid "Private key absolute path"
msgstr "مسیر مطلق کلید خصوصی"
msgid "Path to the private key file"
msgstr "مسیر فایل کلید خصوصی"
msgid "Can't find this file!"
msgstr "این فایل پیدا نشد!"
msgid "Public key and Private key path can not be empty!"
msgstr "مسیر کلید عمومی و کلید خصوصی نمی‌تواند خالی باشد!"
msgid "Certificate and Private key path can not be empty!"
msgstr "مسیر گواهی و کلید خصوصی نمی‌تواند خالی باشد!"
msgid "Certificate and Private key PEM can not be empty!"
msgstr "گواهی و کلید خصوصی PEM نمی‌توانند خالی باشند!"
msgid "Use PEM"
msgstr "از PEM استفاده کنید"
msgid "Use certificate and private key PEM content."
msgstr "از محتوای PEM گواهی و کلید خصوصی استفاده کنید."
msgid "Server-Side"
msgstr "سمت سرور"
@@ -1648,6 +1657,12 @@ msgstr "گواهی TLS (PEM)"
msgid "Full certificate (chain), PEM format."
msgstr "گواهی کامل (زنجیره‌ای)، فرمت PEM."
msgid "TLS Private Key (PEM)"
msgstr "کلید خصوصی TLS (PEM)"
msgid "Private key in PEM format."
msgstr "کلید خصوصی با فرمت PEM."
msgid "Fetch Manually"
msgstr "دریافت دستی"
+21 -6
View File
@@ -1454,17 +1454,26 @@ msgstr "Удалённый порт"
msgid "as:"
msgstr "например:"
msgid "Public key absolute path"
msgstr "Полный путь к публичному ключу"
msgid "Path to the certificate file"
msgstr "Путь к файлу сертификата"
msgid "Private key absolute path"
msgstr "Полный путь к приватному ключу"
msgid "Path to the private key file"
msgstr "Путь к файлу закрытого ключа"
msgid "Can't find this file!"
msgstr "Файл не найден!"
msgid "Public key and Private key path can not be empty!"
msgstr "Пути к публичному и приватному ключам не могут быть пустыми!"
msgid "Certificate and Private key path can not be empty!"
msgstr "Пути к сертификату и закрытому ключу не могут быть пустыми!"
msgid "Certificate and Private key PEM can not be empty!"
msgstr "Сертификат и закрытый ключ в формате PEM не могут быть пустыми!"
msgid "Use PEM"
msgstr "Используйте PEM"
msgid "Use certificate and private key PEM content."
msgstr "Используйте содержимое сертификата и закрытого ключа в формате PEM."
msgid "Server-Side"
msgstr "Серверная часть"
@@ -1649,6 +1658,12 @@ msgstr "Сертификат TLS (PEM)"
msgid "Full certificate (chain), PEM format."
msgstr "Полный сертификат (цепочка), формат PEM."
msgid "TLS Private Key (PEM)"
msgstr "Закрытый ключ TLS (PEM)"
msgid "Private key in PEM format."
msgstr "Закрытый ключ в формате PEM."
msgid "Fetch Manually"
msgstr "Получить вручную"
+22 -7
View File
@@ -1296,7 +1296,7 @@ msgid "Private Key"
msgstr "私钥"
msgid "Pre shared key"
msgstr "额外的对称加密密钥"
msgstr "预共享密钥"
msgid "Allowed IPs"
msgstr "允许的 IP"
@@ -1445,17 +1445,26 @@ msgstr "远程端口"
msgid "as:"
msgstr "如:"
msgid "Public key absolute path"
msgstr "公钥文件绝对路径"
msgid "Path to the certificate file"
msgstr "证书文件路径"
msgid "Private key absolute path"
msgstr "私钥文件绝对路径"
msgid "Path to the private key file"
msgstr "私钥文件路径"
msgid "Can't find this file!"
msgstr "找不到这个文件!"
msgid "Public key and Private key path can not be empty!"
msgstr "公钥和私钥文件路径不能为空!"
msgid "Certificate and Private key path can not be empty!"
msgstr "证书和私钥文件路径不能为空!"
msgid "Certificate and Private key PEM can not be empty!"
msgstr "证书和私钥 PEM 不能为空!"
msgid "Use PEM"
msgstr "使用 PEM"
msgid "Use certificate and private key PEM content."
msgstr "使用证书和私钥的 PEM 内容。"
msgid "Server-Side"
msgstr "服务器端"
@@ -1640,6 +1649,12 @@ msgstr "TLS 证书(PEM)"
msgid "Full certificate (chain), PEM format."
msgstr "完整证书(链),PEM 格式。"
msgid "TLS Private Key (PEM)"
msgstr "TLS 私钥(PEM)"
msgid "Private key in PEM format."
msgstr "PEM 格式的私钥。"
msgid "Fetch Manually"
msgstr "手动获取"
+22 -7
View File
@@ -1296,7 +1296,7 @@ msgid "Private Key"
msgstr "私鑰"
msgid "Pre shared key"
msgstr "额外的對称加密密鑰"
msgstr "預共享密鑰"
msgid "Allowed IPs"
msgstr "允許的 IP"
@@ -1445,17 +1445,26 @@ msgstr "遠程端口"
msgid "as:"
msgstr "如:"
msgid "Public key absolute path"
msgstr "公鑰文件绝對路径"
msgid "Path to the certificate file"
msgstr "證書檔案路徑"
msgid "Private key absolute path"
msgstr "私鑰文件绝對路径"
msgid "Path to the private key file"
msgstr "私鑰檔案路徑"
msgid "Can't find this file!"
msgstr "找不到這個文件!"
msgid "Public key and Private key path can not be empty!"
msgstr "公鑰和私鑰文件路径不能為空!"
msgid "Certificate and Private key path can not be empty!"
msgstr "憑證和私鑰路徑不能為空!"
msgid "Certificate and Private key PEM can not be empty!"
msgstr "憑證和私鑰 PEM 檔案不能為空!"
msgid "Use PEM"
msgstr "使用 PEM"
msgid "Use certificate and private key PEM content."
msgstr "使用憑證和私鑰 PEM 內容。"
msgid "Server-Side"
msgstr "服務器端"
@@ -1640,6 +1649,12 @@ msgstr "TLS 憑證(PEM)"
msgid "Full certificate (chain), PEM format."
msgstr "完整憑證(鏈),PEM 格式。"
msgid "TLS Private Key (PEM)"
msgstr "TLS 私鑰 (PEM)"
msgid "Private key in PEM format."
msgstr "PEM格式的私鑰。"
msgid "Fetch Manually"
msgstr "手動獲取"
@@ -760,7 +760,7 @@ local function parseClashNode(node, add_mode, group, sub_cfg)
result.type = 'sing-box'
result.protocol = "anytls"
else
log(2, i18n.translatef("Skip the %s node because the %s core program is not installed.", "AnyTLS", "AnyTLS", "Sing-Box 1.12"))
log(2, i18n.translatef("Skip the %s node because the %s core program is not installed.", "AnyTLS", "Sing-Box 1.12"))
return nil
end
result.password = node.password
@@ -783,6 +783,25 @@ local function parseClashNode(node, add_mode, group, sub_cfg)
if node["disable-reuse"] then
result.anytls_disable_reuse = "1"
end
elseif node.type == 'snell' then
if has_singbox then
result.type = 'sing-box'
result.protocol = "snell"
else
log(2, i18n.translatef("Skip the %s node because the %s core program is not installed.", "Snell", "Sing-Box 1.14"))
return nil
end
result.password = node.password
result.snell_psk = node.psk
result.snell_version = node.version or "4"
if result.snell_version == "4" then
local obfs_opts = node["obfs-opts"] or {}
result.snell_obfs_mode = obfs_opts.mode or "none"
result.snell_obfs_host = obfs_opts.host
else
result.snell_mode = "default"
end
result.snell_reuse = node.reuse and "1" or "0"
end
if not result.remarks or result.remarks == "" then
if result.address and result.port then
@@ -1665,7 +1684,7 @@ local function processData(szType, content, add_mode, group, sub_cfg)
result.type = 'sing-box'
result.protocol = "hysteria"
else
log(2, i18n.translatef("Skip the %s node because the %s core program is not installed.", "Hysteria", "Hysteria", "Sing-Box"))
log(2, i18n.translatef("Skip the %s node because the %s core program is not installed.", "Hysteria", "Sing-Box"))
return nil
end
@@ -1778,7 +1797,7 @@ local function processData(szType, content, add_mode, group, sub_cfg)
result.type = 'sing-box'
result.protocol = "tuic"
else
log(2, i18n.translatef("Skip the %s node because the %s core program is not installed.", "Tuic", "Tuic", "Sing-Box"))
log(2, i18n.translatef("Skip the %s node because the %s core program is not installed.", "Tuic", "Sing-Box"))
return nil
end
@@ -1836,7 +1855,7 @@ local function processData(szType, content, add_mode, group, sub_cfg)
result.type = 'sing-box'
result.protocol = "anytls"
else
log(2, i18n.translatef("Skip the %s node because the %s core program is not installed.", "AnyTLS", "AnyTLS", "Sing-Box 1.12"))
log(2, i18n.translatef("Skip the %s node because the %s core program is not installed.", "AnyTLS", "Sing-Box 1.12"))
return nil
end
@@ -1962,6 +1981,62 @@ local function processData(szType, content, add_mode, group, sub_cfg)
result.naive_quic = "1"
result.naive_congestion_control = params.congestion_control or "bbr"
end
elseif szType == "snell" then
if has_singbox then
result.type = 'sing-box'
result.protocol = "snell"
else
log(2, i18n.translatef("Skip the %s node because the %s core program is not installed.", "Snell", "Sing-Box 1.14"))
return nil
end
local alias = ""
if content:find("#") then
local idx_sp = content:find("#")
alias = content:sub(idx_sp + 1, -1)
content = content:sub(0, idx_sp - 1)
end
result.remarks = UrlDecode(alias)
local Info = content
if content:find("@") then
local info = split(content, "@")
result.snell_psk = UrlDecode(info[1])
Info = info[2]
end
Info = (Info or ""):gsub("/%?", "?")
local query = split(Info, "%?")
local host_port = query[1]
local params = {}
for _, v in pairs(split(query[2], '&')) do
local s = v:find("=", 1, true)
if s and s > 1 then
params[UrlDecode(v:sub(1, s - 1)):lower()] = UrlDecode(v:sub(s + 1))
end
end
-- [2001:4860:4860::8888]:443
-- 8.8.8.8:443
result.port = "443"
if host_port:find(":") then
local sp = split(host_port, ":")
result.port = sp[#sp]
if api.is_ipv6addrport(host_port) then
result.address = api.get_ipv6_only(host_port)
else
result.address = sp[1]
end
else
result.address = host_port
end
result.snell_psk = params.psk or result.snell_psk
result.password = params.userkey
result.snell_version = params.version or "4"
if result.snell_version == "4" then
result.snell_obfs_mode = params.obfs or "none"
result.snell_obfs_host = params['obfs-host'] or params.obfs_host
else
result.snell_mode = params.mode or "default"
end
result.snell_reuse = (params.reuse == "1") and "1" or "0"
else
log(2, i18n.translatef("%s type node subscriptions are not currently supported, skip this node.", szType))
return nil
@@ -45,6 +45,7 @@ return view.extend({
o.rmempty = false;
o.value('dhcp', _('DHCP client'));
o.value('pppoe', _('PPPoE'));
o.value('siderouter', _('Siderouter'));
o = s.taboption('netsetup', form.Value, 'wan_pppoe_user', _('PAP/CHAP username'));
o.depends('wan_proto', 'pppoe');
@@ -60,19 +61,15 @@ return view.extend({
o.datatype = 'ip4addr';
o.cast = 'string';
o = s.taboption('netsetup', form.Flag, 'siderouter', _('Siderouter'),
_('Use as downstream router i.e. it will work like a switch'));
o.rmempty = false;
o = s.taboption('netsetup', form.Value, 'lan_gateway', _('IPv4 gateway'));
o.depends('siderouter', '1');
o.depends('wan_proto', 'siderouter');
o.datatype = 'ip4addr';
o.placeholder = _('Enter the main router IP');
o.rmempty = false;
o = s.taboption('netsetup', form.Flag, 'dhcp', _('DHCP Server'),
_('To turn on this DHCP, you need to turn off the DHCP of the main router, and to turn off this DHCP, you need to manually change the gateway and DNS of all Internet devices to the IP of this bypass router'));
o.depends('siderouter', '1');
o.depends('wan_proto', 'siderouter');
o.default = o.enabled;
o = s.taboption('netsetup', form.Flag, 'ipv6', _('Enable IPv6'), _('Enable/Disable IPv6'));
+35 -34
View File
@@ -15,10 +15,26 @@ add_wizard() {
local device ipv6 old_ipv6 wifi_ssid wifi_key old_wifi_ssid old_wifi_key
config_get wan_proto "$cfg" wan_proto
config_get lan_gateway "$cfg" lan_gateway
config_get dhcp "$cfg" dhcp
config_get lan_dns "$cfg" lan_dns
config_get ipv6 "$cfg" ipv6
config_get old_ipv6 "$cfg" old_ipv6
config_get lan_ipaddr "$cfg" lan_ipaddr
config_get autoupgrade_pkg "$cfg" autoupgrade_pkg
device=$(uci get network.wan.device 2>/dev/null)
test -n "${lan_ipaddr}" && {
uci set network.lan.ipaddr="${lan_ipaddr}"
}
device=$(uci -q get network.wan.device || uci -q get wizard.default.old_network_device)
case "${wan_proto}" in
dhcp)
uci -q del_list network.@device[0].ports="$device"
uci -q del network.lan.gateway
uci -q del dhcp.lan.ignore
uci -q del network.lan.dns
uci -q del firewall.@zone[0].masq
uci delete network.wan
uci set network.wan=interface
uci set network.wan.device="$device"
@@ -28,6 +44,11 @@ add_wizard() {
config_get wan_pppoe_user "$cfg" wan_pppoe_user
config_get wan_pppoe_pass "$cfg" wan_pppoe_pass
[[ "${wan_pppoe_user} != $(uci -q get network.wan.username)" || "${wan_pppoe_pass} != $(uci -q get network.wan.password)" ]] && {
uci -q del_list network.@device[0].ports="$device"
uci -q del network.lan.gateway
uci -q del dhcp.lan.ignore
uci -q del network.lan.dns
uci -q del firewall.@zone[0].masq
uci delete network.wan
uci set network.wan=interface
uci set network.wan.proto='pppoe'
@@ -40,40 +61,20 @@ add_wizard() {
}
}
;;
siderouter)
[ "$dhcp" == "0" ] && dhcp="1"
if [[ "${lan_gateway}" != "$(uci -q get network.lan.gateway)" || "${dhcp}" != "$(uci -q get dhcp.lan.ignore)" ]]; then
uci -q set network.lan.gateway="${lan_gateway}"
[ -n "$lan_dns" ] || lan_dns='223.5.5.5'
[ -n "$dhcp" ] && uci -q set dhcp.lan.ignore="1" || uci -q del dhcp.lan.ignore
uci -q set firewall.@zone[0].masq='1'
WAN=$(uci -q get network.wan.device)
uci -q add_list network.@device[0].ports="$WAN"
uci -q add_list network.@device[0].ports="$WAN"
uci -q set wizard.default.old_network_device="$WAN"
;;
esac
config_get lan_ipaddr "$cfg" lan_ipaddr
test -n "${lan_ipaddr}" && {
uci set network.lan.ipaddr="${lan_ipaddr}"
}
config_get siderouter "$cfg" siderouter
config_get old_siderouter "$cfg" old_siderouter
config_get lan_gateway "$cfg" lan_gateway
config_get dhcp "$cfg" dhcp
config_get lan_dns "$cfg" lan_dns
config_get ipv6 "$cfg" ipv6
config_get old_ipv6 "$cfg" old_ipv6
config_get autoupgrade_pkg "$cfg" autoupgrade_pkg
[ "$dhcp" == "0" ] && dhcp="1"
if [[ "${lan_gateway}" != "$(uci -q get network.lan.gateway)" || "${dhcp}" != "$(uci -q get dhcp.lan.ignore)" ]]; then
if [ "${siderouter}" == "1" ]; then
uci -q set network.lan.gateway="${lan_gateway}"
[ -n "$lan_dns" ] || lan_dns='223.5.5.5'
[ -n "$dhcp" ] && uci -q set dhcp.lan.ignore="1" || uci -q del dhcp.lan.ignore
uci -q set firewall.@zone[0].masq='1'
uci -q set network.wan.proto='none'
elif [ "${siderouter}" == "0" ]; then
uci -q del network.lan.gateway
uci -q del dhcp.lan.ignore
uci -q del network.lan.dns
uci -q del firewall.@zone[0].masq
uci -q set network.wan.proto='dhcp'
fi
fi
[ -n "$lan_dns" ] && {
[ "$(uci -q get dhcp.@dnsmasq[0].noresolv)" == 1 ] && {
uci -q del dhcp.@dnsmasq[0].noresolv
@@ -214,7 +215,7 @@ process_shortcut() {
local shortcut to_url comments lanaddr
local cfg="$1"
lanaddr="$(uci get network.lan.ipaddr)"
[ -n $lanaddr ] && lanaddr="$(echo "$lanaddr" | sed -e "s/\/2\+//")"
[ -n $lanaddr ] && lanaddr="$(echo "$lanaddr"| cut -d/ -f1)"
config_get shortcut "$cfg" shortcut
config_get to_url "$cfg" to_url
+2 -2
View File
@@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=Aurora Theme (A modern browser theme built with Vite and Tailwind CSS)
LUCI_DEPENDS:=+luci-base
PKG_VERSION:=1.3.9
PKG_RELEASE:=83
PKG_VERSION:=1.4.0
PKG_RELEASE:=84
PKG_LICENSE:=Apache-2.0
LUCI_MINIFY_CSS:=
+2 -2
View File
@@ -80,11 +80,11 @@ OpenWrt 25.12+ and snapshots use `apk`; other versions use `opkg`:
cd /tmp
# opkg
uclient-fetch -O luci-theme-aurora.ipk https://github.com/eamonxg/luci-theme-aurora/releases/latest/download/luci-theme-aurora_1.3.0-r20260830_all.ipk
uclient-fetch -O luci-theme-aurora.ipk https://github.com/eamonxg/luci-theme-aurora/releases/latest/download/luci-theme-aurora_1.4.0-r20260920_all.ipk
opkg install luci-theme-aurora.ipk
# apk
uclient-fetch -O luci-theme-aurora.apk https://github.com/eamonxg/luci-theme-aurora/releases/latest/download/luci-theme-aurora-1.3.0-r20260830.apk
uclient-fetch -O luci-theme-aurora.apk https://github.com/eamonxg/luci-theme-aurora/releases/latest/download/luci-theme-aurora-1.4.0-r20260920.apk
apk add --allow-untrusted luci-theme-aurora.apk
```
+2 -2
View File
@@ -80,11 +80,11 @@ OpenWrt 25.12+ 和 Snapshot 版本使用 `apk`;其他版本使用 `opkg`:
cd /tmp
# opkg
uclient-fetch -O luci-theme-aurora.ipk https://github.com/eamonxg/luci-theme-aurora/releases/latest/download/luci-theme-aurora_1.3.0-r20260830_all.ipk
uclient-fetch -O luci-theme-aurora.ipk https://github.com/eamonxg/luci-theme-aurora/releases/latest/download/luci-theme-aurora_1.4.0-r20260920_all.ipk
opkg install luci-theme-aurora.ipk
# apk
uclient-fetch -O luci-theme-aurora.apk https://github.com/eamonxg/luci-theme-aurora/releases/latest/download/luci-theme-aurora-1.3.0-r20260830.apk
uclient-fetch -O luci-theme-aurora.apk https://github.com/eamonxg/luci-theme-aurora/releases/latest/download/luci-theme-aurora-1.4.0-r20260920.apk
apk add --allow-untrusted luci-theme-aurora.apk
```
+2 -2
View File
@@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=A modern sidebar LuCI theme for OpenWrt, built with shadcn/ui design language.
LUCI_DEPENDS:=+luci-base
PKG_VERSION:=0.5.11
PKG_RELEASE:=5
PKG_VERSION:=0.6.0
PKG_RELEASE:=6
PKG_LICENSE:=Apache-2.0
LUCI_MINIFY_CSS:=
+2 -2
View File
@@ -62,11 +62,11 @@ OpenWrt 25.12+ and snapshots use `apk`; older versions use `opkg`.
cd /tmp
# opkg
uclient-fetch -O luci-theme-shadcn.ipk https://github.com/eamonxg/luci-theme-shadcn/releases/latest/download/luci-theme-shadcn_0.5.0-r20260830_all.ipk
uclient-fetch -O luci-theme-shadcn.ipk https://github.com/eamonxg/luci-theme-shadcn/releases/latest/download/luci-theme-shadcn_0.6.0-r20260920_all.ipk
opkg install luci-theme-shadcn.ipk
# apk
uclient-fetch -O luci-theme-shadcn.apk https://github.com/eamonxg/luci-theme-shadcn/releases/latest/download/luci-theme-shadcn-0.5.0-r20260830.apk
uclient-fetch -O luci-theme-shadcn.apk https://github.com/eamonxg/luci-theme-shadcn/releases/latest/download/luci-theme-shadcn-0.6.0-r20260920.apk
apk add --allow-untrusted luci-theme-shadcn.apk
```
+2 -2
View File
@@ -62,11 +62,11 @@ OpenWrt 25.12+ 及 Snapshot 版本使用 `apk`;旧版本使用 `opkg`。
cd /tmp
# opkg
uclient-fetch -O luci-theme-shadcn.ipk https://github.com/eamonxg/luci-theme-shadcn/releases/latest/download/luci-theme-shadcn_0.5.0-r20260830_all.ipk
uclient-fetch -O luci-theme-shadcn.ipk https://github.com/eamonxg/luci-theme-shadcn/releases/latest/download/luci-theme-shadcn_0.6.0-r20260920_all.ipk
opkg install luci-theme-shadcn.ipk
# apk
uclient-fetch -O luci-theme-shadcn.apk https://github.com/eamonxg/luci-theme-shadcn/releases/latest/download/luci-theme-shadcn-0.5.0-r20260830.apk
uclient-fetch -O luci-theme-shadcn.apk https://github.com/eamonxg/luci-theme-shadcn/releases/latest/download/luci-theme-shadcn-0.6.0-r20260920.apk
apk add --allow-untrusted luci-theme-shadcn.apk
```