luci-app-firewall: Add 'any' choice for SNAT 'family' option
[project/luci.git] / applications / luci-app-firewall / htdocs / luci-static / resources / view / firewall / snats.js
1 'use strict';
2 'require view';
3 'require ui';
4 'require rpc';
5 'require uci';
6 'require form';
7 'require firewall as fwmodel';
8 'require tools.firewall as fwtool';
9 'require tools.widgets as widgets';
10
11 function rule_proto_txt(s) {
12 var family = (uci.get('firewall', s, 'family') || '').toLowerCase().replace(/^(?:all|\*)$/, 'any');
13 var sip = uci.get('firewall', s, 'src_ip') || '';
14 var dip = uci.get('firewall', s, 'dest_ip') || '';
15 var rwip = uci.get('firewall', s, 'snat_ip') || '';
16 var proto = L.toArray(uci.get('firewall', s, 'proto')).filter(function(p) {
17 return (p != '*' && p != 'any' && p != 'all');
18 }).map(function(p) {
19 var pr = fwtool.lookupProto(p);
20 return {
21 num: pr[0],
22 name: pr[1]
23 };
24 });
25
26 var m = String(uci.get('firewall', s, 'mark')).match(/^(!\s*)?(0x[0-9a-f]{1,8}|[0-9]{1,10})(?:\/(0x[0-9a-f]{1,8}|[0-9]{1,10}))?$/i);
27 var f = m ? {
28 val: m[0].toUpperCase().replace(/X/g, 'x'),
29 inv: m[1],
30 num: '0x%02X'.format(+m[2]),
31 mask: m[3] ? '0x%02X'.format(+m[3]) : null
32 } : null;
33
34 return fwtool.fmt(_('Forwarded %{ipv6?%{ipv4?<var>IPv4</var> and <var>IPv6</var>:<var>IPv6</var>}:<var>IPv4</var>}%{proto?, protocol %{proto#%{next?, }<var>%{item.name}</var>}}%{mark?, mark <var%{mark.inv? data-tooltip="Match fwmarks except %{mark.num}%{mark.mask? with mask %{mark.mask}}.":%{mark.mask? data-tooltip="Mask fwmark value with %{mark.mask} before compare."}}>%{mark.val}</var>}'), {
35 ipv4: (family == 'ipv4' || (!family && sip.indexOf(':') == -1 && dip.indexOf(':') == -1 && rwip.indexOf(':') == -1)),
36 ipv6: (family == 'ipv6' || (!family && (!sip || !dip || !rwip)) || (!family && (sip.indexOf(':') != -1 || dip.indexOf(':') != -1 || rwip.indexOf(':') != -1))),
37 proto: proto,
38 mark: f
39 });
40 }
41
42 function rule_src_txt(s, hosts) {
43 var z = uci.get('firewall', s, 'src');
44
45 return fwtool.fmt(_('From %{src}%{src_device?, interface <var>%{src_device}</var>}%{src_ip?, IP %{src_ip#%{next?, }<var%{item.inv? data-tooltip="Match IP addresses except %{item.val}."}>%{item.ival}</var>}}%{src_port?, port %{src_port#%{next?, }<var%{item.inv? data-tooltip="Match ports except %{item.val}."}>%{item.ival}</var>}}'), {
46 src: E('span', { 'class': 'zonebadge', 'style': fwmodel.getZoneColorStyle(null) }, [E('em', _('any zone'))]),
47 src_ip: fwtool.map_invert(uci.get('firewall', s, 'src_ip'), 'toLowerCase'),
48 src_port: fwtool.map_invert(uci.get('firewall', s, 'src_port'))
49 });
50 }
51
52 function rule_dest_txt(s) {
53 var z = uci.get('firewall', s, 'src');
54
55 return fwtool.fmt(_('To %{dest}%{dest_device?, via interface <var>%{dest_device}</var>}%{dest_ip?, IP %{dest_ip#%{next?, }<var%{item.inv? data-tooltip="Match IP addresses except %{item.val}."}>%{item.ival}</var>}}%{dest_port?, port %{dest_port#%{next?, }<var%{item.inv? data-tooltip="Match ports except %{item.val}."}>%{item.ival}</var>}}'), {
56 dest: E('span', { 'class': 'zonebadge', 'style': fwmodel.getZoneColorStyle(z) }, [(z == '*') ? E('em', _('any zone')) : (z ? E('strong', z) : E('em', _('this device')))]),
57 dest_ip: fwtool.map_invert(uci.get('firewall', s, 'dest_ip'), 'toLowerCase'),
58 dest_port: fwtool.map_invert(uci.get('firewall', s, 'dest_port')),
59 dest_device: uci.get('firewall', s, 'device')
60 });
61 }
62
63 function rule_limit_txt(s) {
64 var m = String(uci.get('firewall', s, 'limit')).match(/^(\d+)\/([smhd])\w*$/i),
65 l = m ? {
66 num: +m[1],
67 unit: ({ s: _('second'), m: _('minute'), h: _('hour'), d: _('day') })[m[2]],
68 burst: uci.get('firewall', s, 'limit_burst')
69 } : null;
70
71 if (!l)
72 return '';
73
74 return fwtool.fmt(_('Limit matching to <var>%{limit.num}</var> packets per <var>%{limit.unit}</var>%{limit.burst? burst <var>%{limit.burst}</var>}'), { limit: l });
75 }
76
77 function rule_target_txt(s) {
78 var t = uci.get('firewall', s, 'target'),
79 s = {
80 target: t,
81 snat_ip: uci.get('firewall', s, 'snat_ip'),
82 snat_port: uci.get('firewall', s, 'snat_port')
83 };
84
85 switch (t) {
86 case 'SNAT':
87 return fwtool.fmt(_('<var data-tooltip="SNAT">Statically rewrite</var> to source %{snat_ip?IP <var>%{snat_ip}</var>} %{snat_port?port <var>%{snat_port}</var>}'), s);
88
89 case 'MASQUERADE':
90 return fwtool.fmt(_('<var data-tooltip="MASQUERADE">Automatically rewrite</var> source IP'));
91
92 case 'ACCEPT':
93 return fwtool.fmt(_('<var data-tooltip="ACCEPT">Prevent source rewrite</var>'));
94
95 default:
96 return t;
97 }
98 }
99
100 function validate_opt_family(m, section_id, opt) {
101 var sopt = m.section.getOption('src_ip'),
102 dopt = m.section.getOption('dest_ip'),
103 rwopt = m.section.getOption('snat_ip'),
104 fmopt = m.section.getOption('family'),
105 tgopt = m.section.getOption('target');
106
107 if (!sopt.isValid(section_id) && opt != 'src_ip')
108 return true;
109 if (!dopt.isValid(section_id) && opt != 'dest_ip')
110 return true;
111 if (!rwopt.isValid(section_id) && opt != 'snat_ip')
112 return true;
113 if (!fmopt.isValid(section_id) && opt != 'family')
114 return true;
115 if (!tgopt.isValid(section_id) && opt != 'target')
116 return true;
117
118 var sip = sopt.formvalue(section_id) || '',
119 dip = dopt.formvalue(section_id) || '',
120 rwip = rwopt.formvalue(section_id) || '',
121 fm = fmopt.formvalue(section_id) || '',
122 tg = tgopt.formvalue(section_id);
123
124 if (fm == 'ipv6' && (sip.indexOf(':') != -1 || sip == '') && (dip.indexOf(':') != -1 || dip == '') && ((rwip.indexOf(':') != -1 && tg == 'SNAT') || rwip == ''))
125 return true;
126 if (fm == 'ipv4' && (sip.indexOf(':') == -1) && (dip.indexOf(':') == -1) && ((rwip.indexOf(':') == -1 && tg == 'SNAT') || rwip == ''))
127 return true;
128 if (fm == '' || fm == 'any') {
129 if ((sip.indexOf(':') != -1 || sip == '') && (dip.indexOf(':') != -1 || dip == '') && ((rwip.indexOf(':') != -1 && tg == 'SNAT') || rwip == ''))
130 return true;
131 if ((sip.indexOf(':') == -1) && (dip.indexOf(':') == -1) && ((rwip.indexOf(':') == -1 && tg == 'SNAT') || rwip == ''))
132 return true;
133 }
134
135 return _('Address family, source address, destination address, rewrite IP address must match');
136 }
137
138 return view.extend({
139 callHostHints: rpc.declare({
140 object: 'luci-rpc',
141 method: 'getHostHints',
142 expect: { '': {} }
143 }),
144
145 callNetworkDevices: rpc.declare({
146 object: 'luci-rpc',
147 method: 'getNetworkDevices',
148 expect: { '': {} }
149 }),
150
151 load: function() {
152 return Promise.all([
153 this.callHostHints(),
154 this.callNetworkDevices(),
155 uci.load('firewall')
156 ]);
157 },
158
159 render: function(data) {
160 if (fwtool.checkLegacySNAT())
161 return fwtool.renderMigration();
162 else
163 return this.renderNats(data);
164 },
165
166 renderNats: function(data) {
167 var hosts = data[0],
168 devs = data[1],
169 m, s, o;
170 var fw4 = L.hasSystemFeature('firewall4');
171
172 m = new form.Map('firewall', _('Firewall - NAT Rules'),
173 _('NAT rules allow fine grained control over the source IP to use for outbound or forwarded traffic.'));
174
175 s = m.section(form.GridSection, 'nat', _('NAT Rules'));
176 s.addremove = true;
177 s.anonymous = true;
178 s.sortable = true;
179
180 s.tab('general', _('General Settings'));
181 s.tab('advanced', _('Advanced Settings'));
182 s.tab('timed', _('Time Restrictions'));
183
184 s.sectiontitle = function(section_id) {
185 return uci.get('firewall', section_id, 'name') || _('Unnamed NAT');
186 };
187
188 o = s.taboption('general', form.Value, 'name', _('Name'));
189 o.placeholder = _('Unnamed NAT');
190 o.modalonly = true;
191
192 o = s.option(form.DummyValue, '_match', _('Match'));
193 o.modalonly = false;
194 o.textvalue = function(s) {
195 return E('small', [
196 rule_proto_txt(s), E('br'),
197 rule_src_txt(s, hosts), E('br'),
198 rule_dest_txt(s), E('br'),
199 rule_limit_txt(s)
200 ]);
201 };
202
203 o = s.option(form.ListValue, '_target', _('Action'));
204 o.modalonly = false;
205 o.textvalue = function(s) {
206 return rule_target_txt(s);
207 };
208
209 o = s.option(form.Flag, 'enabled', _('Enable'));
210 o.modalonly = false;
211 o.default = o.enabled;
212 o.editable = true;
213
214 if (fw4) {
215 o = s.taboption('general', form.ListValue, 'family', _('Restrict to address family'));
216 o.modalonly = true;
217 o.rmempty = true;
218 o.value('any', _('IPv4 and IPv6'));
219 o.value('ipv4', _('IPv4 only'));
220 o.value('ipv6', _('IPv6 only'));
221 o.value('', _('automatic')); // infer from zone or used IP addresses
222 o.cfgvalue = function(section_id) {
223 var val = this.map.data.get(this.map.config, section_id, 'family');
224
225 if (!val)
226 return '';
227 else if (val == 'any' || val == 'all' || val == '*')
228 return 'any';
229 else if (val == 'inet' || String(val).indexOf('4') != -1)
230 return 'ipv4';
231 else if (String(val).indexOf('6') != -1)
232 return 'ipv6';
233 };
234 o.validate = function(section_id, value) {
235 fwtool.updateHostHints(this.map, section_id, 'src_ip', value, hosts);
236 fwtool.updateHostHints(this.map, section_id, 'dest_ip', value, hosts);
237 return !fw4?true:validate_opt_family(this, section_id, 'family');
238 };
239 }
240
241 o = s.taboption('general', fwtool.CBIProtocolSelect, 'proto', _('Protocol'));
242 o.modalonly = true;
243 o.default = 'all';
244
245 o = s.taboption('general', widgets.ZoneSelect, 'src', _('Outbound zone'));
246 o.modalonly = true;
247 o.rmempty = false;
248 o.nocreate = true;
249 o.allowany = true;
250 o.default = 'lan';
251
252 o = fwtool.addIPOption(s, 'general', 'src_ip', _('Source address'),
253 _('Match forwarded traffic from this IP or range.'), !fw4?'ipv4':'', hosts);
254 o.rmempty = true;
255 o.datatype = !fw4?'neg(ipmask4("true"))':'neg(ipmask("true"))';
256 o.validate = function(section_id, value) {
257 return !fw4?true:validate_opt_family(this, section_id, 'src_ip');
258 };
259
260 o = s.taboption('general', form.Value, 'src_port', _('Source port'),
261 _('Match forwarded traffic originating from the given source port or port range.'));
262 o.modalonly = true;
263 o.rmempty = true;
264 o.datatype = 'neg(portrange)';
265 o.placeholder = _('any');
266 o.depends({ proto: 'tcp', '!contains': true });
267 o.depends({ proto: 'udp', '!contains': true });
268
269 o = fwtool.addIPOption(s, 'general', 'dest_ip', _('Destination address'),
270 _('Match forwarded traffic directed at the given IP address.'), !fw4?'ipv4':'', hosts);
271 o.rmempty = true;
272 o.datatype = !fw4?'neg(ipmask4("true"))':'neg(ipmask("true"))';
273 o.validate = function(section_id, value) {
274 return !fw4?true:validate_opt_family(this, section_id, 'dest_ip');
275 };
276
277 o = s.taboption('general', form.Value, 'dest_port', _('Destination port'),
278 _('Match forwarded traffic directed at the given destination port or port range.'));
279 o.modalonly = true;
280 o.rmempty = true;
281 o.placeholder = _('any');
282 o.datatype = 'neg(portrange)';
283 o.depends({ proto: 'tcp', '!contains': true });
284 o.depends({ proto: 'udp', '!contains': true });
285
286 o = s.taboption('general', form.ListValue, 'target', _('Action'));
287 o.modalonly = true;
288 o.default = 'SNAT';
289 o.value('SNAT', _('SNAT - Rewrite to specific source IP or port'));
290 o.value('MASQUERADE', _('MASQUERADE - Automatically rewrite to outbound interface IP'));
291 o.value('ACCEPT', _('ACCEPT - Disable address rewriting'));
292 o.validate = function(section_id, value) {
293 return !fw4?true:validate_opt_family(this, section_id, 'target');
294 };
295
296 o = fwtool.addLocalIPOption(s, 'general', 'snat_ip', _('Rewrite IP address'),
297 _('Rewrite matched traffic to the specified source IP address.'), devs);
298 o.placeholder = null;
299 o.depends('target', 'SNAT');
300 o.validate = function(section_id, value) {
301 var a = this.formvalue(section_id),
302 p = this.section.formvalue(section_id, 'snat_port');
303
304 if ((a == null || a == '') && (p == null || p == '') && value == '')
305 return _('A rewrite IP must be specified!');
306
307 return !fw4?true:validate_opt_family(this, section_id, 'snat_ip');
308 };
309
310 o = s.taboption('general', form.Value, 'snat_port', _('Rewrite port'),
311 _('Rewrite matched traffic to the specified source port or port range.'));
312 o.modalonly = true;
313 o.rmempty = true;
314 o.placeholder = _('do not rewrite');
315 o.datatype = 'portrange';
316 o.depends({ proto: 'tcp', '!contains': true });
317 o.depends({ proto: 'udp', '!contains': true });
318
319 var have_fw4 = L.hasSystemFeature('firewall4')
320 if (!have_fw4) {
321 o = s.taboption('advanced', form.Value, 'ipset', _('Use ipset'));
322 uci.sections('firewall', 'ipset', function(s) {
323 if (typeof(s.name) == 'string')
324 o.value(s.name, s.comment ? '%s (%s)'.format(s.name, s.comment) : s.name);
325 });
326 o.modalonly = true;
327 o.rmempty = true;
328 }
329
330 o = s.taboption('advanced', widgets.DeviceSelect, 'device', _('Outbound device'),
331 _('Matches forwarded traffic using the specified outbound network device.'));
332 o.noaliases = true;
333 o.modalonly = true;
334 o.rmempty = true;
335
336 fwtool.addMarkOption(s, false);
337 fwtool.addLimitOption(s);
338 fwtool.addLimitBurstOption(s);
339
340 if (!have_fw4) {
341 o = s.taboption('advanced', form.Value, 'extra', _('Extra arguments'),
342 _('Passes additional arguments to iptables. Use with care!'));
343 o.modalonly = true;
344 o.rmempty = true;
345 }
346
347 o = s.taboption('timed', form.MultiValue, 'weekdays', _('Week Days'));
348 o.modalonly = true;
349 o.multiple = true;
350 o.display = 5;
351 o.placeholder = _('Any day');
352 o.value('Sun', _('Sunday'));
353 o.value('Mon', _('Monday'));
354 o.value('Tue', _('Tuesday'));
355 o.value('Wed', _('Wednesday'));
356 o.value('Thu', _('Thursday'));
357 o.value('Fri', _('Friday'));
358 o.value('Sat', _('Saturday'));
359 o.write = function(section_id, value) {
360 return this.super('write', [ section_id, L.toArray(value).join(' ') ]);
361 };
362
363 o = s.taboption('timed', form.MultiValue, 'monthdays', _('Month Days'));
364 o.modalonly = true;
365 o.multiple = true;
366 o.display_size = 15;
367 o.placeholder = _('Any day');
368 o.write = function(section_id, value) {
369 return this.super('write', [ section_id, L.toArray(value).join(' ') ]);
370 };
371 for (var i = 1; i <= 31; i++)
372 o.value(i);
373
374 o = s.taboption('timed', form.Value, 'start_time', _('Start Time (hh:mm:ss)'));
375 o.modalonly = true;
376 o.datatype = 'timehhmmss';
377
378 o = s.taboption('timed', form.Value, 'stop_time', _('Stop Time (hh:mm:ss)'));
379 o.modalonly = true;
380 o.datatype = 'timehhmmss';
381
382 o = s.taboption('timed', form.Value, 'start_date', _('Start Date (yyyy-mm-dd)'));
383 o.modalonly = true;
384 o.datatype = 'dateyyyymmdd';
385
386 o = s.taboption('timed', form.Value, 'stop_date', _('Stop Date (yyyy-mm-dd)'));
387 o.modalonly = true;
388 o.datatype = 'dateyyyymmdd';
389
390 o = s.taboption('timed', form.Flag, 'utc_time', _('Time in UTC'));
391 o.modalonly = true;
392 o.default = o.disabled;
393
394 return m.render();
395 }
396 });