🦄 Sync 2026-08-26 20:42:53
No files matched your search
@@ -10,8 +10,8 @@ include $(TOPDIR)/rules.mk
|
|||||||
PKG_ARCH_BAIDUDRIVE:=$(ARCH)
|
PKG_ARCH_BAIDUDRIVE:=$(ARCH)
|
||||||
|
|
||||||
PKG_NAME:=baidudrive
|
PKG_NAME:=baidudrive
|
||||||
PKG_VERSION:=kaiplus-runtime-v1.0.5
|
PKG_VERSION:=dockermanager-runtime-v0.1.1
|
||||||
PKG_RELEASE:=15
|
PKG_RELEASE:=16
|
||||||
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
|
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
|
||||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/baidudrive-runtime-v$(PKG_VERSION)/
|
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/baidudrive-runtime-v$(PKG_VERSION)/
|
||||||
PKG_HASH:=skip
|
PKG_HASH:=skip
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#
|
||||||
|
# Copyright (C) 2026 LinkEase Contributors
|
||||||
|
#
|
||||||
|
# This is free software, licensed under the GNU General Public License v3.
|
||||||
|
#
|
||||||
|
|
||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
PKG_ARCH_DOCKERMANAGER:=$(ARCH)
|
||||||
|
|
||||||
|
PKG_NAME:=dockermanager
|
||||||
|
PKG_VERSION:=dockermanager-runtime-v0.1.1
|
||||||
|
PKG_RELEASE:=1
|
||||||
|
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
|
||||||
|
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/dockermanager-runtime-v$(PKG_VERSION)/
|
||||||
|
PKG_HASH:=skip
|
||||||
|
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-binary-$(PKG_VERSION)
|
||||||
|
|
||||||
|
PKG_BUILD_PARALLEL:=1
|
||||||
|
PKG_USE_MIPS16:=0
|
||||||
|
|
||||||
|
include $(INCLUDE_DIR)/package.mk
|
||||||
|
|
||||||
|
define Package/$(PKG_NAME)
|
||||||
|
SECTION:=net
|
||||||
|
CATEGORY:=System
|
||||||
|
TITLE:=Docker Manager
|
||||||
|
URL:=https://github.com/linkease/linkease-docker-manager
|
||||||
|
DEPENDS:=+docker +dockerd +ca-bundle +luci-lib-linkeaseauth
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/$(PKG_NAME)/description
|
||||||
|
Docker Manager provides a LinkEase Desktop web module and HTTP API for Docker management.
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/$(PKG_NAME)/conffiles
|
||||||
|
/etc/config/dockermanager
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/$(PKG_NAME)/prerm
|
||||||
|
#!/bin/sh
|
||||||
|
if [ "$${IPKG_INSTROOT}" = "" ]; then
|
||||||
|
if [ "$$(readlink /usr/share/linkeasefull/desktop-apps.d/20-dockermanager-plugin.json 2>/dev/null)" = "/usr/share/dockermanager/dockermanager-plugin.json" ]; then
|
||||||
|
rm -f /usr/share/linkeasefull/desktop-apps.d/20-dockermanager-plugin.json
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Build/Configure
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Build/Compile
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/$(PKG_NAME)/install
|
||||||
|
$(INSTALL_DIR) $(1)/usr/sbin $(1)/usr/share/dockermanager/www $(1)/usr/share/linkeasefull/desktop-apps.d $(1)/etc/init.d $(1)/etc/config
|
||||||
|
$(INSTALL_BIN) $(PKG_BUILD_DIR)/docker-manager.$(PKG_ARCH_DOCKERMANAGER) $(1)/usr/sbin/docker-manager
|
||||||
|
$(CP) $(PKG_BUILD_DIR)/www/* $(1)/usr/share/dockermanager/www/
|
||||||
|
$(INSTALL_DATA) ./files/logo.svg $(1)/usr/share/dockermanager/www/logo.svg
|
||||||
|
$(INSTALL_DATA) ./files/dockermanager-plugin.json $(1)/usr/share/dockermanager/dockermanager-plugin.json
|
||||||
|
ln -sf /usr/share/dockermanager/dockermanager-plugin.json $(1)/usr/share/linkeasefull/desktop-apps.d/20-dockermanager-plugin.json
|
||||||
|
$(INSTALL_BIN) ./files/dockermanager.init $(1)/etc/init.d/dockermanager
|
||||||
|
$(INSTALL_CONF) ./files/dockermanager.config $(1)/etc/config/dockermanager
|
||||||
|
endef
|
||||||
|
|
||||||
|
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"id": "dockermanager",
|
||||||
|
"name": "Docker Manager",
|
||||||
|
"icon": "logo.svg",
|
||||||
|
"staticRoot": "/usr/share/dockermanager/www",
|
||||||
|
"desktop": {
|
||||||
|
"mode": "module",
|
||||||
|
"entry": "desktop-entry.js",
|
||||||
|
"isolation": "shadow-dom"
|
||||||
|
},
|
||||||
|
"standalone": {
|
||||||
|
"entry": "index.html",
|
||||||
|
"basePath": "/apps/dockermanager/"
|
||||||
|
},
|
||||||
|
"backend": {
|
||||||
|
"type": "http",
|
||||||
|
"scheme": "http",
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"portFromUci": "dockermanager.@dockermanager[0].port",
|
||||||
|
"defaultPort": 8192,
|
||||||
|
"upstreamBasePath": "/apps/dockermanager/",
|
||||||
|
"apiPath": "api/",
|
||||||
|
"pathMode": "preserve"
|
||||||
|
},
|
||||||
|
"window": {
|
||||||
|
"width": 1180,
|
||||||
|
"height": 760,
|
||||||
|
"singleton": true
|
||||||
|
},
|
||||||
|
"capabilities": ["docker"],
|
||||||
|
"categories": ["system"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
config dockermanager
|
||||||
|
option 'enabled' '0'
|
||||||
|
option 'data_dir' ''
|
||||||
|
option 'port' '8192'
|
||||||
|
option 'bind_addr' '0.0.0.0'
|
||||||
|
option 'base_path' '/apps/dockermanager/'
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
#!/bin/sh /etc/rc.common
|
||||||
|
|
||||||
|
START=94
|
||||||
|
USE_PROCD=1
|
||||||
|
|
||||||
|
get_config() {
|
||||||
|
config_get_bool enabled "$1" enabled 0
|
||||||
|
config_get data_dir "$1" data_dir ""
|
||||||
|
config_get port "$1" port "8192"
|
||||||
|
config_get bind_addr "$1" bind_addr "0.0.0.0"
|
||||||
|
config_get base_path "$1" base_path "/apps/dockermanager/"
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdir_p() {
|
||||||
|
[ -d "$1" ] || mkdir -p "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
start_service() {
|
||||||
|
config_load dockermanager
|
||||||
|
config_foreach get_config dockermanager
|
||||||
|
|
||||||
|
if [ "$enabled" != 1 ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [ -z "$data_dir" ]; then
|
||||||
|
logger -t dockermanager "data_dir is empty; set option data_dir in /etc/config/dockermanager"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir_p "$data_dir" || return 1
|
||||||
|
mkdir_p "$data_dir/state" || return 1
|
||||||
|
mkdir_p "$data_dir/cache" || return 1
|
||||||
|
|
||||||
|
logger -t dockermanager "Starting Docker Manager service"
|
||||||
|
procd_open_instance docker-manager
|
||||||
|
procd_set_param env \
|
||||||
|
DOCKER_MANAGER_ADDR="$bind_addr:$port" \
|
||||||
|
DOCKER_MANAGER_BASE_PATH="$base_path" \
|
||||||
|
DOCKER_MANAGER_DATA_DIR="$data_dir" \
|
||||||
|
DOCKER_MANAGER_STATIC_ROOT="/usr/share/dockermanager/www" \
|
||||||
|
DOCKER_MANAGER_PLATFORM="openwrt" \
|
||||||
|
DOCKER_MANAGER_AUTH_MODE="openwrt_luci"
|
||||||
|
procd_set_param command /usr/sbin/docker-manager
|
||||||
|
procd_set_param stdout 1
|
||||||
|
procd_set_param stderr 1
|
||||||
|
procd_set_param respawn
|
||||||
|
procd_close_instance
|
||||||
|
logger -t dockermanager "Docker Manager service started"
|
||||||
|
}
|
||||||
|
|
||||||
|
service_triggers() {
|
||||||
|
procd_add_reload_trigger "dockermanager"
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" role="img" aria-label="Docker Manager">
|
||||||
|
<rect width="96" height="96" rx="18" fill="#111827"/>
|
||||||
|
<path fill="#60a5fa" d="M20 53h56c-1 13-11 23-26 23H38c-11 0-18-7-18-18v-5Z"/>
|
||||||
|
<rect x="25" y="36" width="10" height="10" rx="2" fill="#dbeafe"/>
|
||||||
|
<rect x="38" y="36" width="10" height="10" rx="2" fill="#dbeafe"/>
|
||||||
|
<rect x="51" y="36" width="10" height="10" rx="2" fill="#dbeafe"/>
|
||||||
|
<rect x="38" y="23" width="10" height="10" rx="2" fill="#dbeafe"/>
|
||||||
|
<rect x="51" y="23" width="10" height="10" rx="2" fill="#dbeafe"/>
|
||||||
|
<path fill="#93c5fd" d="M70 44c5-1 9-4 11-8 2 6-1 12-7 15l-4-7Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 658 B |
@@ -11,8 +11,8 @@ PKG_ARCH_ISTOREENHANCE:=$(ARCH)
|
|||||||
|
|
||||||
PKG_NAME:=istoreenhance
|
PKG_NAME:=istoreenhance
|
||||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||||
PKG_SOURCE_DATE:=0.7.15
|
PKG_SOURCE_DATE:=0.7.16
|
||||||
PKG_RELEASE:=5
|
PKG_RELEASE:=6
|
||||||
ARCH_HEXCODE:=
|
ARCH_HEXCODE:=
|
||||||
ifeq ($(ARCH),x86_64)
|
ifeq ($(ARCH),x86_64)
|
||||||
ARCH_HEXCODE=8664
|
ARCH_HEXCODE=8664
|
||||||
@@ -29,6 +29,7 @@ PKG_SOURCE_URL:=https://github.com/kspeeder/docker_kspeeder/releases/download/v$
|
|||||||
PKG_HASH:=skip
|
PKG_HASH:=skip
|
||||||
|
|
||||||
PKG_BUILD_DIR:=$(BUILD_DIR)/iStoreEnhance-binary-$(PKG_SOURCE_DATE)
|
PKG_BUILD_DIR:=$(BUILD_DIR)/iStoreEnhance-binary-$(PKG_SOURCE_DATE)
|
||||||
|
KSPEEDER_WEB_ROOT:=$(PKG_BUILD_DIR)/wwwroot
|
||||||
|
|
||||||
PKG_BUILD_PARALLEL:=1
|
PKG_BUILD_PARALLEL:=1
|
||||||
PKG_USE_MIPS16:=0
|
PKG_USE_MIPS16:=0
|
||||||
@@ -61,6 +62,16 @@ if [ -z "$${IPKG_INSTROOT}" ]; then
|
|||||||
fi
|
fi
|
||||||
endef
|
endef
|
||||||
|
|
||||||
|
define Package/$(PKG_NAME)/prerm
|
||||||
|
#!/bin/sh
|
||||||
|
if [ "$${IPKG_INSTROOT}" = "" ]; then
|
||||||
|
if [ "$$(readlink /usr/share/linkeasefull/desktop-apps.d/20-kspeeder.json 2>/dev/null)" = "/usr/share/kspeeder/kspeeder-plugin.json" ]; then
|
||||||
|
rm -f /usr/share/linkeasefull/desktop-apps.d/20-kspeeder.json
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
endef
|
||||||
|
|
||||||
define Build/Configure
|
define Build/Configure
|
||||||
endef
|
endef
|
||||||
|
|
||||||
@@ -68,11 +79,20 @@ define Build/Compile
|
|||||||
endef
|
endef
|
||||||
|
|
||||||
define Package/$(PKG_NAME)/install
|
define Package/$(PKG_NAME)/install
|
||||||
$(INSTALL_DIR) $(1)/usr/sbin $(1)/etc/config $(1)/etc/init.d $(1)/etc/uci-defaults
|
$(INSTALL_DIR) $(1)/usr/sbin $(1)/etc/config $(1)/etc/init.d $(1)/etc/uci-defaults $(1)/usr/share/kspeeder/www $(1)/usr/share/linkeasefull/desktop-apps.d
|
||||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/iStoreEnhance.$(PKG_ARCH_ISTOREENHANCE) $(1)/usr/sbin/iStoreEnhance
|
$(INSTALL_BIN) $(PKG_BUILD_DIR)/iStoreEnhance.$(PKG_ARCH_ISTOREENHANCE) $(1)/usr/sbin/iStoreEnhance
|
||||||
$(INSTALL_CONF) ./files/istoreenhance.config $(1)/etc/config/istoreenhance
|
$(INSTALL_CONF) ./files/istoreenhance.config $(1)/etc/config/istoreenhance
|
||||||
$(INSTALL_BIN) ./files/istoreenhance.init $(1)/etc/init.d/istoreenhance
|
$(INSTALL_BIN) ./files/istoreenhance.init $(1)/etc/init.d/istoreenhance
|
||||||
$(INSTALL_BIN) ./files/istoreenhance.uci-default $(1)/etc/uci-defaults/istoreenhance
|
$(INSTALL_BIN) ./files/istoreenhance.uci-default $(1)/etc/uci-defaults/istoreenhance
|
||||||
|
$(INSTALL_DATA) ./files/kspeeder-plugin.json $(1)/usr/share/kspeeder/kspeeder-plugin.json
|
||||||
|
if [ -d "$(KSPEEDER_WEB_ROOT)" ] && [ -f "$(KSPEEDER_WEB_ROOT)/desktop-entry.js" ]; then \
|
||||||
|
$(CP) $(KSPEEDER_WEB_ROOT)/* $(1)/usr/share/kspeeder/www/; \
|
||||||
|
else \
|
||||||
|
$(INSTALL_DATA) ./files/www/desktop-entry.js $(1)/usr/share/kspeeder/www/desktop-entry.js; \
|
||||||
|
$(INSTALL_DATA) ./files/www/index.html $(1)/usr/share/kspeeder/www/index.html; \
|
||||||
|
fi
|
||||||
|
$(INSTALL_DATA) ./files/logo.png $(1)/usr/share/kspeeder/www/logo.png
|
||||||
|
ln -sf /usr/share/kspeeder/kspeeder-plugin.json $(1)/usr/share/linkeasefull/desktop-apps.d/20-kspeeder.json
|
||||||
endef
|
endef
|
||||||
|
|
||||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"id": "kspeeder",
|
||||||
|
"name": "KSpeeder",
|
||||||
|
"icon": "logo.png",
|
||||||
|
"staticRoot": "/usr/share/kspeeder/www",
|
||||||
|
"desktop": {
|
||||||
|
"mode": "module",
|
||||||
|
"entry": "desktop-entry.js",
|
||||||
|
"isolation": "shadow-dom"
|
||||||
|
},
|
||||||
|
"standalone": {
|
||||||
|
"entry": "index.html",
|
||||||
|
"basePath": "/apps/kspeeder/",
|
||||||
|
"externalOpen": {
|
||||||
|
"enabled": true,
|
||||||
|
"label": "打开 KSpeeder 独立页面",
|
||||||
|
"scheme": "http",
|
||||||
|
"hostMode": "request-host",
|
||||||
|
"defaultPort": 5003,
|
||||||
|
"path": "/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"backend": {
|
||||||
|
"type": "http",
|
||||||
|
"scheme": "http",
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"portFromUci": "istoreenhance.@istoreenhance[0].adminport",
|
||||||
|
"defaultPort": 5003,
|
||||||
|
"upstreamBasePath": "/apps/kspeeder/",
|
||||||
|
"apiPath": "api/",
|
||||||
|
"pathMode": "preserve"
|
||||||
|
},
|
||||||
|
"window": {
|
||||||
|
"width": 1040,
|
||||||
|
"height": 700,
|
||||||
|
"singleton": true
|
||||||
|
},
|
||||||
|
"capabilities": ["docker-acceleration", "registry-mirror", "cache"],
|
||||||
|
"categories": ["network", "docker"]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,260 @@
|
|||||||
|
const stateByElement = new WeakMap();
|
||||||
|
|
||||||
|
function normalizeBasePath(path) {
|
||||||
|
const value = typeof path === "string" && path.trim() ? path.trim() : "/apps/kspeeder/";
|
||||||
|
return value.endsWith("/") ? value : `${value}/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAPIBase(context) {
|
||||||
|
if (context && typeof context.apiBase === "string" && context.apiBase.trim()) {
|
||||||
|
return context.apiBase.endsWith("/") ? context.apiBase : `${context.apiBase}/`;
|
||||||
|
}
|
||||||
|
return `${normalizeBasePath(context && context.basePath)}api/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHTML(value) {
|
||||||
|
return String(value == null ? "" : value).replace(/[&<>"']/g, (char) => ({
|
||||||
|
"&": "&",
|
||||||
|
"<": "<",
|
||||||
|
">": ">",
|
||||||
|
'"': """,
|
||||||
|
"'": "'",
|
||||||
|
})[char]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchJSON(url, options) {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: { Accept: "application/json" },
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
const text = await response.text();
|
||||||
|
let payload = null;
|
||||||
|
if (text) {
|
||||||
|
try {
|
||||||
|
payload = JSON.parse(text);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`API returned non-JSON response (${response.status})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!response.ok) {
|
||||||
|
const message = payload && (payload.message || payload.error || payload.details);
|
||||||
|
throw new Error(message || `API request failed (${response.status})`);
|
||||||
|
}
|
||||||
|
return payload || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mirrorCounts(overview) {
|
||||||
|
const mirrorsByProfile = overview && typeof overview.mirrors === "object" ? overview.mirrors : {};
|
||||||
|
const result = { online: 0, total: 0, servedSpeed: "", servedDownload: "" };
|
||||||
|
Object.values(mirrorsByProfile).forEach((profile) => {
|
||||||
|
const mirrors = Array.isArray(profile && profile.mirrors) ? profile.mirrors : [];
|
||||||
|
result.total += mirrors.length;
|
||||||
|
result.online += mirrors.filter((mirror) => mirror && mirror.status === "online").length;
|
||||||
|
if (!result.servedSpeed && profile && profile.served_speed) {
|
||||||
|
result.servedSpeed = profile.served_speed;
|
||||||
|
}
|
||||||
|
if (!result.servedDownload && profile && profile.served_download) {
|
||||||
|
result.servedDownload = profile.served_download;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function profileRows(overview) {
|
||||||
|
const mirrorsByProfile = overview && typeof overview.mirrors === "object" ? overview.mirrors : {};
|
||||||
|
return Object.entries(mirrorsByProfile).map(([profile, status]) => {
|
||||||
|
const mirrors = Array.isArray(status && status.mirrors) ? status.mirrors : [];
|
||||||
|
const online = mirrors.filter((mirror) => mirror && mirror.status === "online").length;
|
||||||
|
const best = mirrors.find((mirror) => mirror && mirror.status === "online") || mirrors[0] || {};
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td>${escapeHTML(profile)}</td>
|
||||||
|
<td>${online}/${mirrors.length}</td>
|
||||||
|
<td>${escapeHTML(best.current_speed || best.test_speed || "-")}</td>
|
||||||
|
<td>${escapeHTML(best.name || "-")}</td>
|
||||||
|
<td>${escapeHTML(best.error_info || "-")}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(root, model) {
|
||||||
|
const counts = mirrorCounts(model.overview);
|
||||||
|
const source = model.overview && model.overview.source ? model.overview.source : "-";
|
||||||
|
const error = model.error ? `<div class="notice danger">${escapeHTML(model.error)}</div>` : "";
|
||||||
|
const refreshing = model.refreshing ? "disabled" : "";
|
||||||
|
const rows = profileRows(model.overview);
|
||||||
|
root.innerHTML = `
|
||||||
|
<style>
|
||||||
|
:host { color-scheme: light; }
|
||||||
|
.ks-page { box-sizing: border-box; min-height: 100%; background: #f7f8fa; color: #1f2937; font: 14px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
||||||
|
.ks-wrap { box-sizing: border-box; max-width: 1080px; margin: 0 auto; padding: 18px; }
|
||||||
|
.ks-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; margin-bottom: 14px; }
|
||||||
|
h1 { margin: 0; font-size: 20px; line-height: 1.2; font-weight: 650; color: #111827; }
|
||||||
|
.sub { margin-top: 5px; color: #667085; font-size: 13px; }
|
||||||
|
.actions { display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; }
|
||||||
|
button { border: 1px solid #cfd7e3; background: #fff; color: #1f2937; border-radius: 6px; padding: 7px 10px; font-size: 13px; cursor: pointer; }
|
||||||
|
button.primary { background: #0f766e; border-color: #0f766e; color: #fff; }
|
||||||
|
button:disabled { cursor: wait; opacity: .62; }
|
||||||
|
.grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }
|
||||||
|
.card { border: 1px solid #e2e8f0; border-radius: 8px; background: #fff; padding: 12px; min-width: 0; }
|
||||||
|
.label { color: #667085; font-size: 12px; margin-bottom: 6px; }
|
||||||
|
.value { font-size: 18px; font-weight: 650; color: #111827; overflow-wrap: anywhere; }
|
||||||
|
.notice { border-radius: 8px; padding: 10px 12px; margin-bottom: 12px; background: #fff7ed; border: 1px solid #fed7aa; color: #9a3412; }
|
||||||
|
.notice.danger { background: #fef2f2; border-color: #fecaca; color: #b91c1c; }
|
||||||
|
.section { margin-top: 12px; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; background: #fff; }
|
||||||
|
.section-title { display: flex; justify-content: space-between; gap: 10px; padding: 11px 12px; border-bottom: 1px solid #e2e8f0; font-weight: 650; }
|
||||||
|
table { width: 100%; border-collapse: collapse; table-layout: fixed; }
|
||||||
|
th, td { padding: 9px 12px; border-bottom: 1px solid #edf2f7; text-align: left; vertical-align: top; overflow-wrap: anywhere; }
|
||||||
|
th { color: #667085; font-size: 12px; font-weight: 600; background: #fbfcfd; }
|
||||||
|
tr:last-child td { border-bottom: 0; }
|
||||||
|
.empty { padding: 18px 12px; color: #667085; }
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.ks-wrap { padding: 12px; }
|
||||||
|
.ks-top { display: block; }
|
||||||
|
.actions { justify-content: flex-start; margin-top: 10px; }
|
||||||
|
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
|
th:nth-child(5), td:nth-child(5) { display: none; }
|
||||||
|
}
|
||||||
|
@media (max-width: 430px) {
|
||||||
|
.grid { grid-template-columns: 1fr; }
|
||||||
|
th:nth-child(4), td:nth-child(4) { display: none; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="ks-page">
|
||||||
|
<div class="ks-wrap">
|
||||||
|
<div class="ks-top">
|
||||||
|
<div>
|
||||||
|
<h1>KSpeeder</h1>
|
||||||
|
<div class="sub">Docker 镜像下载加速状态</div>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button data-action="reload">刷新</button>
|
||||||
|
<button class="primary" data-action="refresh-mirrors" ${refreshing}>${model.refreshing ? "测速中" : "刷新测速"}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${error}
|
||||||
|
<div class="grid">
|
||||||
|
<div class="card"><div class="label">版本</div><div class="value">${escapeHTML(model.summary.version || "-")}</div></div>
|
||||||
|
<div class="card"><div class="label">可用节点</div><div class="value">${counts.online}/${counts.total}</div></div>
|
||||||
|
<div class="card"><div class="label">当前下发</div><div class="value">${escapeHTML(counts.servedSpeed || "-")}</div></div>
|
||||||
|
<div class="card"><div class="label">累计下发</div><div class="value">${escapeHTML(counts.servedDownload || "-")}</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title"><span>镜像节点</span><span>${escapeHTML(source)}</span></div>
|
||||||
|
${rows ? `
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>类型</th>
|
||||||
|
<th>在线</th>
|
||||||
|
<th>速度</th>
|
||||||
|
<th>节点</th>
|
||||||
|
<th>错误</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>${rows}</tbody>
|
||||||
|
</table>
|
||||||
|
` : '<div class="empty">暂无镜像状态,确认 iStoreEnhance 服务已启动并已配置缓存目录。</div>'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadModel(state) {
|
||||||
|
const [summary, overview] = await Promise.all([
|
||||||
|
fetchJSON(`${state.apiBase}summary`),
|
||||||
|
fetchJSON(`${state.apiBase}mirrors/overview`),
|
||||||
|
]);
|
||||||
|
state.model.summary = summary;
|
||||||
|
state.model.overview = overview;
|
||||||
|
state.model.error = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reload(state) {
|
||||||
|
try {
|
||||||
|
await loadModel(state);
|
||||||
|
} catch (error) {
|
||||||
|
state.model.error = error instanceof Error ? error.message : "加载失败";
|
||||||
|
} finally {
|
||||||
|
render(state.root, state.model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshMirrors(state) {
|
||||||
|
state.model.refreshing = true;
|
||||||
|
state.model.error = "";
|
||||||
|
render(state.root, state.model);
|
||||||
|
try {
|
||||||
|
await fetchJSON(`${state.apiBase}mirrors/refresh`, { method: "POST" });
|
||||||
|
await loadModel(state);
|
||||||
|
if (state.hostAdapter && state.hostAdapter.toast) {
|
||||||
|
state.hostAdapter.toast.success("KSpeeder 测速已刷新");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
state.model.error = error instanceof Error ? error.message : "刷新测速失败";
|
||||||
|
if (state.hostAdapter && state.hostAdapter.toast) {
|
||||||
|
state.hostAdapter.toast.error(state.model.error);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
state.model.refreshing = false;
|
||||||
|
render(state.root, state.model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function bootstrap() {}
|
||||||
|
|
||||||
|
export async function mount(props) {
|
||||||
|
const root = props && props.domElement;
|
||||||
|
if (!root) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const context = props.context || {};
|
||||||
|
const state = {
|
||||||
|
root,
|
||||||
|
apiBase: normalizeAPIBase(context),
|
||||||
|
hostAdapter: props.hostAdapter,
|
||||||
|
model: {
|
||||||
|
summary: {},
|
||||||
|
overview: {},
|
||||||
|
error: "",
|
||||||
|
refreshing: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
stateByElement.set(root, state);
|
||||||
|
root.addEventListener("click", handleClick);
|
||||||
|
render(root, state.model);
|
||||||
|
await reload(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function unmount(props) {
|
||||||
|
const root = props && props.domElement;
|
||||||
|
if (!root) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.removeEventListener("click", handleClick);
|
||||||
|
root.innerHTML = "";
|
||||||
|
stateByElement.delete(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClick(event) {
|
||||||
|
const target = event.target && event.target.closest ? event.target.closest("[data-action]") : null;
|
||||||
|
if (!target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const root = event.currentTarget;
|
||||||
|
const state = stateByElement.get(root);
|
||||||
|
if (!state) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const action = target.getAttribute("data-action");
|
||||||
|
if (action === "reload") {
|
||||||
|
void reload(state);
|
||||||
|
} else if (action === "refresh-mirrors") {
|
||||||
|
void refreshMirrors(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { bootstrap, mount, unmount };
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>KSpeeder</title>
|
||||||
|
<style>
|
||||||
|
html, body, #root {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module">
|
||||||
|
import { mount } from "./desktop-entry.js";
|
||||||
|
|
||||||
|
mount({
|
||||||
|
domElement: document.getElementById("root"),
|
||||||
|
context: {
|
||||||
|
basePath: "/apps/kspeeder/",
|
||||||
|
apiBase: "/apps/kspeeder/api/",
|
||||||
|
app: { id: "kspeeder", name: "KSpeeder", entry: "desktop-entry.js" },
|
||||||
|
window: { id: "standalone", title: "KSpeeder" },
|
||||||
|
theme: { mode: "light", variant: "" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -9,8 +9,8 @@ include $(TOPDIR)/rules.mk
|
|||||||
PKG_ARCH_kaiplus:=$(ARCH)
|
PKG_ARCH_kaiplus:=$(ARCH)
|
||||||
|
|
||||||
PKG_NAME:=kaiplus
|
PKG_NAME:=kaiplus
|
||||||
PKG_VERSION:=kaiplus-runtime-v1.0.5
|
PKG_VERSION:=dockermanager-runtime-v0.1.1
|
||||||
PKG_RELEASE:=5
|
PKG_RELEASE:=6
|
||||||
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
|
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
|
||||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/kaiplus-runtime-v$(PKG_VERSION)/
|
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/kaiplus-runtime-v$(PKG_VERSION)/
|
||||||
PKG_HASH:=skip
|
PKG_HASH:=skip
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
|
|||||||
PKG_NAME:=linkease-common-bin
|
PKG_NAME:=linkease-common-bin
|
||||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||||
PKG_SOURCE_DATE:=1.7.5
|
PKG_SOURCE_DATE:=1.7.5
|
||||||
PKG_RELEASE:=14
|
PKG_RELEASE:=15
|
||||||
|
|
||||||
ARCH_HEXCODE:=
|
ARCH_HEXCODE:=
|
||||||
ifeq ($(ARCH),x86_64)
|
ifeq ($(ARCH),x86_64)
|
||||||
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
|
|||||||
ARCH_HEXCODE=1b0c
|
ARCH_HEXCODE=1b0c
|
||||||
endif
|
endif
|
||||||
|
|
||||||
PKG_SOURCE_VERSION:=2036fce819b5a44574d07cca9927eae73da43efc
|
PKG_SOURCE_VERSION:=b0d9136d243ac1a5c5290190ffa6f66265aa7fba
|
||||||
PKG_SOURCE:=linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz
|
PKG_SOURCE:=linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz
|
||||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/
|
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/
|
||||||
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE)
|
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
|
|||||||
PKG_NAME:=linkease
|
PKG_NAME:=linkease
|
||||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||||
PKG_SOURCE_DATE:=1.7.5
|
PKG_SOURCE_DATE:=1.7.5
|
||||||
PKG_RELEASE:=17
|
PKG_RELEASE:=18
|
||||||
|
|
||||||
ARCH_HEXCODE:=
|
ARCH_HEXCODE:=
|
||||||
ifeq ($(ARCH),x86_64)
|
ifeq ($(ARCH),x86_64)
|
||||||
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
|
|||||||
ARCH_HEXCODE=1b0c
|
ARCH_HEXCODE=1b0c
|
||||||
endif
|
endif
|
||||||
|
|
||||||
PKG_SOURCE_VERSION:=2036fce819b5a44574d07cca9927eae73da43efc
|
PKG_SOURCE_VERSION:=b0d9136d243ac1a5c5290190ffa6f66265aa7fba
|
||||||
PKG_SOURCE:=linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz
|
PKG_SOURCE:=linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz
|
||||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/
|
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/
|
||||||
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE)
|
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE)
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ PKG_ARCH_LINKEASE:=$(ARCH)
|
|||||||
|
|
||||||
PKG_NAME:=linkeasefull
|
PKG_NAME:=linkeasefull
|
||||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||||
PKG_SOURCE_DATE:=3.0.12
|
PKG_SOURCE_DATE:=3.0.13
|
||||||
PKG_RELEASE:=16
|
PKG_RELEASE:=17
|
||||||
|
|
||||||
ARCH_HEXCODE:=
|
ARCH_HEXCODE:=
|
||||||
ifeq ($(ARCH),x86_64)
|
ifeq ($(ARCH),x86_64)
|
||||||
@@ -24,7 +24,7 @@ LINKEASE_RUNTIME_ARCH:=arm64
|
|||||||
PKG_HASH:=skip
|
PKG_HASH:=skip
|
||||||
endif
|
endif
|
||||||
|
|
||||||
PKG_SOURCE_VERSION:=2036fce819b5a44574d07cca9927eae73da43efc
|
PKG_SOURCE_VERSION:=b0d9136d243ac1a5c5290190ffa6f66265aa7fba
|
||||||
PKG_SOURCE:=linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH).tar.gz
|
PKG_SOURCE:=linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH).tar.gz
|
||||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkeasefull-runtime-v$(PKG_SOURCE_DATE)/
|
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkeasefull-runtime-v$(PKG_SOURCE_DATE)/
|
||||||
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH)
|
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH)
|
||||||
|
|||||||
@@ -52,10 +52,6 @@ is_persistent_data_root_parent() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
linkease_enabled() {
|
|
||||||
[ "$(uci -q get linkease.@linkease[0].enabled 2>/dev/null)" = "1" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
read_config() {
|
read_config() {
|
||||||
section="$1"
|
section="$1"
|
||||||
config_get_bool enabled "$section" enabled 1
|
config_get_bool enabled "$section" enabled 1
|
||||||
@@ -80,10 +76,6 @@ start_service() {
|
|||||||
config_foreach read_config linkeasefull
|
config_foreach read_config linkeasefull
|
||||||
[ "$enabled" = "1" ] || return 1
|
[ "$enabled" = "1" ] || return 1
|
||||||
[ -x "$PROG" ] || return 1
|
[ -x "$PROG" ] || return 1
|
||||||
if linkease_enabled; then
|
|
||||||
echo "linkeasefull: linkease is enabled; please disable linkease before starting linkeasefull" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f "$LOCAL_API"
|
rm -f "$LOCAL_API"
|
||||||
if [ -n "$data_root_parent" ]; then
|
if [ -n "$data_root_parent" ]; then
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ LUCI_PKGARCH:=all
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-accesscontrol-plus
|
PKG_NAME:=luci-app-accesscontrol-plus
|
||||||
PKG_VERSION:=1
|
PKG_VERSION:=1
|
||||||
PKG_RELEASE:=2
|
PKG_RELEASE:=3
|
||||||
|
|
||||||
include $(TOPDIR)/feeds/luci/luci.mk
|
include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
# call BuildPackage - OpenWrt buildroot signature
|
# call BuildPackage - OpenWrt buildroot signature
|
||||||
@@ -15,7 +15,8 @@ end
|
|||||||
|
|
||||||
function act_status()
|
function act_status()
|
||||||
local e = {}
|
local e = {}
|
||||||
e.running = luci.sys.call("iptables -t filter -S MIAPLUS >/dev/null 2>&1") == 0
|
-- 交给 init 脚本的 status 方法判断:它会按 fw3(iptables)/fw4(nft) 自动分发
|
||||||
|
e.running = luci.sys.call("/etc/init.d/miaplus status >/dev/null 2>&1") == 0
|
||||||
luci.http.prepare_content("application/json")
|
luci.http.prepare_content("application/json")
|
||||||
luci.http.write_json(e)
|
luci.http.write_json(e)
|
||||||
end
|
end
|
||||||
@@ -16,13 +16,42 @@ e = t:option(Flag, "enable", translate("Enabled"))
|
|||||||
e.rmempty = false
|
e.rmempty = false
|
||||||
e.default = "1"
|
e.default = "1"
|
||||||
|
|
||||||
|
-- HH:MM / H:MM -> 分钟;非法或空返回 nil
|
||||||
|
local function time_to_min(s)
|
||||||
|
local h, m = s:match("^(%d?%d):(%d%d)$")
|
||||||
|
if not h then return nil end
|
||||||
|
h, m = tonumber(h), tonumber(m)
|
||||||
|
if h > 23 or m > 59 then return nil end
|
||||||
|
return h * 60 + m
|
||||||
|
end
|
||||||
|
|
||||||
e = t:option(Value, "timeon", translate("Start time"))
|
e = t:option(Value, "timeon", translate("Start time"))
|
||||||
e.optional = false
|
e.optional = false
|
||||||
e.default = "00:00"
|
e.default = "00:00"
|
||||||
|
function e.validate(self, value, section)
|
||||||
|
local mins = time_to_min(value)
|
||||||
|
if not mins then
|
||||||
|
return nil, translate("Invalid time, expected HH:MM")
|
||||||
|
end
|
||||||
|
return string.format("%02d:%02d", math.floor(mins / 60), mins % 60)
|
||||||
|
end
|
||||||
|
|
||||||
e = t:option(Value, "timeoff", translate("End time"))
|
e = t:option(Value, "timeoff", translate("End time"))
|
||||||
e.optional=false
|
e.optional=false
|
||||||
e.default = "23:59"
|
e.default = "23:59"
|
||||||
|
function e.validate(self, value, section)
|
||||||
|
local mins = time_to_min(value)
|
||||||
|
if not mins then
|
||||||
|
return nil, translate("Invalid time, expected HH:MM")
|
||||||
|
end
|
||||||
|
-- 跨字段:起止时间不能相同(起止相同即无管控,易误配)
|
||||||
|
local on = self.map:formvalue("cbid.miaplus." .. section .. ".timeon")
|
||||||
|
local on_min = on and time_to_min(on)
|
||||||
|
if on_min ~= nil and on_min == mins then
|
||||||
|
return nil, translate("Start and end time must differ")
|
||||||
|
end
|
||||||
|
return string.format("%02d:%02d", math.floor(mins / 60), mins % 60)
|
||||||
|
end
|
||||||
|
|
||||||
e = t:option(Flag, "z1", translate("Mon"))
|
e = t:option(Flag, "z1", translate("Mon"))
|
||||||
e.rmempty = true
|
e.rmempty = true
|
||||||
|
|||||||
@@ -10,8 +10,19 @@ START=30
|
|||||||
|
|
||||||
CONFIG=miaplus
|
CONFIG=miaplus
|
||||||
|
|
||||||
|
# 非 procd 脚本:status 命令不在 rc.common 默认命令表里,需显式注册
|
||||||
|
EXTRA_COMMANDS="status"
|
||||||
|
EXTRA_HELP=" status Show service status (auto fw3/fw4)"
|
||||||
|
|
||||||
. /lib/functions.sh
|
. /lib/functions.sh
|
||||||
|
|
||||||
|
# 检测当前使用的防火墙框架:fw4 (nftables) 还是 fw3 (iptables)
|
||||||
|
# 注意:fw4 与 fw3 共用 /etc/config/firewall,不存在 /etc/config/fw4,
|
||||||
|
# 只能靠 fw4 / nft 命令是否存在来判定。
|
||||||
|
have_fw4() {
|
||||||
|
command -v fw4 >/dev/null 2>&1 && command -v nft >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
uci_export_section_name() {
|
uci_export_section_name() {
|
||||||
local ret=$(uci -n export $CONFIG | grep "config $1" | awk '{print $3}' | awk -F\' '{print $2}')
|
local ret=$(uci -n export $CONFIG | grep "config $1" | awk '{print $3}' | awk -F\' '{print $2}')
|
||||||
echo ${ret:=$2}
|
echo ${ret:=$2}
|
||||||
@@ -62,11 +73,23 @@ add_template_rule(){
|
|||||||
config_get enable "$section" enable 0
|
config_get enable "$section" enable 0
|
||||||
[ "$enable" = "1" ] && add_rule "$section"
|
[ "$enable" = "1" ] && add_rule "$section"
|
||||||
}
|
}
|
||||||
|
# 分发到具体实现
|
||||||
add_rule(){
|
add_rule(){
|
||||||
|
if have_fw4; then
|
||||||
|
add_rule_nft "$1"
|
||||||
|
else
|
||||||
|
add_rule_ipt "$1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# fw3 (iptables / ip6tables)
|
||||||
|
# =====================================================================
|
||||||
|
add_rule_ipt(){
|
||||||
ipv6enable=$(uci -q get miaplus.@basic[0].ipv6enable)
|
ipv6enable=$(uci -q get miaplus.@basic[0].ipv6enable)
|
||||||
local macaddrs=$(uci_get_mac_by_template $1 '')
|
local macaddrs=$(uci_get_mac_by_template $1 '')
|
||||||
if [ -z "$macaddrs" ]; then
|
if [ -z "$macaddrs" ]; then
|
||||||
exit 0
|
return 0
|
||||||
fi
|
fi
|
||||||
for macaddr in $macaddrs
|
for macaddr in $macaddrs
|
||||||
do
|
do
|
||||||
@@ -110,21 +133,9 @@ add_rule(){
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
del_rule(){
|
start_ipt(){
|
||||||
type=$1
|
|
||||||
blackMacAdd=$(iptables -t nat -L $type | grep -w RETURN | grep -w "MAC" | awk '{print $7}')
|
|
||||||
[ -n "$blackMacAdd" ] && {
|
|
||||||
for macaddrb in $blackMacAdd
|
|
||||||
do
|
|
||||||
iptables -t nat -D $type -m mac --mac-source $macaddrb -j RETURN
|
|
||||||
done
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
start(){
|
|
||||||
stop
|
|
||||||
enable=$(uci -q get miaplus.@basic[0].enable)
|
enable=$(uci -q get miaplus.@basic[0].enable)
|
||||||
[ "${enable:-0}" -eq 0 ] && exit 0
|
[ "${enable:-0}" -eq 0 ] && return 0
|
||||||
iptables -t filter -N MIAPLUS
|
iptables -t filter -N MIAPLUS
|
||||||
iptables -I INPUT -p udp --dport 53 -m comment --comment "Rule For Control" -j MIAPLUS
|
iptables -I INPUT -p udp --dport 53 -m comment --comment "Rule For Control" -j MIAPLUS
|
||||||
iptables -I INPUT -p tcp --dport 53 -m comment --comment "Rule For Control" -j MIAPLUS
|
iptables -I INPUT -p tcp --dport 53 -m comment --comment "Rule For Control" -j MIAPLUS
|
||||||
@@ -144,7 +155,7 @@ start(){
|
|||||||
fi
|
fi
|
||||||
add_rules
|
add_rules
|
||||||
}
|
}
|
||||||
status(){
|
status_ipt(){
|
||||||
if iptables -t filter -S MIAPLUS >/dev/null 2>&1; then
|
if iptables -t filter -S MIAPLUS >/dev/null 2>&1; then
|
||||||
echo "running"
|
echo "running"
|
||||||
return 0
|
return 0
|
||||||
@@ -153,7 +164,7 @@ status(){
|
|||||||
echo "not running"
|
echo "not running"
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
stop(){
|
stop_ipt(){
|
||||||
iptables -t filter -D FORWARD -m comment --comment "Rule For Control" -j MIAPLUS 2>/dev/null
|
iptables -t filter -D FORWARD -m comment --comment "Rule For Control" -j MIAPLUS 2>/dev/null
|
||||||
iptables -D INPUT -p udp --dport 53 -m comment --comment "Rule For Control" -j MIAPLUS 2>/dev/null
|
iptables -D INPUT -p udp --dport 53 -m comment --comment "Rule For Control" -j MIAPLUS 2>/dev/null
|
||||||
iptables -D INPUT -p tcp --dport 53 -m comment --comment "Rule For Control" -j MIAPLUS 2>/dev/null
|
iptables -D INPUT -p tcp --dport 53 -m comment --comment "Rule For Control" -j MIAPLUS 2>/dev/null
|
||||||
@@ -173,3 +184,260 @@ stop(){
|
|||||||
ip6tables -t filter -X MIAPLUS 2>/dev/null
|
ip6tables -t filter -X MIAPLUS 2>/dev/null
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# fw4 (nftables)
|
||||||
|
# 使用独立表 inet miaplus,避免与 fw4 自身的 nftables 规则冲突。
|
||||||
|
#
|
||||||
|
# table inet miaplus {
|
||||||
|
# chain input { type filter hook input priority 0; } # 仅匹配 53 端口
|
||||||
|
# chain prerouting{ type nat hook prerouting priority dstnat; } # DNS 重定向到本机
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# DNS 语义与 fw3 一致:只对发往 53 端口的流量做上网控制。
|
||||||
|
#
|
||||||
|
# NAT 竞争说明:本表 prerouting 链与 fw4 主表(inet fw4)的 PREROUTING/dstnat 同为
|
||||||
|
# priority -100(dstnat)。nf_tables 允许多表同优先级共存,但同一数据包的同一 hook
|
||||||
|
# 优先级内按表加载顺序处理,conntrack binding(redirect/redirect)先到先得。若 fw4
|
||||||
|
# 自身对 DNS(53) 也做了重定向/端口转发,两者可能互抢,现象以先加载者为准。
|
||||||
|
# 一般情况下 DNS 默认直通本机,二者不冲突;如遇规则不生效,优先排查此项。
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
# 构造 nft 的星期集合片段,如 "{ 1, 2 }" / "5" / 空(不限制)
|
||||||
|
# z1..z7 => 周一..周日;nft meta day: 0=Sun,1=Mon,...6=Sat => z1->1 ...
|
||||||
|
nft_dayterm() {
|
||||||
|
local days="" n=1 day
|
||||||
|
for z in "$1" "$2" "$3" "$4" "$5" "$6" "$7"; do
|
||||||
|
[ "$z" = "1" ] || { n=$((n+1)); continue; }
|
||||||
|
day=$(( n % 7 )) # z1..z7 -> 1,2,3,4,5,6,0
|
||||||
|
days="${days:+$days, }$day"
|
||||||
|
n=$((n+1))
|
||||||
|
done
|
||||||
|
if [ -z "$days" ]; then
|
||||||
|
echo ""
|
||||||
|
elif echo "$days" | grep -q ,; then
|
||||||
|
echo "{ $days }"
|
||||||
|
else
|
||||||
|
echo "$days"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 去掉前导零(防空串转成 "0"),规避 08/09 被当八进制的算术陷阱
|
||||||
|
_digits() {
|
||||||
|
echo "$1" | sed -e 's/^0*//' -e 's/^$/0/'
|
||||||
|
}
|
||||||
|
# HH:MM -> 分钟数(输入可为 H:M 或 HH:MM,非零填充)
|
||||||
|
_mins() {
|
||||||
|
local h m
|
||||||
|
h=$(_digits "${1%%:*}")
|
||||||
|
m=$(_digits "${1#*:}")
|
||||||
|
echo $(( ${h:-0} * 60 + ${m:-0} ))
|
||||||
|
}
|
||||||
|
# 任意 "H:M"/"HH:MM" -> 规范 "HH:MM"
|
||||||
|
_pad() {
|
||||||
|
local h m
|
||||||
|
h=$(_digits "${1%%:*}")
|
||||||
|
m=$(_digits "${1#*:}")
|
||||||
|
printf '%02d:%02d' "${h:-0}" "${m:-0}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 把一个 [t_on, t_off] 窗口(H:M/HH:MM)转成 nft 时间匹配表达式(行),
|
||||||
|
# 输出到全局变量 NFT_FRAGS。nft 的 meta hour 原生支持 "HH:MM"-"HH:MM"。
|
||||||
|
# 统一语义:结束分钟视为完整包含(闭区间含其末秒),故上界一律补 :59 —
|
||||||
|
# 非跨午夜 [t_on, t_off] -> "HH:MM"-"t_off:59";跨午夜拆 [t_on,23:59:59]+[00:00,t_off:59]。
|
||||||
|
nft_split_window() {
|
||||||
|
local t_on t_off
|
||||||
|
NFT_FRAGS=""
|
||||||
|
[ -n "$1" ] && [ -n "$2" ] || return 0
|
||||||
|
t_on=$(_pad "$1"); t_off=$(_pad "$2")
|
||||||
|
|
||||||
|
if [ "$(_mins "$t_on")" -le "$(_mins "$t_off")" ]; then
|
||||||
|
NFT_FRAGS="meta hour \"$t_on\"-\"$t_off:59\""
|
||||||
|
else
|
||||||
|
# 跨午夜:拆成 [on,23:59:59] + [00:00,off:59]
|
||||||
|
NFT_FRAGS="meta hour \"$t_on\"-\"23:59:59\"
|
||||||
|
meta hour \"00:00\"-\"$t_off:59\""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 统一执行 nft 规则命令并检查返回码。
|
||||||
|
# 失败时记日志、立即回滚删表并置全局 NFT_FAIL=1(供调用方中止后续写入),
|
||||||
|
# 使最终状态收敛为"完整规则表"或"空表",避免留下只 DROP 无 ACCEPT 的断网中间态。
|
||||||
|
nft_rule() {
|
||||||
|
# 已失败则短路,避免对已回滚删除的表继续逐条报错刷屏
|
||||||
|
[ -n "$NFT_FAIL" ] && return 1
|
||||||
|
"$@" 2>/dev/null || {
|
||||||
|
logger -t miaplus "nft rule failed: $*"
|
||||||
|
stop_nft
|
||||||
|
NFT_FAIL=1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 组合规则前缀:链决定是否只限 53 端口;ipv6enable=0 时限定 ipv4
|
||||||
|
nft_prefix() {
|
||||||
|
local chain=$1
|
||||||
|
local pref=""
|
||||||
|
[ "$chain" = "forward" ] || pref="meta l4proto { tcp, udp } th dport 53 "
|
||||||
|
if [ "$ipv6enable" != "1" ]; then
|
||||||
|
pref="meta nfproto ipv4 $pref"
|
||||||
|
fi
|
||||||
|
NFT_PREFIX="$pref"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 插入一条白名单 ACCEPT 规则(insert 到链首,兜底 DROP 在尾)
|
||||||
|
# 参数:MAC day_term [链名=当前]
|
||||||
|
nft_insert_accept() {
|
||||||
|
local mac=$1 day=$2 chain=${3:-input}
|
||||||
|
nft_prefix "$chain"
|
||||||
|
local cond
|
||||||
|
while IFS= read -r frag; do
|
||||||
|
[ -n "$frag" ] || continue
|
||||||
|
cond="$NFT_PREFIX ether saddr $mac $frag"
|
||||||
|
[ -n "$day" ] && cond="$cond meta day $day"
|
||||||
|
nft_rule nft insert rule inet miaplus $chain $cond accept
|
||||||
|
[ -n "$NFT_FAIL" ] && return 1
|
||||||
|
done <<EOF
|
||||||
|
$NFT_FRAGS
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# 插入一条兜底 DROP 规则(append 到链尾)
|
||||||
|
# 参数:MAC [链名=当前]
|
||||||
|
nft_add_drop() {
|
||||||
|
local mac=$1 chain=${2:-input}
|
||||||
|
nft_prefix "$chain"
|
||||||
|
nft_rule nft add rule inet miaplus $chain $NFT_PREFIX ether saddr $mac drop
|
||||||
|
}
|
||||||
|
|
||||||
|
# 需要写入的链列表:非严格=仅 input;严格=input+forward
|
||||||
|
nft_target_chains() {
|
||||||
|
if [ "${strict:-0}" -eq 1 ]; then
|
||||||
|
echo "input forward"
|
||||||
|
else
|
||||||
|
echo "input"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
add_rule_nft(){
|
||||||
|
local ipv6enable=$(uci -q get miaplus.@basic[0].ipv6enable)
|
||||||
|
local macaddrs=$(uci_get_mac_by_template $1 '')
|
||||||
|
if [ -z "$macaddrs" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local chains="$(nft_target_chains)"
|
||||||
|
local macaddr i enable timeon timeoff z1 z2 z3 z4 z5 z6 z7
|
||||||
|
|
||||||
|
# 先为每个绑定 MAC 加兜底 DROP(每条链一次,不重复)
|
||||||
|
for macaddr in $macaddrs; do
|
||||||
|
[ -n "$NFT_FAIL" ] && return 1
|
||||||
|
local ch
|
||||||
|
for ch in $chains; do
|
||||||
|
nft_add_drop "$macaddr" "$ch"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
# 再按时间段插入 ACCEPT 白名单
|
||||||
|
for i in $(seq 0 100); do
|
||||||
|
[ -n "$NFT_FAIL" ] && return 1
|
||||||
|
enable=$(uci_get_by_type $1 enable '' $i)
|
||||||
|
timeon=$(uci_get_by_type $1 timeon '' $i)
|
||||||
|
timeoff=$(uci_get_by_type $1 timeoff '' $i)
|
||||||
|
z1=$(uci_get_by_type $1 z1 '' $i)
|
||||||
|
z2=$(uci_get_by_type $1 z2 '' $i)
|
||||||
|
z3=$(uci_get_by_type $1 z3 '' $i)
|
||||||
|
z4=$(uci_get_by_type $1 z4 '' $i)
|
||||||
|
z5=$(uci_get_by_type $1 z5 '' $i)
|
||||||
|
z6=$(uci_get_by_type $1 z6 '' $i)
|
||||||
|
z7=$(uci_get_by_type $1 z7 '' $i)
|
||||||
|
|
||||||
|
[ -n "$enable" ] || break
|
||||||
|
[ -n "$timeon" ] || break
|
||||||
|
[ -n "$timeoff" ] || break
|
||||||
|
[ "$enable" = "1" ] || continue
|
||||||
|
|
||||||
|
local dayspec=$(nft_dayterm "$z1" "$z2" "$z3" "$z4" "$z5" "$z6" "$z7")
|
||||||
|
|
||||||
|
nft_split_window "$timeon" "$timeoff"
|
||||||
|
for macaddr in $macaddrs; do
|
||||||
|
local ch
|
||||||
|
for ch in $chains; do
|
||||||
|
nft_insert_accept "$macaddr" "$dayspec" "$ch"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
start_nft(){
|
||||||
|
enable=$(uci -q get miaplus.@basic[0].enable)
|
||||||
|
[ "${enable:-0}" -eq 0 ] && return 0
|
||||||
|
strict=$(uci -q get miaplus.@basic[0].strict)
|
||||||
|
local ipv6enable=$(uci -q get miaplus.@basic[0].ipv6enable)
|
||||||
|
|
||||||
|
NFT_FAIL=""
|
||||||
|
|
||||||
|
# 建表/建链也走 nft_rule,使最根本的"建表/建链失败"同样记日志并可回滚
|
||||||
|
nft_rule nft add table inet miaplus
|
||||||
|
nft_rule nft add chain inet miaplus input '{ type filter hook input priority 0; policy accept; }'
|
||||||
|
nft_rule nft add chain inet miaplus prerouting '{ type nat hook prerouting priority dstnat; policy accept; }'
|
||||||
|
# DNS 劫持遵循 ipv6enable:关闭时仅重定向 IPv4 的 53,与 filter 链/DROP 语义对齐
|
||||||
|
if [ "${ipv6enable:-0}" -eq 1 ]; then
|
||||||
|
nft_rule nft add rule inet miaplus prerouting meta l4proto { tcp, udp } th dport 53 redirect to :53
|
||||||
|
else
|
||||||
|
nft_rule nft add rule inet miaplus prerouting meta nfproto ipv4 meta l4proto { tcp, udp } th dport 53 redirect to :53
|
||||||
|
fi
|
||||||
|
[ -n "$NFT_FAIL" ] && return 1
|
||||||
|
|
||||||
|
# 严格模式:额外对 FORWARD 全流量做同样控制
|
||||||
|
if [ "${strict:-0}" -eq 1 ]; then
|
||||||
|
nft_rule nft add chain inet miaplus forward '{ type filter hook forward priority 0; policy accept; }'
|
||||||
|
[ -n "$NFT_FAIL" ] && return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
add_rules
|
||||||
|
|
||||||
|
# 任一规则写入失败:回滚整个表,避免留下只有 DROP、无 ACCEPT 白名单的断网状态
|
||||||
|
if [ -n "$NFT_FAIL" ]; then
|
||||||
|
logger -t miaplus "fw4 rules incomplete, rolling back"
|
||||||
|
stop_nft
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
status_nft(){
|
||||||
|
if nft list table inet miaplus >/dev/null 2>&1; then
|
||||||
|
echo "running"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "not running"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
stop_nft(){
|
||||||
|
nft delete table inet miaplus 2>/dev/null
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# 统一入口
|
||||||
|
# =====================================================================
|
||||||
|
start(){
|
||||||
|
stop
|
||||||
|
if have_fw4; then
|
||||||
|
start_nft
|
||||||
|
else
|
||||||
|
start_ipt
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
status(){
|
||||||
|
if have_fw4; then
|
||||||
|
status_nft
|
||||||
|
else
|
||||||
|
status_ipt
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
stop(){
|
||||||
|
if have_fw4; then
|
||||||
|
stop_nft
|
||||||
|
else
|
||||||
|
stop_ipt
|
||||||
|
fi
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@
|
|||||||
EEOF
|
EEOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# fw4 与 fw3 共用 /etc/config/firewall 的 config include 机制;fw4_compatible
|
||||||
|
# 让 fw4 在 reload 时执行用户脚本,fw3 忽略此未知 option(安全)
|
||||||
uci -q batch <<-EOF >/dev/null
|
uci -q batch <<-EOF >/dev/null
|
||||||
delete ucitrack.@miaplus[-1]
|
delete ucitrack.@miaplus[-1]
|
||||||
add ucitrack miaplus
|
add ucitrack miaplus
|
||||||
@@ -18,6 +20,7 @@ uci -q batch <<-EOF >/dev/null
|
|||||||
set firewall.miaplus.type=script
|
set firewall.miaplus.type=script
|
||||||
set firewall.miaplus.path=/etc/miaplus.include
|
set firewall.miaplus.path=/etc/miaplus.include
|
||||||
set firewall.miaplus.reload=1
|
set firewall.miaplus.reload=1
|
||||||
|
set firewall.miaplus.fw4_compatible=1
|
||||||
commit firewall
|
commit firewall
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
LUCI_TITLE:=baidudrive
|
LUCI_TITLE:=baidudrive
|
||||||
PKG_VERSION:=1.0.1
|
PKG_VERSION:=1.0.2
|
||||||
PKG_RELEASE:=2
|
PKG_RELEASE:=3
|
||||||
LUCI_DEPENDS:=+baidudrive +luci-compat
|
LUCI_DEPENDS:=+baidudrive +luci-compat
|
||||||
LUCI_MINIFY_CSS:=0
|
LUCI_MINIFY_CSS:=0
|
||||||
LUCI_MINIFY_JS:=0
|
LUCI_MINIFY_JS:=0
|
||||||
|
|||||||
@@ -8,11 +8,10 @@ include $(TOPDIR)/rules.mk
|
|||||||
LUCI_TITLE:=LuCI support for KSpeeder
|
LUCI_TITLE:=LuCI support for KSpeeder
|
||||||
LUCI_DEPENDS:=+istoreenhance
|
LUCI_DEPENDS:=+istoreenhance
|
||||||
LUCI_PKGARCH:=all
|
LUCI_PKGARCH:=all
|
||||||
PKG_VERSION:=0.6.6-r1
|
PKG_VERSION:=0.6.6-r2
|
||||||
# PKG_RELEASE MUST be empty for luci.mk
|
# PKG_RELEASE MUST be empty for luci.mk
|
||||||
PKG_RELEASE:=1
|
PKG_RELEASE:=2
|
||||||
|
|
||||||
include $(TOPDIR)/feeds/luci/luci.mk
|
include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
|
|
||||||
# call BuildPackage - OpenWrt buildroot signature
|
# call BuildPackage - OpenWrt buildroot signature
|
||||||
|
|
||||||
@@ -7,8 +7,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
LUCI_TITLE:=KaiPlus
|
LUCI_TITLE:=KaiPlus
|
||||||
PKG_VERSION:=1.0.0
|
PKG_VERSION:=1.0.1
|
||||||
PKG_RELEASE:=4
|
PKG_RELEASE:=5
|
||||||
LUCI_DEPENDS:=+kaiplus +luci-compat
|
LUCI_DEPENDS:=+kaiplus +luci-compat
|
||||||
LUCI_MINIFY_CSS:=0
|
LUCI_MINIFY_CSS:=0
|
||||||
LUCI_MINIFY_JS:=0
|
LUCI_MINIFY_JS:=0
|
||||||
|
|||||||
|
After Width: | Height: | Size: 254 KiB |
|
After Width: | Height: | Size: 459 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 219 KiB |
@@ -0,0 +1,247 @@
|
|||||||
|
# MPTCP — User Guide
|
||||||
|
|
||||||
|
`luci-app-mptcp` is the LuCI front end for the kernel's Multipath TCP stack
|
||||||
|
— the layer that actually bonds your WAN links together. It has seven pages
|
||||||
|
under **Network → MPTCP**: **MPTCP** (settings), **Diagnostics**,
|
||||||
|
**DSCP / Weight Routing**, **Bandwidth**, **Established connections**,
|
||||||
|
**MPTCP Fullmesh**, and **MPTCP monitoring**.
|
||||||
|
|
||||||
|
Screenshots below were taken on a test router (`v0.64-snapshot`, kernel
|
||||||
|
6.18, 3 WAN links). Peer/remote IPs that would reveal the test VPS were
|
||||||
|
replaced with a placeholder (`203.0.113.10`) before capture — everything
|
||||||
|
else is unmodified live output.
|
||||||
|
|
||||||
|
## MPTCP (settings)
|
||||||
|
|
||||||
|
```
|
||||||
|
https://<router-ip>/cgi-bin/luci/admin/network/mptcp/mptcp
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The form's contents adapt to your kernel version — the screenshot is from
|
||||||
|
a 6.18 kernel, which exposes the modern in-kernel/userspace path-manager
|
||||||
|
options; older kernels (<6) show a different, longer set of legacy fields
|
||||||
|
(scheduler-specific tuning like ndiffports/round-robin subflow counts,
|
||||||
|
`mptcp_version`, etc.) instead.
|
||||||
|
|
||||||
|
### GLOBALS
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| **Multipath TCP** | Read-only indicator of whether MPTCP is enabled network-wide. |
|
||||||
|
| **Multipath TCP checksum** | Enables MPTCP-level checksums (extra integrity check, minor overhead). |
|
||||||
|
| **Multipath TCP path-manager** | `default` or `fullmesh` (OMR's normal choice — creates subflows between every local/remote address pair). |
|
||||||
|
| **Multipath TCP scheduler** | Which subflow the kernel picks to send data on. `default`, or a BPF scheduler (`bpf_burst`, `bpf_red`, `bpf_first`, `bpf_rr`, plus any custom `.o` dropped into `/usr/share/bpf/scheduler` — auto-discovered and listed here). On kernels <6, classic in-tree schedulers (round-robin, redundant, BLEST, ECF) are offered instead. |
|
||||||
|
| **Congestion Control** | Populated live from `sysctl net.ipv4.tcp_available_congestion_control` — pick any congestion control algorithm your kernel has compiled in. Default is `cubic`; this bench uses `bbr`. |
|
||||||
|
| **Path Manager type** *(kernel ≥6 only)* | In-kernel (simpler, default) vs. userspace (delegates subflow decisions to `mptcpd`). |
|
||||||
|
| **Max subflows** | Cap on additional subflows per MPTCP connection — usually matches (or exceeds) your WAN count. |
|
||||||
|
| **Retranmission intervals** | How many MPTCP-level retransmit intervals with no traffic/no ack before a subflow is declared *stale*. Lower = faster active-backup switchover; higher = better utilization on lossy/high-BER links. |
|
||||||
|
| **Max add address** | Cap on `ADD_ADDR` suboptions accepted per connection. |
|
||||||
|
| **Control message timeout** | Resend delay for an unacknowledged `ADD_ADDR` message. |
|
||||||
|
| **Blackhole timeout** *(kernel ≥6.18)* | Initial cooldown before re-enabling MPTCP on a socket after a middlebox appears to be blackholing it; grows exponentially on repeat failures. |
|
||||||
|
| **Close timeout** *(kernel ≥6.18)* | Make-after-break grace period: how long a socket holds state after its last subflow drops, before moving to `TCP_CLOSE`. |
|
||||||
|
| **Control message timeout** *(second one, kernel ≥6.18)* | SYN+MP_CAPABLE retransmit count before falling back to plain TCP. |
|
||||||
|
|
||||||
|
When the userspace path manager is selected, extra fields appear
|
||||||
|
(`mptcpd` enable/disable, its plugin and path-manager `.so` lists —
|
||||||
|
auto-discovered from `/usr/lib/mptcpd` — and address announcement/
|
||||||
|
notification flags); not shown here since this bench uses the in-kernel
|
||||||
|
manager.
|
||||||
|
|
||||||
|
### Interfaces Settings
|
||||||
|
|
||||||
|
One block per real network interface (LAN + every WAN — VPN-internal
|
||||||
|
interfaces like `omrvpn`/`omr6in4` are filtered out here since they aren't
|
||||||
|
independent WAN paths):
|
||||||
|
|
||||||
|
- **Multipath TCP** — `disabled`, `enabled`, `master`, or `backup`.
|
||||||
|
Exactly one interface must be `master` (the interface MPTCP's initial
|
||||||
|
subflow uses); the rest are `enabled` (or `backup`, only cut in when
|
||||||
|
masters/enabled links fail).
|
||||||
|
- **Weight** — only used when a `*weight` scheduler/path-manager is
|
||||||
|
active; >100 makes a link more attractive, <100 less, up to 256.
|
||||||
|
|
||||||
|
## Diagnostics
|
||||||
|
|
||||||
|
```
|
||||||
|
https://<router-ip>/cgi-bin/luci/admin/network/mptcp/mptcp_diagnostics
|
||||||
|
```
|
||||||
|
|
||||||
|
Answers *why isn't MPTCP aggregating my WANs correctly?* without requiring
|
||||||
|
you to read raw `nstat`/`ip mptcp` output by hand. Combines the kernel's
|
||||||
|
MPTCP MIB counters, the live endpoint/subflow state, and a per-WAN sanity
|
||||||
|
check into a plain-English, color-coded issue list (fallback to plain TCP,
|
||||||
|
blackholed/stale subflows, resets, checksum errors, JOIN handshake
|
||||||
|
failures, dropped ADD_ADDR/RM_ADDR signalling), refreshed every 15 seconds.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
On this bench, the issue list actually fired: an orange banner flagging
|
||||||
|
*"2644 MPTCP-level reset/fastclose event(s) sent by this router (~119/hour
|
||||||
|
since boot)"* — with its own plain-English caveat underneath explaining
|
||||||
|
that a high count sent *by the router itself* (as opposed to received) is
|
||||||
|
usually just abortive closes from its own health-check/keepalive probes
|
||||||
|
opening a connection, sending one request, then closing it, and is only
|
||||||
|
worth escalating if paired with rising Blackhole/SubflowStale counters —
|
||||||
|
which is exactly the kind of self-diagnosis this page exists to save you
|
||||||
|
from having to work out by hand.
|
||||||
|
|
||||||
|
A **Per-WAN MPTCP endpoint status** table cross-checks every
|
||||||
|
multipath-enabled interface against `ip mptcp endpoint show`: a WAN can
|
||||||
|
look perfectly healthy in OpenMPTCProuter's Status page (IP, gateway, ping
|
||||||
|
all fine) and still never carry a subflow if its endpoint is missing —
|
||||||
|
this is usually the fastest way to catch that.
|
||||||
|
|
||||||
|
A **Live MPTCP subflows** table goes one level deeper than that: a live
|
||||||
|
read of every established subflow (preferring `omr-sockdiag`, a small tool
|
||||||
|
that queries the kernel directly over `NETLINK_SOCK_DIAG` — the same
|
||||||
|
INET_DIAG protocol `ss` uses internally — and falling back to parsing
|
||||||
|
`ss -tin` text if that tool isn't installed), one row per actual established
|
||||||
|
subflow, attributed to its WAN by matching the connection's local address
|
||||||
|
against the endpoint list above. Each row shows the local/remote
|
||||||
|
address:port, whether the kernel is
|
||||||
|
currently using it as an active or backup path, congestion window, RTT
|
||||||
|
(smoothed/variance), retransmissions (current/total), and pacing/delivery
|
||||||
|
rate — the same live TCP-level numbers `ss -i` would show for that
|
||||||
|
connection, without needing to run it by hand over SSH. A WAN with a
|
||||||
|
correctly-registered endpoint but no row here means the path manager
|
||||||
|
just hasn't opened a subflow on it yet (or it's currently down/unreachable),
|
||||||
|
which the endpoint table alone can't tell you.
|
||||||
|
|
||||||
|
A **Kernel MPTCP
|
||||||
|
configuration** block shows the live scheduler/path-manager/checksum/
|
||||||
|
timeout values straight from `/proc/sys/net/mptcp/*`, and a sortable
|
||||||
|
counters table plus collapsible raw `multipath -k/-f/-c` sections are
|
||||||
|
available for anyone who wants the unprocessed data (the raw `-k`/`-f`/`-c`
|
||||||
|
sections mirror **MPTCP monitoring**'s kernel-info line, **MPTCP
|
||||||
|
Fullmesh**, and **Established connections** below).
|
||||||
|
|
||||||
|
The counters table is genuinely cumulative since boot, unlike **MPTCP
|
||||||
|
monitoring**'s raw `multipath -m` output below: that wraps `nstat -z`,
|
||||||
|
which (without `-a`) reports the delta since the *previous* `nstat` call by
|
||||||
|
anyone on the router — confirmed live, two calls a couple seconds apart
|
||||||
|
came back ~0. A page auto-polling every 15s through that would show
|
||||||
|
"since last poll" numbers while looking like a lifetime total, and would
|
||||||
|
race with anything else on the box that also calls `nstat`. Diagnostics
|
||||||
|
reads the kernel's own procfs SNMP-style counters directly instead
|
||||||
|
(`/proc/net/netstat`'s `MPTcpExt:` block, or `/proc/net/mptcp_net/snmp` on
|
||||||
|
older kernels), which has no such history-file/shared-state gotcha.
|
||||||
|
|
||||||
|
Backed by a new method on the same `luci.mptcp` rpcd object as the other
|
||||||
|
pages: `ubus call luci.mptcp diagnose '{}'` (no arguments).
|
||||||
|
|
||||||
|
## DSCP / Weight Routing
|
||||||
|
|
||||||
|
```
|
||||||
|
https://<router-ip>/cgi-bin/luci/admin/network/mptcp/mptcp_dscp_routing
|
||||||
|
```
|
||||||
|
|
||||||
|
A per-WAN pin/weight table shared between two independent consumers —
|
||||||
|
MPTCP's `bpf_dscp`/`bpf_weight*` schedulers (see the MPTCP settings page
|
||||||
|
above) and `mqvpn`'s equivalent scheduler modes — so both can be
|
||||||
|
configured from one place instead of duplicating the same WAN list twice.
|
||||||
|
This page only edits the pins/weights themselves; which scheduler is
|
||||||
|
actually *active* is still chosen on the MPTCP or MQVPN settings pages.
|
||||||
|
|
||||||
|
Its content is entirely conditional: it shows the **DSCP class pins**
|
||||||
|
table only if MPTCP's scheduler is `bpf_dscp` *or* mqvpn is enabled at
|
||||||
|
all (mqvpn pushes its DSCP mask unconditionally whenever it's on, unlike
|
||||||
|
weight below), and the **WAN weight** section only if MPTCP's scheduler
|
||||||
|
is one of the `bpf_weight`/`bpf_weight_rr`/`bpf_burstweight` family *or*
|
||||||
|
mqvpn's scheduler is `wrtt`/`wrr`. With neither condition met, the page
|
||||||
|
just says so and renders nothing else.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
On this bench, MPTCP's scheduler is `blest` (neither DSCP nor weight) but
|
||||||
|
mqvpn is enabled, so only the DSCP half shows:
|
||||||
|
|
||||||
|
| Section | Fields |
|
||||||
|
|---|---|
|
||||||
|
| **Gateway sync** | **Mirror DSCP/weight pins to gateway** — also push each pin/weight to the VPS so it holds for download traffic too (the gateway's own send direction), not just what the router sends. Disabling it only stops *future* syncs; pins already pushed stay pushed. Its *saved* value (not the live checkbox) decides whether the Download column below even renders — toggling it needs Save & Apply and a reload before that column appears/disappears. |
|
||||||
|
| **DSCP class pins** | One row per DSCP class → **Upload interface** (router → internet: pins the class to a WAN for `bpf_dscp`'s local map, and for mqvpn announces it on that path's DSCP mask via the control API) and, only while Gateway sync is on, **Download interface** (gateway → router; `bpf_dscp`-only, pins the VPS's own send-side choice — leave empty to just mirror the Upload interface instead). mqvpn has no separate download pin at all — its downlink always mirrors upload. |
|
||||||
|
| **WAN weight** *(not shown here — no weight scheduler active)* | One row per multipath-enabled WAN, a single **Weight** value (default 100, max 256) used by both upload and download, for both MPTCP's `*weight` schedulers and mqvpn's Weighted RTT/Weighted Round Robin. |
|
||||||
|
|
||||||
|
## Bandwidth
|
||||||
|
|
||||||
|
```
|
||||||
|
https://<router-ip>/cgi-bin/luci/admin/network/mptcp/bandwidth
|
||||||
|
```
|
||||||
|
|
||||||
|
Live, polled-every-second SVG traffic graphs. A tab per multipath-enabled
|
||||||
|
interface, plus an **All interfaces** overview.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The combined view overlays every WAN's inbound (and, separately, outbound)
|
||||||
|
throughput plus a bold total line, with a live current/average/peak table
|
||||||
|
underneath per interface and for the total.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Selecting a WAN tab (e.g. `wan1`) narrows this to that link alone —
|
||||||
|
inbound and outbound charts with their own current/average/peak stats,
|
||||||
|
useful for judging one link's real-world throughput independent of the
|
||||||
|
others.
|
||||||
|
|
||||||
|
## Established connections
|
||||||
|
|
||||||
|
```
|
||||||
|
https://<router-ip>/cgi-bin/luci/admin/network/mptcp/mptcp_connections
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Raw, auto-refreshing (every 10s) output of `multipath -c` — every ESTAB
|
||||||
|
MPTCP connection with local and peer address:port. Useful for confirming
|
||||||
|
traffic is actually using multiple subflows/paths, or for spotting
|
||||||
|
connections stuck on a single WAN.
|
||||||
|
|
||||||
|
## MPTCP Fullmesh
|
||||||
|
|
||||||
|
```
|
||||||
|
https://<router-ip>/cgi-bin/luci/admin/network/mptcp/mptcp_fullmesh
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Raw output of `multipath -f`: the fullmesh path-manager's current subflow
|
||||||
|
map — one line per local address, its subflow ID, and which device it
|
||||||
|
rides on. This is the direct evidence that the fullmesh path-manager
|
||||||
|
picked up every WAN address correctly; a WAN missing from this list isn't
|
||||||
|
being used as an MPTCP subflow.
|
||||||
|
|
||||||
|
## MPTCP monitoring
|
||||||
|
|
||||||
|
```
|
||||||
|
https://<router-ip>/cgi-bin/luci/admin/network/mptcp/mptcp_monitor
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Raw output of `multipath -m`: the kernel's full `MPTcpExt*` counter dump
|
||||||
|
(from `nstat`/`netstat -s`-style MIB counters) — join attempts/failures,
|
||||||
|
retransmissions, checksum errors, address add/remove events, fallbacks to
|
||||||
|
plain TCP, etc. All zero on a healthy, idle test link; non-zero values in
|
||||||
|
categories like `MPTcpExtMPCapableFallbackSYNACK` or `MPTcpExtMPFailTx`
|
||||||
|
are what you'd check first when MPTCP negotiation is suspected to be
|
||||||
|
failing (e.g. a middlebox stripping MPTCP TCP options).
|
||||||
|
|
||||||
|
## Not reachable from the menu: MPTCP Support Check
|
||||||
|
|
||||||
|
The package also ships `mptcp/mptcp_check.js` — a page that runs a
|
||||||
|
`tracebox`-based trace toward the configured VPS over a chosen interface,
|
||||||
|
to show whether MPTCP options survive the path (look for
|
||||||
|
`TCPOptionMPTCPCapable [...] Sender's Key` in the output: present means
|
||||||
|
supported, a leading `-` means something on the path is stripping it).
|
||||||
|
It calls the same `luci.mptcp` rpcd backend as the other pages
|
||||||
|
(`mptcp_check_trace`, wrapping `tracebox -s omr-mptcp-trace.lua`).
|
||||||
|
|
||||||
|
However, `luci-app-mptcp.json` has no menu entry for it, and LuCI's
|
||||||
|
dispatcher falls back to the MPTCP settings page for its URL
|
||||||
|
(`admin/network/mptcp/mptcp_check`) rather than routing to it — so as
|
||||||
|
shipped, this page isn't actually reachable through the web UI. Worth
|
||||||
|
knowing if you're debugging why MPTCP isn't negotiating: the check logic
|
||||||
|
exists and works via the same rpcd method, it just currently has no way in
|
||||||
|
from the menu.
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
|
|
||||||
#
|
|
||||||
# This is free software, licensed under the Apache License, Version 2.0 .
|
|
||||||
#
|
|
||||||
|
|
||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
LUCI_TITLE:=LuCI support for OAF
|
LUCI_TITLE:=LuCI app for OAF
|
||||||
LUCI_PKGARCH:=all
|
LUCI_PKGARCH:=all
|
||||||
LUCI_DEPENDS:=+appfilter +kmod-oaf +luci-compat
|
LUCI_DEPENDS:=+appfilter +kmod-oaf +luci-compat +luci-lib-jsonc
|
||||||
|
|
||||||
PKG_NAME:=luci-app-oaf
|
PKG_NAME:=luci-app-oaf
|
||||||
PKG_VERSION:=6.1.4
|
PKG_VERSION:=7.0
|
||||||
PKG_RELEASE:=3
|
PKG_RELEASE:=4
|
||||||
|
|
||||||
include $(TOPDIR)/feeds/luci/luci.mk
|
include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
|
|
||||||
# call BuildPackage - OpenWrt buildroot signature
|
# call BuildPackage - OpenWrt buildroot signature
|
||||||
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |