mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-02 01:03:56 +08:00
🛸 Sync 2026-08-18 08:57:58
This commit is contained in:
1 parent
a9264dc426
commit
0b567b4be0
56 files changed
+8850
-2138
No files matched your search
@@ -0,0 +1,93 @@
|
||||
/* ── CSS custom properties (same palette as wanstatus.css, duplicated so
|
||||
this page doesn't depend on wanstatus.css having been loaded first) ── */
|
||||
:root {
|
||||
--diag-bg: #ffffff;
|
||||
--diag-border: #e2e6ea;
|
||||
--diag-text: #1a2332;
|
||||
--diag-muted: #6b7a8d;
|
||||
--diag-ok: #22c55e;
|
||||
--diag-warn: #f59e0b;
|
||||
--diag-err: #ef4444;
|
||||
--diag-ok-bg: #f0fdf4;
|
||||
--diag-warn-bg: #fffbeb;
|
||||
--diag-err-bg: #fef2f2;
|
||||
--diag-ok-border: #bbf7d0;
|
||||
--diag-warn-border:#fde68a;
|
||||
--diag-err-border: #fecaca;
|
||||
--diag-radius: 10px;
|
||||
}
|
||||
|
||||
.omrdiag-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin: 10px 0 20px 0;
|
||||
}
|
||||
|
||||
.omrdiag-issue {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--diag-radius);
|
||||
border: 1px solid var(--diag-border);
|
||||
background: var(--diag-bg);
|
||||
}
|
||||
.omrdiag-issue.ok { border-color: var(--diag-ok-border); background: var(--diag-ok-bg); }
|
||||
.omrdiag-issue.warning { border-color: var(--diag-warn-border); background: var(--diag-warn-bg); }
|
||||
.omrdiag-issue.error { border-color: var(--diag-err-border); background: var(--diag-err-bg); }
|
||||
|
||||
.omrdiag-issue .omrdiag-dot {
|
||||
flex: 0 0 10px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin-top: 5px;
|
||||
background: var(--diag-muted);
|
||||
}
|
||||
.omrdiag-issue.ok .omrdiag-dot { background: var(--diag-ok); }
|
||||
.omrdiag-issue.warning .omrdiag-dot { background: var(--diag-warn); }
|
||||
.omrdiag-issue.error .omrdiag-dot { background: var(--diag-err); }
|
||||
|
||||
.omrdiag-issue .omrdiag-body { flex: 1; min-width: 0; }
|
||||
.omrdiag-issue .omrdiag-message { font-weight: 700; color: var(--diag-text); }
|
||||
.omrdiag-issue .omrdiag-detail { font-size: 12px; color: var(--diag-muted); margin-top: 3px; line-height: 1.5; }
|
||||
|
||||
.omrdiag-wan-table,
|
||||
.omrdiag-counters-table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 8px 0 18px 0;
|
||||
}
|
||||
.omrdiag-wan-table th,
|
||||
.omrdiag-wan-table td,
|
||||
.omrdiag-counters-table th,
|
||||
.omrdiag-counters-table td {
|
||||
border: 1px solid var(--diag-border);
|
||||
padding: 6px 10px;
|
||||
text-align: left;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
.omrdiag-wan-table th,
|
||||
.omrdiag-counters-table th {
|
||||
background: rgba(0,0,0,.03);
|
||||
font-weight: 700;
|
||||
}
|
||||
.omrdiag-badge {
|
||||
display: inline-block;
|
||||
padding: 1px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.omrdiag-badge.ok { background: var(--diag-ok-bg); color: #15803d; border: 1px solid var(--diag-ok-border); }
|
||||
.omrdiag-badge.warning { background: var(--diag-warn-bg); color: #b45309; border: 1px solid var(--diag-warn-border); }
|
||||
.omrdiag-badge.error { background: var(--diag-err-bg); color: #b91c1c; border: 1px solid var(--diag-err-border); }
|
||||
|
||||
.omrdiag-counters-table td.omrdiag-nonzero { font-weight: 700; color: #b45309; }
|
||||
|
||||
.omrdiag-updated {
|
||||
font-size: 11px;
|
||||
color: var(--diag-muted);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
|
||||
var callDiagnose = rpc.declare({
|
||||
object: 'luci.mptcp',
|
||||
method: 'diagnose',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
var COUNTER_LABELS = {
|
||||
/* Initial MP_CAPABLE handshake (does this connection become MPTCP at all?) */
|
||||
MPCapableSYNTX: _('MPTCP handshake attempts sent (SYN)'),
|
||||
MPCapableSYNRX: _('MPTCP handshake attempts received (SYN)'),
|
||||
MPCapableSYNACKRX: _('MPTCP handshake accepted by peer (SYN/ACK)'),
|
||||
MPCapableACKRX: _('MPTCP handshake completed (ACK)'),
|
||||
MPCapableFallbackSYNACK: _('Fallback to TCP (SYN/ACK stripped)'),
|
||||
MPCapableFallbackACK: _('Fallback to TCP (ACK stripped)'),
|
||||
MPCapableSYNTXDrop: _('MPTCP handshake dropped client-side, fell back to TCP'),
|
||||
MPCapableSYNTXDisabled: _('MPTCP handshake disabled client-side after past issues'),
|
||||
MPCapableEndpAttempt: _('MPTCP rejected on a port-only endpoint'),
|
||||
MPFallbackTokenInit: _('Could not allocate an MPTCP token, fell back to TCP'),
|
||||
|
||||
/* MP_JOIN handshake (does a WAN succeed in adding a subflow?) */
|
||||
MPJoinSynTx: _('Subflow JOIN attempts sent (SYN)'),
|
||||
MPJoinSynTxCreatSkErr: _('Subflow JOIN failed: could not create socket'),
|
||||
MPJoinSynTxBindErr: _('Subflow JOIN failed: could not bind address'),
|
||||
MPJoinSynTxConnectErr: _('Subflow JOIN failed: could not connect'),
|
||||
MPJoinSynRx: _('Subflow JOIN attempts received (SYN)'),
|
||||
MPJoinSynBackupRx: _('Subflow JOIN attempts received (SYN, backup)'),
|
||||
MPJoinSynAckRx: _('Subflow JOIN accepted by peer (SYN/ACK)'),
|
||||
MPJoinSynAckBackupRx: _('Subflow JOIN accepted by peer (SYN/ACK, backup)'),
|
||||
MPJoinSynAckHMacFailure: _('JOIN SYN/ACK authentication failures'),
|
||||
MPJoinAckRx: _('Subflow JOIN completed (ACK)'),
|
||||
MPJoinAckHMacFailure: _('JOIN ACK authentication failures'),
|
||||
MPJoinNoTokenFound: _('JOIN attempts with unknown token'),
|
||||
MPJoinRejected: _('JOIN request rejected by the path manager'),
|
||||
MPJoinPortSynRx: _('Subflow JOIN SYN received on a different port'),
|
||||
MPJoinPortSynAckRx: _('Subflow JOIN SYN/ACK received on a different port'),
|
||||
MPJoinPortAckRx: _('Subflow JOIN ACK received on a different port'),
|
||||
MismatchPortSynRx: _('Subflow JOIN SYN with mismatched port'),
|
||||
MismatchPortAckRx: _('Subflow JOIN ACK with mismatched port'),
|
||||
|
||||
/* Data-sequence mapping / data path integrity */
|
||||
MPTCPRetrans: _('MPTCP-level retransmissions'),
|
||||
DSSNotMatching: _('Data sequence mapping did not match the previous one'),
|
||||
DSSCorruptionFallback: _('DSS corruption detected, fell back to plain TCP'),
|
||||
DSSCorruptionReset: _('DSS corruption detected, subflow reset'),
|
||||
DSSNoMatchTCP: _('Data sequence mapping did not match TCP sequence numbers'),
|
||||
InfiniteMapTx: _('Infinite mapping sent (fell back to plain TCP semantics)'),
|
||||
InfiniteMapRx: _('Infinite mapping received (peer fell back to plain TCP semantics)'),
|
||||
DataCsumErr: _('Data checksum errors'),
|
||||
NoDSSInWindow: _('Segments received outside the MPTCP mapping window'),
|
||||
DuplicateData: _('Duplicate data segments discarded'),
|
||||
OFOQueueTail: _('Out-of-order segments queued at tail'),
|
||||
OFOQueue: _('Out-of-order segments queued'),
|
||||
OFOMerge: _('Out-of-order segments merged'),
|
||||
|
||||
/* Subflow lifecycle: stale/blackhole detection and removal */
|
||||
Blackhole: _('Blackhole detected on the path'),
|
||||
SubflowStale: _('Subflows marked stale (possible blackhole)'),
|
||||
SubflowRecover: _('Stale subflows recovered'),
|
||||
RmSubflow: _('Subflows removed'),
|
||||
|
||||
/* ADD_ADDR / RM_ADDR signalling (extra WAN addresses advertised to the peer) */
|
||||
AddAddr: _('ADD_ADDR signals received'),
|
||||
AddAddrTx: _('ADD_ADDR signals sent'),
|
||||
AddAddrTxDrop: _('ADD_ADDR signals failed to send'),
|
||||
AddAddrDrop: _('ADD_ADDR signals dropped'),
|
||||
EchoAdd: _('ADD_ADDR echo received'),
|
||||
EchoAddTx: _('ADD_ADDR echo sent'),
|
||||
EchoAddTxDrop: _('ADD_ADDR echo failed to send'),
|
||||
PortAdd: _('ADD_ADDR received with a port number'),
|
||||
RmAddr: _('RM_ADDR signals received'),
|
||||
RmAddrTx: _('RM_ADDR signals sent'),
|
||||
RmAddrTxDrop: _('RM_ADDR signals failed to send'),
|
||||
RmAddrDrop: _('RM_ADDR signals dropped'),
|
||||
|
||||
/* Subflow priority (MP_PRIO: backup/non-backup flag changes) */
|
||||
MPPrioTx: _('Subflow priority change sent (MP_PRIO)'),
|
||||
MPPrioRx: _('Subflow priority change received (MP_PRIO)'),
|
||||
|
||||
/* Connection teardown */
|
||||
MPFailTx: _('Data mapping failures sent (MP_FAIL)'),
|
||||
MPFailRx: _('Data mapping failures received (MP_FAIL)'),
|
||||
MPFastcloseTx: _('Fastclose sent'),
|
||||
MPFastcloseRx: _('Fastclose received'),
|
||||
MPRstTx: _('MPTCP resets sent'),
|
||||
MPRstRx: _('MPTCP resets received'),
|
||||
|
||||
/* Receive-window sharing across subflows */
|
||||
SndWndShared: _('Send window shared across subflows'),
|
||||
RcvWndShared: _('Receive window shared across subflows'),
|
||||
RcvWndConflictUpdate: _('Receive window conflict resolved on update'),
|
||||
RcvWndConflict: _('Receive window update conflicts'),
|
||||
|
||||
/* Misc: whole-connection fallback reasons, gauges */
|
||||
MPCurrEstab: _('Established MPTCP connections (current)'),
|
||||
MPCapableDataFallback: _('Fell back to TCP: missing data on first established packet'),
|
||||
MD5SigFallback: _('Fell back to TCP: conflicting TCP MD5SIG option'),
|
||||
DssFallback: _('Fell back to TCP: bad or missing data sequence signal'),
|
||||
SimultConnectFallback: _('Fell back to TCP: simultaneous connect'),
|
||||
FallbackFailed: _("Couldn't fall back to TCP (connection state didn't allow it)"),
|
||||
WinProbe: _('MPTCP-level zero-window probes')
|
||||
};
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return callDiagnose().catch(function() { return null; });
|
||||
},
|
||||
|
||||
pollData: function(container) {
|
||||
poll.add(L.bind(function() {
|
||||
return callDiagnose().then(L.bind(function(data) {
|
||||
this.renderAll(container, data);
|
||||
}, this)).catch(function() {
|
||||
/* Ignore transient poll errors -- keep showing the last good snapshot */
|
||||
});
|
||||
}, this), 15);
|
||||
},
|
||||
|
||||
render: function(initialData) {
|
||||
if (!document.querySelector('link[href*="mptcp_diagnostics.css"]')) {
|
||||
var link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.type = 'text/css';
|
||||
link.href = L.resource('mptcp/css/mptcp_diagnostics.css');
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
var container = E('div', { 'id': 'omrdiag-container' }, [
|
||||
E('img', { 'src': L.resource('spinner.gif') })
|
||||
]);
|
||||
|
||||
var seeAlso = E('div', { 'class': 'cbi-section' }, [
|
||||
E('legend', {}, [ _('Related pages') ]),
|
||||
E('ul', {}, [
|
||||
E('li', {}, [ E('a', { 'href': L.url('admin/system/openmptcprouter/status') }, [ _('OpenMPTCProuter Network overview / Status') ]) ]),
|
||||
E('li', {}, [ E('a', { 'href': L.url('admin/network/mptcp/mptcp') }, [ _('MPTCP Settings') ]) ]),
|
||||
E('li', {}, [ E('a', { 'href': L.url('admin/network/mptcp/mptcp_fullmesh') }, [ _('MPTCP Fullmesh (subflow map)') ]) ]),
|
||||
E('li', {}, [ E('a', { 'href': L.url('admin/network/mptcp/mptcp_connections') }, [ _('MPTCP Established connections') ]) ]),
|
||||
E('li', {}, [ E('a', { 'href': L.url('admin/network/mptcp/mptcp_monitor') }, [ _('MPTCP monitoring (raw kernel counters)') ]) ])
|
||||
])
|
||||
]);
|
||||
|
||||
var page = E('div', {}, [
|
||||
E('h2', {}, [ _('MPTCP Diagnostics') ]),
|
||||
E('div', { 'class': 'cbi-map-descr' }, [
|
||||
_('Helps explain why MPTCP might not be aggregating your WANs correctly: fallback to plain TCP, ' +
|
||||
'blackholed/stale subflows, resets, checksum errors, and WANs that never got an MPTCP endpoint ' +
|
||||
'registered at all. Refreshes automatically every 15 seconds.')
|
||||
]),
|
||||
container,
|
||||
seeAlso
|
||||
]);
|
||||
|
||||
this.renderAll(container, initialData);
|
||||
this.pollData(container);
|
||||
|
||||
return page;
|
||||
},
|
||||
|
||||
renderAll: function(container, data) {
|
||||
if (!container) return;
|
||||
var d = data || {};
|
||||
|
||||
container.textContent = '';
|
||||
container.appendChild(E('div', { 'class': 'omrdiag-updated' },
|
||||
[ _('Last updated:') + ' ' + (new Date()).toLocaleTimeString() ]));
|
||||
|
||||
container.appendChild(this.renderIssues(d.issues || []));
|
||||
container.appendChild(this.renderWanTable(d.wans || []));
|
||||
container.appendChild(this.renderSettings(d.settings || {}, d.limits || {}, d.established_count));
|
||||
container.appendChild(this.renderCounters(d.counters || {}));
|
||||
},
|
||||
|
||||
renderIssues: function(issues) {
|
||||
var box = E('div', { 'class': 'omrdiag-summary' });
|
||||
if (!issues.length) {
|
||||
box.appendChild(E('div', { 'class': 'omrdiag-issue' }, [
|
||||
E('div', { 'class': 'omrdiag-dot' }),
|
||||
E('div', { 'class': 'omrdiag-body' }, [
|
||||
E('div', { 'class': 'omrdiag-message' }, [ _('No diagnostic data available yet.') ])
|
||||
])
|
||||
]));
|
||||
return box;
|
||||
}
|
||||
issues.forEach(function(issue) {
|
||||
var sev = issue.severity || 'warning';
|
||||
box.appendChild(E('div', { 'class': 'omrdiag-issue ' + sev }, [
|
||||
E('div', { 'class': 'omrdiag-dot' }),
|
||||
E('div', { 'class': 'omrdiag-body' }, [
|
||||
E('div', { 'class': 'omrdiag-message' }, [ issue.message || '' ]),
|
||||
issue.detail ? E('div', { 'class': 'omrdiag-detail' }, [ issue.detail ]) : ''
|
||||
])
|
||||
]));
|
||||
});
|
||||
return box;
|
||||
},
|
||||
|
||||
renderWanTable: function(wans) {
|
||||
var wrap = E('div', {}, [ E('h3', {}, [ _('Per-WAN MPTCP endpoint status') ]) ]);
|
||||
if (!wans.length) {
|
||||
wrap.appendChild(E('p', {}, [ _('No multipath-enabled WAN interfaces found.') ]));
|
||||
return wrap;
|
||||
}
|
||||
var table = E('table', { 'class': 'omrdiag-wan-table' }, [
|
||||
E('tr', {}, [
|
||||
E('th', {}, [ _('WAN') ]),
|
||||
E('th', {}, [ _('Device') ]),
|
||||
E('th', {}, [ _('Mode') ]),
|
||||
E('th', {}, [ _('MPTCP endpoint') ]),
|
||||
E('th', {}, [ _('Endpoint flags') ])
|
||||
])
|
||||
]);
|
||||
wans.forEach(function(wan) {
|
||||
var ok = !!wan.has_endpoint;
|
||||
table.appendChild(E('tr', {}, [
|
||||
E('td', {}, [ wan.name || '' ]),
|
||||
E('td', {}, [ wan.device || '-' ]),
|
||||
E('td', {}, [ wan.multipath || 'off' ]),
|
||||
E('td', {}, [
|
||||
E('span', { 'class': 'omrdiag-badge ' + (ok ? 'ok' : 'error') },
|
||||
[ ok ? _('registered') : _('missing') ])
|
||||
]),
|
||||
E('td', {}, [ wan.endpoint_flags || '-' ])
|
||||
]));
|
||||
});
|
||||
wrap.appendChild(table);
|
||||
return wrap;
|
||||
},
|
||||
|
||||
renderSettings: function(settings, limits, establishedCount) {
|
||||
// "1"/"0" mean enabled/disabled for the enabled and checksum sysctls --
|
||||
// but NOT for path_manager: on the in-kernel MPTCP sysctl layout,
|
||||
// net.mptcp.pm_type is a selector between two path manager
|
||||
// *implementations* (0 = in-kernel, 1 = userspace/mptcpd), not an
|
||||
// on/off switch -- "0" here is the normal default, not "disabled".
|
||||
// On the legacy out-of-tree sysctl layout path_manager is already a
|
||||
// name (e.g. "fullmesh"), not a 0/1 code, so leave those untouched.
|
||||
var formatBool = function(v) {
|
||||
if (v === '1') return _('Enabled') + ' (1)';
|
||||
if (v === '0') return _('Disabled') + ' (0)';
|
||||
return v;
|
||||
};
|
||||
var formatPathManager = function(v) {
|
||||
if (v === '0') return _('In-kernel') + ' (0)';
|
||||
if (v === '1') return _('Userspace') + ' (1)';
|
||||
return v;
|
||||
};
|
||||
var rows = [
|
||||
[ _('MPTCP enabled'), formatBool(settings.enabled) ],
|
||||
[ _('Path manager'), formatPathManager(settings.path_manager) ],
|
||||
[ _('Scheduler'), settings.scheduler ],
|
||||
[ _('Checksum'), formatBool(settings.checksum) ],
|
||||
[ _('Stale loss count threshold'), settings.stale_loss_cnt ],
|
||||
[ _('Blackhole timeout (s)'), settings.blackhole_timeout ],
|
||||
[ _('Add-addr timeout (s)'), settings.add_addr_timeout ],
|
||||
[ _('Close timeout (s)'), settings.close_timeout ],
|
||||
[ _('Subflow limit'), limits.subflows ],
|
||||
[ _('Accepted ADD_ADDR limit'), limits.add_addr_accepted ],
|
||||
[ _('Established MPTCP connections (right now)'), establishedCount ]
|
||||
];
|
||||
var wrap = E('div', {}, [ E('h3', {}, [ _('Kernel MPTCP configuration') ]) ]);
|
||||
var table = E('table', { 'class': 'omrdiag-wan-table' });
|
||||
rows.forEach(function(r) {
|
||||
if (r[1] === undefined || r[1] === null || r[1] === '') return;
|
||||
table.appendChild(E('tr', {}, [ E('th', {}, [ r[0] ]), E('td', {}, [ String(r[1]) ]) ]));
|
||||
});
|
||||
wrap.appendChild(table);
|
||||
return wrap;
|
||||
},
|
||||
|
||||
renderCounters: function(counters) {
|
||||
var keys = Object.keys(counters || {});
|
||||
var wrap = E('div', {}, [ E('h3', {}, [ _('MPTCP kernel counters (since boot)') ]) ]);
|
||||
if (!keys.length) {
|
||||
wrap.appendChild(E('p', {}, [ _('No counters available (nstat/multipath -m returned nothing).') ]));
|
||||
return wrap;
|
||||
}
|
||||
keys.sort(function(a, b) {
|
||||
var av = counters[a] || 0, bv = counters[b] || 0;
|
||||
if (av !== bv) return bv - av;
|
||||
return a < b ? -1 : (a > b ? 1 : 0);
|
||||
});
|
||||
var table = E('table', { 'class': 'omrdiag-counters-table' }, [
|
||||
E('tr', {}, [ E('th', {}, [ _('Counter') ]), E('th', {}, [ _('Value') ]) ])
|
||||
]);
|
||||
keys.forEach(function(key) {
|
||||
var value = counters[key] || 0;
|
||||
var label = COUNTER_LABELS[key] ? (COUNTER_LABELS[key] + ' (' + key + ')') : key;
|
||||
table.appendChild(E('tr', {}, [
|
||||
E('td', {}, [ label ]),
|
||||
E('td', { 'class': value > 0 ? 'omrdiag-nonzero' : '' }, [ String(value) ])
|
||||
]));
|
||||
});
|
||||
wrap.appendChild(table);
|
||||
return wrap;
|
||||
},
|
||||
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -41,6 +41,15 @@ function isEligibleIface(name) {
|
||||
name !== 'omrvpn' && name !== 'OWVPN' && name !== 'omr6in4';
|
||||
}
|
||||
|
||||
// Same "multipath actually turned on" whitelist as the bandwidth page's
|
||||
// _getMultipathIfaces (multipath.js) and the rpcd backend's
|
||||
// multipath_bandwidth: "off"/unset doesn't count.
|
||||
function isMultipathEnabledIface(name) {
|
||||
var multipath = uci.get('network', name, 'multipath');
|
||||
return multipath === 'on' || multipath === 'master' ||
|
||||
multipath === 'backup' || multipath === 'handover';
|
||||
}
|
||||
|
||||
function fillIfaceOptions(option) {
|
||||
option.value('', _('-- not pinned --'));
|
||||
uci.sections('network', 'interface', function(ifc) {
|
||||
@@ -140,7 +149,9 @@ return L.view.extend({
|
||||
|
||||
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.'));
|
||||
_('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.') + ' ' +
|
||||
_('Leave empty to just mirror this row’s Upload interface to the gateway instead -- the same "download always follows upload" behavior mqvpn already has built in natively.') + ' ' +
|
||||
_('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]);
|
||||
@@ -150,9 +161,10 @@ return L.view.extend({
|
||||
|
||||
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.'));
|
||||
_('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.') + ' ' +
|
||||
_('Only WANs with Multipath TCP enabled (the “Multipath TCP” option on the MPTCP page’s Interfaces Settings) are listed below.'));
|
||||
s.filter = function(section) {
|
||||
return isEligibleIface(section);
|
||||
return isEligibleIface(section) && isMultipathEnabledIface(section);
|
||||
};
|
||||
|
||||
o = s.option(form.Value, 'multipath_weight', _('Weight'),
|
||||
|
||||
+1048
-295
File diff suppressed because it is too large.
Load diff
+1051
-286
File diff suppressed because it is too large.
Load diff
+1035
-287
File diff suppressed because it is too large.
Load diff
+984
-154
File diff suppressed because it is too large.
Load diff
+999
-157
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+1067
-313
File diff suppressed because it is too large.
Load diff
@@ -51,6 +51,369 @@ local function bwc_rows(dev)
|
||||
return {}
|
||||
end
|
||||
|
||||
local IP_BIN = "/sbin/ip"
|
||||
|
||||
local function trim(s)
|
||||
return (tostring(s or ""):gsub("^%s+", ""):gsub("%s+$", ""))
|
||||
end
|
||||
|
||||
local function read_proc(path)
|
||||
local f = io.open(path, "r")
|
||||
if not f then return nil end
|
||||
local v = f:read("*l")
|
||||
f:close()
|
||||
if v == nil then return nil end
|
||||
return trim(v)
|
||||
end
|
||||
|
||||
-- First field of /proc/uptime (seconds since boot), used to turn raw
|
||||
-- cumulative MPTcpExt counters into an "events/hour since boot" rate --
|
||||
-- a bare cumulative count (e.g. "7021 resets") doesn't say whether that's
|
||||
-- alarming or a trickle, since it doesn't say over what timespan.
|
||||
local function uptime_seconds()
|
||||
local raw = read_proc("/proc/uptime")
|
||||
local secs = raw and raw:match("^(%S+)")
|
||||
return secs and tonumber(secs) or nil
|
||||
end
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- MPTCP Diagnostics: explain *why* MPTCP might not be aggregating WANs
|
||||
-- correctly -- kernel-level MPTCP counters (fallback to plain TCP,
|
||||
-- blackhole/stale-subflow detection, resets, checksum errors, JOIN
|
||||
-- failures, ...), the live endpoint/limits state, and a per-WAN cross-check
|
||||
-- of whether each multipath-enabled interface actually has a registered
|
||||
-- MPTCP endpoint -- turned into a plain-English issue list.
|
||||
--
|
||||
-- All the underlying commands (multipath -k/-m/-c, ip mptcp ...) are
|
||||
-- read-only/local and near-instant, so none of them are timeout-wrapped.
|
||||
-- NOTE: never wrap "ip" in "timeout" here -- on this router's busybox
|
||||
-- userland, "timeout N ip ..." has been seen to silently dispatch
|
||||
-- busybox's own crippled "ip" applet instead of the real /sbin/ip, which
|
||||
-- has no "mptcp" subcommand at all. Always call /sbin/ip directly.
|
||||
-- -------------------------------------------------------------------------
|
||||
|
||||
-- Kernel MPTCP sysctl snapshot (works across the legacy out-of-tree sysctl
|
||||
-- layout AND the current in-kernel net.mptcp.* layout -- whichever files
|
||||
-- actually exist on this kernel).
|
||||
function kernel_settings()
|
||||
local settings = {}
|
||||
settings.enabled = read_proc("/proc/sys/net/mptcp/mptcp_enabled")
|
||||
or read_proc("/proc/sys/net/mptcp/enabled")
|
||||
settings.checksum = read_proc("/proc/sys/net/mptcp/mptcp_checksum")
|
||||
or read_proc("/proc/sys/net/mptcp/checksum_enabled")
|
||||
settings.path_manager = read_proc("/proc/sys/net/mptcp/mptcp_path_manager")
|
||||
or read_proc("/proc/sys/net/mptcp/pm_type")
|
||||
settings.scheduler = read_proc("/proc/sys/net/mptcp/mptcp_scheduler")
|
||||
or read_proc("/proc/sys/net/mptcp/scheduler")
|
||||
settings.syn_retries = read_proc("/proc/sys/net/mptcp/mptcp_syn_retries")
|
||||
settings.stale_loss_cnt = read_proc("/proc/sys/net/mptcp/stale_loss_cnt")
|
||||
settings.add_addr_timeout = read_proc("/proc/sys/net/mptcp/add_addr_timeout")
|
||||
settings.close_timeout = read_proc("/proc/sys/net/mptcp/close_timeout")
|
||||
settings.blackhole_timeout = read_proc("/proc/sys/net/mptcp/blackhole_timeout")
|
||||
settings.allow_join_initial_addr_port = read_proc("/proc/sys/net/mptcp/allow_join_initial_addr_port")
|
||||
return settings
|
||||
end
|
||||
|
||||
-- The kernel's MPTcpExt* MIB counters, either as the "nstat -z" layout
|
||||
-- (name, value, rate -- one counter per line, what "multipath -m" prints)
|
||||
-- or the SNMP-style two-line layout (a header line of field names followed
|
||||
-- by a value line, both prefixed "MPTcpExt:", found in /proc/net/netstat
|
||||
-- and the legacy /proc/net/mptcp_net/snmp). Parse both.
|
||||
function parse_counters(raw)
|
||||
local counters = {}
|
||||
local header
|
||||
|
||||
for line in (raw or ""):gmatch("[^\n]+") do
|
||||
-- The nstat-style counter name always continues with a letter right
|
||||
-- after the "MPTcpExt" prefix (e.g. "MPTcpExtMPCapableSYNTX"); the
|
||||
-- SNMP-style header/value lines start "MPTcpExt:" instead. Requiring
|
||||
-- a letter here keeps the two forms from being confused with each
|
||||
-- other (a bare "MPTcpExt:" line's second "word" can itself be a
|
||||
-- number, which would otherwise wrongly match as a counter).
|
||||
local name, value = line:match("^%s*(MPTcpExt%a%w*)%s+(%-?%d+)")
|
||||
if name and value then
|
||||
counters[name:gsub("^MPTcpExt", "")] = tonumber(value)
|
||||
elseif line:match("^MPTcpExt:") then
|
||||
-- SNMP-style: first "MPTcpExt:" line is field names, the next is values
|
||||
if not header then
|
||||
header = {}
|
||||
for field in line:gmatch("%S+") do header[#header + 1] = field end
|
||||
else
|
||||
local i = 0
|
||||
for field in line:gmatch("%S+") do
|
||||
i = i + 1
|
||||
local key = header[i]
|
||||
if key and key ~= "MPTcpExt:" then
|
||||
counters[key] = tonumber(field)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return counters
|
||||
end
|
||||
|
||||
-- Where to read the MPTcpExt* counters from for Diagnostics. Deliberately
|
||||
-- NOT "multipath -m" here: that wraps "nstat -z", and nstat (without -a)
|
||||
-- reports the delta since the *previous* nstat invocation by anyone on the
|
||||
-- box (it keeps a shared per-uid history file) -- confirmed live: two
|
||||
-- consecutive calls a couple seconds apart came back ~0, while "nstat -a -z"
|
||||
-- (ignore history) matched /proc/net/netstat exactly. A page that polls
|
||||
-- every 15s and calls that would show "since last poll" numbers while
|
||||
-- labelling them "since boot", and would race with anything else on the
|
||||
-- router that also calls nstat. Reading the kernel's own procfs SNMP-style
|
||||
-- counters directly sidesteps all of that -- always the true cumulative
|
||||
-- count, no shared state with other callers.
|
||||
function mptcp_counters_raw()
|
||||
local raw = exec_output("cat /proc/net/mptcp_net/snmp 2>/dev/null")
|
||||
if raw == "" then
|
||||
raw = exec_output("grep '^MPTcpExt:' /proc/net/netstat 2>/dev/null")
|
||||
end
|
||||
return raw
|
||||
end
|
||||
|
||||
-- "ip mptcp endpoint show": one line per registered endpoint, e.g.
|
||||
-- 10.0.0.2 id 1 subflow dev eth0
|
||||
-- 88.1.2.3 id 2 signal dev wwan0
|
||||
-- 10.0.0.5 id 3 subflow backup fullmesh dev eth1
|
||||
function parse_endpoints(raw)
|
||||
local endpoints = {}
|
||||
for line in (raw or ""):gmatch("[^\n]+") do
|
||||
local id = line:match("id%s+(%d+)")
|
||||
local dev = line:match("dev%s+(%S+)")
|
||||
local addr = line:match("^%s*(%S+)")
|
||||
if id and addr and addr ~= "id" then
|
||||
endpoints[#endpoints + 1] = {
|
||||
id = tonumber(id),
|
||||
address = addr,
|
||||
dev = dev or "",
|
||||
signal = line:match("%f[%a]signal%f[%A]") ~= nil,
|
||||
subflow = line:match("%f[%a]subflow%f[%A]") ~= nil,
|
||||
backup = line:match("%f[%a]backup%f[%A]") ~= nil,
|
||||
fullmesh = line:match("%f[%a]fullmesh%f[%A]") ~= nil,
|
||||
}
|
||||
end
|
||||
end
|
||||
return endpoints
|
||||
end
|
||||
|
||||
-- "ip mptcp limits show" -> "add_addr_accepted 8 subflows 8"
|
||||
function parse_limits(raw)
|
||||
local limits = {}
|
||||
for key, value in (raw or ""):gmatch("(%a[%w_]*)%s+(%d+)") do
|
||||
limits[key] = tonumber(value)
|
||||
end
|
||||
return limits
|
||||
end
|
||||
|
||||
-- Per-WAN endpoint cross-check: for every interface the router considers
|
||||
-- multipath-enabled, is its device actually registered as an MPTCP
|
||||
-- endpoint? If not, that WAN can never carry an MPTCP subflow no matter
|
||||
-- what the schedulers/counters say.
|
||||
function wan_endpoint_status(endpoints)
|
||||
local wans = {}
|
||||
uci:foreach("network", "interface", function(s)
|
||||
local name = s[".name"]
|
||||
if name == "loopback" then return end
|
||||
|
||||
local multipath = s["multipath"]
|
||||
if multipath == nil or multipath == "" then
|
||||
multipath = uci:get("openmptcprouter", name, "multipath")
|
||||
end
|
||||
multipath = multipath or "off"
|
||||
if multipath == "off" then return end
|
||||
|
||||
local dev = get_device(name)
|
||||
if dev == "" then dev = s["device"] or s["ifname"] or "" end
|
||||
|
||||
local has_endpoint = false
|
||||
local endpoint_flags = ""
|
||||
for _, ep in ipairs(endpoints) do
|
||||
if dev ~= "" and ep.dev == dev then
|
||||
has_endpoint = true
|
||||
local flags = {}
|
||||
if ep.signal then flags[#flags + 1] = "signal" end
|
||||
if ep.subflow then flags[#flags + 1] = "subflow" end
|
||||
if ep.backup then flags[#flags + 1] = "backup" end
|
||||
if ep.fullmesh then flags[#flags + 1] = "fullmesh" end
|
||||
endpoint_flags = table.concat(flags, ",")
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
wans[#wans + 1] = {
|
||||
name = name,
|
||||
device = dev,
|
||||
multipath = multipath,
|
||||
has_endpoint = has_endpoint,
|
||||
endpoint_flags = endpoint_flags,
|
||||
}
|
||||
end)
|
||||
return wans
|
||||
end
|
||||
|
||||
-- Turn everything above into a plain-English issue list.
|
||||
local function add_issue(issues, severity, code, message, detail)
|
||||
issues[#issues + 1] = {
|
||||
severity = severity,
|
||||
code = code,
|
||||
message = message,
|
||||
detail = detail or "",
|
||||
}
|
||||
end
|
||||
|
||||
function build_issues(settings, counters, wans)
|
||||
local issues = {}
|
||||
|
||||
if settings.enabled == "0" then
|
||||
add_issue(issues, "error", "mptcp_disabled",
|
||||
"MPTCP is disabled in the kernel.",
|
||||
"No connection can use more than one WAN until MPTCP is re-enabled.")
|
||||
end
|
||||
|
||||
for _, wan in ipairs(wans) do
|
||||
if wan.device == "" then
|
||||
add_issue(issues, "warning", "wan_no_device",
|
||||
string.format("%s: multipath is enabled but the interface has no device yet.", wan.name),
|
||||
"The interface may still be coming up (no IP/link yet).")
|
||||
elseif not wan.has_endpoint then
|
||||
add_issue(issues, "error", "wan_no_endpoint",
|
||||
string.format("%s (%s): no MPTCP endpoint registered for this WAN.", wan.name, wan.device),
|
||||
"This WAN will never be used for an MPTCP subflow, regardless of scheduler/weight settings. " ..
|
||||
"Usually fixed by a hotplug/interface restart (which re-runs 'multipath <dev> on|backup'), " ..
|
||||
"or by checking that the interface actually has a global (non-private-only, non-CGNAT) IP.")
|
||||
end
|
||||
end
|
||||
|
||||
local fallback = (counters.MPCapableFallbackSYNACK or 0) + (counters.MPCapableFallbackACK or 0)
|
||||
if fallback > 0 then
|
||||
add_issue(issues, "warning", "mptcp_fallback",
|
||||
string.format("MPTCP fell back to plain TCP %d time(s).", fallback),
|
||||
"A device on the path (firewall/middlebox/some ISP CGNAT boxes) is stripping the MPTCP TCP option " ..
|
||||
"from the SYN or SYN/ACK, so the kernel gave up and used regular single-path TCP for those " ..
|
||||
"connections. Use the MPTCP Support Check page (tracebox) per-WAN to confirm which WAN(s) strip it.")
|
||||
end
|
||||
|
||||
local stale, recover = counters.SubflowStale or 0, counters.SubflowRecover or 0
|
||||
if stale > 0 then
|
||||
local stuck = stale - recover
|
||||
if stuck > 0 then
|
||||
add_issue(issues, "error", "mptcp_blackhole",
|
||||
string.format("%d subflow(s) currently marked stale (possible blackhole) out of %d total.", stuck, stale),
|
||||
"A path stopped acknowledging data for one or more subflows; the kernel is avoiding it. " ..
|
||||
"If this keeps climbing on one specific WAN, that WAN's path is likely being blackholed.")
|
||||
else
|
||||
add_issue(issues, "warning", "mptcp_blackhole_recovered",
|
||||
string.format("%d subflow(s) were marked stale but all recovered.", stale),
|
||||
"Transient -- a path briefly stopped acking data and the kernel recovered it automatically.")
|
||||
end
|
||||
end
|
||||
|
||||
local hmac_fail = (counters.MPJoinSynAckHMacFailure or 0) + (counters.MPJoinAckHMacFailure or 0)
|
||||
if hmac_fail > 0 then
|
||||
add_issue(issues, "warning", "mptcp_join_hmac_failure",
|
||||
string.format("%d MPTCP JOIN handshake authentication failure(s).", hmac_fail),
|
||||
"A new subflow's JOIN handshake failed HMAC verification -- either packets were altered in " ..
|
||||
"transit or the two ends disagree on the connection's key/token.")
|
||||
end
|
||||
|
||||
if (counters.MPJoinNoTokenFound or 0) > 0 then
|
||||
add_issue(issues, "warning", "mptcp_join_no_token",
|
||||
string.format("%d subflow JOIN attempt(s) referenced an unknown token.", counters.MPJoinNoTokenFound),
|
||||
"The peer (usually the VPS) no longer recognizes the MPTCP connection -- often because its proxy " ..
|
||||
"process restarted and lost state. A new subflow can't attach to a connection the peer forgot.")
|
||||
end
|
||||
|
||||
if (counters.DataCsumErr or 0) > 0 then
|
||||
add_issue(issues, "warning", "mptcp_checksum_error",
|
||||
string.format("%d MPTCP data checksum error(s).", counters.DataCsumErr),
|
||||
"Check that 'mptcp_checksum' matches on both the router and the VPS -- a mismatch (or a path " ..
|
||||
"that corrupts data) triggers this.")
|
||||
end
|
||||
|
||||
-- Split by direction, not just summed: which side is doing the closing
|
||||
-- matters. Confirmed live on the bench (2026-08-16) -- a steady climb
|
||||
-- here traced back to OMR's own per-WAN shadowsocks health-check loop
|
||||
-- (omr-tracker-ss) opening a fresh MPTCP connection through the
|
||||
-- mptcp-enabled sslocal proxy every ~10s and closing it abortively, NOT
|
||||
-- to path interference or the VPS dropping the tunnel -- confirmed by
|
||||
-- MPRstRx staying ~0 while MPRstTx climbed, and by reproducing an
|
||||
-- MPRstTx increment live by firing that exact health-check request.
|
||||
local reset_tx = (counters.MPRstTx or 0) + (counters.MPFastcloseTx or 0)
|
||||
local reset_rx = (counters.MPRstRx or 0) + (counters.MPFastcloseRx or 0)
|
||||
local resets = reset_tx + reset_rx
|
||||
if resets > 0 then
|
||||
local rate_suffix = ""
|
||||
local uptime = uptime_seconds()
|
||||
if uptime and uptime >= 60 then
|
||||
rate_suffix = string.format(" (~%.1f/hour since boot)", resets / (uptime / 3600))
|
||||
end
|
||||
if reset_rx == 0 or reset_tx >= reset_rx * 3 then
|
||||
-- Tx-heavy: this router is doing almost all the closing and the
|
||||
-- peer is barely ever resetting back. That's the signature of a
|
||||
-- local abortive close (e.g. a health-check script tearing down
|
||||
-- its own short-lived proxied connection), not the peer/path
|
||||
-- forcing the connection shut.
|
||||
add_issue(issues, "warning", "mptcp_resets",
|
||||
string.format("%d MPTCP-level reset/fastclose event(s) sent by this router%s.", reset_tx, rate_suffix),
|
||||
"Almost all of these were sent by this router, not the peer (received resets are near zero) -- " ..
|
||||
"that points at something local doing an abortive close of its own MPTCP connections (a common " ..
|
||||
"cause: a health-check/keepalive script that opens a proxied connection, makes one quick request, " ..
|
||||
"then closes it hard) rather than active interference on the path or the VPS dropping the tunnel. " ..
|
||||
"Only worth escalating if it's paired with rising SubflowStale/Blackhole counters above.")
|
||||
else
|
||||
add_issue(issues, "warning", "mptcp_resets",
|
||||
string.format("%d MPTCP-level reset/fastclose event(s)%s.", resets, rate_suffix),
|
||||
"One side force-closed the whole MPTCP connection (all subflows at once) rather than a single " ..
|
||||
"subflow -- can indicate active interference or the proxy/VPN tunnel dropping abruptly.")
|
||||
end
|
||||
end
|
||||
|
||||
local addr_drop = (counters.AddAddrDrop or 0) + (counters.RmAddrDrop or 0)
|
||||
if addr_drop > 0 then
|
||||
-- A handful of drops only matters relative to how much ADD_ADDR/RM_ADDR
|
||||
-- traffic there's actually been -- against a large total it's usually a
|
||||
-- one-off from an interface flap or brief restart, already self-healed
|
||||
-- by the next successful signal, not an ongoing problem. Only surface
|
||||
-- this as an issue once drops are a meaningful share of that traffic
|
||||
-- (>1%); with no successful signals at all to compare against, treat
|
||||
-- it as 100% (nothing has gotten through), which is always worth
|
||||
-- surfacing.
|
||||
local addr_total = (counters.AddAddr or 0) + (counters.AddAddrTx or 0)
|
||||
+ (counters.RmAddr or 0) + (counters.RmAddrTx or 0)
|
||||
local addr_drop_pct = addr_total > 0 and (100 * addr_drop / addr_total) or 100
|
||||
if addr_drop_pct > 1 then
|
||||
local ratio_suffix = ""
|
||||
if addr_total > 0 then
|
||||
ratio_suffix = string.format(" (%d of %d ADD_ADDR/RM_ADDR signals, %.3g%%)",
|
||||
addr_drop, addr_total, addr_drop_pct)
|
||||
end
|
||||
add_issue(issues, "warning", "mptcp_addr_signal_dropped",
|
||||
string.format("%d ADD_ADDR/RM_ADDR signal(s) dropped%s.", addr_drop, ratio_suffix),
|
||||
"A WAN address announcement (or removal) didn't make it to the peer, so the peer may never " ..
|
||||
"attempt a subflow toward that WAN. A handful of drops against a much larger signal volume is " ..
|
||||
"usually a one-off from an interface flap or a brief proxy restart, and self-heals on the next " ..
|
||||
"successful signal -- only worth chasing further if this keeps climbing.")
|
||||
end
|
||||
end
|
||||
|
||||
if (counters.MPFailTx or 0) > 0 or (counters.MPFailRx or 0) > 0 then
|
||||
add_issue(issues, "warning", "mptcp_mapping_failure",
|
||||
"MPTCP data mapping failure signalled (MP_FAIL).",
|
||||
"One side couldn't map received data to the right place in the byte stream on a subflow -- " ..
|
||||
"that subflow gets torn down and traffic continues on the others, if any remain.")
|
||||
end
|
||||
|
||||
if #issues == 0 then
|
||||
add_issue(issues, "ok", "no_issues_detected",
|
||||
"No MPTCP fallback, blackhole, reset or checksum issues detected right now.",
|
||||
"This only reflects counters since boot/last reset -- a healthy snapshot doesn't rule out an " ..
|
||||
"intermittent issue on a specific path; keep this page open while reproducing the problem.")
|
||||
end
|
||||
|
||||
return issues
|
||||
end
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- RPC methods
|
||||
-- -------------------------------------------------------------------------
|
||||
@@ -354,6 +717,36 @@ local methods = {
|
||||
return { output = exec_output(cmd) }
|
||||
end
|
||||
},
|
||||
|
||||
-- Diagnostics: kernel MPTCP counters/endpoints/limits + a per-WAN
|
||||
-- endpoint cross-check, turned into a plain-English issue list. See the
|
||||
-- helpers above ("MPTCP Diagnostics" section) for the actual logic.
|
||||
diagnose = {
|
||||
call = function()
|
||||
local settings = kernel_settings()
|
||||
local counters = parse_counters(mptcp_counters_raw())
|
||||
local endpoints = parse_endpoints(exec_output(IP_BIN .. " mptcp endpoint show 2>/dev/null"))
|
||||
local limits = parse_limits(exec_output(IP_BIN .. " mptcp limits show 2>/dev/null"))
|
||||
local wans = wan_endpoint_status(endpoints)
|
||||
local issues = build_issues(settings, counters, wans)
|
||||
|
||||
local connections_raw = exec_output("multipath -c 2>/dev/null")
|
||||
local established_count = 0
|
||||
for _ in connections_raw:gmatch("[Ee][Ss][Tt][Aa][Bb]") do
|
||||
established_count = established_count + 1
|
||||
end
|
||||
|
||||
return {
|
||||
settings = settings,
|
||||
counters = counters,
|
||||
endpoints = endpoints,
|
||||
limits = limits,
|
||||
wans = wans,
|
||||
issues = issues,
|
||||
established_count = established_count,
|
||||
}
|
||||
end
|
||||
},
|
||||
}
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
|
||||
@@ -21,6 +21,17 @@
|
||||
"acl": [ "luci-app-mptcp" ]
|
||||
}
|
||||
},
|
||||
"admin/network/mptcp/mptcp_diagnostics": {
|
||||
"title": "Diagnostics",
|
||||
"order": 12,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "mptcp/mptcp_diagnostics"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-mptcp" ]
|
||||
}
|
||||
},
|
||||
"admin/network/mptcp/mptcp_dscp_routing": {
|
||||
"title": "DSCP / Weight Routing",
|
||||
"order": 15,
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
"get_settings",
|
||||
"set_settings",
|
||||
"get_interface_settings",
|
||||
"set_interface_settings"
|
||||
"set_interface_settings",
|
||||
"diagnose"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user