💋 Sync 2026-06-01 16:00:38

This commit is contained in:
github-actions[bot] 2026-06-01 16:00:38 +08:00
parent 454496f6ff
commit e781685bdc
64 changed files with 75115 additions and 8 deletions

View File

@ -0,0 +1,17 @@
#
# Copyright (C) 2008-2014 The LuCI Team <luci@lists.subsignal.org>
#
# This is free software, licensed under the Apache License, Version 2.0 .
#
include $(TOPDIR)/rules.mk
LUCI_TITLE:=Firewall and port forwarding application
LUCI_DEPENDS:=+luci-base +uci-firewall
PKG_LICENSE:=Apache-2.0
PKG_MAINTAINER:=Jo-Philipp Wich <jo@mein.io>
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature

View File

@ -0,0 +1,702 @@
'use strict';
'require baseclass';
'require dom';
'require ui';
'require uci';
'require form';
'require network';
'require firewall';
'require validation';
'require tools.prng as random';
const protocols = [
'ip', 0, 'IP',
'hopopt', 0, 'HOPOPT',
'icmp', 1, 'ICMP',
'igmp', 2, 'IGMP',
'ggp', 3 , 'GGP',
'ipencap', 4, 'IP-ENCAP',
'st', 5, 'ST',
'tcp', 6, 'TCP',
'egp', 8, 'EGP',
'igp', 9, 'IGP',
'pup', 12, 'PUP',
'udp', 17, 'UDP',
'hmp', 20, 'HMP',
'xns-idp', 22, 'XNS-IDP',
'rdp', 27, 'RDP',
'iso-tp4', 29, 'ISO-TP4',
'dccp', 33, 'DCCP',
'xtp', 36, 'XTP',
'ddp', 37, 'DDP',
'idpr-cmtp', 38, 'IDPR-CMTP',
'ipv6', 41, 'IPv6',
'ipv6-route', 43, 'IPv6-Route',
'ipv6-frag', 44, 'IPv6-Frag',
'idrp', 45, 'IDRP',
'rsvp', 46, 'RSVP',
'gre', 47, 'GRE',
'esp', 50, 'IPSEC-ESP',
'ah', 51, 'IPSEC-AH',
'skip', 57, 'SKIP',
'icmpv6', 58, 'IPv6-ICMP',
'ipv6-icmp', 58, 'IPv6-ICMP',
'ipv6-nonxt', 59, 'IPv6-NoNxt',
'ipv6-opts', 60, 'IPv6-Opts',
'rspf', 73, 'RSPF',
'rspf', 73, 'CPHB',
'vmtp', 81, 'VMTP',
'eigrp', 88, 'EIGRP',
'ospf', 89, 'OSPFIGP',
'ax.25', 93, 'AX.25',
'ipip', 94, 'IPIP',
'etherip', 97, 'ETHERIP',
'encap', 98, 'ENCAP',
'pim', 103, 'PIM',
'ipcomp', 108, 'IPCOMP',
'vrrp', 112, 'VRRP',
'l2tp', 115, 'L2TP',
'isis', 124, 'ISIS',
'sctp', 132, 'SCTP',
'fc', 133, 'FC',
'mh', 135, 'Mobility-Header',
'ipv6-mh', 135, 'Mobility-Header',
'mobility-header', 135, 'Mobility-Header',
'udplite', 136, 'UDPLite',
'mpls-in-ip', 137, 'MPLS-in-IP',
'manet', 138, 'MANET',
'hip', 139, 'HIP',
'shim6', 140, 'Shim6',
'wesp', 141, 'WESP',
'rohc', 142, 'ROHC',
];
function lookupProto(x) {
if (x == null || x === '')
return null;
const s = String(x).toLowerCase();
for (let i = 0; i < protocols.length; i += 3)
if (s == protocols[i] || s == protocols[i+1])
return [ protocols[i+1], protocols[i+2], protocols[i] ];
return [ -1, x, x ];
}
return baseclass.extend({
fmt(fmtstr, args, values) {
let wrap = false;
const tokens = [];
if (values == null) {
values = [];
wrap = true;
}
const get = function(args, key) {
const names = key.trim().split(/\./);
let obj = args;
let ctx = obj;
for (let n of names) {
if (!L.isObject(obj))
return null;
ctx = obj;
obj = obj[n];
}
if (typeof(obj) == 'function')
return obj.call(ctx);
return obj;
};
const isset = function(val) {
if (L.isObject(val) && !dom.elem(val)) {
for (let k in val)
if (val.hasOwnProperty(k))
return true;
return false;
}
else if (Array.isArray(val)) {
return (val.length > 0);
}
else {
return (val !== null && val !== undefined && val !== '' && val !== false);
}
};
const parse = function(tokens, text) {
if (dom.elem(text)) {
tokens.push('<span data-fmt-placeholder="%d"></span>'.format(values.length));
values.push(text);
}
else {
tokens.push(String(text).replace(/\\(.)/g, '$1'));
}
};
for (let i = 0, last = 0; i <= fmtstr.length; i++) {
if (fmtstr.charAt(i) == '%' && fmtstr.charAt(i + 1) == '{') {
if (i > last)
parse(tokens, fmtstr.substring(last, i));
let j = i + 1, nest = 0;
const subexpr = [];
for (let off = j + 1, esc = false; j <= fmtstr.length; j++) {
const ch = fmtstr.charAt(j);
if (esc) {
esc = false;
}
else if (ch == '\\') {
esc = true;
}
else if (ch == '{') {
nest++;
}
else if (ch == '}') {
if (--nest == 0) {
subexpr.push(fmtstr.substring(off, j));
break;
}
}
else if (ch == '?' || ch == ':' || ch == '#') {
if (nest == 1) {
subexpr.push(fmtstr.substring(off, j));
subexpr.push(ch);
off = j + 1;
}
}
}
const varname = subexpr[0].trim();
const op1 = (subexpr[1] != null) ? subexpr[1] : '?';
const if_set = (subexpr[2] != null && subexpr[2] != '') ? subexpr[2] : '%{' + varname + '}';
const op2 = (subexpr[3] != null) ? subexpr[3] : ':';
const if_unset = (subexpr[4] != null) ? subexpr[4] : '';
/* Invalid expression */
if (nest != 0 || subexpr.length > 5 || varname == '') {
return fmtstr;
}
/* enumeration */
else if (op1 == '#' && subexpr.length == 3) {
const items = L.toArray(get(args, varname));
for (let k = 0; k < items.length; k++) {
tokens.push.apply(tokens, this.fmt(if_set, Object.assign({}, args, {
first: k == 0,
next: k > 0,
last: (k + 1) == items.length,
item: items[k]
}), values));
}
}
/* ternary expression */
else if (op1 == '?' && op2 == ':' && (subexpr.length == 1 || subexpr.length == 3 || subexpr.length == 5)) {
const val = get(args, varname);
if (subexpr.length == 1)
parse(tokens, isset(val) ? val : '');
else if (isset(val))
tokens.push.apply(tokens, this.fmt(if_set, args, values));
else
tokens.push.apply(tokens, this.fmt(if_unset, args, values));
}
/* unrecognized command */
else {
return fmtstr;
}
last = j + 1;
i = j;
}
else if (i >= fmtstr.length) {
if (i > last)
parse(tokens, fmtstr.substring(last, i));
}
}
if (wrap) {
const node = E('span', {}, tokens.join(''));
const repl = node.querySelectorAll('span[data-fmt-placeholder]');
for (let r of repl)
r.parentNode.replaceChild(values[r.getAttribute('data-fmt-placeholder')], r);
return node;
}
else {
return tokens;
}
},
map_invert(v, fn) {
return L.toArray(v).map(function(v) {
v = String(v);
if (fn != null && typeof(v[fn]) == 'function')
v = v[fn].call(v);
return {
ival: v,
inv: v.charAt(0) == '!',
val: v.replace(/^!\s*/, '')
};
});
},
lookupProto: lookupProto,
addDSCPOption(s, is_target) {
const o = s.taboption(is_target ? 'general' : 'advanced', form.Value, is_target ? 'set_dscp' : 'dscp',
is_target ? _('DSCP mark') : _('Match DSCP'),
is_target ? _('Apply the given DSCP class or value to established connections.') : _('Matches traffic carrying the specified DSCP marking.'));
o.modalonly = true;
o.rmempty = !is_target;
o.placeholder = _('any');
if (is_target)
o.depends('target', 'DSCP');
o.value('CS0');
o.value('CS1');
o.value('CS2');
o.value('CS3');
o.value('CS4');
o.value('CS5');
o.value('CS6');
o.value('CS7');
o.value('BE');
o.value('AF11');
o.value('AF12');
o.value('AF13');
o.value('AF21');
o.value('AF22');
o.value('AF23');
o.value('AF31');
o.value('AF32');
o.value('AF33');
o.value('AF41');
o.value('AF42');
o.value('AF43');
o.value('EF');
o.validate = function(section_id, value) {
if (value == '')
return is_target ? _('DSCP mark required') : true;
if (!is_target)
value = String(value).replace(/^!\s*/, '');
const m = value.match(/^(?:CS[0-7]|BE|AF[1234][123]|EF|(0x[0-9a-f]{1,2}|[0-9]{1,2}))$/);
if (!m || (m[1] != null && +m[1] > 0x3f))
return _('Invalid DSCP mark');
return true;
};
return o;
},
addMarkOption(s, is_target) {
const o = s.taboption(is_target ? 'general' : 'advanced', form.Value,
(is_target > 1) ? 'set_xmark' : (is_target ? 'set_mark' : 'mark'),
(is_target > 1) ? _('XOR mark') : (is_target ? _('Set mark') : _('Match mark')),
(is_target > 1) ? _('Apply a bitwise XOR of the given value and the existing mark value on established connections. Format is value[/mask]. If a mask is specified then those bits set in the mask are zeroed out.') :
(is_target ? _('Set the given mark value on established connections. Format is value[/mask]. If a mask is specified then only those bits set in the mask are modified.') :
_('Matches a specific firewall mark or a range of different marks.')));
o.modalonly = true;
o.rmempty = true;
if (is_target > 1)
o.depends('target', 'MARK_XOR');
else if (is_target)
o.depends('target', 'MARK_SET');
o.validate = function(section_id, value) {
if (value == '')
return is_target ? _('Valid firewall mark required') : true;
if (!is_target)
value = String(value).replace(/^!\s*/, '');
const m = value.match(/^(0x[0-9a-f]{1,8}|[0-9]{1,10})(?:\/(0x[0-9a-f]{1,8}|[0-9]{1,10}))?$/i);
if (!m || +m[1] > 0xffffffff || (m[2] != null && +m[2] > 0xffffffff))
return _('Expecting: %s').format(_('valid firewall mark'));
return true;
};
return o;
},
addLimitOption(s) {
const o = s.taboption('advanced', form.Value, 'limit',
_('Limit matching'),
_('Limits traffic matching to the specified rate.'));
o.modalonly = true;
o.rmempty = true;
o.placeholder = _('unlimited');
o.value('10/second');
o.value('60/minute');
o.value('3/hour');
o.value('500/day');
o.validate = function(section_id, value) {
if (value == '')
return true;
const m = String(value).toLowerCase().match(/^(?:0x[0-9a-f]{1,8}|[0-9]{1,10})\/([a-z]+)$/),
u = ['second', 'minute', 'hour', 'day'],
i = 0;
if (m)
for (let i = 0; i < u.length; i++)
if (u[i].indexOf(m[1]) == 0)
break;
if (!m || i >= u.length)
return _('Invalid limit value');
return true;
};
return o;
},
addLimitBurstOption(s) {
const o = s.taboption('advanced', form.Value, 'limit_burst',
_('Limit burst'),
_('Maximum initial number of packets to match: this number gets recharged by one every time the limit specified above is not reached, up to this number.'));
o.modalonly = true;
o.rmempty = true;
o.placeholder = '5';
o.datatype = 'uinteger';
o.depends({ limit: null, '!reverse': true });
return o;
},
transformHostHints(family, hosts) {
const choice_values = [];
const choice_labels = {};
const ip6addrs = {};
const ipaddrs = {};
for (let mac in hosts) {
L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4).forEach(function(ip) {
ipaddrs[ip] = mac;
});
L.toArray(hosts[mac].ip6addrs || hosts[mac].ipv6).forEach(function(ip) {
ip6addrs[ip] = mac;
});
}
if (!family || family == 'ipv4') {
L.sortedKeys(ipaddrs, null, 'addr').forEach(function(ip) {
const val = ip;
const txt = hosts[ipaddrs[ip]].name || ipaddrs[ip];
choice_values.push(val);
choice_labels[val] = E([], [ val, ' (', E('strong', {}, [txt]), ')' ]);
});
}
if (!family || family == 'ipv6') {
L.sortedKeys(ip6addrs, null, 'addr').forEach(function(ip) {
const val = ip;
const txt = hosts[ip6addrs[ip]].name || ip6addrs[ip];
choice_values.push(val);
choice_labels[val] = E([], [ val, ' (', E('strong', {}, [txt]), ')' ]);
});
}
return [choice_values, choice_labels];
},
updateHostHints(map, section_id, option, family, hosts) {
const opt = map.lookupOption(option, section_id)[0].getUIElement(section_id);
const choices = this.transformHostHints(family, hosts);
opt.clearChoices();
opt.addChoices(choices[0], choices[1]);
},
CBIDynamicMultiValueList: form.DynamicList.extend({
renderWidget(/* ... */) {
const dl = form.DynamicList.prototype.renderWidget.apply(this, arguments);
const inst = dom.findClassInstance(dl);
inst.addItem = function(dl, value, text, flash) {
const values = L.toArray(value);
for (let val of values)
ui.DynamicList.prototype.addItem.call(this, dl, val, null, true);
};
return dl;
}
}),
addIPOption(s, tab, name, label, description, family, hosts, multiple) {
const o = s.taboption(tab, multiple ? this.CBIDynamicMultiValueList : form.Value, name, label, description);
const fw4 = L.hasSystemFeature('firewall4');
o.modalonly = true;
o.datatype = (fw4 && validation.types.iprange) ? 'list(neg(or(ipmask("true"),iprange)))' : 'list(neg(ipmask("true")))';
o.placeholder = multiple ? _('-- add IP --') : _('any');
if (family != null) {
const choices = this.transformHostHints(family, hosts);
for (let ch of choices[0])
o.value(ch, choices[1][ch]);
}
/* force combobox rendering */
o.transformChoices = function() {
return this.super('transformChoices', []) || {};
};
return o;
},
addLocalIPOption(s, tab, name, label, description, devices) {
const o = s.taboption(tab, form.Value, name, label, description);
const fw4 = L.hasSystemFeature('firewall4');
o.modalonly = true;
o.datatype = !fw4?'ip4addr("nomask")':'ipaddr("nomask")';
o.placeholder = _('any');
L.sortedKeys(devices, 'name').forEach(function(dev) {
const ip4addrs = devices[dev].ipaddrs;
const ip6addrs = devices[dev].ip6addrs;
if (!L.isObject(devices[dev].flags) || devices[dev].flags.loopback)
return;
for (let i = 0; Array.isArray(ip4addrs) && i < ip4addrs.length; i++) {
if (!L.isObject(ip4addrs[i]) || !ip4addrs[i].address)
continue;
o.value(ip4addrs[i].address, E([], [
ip4addrs[i].address, ' (', E('strong', {}, [dev]), ')'
]));
}
for (let i = 0; fw4 && Array.isArray(ip6addrs) && i < ip6addrs.length; i++) {
if (!L.isObject(ip6addrs[i]) || !ip6addrs[i].address)
continue;
o.value(ip6addrs[i].address, E([], [
ip6addrs[i].address, ' (', E('strong', {}, [dev]), ')'
]));
}
});
return o;
},
addMACOption(s, tab, name, label, description, hosts) {
const o = s.taboption(tab, this.CBIDynamicMultiValueList, name, label, description);
o.modalonly = true;
o.datatype = 'list(macaddr)';
o.placeholder = _('-- add MAC --');
L.sortedKeys(hosts).forEach(function(mac) {
o.value(mac, E([], [ mac, ' (', E('strong', {}, [
hosts[mac].name ||
L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0] ||
L.toArray(hosts[mac].ip6addrs || hosts[mac].ipv6)[0] ||
'?'
]), ')' ]));
});
return o;
},
CBIProtocolSelect: form.MultiValue.extend({
__name__: 'CBI.ProtocolSelect',
addChoice(value, label) {
if (!Array.isArray(this.keylist) || this.keylist.indexOf(value) == -1)
this.value(value, label);
},
load(section_id) {
let cfgvalue = L.toArray(this.super('load', [section_id]) || this.default).sort();
let m, p;
['all', 'tcp', 'udp', 'icmp'].concat(cfgvalue).forEach(L.bind(function(value) {
switch (value) {
case 'all':
case 'any':
case '*':
this.addChoice('all', _('Any'));
break;
case 'tcpudp':
this.addChoice('tcp', 'TCP');
this.addChoice('udp', 'UDP');
break;
default:
m = value.match(/^(0x[0-9a-f]{1,2}|[0-9]{1,3})$/);
p = lookupProto(m ? +m[1] : value);
this.addChoice(p[2], p[1]);
break;
}
}, this));
if (cfgvalue == '*' || cfgvalue == 'any' || cfgvalue == 'all')
cfgvalue = 'all';
return cfgvalue;
},
renderWidget(section_id, option_index, cfgvalue) {
const value = (cfgvalue != null) ? cfgvalue : this.default;
const choices = this.transformChoices();
const widget = new ui.Dropdown(L.toArray(value), choices, {
id: this.cbid(section_id),
sort: this.keylist,
multiple: true,
optional: false,
display_items: 10,
dropdown_items: -1,
create: true,
disabled: (this.readonly != null) ? this.readonly : this.map.readonly,
validate(value) {
const vals = L.toArray(value);
for (let v of vals) {
if (v == 'all')
continue;
const m = v.match(/^(0x[0-9a-f]{1,2}|[0-9]{1,3})$/);
if (m ? (+m[1] > 255) : (lookupProto(v)[0] == -1))
return _('Unrecognized protocol');
}
return true;
}
});
widget.createChoiceElement = function(sb, value) {
const p = lookupProto(value);
return ui.Dropdown.prototype.createChoiceElement.call(this, sb, p[2], p[1]);
};
widget.createItems = function(sb, value) {
const values = L.toArray(value).map(function(value) {
const m = value.match(/^(0x[0-9a-f]{1,2}|[0-9]{1,3})$/);
const p = lookupProto(m ? +m[1] : value);
return (p[0] > -1) ? p[2] : p[1];
});
values.sort();
return ui.Dropdown.prototype.createItems.call(this, sb, values.join(' '));
};
widget.toggleItem = function(sb, li) {
const value = li.getAttribute('data-value');
const toggleFn = ui.Dropdown.prototype.toggleItem;
toggleFn.call(this, sb, li);
if (value == 'all') {
const items = li.parentNode.querySelectorAll('li[data-value]');
for (let i of items)
if (i !== li)
toggleFn.call(this, sb, i, false);
}
else {
toggleFn.call(this, sb, li.parentNode.querySelector('li[data-value="all"]'), false);
}
};
return widget.render();
}
}),
checkLegacySNAT() {
const redirects = uci.sections('firewall', 'redirect');
for (let red of redirects)
if ((red['target'] || '').toLowerCase() == 'snat')
return true;
return false;
},
handleMigration(ev) {
const redirects = uci.sections('firewall', 'redirect');
const mapping = {
dest: 'src',
reflection: null,
reflection_src: null,
src_dip: 'snat_ip',
src_dport: 'snat_port',
src: null
};
for (let red of redirects) {
if ((red['target'] || '').toLowerCase() != 'snat')
continue;
const sid = uci.add('firewall', 'nat');
for (let opt in red) {
if (opt.charAt(0) == '.')
continue;
if (mapping[opt] === null)
continue;
uci.set('firewall', sid, mapping[opt] || opt, red[opt]);
}
uci.remove('firewall', red['.name']);
}
return uci.save()
.then(L.bind(ui.changes.init, ui.changes))
.then(L.bind(ui.changes.apply, ui.changes));
},
renderMigration() {
ui.showModal(_('Firewall configuration migration'), [
E('p', _('The existing firewall configuration needs to be changed for LuCI to function properly.')),
E('p', _('Upon pressing "Continue", "redirect" sections with target "SNAT" will be converted to "nat" sections and the firewall will be restarted to apply the updated configuration.')),
E('div', { 'class': 'right' },
E('button', {
'class': 'btn cbi-button-action important',
'click': ui.createHandlerFn(this, 'handleMigration')
}, _('Continue')))
]);
},
});

View File

@ -0,0 +1,33 @@
'use strict';
'require view';
'require fs';
'require ui';
return view.extend({
load() {
return L.resolveDefault(fs.read('/etc/firewall.user'), '');
},
handleSave(ev) {
const value = (document.querySelector('textarea').value || '').trim().replace(/\r\n/g, '\n') + '\n';
return fs.write('/etc/firewall.user', value).then(function(rc) {
document.querySelector('textarea').value = value;
ui.addNotification(null, E('p', _('Contents have been saved.')), 'info');
fs.exec('/etc/init.d/firewall', ['restart']);
}).catch(function(e) {
ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e.message)));
});
},
render(fwuser) {
return E([
E('h2', _('Firewall - Custom Rules')),
E('p', {}, _('Custom rules allow you to execute arbitrary iptables commands which are not otherwise covered by the firewall framework. The commands are executed after each firewall restart, right after the default ruleset has been loaded.')),
E('p', {}, E('textarea', { 'style': 'width:100%', 'rows': 25 }, [ fwuser != null ? fwuser : '' ]))
]);
},
handleSaveApply: null,
handleReset: null
});

View File

@ -0,0 +1,363 @@
'use strict';
'require view';
'require ui';
'require rpc';
'require uci';
'require form';
'require firewall as fwmodel';
'require tools.firewall as fwtool';
'require tools.widgets as widgets';
function rule_proto_txt(s, ctHelpers) {
const family = (uci.get('firewall', s, 'family') || '').toLowerCase().replace(/^(?:all|\*)$/, 'any');
const dip = uci.get('firewall', s, 'dest_ip') || '';
const proto = L.toArray(uci.get('firewall', s, 'proto')).filter(function(p) {
return (p != '*' && p != 'any' && p != 'all');
}).map(function(p) {
const pr = fwtool.lookupProto(p);
return {
num: pr[0],
name: pr[1],
types: (pr[0] == 1 || pr[0] == 58) ? L.toArray(uci.get('firewall', s, 'icmp_type')) : null
};
});
let m = String(uci.get('firewall', s, 'helper') || '').match(/^(!\s*)?(\S+)$/);
const h = m ? {
val: m[0].toUpperCase(),
inv: m[1],
name: (ctHelpers.filter(function(ctH) { return ctH.name.toLowerCase() == m[2].toLowerCase() })[0] || {}).description
} : null;
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);
const f = m ? {
val: m[0].toUpperCase().replace(/X/g, 'x'),
inv: m[1],
num: '0x%02X'.format(+m[2]),
mask: m[3] ? '0x%02X'.format(+m[3]) : null
} : null;
return fwtool.fmt(_('Incoming %{ipv6?%{ipv4?<var>IPv4</var> and <var>IPv6</var>:<var>IPv6</var>}:<var>IPv4</var>}%{proto?, protocol %{proto#%{next?, }%{item.types?<var class="cbi-tooltip-container">%{item.name}<span class="cbi-tooltip">ICMP with types %{item.types#%{next?, }<var>%{item}</var>}</span></var>:<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>}%{helper?, helper %{helper.inv?<var data-tooltip="Match any helper except &quot;%{helper.name}&quot;">%{helper.val}</var>:<var data-tooltip="%{helper.name}">%{helper.val}</var>}}'), {
ipv4: ((!family && dip.indexOf(':') == -1) || family == 'any' || (!family && !dip) || family == 'ipv4'),
ipv6: ((!family && dip.indexOf(':') != -1) || family == 'any' || family == 'ipv6'),
proto: proto,
helper: h,
mark: f
});
}
function rule_src_txt(s, hosts) {
const z = uci.get('firewall', s, 'src');
return fwtool.fmt(_('From %{src}%{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>}}%{src_mac?, MAC %{src_mac#%{next?, }<var%{item.inv? data-tooltip="Match MACs except %{item.val}%{item.hint.name? a.k.a. %{item.hint.name}}.":%{item.hint.name? data-tooltip="%{item.hint.name}"}}>%{item.ival}</var>}}'), {
src: E('span', { 'class': 'zonebadge', 'style': fwmodel.getZoneColorStyle(z) }, [(z == '*') ? E('em', _('any zone')) : (z ? E('strong', z) : E('em', _('this device')))]),
src_ip: fwtool.map_invert(uci.get('firewall', s, 'src_ip'), 'toLowerCase'),
src_mac: fwtool.map_invert(uci.get('firewall', s, 'src_mac'), 'toUpperCase').map(function(v) { return Object.assign(v, { hint: hosts[v.val] }) }),
src_port: fwtool.map_invert(uci.get('firewall', s, 'src_port'))
});
}
function rule_dest_txt(s) {
return fwtool.fmt(_('To %{dest}%{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>}}'), {
dest: E('span', { 'class': 'zonebadge', 'style': fwmodel.getZoneColorStyle(null) }, [E('em', _('this device'))]),
dest_ip: fwtool.map_invert(uci.get('firewall', s, 'src_dip'), 'toLowerCase'),
dest_port: fwtool.map_invert(uci.get('firewall', s, 'src_dport'))
});
}
function rule_limit_txt(s) {
const m = String(uci.get('firewall', s, 'limit')).match(/^(\d+)\/([smhd])\w*$/i);
const l = m ? {
num: +m[1],
unit: ({ s: _('second'), m: _('minute'), h: _('hour'), d: _('day') })[m[2]],
burst: uci.get('firewall', s, 'limit_burst')
} : null;
if (!l)
return '';
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 });
}
function rule_target_txt(s) {
const z = uci.get('firewall', s, 'dest');
return fwtool.fmt(_('<var data-tooltip="DNAT">Forward</var> to %{dest}%{dest_ip? IP <var>%{dest_ip}</var>}%{dest_port? port <var>%{dest_port}</var>}'), {
dest: E('span', { 'class': 'zonebadge', 'style': 'background-color:' + fwmodel.getColorForName((z && z != '*') ? z : null) }, [(z == '*') ? E('em', _('any zone')) : (z ? E('strong', z) : E('em', _('this device')))]),
dest_ip: (uci.get('firewall', s, 'dest_ip') || '').toLowerCase(),
dest_port: uci.get('firewall', s, 'dest_port')
});
}
function validate_opt_family(m, section_id, opt) {
const dopt = m.section.getOption('dest_ip');
const fmopt = m.section.getOption('family');
if (!dopt.isValid(section_id) && opt != 'dest_ip')
return true;
if (!fmopt.isValid(section_id) && opt != 'family')
return true;
const dip = dopt.formvalue(section_id) || '';
const fm = fmopt.formvalue(section_id) || '';
if (fm == '' || (fm == 'any' && dip == '') || (fm == 'ipv6' && (dip.indexOf(':') != -1 || dip == '')) || (fm == 'ipv4' && dip.indexOf(':') == -1))
return true;
return _('Address family, Internal IP address must match');
}
return view.extend({
callHostHints: rpc.declare({
object: 'luci-rpc',
method: 'getHostHints',
expect: { '': {} }
}),
callConntrackHelpers: rpc.declare({
object: 'luci',
method: 'getConntrackHelpers',
expect: { result: [] }
}),
callNetworkDevices: rpc.declare({
object: 'luci-rpc',
method: 'getNetworkDevices',
expect: { '': {} }
}),
load() {
return Promise.all([
this.callHostHints(),
this.callConntrackHelpers(),
this.callNetworkDevices(),
uci.load('firewall')
]);
},
render(data) {
if (fwtool.checkLegacySNAT())
return fwtool.renderMigration();
else
return this.renderForwards(data);
},
renderForwards([hosts, ctHelpers, devs]) {
let m, s, o;
const fw4 = L.hasSystemFeature('firewall4');
m = new form.Map('firewall', _('Firewall - Port Forwards'),
_('Port forwarding allows remote computers on the Internet to connect to a specific computer or service within the private LAN.'));
s = m.section(form.GridSection, 'redirect', _('Port Forwards'));
s.addremove = true;
s.anonymous = true;
s.sortable = true;
s.cloneable = true;
s.filterrow = true;
s.tab('general', _('General Settings'));
s.tab('advanced', _('Advanced Settings'));
s.filter = function(section_id) {
return (uci.get('firewall', section_id, 'target') != 'SNAT');
};
s.sectiontitle = function(section_id) {
return uci.get('firewall', section_id, 'name') || _('Unnamed forward');
};
s.handleAdd = function(ev) {
const config_name = this.uciconfig || this.map.config;
const section_id = uci.add(config_name, this.sectiontype);
uci.set(config_name, section_id, 'dest', 'lan');
uci.set(config_name, section_id, 'target', 'DNAT');
m.addedSection = section_id;
this.renderMoreOptionsModal(section_id);
};
o = s.taboption('general', form.Value, 'name', _('Name'));
o.placeholder = _('Unnamed forward');
o.modalonly = true;
if (fw4) {
o = s.taboption('general', form.ListValue, 'family', _('Restrict to address family'));
o.modalonly = true;
o.rmempty = true;
o.value('any', _('IPv4 and IPv6'));
o.value('ipv4', _('IPv4 only'));
o.value('ipv6', _('IPv6 only'));
o.value('', _('automatic')); // infer from zone or used IP addresses
o.cfgvalue = function(section_id) {
const val = this.map.data.get(this.map.config, section_id, 'family');
if (!val)
return '';
else if (val == 'any' || val == 'all' || val == '*')
return 'any';
else if (val == 'inet' || String(val).indexOf('4') != -1)
return 'ipv4';
else if (String(val).indexOf('6') != -1)
return 'ipv6';
};
o.validate = function(section_id, value) {
fwtool.updateHostHints(this.map, section_id, 'dest_ip', value, hosts);
return !fw4?true:validate_opt_family(this, section_id, 'family');
};
}
o = s.option(form.DummyValue, '_match', _('Match'));
o.modalonly = false;
o.textvalue = function(s) {
return E('small', [
rule_proto_txt(s, ctHelpers), E('br'),
rule_src_txt(s, hosts), E('br'),
rule_dest_txt(s), E('br'),
rule_limit_txt(s)
]);
};
o = s.option(form.ListValue, '_dest', _('Action'));
o.modalonly = false;
o.textvalue = function(s) {
return E('small', [
rule_target_txt(s)
]);
};
o = s.option(form.Flag, 'enabled', _('Enable'));
o.modalonly = false;
o.default = o.enabled;
o.editable = true;
o = s.taboption('general', fwtool.CBIProtocolSelect, 'proto', _('Protocol'));
o.modalonly = true;
o.default = 'tcp udp';
o = s.taboption('general', widgets.ZoneSelect, 'src', _('Source zone'));
o.modalonly = true;
o.rmempty = false;
o.nocreate = true;
o.default = 'wan';
o = s.taboption('advanced', form.Value, 'ipset', _('Use ipset'));
uci.sections('firewall', 'ipset', function(s) {
if (typeof(s.name) == 'string')
o.value(s.name, s.comment ? '%s (%s)'.format(s.name, s.comment) : s.name);
});
o.modalonly = true;
o.rmempty = true;
o = fwtool.addMACOption(s, 'advanced', 'src_mac', _('Source MAC address'),
_('Only match incoming traffic from these MACs.'), hosts);
o.rmempty = true;
o.datatype = 'list(neg(macaddr))';
o = fwtool.addIPOption(s, 'advanced', 'src_ip', _('Source IP address'),
_('Only match incoming traffic from this IP or range.'), !fw4?'ipv4':'', hosts);
o.rmempty = true;
o.datatype = !fw4?'neg(ipmask4("true"))':'neg(ipmask("true"))';
o = s.taboption('advanced', form.Value, 'src_port', _('Source port'),
_('Only match incoming traffic originating from the given source port or port range on the client host'));
o.modalonly = true;
o.rmempty = true;
o.datatype = 'neg(portrange)';
o.placeholder = _('any');
o.depends({ proto: 'tcp', '!contains': true });
o.depends({ proto: 'udp', '!contains': true });
o = fwtool.addLocalIPOption(s, 'advanced', 'src_dip', _('External IP address'),
_('Only match incoming traffic directed at the given IP address.'), devs);
o.datatype = !fw4?'neg(ipmask4("true"))':'neg(ipmask("true"))';
o.rmempty = true;
o = s.taboption('general', form.Value, 'src_dport', _('External port'),
_('Match incoming traffic directed at the given destination port or port range on this host'));
o.modalonly = true;
o.rmempty = false;
o.datatype = 'neg(portrange)';
o.depends({ proto: 'tcp', '!contains': true });
o.depends({ proto: 'udp', '!contains': true });
o = s.taboption('general', widgets.ZoneSelect, 'dest', _('Internal zone'));
o.modalonly = true;
o.rmempty = true;
o.nocreate = true;
o = fwtool.addIPOption(s, 'general', 'dest_ip', _('Internal IP address'),
_('Redirect matched incoming traffic to the specified internal host'), !fw4?'ipv4':'', hosts);
o.rmempty = true;
o.datatype = !fw4?'ipmask4':'ipmask';
o = s.taboption('general', form.Value, 'dest_port', _('Internal port'),
_('Redirect matched incoming traffic to the given port on the internal host'));
o.modalonly = true;
o.rmempty = true;
o.placeholder = _('any');
o.datatype = 'portrange';
o.depends({ proto: 'tcp', '!contains': true });
o.depends({ proto: 'udp', '!contains': true });
o = s.taboption('advanced', form.Flag, 'reflection', _('Enable NAT Loopback'));
o.modalonly = true;
o.rmempty = true;
o.default = o.enabled;
o = s.taboption('advanced', form.ListValue, 'reflection_src', _('Loopback source IP'), _('Specifies whether to use the external or the internal IP address for reflected traffic.'));
o.modalonly = true;
o.depends('reflection', '1');
o.value('internal', _('Use internal IP address'));
o.value('external', _('Use external IP address'));
o.write = function(section_id, value) {
uci.set('firewall', section_id, 'reflection_src', (value != 'internal') ? value : null);
};
o = s.taboption('advanced', widgets.ZoneSelect, 'reflection_zone', _('Reflection zones'), _('Zones from which reflection rules shall be created. If unset, only the destination zone is used.'));
o.nocreate = true;
o.multiple = true;
o.modalonly = true;
o.depends('reflection', '1');
o = s.taboption('advanced', form.Value, 'helper', _('Match helper'), _('Match traffic using the specified connection tracking helper.'));
o.modalonly = true;
o.placeholder = _('any');
for (let cth of ctHelpers)
o.value(cth.name, '%s (%s)'.format(cth.description, cth.name.toUpperCase()));
o.validate = function(section_id, value) {
if (value == '' || value == null)
return true;
value = value.replace(/^!\s*/, '');
for (let cth of ctHelpers)
if (value == cth.name)
return true;
return _('Unknown or not installed conntrack helper "%s"').format(value);
};
fwtool.addMarkOption(s, false);
fwtool.addLimitOption(s);
fwtool.addLimitBurstOption(s);
o = s.taboption('advanced', form.Flag, 'log', _('Enable logging'), _('Log matched packets to syslog.'));
o.modalonly = true;
o = s.taboption('advanced', form.Value, 'log_limit', _('Limit log messages'));
o.depends('log', '1');
o.placeholder = '10/minute';
o.modalonly = true;
if (!L.hasSystemFeature('firewall4')) {
o = s.taboption('advanced', form.Value, 'extra', _('Extra arguments'),
_('Passes additional arguments to iptables. Use with care!'));
o.modalonly = true;
o.rmempty = true;
}
return m.render();
}
});

View File

@ -0,0 +1,220 @@
'use strict';
'require view';
'require uci';
'require form';
'require firewall';
'require tools.firewall as fwtool';
return view.extend({
load() {
return Promise.all([
uci.load('firewall')
]);
},
render() {
let m, s, o;
m = new form.Map('firewall', _('Firewall - IP sets'),
_('firewall4 supports referencing and creating IP sets to simplify matching of large address lists without the need to create one rule per item to match. Port ranges in ipsets are unsupported by firewall4.<br />'));
const have_fw4 = L.hasSystemFeature('firewall4');
if (have_fw4) {
s = m.section(form.NamedSection, 'fwver', 'fwver', '', _('Your device runs firewall4.'));
} else {
s = m.section(form.NamedSection, 'fwver', 'fwver', '', _('Your device does not run firewall4.'));
}
s = m.section(form.GridSection, 'ipset', _('IP Sets'));
s.addremove = true;
s.anonymous = true;
s.sortable = true;
s.cloneable = true;
s.nodescriptions = true;
/* refer to: https://ipset.netfilter.org/ipset.man.html */
if (have_fw4) {
o = s.option(form.Value, 'name', _('Name'));
o.optional = false;
o.rmempty = false;
o.validate = function (section_id, value) {
if (!/^[a-zA-Z_.][a-zA-Z0-9/_.-]*$/.test(value))
return _('Invalid set name');
return true;
};
} else {
o = s.option(form.Value, 'name', _('Name'));
o.depends({ external: '' });
/* Default: (none) if external is unset
value of external if external is set */
}
o.placeholder = _('Unnamed set');
/* comment requires https://git.openwrt.org/?p=project/firewall4.git;a=commitdiff;h=39e8c70957c795bf0c12f04299170ae86c6efdf8 */
o = s.option(form.Value, 'comment', _('Comment'));
o.placeholder = _('Comment');
o.modalonly = true;
o.rmempty = true;
o = s.option(form.ListValue, 'family', _('Family'));
o.value('any', _('IPv4 and IPv6'))
o.value('ipv4', _('IPv4'));
o.value('ipv6', _('IPv6'));
o.default = 'ipv4';
/* Direction src, dst; (Data)Types: ip, port, mac, net or set
Tuples: direction_datatype e.g. src_port, dest_net */
o = s.option(form.DynamicList, 'match', _('Packet Field Match'),
_('Packet fields to match upon.<br />' +
'Syntax: <em>direction_datatype</em>. e.g.: <code>src_port, dest_net</code>.<br />' +
'Directions: <code>src, dst</code>. Datatypes: <code>ip, port, mac, net, set</code>.<br />' +
'Direction prefixes are optional.<br />' +
'*Note: datatype <code>set</code> is unsupported in fw4.'));
o.value('ip', _('ip: IP addr'));
o.value('port', _('port: Port'));
o.value('mac', _('mac: MAC addr'));
o.value('net', _('net: (sub)net'));
if (!have_fw4)
o.value('set', _('set: ipset*'));
o.value('src_ip', _('src_ip: Source IP'));
o.value('src_port', _('src_port: Source Port'));
o.value('src_mac', _('src_mac: Source MAC addr'));
o.value('src_net', _('src_net: Source (sub)net'));
if (!have_fw4)
o.value('src_set', _('src_Set: Source ipset*')); // fw4 unsupported
o.value('dest_ip', _('dest_ip: Destination IP'));
o.value('dest_port', _('dest_port: Destination Port'));
o.value('dest_mac', _('dest_mac: Destination MAC addr'));
o.value('dest_net', _('dest_net: Destination (sub)net'));
if (!have_fw4)
o.value('dest_set', _('dest_set: Destination ipset*')); // fw4 unsupported
o.optional = false;
o.rmempty = false;
// TODO: if/when firewall5 arrives, this 'else' check must change.
if (have_fw4) {
//we have fw4
o = s.option(form.DynamicList, 'entry', _('IPs/Networks/MACs'),
_('macaddr|ip[/cidr]<br />'));
o.datatype = 'or(ipaddr,macaddr)';
o.rmempty = true;
o = s.option(form.Value, 'maxelem', _('Max Entries'),
_('up to 65536 entries.'));
o.datatype = 'port'; //covers 16 bit size
o.modalonly = true;
o.rmempty = true;
} else {
// this else section is intended to handle firewall3
o = s.option(form.Value, 'external', _('Refer To External Set'));
/* Todo: loop to fill o.values with all other ipset names except itself */
o.rmempty = true;
o.optional = true;
/* 'storage' depends on fw3. It must be removed for fw4 */
//aka 'method' in netfilter terminology.
o = s.option(form.ListValue, 'storage', _('Storage Method'));
o.value('bitmap', _('bitmap')); //ipv4 only
o.value('hash', _('hash'));
o.value('list', _('list'));
o.validate = function(section_id, value) {
const family = this.section.formvalue(section_id, 'family');
if (value.match(/bitmap/) && !family.match(/ipv4/))
return _('bitmap is ipv4 only');
return true;
}
/* this iprange differs from netfilters range fromip-toip|ip/cidr:
uci enforces a datatype = cidr in order to be able to enter
an IP for all storage/data types. */
o = s.option(form.Value, 'iprange', _('IP (range)'),
_('ip[/cidr]<br />'+
'For use with Match datatypes: <code>*_ip</code>.'));
o.datatype = 'ipaddr';
o.depends({family: 'ipv4', storage: 'bitmap', match: /_ip|_mac/ });
o.depends({storage: 'hash', match: /_ip/ });
o = s.option(form.DynamicList, 'entry', _('IPs/Networks'),
_('ip[/cidr]<br />'));
o.datatype = 'or(ipaddr,macaddr)';
o.depends({storage: 'hash', match: /_ip|_net|_mac/ });
o = s.option(form.Value, 'portrange', _('Port range'),
_('fromport-toport'));
o.datatype = 'neg(portrange)';
o.depends({family: 'ipv4', storage: 'bitmap', match: /_port/ });
o.depends({family: 'ipv4', storage: 'hash', match: /_port/ });
o.depends({family: 'ipv6', storage: 'hash', match: /_port/ });
o = s.option(form.Value, 'netmask', _('Netmask'));
o.datatype = 'or(ip4prefix,ip6prefix)';
o.depends({family: 'ipv4', storage: 'bitmap', match: /_ip/ });
o.depends({storage: 'hash', match: /_ip/});
o = s.option(form.Value, 'maxelem', _('Max Length'),
_('up to 65536 entries.'));
o.datatype = 'port'; //covers 16 bit size
o.depends('storage', 'hash');
o.depends('storage', 'list');
o.modalonly = true;
o = s.option(form.Value, 'hashsize', _('Initial Hash Size'));
o.depends('storage', 'hash');
o.placeholder = _('1024');
o.modalonly = true;
}
o = s.option(form.FileUpload, 'loadfile', _('Include File'),
_('Path to file of CIDRs, subnets, host IPs, etc.<br />'));
o.root_directory = '/etc/luci-uploads';
o.enable_delete = true;
o.enable_upload = true;
o.datatype = 'file';
o.rmempty = true;
o = s.option(form.Value, 'timeout', _('Timeout'),
_('Unit: seconds. Default <code>0</code> means the entry is added permanently to the set.<br />' +
'Max: 2147483 seconds.'));
o.placeholder = _('0');
o.modalonly = true;
o.rmempty = true;
o = s.option(form.Flag, 'counters', _('Counters'),
_('Enables packet and byte count tracking for the set.'));
o.modalonly = true;
o.rmempty = true;
o.default = false;
o = s.option(form.Flag, 'enabled', _('Enabled'));
o.default = true;
o.editable = true;
o.modalonly = false;
return m.render();
}
});

View File

@ -0,0 +1,526 @@
'use strict';
'require view';
'require ui';
'require rpc';
'require uci';
'require form';
'require firewall as fwmodel';
'require tools.firewall as fwtool';
'require tools.widgets as widgets';
function rule_proto_txt(s, ctHelpers) {
const f = (uci.get('firewall', s, 'family') || '').toLowerCase().replace(/^(?:any|\*)$/, '');
const proto = L.toArray(uci.get('firewall', s, 'proto')).filter(function(p) {
return (p != '*' && p != 'any' && p != 'all');
}).map(function(p) {
const pr = fwtool.lookupProto(p);
return {
num: pr[0],
name: pr[1],
types: (pr[0] == 1 || pr[0] == 58) ? L.toArray(uci.get('firewall', s, 'icmp_type')) : null
};
});
let m = String(uci.get('firewall', s, 'helper') || '').match(/^(!\s*)?(\S+)$/);
const h = m ? {
val: m[0].toUpperCase(),
inv: m[1],
name: (ctHelpers.filter(function(ctH) { return ctH.name.toLowerCase() == m[2].toLowerCase() })[0] || {}).description
} : null;
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);
const w = m ? {
val: m[0].toUpperCase().replace(/X/g, 'x'),
inv: m[1],
num: '0x%02X'.format(+m[2]),
mask: m[3] ? '0x%02X'.format(+m[3]) : null
} : null;
m = String(uci.get('firewall', s, 'dscp')).match(/^(!\s*)?(?:(CS[0-7]|BE|AF[1234][123]|EF)|(0x[0-9a-f]{1,2}|[0-9]{1,2}))$/);
const d = m ? {
val: m[0],
inv: m[1],
name: m[2],
num: m[3] ? '0x%02X'.format(+m[3]) : null
} : null;
return fwtool.fmt(_('%{src?%{dest?Forwarded:Incoming}:Outgoing} %{ipv6?%{ipv4?<var>IPv4</var> and <var>IPv6</var>:<var>IPv6</var>}:<var>IPv4</var>}%{proto?, protocol %{proto#%{next?, }%{item.types?<var class="cbi-tooltip-container">%{item.name}<span class="cbi-tooltip">ICMP with types %{item.types#%{next?, }<var>%{item}</var>}</span></var>:<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>}%{dscp?, DSCP %{dscp.inv?<var data-tooltip="Match DSCP classifications except %{dscp.num?:%{dscp.name}}">%{dscp.val}</var>:<var>%{dscp.val}</var>}}%{helper?, helper %{helper.inv?<var data-tooltip="Match any helper except &quot;%{helper.name}&quot;">%{helper.val}</var>:<var data-tooltip="%{helper.name}">%{helper.val}</var>}}'), {
ipv4: (!f || f == 'ipv4'),
ipv6: (!f || f == 'ipv6'),
src: uci.get('firewall', s, 'src'),
dest: uci.get('firewall', s, 'dest'),
proto: proto,
helper: h,
mark: w,
dscp: d
});
}
function rule_src_txt(s, hosts) {
const z = uci.get('firewall', s, 'src');
const d = (uci.get('firewall', s, 'direction') == 'in') ? uci.get('firewall', s, 'device') : null;
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>}}%{src_mac?, MAC %{src_mac#%{next?, }<var%{item.inv? data-tooltip="Match MACs except %{item.val}%{item.hint.name? a.k.a. %{item.hint.name}}.":%{item.hint.name? data-tooltip="%{item.hint.name}"}}>%{item.ival}</var>}}'), {
src: E('span', { 'class': 'zonebadge', 'style': fwmodel.getZoneColorStyle(z) }, [(z == '*') ? E('em', _('any zone')) : (z ? E('strong', z) : E('em', _('this device')))]),
src_ip: fwtool.map_invert(uci.get('firewall', s, 'src_ip'), 'toLowerCase'),
src_mac: fwtool.map_invert(uci.get('firewall', s, 'src_mac'), 'toUpperCase').map(function(v) { return Object.assign(v, { hint: hosts[v.val] }) }),
src_port: fwtool.map_invert(uci.get('firewall', s, 'src_port')),
src_device: d
});
}
function rule_dest_txt(s) {
const z = uci.get('firewall', s, 'dest');
const d = (uci.get('firewall', s, 'direction') == 'out') ? uci.get('firewall', s, 'device') : null;
return fwtool.fmt(_('To %{dest}%{dest_device?, 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>}}'), {
dest: E('span', { 'class': 'zonebadge', 'style': fwmodel.getZoneColorStyle(z) }, [(z == '*') ? E('em', _('any zone')) : (z ? E('strong', z) : E('em', _('this device')))]),
dest_ip: fwtool.map_invert(uci.get('firewall', s, 'dest_ip'), 'toLowerCase'),
dest_port: fwtool.map_invert(uci.get('firewall', s, 'dest_port')),
dest_device: d
});
}
function rule_limit_txt(s) {
const m = String(uci.get('firewall', s, 'limit')).match(/^(\d+)\/([smhd])\w*$/i);
const l = m ? {
num: +m[1],
unit: ({ s: _('second'), m: _('minute'), h: _('hour'), d: _('day') })[m[2]],
burst: uci.get('firewall', s, 'limit_burst')
} : null;
if (!l)
return '';
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 });
}
function rule_target_txt(sid, ctHelpers) {
const t = uci.get('firewall', sid, 'target');
const h = (uci.get('firewall', sid, 'set_helper') || '').toUpperCase();
const s = {
target: t,
src: uci.get('firewall', sid, 'src'),
dest: uci.get('firewall', sid, 'dest'),
set_helper: h,
set_mark: uci.get('firewall', sid, 'set_mark'),
set_xmark: uci.get('firewall', sid, 'set_xmark'),
set_dscp: uci.get('firewall', sid, 'set_dscp'),
helper_name: (ctHelpers.filter(function(ctH) { return ctH.name.toUpperCase() == h })[0] || {}).description
};
switch (t) {
case 'DROP':
return fwtool.fmt(_('<var data-tooltip="DROP">Drop</var> %{src?%{dest?forward:input}:output}'), s);
case 'ACCEPT':
return fwtool.fmt(_('<var data-tooltip="ACCEPT">Accept</var> %{src?%{dest?forward:input}:output}'), s);
case 'REJECT':
return fwtool.fmt(_('<var data-tooltip="REJECT">Reject</var> %{src?%{dest?forward:input}:output}'), s);
case 'NOTRACK':
return fwtool.fmt(_('<var data-tooltip="NOTRACK">Do not track</var> %{src?%{dest?forward:input}:output}'), s);
case 'HELPER':
return fwtool.fmt(_('<var data-tooltip="HELPER">Assign conntrack</var> helper <var%{helper_name? data-tooltip="%{helper_name}"}>%{set_helper}</var>'), s);
case 'MARK':
return fwtool.fmt(_('<var data-tooltip="MARK">%{set_mark?Assign:XOR}</var> firewall mark <var>%{set_mark?:%{set_xmark}}</var>'), s);
case 'DSCP':
return fwtool.fmt(_('<var data-tooltip="DSCP">Assign DSCP</var> classification <var>%{set_dscp}</var>'), s);
default:
return t;
}
}
return view.extend({
callHostHints: rpc.declare({
object: 'luci-rpc',
method: 'getHostHints',
expect: { '': {} }
}),
callConntrackHelpers: rpc.declare({
object: 'luci',
method: 'getConntrackHelpers',
expect: { result: [] }
}),
load() {
return Promise.all([
this.callHostHints(),
this.callConntrackHelpers(),
uci.load('firewall')
]);
},
render(data) {
if (fwtool.checkLegacySNAT())
return fwtool.renderMigration();
else
return this.renderRules(data);
},
renderRules([hosts, ctHelpers]) {
let m, s, o;
m = new form.Map('firewall', _('Firewall - Traffic Rules'),
_('Traffic rules define policies for packets travelling between different zones, for example to reject traffic between certain hosts or to open WAN ports on the router.'));
s = m.section(form.GridSection, 'rule', _('Traffic Rules'));
s.addremove = true;
s.anonymous = true;
s.sortable = true;
s.cloneable = true;
s.filterrow = true;
s.tab('general', _('General Settings'));
s.tab('advanced', _('Advanced Settings'));
s.tab('timed', _('Time Restrictions'));
s.filter = function(section_id) {
return (uci.get('firewall', section_id, 'target') != 'SNAT');
};
s.sectiontitle = function(section_id) {
return uci.get('firewall', section_id, 'name') || _('Unnamed rule');
};
s.handleAdd = function(ev) {
const config_name = this.uciconfig || this.map.config;
const section_id = uci.add(config_name, this.sectiontype);
uci.set(config_name, section_id, 'src', 'wan');
uci.set(config_name, section_id, 'dest', 'lan');
m.addedSection = section_id;
this.renderMoreOptionsModal(section_id);
};
o = s.taboption('general', form.Value, 'name', _('Name'));
o.placeholder = _('Unnamed rule');
o.modalonly = true;
o = s.option(form.DummyValue, '_match', _('Match'));
o.modalonly = false;
o.textvalue = function(s) {
return E('small', [
rule_proto_txt(s, ctHelpers), E('br'),
rule_src_txt(s, hosts), E('br'),
rule_dest_txt(s), E('br'),
rule_limit_txt(s)
]);
};
o = s.option(form.ListValue, '_target', _('Action'));
o.modalonly = false;
o.textvalue = function(s) {
return rule_target_txt(s, ctHelpers);
};
o = s.option(form.Flag, 'enabled', _('Enable'));
o.modalonly = false;
o.default = o.enabled;
o.editable = true;
o.tooltip = function(section_id) {
const weekdays = uci.get('firewall', section_id, 'weekdays');
const monthdays = uci.get('firewall', section_id, 'monthdays');
const start_time = uci.get('firewall', section_id, 'start_time');
const stop_time = uci.get('firewall', section_id, 'stop_time');
const start_date = uci.get('firewall', section_id, 'start_date');
const stop_date = uci.get('firewall', section_id, 'stop_date');
if (weekdays || monthdays || start_time || stop_time || start_date || stop_date )
return _('Time restrictions are enabled for this rule');
return null;
};
o = s.taboption('advanced', form.ListValue, 'direction', _('Match device'));
o.modalonly = true;
o.value('', _('unspecified'));
o.value('in', _('Inbound device'));
o.value('out', _('Outbound device'));
o.cfgvalue = function(section_id) {
const val = uci.get('firewall', section_id, 'direction');
switch (val) {
case 'in':
case 'ingress':
return 'in';
case 'out':
case 'egress':
return 'out';
}
return null;
};
o = s.taboption('advanced', widgets.DeviceSelect, 'device', _('Device name'),
_('Specifies whether to tie this traffic rule to a specific inbound or outbound network device.'));
o.modalonly = true;
o.noaliases = true;
o.rmempty = false;
o.depends('direction', 'in');
o.depends('direction', 'out');
o = s.taboption('advanced', form.ListValue, 'family', _('Restrict to address family'));
o.modalonly = true;
o.rmempty = true;
o.value('', _('IPv4 and IPv6'));
o.value('ipv4', _('IPv4 only'));
o.value('ipv6', _('IPv6 only'));
o.validate = function(section_id, value) {
fwtool.updateHostHints(this.map, section_id, 'src_ip', value, hosts);
fwtool.updateHostHints(this.map, section_id, 'dest_ip', value, hosts);
return true;
};
o = s.taboption('general', fwtool.CBIProtocolSelect, 'proto', _('Protocol'));
o.modalonly = true;
o.default = 'tcp udp';
o = s.taboption('advanced', form.MultiValue, 'icmp_type', _('Match ICMP type'));
o.modalonly = true;
o.multiple = true;
o.custom = true;
o.cast = 'table';
o.placeholder = _('any/all');
o.value('address-mask-reply');
o.value('address-mask-request');
o.value('address-unreachable'); /* icmpv6 1:3 */
o.value('bad-header'); /* icmpv6 4:0 */
o.value('certification-path-solicitation-message'); /* icmpv6 148 */
o.value('certification-path-advertisement-message'); /* icmpv6 149 */
o.value('communication-prohibited');
o.value('destination-unreachable');
o.value('duplicate-address-request'); /* icmpv6 157 */
o.value('duplicate-address-confirmation'); /* icmpv6 158 */
o.value('echo-reply');
o.value('echo-request');
o.value('extended-echo-request'); /* icmpv6 160 */
o.value('extended-echo-reply'); /* icmpv6 161 */
o.value('fmipv6-message'); /* icmpv6 154 */
o.value('fragmentation-needed');
o.value('home-agent-address-discovery-reply-message'); /* icmpv6 145 */
o.value('home-agent-address-discovery-request-message'); /* icmpv6 144 */
o.value('host-precedence-violation');
o.value('host-prohibited');
o.value('host-redirect');
o.value('host-unknown');
o.value('host-unreachable');
o.value('ilnpv6-locator-update-message'); /* icmpv6 156 */
o.value('inverse-neighbour-discovery-advertisement-message'); /* icmpv6 142 */
o.value('inverse-neighbour-discovery-solicitation-message'); /* icmpv6 141 */
o.value('ip-header-bad');
o.value('mobile-prefix-advertisement'); /* icmpv6 147 */
o.value('mobile-prefix-solicitation'); /* icmpv6 146 */
o.value('mpl-control-message'); /* icmpv6 159 */
o.value('multicast-listener-query'); /* icmpv6 130 */
o.value('multicast-listener-report'); /* icmpv6 131 */
o.value('multicast-listener-done'); /* icmpv6 132 */
o.value('multicast-router-advertisement'); /* icmpv6 151 */
o.value('multicast-router-solicitation'); /* icmpv6 152 */
o.value('multicast-router-termination'); /* icmpv6 153 */
o.value('neighbour-advertisement');
o.value('neighbour-solicitation');
o.value('network-prohibited');
o.value('network-redirect');
o.value('network-unknown');
o.value('network-unreachable');
o.value('no-route'); /* icmpv6 1:0 */
o.value('node-info-query'); /* icmpv6 139 */
o.value('node-info-response'); /* icmpv6 140 */
o.value('packet-too-big');
o.value('parameter-problem');
o.value('port-unreachable');
o.value('precedence-cutoff');
o.value('protocol-unreachable');
o.value('redirect');
o.value('required-option-missing');
o.value('router-advertisement');
o.value('router-renumbering'); /* icmpv6 138 */
o.value('router-solicitation');
o.value('rpl-control-message'); /* icmpv6 155 */
o.value('source-quench');
o.value('source-route-failed');
o.value('time-exceeded');
o.value('timestamp-reply');
o.value('timestamp-request');
o.value('TOS-host-redirect');
o.value('TOS-host-unreachable');
o.value('TOS-network-redirect');
o.value('TOS-network-unreachable');
o.value('ttl-zero-during-reassembly');
o.value('ttl-zero-during-transit');
o.value('v2-multicast-listener-report'); /* icmpv6 143 */
o.value('unknown-header-type'); /* icmpv6 4:1 */
o.value('unknown-option'); /* icmpv6 4:2 */
o.depends({ proto: 'icmp', '!contains': true });
o.depends({ proto: 'icmpv6', '!contains': true });
o = s.taboption('general', widgets.ZoneSelect, 'src', _('Source zone'));
o.modalonly = true;
o.nocreate = true;
o.allowany = true;
o.allowlocal = 'src';
o = s.taboption('advanced', form.Value, 'ipset', _('Use ipset'));
uci.sections('firewall', 'ipset', function(s) {
if (typeof(s.name) == 'string')
o.value(s.name, s.comment ? '%s (%s)'.format(s.name, s.comment) : s.name);
});
o.modalonly = true;
o.rmempty = true;
fwtool.addMACOption(s, 'advanced', 'src_mac', _('Source MAC address'), null, hosts);
fwtool.addIPOption(s, 'general', 'src_ip', _('Source address'), null, '', hosts, true);
o = s.taboption('general', form.Value, 'src_port', _('Source port'));
o.modalonly = true;
o.datatype = 'list(neg(portrange))';
o.placeholder = _('any');
o.depends({ proto: 'tcp', '!contains': true });
o.depends({ proto: 'udp', '!contains': true });
o = s.taboption('general', widgets.ZoneSelect, 'dest', _('Destination zone'));
o.modalonly = true;
o.nocreate = true;
o.allowany = true;
o.allowlocal = true;
fwtool.addIPOption(s, 'general', 'dest_ip', _('Destination address'), null, '', hosts, true);
o = s.taboption('general', form.Value, 'dest_port', _('Destination port'));
o.modalonly = true;
o.datatype = 'list(neg(portrange))';
o.placeholder = _('any');
o.depends({ proto: 'tcp', '!contains': true });
o.depends({ proto: 'udp', '!contains': true });
o = s.taboption('general', form.ListValue, 'target', _('Action'));
o.modalonly = true;
o.default = 'ACCEPT';
o.value('DROP', _('drop'));
o.value('ACCEPT', _('accept'));
o.value('REJECT', _('reject'));
o.value('NOTRACK', _("don't track"));
o.value('HELPER', _('assign conntrack helper'));
o.value('MARK_SET', _('apply firewall mark'));
o.value('MARK_XOR', _('XOR firewall mark'));
o.value('DSCP', _('DSCP classification'));
o.cfgvalue = function(section_id) {
const t = uci.get('firewall', section_id, 'target');
const m = uci.get('firewall', section_id, 'set_mark');
if (t == 'MARK')
return m ? 'MARK_SET' : 'MARK_XOR';
return t;
};
o.write = function(section_id, value) {
return this.super('write', [section_id, (value == 'MARK_SET' || value == 'MARK_XOR') ? 'MARK' : value]);
};
fwtool.addMarkOption(s, 1);
fwtool.addMarkOption(s, 2);
fwtool.addDSCPOption(s, true);
o = s.taboption('general', form.ListValue, 'set_helper', _('Tracking helper'), _('Assign the specified connection tracking helper to matched traffic.'));
o.modalonly = true;
o.placeholder = _('any');
o.depends('target', 'HELPER');
for (let cth of ctHelpers)
o.value(cth.name, '%s (%s)'.format(cth.description, cth.name.toUpperCase()));
o = s.taboption('advanced', form.Value, 'helper', _('Match helper'), _('Match traffic using the specified connection tracking helper.'));
o.modalonly = true;
o.placeholder = _('any');
for (let cth of ctHelpers)
o.value(cth.name, '%s (%s)'.format(cth.description, cth.name.toUpperCase()));
o.validate = function(section_id, value) {
if (value == '' || value == null)
return true;
value = value.replace(/^!\s*/, '');
for (let cth of ctHelpers)
if (value == cth.name)
return true;
return _('Unknown or not installed conntrack helper "%s"').format(value);
};
fwtool.addMarkOption(s, false);
fwtool.addDSCPOption(s, false);
fwtool.addLimitOption(s);
fwtool.addLimitBurstOption(s);
o = s.taboption('advanced', form.Flag, 'log', _('Enable logging'), _('Log matched packets to syslog.'));
o.modalonly = true;
o = s.taboption('advanced', form.Value, 'log_limit', _('Limit log messages'));
o.depends('log', '1');
o.placeholder = '10/minute';
o.modalonly = true;
if (!L.hasSystemFeature('firewall4')) {
o = s.taboption('advanced', form.Value, 'extra', _('Extra arguments'),
_('Passes additional arguments to iptables. Use with care!'));
o.modalonly = true;
}
o = s.taboption('timed', form.MultiValue, 'weekdays', _('Week Days'));
o.modalonly = true;
o.multiple = true;
o.display = 5;
o.placeholder = _('Any day');
o.value('Sun', _('Sunday'));
o.value('Mon', _('Monday'));
o.value('Tue', _('Tuesday'));
o.value('Wed', _('Wednesday'));
o.value('Thu', _('Thursday'));
o.value('Fri', _('Friday'));
o.value('Sat', _('Saturday'));
o.write = function(section_id, value) {
return this.super('write', [ section_id, L.toArray(value).join(' ') ]);
};
o = s.taboption('timed', form.MultiValue, 'monthdays', _('Month Days'));
o.modalonly = true;
o.multiple = true;
o.display_size = 15;
o.placeholder = _('Any day');
o.write = function(section_id, value) {
return this.super('write', [ section_id, L.toArray(value).join(' ') ]);
};
for (let i = 1; i <= 31; i++)
o.value(i);
o = s.taboption('timed', form.Value, 'start_time', _('Start Time (hh:mm:ss)'));
o.modalonly = true;
o.datatype = 'timehhmmss';
o = s.taboption('timed', form.Value, 'stop_time', _('Stop Time (hh:mm:ss)'));
o.modalonly = true;
o.datatype = 'timehhmmss';
o = s.taboption('timed', form.Value, 'start_date', _('Start Date (yyyy-mm-dd)'));
o.modalonly = true;
o.datatype = 'dateyyyymmdd';
o = s.taboption('timed', form.Value, 'stop_date', _('Stop Date (yyyy-mm-dd)'));
o.modalonly = true;
o.datatype = 'dateyyyymmdd';
o = s.taboption('timed', form.Flag, 'utc_time', _('Time in UTC'));
o.modalonly = true;
o.default = o.disabled;
return m.render();
}
});

View File

@ -0,0 +1,398 @@
'use strict';
'require view';
'require ui';
'require rpc';
'require uci';
'require form';
'require firewall as fwmodel';
'require tools.firewall as fwtool';
'require tools.widgets as widgets';
function rule_proto_txt(s) {
const family = (uci.get('firewall', s, 'family') || '').toLowerCase().replace(/^(?:all|\*)$/, 'any');
const sip = uci.get('firewall', s, 'src_ip') || '';
const dip = uci.get('firewall', s, 'dest_ip') || '';
const rwip = uci.get('firewall', s, 'snat_ip') || '';
const proto = L.toArray(uci.get('firewall', s, 'proto')).filter(function(p) {
return (p != '*' && p != 'any' && p != 'all');
}).map(function(p) {
const pr = fwtool.lookupProto(p);
return {
num: pr[0],
name: pr[1]
};
});
const 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);
const f = m ? {
val: m[0].toUpperCase().replace(/X/g, 'x'),
inv: m[1],
num: '0x%02X'.format(+m[2]),
mask: m[3] ? '0x%02X'.format(+m[3]) : null
} : null;
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>}'), {
ipv4: (family == 'ipv4' || family == 'any' || (!family && sip.indexOf(':') == -1 && dip.indexOf(':') == -1 && rwip.indexOf(':') == -1)),
ipv6: (family == 'ipv6' || family == 'any' || (!family && (sip.indexOf(':') != -1 || dip.indexOf(':') != -1 || rwip.indexOf(':') != -1))),
proto: proto,
mark: f
});
}
function rule_src_txt(s, hosts) {
const z = uci.get('firewall', s, 'src');
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>}}'), {
src: E('span', { 'class': 'zonebadge', 'style': fwmodel.getZoneColorStyle(null) }, [E('em', _('any zone'))]),
src_ip: fwtool.map_invert(uci.get('firewall', s, 'src_ip'), 'toLowerCase'),
src_port: fwtool.map_invert(uci.get('firewall', s, 'src_port'))
});
}
function rule_dest_txt(s) {
const z = uci.get('firewall', s, 'src');
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>}}'), {
dest: E('span', { 'class': 'zonebadge', 'style': fwmodel.getZoneColorStyle(z) }, [(z == '*') ? E('em', _('any zone')) : (z ? E('strong', z) : E('em', _('this device')))]),
dest_ip: fwtool.map_invert(uci.get('firewall', s, 'dest_ip'), 'toLowerCase'),
dest_port: fwtool.map_invert(uci.get('firewall', s, 'dest_port')),
dest_device: uci.get('firewall', s, 'device')
});
}
function rule_limit_txt(s) {
const m = String(uci.get('firewall', s, 'limit')).match(/^(\d+)\/([smhd])\w*$/i);
const l = m ? {
num: +m[1],
unit: ({ s: _('second'), m: _('minute'), h: _('hour'), d: _('day') })[m[2]],
burst: uci.get('firewall', s, 'limit_burst')
} : null;
if (!l)
return '';
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 });
}
function rule_target_txt(sid) {
const t = uci.get('firewall', sid, 'target');
const s = {
target: t,
snat_ip: uci.get('firewall', sid, 'snat_ip'),
snat_port: uci.get('firewall', sid, 'snat_port')
};
switch (t) {
case 'SNAT':
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);
case 'MASQUERADE':
return fwtool.fmt(_('<var data-tooltip="MASQUERADE">Automatically rewrite</var> source IP'));
case 'ACCEPT':
return fwtool.fmt(_('<var data-tooltip="ACCEPT">Prevent source rewrite</var>'));
default:
return t;
}
}
function validate_opt_family(m, section_id, opt) {
const sopt = m.section.getOption('src_ip');
const dopt = m.section.getOption('dest_ip');
const rwopt = m.section.getOption('snat_ip');
const fmopt = m.section.getOption('family');
const tgopt = m.section.getOption('target');
if (!sopt.isValid(section_id) && opt != 'src_ip')
return true;
if (!dopt.isValid(section_id) && opt != 'dest_ip')
return true;
if (!rwopt.isValid(section_id) && opt != 'snat_ip')
return true;
if (!fmopt.isValid(section_id) && opt != 'family')
return true;
if (!tgopt.isValid(section_id) && opt != 'target')
return true;
const sip = sopt.formvalue(section_id) || '';
const dip = dopt.formvalue(section_id) || '';
const rwip = rwopt.formvalue(section_id) || '';
const fm = fmopt.formvalue(section_id) || '';
const tg = tgopt.formvalue(section_id);
if (fm == 'ipv6' && (sip.indexOf(':') != -1 || sip == '') && (dip.indexOf(':') != -1 || dip == '') && ((rwip.indexOf(':') != -1 && tg == 'SNAT') || rwip == ''))
return true;
if (fm == 'ipv4' && (sip.indexOf(':') == -1) && (dip.indexOf(':') == -1) && ((rwip.indexOf(':') == -1 && tg == 'SNAT') || rwip == ''))
return true;
if (fm == '' || fm == 'any') {
if ((sip.indexOf(':') != -1 || sip == '') && (dip.indexOf(':') != -1 || dip == '') && ((rwip.indexOf(':') != -1 && tg == 'SNAT') || rwip == ''))
return true;
if ((sip.indexOf(':') == -1) && (dip.indexOf(':') == -1) && ((rwip.indexOf(':') == -1 && tg == 'SNAT') || rwip == ''))
return true;
}
return _('Address family, source address, destination address, rewrite IP address must match');
}
return view.extend({
callHostHints: rpc.declare({
object: 'luci-rpc',
method: 'getHostHints',
expect: { '': {} }
}),
callNetworkDevices: rpc.declare({
object: 'luci-rpc',
method: 'getNetworkDevices',
expect: { '': {} }
}),
load() {
return Promise.all([
this.callHostHints(),
this.callNetworkDevices(),
uci.load('firewall')
]);
},
render(data) {
if (fwtool.checkLegacySNAT())
return fwtool.renderMigration();
else
return this.renderNats(data);
},
renderNats([hosts, devs]) {
let m, s, o;
const fw4 = L.hasSystemFeature('firewall4');
m = new form.Map('firewall', _('Firewall - NAT Rules'),
_('NAT rules allow fine grained control over the source IP to use for outbound or forwarded traffic.'));
s = m.section(form.GridSection, 'nat', _('NAT Rules'));
s.addremove = true;
s.anonymous = true;
s.sortable = true;
s.cloneable = true;
s.filterrow = true;
s.tab('general', _('General Settings'));
s.tab('advanced', _('Advanced Settings'));
s.tab('timed', _('Time Restrictions'));
s.sectiontitle = function(section_id) {
return uci.get('firewall', section_id, 'name') || _('Unnamed NAT');
};
o = s.taboption('general', form.Value, 'name', _('Name'));
o.placeholder = _('Unnamed NAT');
o.modalonly = true;
o = s.option(form.DummyValue, '_match', _('Match'));
o.modalonly = false;
o.textvalue = function(s) {
return E('small', [
rule_proto_txt(s), E('br'),
rule_src_txt(s, hosts), E('br'),
rule_dest_txt(s), E('br'),
rule_limit_txt(s)
]);
};
o = s.option(form.ListValue, '_target', _('Action'));
o.modalonly = false;
o.textvalue = function(s) {
return rule_target_txt(s);
};
o = s.option(form.Flag, 'enabled', _('Enable'));
o.modalonly = false;
o.default = o.enabled;
o.editable = true;
if (fw4) {
o = s.taboption('general', form.ListValue, 'family', _('Restrict to address family'));
o.modalonly = true;
o.rmempty = true;
o.value('any', _('IPv4 and IPv6'));
o.value('ipv4', _('IPv4 only'));
o.value('ipv6', _('IPv6 only'));
o.value('', _('automatic')); // infer from zone or used IP addresses
o.cfgvalue = function(section_id) {
const val = this.map.data.get(this.map.config, section_id, 'family');
if (!val)
return '';
else if (val == 'any' || val == 'all' || val == '*')
return 'any';
else if (val == 'inet' || String(val).indexOf('4') != -1)
return 'ipv4';
else if (String(val).indexOf('6') != -1)
return 'ipv6';
};
o.validate = function(section_id, value) {
fwtool.updateHostHints(this.map, section_id, 'src_ip', value, hosts);
fwtool.updateHostHints(this.map, section_id, 'dest_ip', value, hosts);
return !fw4?true:validate_opt_family(this, section_id, 'family');
};
}
o = s.taboption('general', fwtool.CBIProtocolSelect, 'proto', _('Protocol'));
o.modalonly = true;
o.default = 'all';
o = s.taboption('general', widgets.ZoneSelect, 'src', _('Outbound zone'));
o.modalonly = true;
o.rmempty = false;
o.nocreate = true;
o.allowany = true;
o.default = 'lan';
o = fwtool.addIPOption(s, 'general', 'src_ip', _('Source address'),
_('Match forwarded traffic from this IP or range.'), !fw4?'ipv4':'', hosts);
o.rmempty = true;
o.datatype = !fw4?'neg(ipmask4("true"))':'neg(ipmask("true"))';
o.validate = function(section_id, value) {
return !fw4?true:validate_opt_family(this, section_id, 'src_ip');
};
o = s.taboption('general', form.Value, 'src_port', _('Source port'),
_('Match forwarded traffic originating from the given source port or port range.'));
o.modalonly = true;
o.rmempty = true;
o.datatype = 'neg(portrange)';
o.placeholder = _('any');
o.depends({ proto: 'tcp', '!contains': true });
o.depends({ proto: 'udp', '!contains': true });
o = fwtool.addIPOption(s, 'general', 'dest_ip', _('Destination address'),
_('Match forwarded traffic directed at the given IP address.'), !fw4?'ipv4':'', hosts);
o.rmempty = true;
o.datatype = !fw4?'neg(ipmask4("true"))':'neg(ipmask("true"))';
o.validate = function(section_id, value) {
return !fw4?true:validate_opt_family(this, section_id, 'dest_ip');
};
o = s.taboption('general', form.Value, 'dest_port', _('Destination port'),
_('Match forwarded traffic directed at the given destination port or port range.'));
o.modalonly = true;
o.rmempty = true;
o.placeholder = _('any');
o.datatype = 'neg(portrange)';
o.depends({ proto: 'tcp', '!contains': true });
o.depends({ proto: 'udp', '!contains': true });
o = s.taboption('general', form.ListValue, 'target', _('Action'));
o.modalonly = true;
o.default = 'SNAT';
o.value('SNAT', _('SNAT - Rewrite to specific source IP or port'));
o.value('MASQUERADE', _('MASQUERADE - Automatically rewrite to outbound interface IP'));
o.value('ACCEPT', _('ACCEPT - Disable address rewriting'));
o.validate = function(section_id, value) {
return !fw4?true:validate_opt_family(this, section_id, 'target');
};
o = fwtool.addLocalIPOption(s, 'general', 'snat_ip', _('Rewrite IP address'),
_('Rewrite matched traffic to the specified source IP address.'), devs);
o.placeholder = null;
o.depends('target', 'SNAT');
o.validate = function(section_id, value) {
const a = this.formvalue(section_id);
const p = this.section.formvalue(section_id, 'snat_port');
if ((a == null || a == '') && (p == null || p == '') && value == '')
return _('A rewrite IP must be specified!');
return !fw4?true:validate_opt_family(this, section_id, 'snat_ip');
};
o = s.taboption('general', form.Value, 'snat_port', _('Rewrite port'),
_('Rewrite matched traffic to the specified source port or port range.'));
o.modalonly = true;
o.rmempty = true;
o.placeholder = _('do not rewrite');
o.datatype = 'portrange';
o.depends({ proto: 'tcp', '!contains': true });
o.depends({ proto: 'udp', '!contains': true });
if (!fw4) {
o = s.taboption('advanced', form.Value, 'ipset', _('Use ipset'));
uci.sections('firewall', 'ipset', function(s) {
if (typeof(s.name) == 'string')
o.value(s.name, s.comment ? '%s (%s)'.format(s.name, s.comment) : s.name);
});
o.modalonly = true;
o.rmempty = true;
}
o = s.taboption('advanced', widgets.DeviceSelect, 'device', _('Outbound device'),
_('Matches forwarded traffic using the specified outbound network device.'));
o.noaliases = true;
o.modalonly = true;
o.rmempty = true;
fwtool.addMarkOption(s, false);
fwtool.addLimitOption(s);
fwtool.addLimitBurstOption(s);
o = s.taboption('advanced', form.Flag, 'log', _('Enable logging'), _('Log matched packets to syslog.'));
o.modalonly = true;
if (!fw4) {
o = s.taboption('advanced', form.Value, 'extra', _('Extra arguments'),
_('Passes additional arguments to iptables. Use with care!'));
o.modalonly = true;
o.rmempty = true;
}
o = s.taboption('timed', form.MultiValue, 'weekdays', _('Week Days'));
o.modalonly = true;
o.multiple = true;
o.display = 5;
o.placeholder = _('Any day');
o.value('Sun', _('Sunday'));
o.value('Mon', _('Monday'));
o.value('Tue', _('Tuesday'));
o.value('Wed', _('Wednesday'));
o.value('Thu', _('Thursday'));
o.value('Fri', _('Friday'));
o.value('Sat', _('Saturday'));
o.write = function(section_id, value) {
return this.super('write', [ section_id, L.toArray(value).join(' ') ]);
};
o = s.taboption('timed', form.MultiValue, 'monthdays', _('Month Days'));
o.modalonly = true;
o.multiple = true;
o.display_size = 15;
o.placeholder = _('Any day');
o.write = function(section_id, value) {
return this.super('write', [ section_id, L.toArray(value).join(' ') ]);
};
for (let i = 1; i <= 31; i++)
o.value(i);
o = s.taboption('timed', form.Value, 'start_time', _('Start Time (hh:mm:ss)'));
o.modalonly = true;
o.datatype = 'timehhmmss';
o = s.taboption('timed', form.Value, 'stop_time', _('Stop Time (hh:mm:ss)'));
o.modalonly = true;
o.datatype = 'timehhmmss';
o = s.taboption('timed', form.Value, 'start_date', _('Start Date (yyyy-mm-dd)'));
o.modalonly = true;
o.datatype = 'dateyyyymmdd';
o = s.taboption('timed', form.Value, 'stop_date', _('Stop Date (yyyy-mm-dd)'));
o.modalonly = true;
o.datatype = 'dateyyyymmdd';
o = s.taboption('timed', form.Flag, 'utc_time', _('Time in UTC'));
o.modalonly = true;
o.default = o.disabled;
return m.render();
}
});

View File

@ -0,0 +1,410 @@
'use strict';
'require view';
'require rpc';
'require uci';
'require form';
'require network';
'require firewall';
'require tools.firewall as fwtool';
'require tools.widgets as widgets';
return view.extend({
callConntrackHelpers: rpc.declare({
object: 'luci',
method: 'getConntrackHelpers',
expect: { result: [] }
}),
load() {
return Promise.all([
this.callConntrackHelpers(),
firewall.getDefaults()
]);
},
render(data) {
if (fwtool.checkLegacySNAT())
return fwtool.renderMigration();
else
return this.renderZones(data);
},
renderZones([ctHelpers, fwDefaults]) {
let m, s, o, out;
const fw4 = L.hasSystemFeature('firewall4');
m = new form.Map('firewall', _('Firewall - Zone Settings'),
_('The firewall creates zones over your network interfaces to control network traffic flow.'));
s = m.section(form.TypedSection, 'defaults', _('General Settings'));
s.anonymous = true;
s.addremove = false;
o = s.option(form.Flag, 'synflood_protect', _('Enable SYN-flood protection'));
o.cfgvalue = function(section_id) {
const val = uci.get('firewall', section_id, 'synflood_protect');
return (val != null) ? val : uci.get('firewall', section_id, 'syn_flood');
};
o.write = function(section_id, value) {
uci.unset('firewall', section_id, 'syn_flood');
uci.set('firewall', section_id, 'synflood_protect', value);
};
o.remove = function(section_id) {
uci.unset('firewall', section_id, 'syn_flood');
uci.unset('firewall', section_id, 'synflood_protect');
};
o = s.option(form.Flag, 'drop_invalid', _('Drop invalid packets'));
if (L.hasSystemFeature('fullcone')) {
o = s.option(form.Flag, 'fullcone', _('Enable FullCone NAT'));
if (fw4)
o = s.option(form.Flag, 'fullcone6', _('Enable FullCone NAT6'));
}
let p = [
s.option(form.ListValue, 'input', _('Input')),
s.option(form.ListValue, 'output', _('Output')),
s.option(form.ListValue, 'forward', _('Forward'))
];
for (let popt of p) {
popt.value('REJECT', _('reject'));
popt.value('DROP', _('drop'));
popt.value('ACCEPT', _('accept'));
}
/* Netfilter flow offload support */
if (L.hasSystemFeature('offloading')) {
s = m.section(form.TypedSection, 'defaults', _('Routing/NAT Offloading'),
_('Not fully compatible with QoS/SQM.'));
s.anonymous = true;
s.addremove = false;
o = s.option(form.RichListValue, "offloading_type", _("Flow offloading type"));
o.value('0', _("None"));
o.value('1', _("Software flow offloading"), _('Software based offloading for routing/NAT.'));
o.value('2', _("Hardware flow offloading"), _('Hardware based offloading for routing with/without NAT.') + ' ' + _(' Requires hardware NAT support.'));
o.optional = false;
o.load = function (section_id) {
const flow_offloading = uci.get('firewall', section_id, 'flow_offloading');
const flow_offloading_hw = uci.get('firewall', section_id, 'flow_offloading_hw');
return (flow_offloading === '1')
? (flow_offloading_hw === '1' ? '2' : '1')
: '0';
};
o.write = function(section_id, value) {
uci.set('firewall', section_id, 'flow_offloading', value === '0' ? null : '1');
uci.set('firewall', section_id, 'flow_offloading_hw', value === '2' ? '1' : null);
};
}
s = m.section(form.GridSection, 'zone', _('Zones'));
s.addremove = true;
s.anonymous = true;
s.sortable = true;
s.filterrow = true;
s.nodescriptions = true;
s.handleRemove = function(section_id, ev) {
return firewall.deleteZone(section_id).then(L.bind(function() {
return this.super('handleRemove', [section_id, ev]);
}, this));
};
s.tab('general', _('General Settings'));
s.tab('advanced', _('Advanced Settings'));
s.tab('conntrack', _('Conntrack Settings'));
s.tab('extra', _('Extra iptables arguments'));
o = s.taboption('general', form.DummyValue, '_generalinfo');
o.rawhtml = true;
o.modalonly = true;
o.cfgvalue = function(section_id) {
let name = uci.get('firewall', section_id, 'name');
if (name == null)
name = _("this new zone");
return _('This section defines common properties of %q. The <em>input</em> and <em>output</em> options set the default policies for traffic entering and leaving this zone while the <em>forward</em> option describes the policy for forwarded traffic between different networks within the zone. <em>Covered networks</em> specifies which available networks are members of this zone.')
.replace(/%s/g, name).replace(/%q/g, '"' + name + '"');
};
o = s.taboption('general', form.Value, 'name', _('Name'));
o.placeholder = _('Unnamed zone');
o.modalonly = true;
o.rmempty = false;
o.datatype = fw4 ? 'ucifw4zonename' : 'and(ucifw4zonename,maxlength(11))';
o.write = function(section_id, formvalue) {
const cfgvalue = this.cfgvalue(section_id);
if (cfgvalue == null || cfgvalue == '')
return uci.set('firewall', section_id, 'name', formvalue);
else if (cfgvalue != formvalue)
return firewall.renameZone(cfgvalue, formvalue);
};
o = s.option(widgets.ZoneForwards, '_info', _('Zone ⇒ Forwards'));
o.editable = true;
o.modalonly = false;
o.cfgvalue = function(section_id) {
return uci.get('firewall', section_id, 'name');
};
p = [
s.taboption('general', form.ListValue, 'input', _('Input')),
s.taboption('general', form.ListValue, 'output', _('Output')),
s.taboption('general', form.ListValue, 'forward', _('Intra zone forward'))
];
for (let popt of p) {
popt.value('REJECT', _('reject'));
popt.value('DROP', _('drop'));
popt.value('ACCEPT', _('accept'));
popt.editable = true;
}
p[0].default = fwDefaults.getInput();
p[1].default = fwDefaults.getOutput();
p[2].default = fwDefaults.getForward();
o = s.taboption('general', form.Flag, 'masq', _('IPv4 Masquerading'),
_('Enable network address and port translation IPv4 (NAT4 or NAPT4) for outbound traffic on this zone. This is typically enabled on the <em>wan</em> zone.'));
o.editable = true;
o.tooltip = function(section_id) {
var family = uci.get('firewall', section_id, 'family')
var masq_src = uci.get('firewall', section_id, 'masq_src')
var masq_dest = uci.get('firewall', section_id, 'masq_dest')
if ((!family || family.indexOf('6') == -1) && (masq_src || masq_dest))
return _('Limited masquerading enabled');
return null;
};
o = s.taboption('general', form.Flag, 'mtu_fix', _('MSS clamping'));
o.modalonly = true;
o = s.taboption('general', widgets.NetworkSelect, 'network', _('Covered networks'));
o.modalonly = true;
o.multiple = true;
o.cfgvalue = function(section_id) {
return uci.get('firewall', section_id, 'network');
};
o.write = function(section_id, formvalue) {
const name = uci.get('firewall', section_id, 'name');
const cfgvalue = this.cfgvalue(section_id);
const oldNetworks = L.toArray(cfgvalue);
const newNetworks = L.toArray(formvalue);
oldNetworks.sort();
newNetworks.sort();
if (oldNetworks.join(' ') == newNetworks.join(' '))
return;
const tasks = [ firewall.getZone(name) ];
if (Array.isArray(formvalue))
for (let netname of newNetworks) {
tasks.push(network.getNetwork(netname).then(L.bind(function(netname, net) {
return net || network.addNetwork(netname, { 'proto': 'none' });
}, this, netname)));
}
return Promise.all(tasks).then(function(zone_networks) {
if (zone_networks[0]) {
zone_networks[0].clearNetworks();
for (var i = 1; i < zone_networks.length; i++)
zone_networks[0].addNetwork(zone_networks[i].getName());
}
});
};
o = s.taboption('advanced', form.DummyValue, '_advancedinfo');
o.rawhtml = true;
o.modalonly = true;
o.cfgvalue = function(section_id) {
let name = uci.get('firewall', section_id, 'name');
if (name == null)
name = _("this new zone");
return _('The options below control the forwarding policies between this zone (%s) and other zones. <em>Destination zones</em> cover forwarded traffic <strong>originating from %q</strong>. <em>Source zones</em> match forwarded traffic from other zones <strong>targeted at %q</strong>. The forwarding rule is <em>unidirectional</em>, e.g. a forward from lan to wan does <em>not</em> imply a permission to forward from wan to lan as well.')
.format(name);
};
o = s.taboption('advanced', widgets.DeviceSelect, 'device', _('Covered devices'), _('Use this option to classify zone traffic by raw, non-<em>uci</em> managed network devices.'));
o.modalonly = true;
o.noaliases = true;
o.multiple = true;
o = s.taboption('advanced', form.DynamicList, 'subnet', _('Covered subnets'), _('Use this option to classify zone traffic by source or destination subnet instead of networks or devices.'));
o.datatype = 'neg(cidr("true"))';
o.modalonly = true;
o.multiple = true;
if (fw4) {
o = s.taboption('advanced', form.Flag, 'masq6', _('IPv6 Masquerading'),
_('Enable network address and port translation IPv6 (NAT6 or NAPT6) for outbound traffic on this zone.'));
o.modalonly = true;
o.tooltip = function(section_id) {
const family = uci.get('firewall', section_id, 'family')
const masq_src = uci.get('firewall', section_id, 'masq_src')
const masq_dest = uci.get('firewall', section_id, 'masq_dest')
if ((!family || family.indexOf('6') >= 0) && (masq_src || masq_dest))
return _('Limited masquerading enabled');
return null;
};
}
o = s.taboption('advanced', form.ListValue, 'family', _('Restrict to address family'));
o.value('', _('IPv4 and IPv6'));
o.value('ipv4', _('IPv4 only'));
o.value('ipv6', _('IPv6 only'));
o.modalonly = true;
o = s.taboption('advanced', form.DynamicList, 'masq_src', _('Restrict Masquerading to given source subnets'));
if (fw4) {
o.datatype = 'list(neg(or(uciname,hostname,ipmask)))';
} else {
o.depends('family', '');
o.depends('family', 'ipv4');
o.datatype = 'list(neg(or(uciname,hostname,ipmask4)))';
}
o.placeholder = '0.0.0.0/0';
o.modalonly = true;
o = s.taboption('advanced', form.DynamicList, 'masq_dest', _('Restrict Masquerading to given destination subnets'));
if (fw4) {
o.datatype = 'list(neg(or(uciname,hostname,ipmask)))';
} else {
o.depends('family', '');
o.depends('family', 'ipv4');
o.datatype = 'list(neg(or(uciname,hostname,ipmask4)))';
}
o.placeholder = '0.0.0.0/0';
o.modalonly = true;
o = s.taboption('conntrack', form.Flag, 'masq_allow_invalid', _('Allow "invalid" traffic'), _('Do not install extra rules to reject forwarded traffic with conntrack state <em>invalid</em>. This may be required for complex asymmetric route setups.'));
o.modalonly = true;
o = s.taboption('conntrack', form.Flag, 'auto_helper', _('Automatic helper assignment'), _('Automatically assign conntrack helpers based on traffic protocol and port'));
o.default = o.enabled;
o.modalonly = true;
o = s.taboption('conntrack', form.MultiValue, 'helper', _('Conntrack helpers'), _('Explicitly choses allowed connection tracking helpers for zone traffic'));
o.depends('auto_helper', '0');
o.modalonly = true;
for (let cth of ctHelpers)
o.value(cth.name, E('<span><span class="hide-close">%s (%s)</span><span class="hide-open">%s</span></span>'.format(cth.description, cth.name.toUpperCase(), cth.name.toUpperCase())));
o = s.taboption('advanced', form.MultiValue, 'log', _('Enable logging'), _('Log matched packets on the selected tables to syslog.'));
o.modalonly = true;
o.value('filter');
o.value('mangle');
o.placeholder = 'No table selected';
const TABLES = { filter: 1, mangle: 2 };
o.cfgvalue = function (section_id) {
let bitfield = this.super('load', [section_id]) || this.default;
return Object.keys(TABLES).filter(table => bitfield & TABLES[table]);
};
o.write = function (section_id, value) {
let bitfield = L.toArray(value).reduce((acc, table) => acc | (TABLES[table] || 0), 0);
return this.super('write', [section_id, bitfield]);
};
o = s.taboption('advanced', form.Value, 'log_limit', _('Limit log messages'));
o.depends({log: [], "!reverse": true});
o.placeholder = '10/minute';
o.modalonly = true;
if (!fw4) {
o = s.taboption('extra', form.DummyValue, '_extrainfo');
o.rawhtml = true;
o.modalonly = true;
o.cfgvalue = function(section_id) {
return _('Passing raw iptables arguments to source and destination traffic classification rules allows to match packets based on other criteria than interfaces or subnets. These options should be used with extreme care as invalid values could render the firewall ruleset broken, completely exposing all services.');
};
o = s.taboption('extra', form.Value, 'extra_src', _('Extra source arguments'), _('Additional raw <em>iptables</em> arguments to classify zone source traffic, e.g. <code>-p tcp --sport 443</code> to only match inbound HTTPS traffic.'));
o.modalonly = true;
o.cfgvalue = function(section_id) {
return uci.get('firewall', section_id, 'extra_src') || uci.get('firewall', section_id, 'extra');
};
o.write = function(section_id, value) {
uci.unset('firewall', section_id, 'extra');
uci.set('firewall', section_id, 'extra_src', value);
};
o = s.taboption('extra', form.Value, 'extra_dest', _('Extra destination arguments'), _('Additional raw <em>iptables</em> arguments to classify zone destination traffic, e.g. <code>-p tcp --dport 443</code> to only match outbound HTTPS traffic.'));
o.modalonly = true;
o.cfgvalue = function(section_id) {
return uci.get('firewall', section_id, 'extra_dest') || uci.get('firewall', section_id, 'extra_src') || uci.get('firewall', section_id, 'extra');
};
o.write = function(section_id, value) {
uci.unset('firewall', section_id, 'extra');
uci.set('firewall', section_id, 'extra_dest', value);
};
}
o = s.taboption('general', form.DummyValue, '_forwardinfo');
o.rawhtml = true;
o.modalonly = true;
o.cfgvalue = function(section_id) {
let name = uci.get('firewall', section_id, 'name');
if (name == null)
name = _("this new zone");
return _('The options below control the forwarding policies between this zone (%s) and other zones. <em>Destination zones</em> cover forwarded traffic <strong>originating from %q</strong>. <em>Source zones</em> match forwarded traffic from other zones <strong>targeted at %q</strong>. The forwarding rule is <em>unidirectional</em>, e.g. a forward from lan to wan does <em>not</em> imply a permission to forward from wan to lan as well.')
.format(name);
};
out = o = s.taboption('general', widgets.ZoneSelect, 'out', _('Allow forward to <em>destination zones</em>:'));
o.nocreate = true;
o.multiple = true;
o.modalonly = true;
o.filter = function(section_id, value) {
return (uci.get('firewall', section_id, 'name') != value);
};
o.cfgvalue = function(section_id) {
const out = (this.option == 'out');
const zone = this.lookupZone(uci.get('firewall', section_id, 'name'));
const fwds = zone ? zone.getForwardingsBy(out ? 'src' : 'dest') : [];
const value = [];
for (let fwd of fwds)
value.push(out ? fwd.getDestination() : fwd.getSource());
return value;
};
o.write = o.remove = function(section_id, formvalue) {
const out = (this.option == 'out');
const zone = this.lookupZone(uci.get('firewall', section_id, 'name'));
const fwds = zone ? zone.getForwardingsBy(out ? 'src' : 'dest') : [];
if (formvalue == null)
formvalue = [];
if (Array.isArray(formvalue)) {
for (let fwd of fwds) {
var cmp = out ? fwd.getDestination() : fwd.getSource();
if (!formvalue.filter(function(d) { return d == cmp }).length)
zone.deleteForwarding(fwd);
}
for (let fv of formvalue)
if (out)
zone.addForwardingTo(fv);
else
zone.addForwardingFrom(fv);
}
};
o = s.taboption('general', widgets.ZoneSelect, 'in', _('Allow forward from <em>source zones</em>:'));
o.nocreate = true;
o.multiple = true;
o.modalonly = true;
o.write = o.remove = out.write;
o.filter = out.filter;
o.cfgvalue = out.cfgvalue;
return m.render();
}
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1
luci-app-firewall/po/zh-cn Symbolic link
View File

@ -0,0 +1 @@
zh_Hans

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,72 @@
{
"admin/network/firewall": {
"title": "Firewall",
"order": 60,
"action": {
"type": "alias",
"path": "admin/network/firewall/zones"
},
"depends": {
"acl": [ "luci-app-firewall" ],
"fs": { "/sbin/fw3": "executable" },
"uci": { "firewall": true }
}
},
"admin/network/firewall/zones": {
"title": "General Settings",
"order": 10,
"action": {
"type": "view",
"path": "firewall/zones"
}
},
"admin/network/firewall/forwards": {
"title": "Port Forwards",
"order": 20,
"action": {
"type": "view",
"path": "firewall/forwards"
}
},
"admin/network/firewall/rules": {
"title": "Traffic Rules",
"order": 30,
"action": {
"type": "view",
"path": "firewall/rules"
}
},
"admin/network/firewall/snats": {
"title": "NAT Rules",
"order": 40,
"action": {
"type": "view",
"path": "firewall/snats"
}
},
"admin/network/firewall/ipsets": {
"title": "IP Sets",
"order": 45,
"action": {
"type": "view",
"path": "firewall/ipsets"
}
},
"admin/network/firewall/custom": {
"title": "Custom Rules",
"order": 50,
"action": {
"type": "view",
"path": "firewall/custom"
},
"depends": {
"fs": { "/usr/share/fw3/helpers.conf": "file" }
}
}
}

View File

@ -0,0 +1,24 @@
{
"luci-app-firewall": {
"description": "Grant access to firewall configuration",
"read": {
"file": {
"/etc/firewall.user": [ "read" ]
},
"ubus": {
"file": [ "read" ],
"luci": [ "getConntrackHelpers" ]
},
"uci": [ "firewall" ]
},
"write": {
"file": {
"/etc/firewall.user": [ "write" ]
},
"ubus": {
"file": [ "write" ]
},
"uci": [ "firewall" ]
}
}
}

View File

@ -0,0 +1,9 @@
{
"config": "firewall",
"init": "firewall",
"affects": [
"luci-splash",
"qos",
"miniupnpd"
]
}

View File

@ -7,7 +7,7 @@ LUCI_DEPENDS:=+luci-nginx +quickfile
PKG_LICENSE:=Apache-2.0
PKG_VERSION:=1.0.0
PKG_RELEASE:=3
PKG_RELEASE:=4
PKG_MAINTAINER:=sbwml <admin@cooluc.com>
include $(TOPDIR)/feeds/luci/luci.mk

View File

@ -19,7 +19,8 @@ return view.extend({
flex-direction: column;
width: 100%;
max-width: 1600px;
height: 800px;
height: calc(100vh - 180px);
min-height: 800px;
overflow: hidden;
border-radius: 10px;
`

View File

@ -7,8 +7,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=quickfile
PKG_VERSION:=1.0.22
PKG_RELEASE:=7
PKG_VERSION:=1.0.23
PKG_RELEASE:=8
PKG_SOURCE:=quickfile-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://r2.cooluc.com/source

View File

@ -5,8 +5,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=tuic-client
PKG_VERSION:=1.8.4
PKG_RELEASE:=7
PKG_VERSION:=1.8.5
PKG_RELEASE:=8
PKG_LICENSE_FILES:=LICENSE
PKG_MAINTAINER:=Tianling Shen <cnsztl@immortalwrt.org>

View File

@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=Xray-core
PKG_VERSION:=26.5.9
PKG_RELEASE:=10
PKG_VERSION:=26.6.1
PKG_RELEASE:=11
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/XTLS/Xray-core/tar.gz/v$(PKG_VERSION)?