🎁 Sync 2026-08-13 00:16:16

This commit is contained in:
github-actions[bot]
2026-08-13 00:16:16 +08:00
parent 5c244874df
commit 2a6de1e65f
29 changed files with 1230 additions and 43 deletions
+2 -2
View File
@@ -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
+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:=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
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:=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)
+2 -2
View File
@@ -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)
+2 -2
View File
@@ -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;
});
}
});
+24
View File
@@ -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"
+24
View File
@@ -1112,6 +1112,30 @@ msgstr "Réinitialiser"
msgid "Today"
msgstr "Aujourd'hui"
msgid "Yesterday"
msgstr "Hier"
msgid "Support the Author"
msgstr "Soutenir lauteur"
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"
+24
View File
@@ -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 "今週"
+24
View File
@@ -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ń"
+24
View File
@@ -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 "На этой неделе"
+33
View File
@@ -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 "本周"
+33
View File
@@ -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 WANs 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 mqvpns 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 WANs 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 MPTCPs bpf_dscp scheduler (local dscp_iface map) and, for mqvpn, announces the class on this paths 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 MPTCPs bpf_dscp scheduler (pins the VPSs 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 mqvpns "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
View File
@@ -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)
+32 -4
View File
@@ -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
+19 -4
View File
@@ -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;;
"")
+16
View File
@@ -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
}
+2 -2
View File
@@ -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
View File
@@ -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}] ))