9f1f8dc57f75d7fb59ca76900833ecc4d92b1df7
[project/luci.git] / modules / luci-mod-network / htdocs / luci-static / resources / view / network / dhcp.js
1 'use strict';
2 'require rpc';
3 'require uci';
4 'require form';
5 'require validation';
6
7 var callHostHints, callDUIDHints, callDHCPLeases, CBILeaseStatus, CBILease6Status;
8
9 callHostHints = rpc.declare({
10 object: 'luci-rpc',
11 method: 'getHostHints',
12 expect: { '': {} }
13 });
14
15 callDUIDHints = rpc.declare({
16 object: 'luci-rpc',
17 method: 'getDUIDHints',
18 expect: { '': {} }
19 });
20
21 callDHCPLeases = rpc.declare({
22 object: 'luci-rpc',
23 method: 'getDHCPLeases',
24 expect: { '': {} }
25 });
26
27 CBILeaseStatus = form.DummyValue.extend({
28 renderWidget: function(section_id, option_id, cfgvalue) {
29 return E([
30 E('h4', _('Active DHCP Leases')),
31 E('div', { 'id': 'lease_status_table', 'class': 'table' }, [
32 E('div', { 'class': 'tr table-titles' }, [
33 E('div', { 'class': 'th' }, _('Hostname')),
34 E('div', { 'class': 'th' }, _('IPv4-Address')),
35 E('div', { 'class': 'th' }, _('MAC-Address')),
36 E('div', { 'class': 'th' }, _('Leasetime remaining'))
37 ]),
38 E('div', { 'class': 'tr placeholder' }, [
39 E('div', { 'class': 'td' }, E('em', _('Collecting data...')))
40 ])
41 ])
42 ]);
43 }
44 });
45
46 CBILease6Status = form.DummyValue.extend({
47 renderWidget: function(section_id, option_id, cfgvalue) {
48 return E([
49 E('h4', _('Active DHCPv6 Leases')),
50 E('div', { 'id': 'lease6_status_table', 'class': 'table' }, [
51 E('div', { 'class': 'tr table-titles' }, [
52 E('div', { 'class': 'th' }, _('Host')),
53 E('div', { 'class': 'th' }, _('IPv6-Address')),
54 E('div', { 'class': 'th' }, _('DUID')),
55 E('div', { 'class': 'th' }, _('Leasetime remaining'))
56 ]),
57 E('div', { 'class': 'tr placeholder' }, [
58 E('div', { 'class': 'td' }, E('em', _('Collecting data...')))
59 ])
60 ])
61 ]);
62 }
63 });
64
65 function validateHostname(sid, s) {
66 if (s.length > 256)
67 return _('Expecting: %s').format(_('valid hostname'));
68
69 var labels = s.replace(/^\.+|\.$/g, '').split(/\./);
70
71 for (var i = 0; i < labels.length; i++)
72 if (!labels[i].match(/^[a-z0-9_](?:[a-z0-9-]{0,61}[a-z0-9])?$/i))
73 return _('Expecting: %s').format(_('valid hostname'));
74
75 return true;
76 }
77
78 function validateAddressList(sid, s) {
79 if (s == null || s == '')
80 return true;
81
82 var m = s.match(/^\/(.+)\/$/),
83 names = m ? m[1].split(/\//) : [ s ];
84
85 for (var i = 0; i < names.length; i++) {
86 var res = validateHostname(sid, names[i]);
87
88 if (res !== true)
89 return res;
90 }
91
92 return true;
93 }
94
95 function validateServerSpec(sid, s) {
96 if (s == null || s == '')
97 return true;
98
99 var m = s.match(/^\/(.+)\/(.*)$/);
100 if (!m)
101 return _('Expecting: %s').format(_('valid hostname'));
102
103 var res = validateAddressList(sid, m[1]);
104 if (res !== true)
105 return res;
106
107 if (m[2] == '' || m[2] == '#')
108 return true;
109
110 // ipaddr%scopeid#srvport@source@interface#srcport
111
112 m = m[2].match(/^([0-9a-f:.]+)(?:%[^#@]+)?(?:#(\d+))?(?:@([0-9a-f:.]+)(?:@[^#]+)?(?:#(\d+))?)?$/);
113
114 if (!m)
115 return _('Expecting: %s').format(_('valid IP address'));
116 else if (validation.parseIPv4(m[1]) && m[3] != null && !validation.parseIPv4(m[3]))
117 return _('Expecting: %s').format(_('valid IPv4 address'));
118 else if (validation.parseIPv6(m[1]) && m[3] != null && !validation.parseIPv6(m[3]))
119 return _('Expecting: %s').format(_('valid IPv6 address'));
120 else if ((m[2] != null && +m[2] > 65535) || (m[4] != null && +m[4] > 65535))
121 return _('Expecting: %s').format(_('valid port value'));
122
123 return true;
124 }
125
126 return L.view.extend({
127 load: function() {
128 return Promise.all([
129 callHostHints(),
130 callDUIDHints()
131 ]);
132 },
133
134 render: function(hosts_duids) {
135 var has_dhcpv6 = L.hasSystemFeature('dnsmasq', 'dhcpv6') || L.hasSystemFeature('odhcpd'),
136 hosts = hosts_duids[0],
137 duids = hosts_duids[1],
138 m, s, o, ss, so;
139
140 m = new form.Map('dhcp', _('DHCP and DNS'), _('Dnsmasq is a combined <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr>-Server and <abbr title="Domain Name System">DNS</abbr>-Forwarder for <abbr title="Network Address Translation">NAT</abbr> firewalls'));
141
142 s = m.section(form.TypedSection, 'dnsmasq', _('Server Settings'));
143 s.anonymous = true;
144 s.addremove = false;
145
146 s.tab('general', _('General Settings'));
147 s.tab('files', _('Resolv and Hosts Files'));
148 s.tab('tftp', _('TFTP Settings'));
149 s.tab('advanced', _('Advanced Settings'));
150 s.tab('leases', _('Static Leases'));
151
152 s.taboption('general', form.Flag, 'domainneeded',
153 _('Domain required'),
154 _('Don\'t forward <abbr title="Domain Name System">DNS</abbr>-Requests without <abbr title="Domain Name System">DNS</abbr>-Name'));
155
156 s.taboption('general', form.Flag, 'authoritative',
157 _('Authoritative'),
158 _('This is the only <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr> in the local network'));
159
160
161 s.taboption('files', form.Flag, 'readethers',
162 _('Use <code>/etc/ethers</code>'),
163 _('Read <code>/etc/ethers</code> to configure the <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr>-Server'));
164
165 s.taboption('files', form.Value, 'leasefile',
166 _('Leasefile'),
167 _('file where given <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr>-leases will be stored'));
168
169 s.taboption('files', form.Flag, 'noresolv',
170 _('Ignore resolve file')).optional = true;
171
172 o = s.taboption('files', form.Value, 'resolvfile',
173 _('Resolve file'),
174 _('local <abbr title="Domain Name System">DNS</abbr> file'));
175
176 o.depends('noresolv', '');
177 o.optional = true;
178
179
180 s.taboption('files', form.Flag, 'nohosts',
181 _('Ignore <code>/etc/hosts</code>')).optional = true;
182
183 s.taboption('files', form.DynamicList, 'addnhosts',
184 _('Additional Hosts files')).optional = true;
185
186 o = s.taboption('advanced', form.Flag, 'quietdhcp',
187 _('Suppress logging'),
188 _('Suppress logging of the routine operation of these protocols'));
189 o.optional = true;
190
191 o = s.taboption('advanced', form.Flag, 'sequential_ip',
192 _('Allocate IP sequentially'),
193 _('Allocate IP addresses sequentially, starting from the lowest available address'));
194 o.optional = true;
195
196 o = s.taboption('advanced', form.Flag, 'boguspriv',
197 _('Filter private'),
198 _('Do not forward reverse lookups for local networks'));
199 o.default = o.enabled;
200
201 s.taboption('advanced', form.Flag, 'filterwin2k',
202 _('Filter useless'),
203 _('Do not forward requests that cannot be answered by public name servers'));
204
205
206 s.taboption('advanced', form.Flag, 'localise_queries',
207 _('Localise queries'),
208 _('Localise hostname depending on the requesting subnet if multiple IPs are available'));
209
210 if (L.hasSystemFeature('dnsmasq', 'dnssec')) {
211 o = s.taboption('advanced', form.Flag, 'dnssec',
212 _('DNSSEC'));
213 o.optional = true;
214
215 o = s.taboption('advanced', form.Flag, 'dnsseccheckunsigned',
216 _('DNSSEC check unsigned'),
217 _('Requires upstream supports DNSSEC; verify unsigned domain responses really come from unsigned domains'));
218 o.default = o.enabled;
219 o.optional = true;
220 }
221
222 s.taboption('general', form.Value, 'local',
223 _('Local server'),
224 _('Local domain specification. Names matching this domain are never forwarded and are resolved from DHCP or hosts files only'));
225
226 s.taboption('general', form.Value, 'domain',
227 _('Local domain'),
228 _('Local domain suffix appended to DHCP names and hosts file entries'));
229
230 s.taboption('advanced', form.Flag, 'expandhosts',
231 _('Expand hosts'),
232 _('Add local domain suffix to names served from hosts files'));
233
234 s.taboption('advanced', form.Flag, 'nonegcache',
235 _('No negative cache'),
236 _('Do not cache negative replies, e.g. for not existing domains'));
237
238 s.taboption('advanced', form.Value, 'serversfile',
239 _('Additional servers file'),
240 _('This file may contain lines like \'server=/domain/1.2.3.4\' or \'server=1.2.3.4\' for domain-specific or full upstream <abbr title="Domain Name System">DNS</abbr> servers.'));
241
242 s.taboption('advanced', form.Flag, 'strictorder',
243 _('Strict order'),
244 _('<abbr title="Domain Name System">DNS</abbr> servers will be queried in the order of the resolvfile')).optional = true;
245
246 s.taboption('advanced', form.Flag, 'allservers',
247 _('All Servers'),
248 _('Query all available upstream <abbr title="Domain Name System">DNS</abbr> servers')).optional = true;
249
250 o = s.taboption('advanced', form.DynamicList, 'bogusnxdomain', _('Bogus NX Domain Override'),
251 _('List of hosts that supply bogus NX domain results'));
252
253 o.optional = true;
254 o.placeholder = '67.215.65.132';
255
256
257 s.taboption('general', form.Flag, 'logqueries',
258 _('Log queries'),
259 _('Write received DNS requests to syslog')).optional = true;
260
261 o = s.taboption('general', form.DynamicList, 'server', _('DNS forwardings'),
262 _('List of <abbr title="Domain Name System">DNS</abbr> servers to forward requests to'));
263
264 o.optional = true;
265 o.placeholder = '/example.org/10.1.2.3';
266 o.validate = validateServerSpec;
267
268
269 o = s.taboption('general', form.Flag, 'rebind_protection',
270 _('Rebind protection'),
271 _('Discard upstream RFC1918 responses'));
272
273 o.rmempty = false;
274
275
276 o = s.taboption('general', form.Flag, 'rebind_localhost',
277 _('Allow localhost'),
278 _('Allow upstream responses in the 127.0.0.0/8 range, e.g. for RBL services'));
279
280 o.depends('rebind_protection', '1');
281
282
283 o = s.taboption('general', form.DynamicList, 'rebind_domain',
284 _('Domain whitelist'),
285 _('List of domains to allow RFC1918 responses for'));
286 o.optional = true;
287
288 o.depends('rebind_protection', '1');
289 o.placeholder = 'ihost.netflix.com';
290 o.validate = validateAddressList;
291
292
293 o = s.taboption('advanced', form.Value, 'port',
294 _('<abbr title="Domain Name System">DNS</abbr> server port'),
295 _('Listening port for inbound DNS queries'));
296
297 o.optional = true;
298 o.datatype = 'port';
299 o.placeholder = 53;
300
301
302 o = s.taboption('advanced', form.Value, 'queryport',
303 _('<abbr title="Domain Name System">DNS</abbr> query port'),
304 _('Fixed source port for outbound DNS queries'));
305
306 o.optional = true;
307 o.datatype = 'port';
308 o.placeholder = _('any');
309
310
311 o = s.taboption('advanced', form.Value, 'dhcpleasemax',
312 _('<abbr title="maximal">Max.</abbr> <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr> leases'),
313 _('Maximum allowed number of active DHCP leases'));
314
315 o.optional = true;
316 o.datatype = 'uinteger';
317 o.placeholder = _('unlimited');
318
319
320 o = s.taboption('advanced', form.Value, 'ednspacket_max',
321 _('<abbr title="maximal">Max.</abbr> <abbr title="Extension Mechanisms for Domain Name System">EDNS0</abbr> packet size'),
322 _('Maximum allowed size of EDNS.0 UDP packets'));
323
324 o.optional = true;
325 o.datatype = 'uinteger';
326 o.placeholder = 1280;
327
328
329 o = s.taboption('advanced', form.Value, 'dnsforwardmax',
330 _('<abbr title="maximal">Max.</abbr> concurrent queries'),
331 _('Maximum allowed number of concurrent DNS queries'));
332
333 o.optional = true;
334 o.datatype = 'uinteger';
335 o.placeholder = 150;
336
337 o = s.taboption('advanced', form.Value, 'cachesize',
338 _('Size of DNS query cache'),
339 _('Number of cached DNS entries (max is 10000, 0 is no caching)'));
340 o.optional = true;
341 o.datatype = 'range(0,10000)';
342 o.placeholder = 150;
343
344 s.taboption('tftp', form.Flag, 'enable_tftp',
345 _('Enable TFTP server')).optional = true;
346
347 o = s.taboption('tftp', form.Value, 'tftp_root',
348 _('TFTP server root'),
349 _('Root directory for files served via TFTP'));
350
351 o.optional = true;
352 o.depends('enable_tftp', '1');
353 o.placeholder = '/';
354
355
356 o = s.taboption('tftp', form.Value, 'dhcp_boot',
357 _('Network boot image'),
358 _('Filename of the boot image advertised to clients'));
359
360 o.optional = true;
361 o.depends('enable_tftp', '1');
362 o.placeholder = 'pxelinux.0';
363
364 o = s.taboption('general', form.Flag, 'localservice',
365 _('Local Service Only'),
366 _('Limit DNS service to subnets interfaces on which we are serving DNS.'));
367 o.optional = false;
368 o.rmempty = false;
369
370 o = s.taboption('general', form.Flag, 'nonwildcard',
371 _('Non-wildcard'),
372 _('Bind dynamically to interfaces rather than wildcard address (recommended as linux default)'));
373 o.default = o.enabled;
374 o.optional = false;
375 o.rmempty = true;
376
377 o = s.taboption('general', form.DynamicList, 'interface',
378 _('Listen Interfaces'),
379 _('Limit listening to these interfaces, and loopback.'));
380 o.optional = true;
381
382 o = s.taboption('general', form.DynamicList, 'notinterface',
383 _('Exclude interfaces'),
384 _('Prevent listening on these interfaces.'));
385 o.optional = true;
386
387 o = s.taboption('leases', form.SectionValue, '__leases__', form.GridSection, 'host', null,
388 _('Static leases are used to assign fixed IP addresses and symbolic hostnames to DHCP clients. They are also required for non-dynamic interface configurations where only hosts with a corresponding lease are served.') + '<br />' +
389 _('Use the <em>Add</em> Button to add a new lease entry. The <em>MAC-Address</em> identifies the host, the <em>IPv4-Address</em> specifies the fixed address to use, and the <em>Hostname</em> is assigned as a symbolic name to the requesting host. The optional <em>Lease time</em> can be used to set non-standard host-specific lease time, e.g. 12h, 3d or infinite.'));
390
391 ss = o.subsection;
392
393 ss.addremove = true;
394 ss.anonymous = true;
395
396 so = ss.option(form.Value, 'name', _('Hostname'));
397 so.datatype = 'hostname("strict")';
398 so.rmempty = true;
399 so.write = function(section, value) {
400 uci.set('dhcp', section, 'name', value);
401 uci.set('dhcp', section, 'dns', '1');
402 };
403 so.remove = function(section) {
404 uci.unset('dhcp', section, 'name');
405 uci.unset('dhcp', section, 'dns');
406 };
407
408 so = ss.option(form.Value, 'mac', _('<abbr title="Media Access Control">MAC</abbr>-Address'));
409 so.datatype = 'list(unique(macaddr))';
410 so.rmempty = true;
411 so.cfgvalue = function(section) {
412 var macs = uci.get('dhcp', section, 'mac'),
413 result = [];
414
415 if (!Array.isArray(macs))
416 macs = (macs != null && macs != '') ? macs.split(/\ss+/) : [];
417
418 for (var i = 0, mac; (mac = macs[i]) != null; i++)
419 if (/^([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2})$/.test(mac))
420 result.push('%02X:%02X:%02X:%02X:%02X:%02X'.format(
421 parseInt(RegExp.$1, 16), parseInt(RegExp.$2, 16),
422 parseInt(RegExp.$3, 16), parseInt(RegExp.$4, 16),
423 parseInt(RegExp.$5, 16), parseInt(RegExp.$6, 16)));
424
425 return result.length ? result.join(' ') : null;
426 };
427 so.renderWidget = function(section_id, option_index, cfgvalue) {
428 var node = form.Value.prototype.renderWidget.apply(this, [section_id, option_index, cfgvalue]),
429 ipopt = this.section.children.filter(function(o) { return o.option == 'ip' })[0];
430
431 node.addEventListener('cbi-dropdown-change', L.bind(function(ipopt, section_id, ev) {
432 var mac = ev.detail.value.value;
433 if (mac == null || mac == '' || !hosts[mac] || !hosts[mac].ipv4)
434 return;
435
436 var ip = ipopt.formvalue(section_id);
437 if (ip != null && ip != '')
438 return;
439
440 var node = ipopt.map.findElement('id', ipopt.cbid(section_id));
441 if (node)
442 L.dom.callClassMethod(node, 'setValue', hosts[mac].ipv4);
443 }, this, ipopt, section_id));
444
445 return node;
446 };
447 Object.keys(hosts).forEach(function(mac) {
448 var hint = hosts[mac].name || hosts[mac].ipv4;
449 so.value(mac, hint ? '%s (%s)'.format(mac, hint) : mac);
450 });
451
452 so = ss.option(form.Value, 'ip', _('<abbr title="Internet Protocol Version 4">IPv4</abbr>-Address'));
453 so.datatype = 'or(ip4addr,"ignore")';
454 so.validate = function(section, value) {
455 var mac = this.map.lookupOption('mac', section),
456 name = this.map.lookupOption('name', section),
457 m = mac ? mac[0].formvalue(section) : null,
458 n = name ? name[0].formvalue(section) : null;
459
460 if ((m == null || m == '') && (n == null || n == ''))
461 return _('One of hostname or mac address must be specified!');
462
463 return true;
464 };
465 Object.keys(hosts).forEach(function(mac) {
466 if (hosts[mac].ipv4) {
467 var hint = hosts[mac].name;
468 so.value(hosts[mac].ipv4, hint ? '%s (%s)'.format(hosts[mac].ipv4, hint) : hosts[mac].ipv4);
469 }
470 });
471
472 so = ss.option(form.Value, 'leasetime', _('Lease time'));
473 so.rmempty = true;
474
475 so = ss.option(form.Value, 'duid', _('<abbr title="The DHCP Unique Identifier">DUID</abbr>'));
476 so.datatype = 'and(rangelength(20,36),hexstring)';
477 Object.keys(duids).forEach(function(duid) {
478 so.value(duid, '%s (%s)'.format(duid, duids[duid].hostname || duids[duid].macaddr || duids[duid].ip6addr || '?'));
479 });
480
481 so = ss.option(form.Value, 'hostid', _('<abbr title="Internet Protocol Version 6">IPv6</abbr>-Suffix (hex)'));
482
483 o = s.taboption('leases', CBILeaseStatus, '__status__');
484
485 if (has_dhcpv6)
486 o = s.taboption('leases', CBILease6Status, '__status6__');
487
488 return m.render().then(function(mapEl) {
489 L.Poll.add(function() {
490 return callDHCPLeases().then(function(leaseinfo) {
491 var leases = Array.isArray(leaseinfo.dhcp_leases) ? leaseinfo.dhcp_leases : [],
492 leases6 = Array.isArray(leaseinfo.dhcp6_leases) ? leaseinfo.dhcp6_leases : [];
493
494 cbi_update_table(mapEl.querySelector('#lease_status_table'),
495 leases.map(function(lease) {
496 var exp;
497
498 if (lease.expires === false)
499 exp = E('em', _('unlimited'));
500 else if (lease.expires <= 0)
501 exp = E('em', _('expired'));
502 else
503 exp = '%t'.format(lease.expires);
504
505 return [
506 lease.hostname || '?',
507 lease.ipaddr,
508 lease.macaddr,
509 exp
510 ];
511 }),
512 E('em', _('There are no active leases')));
513
514 if (has_dhcpv6) {
515 cbi_update_table(mapEl.querySelector('#lease6_status_table'),
516 leases6.map(function(lease) {
517 var exp;
518
519 if (lease.expires === false)
520 exp = E('em', _('unlimited'));
521 else if (lease.expires <= 0)
522 exp = E('em', _('expired'));
523 else
524 exp = '%t'.format(lease.expires);
525
526 var hint = lease.macaddr ? hosts[lease.macaddr] : null,
527 name = hint ? (hint.name || hint.ipv4 || hint.ipv6) : null,
528 host = null;
529
530 if (name && lease.hostname && lease.hostname != name && lease.ip6addr != name)
531 host = '%s (%s)'.format(lease.hostname, name);
532 else if (lease.hostname)
533 host = lease.hostname;
534 else if (name)
535 host = name;
536
537 return [
538 host || '-',
539 lease.ip6addrs ? lease.ip6addrs.join(' ') : lease.ip6addr,
540 lease.duid,
541 exp
542 ];
543 }),
544 E('em', _('There are no active leases')));
545 }
546 });
547 });
548
549 return mapEl;
550 });
551 }
552 });