🦄 Sync 2026-08-26 20:42:53

This commit is contained in:
github-actions[bot] committed 2026-08-26 20:42:53 +08:00
1 parent 6ca34f4942
commit c0ca32960d
487 files changed
+55081 -19590

No files matched your search

+2 -2
View File
@@ -10,8 +10,8 @@ include $(TOPDIR)/rules.mk
PKG_ARCH_BAIDUDRIVE:=$(ARCH)
PKG_NAME:=baidudrive
PKG_VERSION:=kaiplus-runtime-v1.0.5
PKG_RELEASE:=15
PKG_VERSION:=dockermanager-runtime-v0.1.1
PKG_RELEASE:=16
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_HASH:=skip
+67
View File
@@ -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"]
}
+6
View File
@@ -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/'
+53
View File
@@ -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"
}
+10
View File
@@ -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

+23 -3
View File
@@ -11,8 +11,8 @@ PKG_ARCH_ISTOREENHANCE:=$(ARCH)
PKG_NAME:=istoreenhance
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
PKG_SOURCE_DATE:=0.7.15
PKG_RELEASE:=5
PKG_SOURCE_DATE:=0.7.16
PKG_RELEASE:=6
ARCH_HEXCODE:=
ifeq ($(ARCH),x86_64)
ARCH_HEXCODE=8664
@@ -29,6 +29,7 @@ PKG_SOURCE_URL:=https://github.com/kspeeder/docker_kspeeder/releases/download/v$
PKG_HASH:=skip
PKG_BUILD_DIR:=$(BUILD_DIR)/iStoreEnhance-binary-$(PKG_SOURCE_DATE)
KSPEEDER_WEB_ROOT:=$(PKG_BUILD_DIR)/wwwroot
PKG_BUILD_PARALLEL:=1
PKG_USE_MIPS16:=0
@@ -61,6 +62,16 @@ if [ -z "$${IPKG_INSTROOT}" ]; then
fi
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
endef
@@ -68,11 +79,20 @@ define Build/Compile
endef
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_CONF) ./files/istoreenhance.config $(1)/etc/config/istoreenhance
$(INSTALL_BIN) ./files/istoreenhance.init $(1)/etc/init.d/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
$(eval $(call BuildPackage,$(PKG_NAME)))
+41
View File
@@ -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"]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

+260
View File
@@ -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) => ({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
})[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 };
+32
View File
@@ -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>
+2 -2
View File
@@ -9,8 +9,8 @@ include $(TOPDIR)/rules.mk
PKG_ARCH_kaiplus:=$(ARCH)
PKG_NAME:=kaiplus
PKG_VERSION:=kaiplus-runtime-v1.0.5
PKG_RELEASE:=5
PKG_VERSION:=dockermanager-runtime-v0.1.1
PKG_RELEASE:=6
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_HASH:=skip
+2 -2
View File
@@ -12,7 +12,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
PKG_NAME:=linkease-common-bin
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
PKG_SOURCE_DATE:=1.7.5
PKG_RELEASE:=14
PKG_RELEASE:=15
ARCH_HEXCODE:=
ifeq ($(ARCH),x86_64)
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
ARCH_HEXCODE=1b0c
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_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)
+2 -2
View File
@@ -12,7 +12,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
PKG_NAME:=linkease
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
PKG_SOURCE_DATE:=1.7.5
PKG_RELEASE:=17
PKG_RELEASE:=18
ARCH_HEXCODE:=
ifeq ($(ARCH),x86_64)
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
ARCH_HEXCODE=1b0c
endif
PKG_SOURCE_VERSION:=2036fce819b5a44574d07cca9927eae73da43efc
PKG_SOURCE_VERSION:=b0d9136d243ac1a5c5290190ffa6f66265aa7fba
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_BUILD_DIR:=$(BUILD_DIR)/linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE)
+3 -3
View File
@@ -10,8 +10,8 @@ PKG_ARCH_LINKEASE:=$(ARCH)
PKG_NAME:=linkeasefull
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
PKG_SOURCE_DATE:=3.0.12
PKG_RELEASE:=16
PKG_SOURCE_DATE:=3.0.13
PKG_RELEASE:=17
ARCH_HEXCODE:=
ifeq ($(ARCH),x86_64)
@@ -24,7 +24,7 @@ LINKEASE_RUNTIME_ARCH:=arm64
PKG_HASH:=skip
endif
PKG_SOURCE_VERSION:=2036fce819b5a44574d07cca9927eae73da43efc
PKG_SOURCE_VERSION:=b0d9136d243ac1a5c5290190ffa6f66265aa7fba
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_BUILD_DIR:=$(BUILD_DIR)/linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH)
-8
View File
@@ -52,10 +52,6 @@ is_persistent_data_root_parent() {
return 0
}
linkease_enabled() {
[ "$(uci -q get linkease.@linkease[0].enabled 2>/dev/null)" = "1" ]
}
read_config() {
section="$1"
config_get_bool enabled "$section" enabled 1
@@ -80,10 +76,6 @@ start_service() {
config_foreach read_config linkeasefull
[ "$enabled" = "1" ] || 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"
if [ -n "$data_root_parent" ]; then
+1 -1
View File
@@ -12,7 +12,7 @@ LUCI_PKGARCH:=all
PKG_NAME:=luci-app-accesscontrol-plus
PKG_VERSION:=1
PKG_RELEASE:=2
PKG_RELEASE:=3
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
@@ -15,7 +15,8 @@ end
function act_status()
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.write_json(e)
end
@@ -16,13 +16,42 @@ e = t:option(Flag, "enable", translate("Enabled"))
e.rmempty = false
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.optional = false
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.optional=false
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.rmempty = true
@@ -10,8 +10,19 @@ START=30
CONFIG=miaplus
# 非 procd 脚本:status 命令不在 rc.common 默认命令表里,需显式注册
EXTRA_COMMANDS="status"
EXTRA_HELP=" status Show service status (auto fw3/fw4)"
. /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() {
local ret=$(uci -n export $CONFIG | grep "config $1" | awk '{print $3}' | awk -F\' '{print $2}')
echo ${ret:=$2}
@@ -62,11 +73,23 @@ add_template_rule(){
config_get enable "$section" enable 0
[ "$enable" = "1" ] && add_rule "$section"
}
# 分发到具体实现
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)
local macaddrs=$(uci_get_mac_by_template $1 '')
if [ -z "$macaddrs" ]; then
exit 0
return 0
fi
for macaddr in $macaddrs
do
@@ -110,21 +133,9 @@ add_rule(){
done
}
del_rule(){
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
start_ipt(){
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 -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
@@ -144,7 +155,7 @@ start(){
fi
add_rules
}
status(){
status_ipt(){
if iptables -t filter -S MIAPLUS >/dev/null 2>&1; then
echo "running"
return 0
@@ -153,7 +164,7 @@ status(){
echo "not running"
return 1
}
stop(){
stop_ipt(){
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 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
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
}
# fw4 与 fw3 共用 /etc/config/firewall 的 config include 机制;fw4_compatible
# 让 fw4 在 reload 时执行用户脚本,fw3 忽略此未知 option(安全)
uci -q batch <<-EOF >/dev/null
delete ucitrack.@miaplus[-1]
add ucitrack miaplus
@@ -18,6 +20,7 @@ uci -q batch <<-EOF >/dev/null
set firewall.miaplus.type=script
set firewall.miaplus.path=/etc/miaplus.include
set firewall.miaplus.reload=1
set firewall.miaplus.fw4_compatible=1
commit firewall
EOF
+2 -2
View File
@@ -7,8 +7,8 @@
include $(TOPDIR)/rules.mk
LUCI_TITLE:=baidudrive
PKG_VERSION:=1.0.1
PKG_RELEASE:=2
PKG_VERSION:=1.0.2
PKG_RELEASE:=3
LUCI_DEPENDS:=+baidudrive +luci-compat
LUCI_MINIFY_CSS:=0
LUCI_MINIFY_JS:=0
+2 -3
View File
@@ -8,11 +8,10 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI support for KSpeeder
LUCI_DEPENDS:=+istoreenhance
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:=1
PKG_RELEASE:=2
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
+2 -2
View File
@@ -7,8 +7,8 @@
include $(TOPDIR)/rules.mk
LUCI_TITLE:=KaiPlus
PKG_VERSION:=1.0.0
PKG_RELEASE:=4
PKG_VERSION:=1.0.1
PKG_RELEASE:=5
LUCI_DEPENDS:=+kaiplus +luci-compat
LUCI_MINIFY_CSS:=0
LUCI_MINIFY_JS:=0
Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

+247
View File
@@ -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
```
![MPTCP settings](images/01-mptcp-settings.png)
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.
![MPTCP Diagnostics](images/01a-diagnostics.png)
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.
![DSCP / Weight Routing](images/01b-dscp-weight-routing.png)
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.
![Bandwidth — all interfaces](images/02-bandwidth-all.png)
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.
![Bandwidth — single interface](images/03-bandwidth-iface.png)
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
```
![Established connections](images/04-established-connections.png)
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
```
![MPTCP Fullmesh](images/05-fullmesh.png)
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
```
![MPTCP monitoring](images/06-monitoring.png)
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.
+7 -9
View File
@@ -1,15 +1,13 @@
#
# This is free software, licensed under the Apache License, Version 2.0 .
#
include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI support for OAF
LUCI_TITLE:=LuCI app for OAF
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_VERSION:=6.1.4
PKG_RELEASE:=3
PKG_VERSION:=7.0
PKG_RELEASE:=4
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Loaded 100 of 487 files, more files were not shown because too many files have changed in this diff. Show more