Compare commits

...
15 Commits
Author SHA1 Message Date
github-actions[bot] d733dbc21e 🍉 Sync 2026-09-21 06:35:14
Merge-upstream / merge (push) Canceled after 0s
2026-09-21 06:35:14 +08:00
kiddin9 74fb8aab69 Update dashboard.patch 2026-09-21 06:33:03 +08:00
github-actions[bot] bc48d0d1d3 🍉 Sync 2026-09-21 06:25:47 2026-09-21 06:25:47 +08:00
github-actions[bot] 9e39c8f144 🍕 Sync 2026-09-21 04:46:25 2026-09-21 04:46:25 +08:00
github-actions[bot] 9c6642b69b 🎉 Sync 2026-09-21 03:58:45 2026-09-21 03:58:45 +08:00
github-actions[bot] 2a3f0fa893 💋 Sync 2026-09-21 03:32:21 2026-09-21 03:32:21 +08:00
github-actions[bot] 6a028a2c76 ✨ Sync 2026-09-21 03:25:42 2026-09-21 03:25:42 +08:00
github-actions[bot] 74e8f32f02 🍉 Sync 2026-09-21 02:46:36 2026-09-21 02:46:36 +08:00
kiddin9 5c2e942e2f Merge pull request #8 from anupamme/fix-repo-op-packages-v-002-luci-app-dockerman-root-usr-share-rpcd-ucode-docker-rpc-uc
fix: sanitize shell/subprocess call in docker_rpc.uc
2026-09-21 01:42:10 +08:00
kiddin9 9894670f08 Create ssrserver-python.patch 2026-09-21 01:41:54 +08:00
kiddin9 91946c7de3 Update dockerman.patch 2026-09-21 01:40:58 +08:00
kiddin9 417e630d13 Merge pull request #6 from anupamme/fix-repo-op-packages-autoban-command-injection-v001
fix: use subprocess instead of os.system in autoban.py
2026-09-21 01:37:58 +08:00
github-actions[bot] 7172e6af38 ⛅ Sync 2026-09-21 00:41:36 2026-09-21 00:41:36 +08:00
anupamme 805e42256f fix: sanitize shell/subprocess call in docker_rpc.uc
The run_ttyd function constructs shell commands using template literals that directly interpolate user-supplied parameters (id, cmd, port, uid) without validation
2026-09-20 00:53:16 +00:00
anupamme cbc4bfdd98 fix: V-001 security vulnerability
Automated security fix generated by OrbisAI Security
2026-08-06 19:26:24 +00:00
22 changed files with 985 additions and 369 deletions

No files matched your search

+7 -1
View File
@@ -1,6 +1,12 @@
--- a/luci-mod-dashboard/root/usr/share/luci/menu.d/luci-mod-dashboard.json
+++ b/luci-mod-dashboard/root/usr/share/luci/menu.d/luci-mod-dashboard.json
@@ -7,7 +7,10 @@
@@ -1,13 +1,15 @@
{
"admin/dashboard": {
- "title": "Dashboard",
"order": 1,
"action": {
"type": "view",
"path": "dashboard/index"
},
"depends": {
+19
View File
@@ -451,3 +451,22 @@
}
return {
--- a/luci-app-dockerman/root/usr/share/rpcd/ucode/docker_rpc.uc
+++ b/luci-app-dockerman/root/usr/share/rpcd/ucode/docker_rpc.uc
@@ -316,6 +316,15 @@ function run_ttyd(request) {
return { error: 'Container ID is required' };
}
+ // Strictly validate all user-supplied values before they are interpolated
+ // into a shell command string to prevent command injection.
+ if (!match(id, /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/) ||
+ !match(`${port}`, /^[0-9]{1,5}$/) || int(port) < 1 || int(port) > 65535 ||
+ (uid && !match(`${uid}`, /^[a-zA-Z0-9_-]+$/)) ||
+ !match(cmd, /^[a-zA-Z0-9_\/.\- ]+$/)) {
+ return { error: 'Invalid parameter supplied' };
+ }
+
let ttyd_cmd = `ttyd -q -d 2 --once --writable -p ${port} docker`;
const sock_addr = ds.get_socket_dest();
@@ -0,0 +1,24 @@
--- a/luci-app-ssrserver-python/root/usr/share/ssr/utils/autoban.py
+++ b/luci-app-ssrserver-python/root/usr/share/ssr/utils/autoban.py
@@ -25,9 +25,12 @@
with_statement
import os
+import re
import sys
import argparse
+_VALID_IP_RE = re.compile(r'^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$')
+
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='See README')
parser.add_argument('-c', '--count', default=3, type=int,
@@ -45,7 +48,7 @@
sys.stdout.flush()
else:
ips[ip] += 1
- if ip not in banned and ips[ip] >= config.count:
+ if ip not in banned and ips[ip] >= config.count and _VALID_IP_RE.match(ip):
banned.add(ip)
cmd = 'iptables -A INPUT -s %s -j DROP' % ip
print(cmd, file=sys.stderr)
+3 -3
View File
@@ -10,12 +10,12 @@ AGENTFLOW_ARCH_x86_64:=amd64
AGENTFLOW_ARCH_aarch64:=arm64
AGENTFLOW_ARCH:=$(AGENTFLOW_ARCH_$(ARCH))
AGENTFLOW_HASH_x86_64:=95cb43ba4e2ce5f060e3609d050caf25170dc151ba882a3a626494b7750d5fbc
AGENTFLOW_HASH_aarch64:=93e92628b26087b87d8571400e83046197167e4795593e757d4dcf7dac5f8231
AGENTFLOW_HASH_x86_64:=478fdb17cfb2b1664e7bc01315186e71f228871c0d20c816d8b3b8fe789bdb4d
AGENTFLOW_HASH_aarch64:=257faeaad2445dd14f87c1e1d9ded869fd301c5df2c2c9af817e70303eab2512
PKG_NAME:=agentflow
PKG_VERSION:=0.3.3
PKG_RELEASE:=11
PKG_RELEASE:=12
AGENTFLOW_URL_FILE:=agentflow-linux-$(AGENTFLOW_ARCH)
AGENTFLOW_DOWNLOAD:=$(AGENTFLOW_URL_FILE)-$(AGENTFLOW_HASH_$(ARCH))
+3 -3
View File
@@ -7,9 +7,9 @@
include $(TOPDIR)/rules.mk
LUCI_TITLE:=AgentFlow
PKG_VERSION:=1.0.0
PKG_RELEASE:=5
LUCI_DEPENDS:=+agentflow +luci-compat
PKG_VERSION:=1.0.1
PKG_RELEASE:=6
LUCI_DEPENDS:=+agentflow +luci-compat +luci-lib-taskd
LUCI_MINIFY_CSS:=0
LUCI_MINIFY_JS:=0
@@ -17,10 +17,38 @@ function index()
return
end
local install = entry({"admin", "services", "agentflow", "agent_install"}, call("agentflow_agent_install"))
install.leaf = true
local page = entry({"admin", "services", "agentflow"}, cbi("agentflow"), _("AgentFlow"), 100)
page.dependent = true
end
local function write_json(obj)
http.prepare_content("application/json")
http.write_json(obj)
end
local function require_post_csrf()
local dispatcher = require "luci.dispatcher"
local context = dispatcher.context or {}
local expected = context.authtoken or context.token
if (http.getenv("REQUEST_METHOD") or "") ~= "POST" then
http.status(405, "Method Not Allowed")
write_json({ ok = false, error = "method not allowed" })
return false
end
if not context.authsession or not expected or http.formvalue("token") ~= expected then
http.status(403, "Forbidden")
write_json({ ok = false, error = "invalid csrf token" })
return false
end
return true
end
local function uhttpd_has_apps_proxy_prefix()
local uci = require "luci.model.uci".cursor()
local mappings = uci:get_list("uhttpd", "main", "proxy_prefix") or {}
@@ -119,8 +147,97 @@ function agentflow_status()
proxy_prefix_enabled = uhttpd_apps_proxy_available(),
linkeasefull_running = linkeasefull_running()
}
http.prepare_content("application/json")
http.write_json(status)
write_json(status)
end
function agentflow_agent_install()
local fs = require "nixio.fs"
local jsonc = require "luci.jsonc"
local sys = require "luci.sys"
local util = require "luci.util"
local task_id = "agentflow-agent-install"
local task_script = "/tmp/agentflow-agent-install.sh"
local installer_url = "https://fw.koolcenter.com/binary/geili/agentflow/releases/installapp/installapp-mise.sh"
local agents = {
codexcli = true,
["claude-code"] = true,
opencode = true,
kimi = true,
reasonix = true
}
if not require_post_csrf() then
return
end
local agent = http.formvalue("agent") or ""
if not agents[agent] then
write_json({ ok = false, error = "unsupported agent" })
return
end
if not fs.access("/etc/init.d/tasks") then
write_json({ ok = false, error = "taskd is not available" })
return
end
if not fs.access("/lib/functions/mise.sh") then
write_json({ ok = false, error = "mise environment helper is not available" })
return
end
local task_status = sys.exec("/etc/init.d/tasks task_status " .. task_id .. " 2>/dev/null")
local status = jsonc.parse(task_status) or {}
local running = type(status) == "table" and status.running
if running == true or running == 1 or running == "1" or running == "true" then
write_json({ ok = true, busy = true, task_id = task_id })
return
end
local install_script = table.concat({
"set -e",
"agent=" .. util.shellquote(agent),
"installer_url=" .. util.shellquote(installer_url),
'installer="/tmp/agentflow-installapp-mise.$$"',
'cleanup() { rm -f "$installer" "$0"; }',
"trap cleanup 0 HUP INT TERM",
'. /lib/functions/mise.sh',
'if ! istore_runtime_env; then echo "[agentflow] Failed to initialize the shared runtime environment" >&2; exit 1; fi',
"set -u",
"export MISE_YES=1",
'echo "[agentflow] Downloading installer: $installer_url"',
'if command -v wget >/dev/null 2>&1; then',
'\twget -O "$installer" "$installer_url"',
'elif command -v curl >/dev/null 2>&1; then',
'\tcurl -fL -o "$installer" "$installer_url"',
'elif command -v uclient-fetch >/dev/null 2>&1; then',
'\tuclient-fetch -O "$installer" "$installer_url"',
"else",
'\techo "[agentflow] No HTTPS download tool is available" >&2',
"\texit 1",
"fi",
'if [ ! -s "$installer" ]; then echo "[agentflow] Failed to download the agent installer" >&2; exit 1; fi',
'chmod 0700 "$installer"',
'echo "[agentflow] Running installapp-mise.sh for $agent in $HOME"',
'/bin/sh "$installer" "$agent"'
}, "\n")
if not fs.writefile(task_script, install_script .. "\n") then
write_json({ ok = false, error = "failed to create install task", task_id = task_id })
return
end
if sys.call("chmod 0600 " .. util.shellquote(task_script)) ~= 0 then
fs.unlink(task_script)
write_json({ ok = false, error = "failed to secure install task", task_id = task_id })
return
end
local command = "/bin/sh " .. util.shellquote(task_script)
local rc = sys.call("/etc/init.d/tasks task_add " .. task_id .. " " .. util.shellquote(command) .. " >/dev/null 2>&1")
if rc ~= 0 then
fs.unlink(task_script)
write_json({ ok = false, error = "failed to start install task", task_id = task_id })
return
end
write_json({ ok = true, task_id = task_id })
end
function agentflow_open()
@@ -1,11 +1,80 @@
<%+tasks/embed%>
<style type="text/css">
.agentflow-actions { display:flex; gap:10px; flex-wrap:wrap; margin:12px; }
.agentflow-actions .btn, .agentflow-agent-dialog-actions .btn { min-height:40px; }
.agentflow-agent-modal { position:fixed; inset:0; z-index:10000; display:none; align-items:center; justify-content:center; padding:20px; box-sizing:border-box; background:rgba(15,23,42,.45); }
.agentflow-agent-modal.open { display:flex; }
.agentflow-agent-dialog, body[theme="light"] .agentflow-agent-dialog { width:min(520px, 100%); padding:20px; box-sizing:border-box; border:1px solid #d0d7de; border-radius:12px; background:#fff; color:inherit; box-shadow:0 18px 48px rgba(15,23,42,.24); }
.agentflow-agent-dialog h3 { margin:0 0 8px; text-wrap:balance; }
.agentflow-agent-dialog > p, body[theme="light"] .agentflow-agent-dialog > p { margin:0 0 16px; color:#666; text-wrap:pretty; }
.agentflow-agent-field label { display:block; margin-bottom:6px; font-weight:600; }
.agentflow-agent-field select { width:100%; min-height:40px; box-sizing:border-box; }
.agentflow-agent-dialog-actions { display:flex; justify-content:flex-end; gap:10px; margin-top:18px; }
body[theme="dark"] .agentflow-agent-dialog { border-color:#465166; background:#1f2937; color:#e5e7eb; }
body[theme="dark"] .agentflow-agent-dialog > p { color:#aeb8c7; }
@media (prefers-color-scheme: dark) {
body:not([theme]) .agentflow-agent-dialog { border-color:#465166; background:#1f2937; color:#e5e7eb; }
body:not([theme]) .agentflow-agent-dialog > p { color:#aeb8c7; }
}
</style>
<script type="text/javascript">//<![CDATA[
var agentflowOpenUrl = '<%=url("admin/services/agentflow/open")%>';
var agentflowAgentInstallUrl = '<%=url("admin/services/agentflow/agent_install")%>';
var agentflowCsrfToken = (window.taskd && window.taskd.csrfToken) || '';
function agentflowAgentDialog(open) {
var modal = document.getElementById('agentflow-agent-modal');
var select = document.getElementById('agentflow-agent-select');
var trigger = document.getElementById('agentflow-agent-open');
if (!modal) return;
if (open) {
modal.classList.add('open');
if (select) select.focus();
} else {
modal.classList.remove('open');
if (trigger) trigger.focus();
}
}
function agentflowInstallAgent() {
var selected = document.getElementById('agentflow-agent-select');
var button = document.getElementById('agentflow-agent-install-now');
if (!selected || !button) return;
if (!window.taskd || !window.taskd.show_log) {
alert('<%:Task log window is not available.%>');
return;
}
button.disabled = true;
(new XHR()).post(agentflowAgentInstallUrl, {
agent: selected.value,
token: agentflowCsrfToken
}, function(x, data) {
button.disabled = false;
if (!data) {
try { data = JSON.parse(x.responseText || '{}'); }
catch (e) { data = {}; }
}
if (!data.ok) {
alert(data.error || '<%:Failed to start agent installation.%>');
return;
}
agentflowAgentDialog(false);
window.taskd.show_log(data.task_id || 'agentflow-agent-install');
});
}
XHR.poll(5, '<%=url("admin/services/agentflow_status")%>', null, function(x, st) {
var el = document.getElementById('agentflow_status');
var openButton = document.getElementById('agentflow-open');
if (st && el) {
if (!st.running) {
if (openButton) openButton.style.display = 'none';
el.innerHTML = '<br/><em style="color:red"><%:The AgentFlow service is not running.%></em>';
} else {
if (openButton) openButton.style.display = '';
var accessMode = '';
if (st.proxy_prefix_enabled && st.linkeasefull_running) {
accessMode = '<br/><em><%:Access mode: LinkEase Desktop /apps proxy.%></em>';
@@ -13,11 +82,14 @@ XHR.poll(5, '<%=url("admin/services/agentflow_status")%>', null, function(x, st)
accessMode = '<br/><em><%:Access mode: external port.%></em>';
}
el.innerHTML = '<br/><em style="color:green"><%:The AgentFlow service is running.%></em>'
+ accessMode
+ "<br/><br/><input class=\"btn cbi-button cbi-button-apply\" type=\"button\" value=\" <%:Click to open AgentFlow%> \" onclick=\"window.open('" + agentflowOpenUrl + "')\"/>";
+ accessMode;
}
}
});
document.addEventListener('keydown', function(ev) {
if (ev.key === 'Escape') agentflowAgentDialog(false);
});
//]]></script>
<fieldset class="cbi-section">
@@ -25,4 +97,29 @@ XHR.poll(5, '<%=url("admin/services/agentflow_status")%>', null, function(x, st)
<p id="agentflow_status">
<em><%:Collecting data...%></em>
</p>
<div class="agentflow-actions">
<input id="agentflow-open" class="btn cbi-button cbi-button-apply" type="button" value="<%:Click to open AgentFlow%>" onclick="window.open(agentflowOpenUrl)" style="display:none">
<input id="agentflow-agent-open" class="btn cbi-button cbi-button-apply" type="button" value="<%:Install agent%>" onclick="agentflowAgentDialog(true)">
</div>
</fieldset>
<div id="agentflow-agent-modal" class="agentflow-agent-modal" role="dialog" aria-modal="true" aria-labelledby="agentflow-agent-title" onclick="if (event.target === this) agentflowAgentDialog(false)">
<div class="agentflow-agent-dialog">
<h3 id="agentflow-agent-title"><%:Install agent%></h3>
<p><%:Select a coding agent to install into the shared AgentFlow runtime.%></p>
<div class="agentflow-agent-field">
<label for="agentflow-agent-select"><%:Agent%></label>
<select id="agentflow-agent-select" class="cbi-input-select">
<option value="codexcli">Codex</option>
<option value="claude-code">Claude Code</option>
<option value="opencode">OpenCode</option>
<option value="kimi">Kimi</option>
<option value="reasonix">Reasonix</option>
</select>
</div>
<div class="agentflow-agent-dialog-actions">
<input class="btn cbi-button" type="button" value="<%:Cancel%>" onclick="agentflowAgentDialog(false)">
<input id="agentflow-agent-install-now" class="btn cbi-button cbi-button-apply" type="button" value="<%:Install now%>" onclick="agentflowInstallAgent()">
</div>
</div>
</div>
+21
View File
@@ -51,3 +51,24 @@ msgstr "AgentFlow 服务未运行。"
msgid "The AgentFlow service is running."
msgstr "AgentFlow 服务运行中。"
msgid "Install agent"
msgstr "安装智能体"
msgid "Agent"
msgstr "智能体"
msgid "Select a coding agent to install into the shared AgentFlow runtime."
msgstr "选择要安装到 AgentFlow 共享运行环境中的编码智能体。"
msgid "Cancel"
msgstr "取消"
msgid "Install now"
msgstr "立即安装"
msgid "Task log window is not available."
msgstr "任务日志窗口不可用。"
msgid "Failed to start agent installation."
msgstr "启动智能体安装失败。"
@@ -0,0 +1,69 @@
from pathlib import Path
import unittest
APP_DIR = Path(__file__).resolve().parents[1]
class AgentInstallContractTest(unittest.TestCase):
def read(self, relative):
return (APP_DIR / relative).read_text(encoding="utf-8")
def test_luci_loads_task_window_and_starts_whitelisted_installer(self):
makefile = self.read("Makefile")
controller = self.read("luasrc/controller/agentflow.lua")
status = self.read("luasrc/view/agentflow/status.htm")
self.assertIn("+luci-lib-taskd", makefile)
self.assertIn('<%+tasks/embed%>', status)
self.assertIn('<select id="agentflow-agent-select"', status)
for agent in ("codexcli", "claude-code", "opencode", "kimi", "reasonix"):
self.assertIn(f'<option value="{agent}">', status)
self.assertNotIn('type="radio"', status)
self.assertIn('id="agentflow-open"', status)
self.assertIn('id="agentflow-agent-open"', status)
self.assertGreater(status.index('id="agentflow-agent-open"'), status.index('id="agentflow_status"'))
self.assertIn('margin:12px;', status)
self.assertIn("var selected = document.getElementById('agentflow-agent-select')", status)
self.assertIn("window.taskd.show_log(data.task_id", status)
self.assertNotIn('local token =', status)
self.assertIn("window.taskd.csrfToken", status)
self.assertIn("token: agentflowCsrfToken", status)
self.assertIn('http.formvalue("agent")', controller)
self.assertIn('codexcli = true', controller)
self.assertIn('["claude-code"] = true', controller)
self.assertIn('opencode = true', controller)
self.assertIn('kimi = true', controller)
self.assertIn('reasonix = true', controller)
self.assertIn('context.authtoken or context.token', controller)
self.assertIn('http.formvalue("token") ~= expected', controller)
self.assertIn('/etc/init.d/tasks task_add ', controller)
self.assertIn('task_id = "agentflow-agent-install"', controller)
def test_modal_theme_prefers_body_attribute_then_system_theme(self):
status = self.read("luasrc/view/agentflow/status.htm")
self.assertIn('body[theme="light"] .agentflow-agent-dialog', status)
self.assertIn('body[theme="dark"] .agentflow-agent-dialog', status)
self.assertIn('@media (prefers-color-scheme: dark)', status)
self.assertIn('body:not([theme]) .agentflow-agent-dialog', status)
def test_controller_runs_remote_installer_inside_taskd(self):
controller = self.read("luasrc/controller/agentflow.lua")
self.assertFalse((APP_DIR / "root/usr/libexec/istorec/agentflow-agent.sh").exists())
self.assertNotIn("/usr/libexec/istorec/agentflow-agent.sh", controller)
self.assertIn("installapp/installapp-mise.sh", controller)
self.assertIn("istore_runtime_env", controller)
self.assertIn('wget -O "$installer" "$installer_url"', controller)
self.assertIn('/bin/sh "$installer" "$agent"', controller)
self.assertIn('task_script = "/tmp/agentflow-agent-install.sh"', controller)
self.assertIn("fs.writefile(task_script", controller)
self.assertIn('rm -f "$installer" "$0"', controller)
self.assertIn('local command = "/bin/sh "', controller)
self.assertNotIn('local command = "/bin/sh -c "', controller)
self.assertLess(controller.index("istore_runtime_env"), controller.index('"set -u"'))
if __name__ == "__main__":
unittest.main()
@@ -316,6 +316,15 @@ function run_ttyd(request) {
return { error: 'Container ID is required' };
}
// Strictly validate all user-supplied values before they are interpolated
// into a shell command string to prevent command injection.
if (!match(id, /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/) ||
!match(`${port}`, /^[0-9]{1,5}$/) || int(port) < 1 || int(port) > 65535 ||
(uid && !match(`${uid}`, /^[a-zA-Z0-9_-]+$/)) ||
!match(cmd, /^[a-zA-Z0-9_\/.\- ]+$/)) {
return { error: 'Invalid parameter supplied' };
}
let ttyd_cmd = `ttyd -q -d 2 --once --writable -p ${port} docker`;
const sock_addr = ds.get_socket_dest();
+2 -2
View File
@@ -7,12 +7,12 @@
include $(TOPDIR)/rules.mk
LUCI_TITLE:=Firewall Live View
LUCI_DESCRIPTION:=Live firewall log table (nftables/fw4 and iptables LOG) with filters
LUCI_DESCRIPTION:=Live firewall log table (firewall4/nft; iptables-tagged lines still classify) with filters
LUCI_DEPENDS:=+luci-base +logd +jsonfilter
LUCI_PKGARCH:=all
PKG_VERSION:=0.1.44
PKG_RELEASE:=27
PKG_RELEASE:=28
# Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI).
ifdef SOURCE_DATE_EPOCH
@@ -599,9 +599,10 @@ collect_logging_warnings() {
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.
# iptables table can still exist in the namespace visible to rpcd. LuCI
# surfaces this warning in the backend label; it must not affect readiness
# or the enable gate. A registered table can come from a loaded module even
# when it has no rules, so this is inventory evidence, not proof of traffic.
legacy_iptables_active && logging_warnings_append 'legacy_iptables_detected'
# Report via LOGGING_WARNINGS, not exit status.
+1 -1
View File
@@ -12,7 +12,7 @@ LUCI_PKGARCH:=all
PKG_NAME:=luci-app-ssrserver-python
PKG_VERSION:=3.2.0
PKG_RELEASE:=1
PKG_RELEASE:=3
include $(TOPDIR)/feeds/luci/luci.mk
@@ -25,9 +25,12 @@ from __future__ import absolute_import, division, print_function, \
with_statement
import os
import re
import sys
import argparse
_VALID_IP_RE = re.compile(r'^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$')
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='See README')
parser.add_argument('-c', '--count', default=3, type=int,
@@ -45,7 +48,7 @@ if __name__ == '__main__':
sys.stdout.flush()
else:
ips[ip] += 1
if ip not in banned and ips[ip] >= config.count:
if ip not in banned and ips[ip] >= config.count and _VALID_IP_RE.match(ip):
banned.add(ip)
cmd = 'iptables -A INPUT -s %s -j DROP' % ip
print(cmd, file=sys.stderr)
@@ -71,6 +71,7 @@ function getRadioBand(devName) {
return view.extend({
initialValues: {},
initialShortcuts: [],
originalLanIp: null, // 持久快照,防止被 handleSave 覆盖
hasWireless: false,
hasNginx: false,
map: null,
@@ -96,7 +97,17 @@ return view.extend({
// 2. 统一聚合读取系统当前各子模块的实时配置
getSystemConfig: function() {
var isSideRouter = !!uci.get('network', 'lan', 'gateway') && uci.get('network', 'wan', 'auto') === '0';
var lanGw = getFirstIp(uci.get('network', 'lan', 'gateway'), '');
var wanSec = uci.get('network', 'wan');
// 判定 WAN 接口是否不存在或处于停用状态
var isWanDisabled = !wanSec ||
uci.get('network', 'wan', 'auto') === '0' ||
uci.get('network', 'wan', 'disabled') === '1' ||
uci.get('network', 'wan', 'proto') === 'none';
// 核心判定:配置了局域网网关,且 WAN 口被停用或不存在,即确认处于旁路由模式
var isSideRouter = !!lanGw && isWanDisabled;
var ap = null;
if (this.hasWireless) {
@@ -118,7 +129,7 @@ return view.extend({
wan_pppoe_user: uci.get('network', 'wan', 'username') || '',
wan_pppoe_pass: uci.get('network', 'wan', 'password') || '',
lan_ipaddr: getFirstIp(uci.get('network', 'lan', 'ipaddr'), '10.0.0.1'),
lan_gateway: getFirstIp(uci.get('network', 'lan', 'gateway'), ''),
lan_gateway: lanGw,
lan_dns: toArray(uci.get('network', 'lan', 'dns')),
dhcp: uci.get('dhcp', 'lan', 'ignore') === '1' ? '0' : '1',
ipv6: uci.get('network', 'wan6', 'auto') === '0' ? '0' : '1',
@@ -138,6 +149,7 @@ return view.extend({
var sys = this.getSystemConfig();
this.initialValues = Object.assign({}, sys);
this.originalLanIp = sys.lan_ipaddr; // 记录不可变初始 IP
// 缓存初始 shortcuts 配置快照,用于保存时比对差异
var shortcuts = uci.sections('wizard', 'shortcuts') || [];
@@ -403,16 +415,34 @@ return view.extend({
}
}
// D. 自定义 LAN DNS
if (has(changed, 'lan_dns')) {
if (cur.lan_dns && cur.lan_dns.length > 0) {
uci.set('network', 'lan', 'dns', cur.lan_dns);
// D. 自定义 LAN DNS(支持旁路由模式自动回落至网关或公网兜底 DNS)
if (has(changed, 'lan_dns') || has(changed, 'wan_proto') || has(changed, 'lan_gateway')) {
if (cur.wan_proto === 'siderouter') {
var effectiveDns = toArray(cur.lan_dns);
// 若 DNS 列表为空,按优先级进行智能兜底
if (effectiveDns.length === 0) {
var cleanGw = getFirstIp(cur.lan_gateway, '');
if (cleanGw) {
// 优先级 1:回落至主路由网关 IP
effectiveDns.push(cleanGw);
} else {
// 优先级 2:若网关也未填写,使用常用可靠公网 DNS 兜底
effectiveDns.push('223.5.5.5', '119.29.29.29');
}
}
uci.set('network', 'lan', 'dns', effectiveDns);
} else {
safeUnset('network', 'lan', 'dns');
// 常规主路由模式:用户未配则清除,让系统通过 WAN 动态获取
if (cur.lan_dns && cur.lan_dns.length > 0) {
uci.set('network', 'lan', 'dns', cur.lan_dns);
} else {
safeUnset('network', 'lan', 'dns');
}
}
}
// E. LAN 网关(独立变更或切换至旁路由模式时更新)
// E. LAN 网关
if (has(changed, 'lan_gateway') || has(changed, 'wan_proto')) {
if (cur.wan_proto === 'siderouter') {
if (cur.lan_gateway) {
@@ -475,32 +505,59 @@ return view.extend({
});
},
// 5. 重写 View 级别的 handleSaveApply:应用生效并实现 LAN IP 变更平滑迁移与自动跳转
// 5. 重写 View 级别的 handleSaveApply:前置检测 LAN IP 变动,强制非回滚提交并精准弹出倒计时
handleSaveApply: function(ev, mode) {
var self = this;
// 关键修复:在 handleSave 覆盖 initialValues 之前,立即计算并锁定是否修改了 LAN IP
var oldIp = (self.originalLanIp || self.initialValues.lan_ipaddr || '').split('/')[0].trim();
var newIp = (self.optMap['lan_ipaddr'] ? self.optMap['lan_ipaddr'].formvalue('default') : '') || '';
newIp = newIp.split('/')[0].trim();
var ipChanged = !!(newIp && oldIp && (newIp !== oldIp));
return this.handleSave(ev).then(function(hasChanges) {
if (!hasChanges) return false;
var oldIp = self.initialValues.lan_ipaddr;
var newIp = (self.optMap['lan_ipaddr'].formvalue('default') || '').split('/')[0].trim();
var ipChanged = newIp && (newIp !== oldIp);
if (ipChanged) {
var sec = 15;
var targetUrl = window.location.protocol + '//' + newIp + (window.location.pathname || '/cgi-bin/luci/');
var countSpan = E('strong', {}, String(sec));
return ui.changes.apply(mode == '0').then(function() {
if (ipChanged) {
var sec = 15;
ui.showModal(_('LAN IP Address Changed'), [
E('p', _('LAN IP changed to %s. Redirecting in %d seconds...').format(newIp, sec)),
E('div', { 'class': 'spinning', 'style': 'margin: 1em auto;' })
]);
var timer = window.setInterval(function() {
sec--;
if (sec <= 0) {
window.clearInterval(timer);
window.location.href = window.location.protocol + '//' + newIp + window.location.pathname;
}
}, 1000);
}
});
// 1. 核心操作:调用 apply_unchecked,进行强制永久提交,彻底绕过 90 秒回滚保护机制!
L.post(L.url('admin', 'uci', 'apply_unchecked')).catch(function() {});
// 2. 立即在前端呼出弹窗遮罩层
ui.showModal(_('LAN IP Address Changed'), [
E('p', {}, [_('LAN IP has been changed to '), E('strong', {}, newIp), '.']),
E('p', {}, [_('Applying changes without rollback. Redirecting to the new address in '), countSpan, _(' seconds...')]),
E('p', { 'class': 'alert-message notice', 'style': 'margin-top: 1em;' },
_('提示:更改网段后,若未能自动加载新页面,请尝试重新插拔网线或断开重连 Wi-Fi,以获取新网段的 IP 地址。')),
E('div', { 'class': 'spinning', 'style': 'margin: 1.5em auto;' }),
E('div', { 'class': 'right' }, [
E('button', {
'class': 'cbi-button cbi-button-action',
'click': function() {
window.location.href = targetUrl;
}
}, _('Redirect Now'))
])
]);
// 3. 启动倒计时并自动跳转
var timer = window.setInterval(function() {
sec--;
countSpan.textContent = String(sec);
if (sec <= 0) {
window.clearInterval(timer);
window.location.href = targetUrl;
}
}, 1000);
return true;
} else {
// 未改动 LAN IP 的常规操作,保留原生的回滚自愈保护
return ui.changes.apply(mode == '0');
}
});
},
+27 -3
View File
@@ -4,7 +4,25 @@ local uci = luci.model.uci.cursor()
local http = require "luci.http"
function index()
entry({"admin", "index"}, call("landing_page"), _("Home") , 1).dependent = false
entry({"admin", "index"}, call("landing_page"), _("Home") , 0).dependent = false
-- 2. 核心:直接改写 admin 根节点的调度目标
-- 这样当用户访问 /admin、登录后跳转或访问根目录时,无需经过 firstchild(),直接执行 landing_page()
local admin = node("admin")
if admin then
admin.target = call("landing_page")
end
end
local function check_wifi()
local has_wifi = false
pcall(function()
uci:foreach("wireless", "wifi-device", function(s)
has_wifi = true
return false -- 检测到任意 wifi-device 即终止遍历
end)
end)
return has_wifi
end
function landing_page()
@@ -22,7 +40,13 @@ function landing_page()
http.redirect(luci.dispatcher.build_url("admin","quickstart"));
end
else
http.redirect(luci.dispatcher.build_url("admin","status", "overview"))
end
if check_wifi() then
-- 具备无线功能,跳转至常用无线仪表盘界面
http.redirect(luci.dispatcher.build_url("admin", "dashboard"))
else
-- 无无线设备(纯有线环境),跳转至原生状态概览
http.redirect(luci.dispatcher.build_url("admin", "status", "overview"))
end
end
end
+82 -44
View File
@@ -1,92 +1,109 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8\n"
msgstr ""
"Project-Id-Version: luci-app-wizard\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-20 20:00+0800\n"
"PO-Revision-Date: 2026-09-20 20:00+0800\n"
"Last-Translator: Eric\n"
"Language-Team: Chinese (Simplified) <zh_Hans>\n"
"Language: zh_Hans\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
msgid "Home"
msgstr "首页"
msgid "Setup Wizard"
msgstr "设置向导"
msgstr "快速设置向导"
msgid "Quickly configure common network, wireless, and system settings."
msgstr "快速配置常用网络、无线与系统设置。"
msgstr "快速配置常用网络、无线与系统运行设置。"
msgid "Net Settings"
msgstr "网络设置"
msgid "Firmware Settings"
msgstr "固件与系统"
msgstr "固件设置"
msgid "Wireless Settings"
msgstr "无线设置"
msgid "Shortcuts"
msgstr "快捷方式"
msgstr "域名快捷跳转"
msgid "比如设置google.com的快捷方式为字母g,则在此路由器网络的任何浏览器中输入g/即可访问google.com"
msgstr "比如设置 google.com 的快捷方式为字母 g,则在此路由器网络的任何浏览器中输入 g/ 即可访问 google.com"
msgid "WAN Protocol / Mode"
msgstr "WAN 协议 / 工作模式"
msgid "DHCP Client (Default Router)"
msgstr "DHCP 客户端 (标准主路由模式)"
msgstr "DHCP 客户端(标准主路由模式)"
msgid "PPPoE Dial-up"
msgstr "PPPoE 拨号上网"
msgstr "PPPoE 拨号"
msgid "Side-Router Mode (Bypass Gateway)"
msgstr "旁路由模式 (网关旁路)"
msgstr "旁路由模式(网关旁路)"
msgid "PPPoE Username"
msgstr "宽带拨号账号"
msgstr "PPPoE 用户名"
msgid "PPPoE Password"
msgstr "宽带拨号密码"
msgstr "PPPoE 密码"
msgid "LAN IPv4 Address"
msgstr "LAN 口 IPv4 地址"
msgstr "LAN IPv4 地址"
msgid "Custom DNS Server(s)"
msgstr "自定义 DNS 服务器,留空默认运营商DNS"
msgstr "自定义 DNS 服务器"
msgid "Gateway Address"
msgstr "网关地址"
msgid "Primary router IP address used when operating in side-router mode."
msgstr "处于旁路由模式时的主路由 IP 地址。"
msgstr "在旁路由模式下工作时填写的上级主路由 IP 地址。"
msgid "Enable DHCP Server"
msgstr "启用 DHCP 服务"
msgstr "启用 DHCP 服务器"
msgid "Usually disabled in side-router mode to avoid IP address assignment conflicts with the main router."
msgstr "在旁路由模式下通常关闭此项,避免与主路由的 IP 分配冲突。"
msgid "Enable or disable LAN DHCP server (usually disabled in side-router mode to avoid IP address conflicts)."
msgstr "开启或关闭局域网 DHCP 服务(旁路由模式下通常建议关闭,避免局域网 IP 分配冲突)。"
msgid "IPv6 Support"
msgstr "IPv6 支持"
msgid "Enable or disable IPv6 router advertisements and DHCPv6."
msgstr "启用或关闭 IPv6 路由通告与 DHCPv6 服务。"
msgstr "开启或关闭 IPv6 路由通告 (RA) 与 DHCPv6 服务。"
msgid "Firmware Upgrade Notice"
msgstr "固件升级提示"
msgstr "固件更新检测提示"
msgid "Check and display notices for newer firmware versions."
msgstr "检查并显示新版本固件更新提示。"
msgstr "开机或访问时检测并提示可用的新版本固件。"
msgid "Run CoreMark on Boot"
msgstr "开机 CoreMark 跑分"
msgstr "开机自动运行 CoreMark 跑分"
msgid "Run CPU benchmark asynchronously upon router initialization."
msgstr "在路由器初始化启动时运行 CPU 基准测试。"
msgstr "路由器启动完成后在后台异步运行 CPU 性能基准测试。"
msgid "Persistent Cookie Session"
msgstr "持久 Cookie 登录"
msgstr "保持登录会话 (Persistent Cookie)"
msgid "Maintain persistent login sessions in the web browser."
msgstr "在浏览器中保持登录会话状态,避免频繁重复登录。"
msgstr "关闭浏览器后仍保持管理后台登录状态。"
msgid "Enforce HTTPS Access"
msgstr "强制 HTTPS 访问"
msgid "Automatically redirect HTTP requests to secure HTTPS."
msgstr "将所有 HTTP 请求自动重定向至安全加密的 HTTPS。"
msgstr "自动将普通 HTTP 请求重定向到加密的 HTTPS 连接。"
msgid "Landing Dashboard Mode"
msgstr "主页展示模式"
msgstr "管理首页展示模式"
msgid "Default"
msgstr "默认"
@@ -95,46 +112,67 @@ msgid "RouterDog"
msgstr "路由狗"
msgid "NAS"
msgstr "NAS 模式"
msgstr "NAS"
msgid "Next-NAS"
msgstr "Next-NAS 模式"
msgstr "Next-NAS"
msgid "Router"
msgstr "路由器模式"
msgstr "Router"
msgid "Wireless Network Name (SSID)"
msgstr "无线网络名称 (SSID)"
msgid "无线名称基准前缀,系统将自动识别并为 2.4G 追加 _2.4G、5G 追加 _5G、6G 追加 _6G。"
msgstr "无线名称基准前缀,系统将自动识别并为 2.4G 追加 _2.4G、5G 追加 _5G、6G 追加 _6G。"
msgid "Wireless Password (Key)"
msgstr "无线网络密码"
msgstr "无线密码"
msgid "Leave empty for open network or 8+ characters"
msgstr "留空为开放无密码网络,或输入 8 位以上密码"
msgstr "留空表示无密码开放网络,或输入至少 8 位密码"
msgid "URL Shortcuts"
msgstr "快捷方式列表"
msgid "Wireless key must be between 8 and 64 characters long"
msgstr "无线密码长度必须在 8 到 64 个字符之间"
msgid "Applying Changes"
msgstr "正在应用修改"
msgid "Shortcut"
msgstr "快捷短语"
msgid "Applying modified settings: %s..."
msgstr "正在应用已修改的设置项: %s..."
msgid "Shortcut phrase cannot be empty"
msgstr "快捷短语不能为空"
msgid "No settings were modified. Services remain uninterrupted."
msgstr "未检测到任何修改,无需保存或重启服务。"
msgid "Only alphanumeric characters, dashes and underscores are allowed"
msgstr "仅允许输入字母、数字、破折号 (-) 和下划线 (_)"
msgid "Settings successfully applied: %s"
msgstr "已成功应用并重载修改项: %s"
msgid "Target URL"
msgstr "目标网址 (URL)"
msgid "Failed to apply settings: %s"
msgstr "应用设置失败: %s"
msgid "Please enter a valid URL starting with http:// or https://"
msgstr "请输入以 http:// 或 https:// 开头的有效网址"
msgid "Comments"
msgstr "备注说明"
msgid "Optional"
msgstr "可选"
msgid "There are no changes to apply"
msgstr "没有需要应用的更改"
msgid "LAN IP Address Changed"
msgstr "LAN 口 IP 地址已变更"
msgid "LAN IP changed to %s. Redirecting to the new address in %d seconds..."
msgstr "LAN 口 IP 已变更为 %s。将在 %d 秒后自动跳转至新地址..."
msgid "LAN IP has been changed to "
msgstr "LAN 口 IP 地址已更改为 "
msgid "Applying changes without rollback. Redirecting to the new address in "
msgstr "正在永久应用更改(已禁用回滚保护)。将在 "
msgid " seconds..."
msgstr " 秒后自动跳转至新地址..."
msgid "提示:更改网段后,若未能自动加载新页面,请尝试重新插拔网线或断开重连 Wi-Fi,以获取新网段的 IP 地址。"
msgstr "提示:更改网段后,若未能自动加载新页面,请尝试重新插拔网线或断开重连 Wi-Fi,以获取新网段的 IP 地址。"
msgid "Redirect Now"
msgstr "立即跳转"
@@ -1,6 +1,5 @@
{
"admin/dashboard": {
"title": "Dashboard",
"order": 1,
"action": {
"type": "view",
+1 -1
View File
@@ -10,7 +10,7 @@ include $(INCLUDE_DIR)/kernel.mk
PKG_NAME:=my-default-settings
PKG_VERSION:=2
PKG_RELEASE:=11
PKG_RELEASE:=16
include $(INCLUDE_DIR)/package.mk
@@ -1,246 +1,149 @@
#!/bin/sh
sed -i "s/op.supes.top/dl.openwrt.ai/" /etc/opkg/distfeeds.conf
[ -x /bin/bash ] && grep -q "/bin/ash" /etc/passwd && {
sed -i 's|/bin/ash|/bin/bash|g' "/etc/passwd"
ln -sf /bin/bash /bin/sh
touch /root/.bash_history
ln -sf /root/.bash_history /tmp/.busybox_ash_history
}
# 1. 基础系统与组件链接修正
sed -i 's|op.supes.top|dl.openwrt.ai|g' /etc/opkg/distfeeds.conf 2>/dev/null
[ -x /bin/bash ] && sed -i 's|/bin/ash|/bin/bash|g' /etc/passwd 2>/dev/null && ln -sf /bin/bash /bin/sh 2>/dev/null
[ -x /usr/bin/wget ] && [ ! -x /usr/bin/wget-ssl ] && ln -sf /usr/bin/wget /usr/bin/wget-ssl
sed -i -e '/profile.d/d' /lib/upgrade/keep.d/base-files \
-e 's/git-.*-\(.*\)/git-\1/g' /usr/lib/lua/luci/version.lua 2>/dev/null
# 2. 版本与环境参数提取
version=$(uci -q get base_config.@status[0].version)
case "$version" in ''|*[!0-9]*) version=0 ;; esac
hostname=$(uci -q get system.@system[0].hostname)
[ -n "${hostname}" ] || hostname="Kwrt"
for ip in $(uci -q get network.lan.ipaddr); do
case "$ip" in *.*) lan_ip="${ip%%/*}"; break ;; esac
done
lan_ip="${lan_ip:-10.0.0.1}"
if [ "$(uci -q get dhcp.@dnsmasq[0].port)" != "53" ]; then
uci -q set dhcp.@dnsmasq[0].port='53'
uci commit dhcp
fi
[ -z "$(uci -q get system.@system[0].hostname)" ] && uci -q set system.@system[0].hostname='Kwrt'
[ "$(uci -q get dropbear.main._direct)" != "1" ] && uci -q delete dropbear.main.Interface
case "$(uci -q get uhttpd.main.index_page)" in *cgi-bin/luci*) ;; *) uci -q add_list uhttpd.main.index_page='cgi-bin/luci' ;; esac
sed -i '/profile.d/d' /lib/upgrade/keep.d/base-files
proc=$(grep -c '^processor' /proc/cpuinfo 2>/dev/null)
[ "${proc:-0}" -gt 0 ] 2>/dev/null || proc=3
sed -i "s/^threads =.*/threads = $proc/" /etc/uwsgi/vassals/luci-webui.ini 2>/dev/null
if ! echo "$(uci -q get uhttpd.main.index_page)" | grep -q "cgi-bin/luci"; then
uci -q add_list uhttpd.main.index_page='cgi-bin/luci'
uci commit uhttpd
fi
# 3. 内存分级计算
mem=0
[ -r /proc/meminfo ] && read -r _ mem _ < /proc/meminfo
case "$mem" in ''|*[!0-9]*) mem=0 ;; esac
if [ "$(uci -q get dropbear.main._direct)" != "1" ] && [ -n "$(uci -q get dropbear.main.Interface)" ]; then
uci del dropbear.main.Interface
uci commit dropbear
fi
c=1024; nf=16384
[ "$mem" -ge 65536 ] && { c=2048; nf=32768; }
[ "$mem" -ge 131072 ] && { c=4096; nf=65536; }
[ "$mem" -ge 262144 ] && { c=8192; nf=65536; }
[ "$mem" -ge 524288 ] && { c=10000; nf=131072; }
[ "$mem" -ge 1048576 ] && { c=10000; nf=262144; }
[ -x /usr/bin/wget ] && [ ! -x /usr/bin/wget-ssl ] && \
ln -s /usr/bin/wget /usr/bin/wget-ssl
# 4. 首次开机参数统一批量预设
if [ "$version" -lt 1 ]; then
uci -q batch <<-EOF
set luci.main.lang='auto'
set uhttpd.main.rfc1918_filter='0'
set uhttpd.main.redirect_https='0'
set uhttpd.main.script_timeout='120'
set upnpd.config.enabled='1'
set firewall.@defaults[0].flow_offloading='1'
set firewall.@defaults[0].flow_offloading_hw='1'
set fstab.@global[0].anon_mount='1'
set network.globals.packet_steering='1'
set nft-qos.default.limit_enable='0'
set system.@system[0].zonename='Asia/Shanghai'
set system.@system[0].timezone='CST-8'
set system.@system[0].ttylogin='1'
set system.@system[0].zram_size_mb='$((mem / 1024 / 3))'
del system.ntp.server
add_list system.ntp.server='ntp.aliyun.com'
add_list system.ntp.server='0.openwrt.pool.ntp.org'
set dhcp.@dnsmasq[0].port='53'
set dhcp.@dnsmasq[0].dns_redirect='1'
set dhcp.@dnsmasq[0].allservers='1'
del dhcp.@dnsmasq[0].noresolv
set dhcp.@dnsmasq[0].cachesize='$c'
set dhcp.@dnsmasq[0].dnsforwardmax='$c'
set dhcp.@dnsmasq[0].min_ttl='3600'
set dhcp.lan.force='1'
set dhcp._ta=domain
set dhcp._ta.name='time.android.com'
set dhcp._ta.ip='203.107.6.88'
set dhcp._ta.comments='time.android'
EOF
sed -i "s/git-.*-\(.*\)/git-\1/g" /usr/lib/lua/luci/version.lua
(echo root; echo root) | passwd root >/dev/null 2>&1
[ -f /usr/share/rpcd/acl.d/luci-theme-argon.json ] && uci -q set luci.main.mediaurlbase='/luci-static/argon'
grep -q 'log-facility' /etc/dnsmasq.conf 2>/dev/null || echo "log-facility=/dev/null" >> /etc/dnsmasq.conf
if [ -f /etc/uwsgi/vassals/luci-webui.ini ]; then
processor=$(grep -c 'processor' /proc/cpuinfo)
[ -n "$processor" ] && [ "$processor" -gt 0 ] || processor=3
sed -i "/^threads =/c\threads = $processor" /etc/uwsgi/vassals/luci-webui.ini
fi
if [ -f /etc/config/attendedsysupgrade ]; then
uci -q set attendedsysupgrade.server.url='https://openwrt.ai'
uci commit attendedsysupgrade
fi
if [ -z "$version" ] || [ "$version" -lt 1 ]; then
uci -q set luci.main.lang='auto'
if [ -f /usr/share/rpcd/acl.d/luci-theme-footstrap.json ]; then
uci -q set luci.main.mediaurlbase=/luci-static/footstrap
fi
uci commit luci
if uci -q get system.@system[0] >/dev/null; then
uci -q set system.@system[0].zonename='Asia/Shanghai'
uci -q set system.@system[0].timezone='CST-8'
uci -q set system.@system[0].conloglevel='4'
uci -q set system.@system[0].cronloglevel='8'
uci -q set system.@system[0].ttylogin='1'
uci -q del system.ntp.server
uci -q add_list system.ntp.server='ntp.aliyun.com'
uci -q add_list system.ntp.server='0.openwrt.pool.ntp.org'
uci commit system
fi
uci -q set uhttpd.main.rfc1918_filter=0
uci -q set uhttpd.main.redirect_https=0
uci -q set uhttpd.main.script_timeout='120'
uci commit uhttpd
uci -q set upnpd.config.enabled='1'
uci commit upnpd
(echo "root"; sleep 1; echo "root") | /bin/busybox passwd root >/dev/null 2>&1 &
uci -q set firewall.@defaults[0].flow_offloading='1'
uci -q set firewall.@defaults[0].flow_offloading_hw='1'
uci commit firewall
uci -q set upnpd.config.enabled='1'
uci commit upnpd
grep -q log-facility /etc/dnsmasq.conf || \
echo "log-facility=/dev/null" >> /etc/dnsmasq.conf
#kernel_version=$(uname -r)
#if [ -f "/lib/modules/$kernel_version/xt_FULLCONENAT.ko" ] || [ -f "/lib/modules/$kernel_version/nft_fullcone.ko" ]; then
# uci -q set firewall.@defaults[0].fullcone='1'
# uci commit firewall
#fi
uci -q set fstab.@global[0].anon_mount=1
uci commit fstab
uci -q set network.globals.packet_steering=1
uci -q set network.@device[0].promisc=1
uci commit network
uci -q set dhcp.@dnsmasq[0].dns_redirect='1'
uci -q set dhcp.@dnsmasq[0].allservers='1'
uci -q del dhcp.@dnsmasq[0].noresolv
uci -q set dhcp.@dnsmasq[0].cachesize='1500'
uci -q set dhcp.@dnsmasq[0].min_ttl='3600'
uci -q set dhcp.lan.force='1'
uci -q set dhcp.lan.ra='hybrid'
uci -q set dhcp.lan.ndp='hybrid'
uci -q set dhcp.lan.dhcpv6='hybrid'
uci commit dhcp
uci -q set nft-qos.default.limit_enable='0'
uci commit nft-qos
fi
# kB
memtotal=$(grep MemTotal /proc/meminfo | awk '{print $2}')
if [ "$memtotal" -ge 1048576 ]; then
# > 1024M
cachesize=10000
dnsforwardmax=10000
nf_conntrack_max=262144
elif [ "$memtotal" -ge 524288 ]; then
# <= 1024M
cachesize=10000
dnsforwardmax=10000
nf_conntrack_max=131072
elif [ "$memtotal" -ge 262144 ]; then
# <= 512M
cachesize=8192
dnsforwardmax=8192
nf_conntrack_max=65536
elif [ "$memtotal" -ge 131072 ]; then
# <= 256M
cachesize=4096
dnsforwardmax=4096
nf_conntrack_max=65536
elif [ "$memtotal" -ge 65536 ]; then
# <= 128M
cachesize=2048
dnsforwardmax=2048
nf_conntrack_max=32768
else
# < 64M
cachesize=1024
dnsforwardmax=1024
nf_conntrack_max=16384
fi
if [ -z "$version" ] || [ "$version" -lt 1 ]; then
uci -q get dhcp.@dnsmasq[0] || uci -q add dhcp dnsmasq
uci -q set dhcp.@dnsmasq[0].cachesize="$cachesize"
uci -q set dhcp.@dnsmasq[0].dnsforwardmax="$dnsforwardmax"
uci -q set dhcp.@dnsmasq[0].localservice='0'
uci -q set dhcp.@dnsmasq[0].localise_queries='1'
uci -q set dhcp.@dnsmasq[0].rebind_protection='0'
uci -q set dhcp.@dnsmasq[0].rebind_localhost='1'
uci commit dhcp
uci -q set system.@system[0].zram_size_mb="$((memtotal / 1024 / 3))"
uci commit system
# sysctl overwrite
SYSCTL_LOCAL=/etc/sysctl.d/50-local.conf
mkdir -p /etc/sysctl.d
> "$SYSCTL_LOCAL"
echo "net.nf_conntrack_max=$nf_conntrack_max" >> "$SYSCTL_LOCAL"
echo "net.core.rmem_max=$nf_conntrack_max" >> "$SYSCTL_LOCAL"
printf "net.nf_conntrack_max=%s\nnet.core.rmem_max=%s\n" "$nf" "$nf" > /etc/sysctl.d/50-local.conf
if [ -n "$(command -v nginx)" ]; then
uci add wizard shortcuts
uci set wizard.@shortcuts[-1].shortcut='pw'
uci set wizard.@shortcuts[-1].to_url='http://10.0.0.1/cgi-bin/luci/admin/services/passwall'
uci set wizard.@shortcuts[-1].comments='passwall'
uci commit wizard
uci -q set nginx._sc_pw=server
uci -q set nginx._sc_pw.server_name="pw"
uci -q add_list nginx._sc_pw.listen="80"
uci -q add_list nginx._sc_pw.listen="443"
uci -q set nginx._sc_pw.return="302 http://10.0.0.1/cgi-bin/luci/admin/services/passwall"
uci -q set dhcp._sc_pw=domain
uci -q set dhcp._sc_pw.name="pw"
uci -q set dhcp._sc_pw.ip="10.0.0.1"
uci -q set dhcp._sc_pw.comments="PassWall"
uci commit dhcp
fi
if [ -z "$(uci -q get dhcp._ta)" ]; then
uci -q set dhcp._ta=domain
uci -q set dhcp._ta.name="time.android.com"
uci -q set dhcp._ta.ip="203.107.6.88"
uci -q set dhcp._ta.comments="time.android"
uci commit dhcp
if command -v nginx >/dev/null 2>&1 && [ -z "$(uci -q get wizard.pw)" ]; then
uci -q batch <<-EOF
set wizard.pw=shortcuts
set wizard.pw.shortcut='pw'
set wizard.pw.to_url='http://${lan_ip}/cgi-bin/luci/admin/services/passwall'
set wizard.pw.comments='passwall'
set nginx._sc_pw=server
set nginx._sc_pw.server_name='pw'
add_list nginx._sc_pw.listen='80'
add_list nginx._sc_pw.listen='443'
set nginx._sc_pw.return='302 http://${lan_ip}/cgi-bin/luci/admin/services/passwall'
set dhcp._sc_pw=domain
set dhcp._sc_pw.name='pw'
set dhcp._sc_pw.ip='${lan_ip}'
set dhcp._sc_pw.comments='PassWall'
EOF
fi
version=1
fi
if [ -z "$(uci -q get dhcp.default_server)" ]; then
uci -q set dhcp.default_server=domain
uci -q set dhcp.default_server.name='kwrt'
uci -q set dhcp.default_server.ip='10.0.0.1'
uci -q set dhcp.default_server.comments='后台地址'
uci commit dhcp
# 5. 默认后台域名映射与 Nginx 规则
[ -z "$(uci -q get dhcp.default_server)" ] && uci -q batch <<-EOF
set dhcp.default_server=domain
set dhcp.default_server.name='kwrt'
set dhcp.default_server.ip='${lan_ip}'
set dhcp.default_server.comments='后台地址'
EOF
if command -v nginx >/dev/null 2>&1; then
[ -z "$(uci -q get nginx.default_server)" ] && uci -q batch <<-EOF
set nginx.default_server=server
set nginx.default_server.server_name='kwrt'
add_list nginx.default_server.listen='80 default_server'
add_list nginx.default_server.listen='[::]:80 default_server'
add_list nginx.default_server.include='conf.d/*.locations'
set nginx.default_server.access_log='off; # logd openwrt'
EOF
if [ -f /etc/nginx/conf.d/luci.locations ]; then
grep -q 'uwsgi_read_timeout' /etc/nginx/conf.d/luci.locations || sed -i '/include uwsgi_params;/a\\t\tuwsgi_read_timeout 300s;' /etc/nginx/conf.d/luci.locations
grep -q 'ubus_script_timeout' /etc/nginx/conf.d/luci.locations || sed -i '/ubus_parallel_req/a\ ubus_script_timeout 600;' /etc/nginx/conf.d/luci.locations
fi
if [ -f /etc/nginx/uci.conf.template ]; then
sed -i -e 's/128M/3072M/' -e 's/2 1k/4 512k/' /etc/nginx/uci.conf.template
grep -q 'client_body_buffer_size' /etc/nginx/uci.conf.template || sed -i '/large_client_header_buffers/a\\tclient_body_buffer_size 128K;' /etc/nginx/uci.conf.template
fi
fi
if [ -n "$(command -v nginx)" ]; then
if [ -z "$(uci -q get nginx.default_server)" ]; then
uci -q set nginx.default_server=server
uci -q set nginx.default_server.server_name='kwrt'
uci -q add_list nginx.default_server.listen='80 default_server'
uci -q add_list nginx.default_server.listen='[::]:80 default_server'
uci -q add_list nginx.default_server.include='conf.d/*.locations'
uci -q set nginx.default_server.access_log='off; # logd openwrt'
uci commit nginx
fi
grep -q "uwsgi_read_timeout" /etc/nginx/conf.d/luci.locations 2>/dev/null || {
sed -i "s/include uwsgi_params;/include uwsgi_params;\n\t\tuwsgi_read_timeout 300s;/" /etc/nginx/conf.d/luci.locations
sed -i '/ubus_parallel_req/a\ ubus_script_timeout 600;' /etc/nginx/conf.d/luci.locations
}
# 6. uWSGI 超时与 FullCone 探测(兼容 fw3/fw4)
for ini in /etc/uwsgi/vassals/luci-*.ini; do
[ -f "$ini" ] && ! grep -q 'cgi-timeout' "$ini" && echo "cgi-timeout = 500" >> "$ini"
done
sed -i 's/limit-as = 1000/limit-as = 5000/g' /etc/uwsgi/vassals/luci-webui.ini 2>/dev/null
sed -i \
-e "s/client_max_body_size 128M/client_max_body_size 3072M/" \
-e "s/large_client_header_buffers 2 1k/large_client_header_buffers 4 512k/" \
/etc/nginx/uci.conf.template
grep -q "client_body_buffer_size" /etc/nginx/uci.conf.template || \
sed -i "/large_client_header_buffers/a\ client_body_buffer_size 128K;" /etc/nginx/uci.conf.template
fi
rm -rf /tmp/luci-*
uci -q set base_config.@status[0].version="$version"
uci commit base_config
grep -q "cgi-timeout" /etc/uwsgi/vassals/luci-*.ini 2>/dev/null || sed -i '$a cgi-timeout = 500' /etc/uwsgi/vassals/luci-*.ini
sed -i "s/limit-as = 1000/limit-as = 5000/g" /etc/uwsgi/vassals/luci-webui.ini
modprobe xt_FULLCONENAT || {
if ! iptables -L -t nat 2>/dev/null | grep -q "FULLCONENAT"; then
uci -q set firewall.@defaults[0].fullcone='0'
uci commit firewall
fi
modprobe xt_FULLCONENAT >/dev/null 2>&1 || modprobe nft_fullcone >/dev/null 2>&1 || {
iptables -L -t nat 2>/dev/null | grep -q 'FULLCONENAT' || \
nft list ruleset 2>/dev/null | grep -q 'fullcone' || \
uci -q set firewall.@defaults[0].fullcone='0'
}
# 7. 更新版本状态、一次性提交所有 UCI 配置并安全退出
[ -f /etc/config/base_config ] && {
uci -q get base_config.@status[0] >/dev/null || uci -q add base_config status >/dev/null
uci -q set base_config.@status[0].version="$version"
}
uci -q commit
rm -rf /tmp/luci-*
exit 0
+276 -47
View File
@@ -1,55 +1,284 @@
#!/bin/sh
#
# Optimized & Refactored OpenWrt WiFi Configuration Script
# Source Reference: https://github.com/kiddin9/my-packages/blob/master/my-default-settings/files/sbin/mywifi
#
board_name() {
[ -e /tmp/sysinfo/board_name ] && cat /tmp/sysinfo/board_name || echo "generic"
show_help() {
cat << 'EOF_HELP'
用法:
mywifi [选项] [WiFi名称] [WiFi密码]
参数:
WiFi名称 基础SSID名称(缺省时自动使用系统 hostname)
WiFi密码 无线密码(缺省或未指定时默认无密码,指定时需 8-63 位)
选项:
-s, --ssid <名称> 指定基础WiFi名称
-p, --password <密码> 指定WiFi密码
-h, --help 查看帮助信息
WiFi命名规范:
2.4 GHz -> <WiFi名>_2.4G
5 GHz -> <WiFi名>_5G
6 GHz -> <WiFi名>_6G
使用示例:
mywifi # 默认使用 hostname,无密码
mywifi "HomeWiFi" # HomeWiFi_2.4G / HomeWiFi_5G,无密码
mywifi "HomeWiFi" "12345678" # HomeWiFi_2.4G / HomeWiFi_5G,密码 12345678
mywifi -s "HomeWiFi" -p "12345678" # 选项传参
EOF_HELP
}
wifi_setup_radio(){
local radio=$1
htmode="$(uci -q get wireless.${radio}.htmode)"
if uci -q get wireless.${radio} >/dev/null; then
uci -q del wireless.${radio}.disabled
uci -q set wireless.${radio}.country='CN'
if [ "$(uci -q get wireless.${radio}.band)" = "5g" ]; then
uci -q set wireless.default_${radio}.ssid="${SSID}_5G"
if [[ "$DISTRIB_TARGET" == *ipq60xx ]]; then
uci set wireless.${radio}.htmode='HE80'
fi
elif [ "$(uci -q get wireless.${radio}.band)" = "2g" ]; then
uci -q set wireless.default_${radio}.ssid="${SSID}_2.4G"
fi
uci -q set wireless.default_${radio}.device="${radio}"
SSID_PASSWD="${SSID_PASSWD:-$(uci -q get wireless.default_${radio}.key)}"
if [ "${SSID_PASSWD}" ]; then
uci -q set wireless.default_${radio}.encryption='psk2'
uci -q set wireless.default_${radio}.key="${SSID_PASSWD}"
else
uci -q set wireless.default_${radio}.encryption='none'
fi
fi
}
# 1. 解析命令行参数(同时兼顾位置参数与选项参数)
CUSTOM_SSID=""
CUSTOM_PASSWD=""
HAS_PASSWD=0
for i in $(seq 1 10); do
[[ "$(uci -q get base_config.@status[0].inited)" == 1 && "$(cat /etc/config/wireless)" ]] && break
[ "$(cat /etc/config/wireless)" ] || /sbin/wifi config
sleep 1s
while [ $# -gt 0 ]; do
case "$1" in
-h|--help)
show_help
exit 0
;;
-s|--ssid)
shift
CUSTOM_SSID="$1"
;;
-p|--password)
shift
CUSTOM_PASSWD="$1"
HAS_PASSWD=1
;;
*)
if [ -z "$CUSTOM_SSID" ]; then
CUSTOM_SSID="$1"
elif [ $HAS_PASSWD -eq 0 ]; then
CUSTOM_PASSWD="$1"
HAS_PASSWD=1
fi
;;
esac
shift
done
if [[ "$(cat /etc/config/wireless)" ]]; then
. /etc/openwrt_release
SSID="$(uci -q get wizard.default.wifi_ssid || uci -q get system.@system[0].hostname || echo Kwrt)"
SSID_PASSWD="$(uci -q get wizard.default.wifi_key)"
sed -i '/option disabled/d' /etc/config/wireless
# 密码合法性校验
if [ $HAS_PASSWD -eq 1 ] && [ -n "$CUSTOM_PASSWD" ]; then
pwd_len=${#CUSTOM_PASSWD}
if [ "$pwd_len" -lt 8 ] || [ "$pwd_len" -gt 63 ]; then
echo "[ERROR] WiFi 密码长度必须在 8 到 63 个字符之间!" >&2
exit 1
fi
SSID_PASSWD="$CUSTOM_PASSWD"
else
SSID_PASSWD=""
fi
[ "$(uci -q get wireless.radio1.band)" == "5g" ] && uci -q set wireless.radio0.band="2g"
widx=0
while uci rename wireless.@wifi-iface[$widx]=default_radio$widx >/dev/null 2>&1; do widx=$((widx+1)); done
for radio in radio0 radio1 radio2 radio3; do
wifi_setup_radio ${radio}
done
uci commit wireless
uci -q set base_config.@status[0].wifi=1
uci commit base_config
/sbin/wifi reload
fi
# 2. 深度就绪检测:确保内核PHY驱动、多频硬件、系统初始化状态和UCI配置全部真正就绪
wait_wifi_ready() {
local max_wait=15
local waited=0
while [ $waited -lt $max_wait ]; do
local phy_count=0
local uci_count=0
# B. 检查内核物理无线设备 (PHY)
local phys="$(ls -d /sys/class/ieee80211/phy* 2>/dev/null)"
[ -n "$phys" ] && phy_count=$(echo "$phys" | wc -w)
# C. 统计 UCI wireless 中实际已解析出的 wifi-device 节点数量(非文件大小)
if [ -s /etc/config/wireless ]; then
local uci_radios="$(uci -q show wireless 2>/dev/null | sed -n 's/^wireless\.\([^.]*\)=wifi-device/\1/p')"
[ -n "$uci_radios" ] && uci_count=$(echo "$uci_radios" | wc -w)
fi
# D. 自动探测与配置补全:
# - 物理网卡已加载,但 UCI 配置中的 radio 数量少于物理网卡数量(多频网卡尚未完全生成配置)
# - 或者配置文件缺失/没有 radio 配置
if [ $phy_count -gt 0 ] && [ $uci_count -lt $phy_count ]; then
[ -x /sbin/wifi ] && /sbin/wifi config >/dev/null 2>&1
elif [ $phy_count -eq 0 ] && [ $uci_count -eq 0 ] && [ ! -s /etc/config/wireless ]; then
[ -x /sbin/wifi ] && /sbin/wifi config >/dev/null 2>&1
fi
# 重新同步一次 UCI radio 数量
if [ -s /etc/config/wireless ]; then
local uci_radios_check="$(uci -q show wireless 2>/dev/null | sed -n 's/^wireless\.\([^.]*\)=wifi-device/\1/p')"
[ -n "$uci_radios_check" ] && uci_count=$(echo "$uci_radios_check" | wc -w)
fi
# E. 判定就绪标准:
# 1) UCI 中有效 wifi-device 数量 > 0
# 2) 若检测到物理 PHY,UCI radio 数量必须完全覆盖检测到的 PHY 数量
if [ $uci_count -gt 0 ]; then
if [ $phy_count -eq 0 ] || [ $uci_count -ge $phy_count ]; then
return 0
fi
fi
sleep 1
waited=$((waited + 1))
done
# 超时保护:只要至少有一个 radio 节点,允许降级继续
if [ $uci_count -gt 0 ]; then
return 0
fi
return 1
}
# 执行深度就绪等待
if ! wait_wifi_ready; then
echo "[ERROR] 未检测到可用无线硬件或无线配置生成超时!" >&2
exit 1
fi
# 3. 识别射频频段函数 (2g, 5g, 6g)
detect_band() {
local radio="$1"
local band="$(uci -q get wireless.${radio}.band 2>/dev/null | tr 'A-Z' 'a-z')"
case "$band" in
*2g*|*2.4g*) echo "2g"; return ;;
*5g*) echo "5g"; return ;;
*6g*) echo "6g"; return ;;
esac
# 信道 (channel) 推断
local channel="$(uci -q get wireless.${radio}.channel 2>/dev/null)"
if [ -n "$channel" ] && [ "$channel" -eq "$channel" 2>/dev/null ]; then
if [ "$channel" -ge 1 ] && [ "$channel" -le 14 ]; then
echo "2g"; return
elif [ "$channel" -ge 32 ] && [ "$channel" -le 177 ]; then
echo "5g"; return
elif [ "$channel" -ge 180 ]; then
echo "6g"; return
fi
fi
# hwmode 推断
local hwmode="$(uci -q get wireless.${radio}.hwmode 2>/dev/null | tr 'A-Z' 'a-z')"
case "$hwmode" in
*a*) echo "5g"; return ;;
*b*|*g*) echo "2g"; return ;;
esac
# 双频常规配对推断
if [ "$radio" = "radio0" ]; then
local r1_band="$(uci -q get wireless.radio1.band 2>/dev/null | tr 'A-Z' 'a-z')"
[ "$r1_band" = "5g" ] && echo "2g" && return
elif [ "$radio" = "radio1" ]; then
local r0_band="$(uci -q get wireless.radio0.band 2>/dev/null | tr 'A-Z' 'a-z')"
[ "$r0_band" = "2g" ] && echo "5g" && return
fi
echo "2g"
}
# 4. 加载系统发行版信息(若存在)
[ -f /etc/openwrt_release ] && . /etc/openwrt_release
# 5. 确定基础 WiFi 名称
if [ -n "$CUSTOM_SSID" ]; then
SSID="$CUSTOM_SSID"
else
SSID="$(uci -q get system.@system[0].hostname 2>/dev/null)"
[ -z "$SSID" ] && [ -r /proc/sys/kernel/hostname ] && SSID="$(cat /proc/sys/kernel/hostname)"
[ -z "$SSID" ] && SSID="Kwrt"
fi
# 移除禁用标记
sed -i '/option disabled/d' /etc/config/wireless 2>/dev/null
# 修正双频标记
if [ "$(uci -q get wireless.radio1.band 2>/dev/null)" = "5g" ]; then
[ -z "$(uci -q get wireless.radio0.band 2>/dev/null)" ] && uci -q set wireless.radio0.band="2g"
fi
# 重命名匿名接口 section 为 default_radioX
widx=0
while uci rename wireless.@wifi-iface[$widx]=default_radio$widx >/dev/null 2>&1; do
widx=$((widx + 1))
done
# 6. 配置各 Radio 参数
wifi_setup_radio() {
local radio="$1"
uci -q get wireless.${radio} >/dev/null 2>&1 || return 0
uci -q del wireless.${radio}.disabled
uci -q set wireless.${radio}.country='CN'
local band="$(detect_band "$radio")"
local ssid_suffix=""
case "$band" in
2g) ssid_suffix="_2.4G" ;;
5g) ssid_suffix="_5G" ;;
6g) ssid_suffix="_6G" ;;
*) ssid_suffix="_${band}" ;;
esac
local full_ssid="${SSID}${ssid_suffix}"
# 特定硬件架构适配(如高通 ipq60xx)
if [ "$band" = "5g" ]; then
case "$DISTRIB_TARGET" in
*ipq60xx*)
uci -q set wireless.${radio}.htmode='HE80'
;;
esac
fi
local iface="default_${radio}"
uci -q get wireless.${iface} >/dev/null 2>&1 || uci -q set wireless.${iface}=wifi-iface
uci -q set wireless.${iface}.device="${radio}"
uci -q set wireless.${iface}.mode='ap'
uci -q set wireless.${iface}.network='lan'
uci -q set wireless.${iface}.ssid="${full_ssid}"
# 密码与加密策略配置
if [ -n "$SSID_PASSWD" ]; then
if [ "$band" = "6g" ]; then
# 6GHz 频段按标准强制要求 WPA3-SAE
uci -q set wireless.${iface}.encryption='sae'
else
uci -q set wireless.${iface}.encryption='psk2'
fi
uci -q set wireless.${iface}.key="${SSID_PASSWD}"
echo "[INFO] ${radio} (${band}): SSID='${full_ssid}', 加密='$(uci -q get wireless.${iface}.encryption)'"
else
uci -q set wireless.${iface}.encryption='none'
uci -q del wireless.${iface}.key
echo "[INFO] ${radio} (${band}): SSID='${full_ssid}', 无密码 (开放网络)"
fi
}
# 7. 动态遍历所有射频设备并应用设置
radios="$(uci -q show wireless 2>/dev/null | sed -n 's/^wireless\.\([^.]*\)=wifi-device/\1/p')"
[ -z "$radios" ] && radios="radio0 radio1 radio2 radio3"
for radio in $radios; do
wifi_setup_radio "$radio"
done
# 提交配置
uci commit wireless 2>/dev/null
# 若存在 base_config(kiddin9 固件状态记录),更新状态
if uci -q get base_config.@status[0] >/dev/null 2>&1; then
uci -q set base_config.@status[0].wifi=1
uci commit base_config 2>/dev/null
fi
# 重载无线网络
if command -v wifi >/dev/null 2>&1; then
wifi reload
elif [ -x /sbin/wifi ]; then
/sbin/wifi reload
fi
echo "[SUCCESS] 无线配置已更新并生效。"
+3 -3
View File
@@ -50,12 +50,12 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=wwand
PKG_VERSION:=1.6.7_p55
PKG_RELEASE:=48
PKG_VERSION:=1.6.7_p58
PKG_RELEASE:=50
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git
PKG_SOURCE_VERSION:=aaf2ba064bbf751eaa0237e00f94c1619f05b648
PKG_SOURCE_VERSION:=1b575d41dd35684e035541afe1f129da072275dd
PKG_MIRROR_HASH:=skip
PKG_LICENSE:=GPL-2.0-only