mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-30 02:35:13 +08:00
⛄ Sync 2026-04-23 20:32:35
This commit is contained in:
1 parent
17738c6f79
commit
fe51d2b304
206 files changed
+725671
No files matched your search
@@ -0,0 +1,411 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
|
||||
|
||||
const svcParamKeyMap = {
|
||||
/* RFC9460 §14.3.2 */
|
||||
mandatory: 0,
|
||||
alpn: 1,
|
||||
'no-default-alpn': 2,
|
||||
port: 3,
|
||||
ipv4hint: 4,
|
||||
ech: 5,
|
||||
ipv6hint: 6
|
||||
};
|
||||
|
||||
|
||||
return baseclass.extend({
|
||||
|
||||
/* RFC9460 Test Vectors pass:
|
||||
D1 Figure 2: AliasMode
|
||||
D2 Figure 3: TargetName Is "."
|
||||
D2 Figure 4: Specifies a Port
|
||||
D2 Figure 5: A Generic Key and Unquoted Value
|
||||
|
||||
D2 Figure 7: Two Quoted IPv6 Hints
|
||||
D2 Figure 8: An IPv6 Hint Using the Embedded IPv4 Syntax
|
||||
D2 Figure 9: SvcParamKey Ordering Is Arbitrary in Presentation Format but Sorted in Wire Format
|
||||
|
||||
Failure cases (pass):
|
||||
D3 Figure 11: Multiple Instances of the Same SvcParamKey
|
||||
D3 Figure 12: Missing SvcParamValues That Must Be Non-Empty
|
||||
D3 Figure 13: The "no-default-alpn" SvcParamKey Value Must Be Empty
|
||||
D3 Figure 14: A Mandatory SvcParam Is Missing
|
||||
D3 Figure 15: The "mandatory" SvcParamKey Must Not Be Included in the Mandatory List
|
||||
D3 Figure 16: Multiple Instances of the Same SvcParamKey in the Mandatory List
|
||||
|
||||
Encoding - Not implemented - escape sequence handling
|
||||
D2 Figure 6: A Generic Key and Quoted Value with a Decimal Escape
|
||||
D2 Figure 10: An "alpn" Value with an Escaped Comma and an Escaped Backslash in Two Presentation Formats
|
||||
*/
|
||||
|
||||
buildSvcbHex(priority, target, params) {
|
||||
let buf = [];
|
||||
|
||||
priority = isNaN(priority) ? 1 : priority;
|
||||
|
||||
// Priority: 2 bytes
|
||||
buf.push((priority >> 8) & 0xff, priority & 0xff);
|
||||
|
||||
// TargetName in DNS wire format (labels with length prefixes)
|
||||
if (target !== '.') { // D2 Figure 3
|
||||
if (target.endsWith('.')) target = target.slice(0, -1);
|
||||
target.split('.').forEach(part => {
|
||||
buf.push(part.length);
|
||||
for (let i = 0; i < part.length; i++)
|
||||
buf.push(part.charCodeAt(i));
|
||||
});
|
||||
}
|
||||
buf.push(0); // end of name
|
||||
|
||||
if (priority === 0) {
|
||||
// AliasMode (priority 0) shall point to something; target '.' is ServiceMode
|
||||
if (target === '.') return null;
|
||||
|
||||
/* RFC 9461 §1.2: "SvcPriority (Section 2.4.1): The priority of this record
|
||||
(relative to others, with lower values preferred). A value of 0 indicates AliasMode."
|
||||
So return here - AliasMode needs only priority and target. */
|
||||
return buf.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
// Collect all parameters as { keyNum, keyName, valueBytes }
|
||||
const seenKeys = new Set();
|
||||
const paramList = [];
|
||||
let mandatoryKeys = new Set();
|
||||
let definedAlpn = new Set();
|
||||
let noDefaultAlpn = false;
|
||||
|
||||
params.forEach(line => {
|
||||
if (!line.trim()) return;
|
||||
|
||||
let [keyName, val = ''] = line.split('=');
|
||||
keyName = keyName.trim().replace(/^"(.*)"$/, '$1');
|
||||
val = val.trim().replace(/^"(.*)"$/, '$1');
|
||||
|
||||
let keyNum = this.svcParamKeyToNumber(keyName);
|
||||
if (keyNum == null) return null; // Stop on unknown keys
|
||||
|
||||
// Stop on duplicate keys - D3 Figure 11
|
||||
if (seenKeys.has(keyName)) return null;
|
||||
seenKeys.add(keyName);
|
||||
|
||||
// Only 'no-default-alpn' key takes no values - D3 Figure 12
|
||||
if (keyNum !== 2 && val === '')
|
||||
return null;
|
||||
|
||||
// Stash 'mandatory' keys
|
||||
if (keyNum === 0) mandatoryKeys = new Set(val.split(',').filter(n => n != ''));
|
||||
|
||||
// Stash 'alpn' values
|
||||
if (keyNum === 1) definedAlpn = new Set(val.split(',').filter(n => n != ''));
|
||||
|
||||
// Encountered 'no-default-alpn'
|
||||
if (keyNum === 2) noDefaultAlpn = true;
|
||||
|
||||
let valueBytes = this.encodeSvcParamValue(keyName, val);
|
||||
paramList.push({ keyNum, keyName, valueBytes });
|
||||
});
|
||||
|
||||
/* RFC9460 - §7.1.1
|
||||
When "no-default-alpn" is specified in an RR, "alpn" must also be
|
||||
specified in order for the RR to be "self-consistent" (Section 2.4.3). */
|
||||
if (noDefaultAlpn && definedAlpn.size === 0) return null;
|
||||
|
||||
// Ensure we got mandated keys - D3 Figure 14
|
||||
for (const key of mandatoryKeys) {
|
||||
if (!seenKeys.has(key)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by numeric key - D2 Figure 9
|
||||
paramList.sort((a, b) => a.keyNum - b.keyNum);
|
||||
|
||||
// Write each key/value in wire format
|
||||
for (const p of paramList) {
|
||||
buf.push((p.keyNum >> 8) & 0xff, p.keyNum & 0xff);
|
||||
buf.push((p.valueBytes.length >> 8) & 0xff, p.valueBytes.length & 0xff);
|
||||
buf.push(...p.valueBytes);
|
||||
}
|
||||
|
||||
// Convert to hex string
|
||||
return buf.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
},
|
||||
|
||||
svcParamKeyToNumber(name) {
|
||||
name = name.toLowerCase();
|
||||
if (name in svcParamKeyMap)
|
||||
return svcParamKeyMap[name];
|
||||
|
||||
const match = name.match(/^key(\d{1,5})$/);
|
||||
if (match) {
|
||||
const n = parseInt(match[1], 10);
|
||||
if (n >= 0 && n <= 65535) return n;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
encodeSvcParamValue(key, value) {
|
||||
const seen = new Set();
|
||||
let keys, port;
|
||||
switch (key) {
|
||||
case 'mandatory':
|
||||
keys = value.split(',')
|
||||
.map(k => k.trim())
|
||||
.filter(k => {
|
||||
if (seen.has(k)) return false; // D3 Figure 16
|
||||
seen.add(k);
|
||||
return true;
|
||||
})
|
||||
.map(k => this.svcParamKeyToNumber(k))
|
||||
.filter(n => n != null)
|
||||
.filter(n => n != 0) // D3 Figure 15
|
||||
.sort((a, b) => a - b); // Ascending order - D2 Figure 9
|
||||
return keys.map(n => [(n >> 8) & 0xff, n & 0xff]).flat();
|
||||
|
||||
case 'ech': // Assume ech is in base64
|
||||
return Array.prototype.map.call(atob(value), c => c.charCodeAt(0)); // OR Uint8Array.fromBase64(value)
|
||||
case 'alpn':
|
||||
/* (RFC 9460 §7.1.1 The wire-format value for "alpn" consists of
|
||||
at least one alpn-id prefixed by its length as a single octet */
|
||||
return value.split(',').map(v => {
|
||||
const len = v.length;
|
||||
return [len, ...[...v].map(c => c.charCodeAt(0))];
|
||||
}).flat();
|
||||
|
||||
case 'no-default-alpn':
|
||||
return []; // zero-length value - D3 Figure 13
|
||||
|
||||
case 'port': // D2 Figure 4
|
||||
port = parseInt(value, 10);
|
||||
return [(port >> 8) & 0xff, port & 0xff];
|
||||
|
||||
case 'ipv4hint':
|
||||
return value.split(',').map(ip => ip.trim().split('.').map(x => parseInt(x, 10))).flat();
|
||||
|
||||
// case 'ech':
|
||||
// return value.match(/.{1,2}/g).map(b => parseInt(b, 16));
|
||||
|
||||
case 'ipv6hint':
|
||||
return value.split(',').map(ip => {
|
||||
ip = ip.trim();
|
||||
|
||||
// Check for IPv4-in-IPv6 (e.g. ::192.0.2.33) - D2 Figure 8
|
||||
let ipv4Tail = null;
|
||||
if (ip.match(/\d+\.\d+\.\d+\.\d+$/)) {
|
||||
const parts = ip.split(':');
|
||||
ipv4Tail = parts.pop(); // last part is IPv4
|
||||
ip = parts.join(':');
|
||||
|
||||
const octets = ipv4Tail.split('.').map(n => parseInt(n, 10));
|
||||
if (octets.length !== 4) return null;
|
||||
|
||||
const word1 = ((octets[0] << 8) | octets[1]).toString(16).padStart(4, '0');
|
||||
const word2 = ((octets[2] << 8) | octets[3]).toString(16).padStart(4, '0');
|
||||
|
||||
ip += `:${word1}:${word2}`;
|
||||
}
|
||||
|
||||
// Split and expand abbreviated ::
|
||||
let parts = ip.trim().split(':');
|
||||
// Expand shorthand :: into full 8-part address
|
||||
if (parts.includes('')) {
|
||||
const missing = 8 - parts.filter(p => p !== '').length;
|
||||
const expanded = [];
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (parts[i] === '' && (i === 0 || parts[i - 1] !== '')) {
|
||||
for (let j = 0; j < missing; j++) expanded.push('0000');
|
||||
} else if (parts[i] !== '') {
|
||||
expanded.push(parts[i].padStart(4, '0'));
|
||||
}
|
||||
}
|
||||
parts = expanded;
|
||||
} else {
|
||||
parts = parts.map(p => p.padStart(4, '0'));
|
||||
}
|
||||
return parts.map(p => [
|
||||
parseInt(p.slice(0, 2), 16),
|
||||
parseInt(p.slice(2, 4), 16)
|
||||
]).flat();
|
||||
}).flat();
|
||||
|
||||
default:
|
||||
// Support custom keyNNNN = value (RFC 9461 §8)
|
||||
/* In wire format, the keys are represented by their numeric values
|
||||
in network byte order, concatenated in strictly increasing numeric order. */
|
||||
if (/^key\d{1,5}$/i.test(key)) {
|
||||
return value.split(',').map(v => {
|
||||
// interpret as ASCII text — one value or comma-separated
|
||||
return [...v].map(c => c.charCodeAt(0));
|
||||
}).flat();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
parseSvcbHex(hex) {
|
||||
if (!hex) return null;
|
||||
|
||||
let data = hex.replace(/[\s:]/g, '').toLowerCase();
|
||||
let buf = new Uint8Array(data.match(/.{2}/g).map(b => parseInt(b, 16)));
|
||||
let view = new DataView(buf.buffer);
|
||||
|
||||
let offset = 0;
|
||||
|
||||
// Parse priority
|
||||
if (buf.length < 2) return null;
|
||||
let priority = view.getUint16(offset);
|
||||
offset += 2;
|
||||
|
||||
// Parse target name (DNS wire format)
|
||||
function parseName() {
|
||||
let labels = [];
|
||||
while (offset < buf.length) {
|
||||
let len = buf[offset++];
|
||||
if (len === 0) break;
|
||||
if (offset + len > buf.length) return null;
|
||||
let label = String.fromCharCode(...buf.slice(offset, offset + len));
|
||||
labels.push(label);
|
||||
offset += len;
|
||||
}
|
||||
return labels.join('.') + '.';
|
||||
}
|
||||
let target = parseName();
|
||||
if (target === null) return null;
|
||||
|
||||
let svcParams = [];
|
||||
|
||||
// Parse svcParams
|
||||
while (offset + 4 <= buf.length) {
|
||||
let key = view.getUint16(offset);
|
||||
let len = view.getUint16(offset + 2);
|
||||
offset += 4;
|
||||
|
||||
if (offset + len > buf.length) break;
|
||||
|
||||
let valBuf = buf.slice(offset, offset + len);
|
||||
offset += len;
|
||||
|
||||
let keyname = this.svcParamKeyFromNumber(key);
|
||||
|
||||
// Handle empty-value flag "no-default-alpn"
|
||||
if (keyname === 'no-default-alpn' && valBuf.length === 0) {
|
||||
svcParams.push(keyname);
|
||||
} else {
|
||||
let valstr = this.decodeSvcParamValue(keyname, valBuf);
|
||||
svcParams.push(`${keyname}=${valstr}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
priority,
|
||||
target,
|
||||
params: svcParams
|
||||
};
|
||||
},
|
||||
|
||||
svcParamKeyFromNumber(num) {
|
||||
for (const [key, val] of Object.entries(svcParamKeyMap)) {
|
||||
if (val === num) return key;
|
||||
}
|
||||
return `key${num}`;
|
||||
},
|
||||
|
||||
decodeSvcParamValue(key, buf) {
|
||||
const keys = [];
|
||||
const addrs = [];
|
||||
|
||||
switch (key) {
|
||||
case 'mandatory':
|
||||
for (let i = 0; i + 1 < buf.length; i += 2) {
|
||||
const k = (buf[i] << 8) | buf[i + 1];
|
||||
keys.push(this.svcParamKeyFromNumber(k));
|
||||
}
|
||||
return keys.join(',');
|
||||
|
||||
case 'ech':
|
||||
return btoa(String.fromCharCode(...buf)); // OR buf.toBase64()
|
||||
case 'alpn': {
|
||||
let pos = 0, result = [];
|
||||
while (pos < buf.length) {
|
||||
let len = buf[pos++];
|
||||
if (pos + len > buf.length) break;
|
||||
let s = String.fromCharCode(...buf.slice(pos, pos + len));
|
||||
result.push(s);
|
||||
pos += len;
|
||||
}
|
||||
return result.join(',');
|
||||
}
|
||||
|
||||
case 'no-default-alpn':
|
||||
return ''; // Flag only
|
||||
|
||||
case 'port':
|
||||
return (buf[0] << 8 | buf[1]).toString();
|
||||
|
||||
case 'ipv4hint':
|
||||
return [...buf].reduce((acc, byte, i) => {
|
||||
if (i % 4 === 0) acc.push([]);
|
||||
acc[acc.length - 1].push(byte);
|
||||
return acc;
|
||||
}, []).map(ip => ip.join('.')).join(',');
|
||||
|
||||
// case 'ech':
|
||||
// return Array.from(buf).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
case 'ipv6hint':
|
||||
for (let i = 0; i + 15 <= buf.length; i += 16) {
|
||||
let addr = [];
|
||||
for (let j = 0; j < 16; j += 2) {
|
||||
const hi = buf[i + j];
|
||||
const lo = buf[i + j + 1];
|
||||
const word = ((hi << 8) | lo).toString(16).padStart(4, '0');
|
||||
addr.push(word);
|
||||
}
|
||||
addrs.push(this.compressIPv6(addr));
|
||||
}
|
||||
return addrs.join(',');
|
||||
|
||||
default:
|
||||
// Decode keyNNNN=... as raw ASCII if it's a custom numeric key
|
||||
if (/^key\d{1,5}$/i.test(key)) {
|
||||
return String.fromCharCode(...buf);
|
||||
}
|
||||
return Array.from(buf).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
},
|
||||
|
||||
compressIPv6(hextets) {
|
||||
// hextets: Array of 8 strings like ['2001', '0db8', '0000', ..., '0001']
|
||||
|
||||
// Normalize to lowercase + strip leading zeros
|
||||
const normalized = hextets.map(h => parseInt(h, 16).toString(16));
|
||||
|
||||
// Find the longest run of zeroes
|
||||
let bestStart = -1, bestLen = 0;
|
||||
for (let i = 0; i < normalized.length; ) {
|
||||
if (normalized[i] !== '0') {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
let start = i;
|
||||
while (i < normalized.length && normalized[i] === '0') i++;
|
||||
let len = i - start;
|
||||
if (len > bestLen) {
|
||||
bestStart = start;
|
||||
bestLen = len;
|
||||
}
|
||||
}
|
||||
|
||||
// If no run of two or more zeroes, no compression
|
||||
if (bestLen < 2) return normalized.join(':');
|
||||
|
||||
// Compress
|
||||
const head = normalized.slice(0, bestStart).join(':');
|
||||
const tail = normalized.slice(bestStart + bestLen).join(':');
|
||||
if (head && tail) return `${head}::${tail}`;
|
||||
else if (head) return `${head}::`;
|
||||
else if (tail) return `::${tail}`;
|
||||
else return `::`;
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,176 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require dom';
|
||||
'require fs';
|
||||
'require ui';
|
||||
'require uci';
|
||||
'require network';
|
||||
|
||||
return view.extend({
|
||||
handleCommand(exec, args) {
|
||||
const buttons = document.querySelectorAll('.diag-action > .cbi-button');
|
||||
const out = document.querySelector('textarea');
|
||||
|
||||
for (const button of buttons)
|
||||
button.setAttribute('disabled', 'true');
|
||||
|
||||
return fs.exec_direct(exec, args, 'text', false, true, (ev) => {
|
||||
out.textContent = ev.target.response;
|
||||
}).then((res) => {
|
||||
out.textContent = res;
|
||||
}).catch((err) => {
|
||||
ui.addNotification(null, E('p', [ err ]))
|
||||
}).finally(() => {
|
||||
for (const button of buttons)
|
||||
button.removeAttribute('disabled');
|
||||
});
|
||||
},
|
||||
|
||||
handlePing(ev, cmd) {
|
||||
const exec = cmd || 'ping';
|
||||
const addr = ev.currentTarget.parentNode.previousSibling.value;
|
||||
const args = (exec == 'ping') ? [ '-4', '-c', '5', '-W', '1', addr ] : [ '-6', '-c', '5', addr ];
|
||||
|
||||
return this.handleCommand(exec, args);
|
||||
},
|
||||
|
||||
handleTraceroute(ev, cmd) {
|
||||
const exec = cmd || 'traceroute';
|
||||
const addr = ev.currentTarget.parentNode.previousSibling.value;
|
||||
const args = (exec == 'traceroute') ? [ '-4', '-q', '1', '-w', '1', '-n', '-m', String(L.env.rpctimeout || 20), addr ] : [ '-q', '1', '-w', '2', '-n', addr ];
|
||||
|
||||
return this.handleCommand(exec, args);
|
||||
},
|
||||
|
||||
handleNslookup(ev, cmd) {
|
||||
const addr = ev.currentTarget.parentNode.previousSibling.value;
|
||||
|
||||
return this.handleCommand('nslookup', [ addr ]);
|
||||
},
|
||||
|
||||
handleArpScan(ev, cmd) {
|
||||
const addr = ev.currentTarget.parentNode.previousSibling.value;
|
||||
|
||||
return this.handleCommand('arp-scan', [ '-l', '-I', addr ]);
|
||||
},
|
||||
|
||||
load() {
|
||||
return Promise.all([
|
||||
L.resolveDefault(fs.stat('/bin/ping6') || fs.stat('/usr/bin/ping6'), false),
|
||||
L.resolveDefault(fs.stat('/bin/traceroute6') || fs.stat('/usr/bin/traceroute6'), false),
|
||||
L.resolveDefault(fs.stat('/usr/bin/arp-scan'), false),
|
||||
network.getDevices(),
|
||||
uci.load('luci')
|
||||
]);
|
||||
},
|
||||
|
||||
render([has_ping6, has_traceroute6, has_arpscan, devices]) {
|
||||
const dns_host = uci.get('luci', 'diag', 'dns') || 'openwrt.org';
|
||||
const ping_host = uci.get('luci', 'diag', 'ping') || 'openwrt.org';
|
||||
const route_host = uci.get('luci', 'diag', 'route') || 'openwrt.org';
|
||||
|
||||
const table = E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left', 'style': 'overflow:initial' }, [
|
||||
E('input', {
|
||||
'style': 'margin:5px 0',
|
||||
'type': 'text',
|
||||
'value': ping_host
|
||||
}),
|
||||
E('span', { 'class': 'diag-action' }, [
|
||||
has_ping6 ? new ui.ComboButton('ping', {
|
||||
'ping': '%s %s'.format(_('IPv4'), _('Ping')),
|
||||
'ping6': '%s %s'.format(_('IPv6'), _('Ping')),
|
||||
}, {
|
||||
'click': ui.createHandlerFn(this, 'handlePing'),
|
||||
'classes': {
|
||||
'ping': 'btn cbi-button cbi-button-action',
|
||||
'ping6': 'btn cbi-button cbi-button-action'
|
||||
}
|
||||
}).render() : E('button', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'click': ui.createHandlerFn(this, 'handlePing')
|
||||
}, [ _('Ping') ])
|
||||
])
|
||||
]),
|
||||
|
||||
E('td', { 'class': 'td left', 'style': 'overflow:initial' }, [
|
||||
E('input', {
|
||||
'style': 'margin:5px 0',
|
||||
'type': 'text',
|
||||
'value': route_host
|
||||
}),
|
||||
E('span', { 'class': 'diag-action' }, [
|
||||
has_traceroute6 ? new ui.ComboButton('traceroute', {
|
||||
'traceroute': '%s %s'.format(_('IPv4'), _('Traceroute')),
|
||||
'traceroute6': '%s %s'.format(_('IPv6'), _('Traceroute')),
|
||||
}, {
|
||||
'click': ui.createHandlerFn(this, 'handleTraceroute'),
|
||||
'classes': {
|
||||
'traceroute': 'btn cbi-button cbi-button-action',
|
||||
'traceroute6': 'btn cbi-button cbi-button-action'
|
||||
}
|
||||
}).render() : E('button', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'click': ui.createHandlerFn(this, 'handleTraceroute')
|
||||
}, [ _('Traceroute') ])
|
||||
])
|
||||
]),
|
||||
|
||||
E('td', { 'class': 'td left' }, [
|
||||
E('input', {
|
||||
'style': 'margin:5px 0',
|
||||
'type': 'text',
|
||||
'value': dns_host
|
||||
}),
|
||||
E('span', { 'class': 'diag-action' }, [
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'click': ui.createHandlerFn(this, 'handleNslookup')
|
||||
}, [ _('Nslookup') ])
|
||||
])
|
||||
]),
|
||||
|
||||
has_arpscan ? E('td', { 'class': 'td left' }, [
|
||||
E('select', {
|
||||
'style': 'margin:5px 0'
|
||||
}, devices.map((device) => {
|
||||
if (!device.isUp())
|
||||
return E([]);
|
||||
|
||||
return E('option', { 'value': device.getName() }, [ device.getI18n() ]);
|
||||
})),
|
||||
E('span', { 'class': 'diag-action' }, [
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'click': ui.createHandlerFn(this, 'handleArpScan')
|
||||
}, [ _('Arp-scan') ])
|
||||
])
|
||||
]) : E([]),
|
||||
])
|
||||
]);
|
||||
|
||||
const view = E('div', { 'class': 'cbi-map'}, [
|
||||
E('h2', {}, [ _('Diagnostics') ]),
|
||||
E('div', { 'class': 'cbi-map-descr'}, _('Execution of various network commands to check the connection and name resolution to other systems.')),
|
||||
table,
|
||||
E('div', {'class': 'cbi-section'}, [
|
||||
E('div', { 'id' : 'command-output'},
|
||||
E('textarea', {
|
||||
'id': 'widget.command-output',
|
||||
'style': 'width: 100%; font-family:monospace; white-space:pre',
|
||||
'readonly': true,
|
||||
'wrap': 'off',
|
||||
'rows': '20'
|
||||
})
|
||||
)
|
||||
])
|
||||
]);
|
||||
|
||||
return view;
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,873 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require dom';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
'require form';
|
||||
'require network';
|
||||
'require validation';
|
||||
'require tools.widgets as widgets';
|
||||
'require tools.dnsrecordhandlers as drh';
|
||||
|
||||
const callHostHints = rpc.declare({
|
||||
object: 'luci-rpc',
|
||||
method: 'getHostHints',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
function validateHostname(sid, s) {
|
||||
if (!s) return true;
|
||||
|
||||
if (s.length > 256)
|
||||
return _('Expecting: %s').format(_('valid hostname'));
|
||||
|
||||
const labels = s.replace(/^\*?\.?|\.$/g, '').split(/\./);
|
||||
|
||||
for (const label of labels) {
|
||||
if (!label.match(/^[a-z0-9_](?:[a-z0-9-]{0,61}[a-z0-9])?$/i))
|
||||
return _('Expecting: %s').format(_('valid hostname'));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateAddressList(sid, s) {
|
||||
if (!s) return true;
|
||||
|
||||
const m = s.match(/^\/(.+)\/$/);
|
||||
const names = m ? m[1].split(/\//) : [ s ];
|
||||
|
||||
for (const name of names) {
|
||||
const res = validateHostname(sid, name);
|
||||
|
||||
if (res !== true)
|
||||
return res;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateServerSpec(sid, s) {
|
||||
if (!s) return true;
|
||||
|
||||
let m = s.match(/^(\/.*\/)?(.*)$/);
|
||||
if (!m)
|
||||
return _('Expecting: %s').format(_('valid hostname'));
|
||||
|
||||
if (m[1] != '//' && m[1] != '/#/') {
|
||||
const res = validateAddressList(sid, m[1]);
|
||||
if (res !== true)
|
||||
return res;
|
||||
}
|
||||
|
||||
if (m[2] == '' || m[2] == '#')
|
||||
return true;
|
||||
|
||||
// ipaddr%scopeid#srvport@source@interface#srcport
|
||||
|
||||
m = m[2].match(/^([0-9a-f:.]+)(?:%[^#@]+)?(?:#(\d+))?(?:@([0-9a-f:.]+)(?:@[^#]+)?(?:#(\d+))?)?$/);
|
||||
|
||||
if (!m)
|
||||
return _('Expecting: %s').format(_('valid IP address'));
|
||||
|
||||
if (validation.parseIPv4(m[1])) {
|
||||
if (m[3] != null && !validation.parseIPv4(m[3]))
|
||||
return _('Expecting: %s').format(_('valid IPv4 address'));
|
||||
}
|
||||
else if (validation.parseIPv6(m[1])) {
|
||||
if (m[3] != null && !validation.parseIPv6(m[3]))
|
||||
return _('Expecting: %s').format(_('valid IPv6 address'));
|
||||
}
|
||||
else {
|
||||
return _('Expecting: %s').format(_('valid IP address'));
|
||||
}
|
||||
|
||||
if ((m[2] != null && +m[2] > 65535) || (m[4] != null && +m[4] > 65535))
|
||||
return _('Expecting: %s').format(_('valid port value'));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load() {
|
||||
return Promise.all([
|
||||
callHostHints(),
|
||||
uci.load('firewall')
|
||||
]);
|
||||
},
|
||||
|
||||
render([hosts]) {
|
||||
let m, s, o, ss, so, dnss;
|
||||
|
||||
let noi18nstrings = {
|
||||
etc_hosts: '<code>/etc/hosts</code>',
|
||||
localhost_v6: '<code>::1</code>',
|
||||
loopback_slash_8_v4: '<code>127.0.0.0/8</code>',
|
||||
not_found: '<code>Not found</code>',
|
||||
nxdomain: '<code>NXDOMAIN</code>',
|
||||
rfc_1918_link: '<a href="https://www.rfc-editor.org/rfc/rfc1918">RFC1918</a>',
|
||||
rfc_4193_link: '<a href="https://www.rfc-editor.org/rfc/rfc4193">RFC4193</a>',
|
||||
rfc_4291_link: '<a href="https://www.rfc-editor.org/rfc/rfc4291">RFC4291</a>',
|
||||
rfc_6303_link: '<a href="https://www.rfc-editor.org/rfc/rfc6303">RFC6303</a>',
|
||||
reverse_arpa: '<code>*.IN-ADDR.ARPA,*.IP6.ARPA</code>',
|
||||
servers_file_entry01: '<code>server=1.2.3.4</code>',
|
||||
servers_file_entry02: '<code>server=/domain/1.2.3.4</code>',
|
||||
|
||||
};
|
||||
|
||||
const recordtypes = [
|
||||
'ANY',
|
||||
'A',
|
||||
'AAAA',
|
||||
'ALIAS',
|
||||
'CAA',
|
||||
'CERT',
|
||||
'CNAME',
|
||||
'DS',
|
||||
'HINFO',
|
||||
'HIP',
|
||||
'HTTPS',
|
||||
'KEY',
|
||||
'LOC',
|
||||
'MX',
|
||||
'NAPTR',
|
||||
'NS',
|
||||
'OPENPGPKEY',
|
||||
'PTR',
|
||||
'RP',
|
||||
'SIG',
|
||||
'SOA',
|
||||
'SRV',
|
||||
'SSHFP',
|
||||
'SVCB',
|
||||
'TLSA',
|
||||
'TXT',
|
||||
'URI',
|
||||
]
|
||||
|
||||
function customi18n(template, values) {
|
||||
if (!values)
|
||||
values = noi18nstrings;
|
||||
return template.replace(/\{(\w+)\}/g, (match, key) => values[key] || match);
|
||||
};
|
||||
|
||||
m = new form.Map('dhcp', _('DNS'));
|
||||
|
||||
s = m.section(form.TypedSection, 'dnsmasq');
|
||||
s.anonymous = false;
|
||||
s.addremove = true;
|
||||
s.addbtntitle = _('Add server instance', 'Dnsmasq instance');
|
||||
|
||||
s.renderContents = function(/* ... */) {
|
||||
const renderTask = form.TypedSection.prototype.renderContents.apply(this, arguments);
|
||||
const sections = this.cfgsections();
|
||||
|
||||
return Promise.resolve(renderTask).then(function(nodes) {
|
||||
if (sections.length < 2) {
|
||||
nodes.querySelector('#cbi-dhcp-dnsmasq > h3').remove();
|
||||
nodes.querySelector('#cbi-dhcp-dnsmasq > .cbi-section-remove').remove();
|
||||
}
|
||||
else {
|
||||
nodes.querySelectorAll('#cbi-dhcp-dnsmasq > .cbi-section-remove').forEach(function(div, i) {
|
||||
const section = uci.get('dhcp', sections[i]);
|
||||
const hline = div.nextElementSibling;
|
||||
const btn = div.firstElementChild;
|
||||
|
||||
if (!section || section['.anonymous']) {
|
||||
hline.innerText = i ? _('Unnamed instance #%d', 'Dnsmasq instance').format(i+1) : _('Default instance', 'Dnsmasq instance');
|
||||
btn.innerText = i ? _('Remove instance #%d', 'Dnsmasq instance').format(i+1) : _('Remove default instance', 'Dnsmasq instance');
|
||||
}
|
||||
else {
|
||||
hline.innerText = _('Instance "%q"', 'Dnsmasq instance').format(section['.name']);
|
||||
btn.innerText = _('Remove instance "%q"', 'Dnsmasq instance').format(section['.name']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
nodes.querySelector('#cbi-dhcp-dnsmasq > .cbi-section-create input').placeholder = _('New instance name…', 'Dnsmasq instance');
|
||||
|
||||
return nodes;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
s.tab('general', _('General'));
|
||||
s.tab('cache', _('Cache'));
|
||||
s.tab('devices', _('Devices & Ports'));
|
||||
s.tab('dnsrecords', _('DNS Records'));
|
||||
s.tab('dnssecopt', _('DNSSEC'));
|
||||
s.tab('filteropts', _('Filter'));
|
||||
s.tab('forward', _('Forwards'));
|
||||
s.tab('limits', _('Limits'));
|
||||
s.tab('logging', _('Log'));
|
||||
s.tab('files', _('Resolv & Hosts Files'));
|
||||
s.tab('ipsets', _('IP Sets'));
|
||||
|
||||
// Begin general
|
||||
|
||||
s.taboption('general', form.Flag, 'dns_redirect',
|
||||
_('DNS redirect'),
|
||||
_('Force redirect all local DNS queries to DNSMasq, a.k.a. DNS Hijacking.'));
|
||||
|
||||
o = s.taboption('general', form.Value, 'local',
|
||||
_('Resolve these locally'),
|
||||
_('Never forward these matching domains or subdomains; resolve from DHCP or hosts files only.'));
|
||||
o.placeholder = '/internal.example.com/private.example.com/example.org';
|
||||
|
||||
s.taboption('general', form.Value, 'domain',
|
||||
_('Local domain'),
|
||||
_('Local domain suffix appended to DHCP names and hosts file entries.'));
|
||||
|
||||
s.taboption('general', form.Flag, 'expandhosts',
|
||||
_('Expand hosts'),
|
||||
_('Add local domain suffix to names served from hosts files.'));
|
||||
|
||||
o = s.taboption('general', form.DynamicList, 'address',
|
||||
_('Addresses'),
|
||||
_('Resolve specified FQDNs to an IP.') + '<br />' +
|
||||
customi18n(_('Syntax: {code_syntax}.'),
|
||||
{code_syntax: '<code>/fqdn[/fqdn…]/[ipaddr]</code>'}) + '<br />' +
|
||||
customi18n(_('{example_nx} returns {nxdomain}.',
|
||||
'hint: <code>/example.com/</code> returns <code>NXDOMAIN</code>.'),
|
||||
{example_nx: '<code>/example.com/</code>', nxdomain: '<code>NXDOMAIN</code>'}) + '<br />' +
|
||||
customi18n(_('{any_domain} matches any domain (and returns {nxdomain}).',
|
||||
'hint: <code>/#/</code> matches any domain (and returns NXDOMAIN).'),
|
||||
{any_domain:'<code>/#/</code>', nxdomain: '<code>NXDOMAIN</code>'}) + '<br />' +
|
||||
customi18n(
|
||||
_('{example_null} returns {null_addr} addresses ({null_ipv4}, {null_ipv6}) for {example_com} and its subdomains.',
|
||||
'hint: <code>/example.com/#</code> returns NULL addresses (<code>0.0.0.0</code>, <code>::</code>) for example.com and its subdomains.'),
|
||||
{ example_null: '<code>/example.com/#</code>',
|
||||
null_addr: '<code>NULL</code>',
|
||||
null_ipv4: '<code>0.0.0.0</code>',
|
||||
null_ipv6: '<code>::</code>',
|
||||
example_com: '<code>example.com</code>',
|
||||
}
|
||||
)
|
||||
);
|
||||
o.optional = true;
|
||||
o.placeholder = '/router.local/router.lan/192.168.0.1';
|
||||
|
||||
o = s.taboption('general', form.Flag, 'allservers',
|
||||
_('All servers'),
|
||||
_('Query all available upstream resolvers.') + ' ' + _('First answer wins.'));
|
||||
o.optional = true;
|
||||
// End general
|
||||
|
||||
// Begin cache
|
||||
o = s.taboption('cache', form.MultiValue, 'cache_rr',
|
||||
_('Cache arbitrary RR'), _('By default, dnsmasq caches A, AAAA, CNAME and SRV DNS record types.') + '<br/>' +
|
||||
_('This option adds additional record types to the cache.'));
|
||||
o.optional = true;
|
||||
o.create = true;
|
||||
o.multiple = true;
|
||||
o.display_size = 5;
|
||||
recordtypes.forEach(r => {
|
||||
o.value(r);
|
||||
});
|
||||
// End cache
|
||||
|
||||
// Begin devices
|
||||
o = s.taboption('devices', form.Flag, 'nonwildcard',
|
||||
_('Non-wildcard'),
|
||||
_('Bind only to configured interface addresses, instead of the wildcard address.'));
|
||||
o.default = o.enabled;
|
||||
o.optional = false;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('devices', widgets.NetworkSelect, 'interface',
|
||||
_('Listen interfaces'),
|
||||
_('Listen only on the specified interfaces, and loopback if not excluded explicitly.'));
|
||||
o.multiple = true;
|
||||
o.nocreate = true;
|
||||
|
||||
o = s.taboption('devices', widgets.IPSelect, 'listen_address',
|
||||
_('Listen addresses'),
|
||||
_('Listen only on the specified addresses.'));
|
||||
o.multiple = true;
|
||||
|
||||
o = s.taboption('devices', widgets.NetworkSelect, 'notinterface',
|
||||
_('Exclude interfaces'),
|
||||
_('Do not listen on the specified interfaces.'));
|
||||
o.loopback = true;
|
||||
o.multiple = true;
|
||||
o.nocreate = true;
|
||||
|
||||
o = s.taboption('devices', form.Value, 'port',
|
||||
_('DNS server port'),
|
||||
_('Listening port for inbound DNS queries.'));
|
||||
o.optional = true;
|
||||
o.datatype = 'port';
|
||||
o.placeholder = 53;
|
||||
|
||||
o = s.taboption('devices', form.Value, 'queryport',
|
||||
_('DNS query port'),
|
||||
_('Fixed source port for outbound DNS queries.'));
|
||||
o.optional = true;
|
||||
o.datatype = 'port';
|
||||
o.placeholder = _('any');
|
||||
|
||||
o = s.taboption('devices', form.Value, 'minport',
|
||||
_('Minimum source port #'),
|
||||
_('Min valid value %s.').format('<code>1024</code>') + ' ' + _('Useful for systems behind firewalls.'));
|
||||
o.optional = true;
|
||||
o.datatype = 'port';
|
||||
o.placeholder = 1024;
|
||||
o.depends('queryport', '');
|
||||
|
||||
o = s.taboption('devices', form.Value, 'maxport',
|
||||
_('Maximum source port #'),
|
||||
_('Max valid value %s.').format('<code>65535</code>') + ' ' + _('Useful for systems behind firewalls.'));
|
||||
o.optional = true;
|
||||
o.datatype = 'port';
|
||||
o.placeholder = 50000;
|
||||
o.depends('queryport', '');
|
||||
// End devices
|
||||
|
||||
// Begin dnsrecords
|
||||
o = s.taboption('dnsrecords', form.SectionValue, '__dnsrecords__', form.TypedSection, '__dnsrecords__');
|
||||
|
||||
dnss = o.subsection;
|
||||
|
||||
dnss.anonymous = true;
|
||||
dnss.cfgsections = function() { return [ '__dnsrecords__' ] };
|
||||
|
||||
dnss.tab('hosts', _('Hostnames'));
|
||||
dnss.tab('srvhosts', _('SRV'));
|
||||
dnss.tab('mxhosts', _('MX'));
|
||||
dnss.tab('cnamehosts', _('CNAME'));
|
||||
dnss.tab('dnsrr', _('DNS-RR'));
|
||||
|
||||
o = dnss.taboption('srvhosts', form.SectionValue, '__srvhosts__', form.TableSection, 'srvhost', null,
|
||||
_('Bind service records to a domain name: specify the location of services. See <a href="%s">RFC2782</a>.').format('https://datatracker.ietf.org/doc/html/rfc2782')
|
||||
+ '<br />' + _('_service: _sip, _ldap, _imap, _stun, _xmpp-client, … . (Note: while _http is possible, no browsers support SRV records.)')
|
||||
+ '<br />' + _('_proto: _tcp, _udp, _sctp, _quic, … .')
|
||||
+ '<br />' + _('You may add multiple records for the same Target.')
|
||||
+ '<br />' + _('Larger weights (of the same prio) are given a proportionately higher probability of being selected.'));
|
||||
|
||||
ss = o.subsection;
|
||||
|
||||
ss.addremove = true;
|
||||
ss.anonymous = true;
|
||||
ss.sortable = true;
|
||||
ss.rowcolors = true;
|
||||
|
||||
so = ss.option(form.Value, 'srv', _('SRV'), _('Syntax:') + ' ' + '<code>_service._proto.example.com.</code>');
|
||||
so.rmempty = false;
|
||||
so.datatype = 'hostname';
|
||||
so.placeholder = '_sip._tcp.example.com.';
|
||||
|
||||
so = ss.option(form.Value, 'target', _('Target'), _('CNAME or fqdn'));
|
||||
so.rmempty = false;
|
||||
so.datatype = 'hostname';
|
||||
so.placeholder = 'sip.example.com.';
|
||||
|
||||
so = ss.option(form.Value, 'port', _('Port'));
|
||||
so.rmempty = false;
|
||||
so.datatype = 'port';
|
||||
so.placeholder = '5060';
|
||||
|
||||
so = ss.option(form.Value, 'class', _('Priority'), _('Ordinal: lower comes first.'));
|
||||
so.rmempty = true;
|
||||
so.datatype = 'range(0,65535)';
|
||||
so.placeholder = '10';
|
||||
|
||||
so = ss.option(form.Value, 'weight', _('Weight'));
|
||||
so.rmempty = true;
|
||||
so.datatype = 'range(0,65535)';
|
||||
so.placeholder = '50';
|
||||
|
||||
o = dnss.taboption('mxhosts', form.SectionValue, '__mxhosts__', form.TableSection, 'mxhost', null,
|
||||
_('Bind service records to a domain name: specify the location of services.')
|
||||
+ '<br />' + _('You may add multiple records for the same domain.'));
|
||||
|
||||
ss = o.subsection;
|
||||
|
||||
ss.addremove = true;
|
||||
ss.anonymous = true;
|
||||
ss.sortable = true;
|
||||
ss.rowcolors = true;
|
||||
ss.nodescriptions = true;
|
||||
|
||||
so = ss.option(form.Value, 'domain', _('Domain'));
|
||||
so.rmempty = false;
|
||||
so.datatype = 'hostname';
|
||||
so.placeholder = 'example.com.';
|
||||
|
||||
so = ss.option(form.Value, 'relay', _('Relay'));
|
||||
so.rmempty = false;
|
||||
so.datatype = 'hostname';
|
||||
so.placeholder = 'relay.example.com.';
|
||||
|
||||
so = ss.option(form.Value, 'pref', _('Priority'), _('Ordinal: lower comes first.'));
|
||||
so.rmempty = true;
|
||||
so.datatype = 'range(0,65535)';
|
||||
so.placeholder = '0';
|
||||
|
||||
o = dnss.taboption('cnamehosts', form.SectionValue, '__cname__', form.TableSection, 'cname', null,
|
||||
_('Set an alias for a hostname.'));
|
||||
|
||||
ss = o.subsection;
|
||||
|
||||
ss.addremove = true;
|
||||
ss.anonymous = true;
|
||||
ss.sortable = true;
|
||||
ss.rowcolors = true;
|
||||
ss.nodescriptions = true;
|
||||
|
||||
so = ss.option(form.Value, 'cname', _('Domain'));
|
||||
so.rmempty = false;
|
||||
so.validate = validateHostname;
|
||||
so.placeholder = 'www.example.com.';
|
||||
|
||||
so = ss.option(form.Value, 'target', _('Target'));
|
||||
so.rmempty = false;
|
||||
so.datatype = 'hostname';
|
||||
so.placeholder = 'example.com.';
|
||||
|
||||
o = dnss.taboption('hosts', form.SectionValue, '__hosts__', form.GridSection, 'domain', null,
|
||||
_('Hostnames are used to bind a domain name to an IP address. This setting is redundant for hostnames already configured with static leases, but it can be useful to rebind an FQDN.'));
|
||||
|
||||
ss = o.subsection;
|
||||
|
||||
ss.addremove = true;
|
||||
ss.anonymous = true;
|
||||
ss.sortable = true;
|
||||
|
||||
so = ss.option(form.Value, 'name', _('Hostname'));
|
||||
so.rmempty = false;
|
||||
so.datatype = 'hostname';
|
||||
|
||||
so = ss.option(form.Value, 'ip', _('IP address'));
|
||||
so.rmempty = false;
|
||||
so.datatype = 'ipaddr("nomask")';
|
||||
|
||||
so = ss.option(form.Value, 'comments', _('Comments'));
|
||||
so.rmempty = true;
|
||||
|
||||
const ipaddrs = {};
|
||||
|
||||
Object.keys(hosts).forEach(function(mac) {
|
||||
for (const addr of L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4))
|
||||
ipaddrs[addr] = hosts[mac].name || mac;
|
||||
});
|
||||
|
||||
L.sortedKeys(ipaddrs, null, 'addr').forEach(function(ipv4) {
|
||||
so.value(ipv4, '%s (%s)'.format(ipv4, ipaddrs[ipv4]));
|
||||
});
|
||||
|
||||
o = dnss.taboption('dnsrr', form.SectionValue, '__dnsrr__', form.GridSection, 'dnsrr', null,
|
||||
_('Set an arbitrary resource record (RR) type.') + '<br/>' +
|
||||
_('Hexdata is automatically en/decoded on save and load'));
|
||||
|
||||
ss = o.subsection;
|
||||
|
||||
ss.addremove = true;
|
||||
ss.anonymous = true;
|
||||
ss.sortable = true;
|
||||
ss.rowcolors = true;
|
||||
ss.nodescriptions = true;
|
||||
|
||||
function hexdecodeload(section_id) {
|
||||
let value = uci.get('dhcp', section_id, 'hexdata') || '';
|
||||
// Remove any spaces or colons from the hex string - they're allowed
|
||||
value = value.replace(/[\s:]/g, '');
|
||||
// Hex-decode the string before displaying
|
||||
let decodedString = '';
|
||||
for (let i = 0; i < value.length; i += 2) {
|
||||
decodedString += String.fromCharCode(parseInt(value.substr(i, 2), 16));
|
||||
}
|
||||
return decodedString;
|
||||
}
|
||||
|
||||
function hexencodesave(section, value) {
|
||||
if (!value || value.length === 0) {
|
||||
uci.unset('dhcp', section, 'hexdata');
|
||||
return;
|
||||
}
|
||||
// Hex-encode the string before saving
|
||||
const encodedArr = value.split('').map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join('');
|
||||
uci.set('dhcp', section, this.option, encodedArr);
|
||||
}
|
||||
|
||||
so = ss.option(form.Value, 'rrname', _('Resource Record Name'));
|
||||
so.rmempty = false;
|
||||
so.datatype = 'hostname';
|
||||
so.placeholder = 'svcb.example.com.';
|
||||
|
||||
so = ss.option(form.Value, 'rrnumber', _('Resource Record Number'));
|
||||
so.rmempty = false;
|
||||
so.datatype = 'uinteger';
|
||||
so.placeholder = '64';
|
||||
|
||||
so = ss.option(form.Value, '_hexdata', _('Raw Data'));
|
||||
so.rmempty = true;
|
||||
so.datatype = 'string';
|
||||
so.placeholder = 'free-form string';
|
||||
so.load = hexdecodeload;
|
||||
so.write = hexencodesave;
|
||||
so.modalonly = true;
|
||||
so.depends({ rrnumber: '65', '!reverse': true });
|
||||
|
||||
so = ss.option(form.DummyValue, 'hexdata', _('Hex Data'));
|
||||
so.width = '50%';
|
||||
so.rawhtml = true;
|
||||
so.load = function(section_id) {
|
||||
let hexdata = uci.get('dhcp', section_id, 'hexdata') || '';
|
||||
hexdata = hexdata.replace(/[:]/g, '');
|
||||
return hexdata.replace(/(.{2})/g, '$1 ');
|
||||
};
|
||||
|
||||
function writetype65(section_id, value) {
|
||||
let rrnum = uci.get('dhcp', section_id, 'rrnumber');
|
||||
if (rrnum !== '65') return;
|
||||
|
||||
let priority = parseInt(this.section.formvalue(section_id, '_svc_priority'), 10);
|
||||
let target = this.section.formvalue(section_id, '_svc_target') || '.';
|
||||
let params = value.trim().split('\n').map(l => l.trim()).filter(Boolean);
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const hex = drh.buildSvcbHex(priority, target, params);
|
||||
uci.set('dhcp', section_id, 'hexdata', hex);
|
||||
};
|
||||
|
||||
function loadtype65(section_id) {
|
||||
let rrnum = uci.get('dhcp', section_id, 'rrnumber');
|
||||
if (rrnum !== '65') return null;
|
||||
|
||||
let hexdata = uci.get('dhcp', section_id, 'hexdata');
|
||||
// eslint-disable-next-line no-undef
|
||||
return drh.parseSvcbHex(hexdata);
|
||||
};
|
||||
|
||||
// Type 65 builder fields (hidden unless rrnumber === 65)
|
||||
so = ss.option(form.Value, '_svc_priority', _('Svc Priority'));
|
||||
so.placeholder = 1;
|
||||
so.datatype = 'and(uinteger,min(0),max(65535))'
|
||||
so.modalonly = true;
|
||||
so.depends({ rrnumber: '65' });
|
||||
so.write = writetype65;
|
||||
so.load = function(section_id) {
|
||||
const parsed = loadtype65(section_id);
|
||||
return parsed?.priority?.toString() || '';
|
||||
};
|
||||
|
||||
so = ss.option(form.Value, '_svc_target', _('Svc Target'));
|
||||
so.placeholder = 'svc.example.com.';
|
||||
so.dataype = 'hostname';
|
||||
so.modalonly = true;
|
||||
so.depends({ rrnumber: '65' });
|
||||
so.write = writetype65;
|
||||
so.load = function(section_id) {
|
||||
const parsed = loadtype65(section_id);
|
||||
return parsed?.target || '';
|
||||
};
|
||||
|
||||
so = ss.option(form.TextValue, '_svc_params', _('Svc Parameters'));
|
||||
so.placeholder = 'alpn=h2,h3\nipv4hint=192.0.2.1,192.0.2.2\nipv6hint=2001:db8::1,2001:db8::2\nport=8000';
|
||||
so.modalonly = true;
|
||||
so.rows = 4;
|
||||
so.depends({ rrnumber: '65' });
|
||||
so.write = writetype65;
|
||||
so.load = function(section_id) {
|
||||
const parsed = loadtype65(section_id);
|
||||
return parsed?.params?.join('\n') || '';
|
||||
};
|
||||
// End dnsrecords
|
||||
|
||||
// Begin dnssec
|
||||
if (L.hasSystemFeature('dnsmasq', 'dnssec')) {
|
||||
o = s.taboption('dnssecopt', form.Flag, 'dnssec',
|
||||
_('DNSSEC'),
|
||||
_('Validate DNS replies and cache DNSSEC data, requires upstream to support DNSSEC.'));
|
||||
o.optional = true;
|
||||
|
||||
o = s.taboption('dnssecopt', form.Flag, 'dnsseccheckunsigned',
|
||||
_('DNSSEC check unsigned'),
|
||||
_('Verify unsigned domain responses really come from unsigned domains.'));
|
||||
o.default = o.enabled;
|
||||
o.optional = true;
|
||||
}
|
||||
// End dnssec
|
||||
|
||||
// Begin filteropts
|
||||
s.taboption('filteropts', form.Flag, 'domainneeded',
|
||||
_('Domain required'),
|
||||
_('Never forward DNS queries which lack dots or domain parts.') + '<br />' +
|
||||
customi18n(_('Names not in {etc_hosts} are answered {not_found}.') )
|
||||
);
|
||||
|
||||
o = s.taboption('filteropts', form.Flag, 'rebind_protection',
|
||||
_('Rebind protection'),
|
||||
customi18n(_('Discard upstream responses containing {rfc_1918_link} addresses.') ) + '<br />' +
|
||||
customi18n(_('Discard also upstream responses containing {rfc_4193_link}, Link-Local and private IPv4-Mapped {rfc_4291_link} IPv6 Addresses.') )
|
||||
);
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('filteropts', form.Flag, 'rebind_localhost',
|
||||
_('Allow localhost'),
|
||||
customi18n(
|
||||
_('Exempt {loopback_slash_8_v4} and {localhost_v6} from rebinding checks, e.g. for <abbr title="Real-time Block List">RBL</abbr> services.')
|
||||
)
|
||||
);
|
||||
o.depends('rebind_protection', '1');
|
||||
|
||||
o = s.taboption('filteropts', form.DynamicList, 'rebind_domain',
|
||||
_('Domain whitelist'),
|
||||
customi18n(_('List of domains to allow {rfc_1918_link} responses for.') )
|
||||
);
|
||||
o.depends('rebind_protection', '1');
|
||||
o.optional = true;
|
||||
o.placeholder = 'ihost.netflix.com';
|
||||
o.validate = validateAddressList;
|
||||
|
||||
o = s.taboption('filteropts', form.Flag, 'localservice',
|
||||
_('Local service only'),
|
||||
_('Accept DNS queries only from hosts whose address is on a local subnet.'));
|
||||
o.optional = false;
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('filteropts', form.Flag, 'boguspriv',
|
||||
_('Filter private'),
|
||||
customi18n(
|
||||
_('Reject reverse lookups to {rfc_6303_link} IP ranges ({reverse_arpa}) not in {etc_hosts}.') )
|
||||
);
|
||||
o.default = o.enabled;
|
||||
|
||||
s.taboption('filteropts', form.Flag, 'filterwin2k',
|
||||
_('Filter SRV/SOA service discovery'),
|
||||
_('Filters SRV/SOA service discovery, to avoid triggering dial-on-demand links.') + '<br />' +
|
||||
_('May prevent VoIP or other services from working.'));
|
||||
|
||||
o = s.taboption('filteropts', form.Flag, 'filter_aaaa',
|
||||
_('Filter IPv6 AAAA records'),
|
||||
_('Remove IPv6 addresses from the results and only return IPv4 addresses.') + '<br />' +
|
||||
_('Can be useful if ISP has IPv6 nameservers but does not provide IPv6 routing.'));
|
||||
o.optional = true;
|
||||
|
||||
o = s.taboption('filteropts', form.Flag, 'filter_a',
|
||||
_('Filter IPv4 A records'),
|
||||
_('Remove IPv4 addresses from the results and only return IPv6 addresses.'));
|
||||
o.optional = true;
|
||||
|
||||
o = s.taboption('filteropts', form.MultiValue, 'filter_rr',
|
||||
_('Filter arbitrary RR'), _('Removes records of the specified type(s) from answers.'));
|
||||
o.optional = true;
|
||||
o.create = true;
|
||||
o.multiple = true;
|
||||
o.display_size = 5;
|
||||
recordtypes.forEach(r => {
|
||||
o.value(r);
|
||||
});
|
||||
|
||||
s.taboption('filteropts', form.Flag, 'localise_queries',
|
||||
_('Localise queries'),
|
||||
customi18n(_('Limit response records (from {etc_hosts}) to those that fall within the subnet of the querying interface.') ) + '<br />' +
|
||||
_('This prevents unreachable IPs in subnets not accessible to you.') + '<br />' +
|
||||
_('Note: IPv4 only.'));
|
||||
|
||||
s.taboption('filteropts', form.Flag, 'nonegcache',
|
||||
_('No negative cache'),
|
||||
_('Do not cache negative replies, e.g. for non-existent domains.'));
|
||||
|
||||
o = s.taboption('filteropts', form.DynamicList, 'bogusnxdomain',
|
||||
customi18n(_('IPs to override with {nxdomain}') ),
|
||||
customi18n(_('Transform replies which contain the specified addresses or subnets into {nxdomain} responses.') )
|
||||
);
|
||||
o.optional = true;
|
||||
o.placeholder = '64.94.110.11';
|
||||
// End filteropts
|
||||
|
||||
// Begin forward
|
||||
o = s.taboption('forward', form.DynamicList, 'server',
|
||||
_('DNS Forwards'),
|
||||
_('Forward specific domain queries to specific upstream servers.'));
|
||||
o.optional = true;
|
||||
o.placeholder = '/*.example.org/10.1.2.3';
|
||||
o.validate = validateServerSpec;
|
||||
|
||||
o = s.taboption('forward', form.Value, 'serversfile',
|
||||
_('Additional servers file'),
|
||||
customi18n(_('File listing upstream resolvers, optionally domain-specific, e.g. {servers_file_entry01}, {servers_file_entry02}.') )
|
||||
);
|
||||
o.placeholder = '/etc/dnsmasq.servers';
|
||||
|
||||
o = s.taboption('forward', form.Value, 'addmac',
|
||||
_('Add requestor MAC'),
|
||||
_('Add the MAC address of the requestor to DNS queries which are forwarded upstream.') + ' ' + '<br />' +
|
||||
_('%s uses the default MAC address format encoding').format('<code>enabled</code>') + ' ' + '<br />' +
|
||||
_('%s uses an alternative encoding of the MAC as base64').format('<code>base64</code>') + ' ' + '<br />' +
|
||||
_('%s uses a human-readable encoding of hex-and-colons').format('<code>text</code>'));
|
||||
o.optional = true;
|
||||
o.value('', _('off'));
|
||||
o.value('1', _('enabled (default)'));
|
||||
o.value('base64');
|
||||
o.value('text');
|
||||
|
||||
s.taboption('forward', form.Flag, 'stripmac',
|
||||
_('Remove MAC address before forwarding query'),
|
||||
_('Remove any MAC address information already in downstream queries before forwarding upstream.'));
|
||||
|
||||
o = s.taboption('forward', form.Value, 'addsubnet',
|
||||
_('Add subnet address to forwards'),
|
||||
_('Add a subnet address to the DNS queries which are forwarded upstream, leaving this value empty disables the feature.') + ' ' +
|
||||
_('If an address is specified in the flag, it will be used, otherwise, the address of the requestor will be used.') + ' ' +
|
||||
_('The amount of the address forwarded depends on the prefix length parameter: 32 (128 for IPv6) forwards the whole address, zero forwards none of it but still marks the request so that no upstream nameserver will add client address information either.') + ' ' + '<br />' +
|
||||
_('The default (%s) is zero for both IPv4 and IPv6.').format('<code>0,0</code>') + ' ' + '<br />' +
|
||||
_('%s adds the /24 and /96 subnets of the requestor for IPv4 and IPv6 requestors, respectively.').format('<code>24,96</code>') + ' ' + '<br />' +
|
||||
_('%s adds 1.2.3.0/24 for IPv4 requestors and ::/0 for IPv6 requestors.').format('<code>1.2.3.4/24</code>') + ' ' + '<br />' +
|
||||
_('%s adds 1.2.3.0/24 for both IPv4 and IPv6 requestors.').format('<code>1.2.3.4/24,1.2.3.4/24</code>'));
|
||||
o.optional = true;
|
||||
|
||||
s.taboption('forward', form.Flag, 'stripsubnet',
|
||||
_('Remove subnet address before forwarding query'),
|
||||
_('Remove any subnet address already present in a downstream query before forwarding it upstream.'));
|
||||
// End forward
|
||||
|
||||
// Begin limits
|
||||
o = s.taboption('limits', form.Value, 'ednspacket_max',
|
||||
_('Max. EDNS0 packet size'),
|
||||
_('Maximum allowed size of EDNS0 UDP packets.'));
|
||||
o.optional = true;
|
||||
o.datatype = 'uinteger';
|
||||
o.placeholder = 1280;
|
||||
|
||||
o = s.taboption('limits', form.Value, 'dnsforwardmax',
|
||||
_('Max. concurrent queries'),
|
||||
_('Maximum allowed number of concurrent DNS queries.'));
|
||||
o.optional = true;
|
||||
o.datatype = 'uinteger';
|
||||
o.placeholder = 150;
|
||||
|
||||
o = s.taboption('limits', form.Value, 'cachesize',
|
||||
_('Size of DNS query cache'),
|
||||
_('Number of cached DNS entries, 10000 is maximum, 0 is no caching.'));
|
||||
o.optional = true;
|
||||
o.datatype = 'range(0,10000)';
|
||||
o.placeholder = 150;
|
||||
|
||||
o = s.taboption('limits', form.Value, 'min_cache_ttl',
|
||||
_('Min cache TTL'),
|
||||
_('Extend short TTL values to the seconds value given when caching them. Use with caution.') +
|
||||
_(' (Max 1h == 3600)'));
|
||||
o.optional = true;
|
||||
o.placeholder = 60;
|
||||
|
||||
o = s.taboption('limits', form.Value, 'max_cache_ttl',
|
||||
_('Max cache TTL'),
|
||||
_('Set a maximum seconds TTL value for entries in the cache.'));
|
||||
o.optional = true;
|
||||
o.placeholder = 3600;
|
||||
// End limits
|
||||
|
||||
// Being logging
|
||||
o = s.taboption('logging', form.Flag, 'logqueries',
|
||||
_('Log queries'),
|
||||
_('Write received DNS queries to syslog.') + ' ' + _('Dump cache on SIGUSR1, include requesting IP.'));
|
||||
o.optional = true;
|
||||
|
||||
o = s.taboption('logging', form.Value, 'logfacility',
|
||||
_('Log facility'),
|
||||
_('Set log class/facility for syslog entries.'));
|
||||
o.optional = true;
|
||||
o.value('KERN');
|
||||
o.value('USER');
|
||||
o.value('MAIL');
|
||||
o.value('DAEMON');
|
||||
o.value('AUTH');
|
||||
o.value('LPR');
|
||||
o.value('NEWS');
|
||||
o.value('UUCP');
|
||||
o.value('CRON');
|
||||
o.value('LOCAL0');
|
||||
o.value('LOCAL1');
|
||||
o.value('LOCAL2');
|
||||
o.value('LOCAL3');
|
||||
o.value('LOCAL4');
|
||||
o.value('LOCAL5');
|
||||
o.value('LOCAL6');
|
||||
o.value('LOCAL7');
|
||||
o.value('-', _('stderr'));
|
||||
// End logging
|
||||
|
||||
// Begin files
|
||||
o = s.taboption('files', form.Flag, 'noresolv',
|
||||
_('Ignore resolv file'));
|
||||
o.optional = true;
|
||||
|
||||
o = s.taboption('files', form.Value, 'resolvfile',
|
||||
_('Resolv file'),
|
||||
_('File with upstream resolvers.'));
|
||||
o.depends('noresolv', '0');
|
||||
o.placeholder = '/tmp/resolv.conf.d/resolv.conf.auto';
|
||||
o.optional = true;
|
||||
|
||||
o = s.taboption('files', form.Flag, 'strictorder',
|
||||
_('Strict order'),
|
||||
_('Query upstream resolvers in the order they appear in the resolv file.'));
|
||||
o.optional = true;
|
||||
|
||||
o = s.taboption('files', form.Flag, 'ignore_hosts_dir',
|
||||
_('Ignore hosts files directory'),
|
||||
_('On: use instance specific hosts file only') + '<br/>' +
|
||||
_('Off: use all files in the directory including the instance specific hosts file')
|
||||
);
|
||||
o.optional = true;
|
||||
|
||||
o = s.taboption('files', form.Flag, 'nohosts',
|
||||
customi18n(_('Ignore {etc_hosts} file') )
|
||||
);
|
||||
o.optional = true;
|
||||
|
||||
o = s.taboption('files', form.DynamicList, 'addnhosts',
|
||||
_('Additional hosts files'));
|
||||
o.optional = true;
|
||||
o.placeholder = '/etc/dnsmasq.hosts';
|
||||
// End files
|
||||
|
||||
// Begin ipsets
|
||||
o = s.taboption('ipsets', form.SectionValue, '__ipsets__', form.GridSection, 'ipset', null,
|
||||
_('List of IP sets to populate with the IPs of DNS lookup results of the FQDNs also specified here.') + '<br />' +
|
||||
_('The netfilter components below are only regarded when running fw4.'));
|
||||
|
||||
ss = o.subsection;
|
||||
|
||||
ss.addremove = true;
|
||||
ss.anonymous = true;
|
||||
ss.sortable = true;
|
||||
ss.rowcolors = true;
|
||||
ss.nodescriptions = true;
|
||||
ss.modaltitle = _('Edit IP set');
|
||||
|
||||
so = ss.option(form.DynamicList, 'name', _('Name of the set'));
|
||||
uci.sections('firewall', 'ipset', function(s) {
|
||||
if (typeof(s.name) == 'string')
|
||||
so.value(s.name, s.comment ? '%s (%s)'.format(s.name, s.comment) : s.name);
|
||||
});
|
||||
so.rmempty = false;
|
||||
so.editable = false;
|
||||
so.datatype = 'string';
|
||||
|
||||
so = ss.option(form.DynamicList, 'domain', _('FQDN'));
|
||||
so.rmempty = false;
|
||||
so.editable = false;
|
||||
so.datatype = 'hostname';
|
||||
|
||||
so = ss.option(form.Value, 'table', _('Netfilter table name'), _('Defaults to fw4.'));
|
||||
so.editable = false;
|
||||
so.placeholder = 'fw4';
|
||||
so.rmempty = true;
|
||||
|
||||
so = ss.option(form.ListValue, 'table_family', _('Table IP family'), _('Defaults to IPv4+6.') + ' ' + _('Can be hinted by adding 4 or 6 to the name.') + '<br />' +
|
||||
_('Adding an IPv6 to an IPv4 set and vice-versa silently fails.'));
|
||||
so.editable = false;
|
||||
so.rmempty = true;
|
||||
so.value('inet', _('IPv4+6'));
|
||||
so.value('ip', _('IPv4'));
|
||||
so.value('ip6', _('IPv6'));
|
||||
// End ipsets
|
||||
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,235 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require fs';
|
||||
'require uci';
|
||||
'require form';
|
||||
'require network';
|
||||
'require tools.widgets as widgets';
|
||||
'require tools.network as nettools';
|
||||
|
||||
return view.extend({
|
||||
load() {
|
||||
return Promise.all([
|
||||
network.getDevices(),
|
||||
fs.lines('/etc/iproute2/rt_tables')
|
||||
]);
|
||||
},
|
||||
|
||||
render([netDevs, rtTables]) {
|
||||
let m, s, o;
|
||||
|
||||
rtTables = rtTables.map(function(l) {
|
||||
const m = l.trim().match(/^(\d+)\s+(\S+)$/);
|
||||
return m ? [ +m[1], m[2] ] : null;
|
||||
}).filter(function(e) {
|
||||
return e && e[0] > 0;
|
||||
});
|
||||
|
||||
m = new form.Map('network', _('Routing'), _('Routing defines over which interface and gateway a certain host or network can be reached.') +
|
||||
'<br/>' + _('Routes go in routing tables and define the specific path to reach destinations.') +
|
||||
'<br/>' + _('Rules determine which routing table to use, based on conditions like source address or interface.'));
|
||||
m.tabbed = true;
|
||||
|
||||
for (let family = 4; family <= 6; family += 2) {
|
||||
s = m.section(form.GridSection, (family == 6) ? 'route6' : 'route', (family == 6) ? _('Static IPv6 Routes') : _('Static IPv4 Routes'));
|
||||
s.anonymous = true;
|
||||
s.addremove = true;
|
||||
s.sortable = true;
|
||||
s.cloneable = true;
|
||||
s.nodescriptions = true;
|
||||
|
||||
s.tab('general', _('General Settings'));
|
||||
s.tab('advanced', _('Advanced Settings'));
|
||||
|
||||
o = s.taboption('general', widgets.NetworkSelect, 'interface', _('Interface'), _('Specifies the logical interface name of the parent (or master) interface this route belongs to'));
|
||||
o.loopback = true;
|
||||
o.nocreate = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('general', form.ListValue, 'type', _('Route type'), _('Specifies the route type to be created'));
|
||||
o.modalonly = true;
|
||||
o.value('', 'unicast');
|
||||
o.value('local');
|
||||
o.value('broadcast');
|
||||
o.value('multicast');
|
||||
o.value('unreachable');
|
||||
o.value('prohibit');
|
||||
o.value('blackhole');
|
||||
o.value('anycast');
|
||||
o.value('throw');
|
||||
|
||||
o = s.taboption('general', form.Value, 'target', _('Target'), _('Network address'));
|
||||
o.rmempty = false;
|
||||
o.datatype = (family == 6) ? 'cidr6' : 'cidr4';
|
||||
o.placeholder = (family == 6) ? '::/0' : '0.0.0.0/0';
|
||||
o.cfgvalue = function(section_id) {
|
||||
const section_type = uci.get('network', section_id, '.type');
|
||||
const target = uci.get('network', section_id, 'target');
|
||||
const mask = uci.get('network', section_id, 'netmask');
|
||||
const v6 = (section_type == 'route6') ? true : false;
|
||||
const bits = mask ? network.maskToPrefix(mask, v6) : (v6 ? 128 : 32);
|
||||
if (target) {
|
||||
return target.split('/')[1] ? target : target + '/' + bits;
|
||||
}
|
||||
}
|
||||
o.write = function(section_id, formvalue) {
|
||||
uci.set('network', section_id, 'target', formvalue);
|
||||
uci.unset('network', section_id, 'netmask');
|
||||
}
|
||||
|
||||
o = s.taboption('general', form.Value, 'gateway', _('Gateway'), _('Specifies the network gateway. If omitted, the gateway from the parent interface is taken if any, otherwise creates a link scope route. If set to 0.0.0.0 no gateway will be specified for the route'));
|
||||
o.datatype = (family == 6) ? 'ip6addr("nomask")' : 'ip4addr("nomask")';
|
||||
o.placeholder = (family == 6) ? 'fe80::1' : '192.168.0.1';
|
||||
|
||||
o = s.taboption('advanced', form.Value, 'metric', _('Metric'), _('Ordinal: routes with the lowest metric match first'));
|
||||
o.datatype = 'uinteger';
|
||||
o.placeholder = 0;
|
||||
o.textvalue = function(section_id) {
|
||||
return this.cfgvalue(section_id) || E('em', _('auto'));
|
||||
};
|
||||
|
||||
o = s.taboption('advanced', form.Value, 'mtu', _('MTU'), _('Packets exceeding this value may be fragmented'));
|
||||
o.modalonly = true;
|
||||
o.datatype = 'and(uinteger,range(64,9000))';
|
||||
o.placeholder = 1500;
|
||||
|
||||
o = s.taboption('advanced', form.Value, 'table', _('Table'), _('Routing table into which to insert this rule.') + '<br/>' +
|
||||
_('A numeric table index, or symbol alias declared in %s. Special aliases local (255), main (254) and default (253) are also valid').format('<code>/etc/iproute2/rt_tables</code>')
|
||||
+ '<br/>' + _('Only interfaces using this table (via override) will use this route.'));
|
||||
o.datatype = 'or(uinteger, string)';
|
||||
for (let rt of rtTables)
|
||||
o.value(rt[1], '%s (%d)'.format(rt[1], rt[0]));
|
||||
o.textvalue = function(section_id) {
|
||||
return this.cfgvalue(section_id) || E('em', _('auto'));
|
||||
};
|
||||
|
||||
o = s.taboption('advanced', form.Value, 'source', _('Source'), _('Specifies the preferred source address when sending to destinations covered by the target')
|
||||
+ '<br/>' + _('This is only used if no default route matches the destination gateway'));
|
||||
o.modalonly = true;
|
||||
o.datatype = (family == 6) ? 'ip6addr' : 'ip4addr';
|
||||
for (let nd of netDevs) {
|
||||
const addrs = (family == 6) ? nd.getIP6Addrs() : nd.getIPAddrs();
|
||||
for (let a of addrs)
|
||||
o.value(a.split('/')[0]);
|
||||
}
|
||||
|
||||
o = s.taboption('advanced', form.Flag, 'onlink', _('On-link'), _('When enabled, gateway is on-link even if the gateway does not match any interface prefix'));
|
||||
o.modalonly = true;
|
||||
o.default = o.disabled;
|
||||
|
||||
o = s.taboption('advanced', form.Flag, 'disabled', _('Disable'));
|
||||
o.modalonly = false;
|
||||
o.editable = true;
|
||||
o.default = o.disabled;
|
||||
}
|
||||
|
||||
for (let family = 4; family <= 6; family += 2) {
|
||||
s = m.section(form.GridSection, (family == 6) ? 'rule6' : 'rule', (family == 6) ? _('IPv6 Rules') : _('IPv4 Rules'));
|
||||
s.anonymous = true;
|
||||
s.addremove = true;
|
||||
s.sortable = true;
|
||||
s.cloneable = true;
|
||||
s.nodescriptions = true;
|
||||
|
||||
s.tab('general', _('General Settings'));
|
||||
s.tab('advanced', _('Advanced Settings'));
|
||||
|
||||
o = s.taboption('general', form.Value, 'priority', _('Priority'), _('Execution order of this IP rule: lower numbers go first'));
|
||||
o.datatype = 'uinteger';
|
||||
o.placeholder = 30000;
|
||||
o.textvalue = function(section_id) {
|
||||
return this.cfgvalue(section_id) || E('em', _('auto'));
|
||||
};
|
||||
|
||||
o = s.taboption('general', form.ListValue, 'action', _('Rule type'), _('Specifies the rule target routing action'));
|
||||
o.modalonly = true;
|
||||
o.value('', 'unicast');
|
||||
o.value('unreachable');
|
||||
o.value('prohibit');
|
||||
o.value('blackhole');
|
||||
o.value('throw');
|
||||
|
||||
o = s.taboption('general', widgets.NetworkSelect, 'in', _('Incoming interface'), _('Match traffic from this interface'));
|
||||
o.loopback = true;
|
||||
o.nocreate = true;
|
||||
|
||||
o = s.taboption('general', form.Value, 'src', _('Source'), _('Match traffic from this source subnet (CIDR notation)'));
|
||||
o.datatype = (family == 6) ? 'cidr6' : 'cidr4';
|
||||
o.placeholder = (family == 6) ? '::/0' : '0.0.0.0/0';
|
||||
o.textvalue = function(section_id) {
|
||||
return this.cfgvalue(section_id) || E('em', _('any'));
|
||||
};
|
||||
|
||||
o = s.taboption('general', form.Value, 'ipproto', _('IP Protocol'), _('Match traffic IP protocol type'));
|
||||
o.datatype = 'range(0,255)';
|
||||
nettools.protocols.forEach(function(p) {
|
||||
o.value(p.i, p.d);
|
||||
});
|
||||
|
||||
o = s.taboption('general', widgets.NetworkSelect, 'out', _('Outgoing interface'), _('Match traffic destined to this interface'));
|
||||
o.loopback = true;
|
||||
o.nocreate = true;
|
||||
|
||||
o = s.taboption('general', form.Value, 'dest', _('Destination'), _('Match traffic destined to this subnet (CIDR notation)'));
|
||||
o.datatype = (family == 6) ? 'cidr6' : 'cidr4';
|
||||
o.placeholder = (family == 6) ? '::/0' : '0.0.0.0/0';
|
||||
o.textvalue = function(section_id) {
|
||||
return this.cfgvalue(section_id) || E('em', _('any'));
|
||||
};
|
||||
|
||||
o = s.taboption('advanced', form.Value, 'lookup', _('Table'), _('Routing table to use for traffic matching this rule.') + '<br/>' +
|
||||
_('A numeric table index, or symbol alias declared in %s. Special aliases local (255), main (254) and default (253) are also valid').format('<code>/etc/iproute2/rt_tables</code>')
|
||||
+ '<br/>' + _('Matched traffic re-targets to an interface using this table.'));
|
||||
o.datatype = 'or(uinteger, string)';
|
||||
for (let rt of rtTables)
|
||||
o.value(rt[1], '%s (%d)'.format(rt[1], rt[0]));
|
||||
|
||||
o = s.taboption('advanced', form.Value, 'goto', _('Jump to rule'), _('Jumps to another rule specified by its priority value'));
|
||||
o.modalonly = true;
|
||||
o.datatype = 'uinteger';
|
||||
o.placeholder = 80000;
|
||||
|
||||
o = s.taboption('advanced', form.Value, 'mark', _('Firewall mark'), _('Specifies the fwmark and optionally its mask to match, e.g. 0xFF to match mark 255 or 0x0/0x1 to match any even mark value'));
|
||||
o.modalonly = true;
|
||||
o.datatype = 'string';
|
||||
o.placeholder = '0x1/0xf';
|
||||
|
||||
o = s.taboption('advanced', form.Value, 'sport', _('Source port'), _('Match traffic from this source port (range)'));
|
||||
o.modalonly = true;
|
||||
o.datatype = 'portrange';
|
||||
o.placeholder = '0-65535';
|
||||
|
||||
o = s.taboption('advanced', form.Value, 'dport', _('Destination port'), _('Match traffic from this destination port (range)'));
|
||||
o.modalonly = true;
|
||||
o.datatype = 'portrange';
|
||||
o.placeholder = '0-65535';
|
||||
|
||||
o = s.taboption('advanced', form.Value, 'tos', _('Type of service'), _('Specifies the TOS value to match in IP headers'));
|
||||
o.modalonly = true;
|
||||
o.datatype = 'uinteger';
|
||||
o.placeholder = 10;
|
||||
|
||||
o = s.taboption('advanced', form.Value, 'uidrange', _('User identifier'), _('Specifies an individual UID or range of UIDs to match, e.g. 1000 to match corresponding UID or 1000-1005 to inclusively match all UIDs within the corresponding range'));
|
||||
o.modalonly = true;
|
||||
o.datatype = 'string';
|
||||
o.placeholder = '1000-1005';
|
||||
|
||||
o = s.taboption('advanced', form.Value, 'suppress_prefixlength', _('Prefix suppressor'), _('Reject routing decisions that have a prefix length less than or equal to the specified value')
|
||||
+ '<br/>' + _('Prevents overly broad routes being considered. Setting 16 would consider /17, /24, /28 or more specific routes yet ignore /16, /8, /0 (default) routes'));
|
||||
o.modalonly = true;
|
||||
o.datatype = (family == 6) ? 'ip6prefix' : 'ip4prefix';
|
||||
o.placeholder = (family == 6) ? 64 : 24;
|
||||
|
||||
o = s.taboption('advanced', form.Flag, 'invert', _('Invert match'), _('If set, the meaning of the match options is inverted'));
|
||||
o.modalonly = true;
|
||||
o.default = o.disabled;
|
||||
|
||||
o = s.taboption('advanced', form.Flag, 'disabled', _('Disable'));
|
||||
o.modalonly = false;
|
||||
o.editable = true;
|
||||
o.default = o.disabled;
|
||||
}
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,375 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require dom';
|
||||
'require poll';
|
||||
'require ui';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
'require form';
|
||||
'require network';
|
||||
|
||||
function parse_portvalue(section_id) {
|
||||
const ports = L.toArray(uci.get('network', section_id, 'ports'));
|
||||
|
||||
for (let p of ports) {
|
||||
const m = p.match(/^(\d+)([tu]?)/);
|
||||
|
||||
if (m && m[1] == this.option)
|
||||
return m[2] || 'u';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function validate_portvalue(section_id, value) {
|
||||
if (value != 'u')
|
||||
return true;
|
||||
|
||||
const sections = this.section.cfgsections();
|
||||
|
||||
for (let s of sections) {
|
||||
if (s == section_id)
|
||||
continue;
|
||||
|
||||
if (this.formvalue(s) == 'u')
|
||||
return _('%s is untagged in multiple VLANs!').format(this.title);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function update_interfaces(old_ifname, new_ifname) {
|
||||
const interfaces = uci.sections('network', 'interface');
|
||||
|
||||
for (let intf of interfaces) {
|
||||
const old_ifnames = L.toArray(intf.ifname);
|
||||
const new_ifnames = [];
|
||||
let changed = false;
|
||||
|
||||
for (let oif of old_ifnames) {
|
||||
if (oif == old_ifname) {
|
||||
new_ifnames.push(new_ifname);
|
||||
changed = true;
|
||||
}
|
||||
else {
|
||||
new_ifnames.push(oif);
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
uci.set('network', intf['.name'], 'ifname', new_ifnames.join(' '));
|
||||
|
||||
ui.addNotification(null, E('p', _('Interface %q device auto-migrated from %q to %q.')
|
||||
.replace(/%q/g, '"%s"').format(intf['.name'], old_ifname, new_ifname)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function render_port_status(node, portstate) {
|
||||
if (!node)
|
||||
return null;
|
||||
|
||||
if (!portstate || !portstate.link)
|
||||
dom.content(node, [
|
||||
E('img', { src: L.resource('icons/port_down.svg') }),
|
||||
E('br'),
|
||||
_('no link')
|
||||
]);
|
||||
else
|
||||
dom.content(node, [
|
||||
E('img', { src: L.resource('icons/port_up.svg') }),
|
||||
E('br'),
|
||||
'%d'.format(portstate.speed) + _('baseT'),
|
||||
E('br'),
|
||||
portstate.duplex ? _('full-duplex') : _('half-duplex')
|
||||
]);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
function update_port_status(topologies) {
|
||||
const tasks = [];
|
||||
|
||||
for (let switch_name in topologies)
|
||||
tasks.push(callSwconfigPortState(switch_name).then(L.bind(function(switch_name, ports) {
|
||||
for (let p of ports) {
|
||||
const node = document.querySelector('[data-switch="%s"][data-port="%d"]'.format(switch_name, p.port));
|
||||
render_port_status(node, p);
|
||||
}
|
||||
}, topologies[switch_name], switch_name)));
|
||||
|
||||
return Promise.all(tasks);
|
||||
}
|
||||
|
||||
const callSwconfigFeatures = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getSwconfigFeatures',
|
||||
params: [ 'switch' ],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callSwconfigPortState = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'getSwconfigPortState',
|
||||
params: [ 'switch' ],
|
||||
expect: { result: [] }
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
load() {
|
||||
return network.getSwitchTopologies().then(function(topologies) {
|
||||
const tasks = [];
|
||||
|
||||
for (let switch_name in topologies) {
|
||||
tasks.push(callSwconfigFeatures(switch_name).then(L.bind(function(features) {
|
||||
this.features = features;
|
||||
}, topologies[switch_name])));
|
||||
tasks.push(callSwconfigPortState(switch_name).then(L.bind(function(ports) {
|
||||
this.portstate = ports;
|
||||
}, topologies[switch_name])));
|
||||
}
|
||||
|
||||
return Promise.all(tasks).then(function() { return topologies });
|
||||
});
|
||||
},
|
||||
|
||||
render(topologies) {
|
||||
let m, s, o;
|
||||
|
||||
m = new form.Map('network', _('Switch'), _("The network ports on this device can be combined to several <abbr title='Virtual Local Area Network'>VLAN</abbr>s in which computers can communicate directly with each other. <abbr title='Virtual Local Area Network'>VLAN</abbr>s are often used to separate different network segments. Often there is by default one Uplink port for a connection to the next greater network like the internet and other ports for a local network."));
|
||||
|
||||
const switchSections = uci.sections('network', 'switch');
|
||||
|
||||
for (let switchSection of switchSections) {
|
||||
const sid = switchSection['.name'];
|
||||
const switch_name = switchSection.name || sid;
|
||||
let topology = topologies[switch_name];
|
||||
|
||||
if (!topology) {
|
||||
ui.addNotification(null, _('Switch %q has an unknown topology - the VLAN settings might not be accurate.').replace(/%q/, switch_name));
|
||||
|
||||
topologies[switch_name] = topology = {
|
||||
features: {},
|
||||
netdevs: {
|
||||
5: 'eth0'
|
||||
},
|
||||
ports: [
|
||||
{ num: 0, label: 'Port 1' },
|
||||
{ num: 1, label: 'Port 2' },
|
||||
{ num: 2, label: 'Port 3' },
|
||||
{ num: 3, label: 'Port 4' },
|
||||
{ num: 4, label: 'Port 5' },
|
||||
{ num: 5, label: 'CPU (eth0)', device: 'eth0', need_tag: false }
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
const feat = topology.features;
|
||||
const min_vid = feat.min_vid || 0;
|
||||
const num_vlans = feat.num_vlans || 16;
|
||||
let switch_title = _('Switch %q').replace(/%q/, '"%s"'.format(switch_name));
|
||||
let vlan_title = _('VLANs on %q').replace(/%q/, '"%s"'.format(switch_name));
|
||||
|
||||
if (feat.switch_title) {
|
||||
switch_title += ' (%s)'.format(feat.switch_title);
|
||||
vlan_title += ' (%s)'.format(feat.switch_title);
|
||||
}
|
||||
|
||||
s = m.section(form.NamedSection, sid, 'switch', switch_title);
|
||||
s.addremove = false;
|
||||
|
||||
if (feat.vlan_option) {
|
||||
o = s.option(form.Flag, feat.vlan_option, _('Enable VLAN functionality'));
|
||||
o.rmempty = false;
|
||||
}
|
||||
|
||||
if (feat.learning_option) {
|
||||
o = s.option(form.Flag, feat.learning_option, _('Enable learning and aging'));
|
||||
o.default = o.enabled;
|
||||
}
|
||||
|
||||
if (feat.jumbo_option) {
|
||||
o = s.option(form.Flag, feat.jumbo_option, _('Enable Jumbo Frame passthrough'));
|
||||
o.enabled = '3';
|
||||
o.rmempty = true;
|
||||
}
|
||||
|
||||
if (feat.mirror_option) {
|
||||
s.option(form.Flag, 'enable_mirror_rx', _('Enable mirroring of incoming packets'));
|
||||
s.option(form.Flag, 'enable_mirror_tx', _('Enable mirroring of outgoing packets'));
|
||||
|
||||
const sp = s.option(form.ListValue, 'mirror_source_port', _('Mirror source port'));
|
||||
const mp = s.option(form.ListValue, 'mirror_monitor_port', _('Mirror monitor port'));
|
||||
|
||||
sp.depends('enable_mirror_rx', '1');
|
||||
sp.depends('enable_mirror_tx', '1');
|
||||
|
||||
mp.depends('enable_mirror_rx', '1');
|
||||
mp.depends('enable_mirror_tx', '1');
|
||||
|
||||
for (let tp of topology.ports) {
|
||||
sp.value(tp.num, tp.label);
|
||||
mp.value(tp.num, tp.label);
|
||||
}
|
||||
}
|
||||
|
||||
s = m.section(form.TableSection, 'switch_vlan', vlan_title);
|
||||
s.anonymous = true;
|
||||
s.addremove = true;
|
||||
s.addbtntitle = _('Add VLAN');
|
||||
s.topology = topology;
|
||||
s.device = switch_name;
|
||||
|
||||
s.filter = function(section_id) {
|
||||
const device = uci.get('network', section_id, 'device');
|
||||
return (device == this.device);
|
||||
};
|
||||
|
||||
s.cfgsections = function() {
|
||||
const sections = form.TableSection.prototype.cfgsections.apply(this);
|
||||
|
||||
return sections.sort(function(a, b) {
|
||||
let vidA = feat.vid_option ? uci.get('network', a, feat.vid_option) : null;
|
||||
let vidB = feat.vid_option ? uci.get('network', b, feat.vid_option) : null;
|
||||
|
||||
vidA = +(vidA != null ? vidA : uci.get('network', a, 'vlan') || 9999);
|
||||
vidB = +(vidB != null ? vidB : uci.get('network', b, 'vlan') || 9999);
|
||||
|
||||
return (vidA - vidB);
|
||||
});
|
||||
};
|
||||
|
||||
s.handleAdd = function(ev) {
|
||||
const sections = uci.sections('network', 'switch_vlan');
|
||||
const section_id = uci.add('network', 'switch_vlan');
|
||||
let max_vlan = 0;
|
||||
let max_vid = 0;
|
||||
|
||||
for (let s of sections) {
|
||||
if (s.device != this.device)
|
||||
continue;
|
||||
|
||||
const vlan = +s.vlan;
|
||||
const vid = feat.vid_option ? +s[feat.vid_option] : null;
|
||||
|
||||
if (vlan > max_vlan)
|
||||
max_vlan = vlan;
|
||||
|
||||
if (vid > max_vid)
|
||||
max_vid = vid;
|
||||
}
|
||||
|
||||
uci.set('network', section_id, 'device', this.device);
|
||||
uci.set('network', section_id, 'vlan', max_vlan + 1);
|
||||
|
||||
if (feat.vid_option)
|
||||
uci.set('network', section_id, feat.vid_option, max_vid + 1);
|
||||
|
||||
return this.map.save(null, true);
|
||||
};
|
||||
|
||||
o = s.option(form.Value, feat.vid_option || 'vlan', 'VLAN ID');
|
||||
o.rmempty = false;
|
||||
o.forcewrite = true;
|
||||
o.vlan_used = {};
|
||||
o.datatype = 'range(%u,%u)'.format(min_vid, feat.vid_option ? 4094 : num_vlans - 1);
|
||||
o.description = _('Port status:');
|
||||
|
||||
o.validate = function(section_id, value) {
|
||||
const v = +value;
|
||||
const m = feat.vid_option ? 4094 : num_vlans - 1;
|
||||
|
||||
if (isNaN(v) || v < min_vid || v > m)
|
||||
return _('Invalid VLAN ID given! Only IDs between %d and %d are allowed.').format(min_vid, m);
|
||||
|
||||
const sections = this.section.cfgsections();
|
||||
|
||||
for (let s of sections) {
|
||||
if (s == section_id)
|
||||
continue;
|
||||
|
||||
if (this.formvalue(s) == v)
|
||||
return _('Invalid VLAN ID given! Only unique IDs are allowed');
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const port_opts = o.port_opts = [];
|
||||
|
||||
o.write = function(section_id, value) {
|
||||
const topology = this.section.topology;
|
||||
const values = [];
|
||||
|
||||
for (let i = 0; i < this.port_opts.length; i++) {
|
||||
const tagging = this.port_opts[i].formvalue(section_id);
|
||||
const portspec = Array.isArray(topology.ports) ? topology.ports[i] : null;
|
||||
|
||||
if (tagging == 't')
|
||||
values.push(this.port_opts[i].option + tagging);
|
||||
else if (tagging == 'u')
|
||||
values.push(this.port_opts[i].option);
|
||||
|
||||
if (portspec && portspec.device) {
|
||||
const old_tag = this.port_opts[i].cfgvalue(section_id);
|
||||
const old_vid = this.cfgvalue(section_id);
|
||||
|
||||
if (old_tag != tagging || old_vid != value) {
|
||||
const old_ifname = portspec.device + (old_tag != 'u' ? '.' + old_vid : '');
|
||||
const new_ifname = portspec.device + (tagging != 'u' ? '.' + value : '');
|
||||
|
||||
if (old_ifname != new_ifname)
|
||||
update_interfaces(old_ifname, new_ifname);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (feat.vlan4k_option)
|
||||
uci.set('network', sid, feat.vlan4k_option, '1');
|
||||
|
||||
uci.set('network', section_id, 'ports', values.join(' '));
|
||||
|
||||
return form.Value.prototype.write.apply(this, [section_id, value]);
|
||||
};
|
||||
|
||||
o.cfgvalue = function(section_id) {
|
||||
const value = feat.vid_option ? uci.get('network', section_id, feat.vid_option) : null;
|
||||
return (value || uci.get('network', section_id, 'vlan'));
|
||||
};
|
||||
|
||||
s.option(form.Value, 'description', _('Description'));
|
||||
|
||||
for (let j = 0; Array.isArray(topology.ports) && j < topology.ports.length; j++) {
|
||||
const portspec = topology.ports[j];
|
||||
const portstate = Array.isArray(topology.portstate) ? topology.portstate[portspec.num] : null;
|
||||
|
||||
o = s.option(form.ListValue, String(portspec.num), portspec.label);
|
||||
o.value('', _('off'));
|
||||
|
||||
if (!portspec.need_tag)
|
||||
o.value('u', _('untagged'));
|
||||
|
||||
o.value('t', _('tagged'));
|
||||
|
||||
o.cfgvalue = parse_portvalue;
|
||||
o.validate = validate_portvalue;
|
||||
o.write = function() {};
|
||||
|
||||
o.description = render_port_status(E('small', {
|
||||
'data-switch': switch_name,
|
||||
'data-port': portspec.num
|
||||
}), portstate);
|
||||
|
||||
port_opts.push(o);
|
||||
}
|
||||
|
||||
port_opts.sort(function(a, b) {
|
||||
return a.option > b.option;
|
||||
});
|
||||
}
|
||||
|
||||
poll.add(L.bind(update_port_status, m, topologies));
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user