mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-11 02:44:57 +08:00
🎁 Sync 2026-08-13 00:16:16
This commit is contained in:
+2
-2
@@ -10,8 +10,8 @@ include $(TOPDIR)/rules.mk
|
||||
PKG_ARCH_BAIDUDRIVE:=$(ARCH)
|
||||
|
||||
PKG_NAME:=baidudrive
|
||||
PKG_VERSION:=linkeasefull-runtime-v3.0.6
|
||||
PKG_RELEASE:=9
|
||||
PKG_VERSION:=linkeasefull-runtime-v3.0.8
|
||||
PKG_RELEASE:=10
|
||||
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
|
||||
|
||||
@@ -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:=5
|
||||
PKG_RELEASE:=6
|
||||
|
||||
ARCH_HEXCODE:=
|
||||
ifeq ($(ARCH),x86_64)
|
||||
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
|
||||
ARCH_HEXCODE=1b0c
|
||||
endif
|
||||
|
||||
PKG_SOURCE_VERSION:=9f351503d4320bb3767c300bc677a85c5685c9ba
|
||||
PKG_SOURCE_VERSION:=78c1b63d4be857054a8e6e37bc22eb235e3f151e
|
||||
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
@@ -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:=8
|
||||
PKG_RELEASE:=9
|
||||
|
||||
ARCH_HEXCODE:=
|
||||
ifeq ($(ARCH),x86_64)
|
||||
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
|
||||
ARCH_HEXCODE=1b0c
|
||||
endif
|
||||
|
||||
PKG_SOURCE_VERSION:=9f351503d4320bb3767c300bc677a85c5685c9ba
|
||||
PKG_SOURCE_VERSION:=78c1b63d4be857054a8e6e37bc22eb235e3f151e
|
||||
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)
|
||||
|
||||
@@ -11,7 +11,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
|
||||
PKG_NAME:=linkeasefull
|
||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||
PKG_SOURCE_DATE:=3.0.5
|
||||
PKG_RELEASE:=7
|
||||
PKG_RELEASE:=8
|
||||
|
||||
ARCH_HEXCODE:=
|
||||
ifeq ($(ARCH),x86_64)
|
||||
@@ -24,7 +24,7 @@ LINKEASE_RUNTIME_ARCH:=arm64
|
||||
PKG_HASH:=skip
|
||||
endif
|
||||
|
||||
PKG_SOURCE_VERSION:=9f351503d4320bb3767c300bc677a85c5685c9ba
|
||||
PKG_SOURCE_VERSION:=78c1b63d4be857054a8e6e37bc22eb235e3f151e
|
||||
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)
|
||||
|
||||
@@ -10,8 +10,8 @@ LUCI_DEPENDS:=+luci-base +luci-lib-jsonc +curl +bandix
|
||||
|
||||
PKG_MAINTAINER:=timsaya
|
||||
|
||||
PKG_VERSION:=0.12.8
|
||||
PKG_RELEASE:=6
|
||||
PKG_VERSION:=0.12.9
|
||||
PKG_RELEASE:=7
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 149 KiB |
@@ -9,6 +9,11 @@
|
||||
var BANDIX_COLOR_UPLOAD = '#f97316'; // 橙色 - 上传/上行
|
||||
var BANDIX_COLOR_DOWNLOAD = '#06b6d4'; // 青色 - 下载/下行
|
||||
|
||||
// 远程广告配置
|
||||
var BANDIX_AD_CONFIG_URL = 'https://raw.githubusercontent.com/timsaya/bandix-ads/main/ads.json';
|
||||
var BANDIX_AD_DEFAULT_REFRESH_INTERVAL = 3600;
|
||||
var BANDIX_AD_RETRY_INTERVAL = 900;
|
||||
|
||||
// 悬浮框背景色定义
|
||||
var BANDIX_TOOLTIP_BG_OPENWRT2020_LIGHT = '#ffffff'; // OpenWrt 2020 浅色主题悬浮框背景
|
||||
var BANDIX_TOOLTIP_BG_OPENWRT2020_DARK = '#2a2a2a'; // OpenWrt 2020 深色主题悬浮框背景
|
||||
@@ -556,6 +561,117 @@ return view.extend({
|
||||
background-color: rgba(239, 68, 68, 0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.bandix-ad-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--bandix-ad-count, 6), minmax(112px, 1fr));
|
||||
gap: 8px;
|
||||
margin: 12px 0;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.bandix-ad-slot {
|
||||
flex: 1 1 0;
|
||||
min-width: 112px;
|
||||
padding: 0;
|
||||
aspect-ratio: 3 / 1;
|
||||
border: 1px solid rgba(107, 114, 128, 0.35);
|
||||
border-radius: 6px;
|
||||
background: linear-gradient(135deg, rgba(225, 37, 27, 0.08), rgba(107, 114, 128, 0.04));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: inherit;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
transition: border-color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.bandix-ad-slot:not(.bandix-ad-disabled):hover,
|
||||
.bandix-ad-slot:not(.bandix-ad-disabled):focus-visible {
|
||||
border-color: rgba(225, 37, 27, 0.75);
|
||||
box-shadow: 0 4px 12px rgba(225, 37, 27, 0.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.bandix-ad-disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.bandix-ad-label {
|
||||
align-self: flex-start;
|
||||
padding: 1px 5px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
background: rgba(225, 37, 27, 0.88);
|
||||
color: #ffffff;
|
||||
font-size: 0.5625rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.bandix-ad-copy {
|
||||
align-self: stretch;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
padding: 6px 4px 6px 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
gap: 3px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bandix-ad-title {
|
||||
width: 100%;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.bandix-ad-media {
|
||||
align-self: stretch;
|
||||
flex: 0 0 33.333333%;
|
||||
min-width: 0;
|
||||
aspect-ratio: 1 / 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
|
||||
.bandix-ad-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.bandix-ad-placeholder {
|
||||
flex: 0 0 33.333333%;
|
||||
aspect-ratio: 1 / 1;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
color: #e1251b;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
/* 移动端隐藏版本信息和更新徽章 */
|
||||
@@ -563,6 +679,26 @@ return view.extend({
|
||||
.bandix-version-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bandix-ad-row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.bandix-ad-slot {
|
||||
flex: 0 0 180px;
|
||||
min-width: 180px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.bandix-ad-copy {
|
||||
padding-left: 7px;
|
||||
}
|
||||
|
||||
.bandix-ad-title {
|
||||
font-size: 0.625rem;
|
||||
}
|
||||
}
|
||||
|
||||
.device-mode-group {
|
||||
@@ -2820,6 +2956,25 @@ return view.extend({
|
||||
])
|
||||
]),
|
||||
|
||||
// 广告数据从公开仓库动态加载
|
||||
E('div', {
|
||||
'class': 'bandix-ad-row',
|
||||
'id': 'bandix-ad-row',
|
||||
'aria-label': _('Advertising spaces')
|
||||
}, [1, 2, 3, 4, 5, 6].map(function (index) {
|
||||
return E('div', {
|
||||
'class': 'bandix-ad-slot bandix-ad-disabled',
|
||||
'data-ad-slot': String(index),
|
||||
'aria-disabled': 'true'
|
||||
}, [
|
||||
E('div', { 'class': 'bandix-ad-copy' }, [
|
||||
E('span', { 'class': 'bandix-ad-label' }, _('AD')),
|
||||
E('span', { 'class': 'bandix-ad-title' }, _('Advertising space') + ' ' + index)
|
||||
]),
|
||||
E('span', { 'class': 'bandix-ad-placeholder' }, _('Advertising space'))
|
||||
]);
|
||||
})),
|
||||
|
||||
// 警告提示(包含在线设备数)
|
||||
E('div', {
|
||||
'class': 'bandix-alert' + (getThemeType() === 'wide' ? ' wide-theme' : '')
|
||||
@@ -2975,7 +3130,8 @@ return view.extend({
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'usage-ranking-query-presets' }, [
|
||||
E('button', { 'class': 'cbi-button cbi-button-neutral', 'data-preset': 'today' }, _('Today')),
|
||||
E('button', { 'class': 'cbi-button cbi-button-positive', 'data-preset': 'today' }, _('Today')),
|
||||
E('button', { 'class': 'cbi-button cbi-button-neutral', 'data-preset': 'yesterday' }, _('Yesterday')),
|
||||
E('button', { 'class': 'cbi-button cbi-button-neutral', 'data-preset': 'thisweek' }, _('This Week')),
|
||||
E('button', { 'class': 'cbi-button cbi-button-neutral', 'data-preset': 'lastweek' }, _('Last Week')),
|
||||
E('button', { 'class': 'cbi-button cbi-button-neutral', 'data-preset': 'thismonth' }, _('This Month')),
|
||||
@@ -2983,7 +3139,7 @@ return view.extend({
|
||||
E('button', { 'class': 'cbi-button cbi-button-neutral', 'data-preset': '7days' }, _('Last 7 Days')),
|
||||
E('button', { 'class': 'cbi-button cbi-button-neutral', 'data-preset': '30days' }, _('Last 30 Days')),
|
||||
E('button', { 'class': 'cbi-button cbi-button-neutral', 'data-preset': '90days' }, _('Last 90 Days')),
|
||||
E('button', { 'class': 'cbi-button cbi-button-positive', 'data-preset': '1year' }, _('Last Year'))
|
||||
E('button', { 'class': 'cbi-button cbi-button-neutral', 'data-preset': '1year' }, _('Last Year'))
|
||||
]),
|
||||
E('div', { 'class': 'usage-ranking-timeline', 'id': 'usage-ranking-timeline' }, [
|
||||
E('div', { 'class': 'usage-ranking-timeline-range', 'id': 'usage-ranking-timeline-range' })
|
||||
@@ -3047,7 +3203,8 @@ return view.extend({
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'usage-ranking-query-presets' }, [
|
||||
E('button', { 'class': 'cbi-button cbi-button-neutral', 'data-preset': 'today' }, _('Today')),
|
||||
E('button', { 'class': 'cbi-button cbi-button-positive', 'data-preset': 'today' }, _('Today')),
|
||||
E('button', { 'class': 'cbi-button cbi-button-neutral', 'data-preset': 'yesterday' }, _('Yesterday')),
|
||||
E('button', { 'class': 'cbi-button cbi-button-neutral', 'data-preset': 'thisweek' }, _('This Week')),
|
||||
E('button', { 'class': 'cbi-button cbi-button-neutral', 'data-preset': 'lastweek' }, _('Last Week')),
|
||||
E('button', { 'class': 'cbi-button cbi-button-neutral', 'data-preset': 'thismonth' }, _('This Month')),
|
||||
@@ -3055,7 +3212,7 @@ return view.extend({
|
||||
E('button', { 'class': 'cbi-button cbi-button-neutral', 'data-preset': '7days' }, _('Last 7 Days')),
|
||||
E('button', { 'class': 'cbi-button cbi-button-neutral', 'data-preset': '30days' }, _('Last 30 Days')),
|
||||
E('button', { 'class': 'cbi-button cbi-button-neutral', 'data-preset': '90days' }, _('Last 90 Days')),
|
||||
E('button', { 'class': 'cbi-button cbi-button-positive', 'data-preset': '1year' }, _('Last Year'))
|
||||
E('button', { 'class': 'cbi-button cbi-button-neutral', 'data-preset': '1year' }, _('Last Year'))
|
||||
]),
|
||||
E('div', { 'class': 'usage-ranking-timeline', 'id': 'traffic-increments-timeline' }, [
|
||||
E('div', { 'class': 'usage-ranking-timeline-range', 'id': 'traffic-increments-timeline-range' })
|
||||
@@ -7504,7 +7661,6 @@ return view.extend({
|
||||
updateDeviceData();
|
||||
refreshWhitelistStatus();
|
||||
fetchAllScheduleRules();
|
||||
updateTrafficStatistics();
|
||||
|
||||
// 初始化时间范围查询功能
|
||||
setTimeout(function () {
|
||||
@@ -7631,6 +7787,12 @@ return view.extend({
|
||||
startDate = new Date(today);
|
||||
endDate = new Date(today);
|
||||
break;
|
||||
case 'yesterday':
|
||||
var yesterdayDate = new Date(today);
|
||||
yesterdayDate.setDate(yesterdayDate.getDate() - 1);
|
||||
startDate = new Date(yesterdayDate);
|
||||
endDate = new Date(yesterdayDate);
|
||||
break;
|
||||
case 'thisweek':
|
||||
// 本周:周一到今天
|
||||
var dayOfWeek = today.getDay(); // 0=周日, 1=周一, ..., 6=周六
|
||||
@@ -7724,18 +7886,16 @@ return view.extend({
|
||||
// 重置按钮
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', function () {
|
||||
var oneYearAgo = new Date(today.getFullYear() - 1, today.getMonth(), today.getDate());
|
||||
setDateRange(oneYearAgo, today, '1year');
|
||||
setDateRange(today, today, 'today');
|
||||
queryData();
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化:默认选择最近一年
|
||||
var oneYearAgo = new Date(today.getFullYear() - 1, today.getMonth(), today.getDate());
|
||||
setDateRange(oneYearAgo, today, '1year');
|
||||
// 初始化:默认选择今天
|
||||
setDateRange(today, today, 'today');
|
||||
|
||||
// 设置初始时间范围并自动加载数据(结束时间为当天的 23:59:59)
|
||||
var startMs = oneYearAgo.getTime();
|
||||
var startMs = todayMs;
|
||||
var endToday = new Date(today);
|
||||
endToday.setHours(23, 59, 59, 999);
|
||||
var endMs = endToday.getTime();
|
||||
@@ -7896,6 +8056,12 @@ return view.extend({
|
||||
startDate = new Date(today);
|
||||
endDate = new Date(today);
|
||||
break;
|
||||
case 'yesterday':
|
||||
var yesterdayDate = new Date(today);
|
||||
yesterdayDate.setDate(yesterdayDate.getDate() - 1);
|
||||
startDate = new Date(yesterdayDate);
|
||||
endDate = new Date(yesterdayDate);
|
||||
break;
|
||||
case 'thisweek':
|
||||
var dayOfWeek = today.getDay();
|
||||
var mondayOffset = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
|
||||
@@ -7952,15 +8118,13 @@ return view.extend({
|
||||
// 重置按钮
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', function () {
|
||||
var oneYearAgo = new Date(today.getFullYear() - 1, today.getMonth(), today.getDate());
|
||||
setDateRange(oneYearAgo, today, '1year');
|
||||
setDateRange(today, today, 'today');
|
||||
queryData();
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化:默认选择最近一年
|
||||
var oneYearAgo = new Date(today.getFullYear() - 1, today.getMonth(), today.getDate());
|
||||
setDateRange(oneYearAgo, today, '1year');
|
||||
// 初始化:默认选择今天
|
||||
setDateRange(today, today, 'today');
|
||||
|
||||
startDateInput.addEventListener('change', function () {
|
||||
updateTimeline(this.value, endDateInput.value);
|
||||
@@ -7971,7 +8135,7 @@ return view.extend({
|
||||
});
|
||||
|
||||
// 设置初始时间范围并自动加载数据(不显示 loading)
|
||||
var startMs = oneYearAgo.getTime();
|
||||
var startMs = todayMs;
|
||||
var endToday = new Date(today);
|
||||
endToday.setHours(23, 59, 59, 999);
|
||||
var endMs = endToday.getTime();
|
||||
@@ -7983,6 +8147,205 @@ return view.extend({
|
||||
updateTrafficIncrements(startMs, endMs, null, null);
|
||||
}, 700);
|
||||
|
||||
// 异步加载远程广告(不阻塞主流程)
|
||||
(function () {
|
||||
var refreshTimer = null;
|
||||
|
||||
function resolveHttpsUrl(value, baseUrl) {
|
||||
if (typeof value !== 'string' || !value) return null;
|
||||
|
||||
try {
|
||||
var resolved = new URL(value, baseUrl);
|
||||
return resolved.protocol === 'https:' ? resolved.href : null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleRefresh(seconds) {
|
||||
var interval = Number(seconds);
|
||||
if (!Number.isFinite(interval)) interval = BANDIX_AD_DEFAULT_REFRESH_INTERVAL;
|
||||
interval = Math.max(300, Math.min(86400, interval));
|
||||
|
||||
if (refreshTimer !== null) window.clearTimeout(refreshTimer);
|
||||
refreshTimer = window.setTimeout(loadAdvertisements, interval * 1000);
|
||||
}
|
||||
|
||||
function normalizeAdvertisementLocale(value) {
|
||||
if (typeof value !== 'string') return '';
|
||||
|
||||
var locale = value.trim().replace(/_/g, '-').toLowerCase();
|
||||
if (!locale || locale === 'auto' || locale === 'und') return '';
|
||||
|
||||
if (locale === 'zh' || locale === 'zh-cn' || locale === 'zh-sg' || locale === 'zh-hans') {
|
||||
return 'zh-hans';
|
||||
}
|
||||
if (locale === 'zh-tw' || locale === 'zh-hk' || locale === 'zh-mo' || locale === 'zh-hant') {
|
||||
return 'zh-hant';
|
||||
}
|
||||
|
||||
return locale;
|
||||
}
|
||||
|
||||
function getAdvertisementLocale() {
|
||||
var candidates = [];
|
||||
if (L.env) candidates.push(L.env.lang || L.env.language || '');
|
||||
var htmlLanguage = document.documentElement.getAttribute('lang');
|
||||
if (htmlLanguage) candidates.push(htmlLanguage);
|
||||
if (window.navigator) candidates.push(window.navigator.language || '');
|
||||
|
||||
for (var i = 0; i < candidates.length; i++) {
|
||||
var locale = normalizeAdvertisementLocale(candidates[i]);
|
||||
if (locale) return locale;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function selectLocalizedAdvertisements(config) {
|
||||
// 兼容旧版单语言配置,方便两个仓库分开部署
|
||||
if (config.version === 1 && Array.isArray(config.ads)) {
|
||||
return { locale: 'legacy', ads: config.ads };
|
||||
}
|
||||
|
||||
if (config.version !== 2 || !config.locales || typeof config.locales !== 'object') {
|
||||
throw new Error('Invalid advertising configuration');
|
||||
}
|
||||
|
||||
var currentLocale = getAdvertisementLocale();
|
||||
var defaultLocale = normalizeAdvertisementLocale(config.default_locale);
|
||||
var localeCandidates = [];
|
||||
|
||||
if (currentLocale) {
|
||||
localeCandidates.push(currentLocale);
|
||||
var separatorIndex = currentLocale.indexOf('-');
|
||||
if (separatorIndex > 0) localeCandidates.push(currentLocale.substring(0, separatorIndex));
|
||||
}
|
||||
if (defaultLocale) localeCandidates.push(defaultLocale);
|
||||
|
||||
for (var i = 0; i < localeCandidates.length; i++) {
|
||||
var candidate = localeCandidates[i];
|
||||
if (Object.prototype.hasOwnProperty.call(config.locales, candidate) &&
|
||||
Array.isArray(config.locales[candidate])) {
|
||||
return { locale: candidate, ads: config.locales[candidate] };
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('No advertising locale is available');
|
||||
}
|
||||
|
||||
function renderAdvertisements(config) {
|
||||
var adRow = document.getElementById('bandix-ad-row');
|
||||
if (!adRow || !config) {
|
||||
throw new Error('Invalid advertising configuration');
|
||||
}
|
||||
|
||||
var selection = selectLocalizedAdvertisements(config);
|
||||
adRow.setAttribute('data-ad-locale', selection.locale);
|
||||
|
||||
var ads = selection.ads.slice(0, 6).map(function (ad, index) {
|
||||
if (!ad || typeof ad !== 'object') ad = {};
|
||||
|
||||
var href = resolveHttpsUrl(ad.href, BANDIX_AD_CONFIG_URL);
|
||||
var image = resolveHttpsUrl(ad.image, BANDIX_AD_CONFIG_URL);
|
||||
var title = typeof ad.title === 'string' ? ad.title.trim() : _('Advertising space') + ' ' + (index + 1);
|
||||
if (!title) title = _('Advertising space') + ' ' + (index + 1);
|
||||
var alt = typeof ad.alt === 'string' ? ad.alt.trim() : title;
|
||||
|
||||
return {
|
||||
enabled: ad.enabled === true && href !== null,
|
||||
href: href,
|
||||
image: image,
|
||||
title: title,
|
||||
alt: alt || title
|
||||
};
|
||||
});
|
||||
|
||||
while (ads.length < 6) {
|
||||
ads.push({
|
||||
enabled: false,
|
||||
href: null,
|
||||
image: null,
|
||||
title: _('Advertising space') + ' ' + (ads.length + 1),
|
||||
alt: _('Advertising space')
|
||||
});
|
||||
}
|
||||
|
||||
while (adRow.firstChild) adRow.removeChild(adRow.firstChild);
|
||||
|
||||
ads.forEach(function (ad, index) {
|
||||
var tagName = ad.enabled ? 'a' : 'div';
|
||||
var attributes = {
|
||||
'class': 'bandix-ad-slot' + (ad.enabled ? '' : ' bandix-ad-disabled'),
|
||||
'data-ad-slot': String(index + 1),
|
||||
'title': ad.title
|
||||
};
|
||||
|
||||
if (ad.enabled) {
|
||||
attributes.href = ad.href;
|
||||
attributes.target = '_blank';
|
||||
attributes.rel = 'noopener noreferrer sponsored';
|
||||
} else {
|
||||
attributes['aria-disabled'] = 'true';
|
||||
}
|
||||
|
||||
var content = [E('div', { 'class': 'bandix-ad-copy' }, [
|
||||
E('span', { 'class': 'bandix-ad-label' }, _('AD')),
|
||||
E('span', { 'class': 'bandix-ad-title' }, ad.title)
|
||||
])];
|
||||
if (ad.enabled && ad.image) {
|
||||
content.push(E('div', { 'class': 'bandix-ad-media' }, [
|
||||
E('img', {
|
||||
'class': 'bandix-ad-image',
|
||||
'src': ad.image,
|
||||
'alt': ad.alt,
|
||||
'loading': 'lazy',
|
||||
'referrerpolicy': 'no-referrer'
|
||||
})
|
||||
]));
|
||||
} else {
|
||||
content.push(E('span', { 'class': 'bandix-ad-placeholder' }, _('Advertising space')));
|
||||
}
|
||||
|
||||
adRow.appendChild(E(tagName, attributes, content));
|
||||
});
|
||||
|
||||
adRow.style.setProperty('--bandix-ad-count', '6');
|
||||
}
|
||||
|
||||
function loadAdvertisements() {
|
||||
var adRow = document.getElementById('bandix-ad-row');
|
||||
if (!adRow) return;
|
||||
|
||||
if (typeof window.fetch !== 'function') {
|
||||
console.debug('Failed to load advertisements: Fetch API is unavailable');
|
||||
scheduleRefresh(BANDIX_AD_RETRY_INTERVAL);
|
||||
return;
|
||||
}
|
||||
|
||||
var separator = BANDIX_AD_CONFIG_URL.indexOf('?') === -1 ? '?' : '&';
|
||||
var configUrl = BANDIX_AD_CONFIG_URL + separator + '_=' + Date.now();
|
||||
|
||||
window.fetch(configUrl, {
|
||||
method: 'GET',
|
||||
mode: 'cors',
|
||||
credentials: 'omit',
|
||||
cache: 'no-store'
|
||||
}).then(function (response) {
|
||||
if (!response.ok) throw new Error('HTTP ' + response.status);
|
||||
return response.json();
|
||||
}).then(function (config) {
|
||||
renderAdvertisements(config);
|
||||
scheduleRefresh(config.refresh_interval);
|
||||
}).catch(function (err) {
|
||||
console.debug('Failed to load advertisements:', err);
|
||||
scheduleRefresh(BANDIX_AD_RETRY_INTERVAL);
|
||||
});
|
||||
}
|
||||
|
||||
window.setTimeout(loadAdvertisements, 200);
|
||||
})();
|
||||
|
||||
// 异步加载版本信息(不阻塞主流程)
|
||||
(function () {
|
||||
// 延迟执行,确保页面先完成初始化
|
||||
|
||||
@@ -673,6 +673,46 @@ return view.extend({
|
||||
return false;
|
||||
};
|
||||
|
||||
// 支持作者(基本设置的最后一项)
|
||||
var alipayImage = L.resource('bandix/donate/alipay.jpg');
|
||||
var wechatImage = L.resource('bandix/donate/wechat-pay.png');
|
||||
|
||||
function donationCard(title, imageUrl, altText, dotClass) {
|
||||
return E('div', { 'class': 'bandix-donation-card' }, [
|
||||
E('h4', { 'class': 'bandix-donation-card-title' }, [
|
||||
E('span', { 'class': 'bandix-donation-dot ' + dotClass }),
|
||||
title
|
||||
]),
|
||||
E('a', {
|
||||
'class': 'bandix-donation-image-link',
|
||||
'href': imageUrl,
|
||||
'target': '_blank',
|
||||
'rel': 'noopener noreferrer',
|
||||
'title': altText
|
||||
}, [
|
||||
E('img', {
|
||||
'class': 'bandix-donation-image',
|
||||
'src': imageUrl,
|
||||
'alt': altText,
|
||||
'loading': 'lazy'
|
||||
})
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
o = s.option(form.DummyValue, '_donation_qr_codes', _('Support the Author'));
|
||||
o.renderWidget = function () {
|
||||
return E('div', { 'class': 'bandix-donation-content' }, [
|
||||
E('p', { 'class': 'bandix-donation-description' },
|
||||
_('If Bandix is useful to you, you can support its continued development.')),
|
||||
E('div', { 'class': 'bandix-donation-grid' }, [
|
||||
donationCard(_('Alipay'), alipayImage, _('Scan with Alipay to support'), 'bandix-donation-dot-alipay'),
|
||||
donationCard(_('WeChat Pay'), wechatImage, _('Scan with WeChat Pay to support'), 'bandix-donation-dot-wechat')
|
||||
]),
|
||||
E('p', { 'class': 'bandix-donation-note' }, _('Donations are voluntary. Thank you for supporting Bandix.'))
|
||||
]);
|
||||
};
|
||||
|
||||
// 2. 流量监控设置部分 (traffic)
|
||||
s = m.section(form.NamedSection, 'traffic', 'traffic', _('Traffic Monitor Settings'));
|
||||
s.description = _('Configure traffic monitoring related parameters');
|
||||
@@ -826,6 +866,98 @@ return view.extend({
|
||||
// 将 view 实例关联到 form map,以便在 cfgvalue 中访问
|
||||
m.view = this;
|
||||
|
||||
return m.render();
|
||||
return m.render().then(function (mapEl) {
|
||||
var oldStyle = document.getElementById('bandix-donation-styles');
|
||||
if (oldStyle && oldStyle.parentNode) oldStyle.parentNode.removeChild(oldStyle);
|
||||
|
||||
document.head.appendChild(E('style', { 'id': 'bandix-donation-styles' }, `
|
||||
.bandix-donation-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bandix-donation-description {
|
||||
margin: 0 auto 18px;
|
||||
max-width: 620px;
|
||||
opacity: 0.72;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.bandix-donation-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(220px, 280px));
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.bandix-donation-card {
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(107, 114, 128, 0.3);
|
||||
border-radius: 10px;
|
||||
background: rgba(107, 114, 128, 0.04);
|
||||
box-sizing: border-box;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.bandix-donation-card:hover {
|
||||
border-color: rgba(59, 130, 246, 0.55);
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.bandix-donation-card-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
margin: 0 0 10px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bandix-donation-dot {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bandix-donation-dot-alipay {
|
||||
background: #1677ff;
|
||||
}
|
||||
|
||||
.bandix-donation-dot-wechat {
|
||||
background: #07c160;
|
||||
}
|
||||
|
||||
.bandix-donation-image-link {
|
||||
display: block;
|
||||
border-radius: 7px;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.bandix-donation-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.bandix-donation-note {
|
||||
margin: 14px 0 0;
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.bandix-donation-grid {
|
||||
grid-template-columns: minmax(200px, 280px);
|
||||
gap: 14px;
|
||||
}
|
||||
}
|
||||
`));
|
||||
|
||||
return mapEl;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1112,6 +1112,30 @@ msgstr "Restablecer"
|
||||
msgid "Today"
|
||||
msgstr "Hoy"
|
||||
|
||||
msgid "Yesterday"
|
||||
msgstr "Ayer"
|
||||
|
||||
msgid "Support the Author"
|
||||
msgstr "Apoyar al autor"
|
||||
|
||||
msgid "If Bandix is useful to you, you can support its continued development."
|
||||
msgstr "Si Bandix te resulta útil, puedes apoyar su desarrollo continuo."
|
||||
|
||||
msgid "Alipay"
|
||||
msgstr "Alipay"
|
||||
|
||||
msgid "Scan with Alipay to support"
|
||||
msgstr "Escanea con Alipay para apoyar"
|
||||
|
||||
msgid "WeChat Pay"
|
||||
msgstr "WeChat Pay"
|
||||
|
||||
msgid "Scan with WeChat Pay to support"
|
||||
msgstr "Escanea con WeChat Pay para apoyar"
|
||||
|
||||
msgid "Donations are voluntary. Thank you for supporting Bandix."
|
||||
msgstr "Las donaciones son voluntarias. Gracias por apoyar a Bandix."
|
||||
|
||||
msgid "This Week"
|
||||
msgstr "Esta Semana"
|
||||
|
||||
|
||||
@@ -1112,6 +1112,30 @@ msgstr "Réinitialiser"
|
||||
msgid "Today"
|
||||
msgstr "Aujourd'hui"
|
||||
|
||||
msgid "Yesterday"
|
||||
msgstr "Hier"
|
||||
|
||||
msgid "Support the Author"
|
||||
msgstr "Soutenir l’auteur"
|
||||
|
||||
msgid "If Bandix is useful to you, you can support its continued development."
|
||||
msgstr "Si Bandix vous est utile, vous pouvez soutenir la poursuite de son développement."
|
||||
|
||||
msgid "Alipay"
|
||||
msgstr "Alipay"
|
||||
|
||||
msgid "Scan with Alipay to support"
|
||||
msgstr "Scannez avec Alipay pour nous soutenir"
|
||||
|
||||
msgid "WeChat Pay"
|
||||
msgstr "WeChat Pay"
|
||||
|
||||
msgid "Scan with WeChat Pay to support"
|
||||
msgstr "Scannez avec WeChat Pay pour nous soutenir"
|
||||
|
||||
msgid "Donations are voluntary. Thank you for supporting Bandix."
|
||||
msgstr "Les dons sont facultatifs. Merci de soutenir Bandix."
|
||||
|
||||
msgid "This Week"
|
||||
msgstr "Cette Semaine"
|
||||
|
||||
|
||||
@@ -1112,6 +1112,30 @@ msgstr "リセット"
|
||||
msgid "Today"
|
||||
msgstr "今日"
|
||||
|
||||
msgid "Yesterday"
|
||||
msgstr "昨日"
|
||||
|
||||
msgid "Support the Author"
|
||||
msgstr "作者を支援"
|
||||
|
||||
msgid "If Bandix is useful to you, you can support its continued development."
|
||||
msgstr "Bandix が役に立った場合は、継続的な開発をご支援いただけます。"
|
||||
|
||||
msgid "Alipay"
|
||||
msgstr "Alipay"
|
||||
|
||||
msgid "Scan with Alipay to support"
|
||||
msgstr "Alipay でスキャンして支援"
|
||||
|
||||
msgid "WeChat Pay"
|
||||
msgstr "WeChat Pay"
|
||||
|
||||
msgid "Scan with WeChat Pay to support"
|
||||
msgstr "WeChat Pay でスキャンして支援"
|
||||
|
||||
msgid "Donations are voluntary. Thank you for supporting Bandix."
|
||||
msgstr "寄付は任意です。Bandix をご支援いただきありがとうございます。"
|
||||
|
||||
msgid "This Week"
|
||||
msgstr "今週"
|
||||
|
||||
|
||||
@@ -1112,6 +1112,30 @@ msgstr "Resetuj"
|
||||
msgid "Today"
|
||||
msgstr "Dzisiaj"
|
||||
|
||||
msgid "Yesterday"
|
||||
msgstr "Wczoraj"
|
||||
|
||||
msgid "Support the Author"
|
||||
msgstr "Wesprzyj autora"
|
||||
|
||||
msgid "If Bandix is useful to you, you can support its continued development."
|
||||
msgstr "Jeśli Bandix jest dla Ciebie przydatny, możesz wesprzeć jego dalszy rozwój."
|
||||
|
||||
msgid "Alipay"
|
||||
msgstr "Alipay"
|
||||
|
||||
msgid "Scan with Alipay to support"
|
||||
msgstr "Zeskanuj w Alipay, aby wesprzeć"
|
||||
|
||||
msgid "WeChat Pay"
|
||||
msgstr "WeChat Pay"
|
||||
|
||||
msgid "Scan with WeChat Pay to support"
|
||||
msgstr "Zeskanuj w WeChat Pay, aby wesprzeć"
|
||||
|
||||
msgid "Donations are voluntary. Thank you for supporting Bandix."
|
||||
msgstr "Darowizny są dobrowolne. Dziękujemy za wsparcie Bandix."
|
||||
|
||||
msgid "This Week"
|
||||
msgstr "Ten Tydzień"
|
||||
|
||||
|
||||
@@ -1112,6 +1112,30 @@ msgstr "Сбросить"
|
||||
msgid "Today"
|
||||
msgstr "Сегодня"
|
||||
|
||||
msgid "Yesterday"
|
||||
msgstr "Вчера"
|
||||
|
||||
msgid "Support the Author"
|
||||
msgstr "Поддержать автора"
|
||||
|
||||
msgid "If Bandix is useful to you, you can support its continued development."
|
||||
msgstr "Если Bandix полезен вам, вы можете поддержать его дальнейшую разработку."
|
||||
|
||||
msgid "Alipay"
|
||||
msgstr "Alipay"
|
||||
|
||||
msgid "Scan with Alipay to support"
|
||||
msgstr "Отсканируйте в Alipay, чтобы поддержать"
|
||||
|
||||
msgid "WeChat Pay"
|
||||
msgstr "WeChat Pay"
|
||||
|
||||
msgid "Scan with WeChat Pay to support"
|
||||
msgstr "Отсканируйте в WeChat Pay, чтобы поддержать"
|
||||
|
||||
msgid "Donations are voluntary. Thank you for supporting Bandix."
|
||||
msgstr "Пожертвования добровольны. Спасибо за поддержку Bandix."
|
||||
|
||||
msgid "This Week"
|
||||
msgstr "На этой неделе"
|
||||
|
||||
|
||||
@@ -1094,6 +1094,15 @@ msgstr "有更新"
|
||||
msgid "Update available, click to go to settings"
|
||||
msgstr "有更新,点击前往设置"
|
||||
|
||||
msgid "Advertising spaces"
|
||||
msgstr "广告位"
|
||||
|
||||
msgid "Advertising space"
|
||||
msgstr "广告位"
|
||||
|
||||
msgid "AD"
|
||||
msgstr "广告"
|
||||
|
||||
msgid "Traffic Statistics"
|
||||
msgstr "流量统计"
|
||||
|
||||
@@ -1115,6 +1124,30 @@ msgstr "重置"
|
||||
msgid "Today"
|
||||
msgstr "今天"
|
||||
|
||||
msgid "Yesterday"
|
||||
msgstr "昨天"
|
||||
|
||||
msgid "Support the Author"
|
||||
msgstr "支持作者"
|
||||
|
||||
msgid "If Bandix is useful to you, you can support its continued development."
|
||||
msgstr "如果 Bandix 对您有帮助,您可以支持项目的持续开发。"
|
||||
|
||||
msgid "Alipay"
|
||||
msgstr "支付宝"
|
||||
|
||||
msgid "Scan with Alipay to support"
|
||||
msgstr "使用支付宝扫码支持"
|
||||
|
||||
msgid "WeChat Pay"
|
||||
msgstr "微信支付"
|
||||
|
||||
msgid "Scan with WeChat Pay to support"
|
||||
msgstr "使用微信支付扫码支持"
|
||||
|
||||
msgid "Donations are voluntary. Thank you for supporting Bandix."
|
||||
msgstr "赞助完全自愿。感谢您对 Bandix 的支持。"
|
||||
|
||||
msgid "This Week"
|
||||
msgstr "本周"
|
||||
|
||||
|
||||
@@ -1112,6 +1112,30 @@ msgstr "重置"
|
||||
msgid "Today"
|
||||
msgstr "今天"
|
||||
|
||||
msgid "Yesterday"
|
||||
msgstr "昨天"
|
||||
|
||||
msgid "Support the Author"
|
||||
msgstr "支持作者"
|
||||
|
||||
msgid "If Bandix is useful to you, you can support its continued development."
|
||||
msgstr "如果 Bandix 對您有幫助,您可以支持專案的持續開發。"
|
||||
|
||||
msgid "Alipay"
|
||||
msgstr "支付寶"
|
||||
|
||||
msgid "Scan with Alipay to support"
|
||||
msgstr "使用支付寶掃碼支持"
|
||||
|
||||
msgid "WeChat Pay"
|
||||
msgstr "微信支付"
|
||||
|
||||
msgid "Scan with WeChat Pay to support"
|
||||
msgstr "使用微信支付掃碼支持"
|
||||
|
||||
msgid "Donations are voluntary. Thank you for supporting Bandix."
|
||||
msgstr "贊助完全自願。感謝您對 Bandix 的支持。"
|
||||
|
||||
msgid "This Week"
|
||||
msgstr "本週"
|
||||
|
||||
@@ -1255,3 +1279,12 @@ msgstr "TCX 出站錨點程式 ID"
|
||||
|
||||
msgid "Used when tc_order is before/after. Must be a valid egress program id on the same interface."
|
||||
msgstr "當 tc_order 為 before/after 時使用。必須是同一介面上的有效出站程式 ID。"
|
||||
|
||||
msgid "Advertising spaces"
|
||||
msgstr "廣告位"
|
||||
|
||||
msgid "Advertising space"
|
||||
msgstr "廣告位"
|
||||
|
||||
msgid "AD"
|
||||
msgstr "廣告"
|
||||
|
||||
@@ -114,6 +114,14 @@ return L.view.extend({
|
||||
return m.checkDepends();
|
||||
};
|
||||
|
||||
o = s.option(form.Flag, "mptcp_dscp_weight_vps_sync", _("Mirror DSCP/weight pins to gateway"),
|
||||
_("When using a DSCP or weight BPF scheduler, also sync each WAN’s pin to the gateway (VPS) so it also holds for traffic the gateway sends (downloads), not just traffic the router sends (uploads). Disabling this only stops future syncs -- it does not remove pins already pushed to the gateway."));
|
||||
o.default = "1";
|
||||
o.depends("mptcp_scheduler", "bpf_dscp");
|
||||
o.depends("mptcp_scheduler", "bpf_weight");
|
||||
o.depends("mptcp_scheduler", "bpf_weight_rr");
|
||||
o.depends("mptcp_scheduler", "bpf_burstweight");
|
||||
|
||||
if (parseFloat(boardinfo.kernel.substring(0,4)) < 6) {
|
||||
o = s.option(form.Value, "mptcp_syn_retries", _("Multipath TCP SYN retries"));
|
||||
o.datatype = "uinteger";
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
'use strict';
|
||||
'require form';
|
||||
'require uci';
|
||||
|
||||
/*
|
||||
* Copyright (C) 2026 Ycarus (Yannick Chabanois) <contact@openmptcprouter.com> for OpenMPTCProuter
|
||||
* This is free software, licensed under the GNU General Public License v3.
|
||||
* See /LICENSE for more information
|
||||
*/
|
||||
|
||||
// Same class/value pairs as mptcp-scheduler-dscp.sh's dscp_to_val.
|
||||
var DSCP_CLASSES = [
|
||||
['cs0', _('CS0 (0) - Best effort')],
|
||||
['cs1', _('CS1 (8) - Low priority')],
|
||||
['cs2', _('CS2 (16) - High priority')],
|
||||
['cs3', _('CS3 (24) - SIP/signaling')],
|
||||
['cs4', _('CS4 (32) - Real-time interactive')],
|
||||
['cs5', _('CS5 (40) - Broadcast video')],
|
||||
['cs6', _('CS6 (48) - Network control')],
|
||||
['cs7', _('CS7 (56) - Latency sensitive')],
|
||||
['le', _('LE (1) - Lower effort')],
|
||||
['af11', 'AF11 (10)'],
|
||||
['af12', 'AF12 (12)'],
|
||||
['af13', 'AF13 (14)'],
|
||||
['af21', 'AF21 (18)'],
|
||||
['af22', 'AF22 (20)'],
|
||||
['af23', 'AF23 (22)'],
|
||||
['af31', 'AF31 (26)'],
|
||||
['af32', 'AF32 (28)'],
|
||||
['af33', 'AF33 (30)'],
|
||||
['af41', 'AF41 (34)'],
|
||||
['af42', 'AF42 (36)'],
|
||||
['af43', 'AF43 (38)'],
|
||||
['ef', _('EF (46) - Voice')]
|
||||
];
|
||||
|
||||
// Same WAN filter as this app's own mptcp.js "Interfaces Settings"
|
||||
// section: excludes the pseudo-interfaces that are never a real WAN.
|
||||
function isEligibleIface(name) {
|
||||
return !!name && !name.match(/^oip/) && !name.match(/^lo/) &&
|
||||
name !== 'omrvpn' && name !== 'OWVPN' && name !== 'omr6in4';
|
||||
}
|
||||
|
||||
function fillIfaceOptions(option) {
|
||||
option.value('', _('-- not pinned --'));
|
||||
uci.sections('network', 'interface', function(ifc) {
|
||||
if (isEligibleIface(ifc['.name']))
|
||||
option.value(ifc['.name'], ifc.label || ifc['.name']);
|
||||
});
|
||||
}
|
||||
|
||||
// Same normalization as mptcp.js's own normalizeSchedulerValue, so
|
||||
// "mptcp_bpf_dscp.o" (the raw .o filename, seen on older saves / before a
|
||||
// kernel>=6.18 write ever normalizes it) and "bpf_dscp" compare equal.
|
||||
function normalizeSchedulerValue(value) {
|
||||
if (value == null)
|
||||
return value;
|
||||
var normalized = String(value).trim();
|
||||
if (normalized.slice(-2) === '.o')
|
||||
normalized = normalized.slice(0, -2);
|
||||
if (normalized.slice(0, 6) === 'mptcp_')
|
||||
normalized = normalized.slice(6);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
return L.view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
uci.load('network'),
|
||||
// mqvpn may not be installed at all -- resolve to null instead
|
||||
// of rejecting the whole page's load() in that case.
|
||||
L.resolveDefault(uci.load('mqvpn'), null)
|
||||
]);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var m, s, o;
|
||||
|
||||
var mptcpScheduler = normalizeSchedulerValue(uci.get('network', 'globals', 'mptcp_scheduler'));
|
||||
var mptcpHasDscpScheduler = (mptcpScheduler === 'bpf_dscp');
|
||||
var mptcpHasWeightScheduler = (mptcpScheduler === 'bpf_weight' || mptcpScheduler === 'bpf_weight_rr' || mptcpScheduler === 'bpf_burstweight');
|
||||
|
||||
var mqvpnEnabled = (uci.get('mqvpn', 'settings', 'enable') === '1');
|
||||
var mqvpnScheduler = uci.get('mqvpn', 'multipath', 'scheduler');
|
||||
// mqvpn has no scheduler value dedicated to DSCP the way MPTCP has
|
||||
// bpf_dscp -- 007-mqvpn-dscp pushes the DSCP mask unconditionally
|
||||
// whenever mqvpn is enabled, regardless of which scheduler is
|
||||
// running (unlike weight below, which mqvpn only reads for wrtt/
|
||||
// wrr). So "mqvpn wants the DSCP table" just means "mqvpn is
|
||||
// enabled" -- there's no narrower signal to gate on.
|
||||
var mqvpnHasDscpScheduler = mqvpnEnabled;
|
||||
// Matches 006-mqvpn-weight's own gate exactly (it no-ops for any
|
||||
// other scheduler).
|
||||
var mqvpnHasWeightScheduler = mqvpnEnabled && (mqvpnScheduler === 'wrtt' || mqvpnScheduler === 'wrr');
|
||||
|
||||
var showDscp = mptcpHasDscpScheduler || mqvpnHasDscpScheduler;
|
||||
var showWeight = mptcpHasWeightScheduler || mqvpnHasWeightScheduler;
|
||||
|
||||
m = new form.Map('network', _('DSCP / Weight Routing'),
|
||||
_('Choose which WAN carries each DSCP-tagged traffic class -- for the MPTCP bpf_dscp scheduler and for mqvpn -- and review the per-WAN weight used by the *weight schedulers on both. This page only edits the pins/weights; pick the scheduler that actually uses them on the MPTCP and MQVPN pages themselves.'));
|
||||
|
||||
if (!showDscp && !showWeight) {
|
||||
m.description += '<br /><br />' + _('Nothing to show yet: no DSCP scheduler (bpf_dscp) or weight scheduler (bpf_weight/bpf_weight_rr/bpf_burstweight, or mqvpn’s Weighted RTT/Weighted Round Robin) is currently selected on the MPTCP or MQVPN pages.');
|
||||
return m.render();
|
||||
}
|
||||
|
||||
// Same option as luci-app-mptcp's main MPTCP page -- one uci value,
|
||||
// editable from either page. The Download column below only has an
|
||||
// effect while this is enabled (it's what gets pushed to the
|
||||
// gateway as dscp_remote_id); showing it while sync is off would be
|
||||
// a dead control, so its visibility is decided from this option's
|
||||
// *persisted* value at page load, not live -- toggling the box
|
||||
// below needs Save & Apply, then a reload of this page, before the
|
||||
// Download column itself appears/disappears to match.
|
||||
s = m.section(form.TypedSection, 'globals', _('Gateway sync'));
|
||||
|
||||
o = s.option(form.Flag, 'mptcp_dscp_weight_vps_sync', _('Mirror DSCP/weight pins to gateway'),
|
||||
_('When using a DSCP or weight BPF scheduler, also sync each WAN’s pin to the gateway (VPS) so it also holds for traffic the gateway sends (downloads), not just traffic the router sends (uploads). Disabling this only stops future syncs -- it does not remove pins already pushed to the gateway.') + '<br />' +
|
||||
_('While disabled, the DSCP table below only shows Upload -- Download has no effect until this is re-enabled and applied.'));
|
||||
o.default = '1';
|
||||
|
||||
var vpsSyncEnabled = (uci.get('network', 'globals', 'mptcp_dscp_weight_vps_sync') !== '0');
|
||||
|
||||
if (showDscp) {
|
||||
s = m.section(form.TableSection, 'dscp_pin', _('DSCP class pins'));
|
||||
s.anonymous = true;
|
||||
s.addremove = true;
|
||||
s.sortable = true;
|
||||
|
||||
o = s.option(form.ListValue, 'dscp', _('DSCP class'));
|
||||
o.rmempty = false;
|
||||
DSCP_CLASSES.forEach(function(c) { o.value(c[0], c[1]); });
|
||||
|
||||
o = s.option(form.ListValue, 'upload_interface', _('Upload interface'),
|
||||
_('Router → internet. Pins this class to a WAN for MPTCP’s bpf_dscp scheduler (local dscp_iface map) and, for mqvpn, announces the class on this path’s DSCP mask via the control API.'));
|
||||
o.load = function(section_id) {
|
||||
fillIfaceOptions(this);
|
||||
return this.super('load', [section_id]);
|
||||
};
|
||||
|
||||
if (vpsSyncEnabled) {
|
||||
o = s.option(form.ListValue, 'download_interface', _('Download interface'),
|
||||
_('Gateway (VPS) → router. Only takes effect for MPTCP’s bpf_dscp scheduler (pins the VPS’s own send-side choice via dscp_remote_id) and can be a different WAN than Upload. mqvpn has no equivalent: its downlink always mirrors whichever WAN carries the class on upload, so this field has no effect on mqvpn.'));
|
||||
o.load = function(section_id) {
|
||||
fillIfaceOptions(this);
|
||||
return this.super('load', [section_id]);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (showWeight) {
|
||||
s = m.section(form.TypedSection, 'interface', _('WAN weight'),
|
||||
_('Used by the *weight MPTCP schedulers (bpf_weight/bpf_weight_rr) and by mqvpn’s "Weighted RTT"/"Weighted Round Robin" schedulers -- same value, applies to both upload and download (mirrored to the gateway automatically). Unlike the DSCP pins above, weight is a single value per WAN, not per traffic class.'));
|
||||
s.filter = function(section) {
|
||||
return isEligibleIface(section);
|
||||
};
|
||||
|
||||
o = s.option(form.Value, 'multipath_weight', _('Weight'),
|
||||
_('A weight >100 makes a WAN more attractive, a weight <100 makes it less attractive. Max 256.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.rmempty = false;
|
||||
o.default = 100;
|
||||
}
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
@@ -21,6 +21,17 @@
|
||||
"acl": [ "luci-app-mptcp" ]
|
||||
}
|
||||
},
|
||||
"admin/network/mptcp/mptcp_dscp_routing": {
|
||||
"title": "DSCP / Weight Routing",
|
||||
"order": 15,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "mptcp/mptcp_dscp_routing"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-mptcp" ]
|
||||
}
|
||||
},
|
||||
"admin/network/mptcp/bandwidth": {
|
||||
"title": "Bandwidth",
|
||||
"order": 20,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"luci-app-mptcp": {
|
||||
"description": "Grant UCI access for luci-app-mptcp",
|
||||
"read": {
|
||||
"uci": [ "openmptcprouter", "network" ],
|
||||
"uci": [ "openmptcprouter", "network", "mqvpn" ],
|
||||
"file": {
|
||||
"/usr/lib/mptcpd": [ "list" ],
|
||||
"/usr/share/bpf/scheduler": [ "list" ],
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=mptcp
|
||||
PKG_VERSION:=6.1
|
||||
PKG_RELEASE:=8
|
||||
PKG_RELEASE:=9
|
||||
|
||||
PKG_MAINTAINER:=Ycarus (Yannick Chabanois) <ycarus@zugaina.org>
|
||||
PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME)
|
||||
|
||||
@@ -9,6 +9,28 @@ USE_PROCD=1
|
||||
. /usr/lib/unbound/iptools.sh
|
||||
. /lib/functions/network.sh
|
||||
|
||||
restart_scheduler_dependent_services() {
|
||||
local proxy vpn
|
||||
proxy="$(uci -q get openmptcprouter.settings.proxy)"
|
||||
case "$proxy" in
|
||||
shadowsocks) /etc/init.d/shadowsocks-libev restart >/dev/null 2>&1 ;;
|
||||
shadowsocks-rust|shadowsocks-go) /etc/init.d/shadowsocks-rust restart >/dev/null 2>&1 ;;
|
||||
v2ray*) /etc/init.d/v2ray restart >/dev/null 2>&1 ;;
|
||||
xray*) /etc/init.d/xray restart >/dev/null 2>&1 ;;
|
||||
esac
|
||||
|
||||
vpn="$(uci -q get openmptcprouter.settings.vpn)"
|
||||
case "$vpn" in
|
||||
openvpn) /etc/init.d/openvpn restart >/dev/null 2>&1 ;;
|
||||
openvpn_bonding)
|
||||
/etc/init.d/openvpn restart >/dev/null 2>&1
|
||||
/etc/init.d/openvpnbonding restart >/dev/null 2>&1
|
||||
;;
|
||||
glorytun_tcp) /etc/init.d/glorytun restart >/dev/null 2>&1 ;;
|
||||
esac
|
||||
logger -t "MPTCP" "Scheduler changed, restarted proxy=${proxy:-none} vpn=${vpn:-none} (vpn restart skipped unless TCP-based)"
|
||||
}
|
||||
|
||||
global_multipath_settings() {
|
||||
local multipath mptcp_path_manager mptcp_scheduler mptcp_subflows mptcp_debug mptcp_add_addr_accepted congestion mptcp_checksum mptcp_syn_retries mptcp_fullmesh_num_subflows mptcp_fullmesh_create_on_err mptcp_ndiffports_num_subflows mptcp_rr_cwnd_limited mptcp_rr_num_segments
|
||||
local multipath_status=0
|
||||
@@ -47,7 +69,11 @@ global_multipath_settings() {
|
||||
echo "net.mptcp.mptcp_enabled=${multipath_status}" >> /etc/sysctl.d/zzz_openmptcprouter.conf
|
||||
fi
|
||||
[ -z "$mptcp_path_manager" ] || sysctl -qew net.mptcp.mptcp_path_manager="$mptcp_path_manager"
|
||||
[ -z "$mptcp_scheduler" ] || sysctl -qew net.mptcp.mptcp_scheduler="$mptcp_scheduler"
|
||||
if [ -n "$mptcp_scheduler" ]; then
|
||||
local prev_sched="$(cat /proc/sys/net/mptcp/mptcp_scheduler 2>/dev/null)"
|
||||
sysctl -qew net.mptcp.mptcp_scheduler="$mptcp_scheduler"
|
||||
[ -n "$prev_sched" ] && [ "$prev_sched" != "$mptcp_scheduler" ] && restart_scheduler_dependent_services
|
||||
fi
|
||||
[ -z "$mptcp_checksum" ] || sysctl -qew net.mptcp.mptcp_checksum="$mptcp_checksum"
|
||||
[ -z "$mptcp_debug" ] || sysctl -qew net.mptcp.mptcp_debug="$mptcp_debug"
|
||||
[ -z "$mptcp_syn_retries" ] || sysctl -qew net.mptcp.mptcp_syn_retries="$mptcp_syn_retries"
|
||||
@@ -76,6 +102,7 @@ global_multipath_settings() {
|
||||
[ -z "$mptcp_blackhole_timeout" ] || sysctl -qew net.mptcp.blackhole_timeout="$mptcp_blackhole_timeout"
|
||||
if [ -n "$mptcp_scheduler" ]; then
|
||||
local sched="${mptcp_scheduler#mptcp_}"; sched="${sched%.o}"
|
||||
local prev_sched="$(cat /proc/sys/net/mptcp/scheduler 2>/dev/null)"
|
||||
if [ -d /usr/share/bpf/scheduler ]; then
|
||||
local pin_dir="/sys/fs/bpf/mptcp"
|
||||
mkdir -p "$pin_dir"
|
||||
@@ -87,6 +114,7 @@ global_multipath_settings() {
|
||||
[ -f "$obj" ] && bpftool struct_ops register "$obj" "$pin_dir/" >/dev/null 2>&1
|
||||
fi
|
||||
sysctl -qew net.mptcp.scheduler="$sched" >/dev/null 2>&1
|
||||
[ -n "$prev_sched" ] && [ "$prev_sched" != "$sched" ] && restart_scheduler_dependent_services
|
||||
fi
|
||||
fi
|
||||
[ -z "$congestion" ] || sysctl -qew net.ipv4.tcp_congestion_control="$congestion"
|
||||
@@ -518,13 +546,13 @@ interface_multipath_settings() {
|
||||
if [ "$(uci -q get openmptcprouter.settings.force_multipath)" != "0" ] && [ -n "$(ip a show dev $iface | grep inet)" ]; then
|
||||
if ([ "$mode" = "master" ] || [ "$mode" = "on" ]) && [ -z "$(multipath $iface | grep default)" ]; then
|
||||
logger -t "MPTCP" "Set $iface to $mode from $(multipath $iface)"
|
||||
multipath "$iface" "on"
|
||||
multipath "$iface" "on" "$id"
|
||||
elif ([ "$mode" = "off" ] || [ -z "$mode" ]) && [ -z "$(multipath $iface | grep deactivated)" ]; then
|
||||
logger -t "MPTCP" "Set $iface to $mode from $(multipath $iface)"
|
||||
multipath "$iface" "$mode"
|
||||
multipath "$iface" "$mode" "$id"
|
||||
elif [ "$mode" = "backup" ] && [ -z "$(multipath $iface | grep backup)" ]; then
|
||||
logger -t "MPTCP" "Set $iface to $mode from $(multipath $iface)"
|
||||
multipath "$iface" "$mode"
|
||||
multipath "$iface" "$mode" "$id"
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ case $1 in
|
||||
if [ -f /proc/sys/net/mptcp/mptcp_enabled ]; then
|
||||
echo " multipath device {on | off | backup }"
|
||||
else
|
||||
echo " multipath device {on | off | signal | backup }"
|
||||
echo " multipath device {on | off | signal | backup } [id]"
|
||||
fi
|
||||
echo
|
||||
echo "show established conections: -c"
|
||||
@@ -39,6 +39,11 @@ case $1 in
|
||||
echo "will allow a subflow to be established across this interface, but only be used"
|
||||
echo "as backup."
|
||||
echo
|
||||
echo "Optional trailing [id] (on/signal/backup only): explicit MPTCP endpoint id"
|
||||
echo "(0-255) to pass to 'ip mptcp endpoint add ... id <id>' instead of letting the"
|
||||
echo "kernel auto-assign one. Needed for a stable id per WAN across reconnects, e.g."
|
||||
echo "so a peer (VPS) can pin traffic to this WAN via the subflow's remote_id."
|
||||
echo
|
||||
exit 0 ;;
|
||||
"-c")
|
||||
if [ -f /proc/net/mptcp_net/mptcp ]; then
|
||||
@@ -123,6 +128,16 @@ esac
|
||||
|
||||
DEVICE="$1"
|
||||
TYPE="$2"
|
||||
# Optional explicit MPTCP endpoint id (stable across delete/recreate, unlike
|
||||
# the kernel's auto-assigned one) so a peer can key on subflow->remote_id.
|
||||
# Anything that isn't a plain 0-255 integer is ignored (kernel auto-assigns).
|
||||
ENDPOINT_ID="$3"
|
||||
case "$ENDPOINT_ID" in
|
||||
''|*[!0-9]*) ENDPOINT_ID="" ;;
|
||||
*) [ "$ENDPOINT_ID" -ge 0 ] 2>/dev/null && [ "$ENDPOINT_ID" -le 255 ] 2>/dev/null || ENDPOINT_ID="" ;;
|
||||
esac
|
||||
IDOPT=""
|
||||
[ -n "$ENDPOINT_ID" ] && IDOPT="id $ENDPOINT_ID"
|
||||
#FLAG_PATH=`find /sys/devices/ -path "*/net/$DEVICE/flags"`
|
||||
|
||||
[ -e "/sys/class/net/$DEVICE/" ] || {
|
||||
@@ -215,7 +230,7 @@ else
|
||||
done
|
||||
}
|
||||
for i in $IP; do
|
||||
ip mptcp endpoint add $i dev $DEVICE subflow fullmesh
|
||||
ip mptcp endpoint add $i dev $DEVICE subflow fullmesh $IDOPT
|
||||
done
|
||||
exit 0;;
|
||||
"signal")
|
||||
@@ -230,7 +245,7 @@ else
|
||||
}
|
||||
for i in $IP; do
|
||||
#ip mptcp endpoint add $i dev $DEVICE signal subflow fullmesh
|
||||
ip mptcp endpoint add $i dev $DEVICE signal
|
||||
ip mptcp endpoint add $i dev $DEVICE signal $IDOPT
|
||||
done
|
||||
exit 0;;
|
||||
"backup")
|
||||
@@ -244,7 +259,7 @@ else
|
||||
done
|
||||
}
|
||||
for i in $IP; do
|
||||
ip mptcp endpoint add $i dev $DEVICE subflow backup fullmesh
|
||||
ip mptcp endpoint add $i dev $DEVICE subflow backup fullmesh $IDOPT
|
||||
done
|
||||
exit 0;;
|
||||
"")
|
||||
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
|
||||
mptcp_has_master_multipath() {
|
||||
uci -q show network | grep -q "\.multipath='master'$"
|
||||
}
|
||||
|
||||
mptcp_ensure_ipv6_oif_rule() {
|
||||
local table="$1"
|
||||
local iface="$2"
|
||||
|
||||
[ -n "$table" ] || return 1
|
||||
[ -n "$iface" ] || return 1
|
||||
|
||||
ip -6 rule show 2>/dev/null | grep -Fq "oif $iface lookup $table" && return 0
|
||||
ip -6 rule add oif "$iface" table "$table" pref 0
|
||||
}
|
||||
@@ -9,14 +9,14 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=sqm-autorate
|
||||
PKG_VERSION:=0.1
|
||||
PKG_RELEASE:=4
|
||||
PKG_RELEASE:=5
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/$(PKG_NAME)
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
DEPENDS:=+bash +tsping
|
||||
DEPENDS:=+bash +tsping +rpcd
|
||||
TITLE:=SQM Autorate
|
||||
endef
|
||||
|
||||
|
||||
@@ -42,6 +42,15 @@ _launch_autorate() {
|
||||
procd_set_param limits nofile="51200 51200"
|
||||
procd_set_param respawn 0 10 0
|
||||
procd_set_param stderr 1
|
||||
# launcher.sh's own EXIT/INT/TERM trap (kill_cake_instances) SIGTERMs each
|
||||
# cake-autorate.sh child one-by-one and waits for all of them to exit
|
||||
# before it exits itself. procd's default grace period between SIGTERM
|
||||
# and escalating to SIGKILL is too short for that to reliably finish with
|
||||
# multiple WAN instances running -- if procd SIGKILLs launcher.sh first,
|
||||
# the trap never runs (SIGKILL can't be caught) and every cake-autorate.sh
|
||||
# child is orphaned, running forever until the next reboot. Give the trap
|
||||
# enough room to actually reap its children on every stop/restart/reload.
|
||||
procd_set_param term_timeout 15
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
#!/bin/sh
|
||||
# /usr/libexec/rpcd/sqm-autorate
|
||||
# rpcd plugin for sqm-autorate config + live metrics
|
||||
#
|
||||
# Usage:
|
||||
# ubus call sqm-autorate get_config '{"section":"wan1"}'
|
||||
# ubus call sqm-autorate get_config {}
|
||||
# ubus call sqm-autorate set_config '{"section":"wan1","download":"50000","max_download":"80000","restart":true}'
|
||||
# ubus call sqm-autorate get_metrics '{"section":"wan1"}'
|
||||
# ubus call sqm-autorate get_metrics {}
|
||||
|
||||
CONFIG_DIR="/usr/share/sqm-autorate"
|
||||
|
||||
# UCI options readable via get_config / settable via set_config -- mirrors
|
||||
# what luci-app-sqm-autorate's sqm.js form exposes, plus the autorate-only
|
||||
# knobs config_template.sh reads directly. Keep in sync with both when either
|
||||
# side gains/loses a field.
|
||||
VALID_OPTIONS="enabled autorate interface download min_download max_download \
|
||||
upload min_upload max_upload debug_logging verbosity qdisc script \
|
||||
qdisc_advanced use_mq squash_dscp squash_ingress ingress_ecn egress_ecn \
|
||||
qdisc_really_really_advanced ilimit elimit itarget etarget iqdisc_opts \
|
||||
eqdisc_opts linklayer overhead linklayer_advanced tcMTU tcTSIZE tcMPU \
|
||||
linklayer_adaptation_mechanism output_processing_stats output_load_stats \
|
||||
output_reflector_stats output_cake_changes debug sss_compensation pinger \
|
||||
no_pingers reflector_ping_interval_s delay_thr_ms enable_sleep_functions \
|
||||
connection_active_thr_kpbs sustained_idle_sleep_thr \
|
||||
min_shaper_rates_enforcement startup_wait_s"
|
||||
|
||||
_json_str() {
|
||||
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
|
||||
}
|
||||
|
||||
_section_exists() {
|
||||
[ -n "$(uci -q get "sqm.$1" 2>/dev/null)" ]
|
||||
}
|
||||
|
||||
_all_sections() {
|
||||
uci -q show sqm 2>/dev/null | grep '=queue$' | cut -d. -f2 | cut -d= -f1
|
||||
}
|
||||
|
||||
# Emit one queue section as a JSON object of its currently-set options.
|
||||
_section_json() {
|
||||
local sec="$1" opt val entries=""
|
||||
for opt in $VALID_OPTIONS; do
|
||||
val=$(uci -q get "sqm.${sec}.${opt}" 2>/dev/null)
|
||||
[ -z "$val" ] && continue
|
||||
[ -n "$entries" ] && entries="${entries},"
|
||||
entries="${entries}\"$(_json_str "$opt")\":\"$(_json_str "$val")\""
|
||||
done
|
||||
printf '{"section":"%s","options":{%s}}' "$(_json_str "$sec")" "$entries"
|
||||
}
|
||||
|
||||
# tc reports bandwidth/backlog/drop counters as plain text; there is no -j
|
||||
# support to rely on here (see tests/cases/11c which parses the same way).
|
||||
_iface_metrics_json() {
|
||||
local dev="$1" out bandwidth sent_bytes sent_pkts dropped overlimits backlog_bytes backlog_pkts qdisc_name
|
||||
|
||||
if [ -z "$dev" ] || ! out=$(tc -s qdisc show dev "$dev" 2>/dev/null) || [ -z "$out" ]; then
|
||||
printf '{"device":"%s","exists":false}' "$(_json_str "$dev")"
|
||||
return
|
||||
fi
|
||||
|
||||
qdisc_name=$(printf '%s\n' "$out" | awk 'NR==1{print $2}')
|
||||
bandwidth=$(printf '%s\n' "$out" | grep -o 'bandwidth [^ ]*' | head -n1 | cut -d' ' -f2)
|
||||
sent_bytes=$(printf '%s\n' "$out" | sed -n 's/^ *Sent \([0-9]*\) bytes.*/\1/p' | head -n1)
|
||||
sent_pkts=$(printf '%s\n' "$out" | sed -n 's/^ *Sent [0-9]* bytes \([0-9]*\) pkt.*/\1/p' | head -n1)
|
||||
dropped=$(printf '%s\n' "$out" | sed -n 's/.*dropped \([0-9]*\),.*/\1/p' | head -n1)
|
||||
overlimits=$(printf '%s\n' "$out" | sed -n 's/.*overlimits \([0-9]*\).*/\1/p' | head -n1)
|
||||
backlog_bytes=$(printf '%s\n' "$out" | sed -n 's/^ *backlog \([0-9]*\)b .*/\1/p' | head -n1)
|
||||
backlog_pkts=$(printf '%s\n' "$out" | sed -n 's/^ *backlog [0-9]*b \([0-9]*\)p.*/\1/p' | head -n1)
|
||||
|
||||
printf '{"device":"%s","exists":true,"qdisc":"%s","bandwidth":"%s","sent_bytes":%s,"sent_pkts":%s,"dropped":%s,"overlimits":%s,"backlog_bytes":%s,"backlog_pkts":%s}' \
|
||||
"$(_json_str "$dev")" "$(_json_str "$qdisc_name")" "$(_json_str "$bandwidth")" \
|
||||
"${sent_bytes:-0}" "${sent_pkts:-0}" "${dropped:-0}" "${overlimits:-0}" \
|
||||
"${backlog_bytes:-0}" "${backlog_pkts:-0}"
|
||||
}
|
||||
|
||||
_metrics_json() {
|
||||
local sec="$1" interface ul_if dl_if pid running config_file base_dl min_dl max_dl base_ul min_ul max_ul
|
||||
|
||||
interface=$(uci -q get "sqm.${sec}.interface" 2>/dev/null)
|
||||
ul_if="$interface"
|
||||
dl_if="ifb4${interface}"
|
||||
|
||||
config_file="${CONFIG_DIR}/config.${sec}.sh"
|
||||
pid=""
|
||||
running="false"
|
||||
if [ -f "$config_file" ]; then
|
||||
pid=$(pgrep -f "cake-autorate.sh ${config_file}" 2>/dev/null | head -n1)
|
||||
[ -n "$pid" ] && running="true"
|
||||
fi
|
||||
|
||||
base_dl=$(uci -q get "sqm.${sec}.download" 2>/dev/null)
|
||||
min_dl=$(uci -q get "sqm.${sec}.min_download" 2>/dev/null)
|
||||
max_dl=$(uci -q get "sqm.${sec}.max_download" 2>/dev/null)
|
||||
base_ul=$(uci -q get "sqm.${sec}.upload" 2>/dev/null)
|
||||
min_ul=$(uci -q get "sqm.${sec}.min_upload" 2>/dev/null)
|
||||
max_ul=$(uci -q get "sqm.${sec}.max_upload" 2>/dev/null)
|
||||
|
||||
printf '{"section":"%s","interface":"%s","autorate_running":%s,"pid":%s,"download":{"base_kbps":"%s","min_kbps":"%s","max_kbps":"%s","tc":%s},"upload":{"base_kbps":"%s","min_kbps":"%s","max_kbps":"%s","tc":%s}}' \
|
||||
"$(_json_str "$sec")" "$(_json_str "$interface")" "$running" "${pid:-null}" \
|
||||
"$(_json_str "$base_dl")" "$(_json_str "$min_dl")" "$(_json_str "$max_dl")" "$(_iface_metrics_json "$dl_if")" \
|
||||
"$(_json_str "$base_ul")" "$(_json_str "$min_ul")" "$(_json_str "$max_ul")" "$(_iface_metrics_json "$ul_if")"
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
list)
|
||||
echo '{
|
||||
"get_config": { "section": "str" },
|
||||
"set_config": { "section": "str", "restart": false },
|
||||
"get_metrics": { "section": "str" }
|
||||
}'
|
||||
# get_config/get_metrics: omit "section" to get every sqm queue section.
|
||||
# set_config: pass any subset of the known sqm option names alongside
|
||||
# "section" (e.g. download, max_download, delay_thr_ms, ...); only
|
||||
# recognised options are applied. restart=true also bounces the
|
||||
# sqm-autorate service afterwards (regenerates and relaunches every
|
||||
# instance -- there is no per-interface restart, see launcher.sh).
|
||||
;;
|
||||
|
||||
call)
|
||||
input=$(cat)
|
||||
case "$2" in
|
||||
get_config)
|
||||
section=$(echo "$input" | jsonfilter -q -e '@.section')
|
||||
if [ -n "$section" ]; then
|
||||
if ! _section_exists "$section"; then
|
||||
echo "{\"error\":\"section '$(_json_str "$section")' not found in sqm config\"}"
|
||||
exit 1
|
||||
fi
|
||||
_section_json "$section"
|
||||
else
|
||||
entries=""
|
||||
for sec in $(_all_sections); do
|
||||
[ -n "$entries" ] && entries="${entries},"
|
||||
entries="${entries}$(_section_json "$sec")"
|
||||
done
|
||||
printf '{"sections":[%s]}' "$entries"
|
||||
fi
|
||||
;;
|
||||
|
||||
set_config)
|
||||
section=$(echo "$input" | jsonfilter -q -e '@.section')
|
||||
restart=$(echo "$input" | jsonfilter -q -e '@.restart')
|
||||
|
||||
if [ -z "$section" ]; then
|
||||
echo '{"error":"missing section parameter"}'
|
||||
exit 1
|
||||
fi
|
||||
if ! _section_exists "$section"; then
|
||||
echo "{\"error\":\"section '$(_json_str "$section")' not found in sqm config\"}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
applied=""
|
||||
for opt in $VALID_OPTIONS; do
|
||||
val=$(echo "$input" | jsonfilter -q -e "@.${opt}" 2>/dev/null)
|
||||
[ -z "$val" ] && continue
|
||||
# cake-autorate's config validation classifies delay_thr_ms
|
||||
# by whether it contains a decimal point (typeof: float vs
|
||||
# integer) -- a plain integer here fails instance startup
|
||||
# with "not a valid value of type: 'float'" (confirmed on
|
||||
# the bench). Force the decimal so set_config can't
|
||||
# reintroduce that trap.
|
||||
if [ "$opt" = "delay_thr_ms" ]; then
|
||||
case "$val" in
|
||||
*.*) ;;
|
||||
*) val="${val}.0" ;;
|
||||
esac
|
||||
fi
|
||||
uci -q set "sqm.${section}.${opt}=${val}"
|
||||
[ -n "$applied" ] && applied="${applied},"
|
||||
applied="${applied}\"$(_json_str "$opt")\""
|
||||
done
|
||||
|
||||
if [ -z "$applied" ]; then
|
||||
echo '{"error":"no recognised options provided"}'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
uci -q commit sqm
|
||||
|
||||
if [ "$restart" = "true" ]; then
|
||||
/etc/init.d/sqm-autorate restart >/dev/null 2>&1 &
|
||||
fi
|
||||
|
||||
printf '{"result":"ok","section":"%s","applied":[%s],"restarted":%s}' \
|
||||
"$(_json_str "$section")" "$applied" "$([ "$restart" = "true" ] && echo true || echo false)"
|
||||
;;
|
||||
|
||||
get_metrics)
|
||||
section=$(echo "$input" | jsonfilter -q -e '@.section')
|
||||
if [ -n "$section" ]; then
|
||||
if ! _section_exists "$section"; then
|
||||
echo "{\"error\":\"section '$(_json_str "$section")' not found in sqm config\"}"
|
||||
exit 1
|
||||
fi
|
||||
_metrics_json "$section"
|
||||
else
|
||||
entries=""
|
||||
for sec in $(_all_sections); do
|
||||
[ -n "$entries" ] && entries="${entries},"
|
||||
entries="${entries}$(_metrics_json "$sec")"
|
||||
done
|
||||
printf '{"sections":[%s]}' "$entries"
|
||||
fi
|
||||
;;
|
||||
|
||||
*)
|
||||
echo '{"error":"unknown method"}'
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"sqm-autorate": {
|
||||
"description": "Grant ubus access for sqm-autorate",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"sqm-autorate": [ "get_config", "get_metrics" ]
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"sqm-autorate": [ "set_config" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -612,7 +612,22 @@ parse_tsping()
|
||||
|
||||
case "${command[0]}" in
|
||||
REFLECTOR_RESPONSE)
|
||||
read -r timestamp reflector seq _ _ _ _ _ dl_owd_ms ul_owd_ms checksum <<< "${command[@]:1}"
|
||||
if (( ${#command[@]} == 6 ))
|
||||
then
|
||||
# tsping run in ICMP echo mode (e.g. via '-e'/'--icmp-echo' in ping_extra_args,
|
||||
# used so that reflectors which do not answer ICMP timestamp requests can still
|
||||
# be used) only reports a single round trip time and none of the Originate/
|
||||
# Received/Transmit/Finished timestamps used below to derive dl/ul owd, so fall
|
||||
# back to splitting the rtt evenly across dl and ul, exactly as parse_ping() does
|
||||
# for the plain 'ping' binary, which is likewise rtt-only.
|
||||
read -r timestamp reflector seq rtt_ms checksum <<< "${command[@]:1}"
|
||||
dl_owd_ms="${rtt_ms}"
|
||||
ul_owd_ms="${rtt_ms}"
|
||||
icmp_echo_mode=1
|
||||
else
|
||||
read -r timestamp reflector seq _ _ _ _ _ dl_owd_ms ul_owd_ms checksum <<< "${command[@]:1}"
|
||||
icmp_echo_mode=0
|
||||
fi
|
||||
;;
|
||||
|
||||
START_PINGER)
|
||||
@@ -681,8 +696,15 @@ parse_tsping()
|
||||
[[ "${timestamp:-}" && "${reflector:-}" && "${seq:-}" && "${dl_owd_ms:-}" && "${ul_owd_ms:-}" && "${checksum:-}" ]] || continue
|
||||
[[ "${checksum}" == "${timestamp}" ]] || continue
|
||||
|
||||
dl_owd_us="${dl_owd_ms}000"
|
||||
ul_owd_us="${ul_owd_ms}000"
|
||||
if ((icmp_echo_mode))
|
||||
then
|
||||
# rtt-only sample (see note above): split evenly between dl and ul
|
||||
dl_owd_us=$(( (dl_owd_ms * 1000) / 2 ))
|
||||
ul_owd_us="${dl_owd_us}"
|
||||
else
|
||||
dl_owd_us="${dl_owd_ms}000"
|
||||
ul_owd_us="${ul_owd_ms}000"
|
||||
fi
|
||||
|
||||
dl_owd_delta_us=$(( dl_owd_us - dl_owd_baselines_us[${reflector}] ))
|
||||
ul_owd_delta_us=$(( ul_owd_us - ul_owd_baselines_us[${reflector}] ))
|
||||
|
||||
Reference in New Issue
Block a user