network.js 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987
  1. 'use strict';
  2. 'require fs';
  3. 'require ui';
  4. 'require dom';
  5. 'require uci';
  6. 'require form';
  7. 'require network';
  8. 'require baseclass';
  9. 'require validation';
  10. 'require tools.widgets as widgets';
  11. function validateAddr(section_id, value) {
  12. if (value == '')
  13. return true;
  14. var ipv6 = /6$/.test(this.section.formvalue(section_id, 'mode')),
  15. addr = ipv6 ? validation.parseIPv6(value) : validation.parseIPv4(value);
  16. return addr ? true : (ipv6 ? _('Expecting a valid IPv6 address') : _('Expecting a valid IPv4 address'));
  17. }
  18. function validateQoSMap(section_id, value) {
  19. if (value == '')
  20. return true;
  21. var m = value.match(/^(\d+):(\d+)$/);
  22. if (!m || +m[1] > 0xFFFFFFFF || +m[2] > 0xFFFFFFFF)
  23. return _('Expecting two priority values separated by a colon');
  24. return true;
  25. }
  26. function deviceSectionExists(section_id, devname) {
  27. var exists = false;
  28. uci.sections('network', 'device', function(ss) {
  29. exists = exists || (
  30. ss['.name'] != section_id &&
  31. ss.name == devname
  32. );
  33. });
  34. return exists;
  35. }
  36. function isBridgePort(dev) {
  37. if (!dev)
  38. return false;
  39. if (dev.isBridgePort())
  40. return true;
  41. var isPort = false;
  42. uci.sections('network', null, function(s) {
  43. if (s['.type'] != 'interface' && s['.type'] != 'device')
  44. return;
  45. if (s.type == 'bridge' && L.toArray(s.ifname).indexOf(dev.getName()) > -1)
  46. isPort = true;
  47. });
  48. return isPort;
  49. }
  50. function updateDevBadge(node, dev) {
  51. var type = dev.getType(),
  52. up = dev.getCarrier();
  53. dom.content(node, [
  54. E('img', {
  55. 'class': 'middle',
  56. 'src': L.resource('icons/%s%s.png').format(type, up ? '' : '_disabled')
  57. }),
  58. '\x0a', dev.getName()
  59. ]);
  60. return node;
  61. }
  62. function renderDevBadge(dev) {
  63. return updateDevBadge(E('span', {
  64. 'class': 'ifacebadge port-status-device',
  65. 'style': 'font-weight:normal',
  66. 'data-device': dev.getName()
  67. }), dev);
  68. }
  69. function updatePortStatus(node, dev) {
  70. var carrier = dev.getCarrier(),
  71. duplex = dev.getDuplex(),
  72. speed = dev.getSpeed(),
  73. desc, title;
  74. if (carrier && speed > 0 && duplex != null) {
  75. desc = '%d%s'.format(speed, duplex == 'full' ? 'FD' : 'HD');
  76. title = '%s, %d MBit/s, %s'.format(_('Connected'), speed, duplex == 'full' ? _('full-duplex') : _('half-duplex'));
  77. }
  78. else if (carrier) {
  79. desc = _('Connected');
  80. }
  81. else {
  82. desc = _('no link');
  83. }
  84. dom.content(node, [
  85. E('img', {
  86. 'class': 'middle',
  87. 'src': L.resource('icons/port_%s.png').format(carrier ? 'up' : 'down')
  88. }),
  89. '\x0a', desc
  90. ]);
  91. if (title)
  92. node.setAttribute('data-tooltip', title);
  93. else
  94. node.removeAttribute('data-tooltip');
  95. return node;
  96. }
  97. function renderPortStatus(dev) {
  98. return updatePortStatus(E('span', {
  99. 'class': 'ifacebadge port-status-link',
  100. 'data-device': dev.getName()
  101. }), dev);
  102. }
  103. function updatePlaceholders(opt, section_id) {
  104. var dev = network.instantiateDevice(opt.getUIElement(section_id).getValue());
  105. for (var i = 0, co; (co = opt.section.children[i]) != null; i++) {
  106. if (co !== opt) {
  107. switch (co.option) {
  108. case 'mtu':
  109. case 'mtu6':
  110. co.getUIElement(section_id).setPlaceholder(dev.getMTU());
  111. break;
  112. case 'macaddr':
  113. co.getUIElement(section_id).setPlaceholder(dev.getMAC());
  114. break;
  115. case 'txqueuelen':
  116. co.getUIElement(section_id).setPlaceholder(dev._devstate('qlen'));
  117. break;
  118. }
  119. }
  120. }
  121. }
  122. var cbiFlagTristate = form.ListValue.extend({
  123. __init__: function(/* ... */) {
  124. this.super('__init__', arguments);
  125. this.keylist = [ '', '0!', '1!' ];
  126. this.vallist = [ _('automatic'), _('disabled'), _('enabled') ];
  127. },
  128. load: function(section_id) {
  129. var invert = false, sysfs = this.sysfs;
  130. if (sysfs) {
  131. if (sysfs.charAt(0) == '!') {
  132. invert = true;
  133. sysfs = sysfs.substring(1);
  134. }
  135. return L.resolveDefault(fs.read(sysfs), '').then(L.bind(function(res) {
  136. res = (res || '').trim();
  137. if (res == '0')
  138. this.sysfs_default = invert;
  139. else if (res == '1')
  140. this.sysfs_default = !invert;
  141. return this.super('load', [section_id]);
  142. }, this));
  143. }
  144. return this.super('load', [section_id]);
  145. },
  146. write: function(section_id, formvalue) {
  147. if (formvalue == '1!')
  148. return this.super('write', [section_id, '1']);
  149. else if (formvalue == '0!')
  150. return this.super('write', [section_id, '0']);
  151. else
  152. return this.super('remove', [section_id]);
  153. },
  154. renderWidget: function(section_id, option_index, cfgvalue) {
  155. var sysdef = this.sysfs_default;
  156. if (this.sysfs_default !== null) {
  157. this.keylist[0] = sysdef ? '1' : '0';
  158. this.vallist[0] = sysdef ? _('automatic (enabled)') : _('automatic (disabled)');
  159. }
  160. return this.super('renderWidget', [section_id, option_index, cfgvalue ? cfgvalue + '!' : null]);
  161. }
  162. });
  163. var cbiTagValue = form.Value.extend({
  164. renderWidget: function(section_id, option_index, cfgvalue) {
  165. var widget = new ui.Dropdown(cfgvalue || ['-'], {
  166. '-': E([], [
  167. E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ '—' ]),
  168. E('span', { 'class': 'hide-close' }, [ _('Not Member', 'VLAN port state') ])
  169. ]),
  170. 'u': E([], [
  171. E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ 'U' ]),
  172. E('span', { 'class': 'hide-close' }, [ _('Untagged', 'VLAN port state') ])
  173. ]),
  174. 't': E([], [
  175. E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ 'T' ]),
  176. E('span', { 'class': 'hide-close' }, [ _('Tagged', 'VLAN port state') ])
  177. ]),
  178. '*': E([], [
  179. E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ '*' ]),
  180. E('span', { 'class': 'hide-close' }, [ _('Is Primary VLAN', 'VLAN port state') ])
  181. ])
  182. }, {
  183. id: this.cbid(section_id),
  184. sort: [ '-', 'u', 't', '*' ],
  185. optional: false,
  186. multiple: true
  187. });
  188. var field = this;
  189. widget.toggleItem = function(sb, li, force_state) {
  190. var lis = li.parentNode.querySelectorAll('li'),
  191. toggle = ui.Dropdown.prototype.toggleItem;
  192. toggle.apply(this, [sb, li, force_state]);
  193. if (force_state != null)
  194. return;
  195. switch (li.getAttribute('data-value'))
  196. {
  197. case '-':
  198. if (li.hasAttribute('selected')) {
  199. for (var i = 0; i < lis.length; i++) {
  200. switch (lis[i].getAttribute('data-value')) {
  201. case '-':
  202. break;
  203. case '*':
  204. toggle.apply(this, [sb, lis[i], false]);
  205. lis[i].setAttribute('unselectable', '');
  206. break;
  207. default:
  208. toggle.apply(this, [sb, lis[i], false]);
  209. }
  210. }
  211. }
  212. break;
  213. case 't':
  214. case 'u':
  215. if (li.hasAttribute('selected')) {
  216. for (var i = 0; i < lis.length; i++) {
  217. switch (lis[i].getAttribute('data-value')) {
  218. case li.getAttribute('data-value'):
  219. break;
  220. case '*':
  221. lis[i].removeAttribute('unselectable');
  222. break;
  223. default:
  224. toggle.apply(this, [sb, lis[i], false]);
  225. }
  226. }
  227. }
  228. else {
  229. toggle.apply(this, [sb, li, true]);
  230. }
  231. break;
  232. case '*':
  233. if (li.hasAttribute('selected')) {
  234. var section_ids = field.section.cfgsections();
  235. for (var i = 0; i < section_ids.length; i++) {
  236. var other_widget = field.getUIElement(section_ids[i]),
  237. other_value = L.toArray(other_widget.getValue());
  238. if (other_widget === this)
  239. continue;
  240. var new_value = other_value.filter(function(v) { return v != '*' });
  241. if (new_value.length == other_value.length)
  242. continue;
  243. other_widget.setValue(new_value);
  244. break;
  245. }
  246. }
  247. }
  248. };
  249. var node = widget.render();
  250. node.style.minWidth = '4em';
  251. if (cfgvalue == '-')
  252. node.querySelector('li[data-value="*"]').setAttribute('unselectable', '');
  253. return E('div', { 'style': 'display:inline-block' }, node);
  254. },
  255. cfgvalue: function(section_id) {
  256. var ports = L.toArray(uci.get('network', section_id, 'ports'));
  257. for (var i = 0; i < ports.length; i++) {
  258. var s = ports[i].split(/:/);
  259. if (s[0] != this.port)
  260. continue;
  261. var t = /t/.test(s[1] || '') ? 't' : 'u';
  262. return /\x2a/.test(s[1] || '') ? [t, '*'] : [t];
  263. }
  264. return ['-'];
  265. },
  266. write: function(section_id, value) {
  267. var ports = [];
  268. for (var i = 0; i < this.section.children.length; i++) {
  269. var opt = this.section.children[i];
  270. if (opt.port) {
  271. var val = L.toArray(opt.formvalue(section_id)).join('');
  272. switch (val) {
  273. case '-':
  274. break;
  275. case 'u':
  276. ports.push(opt.port);
  277. break;
  278. default:
  279. ports.push('%s:%s'.format(opt.port, val));
  280. break;
  281. }
  282. }
  283. }
  284. uci.set('network', section_id, 'ports', ports.length ? ports : null);
  285. },
  286. remove: function() {}
  287. });
  288. return baseclass.extend({
  289. replaceOption: function(s, tabName, optionClass, optionName, optionTitle, optionDescription) {
  290. var o = s.getOption(optionName);
  291. if (o) {
  292. if (o.tab) {
  293. s.tabs[o.tab].children = s.tabs[o.tab].children.filter(function(opt) {
  294. return opt.option != optionName;
  295. });
  296. }
  297. s.children = s.children.filter(function(opt) {
  298. return opt.option != optionName;
  299. });
  300. }
  301. return s.taboption(tabName, optionClass, optionName, optionTitle, optionDescription);
  302. },
  303. addDeviceOptions: function(s, dev, isNew) {
  304. var parent_dev = dev ? dev.getParent() : null,
  305. devname = dev ? dev.getName() : null,
  306. o, ss;
  307. s.tab('devgeneral', _('General device options'));
  308. s.tab('devadvanced', _('Advanced device options'));
  309. s.tab('brport', _('Bridge port specific options'));
  310. s.tab('bridgevlan', _('Bridge VLAN filtering'));
  311. o = this.replaceOption(s, 'devgeneral', form.ListValue, 'type', _('Device type'));
  312. o.readonly = !isNew;
  313. o.value('', _('Network device'));
  314. o.value('bridge', _('Bridge device'));
  315. o.value('8021q', _('VLAN (802.1q)'));
  316. o.value('8021ad', _('VLAN (802.1ad)'));
  317. o.value('macvlan', _('MAC VLAN'));
  318. o.value('veth', _('Virtual Ethernet'));
  319. o.validate = function(section_id, value) {
  320. if (value == 'bridge' || value == 'veth')
  321. updatePlaceholders(this.section.getOption('name_complex'), section_id);
  322. return true;
  323. };
  324. o = this.replaceOption(s, 'devgeneral', widgets.DeviceSelect, 'name_simple', _('Existing device'));
  325. o.readonly = !isNew;
  326. o.rmempty = false;
  327. o.noaliases = true;
  328. o.default = (dev ? dev.getName() : '');
  329. o.ucioption = 'name';
  330. o.filter = function(section_id, value) {
  331. var dev = network.instantiateDevice(value);
  332. return !deviceSectionExists(section_id, value) && (dev.getType() != 'wifi' || dev.isUp());
  333. };
  334. o.validate = function(section_id, value) {
  335. updatePlaceholders(this, section_id);
  336. return deviceSectionExists(section_id, value)
  337. ? _('A configuration for the device "%s" already exists').format(value) : true;
  338. };
  339. o.onchange = function(ev, section_id, values) {
  340. updatePlaceholders(this, section_id);
  341. };
  342. o.depends('type', '');
  343. o = this.replaceOption(s, 'devgeneral', widgets.DeviceSelect, 'ifname_single', _('Base device'));
  344. o.readonly = !isNew;
  345. o.rmempty = false;
  346. o.noaliases = true;
  347. o.default = (dev ? dev.getName() : '').match(/^.+\.\d+$/) ? dev.getName().replace(/\.\d+$/, '') : '';
  348. o.ucioption = 'ifname';
  349. o.filter = function(section_id, value) {
  350. var dev = network.instantiateDevice(value);
  351. return (dev.getType() != 'wifi' || dev.isUp());
  352. };
  353. o.validate = function(section_id, value) {
  354. updatePlaceholders(this, section_id);
  355. if (isNew) {
  356. var type = this.section.formvalue(section_id, 'type'),
  357. name = this.section.getUIElement(section_id, 'name_complex');
  358. if (type == 'macvlan' && value && name && !name.isChanged()) {
  359. var i = 0;
  360. while (deviceSectionExists(section_id, '%smac%d'.format(value, i)))
  361. i++;
  362. name.setValue('%smac%d'.format(value, i));
  363. name.triggerValidation();
  364. }
  365. }
  366. return true;
  367. };
  368. o.onchange = function(ev, section_id, values) {
  369. updatePlaceholders(this, section_id);
  370. };
  371. o.depends('type', '8021q');
  372. o.depends('type', '8021ad');
  373. o.depends('type', 'macvlan');
  374. o = this.replaceOption(s, 'devgeneral', form.Value, 'vid', _('VLAN ID'));
  375. o.readonly = !isNew;
  376. o.datatype = 'range(1, 4094)';
  377. o.rmempty = false;
  378. o.default = (dev ? dev.getName() : '').match(/^.+\.\d+$/) ? dev.getName().replace(/^.+\./, '') : '';
  379. o.validate = function(section_id, value) {
  380. var base = this.section.formvalue(section_id, 'ifname_single'),
  381. vid = this.section.formvalue(section_id, 'vid'),
  382. name = this.section.getUIElement(section_id, 'name_complex');
  383. if (base && vid && name && !name.isChanged() && isNew) {
  384. name.setValue('%s.%d'.format(base, vid));
  385. name.triggerValidation();
  386. }
  387. return true;
  388. };
  389. o.depends('type', '8021q');
  390. o.depends('type', '8021ad');
  391. o = this.replaceOption(s, 'devgeneral', form.ListValue, 'mode', _('Mode'));
  392. o.value('vepa', _('VEPA (Virtual Ethernet Port Aggregator)', 'MACVLAN mode'));
  393. o.value('private', _('Private (Prevent communication between MAC VLANs)', 'MACVLAN mode'));
  394. o.value('bridge', _('Bridge (Support direct communication between MAC VLANs)', 'MACVLAN mode'));
  395. o.value('passthru', _('Pass-through (Mirror physical device to single MAC VLAN)', 'MACVLAN mode'));
  396. o.depends('type', 'macvlan');
  397. o = this.replaceOption(s, 'devgeneral', form.Value, 'name_complex', _('Device name'));
  398. o.rmempty = false;
  399. o.datatype = 'maxlength(15)';
  400. o.readonly = !isNew;
  401. o.ucioption = 'name';
  402. o.validate = function(section_id, value) {
  403. var dev = network.instantiateDevice(value);
  404. if (deviceSectionExists(section_id, value) || (isNew && (dev.dev || {}).idx))
  405. return _('The device name "%s" is already taken').format(value);
  406. return true;
  407. };
  408. o.depends({ type: '', '!reverse': true });
  409. o = this.replaceOption(s, 'devadvanced', form.DynamicList, 'ingress_qos_mapping', _('Ingress QoS mapping'), _('Defines a mapping of VLAN header priority to the Linux internal packet priority on incoming frames'));
  410. o.rmempty = true;
  411. o.validate = validateQoSMap;
  412. o.depends('type', '8021q');
  413. o.depends('type', '8021ad');
  414. o = this.replaceOption(s, 'devadvanced', form.DynamicList, 'egress_qos_mapping', _('Egress QoS mapping'), _('Defines a mapping of Linux internal packet priority to VLAN header priority but for outgoing frames'));
  415. o.rmempty = true;
  416. o.validate = validateQoSMap;
  417. o.depends('type', '8021q');
  418. o.depends('type', '8021ad');
  419. o = this.replaceOption(s, 'devgeneral', widgets.DeviceSelect, 'ifname_multi', _('Bridge ports'));
  420. o.size = 10;
  421. o.rmempty = true;
  422. o.multiple = true;
  423. o.noaliases = true;
  424. o.nobridges = true;
  425. o.ucioption = 'ports';
  426. o.default = L.toArray(dev ? dev.getPorts() : null).filter(function(p) { return p.getType() != 'wifi' }).map(function(p) { return p.getName() });
  427. o.filter = function(section_id, device_name) {
  428. var bridge_name = uci.get('network', section_id, 'name'),
  429. choice_dev = network.instantiateDevice(device_name),
  430. parent_dev = choice_dev.getParent();
  431. /* only show wifi networks which are already present in "option ifname" */
  432. if (choice_dev.getType() == 'wifi') {
  433. var ifnames = L.toArray(uci.get('network', section_id, 'ports'));
  434. for (var i = 0; i < ifnames.length; i++)
  435. if (ifnames[i] == device_name)
  436. return true;
  437. return false;
  438. }
  439. return (!parent_dev || parent_dev.getName() != bridge_name);
  440. };
  441. o.description = _('Specifies the wired ports to attach to this bridge. In order to attach wireless networks, choose the associated interface as network in the wireless settings.')
  442. o.onchange = function(ev, section_id, values) {
  443. ss.updatePorts(values);
  444. return ss.parse().then(function() {
  445. ss.redraw();
  446. });
  447. };
  448. o.depends('type', 'bridge');
  449. o = this.replaceOption(s, 'devgeneral', form.Flag, 'bridge_empty', _('Bring up empty bridge'), _('Bring up the bridge interface even if no ports are attached'));
  450. o.default = o.disabled;
  451. o.depends('type', 'bridge');
  452. o = this.replaceOption(s, 'devadvanced', form.Value, 'priority', _('Priority'));
  453. o.placeholder = '32767';
  454. o.datatype = 'range(0, 65535)';
  455. o.depends('type', 'bridge');
  456. o = this.replaceOption(s, 'devadvanced', form.Value, 'ageing_time', _('Ageing time'), _('Timeout in seconds for learned MAC addresses in the forwarding database'));
  457. o.placeholder = '30';
  458. o.datatype = 'uinteger';
  459. o.depends('type', 'bridge');
  460. o = this.replaceOption(s, 'devadvanced', form.Flag, 'stp', _('Enable <abbr title="Spanning Tree Protocol">STP</abbr>'), _('Enables the Spanning Tree Protocol on this bridge'));
  461. o.default = o.disabled;
  462. o.depends('type', 'bridge');
  463. o = this.replaceOption(s, 'devadvanced', form.Value, 'hello_time', _('Hello interval'), _('Interval in seconds for STP hello packets'));
  464. o.placeholder = '2';
  465. o.datatype = 'range(1, 10)';
  466. o.depends({ type: 'bridge', stp: '1' });
  467. o = this.replaceOption(s, 'devadvanced', form.Value, 'forward_delay', _('Forward delay'), _('Time in seconds to spend in listening and learning states'));
  468. o.placeholder = '15';
  469. o.datatype = 'range(2, 30)';
  470. o.depends({ type: 'bridge', stp: '1' });
  471. o = this.replaceOption(s, 'devadvanced', form.Value, 'max_age', _('Maximum age'), _('Timeout in seconds until topology updates on link loss'));
  472. o.placeholder = '20';
  473. o.datatype = 'range(6, 40)';
  474. o.depends({ type: 'bridge', stp: '1' });
  475. o = this.replaceOption(s, 'devadvanced', form.Flag, 'igmp_snooping', _('Enable <abbr title="Internet Group Management Protocol">IGMP</abbr> snooping'), _('Enables IGMP snooping on this bridge'));
  476. o.default = o.disabled;
  477. o.depends('type', 'bridge');
  478. o = this.replaceOption(s, 'devadvanced', form.Value, 'hash_max', _('Maximum snooping table size'));
  479. o.placeholder = '512';
  480. o.datatype = 'uinteger';
  481. o.depends({ type: 'bridge', igmp_snooping: '1' });
  482. o = this.replaceOption(s, 'devadvanced', form.Flag, 'multicast_querier', _('Enable multicast querier'));
  483. o.defaults = { '1': [{'igmp_snooping': '1'}], '0': [{'igmp_snooping': '0'}] };
  484. o.depends('type', 'bridge');
  485. o = this.replaceOption(s, 'devadvanced', form.Value, 'robustness', _('Robustness'), _('The robustness value allows tuning for the expected packet loss on the network. If a network is expected to be lossy, the robustness value may be increased. IGMP is robust to (Robustness-1) packet losses'));
  486. o.placeholder = '2';
  487. o.datatype = 'min(1)';
  488. o.depends({ type: 'bridge', multicast_querier: '1' });
  489. o = this.replaceOption(s, 'devadvanced', form.Value, 'query_interval', _('Query interval'), _('Interval in centiseconds between multicast general queries. By varying the value, an administrator may tune the number of IGMP messages on the subnet; larger values cause IGMP Queries to be sent less often'));
  490. o.placeholder = '12500';
  491. o.datatype = 'uinteger';
  492. o.depends({ type: 'bridge', multicast_querier: '1' });
  493. o = this.replaceOption(s, 'devadvanced', form.Value, 'query_response_interval', _('Query response interval'), _('The max response time in centiseconds inserted into the periodic general queries. By varying the value, an administrator may tune the burstiness of IGMP messages on the subnet; larger values make the traffic less bursty, as host responses are spread out over a larger interval'));
  494. o.placeholder = '1000';
  495. o.datatype = 'uinteger';
  496. o.validate = function(section_id, value) {
  497. var qiopt = L.toArray(this.map.lookupOption('query_interval', section_id))[0],
  498. qival = qiopt ? (qiopt.formvalue(section_id) || qiopt.placeholder) : '';
  499. if (value != '' && qival != '' && +value >= +qival)
  500. return _('The query response interval must be lower than the query interval value');
  501. return true;
  502. };
  503. o.depends({ type: 'bridge', multicast_querier: '1' });
  504. o = this.replaceOption(s, 'devadvanced', form.Value, 'last_member_interval', _('Last member interval'), _('The max response time in centiseconds inserted into group-specific queries sent in response to leave group messages. It is also the amount of time between group-specific query messages. This value may be tuned to modify the "leave latency" of the network. A reduced value results in reduced time to detect the loss of the last member of a group'));
  505. o.placeholder = '100';
  506. o.datatype = 'uinteger';
  507. o.depends({ type: 'bridge', multicast_querier: '1' });
  508. o = this.replaceOption(s, 'devgeneral', form.Value, 'mtu', _('MTU'));
  509. o.datatype = 'range(576, 9200)';
  510. o.validate = function(section_id, value) {
  511. var parent_mtu = (dev && dev.getType() == 'vlan') ? (parent_dev ? parent_dev.getMTU() : null) : null;
  512. if (parent_mtu !== null && +value > parent_mtu)
  513. return _('The MTU must not exceed the parent device MTU of %d bytes').format(parent_mtu);
  514. return true;
  515. };
  516. o = this.replaceOption(s, 'devgeneral', form.Value, 'macaddr', _('MAC address'));
  517. o.datatype = 'macaddr';
  518. o = this.replaceOption(s, 'devgeneral', form.Value, 'peer_name', _('Peer device name'));
  519. o.rmempty = true;
  520. o.datatype = 'maxlength(15)';
  521. o.depends('type', 'veth');
  522. o.load = function(section_id) {
  523. var sections = uci.sections('network', 'device'),
  524. idx = 0;
  525. for (var i = 0; i < sections.length; i++)
  526. if (sections[i]['.name'] == section_id)
  527. break;
  528. else if (sections[i].type == 'veth')
  529. idx++;
  530. this.placeholder = 'veth%d'.format(idx);
  531. return form.Value.prototype.load.apply(this, arguments);
  532. };
  533. o = this.replaceOption(s, 'devgeneral', form.Value, 'peer_macaddr', _('Peer MAC address'));
  534. o.rmempty = true;
  535. o.datatype = 'macaddr';
  536. o.depends('type', 'veth');
  537. o = this.replaceOption(s, 'devgeneral', form.Value, 'txqueuelen', _('TX queue length'));
  538. o.placeholder = dev ? dev._devstate('qlen') : '';
  539. o.datatype = 'uinteger';
  540. o = this.replaceOption(s, 'devadvanced', cbiFlagTristate, 'promisc', _('Enable promiscuous mode'));
  541. o.sysfs_default = (dev && dev.dev && dev.dev.flags) ? dev.dev.flags.promisc : null;
  542. o = this.replaceOption(s, 'devadvanced', form.ListValue, 'rpfilter', _('Reverse path filter'));
  543. o.default = '';
  544. o.value('', _('disabled'));
  545. o.value('loose', _('Loose filtering'));
  546. o.value('strict', _('Strict filtering'));
  547. o.cfgvalue = function(/* ... */) {
  548. var val = form.ListValue.prototype.cfgvalue.apply(this, arguments);
  549. switch (val || '') {
  550. case 'loose':
  551. case '1':
  552. return 'loose';
  553. case 'strict':
  554. case '2':
  555. return 'strict';
  556. default:
  557. return '';
  558. }
  559. };
  560. o = this.replaceOption(s, 'devadvanced', cbiFlagTristate, 'acceptlocal', _('Accept local'), _('Accept packets with local source addresses'));
  561. o.sysfs = '/proc/sys/net/ipv4/conf/%s/accept_local'.format(devname || 'default');
  562. o = this.replaceOption(s, 'devadvanced', cbiFlagTristate, 'sendredirects', _('Send ICMP redirects'));
  563. o.sysfs = '/proc/sys/net/ipv4/conf/%s/send_redirects'.format(devname || 'default');
  564. o = this.replaceOption(s, 'devadvanced', cbiFlagTristate, 'arp_accept', _('Honor gratuitous ARP'), _('When enabled, new ARP table entries are added from received gratuitous ARP requests or replies, otherwise only preexisting table entries are updated, but no new hosts are learned.'));
  565. o.sysfs = '/proc/sys/net/ipv4/conf/%s/arp_accept'.format(devname || 'default');
  566. o = this.replaceOption(s, 'devadvanced', cbiFlagTristate, 'drop_gratuitous_arp', _('Drop gratuitous ARP'), _('Drop all gratuitous ARP frames, for example if there’s a known good ARP proxy on the network and such frames need not be used or in the case of 802.11, must not be used to prevent attacks.'));
  567. o.sysfs = '/proc/sys/net/ipv4/conf/%s/drop_gratuitous_arp'.format(devname || 'default');
  568. o = this.replaceOption(s, 'devadvanced', form.Value, 'neighreachabletime', _('Neighbour cache validity'), _('Time in milliseconds'));
  569. o.placeholder = '30000';
  570. o.datatype = 'uinteger';
  571. o = this.replaceOption(s, 'devadvanced', form.Value, 'neighgcstaletime', _('Stale neighbour cache timeout'), _('Timeout in seconds'));
  572. o.placeholder = '60';
  573. o.datatype = 'uinteger';
  574. o = this.replaceOption(s, 'devadvanced', form.Value, 'neighlocktime', _('Minimum ARP validity time'), _('Minimum required time in seconds before an ARP entry may be replaced. Prevents ARP cache thrashing.'));
  575. o.placeholder = '0';
  576. o.datatype = 'uinteger';
  577. o = this.replaceOption(s, 'devgeneral', cbiFlagTristate, 'ipv6', _('Enable IPv6'));
  578. o.sysfs = '!/proc/sys/net/ipv6/conf/%s/disable_ipv6'.format(devname || 'default');
  579. o = this.replaceOption(s, 'devadvanced', cbiFlagTristate, 'ip6segmentrouting', _('Enable IPv6 segment routing'));
  580. o.sysfs = '/proc/sys/net/ipv6/conf/%s/seg6_enabled'.format(devname || 'default');
  581. o.depends('ipv6', /1/);
  582. o = this.replaceOption(s, 'devadvanced', cbiFlagTristate, 'drop_unsolicited_na', _('Drop unsolicited NA'), _('Drop all unsolicited neighbor advertisements, for example if there’s a known good NA proxy on the network and such frames need not be used or in the case of 802.11, must not be used to prevent attacks.'));
  583. o.sysfs = '/proc/sys/net/ipv6/conf/%s/drop_unsolicited_na'.format(devname || 'default');
  584. o.depends('ipv6', /1/);
  585. o = this.replaceOption(s, 'devgeneral', form.Value, 'mtu6', _('IPv6 MTU'));
  586. o.datatype = 'max(9200)';
  587. o.depends('ipv6', /1/);
  588. o = this.replaceOption(s, 'devgeneral', form.Value, 'dadtransmits', _('DAD transmits'), _('Amount of Duplicate Address Detection probes to send'));
  589. o.placeholder = '1';
  590. o.datatype = 'uinteger';
  591. o.depends('ipv6', /1/);
  592. o = this.replaceOption(s, 'devadvanced', cbiFlagTristate, 'multicast', _('Enable multicast support'));
  593. o.sysfs_default = (dev && dev.dev && dev.dev.flags) ? dev.dev.flags.multicast : null;
  594. o = this.replaceOption(s, 'devadvanced', form.ListValue, 'igmpversion', _('Force IGMP version'));
  595. o.value('', _('No enforcement'));
  596. o.value('1', _('Enforce IGMPv1'));
  597. o.value('2', _('Enforce IGMPv2'));
  598. o.value('3', _('Enforce IGMPv3'));
  599. o.depends('multicast', /1/);
  600. o = this.replaceOption(s, 'devadvanced', form.ListValue, 'mldversion', _('Force MLD version'));
  601. o.value('', _('No enforcement'));
  602. o.value('1', _('Enforce MLD version 1'));
  603. o.value('2', _('Enforce MLD version 2'));
  604. o.depends('multicast', /1/);
  605. if (isBridgePort(dev)) {
  606. o = this.replaceOption(s, 'brport', cbiFlagTristate, 'learning', _('Enable MAC address learning'));
  607. o.sysfs = '/sys/class/net/%s/brport/learning'.format(devname || 'default');
  608. o = this.replaceOption(s, 'brport', cbiFlagTristate, 'unicast_flood', _('Enable unicast flooding'));
  609. o.sysfs = '/sys/class/net/%s/brport/unicast_flood'.format(devname || 'default');
  610. o = this.replaceOption(s, 'brport', cbiFlagTristate, 'isolate', _('Port isolation'), _('Only allow communication with non-isolated bridge ports when enabled'));
  611. o.sysfs = '/sys/class/net/%s/brport/isolated'.format(devname || 'default');
  612. o = this.replaceOption(s, 'brport', form.ListValue, 'multicast_router', _('Multicast routing'));
  613. o.value('', _('Never'));
  614. o.value('1', _('Learn'));
  615. o.value('2', _('Always'));
  616. o.depends('multicast', /1/);
  617. o = this.replaceOption(s, 'brport', cbiFlagTristate, 'multicast_to_unicast', _('Multicast to unicast'), _('Forward multicast packets as unicast packets on this device.'));
  618. o.sysfs = '/sys/class/net/%s/brport/multicast_to_unicast'.format(devname || 'default');
  619. o.depends('multicast', /1/);
  620. o = this.replaceOption(s, 'brport', cbiFlagTristate, 'multicast_fast_leave', _('Enable multicast fast leave'));
  621. o.sysfs = '/sys/class/net/%s/brport/multicast_fast_leave'.format(devname || 'default');
  622. o.depends('multicast', /1/);
  623. o = this.replaceOption(s, 'brport', cbiFlagTristate, 'drop_v4_unicast_in_l2_multicast', _('Drop nested IPv4 unicast'), _('Drop layer 2 multicast frames containing IPv4 unicast packets.'));
  624. o.sysfs = '/proc/sys/net/ipv4/conf/%s/drop_unicast_in_l2_multicast'.format(devname || 'default');
  625. o.depends('multicast', /1/);
  626. o = this.replaceOption(s, 'brport', cbiFlagTristate, 'drop_v6_unicast_in_l2_multicast', _('Drop nested IPv6 unicast'), _('Drop layer 2 multicast frames containing IPv6 unicast packets.'));
  627. o.sysfs = '/proc/sys/net/ipv6/conf/%s/drop_unicast_in_l2_multicast'.format(devname || 'default');
  628. o.depends('multicast', /1/);
  629. }
  630. o = this.replaceOption(s, 'bridgevlan', form.Flag, 'vlan_filtering', _('Enable VLAN filtering'));
  631. o.depends('type', 'bridge');
  632. o.updateDefaultValue = function(section_id) {
  633. var device = uci.get('network', s.section, 'name'),
  634. uielem = this.getUIElement(section_id),
  635. has_vlans = false;
  636. uci.sections('network', 'bridge-vlan', function(bvs) {
  637. has_vlans = has_vlans || (bvs.device == device);
  638. });
  639. this.default = has_vlans ? this.enabled : this.disabled;
  640. if (uielem && !uielem.isChanged())
  641. uielem.setValue(this.default);
  642. };
  643. o = this.replaceOption(s, 'bridgevlan', form.SectionValue, 'bridge-vlan', form.TableSection, 'bridge-vlan');
  644. o.depends('type', 'bridge');
  645. ss = o.subsection;
  646. ss.addremove = true;
  647. ss.anonymous = true;
  648. ss.renderHeaderRows = function(/* ... */) {
  649. var node = form.TableSection.prototype.renderHeaderRows.apply(this, arguments);
  650. node.querySelectorAll('.th').forEach(function(th) {
  651. th.classList.add('left');
  652. th.classList.add('middle');
  653. });
  654. return node;
  655. };
  656. ss.filter = function(section_id) {
  657. var devname = uci.get('network', s.section, 'name');
  658. return (uci.get('network', section_id, 'device') == devname);
  659. };
  660. ss.render = function(/* ... */) {
  661. return form.TableSection.prototype.render.apply(this, arguments).then(L.bind(function(node) {
  662. node.style.overflow = 'auto hidden';
  663. node.style.paddingTop = '1em';
  664. if (this.node)
  665. this.node.parentNode.replaceChild(node, this.node);
  666. this.node = node;
  667. return node;
  668. }, this));
  669. };
  670. ss.redraw = function() {
  671. return this.load().then(L.bind(this.render, this));
  672. };
  673. ss.updatePorts = function(ports) {
  674. var devices = ports.map(function(port) {
  675. return network.instantiateDevice(port)
  676. }).filter(function(dev) {
  677. return dev.getType() != 'wifi' || dev.isUp();
  678. }).sort(function(a, b) {
  679. return L.naturalCompare(a.getName(), b.getName());
  680. });
  681. this.children = this.children.filter(function(opt) { return !opt.option.match(/^port_/) });
  682. for (var i = 0; i < devices.length; i++) {
  683. o = ss.option(cbiTagValue, 'port_%s'.format(sfh(devices[i].getName())), renderDevBadge(devices[i]), renderPortStatus(devices[i]));
  684. o.port = devices[i].getName();
  685. }
  686. var section_ids = this.cfgsections(),
  687. device_names = devices.reduce(function(names, dev) { names[dev.getName()] = true; return names }, {});
  688. for (var i = 0; i < section_ids.length; i++) {
  689. var old_spec = L.toArray(uci.get('network', section_ids[i], 'ports')),
  690. new_spec = old_spec.filter(function(spec) { return device_names[spec.replace(/:[ut*]+$/, '')] });
  691. if (old_spec.length != new_spec.length)
  692. uci.set('network', section_ids[i], 'ports', new_spec.length ? new_spec : null);
  693. }
  694. };
  695. ss.handleAdd = function(ev) {
  696. return s.parse().then(L.bind(function() {
  697. var device = uci.get('network', s.section, 'name'),
  698. section_ids = this.cfgsections(),
  699. section_id = null,
  700. max_vlan_id = 0;
  701. if (!device)
  702. return;
  703. for (var i = 0; i < section_ids.length; i++) {
  704. var vid = +uci.get('network', section_ids[i], 'vlan');
  705. if (vid > max_vlan_id)
  706. max_vlan_id = vid;
  707. }
  708. section_id = uci.add('network', 'bridge-vlan');
  709. uci.set('network', section_id, 'device', device);
  710. uci.set('network', section_id, 'vlan', max_vlan_id + 1);
  711. s.children.forEach(function(opt) {
  712. switch (opt.option) {
  713. case 'type':
  714. case 'name_complex':
  715. var input = opt.map.findElement('id', 'widget.%s'.format(opt.cbid(s.section)));
  716. if (input)
  717. input.disabled = true;
  718. break;
  719. }
  720. });
  721. s.getOption('vlan_filtering').updateDefaultValue(s.section);
  722. s.map.addedVLANs = s.map.addedVLANs || [];
  723. s.map.addedVLANs.push(section_id);
  724. return this.redraw();
  725. }, this));
  726. };
  727. ss.handleRemove = function(section_id) {
  728. this.map.data.remove('network', section_id);
  729. s.map.addedVLANs = (s.map.addedVLANs || []).filter(function(sid) {
  730. return sid != section_id;
  731. });
  732. return this.redraw();
  733. };
  734. o = ss.option(form.Value, 'vlan', _('VLAN ID'));
  735. o.datatype = 'range(1, 4094)';
  736. o.renderWidget = function(/* ... */) {
  737. var node = form.Value.prototype.renderWidget.apply(this, arguments);
  738. node.style.width = '5em';
  739. return node;
  740. };
  741. o.validate = function(section_id, value) {
  742. var section_ids = this.section.cfgsections();
  743. for (var i = 0; i < section_ids.length; i++) {
  744. if (section_ids[i] == section_id)
  745. continue;
  746. if (uci.get('network', section_ids[i], 'vlan') == value)
  747. return _('The VLAN ID must be unique');
  748. }
  749. return true;
  750. };
  751. o = ss.option(form.Flag, 'local', _('Local'));
  752. o.default = o.enabled;
  753. var ports = [];
  754. var seen_ports = {};
  755. L.toArray(uci.get('network', s.section, 'ports')).forEach(function(port) {
  756. seen_ports[port] = true;
  757. });
  758. uci.sections('network', 'bridge-vlan', function(bvs) {
  759. if (uci.get('network', s.section, 'name') != bvs.device)
  760. return;
  761. L.toArray(bvs.ports).forEach(function(portspec) {
  762. var m = portspec.match(/^([^:]+)(?::[ut*]+)?$/);
  763. if (m)
  764. seen_ports[m[1]] = true;
  765. });
  766. });
  767. for (var port_name in seen_ports)
  768. ports.push(port_name);
  769. ss.updatePorts(ports);
  770. },
  771. updateDevBadge: updateDevBadge,
  772. updatePortStatus: updatePortStatus
  773. });