applications/luci-firewall: complete rework
authorJo-Philipp Wich <jow@openwrt.org>
Mon, 1 Nov 2010 19:45:54 +0000 (19:45 +0000)
committerJo-Philipp Wich <jow@openwrt.org>
Mon, 1 Nov 2010 19:45:54 +0000 (19:45 +0000)
applications/luci-firewall/luasrc/controller/luci_fw/luci_fw.lua
applications/luci-firewall/luasrc/model/cbi/luci_fw/redirect.lua [deleted file]
applications/luci-firewall/luasrc/model/cbi/luci_fw/rrule.lua
applications/luci-firewall/luasrc/model/cbi/luci_fw/traffic.lua [deleted file]
applications/luci-firewall/luasrc/model/cbi/luci_fw/trule.lua
applications/luci-firewall/luasrc/model/cbi/luci_fw/zones.lua

index 766821a..38670d3 100644 (file)
@@ -5,9 +5,9 @@ function index()
        local i18n = luci.i18n.translate
 
        entry({"admin", "network", "firewall"}, alias("admin", "network", "firewall", "zones"), i18n("Firewall"), 60).i18n = "luci-fw"
-       entry({"admin", "network", "firewall", "zones"}, cbi("luci_fw/zones"), i18n("Zones"), 10)
-       entry({"admin", "network", "firewall", "redirect"}, arcombine(cbi("luci_fw/redirect"), cbi("luci_fw/rrule")), i18n("Traffic Redirection"), 30).leaf = true      
-       entry({"admin", "network", "firewall", "rule"}, arcombine(cbi("luci_fw/traffic"), cbi("luci_fw/trule")), i18n("Traffic Control"), 20).leaf = true
-       
+       entry({"admin", "network", "firewall", "zones"}, arcombine(cbi("luci_fw/zones"), cbi("luci_fw/zone")), nil, 10).leaf = true
+       entry({"admin", "network", "firewall", "rule"}, arcombine(cbi("luci_fw/zones"), cbi("luci_fw/trule")), nil, 20).leaf = true
+       entry({"admin", "network", "firewall", "redirect"}, arcombine(cbi("luci_fw/zones"), cbi("luci_fw/rrule")), nil, 30).leaf = true
+
        entry({"mini", "network", "portfw"}, cbi("luci_fw/miniportfw", {autoapply=true}), i18n("Port forwarding"), 70).i18n = "luci-fw"
-end
\ No newline at end of file
+end
diff --git a/applications/luci-firewall/luasrc/model/cbi/luci_fw/redirect.lua b/applications/luci-firewall/luasrc/model/cbi/luci_fw/redirect.lua
deleted file mode 100644 (file)
index da87015..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
---[[
-LuCI - Lua Configuration Interface
-
-Copyright 2008 Steven Barth <steven@midlink.org>
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-$Id$
-]]--
-require("luci.sys")
-m = Map("firewall", translate("Traffic Redirection"),
-       translate("Traffic redirection allows you to change the " ..
-               "destination address of forwarded packets."))
-
-
-s = m:section(TypedSection, "redirect", "")
-s.template  = "cbi/tblsection"
-s.addremove = true
-s.anonymous = true
-s.extedit   = luci.dispatcher.build_url("admin", "network", "firewall", "redirect", "%s")
-
-name = s:option(Value, "_name", translate("Name"), translate("(optional)"))
-name.size = 10
-
-iface = s:option(ListValue, "src", translate("Zone"))
-iface.default = "wan"
-luci.model.uci.cursor():foreach("firewall", "zone",
-       function (section)
-               iface:value(section.name)
-       end)
-
-proto = s:option(ListValue, "proto", translate("Protocol"))
-proto:value("tcp", "TCP")
-proto:value("udp", "UDP")
-proto:value("tcpudp", "TCP+UDP")
-
-dport = s:option(Value, "src_dport", translate("Source port"))
-dport.size = 5
-
-to = s:option(Value, "dest_ip", translate("Destination IP"))
-for i, dataset in ipairs(luci.sys.net.arptable()) do
-       to:value(dataset["IP address"])
-end
-
-toport = s:option(Value, "dest_port", translate("Destination port"))
-toport.size = 5
-
-return m
index 63e0144..6332d8e 100644 (file)
@@ -2,6 +2,7 @@
 LuCI - Lua Configuration Interface
 
 Copyright 2008 Steven Barth <steven@midlink.org>
+Copyright 2010 Jo-Philipp Wich <xm@subsignal.org>
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -11,70 +12,130 @@ You may obtain a copy of the License at
 
 $Id$
 ]]--
-require("luci.sys")
+
+local sys = require "luci.sys"
+local dsp = require "luci.dispatcher"
+
 arg[1] = arg[1] or ""
 
 m = Map("firewall", translate("Traffic Redirection"),
        translate("Traffic redirection allows you to change the " ..
                "destination address of forwarded packets."))
 
+m.redirect = dsp.build_url("admin", "network", "firewall")
+
+if not m.uci:get(arg[1]) == "redirect" then
+       luci.http.redirect(m.redirect)
+       return
+end
+
+local has_v2 = nixio.fs.access("/lib/firewall/fw.sh")
+local wan_zone = nil
+
+m.uci:foreach("firewall", "zone",
+       function(s)
+               local n = s.network or s.name
+               if n then
+                       local i
+                       for i in n:gmatch("%S+") do
+                               if i == "wan" then
+                                       wan_zone = s.name
+                                       return false
+                               end
+                       end
+               end
+       end)
 
 s = m:section(NamedSection, arg[1], "redirect", "")
 s.anonymous = true
 s.addremove = false
 
-back = s:option(DummyValue, "_overview", translate("Overview"))
+s:tab("general", translate("General Settings"))
+s:tab("advanced", translate("Advanced Settings"))
+
+back = s:taboption("general", DummyValue, "_overview", translate("Overview"))
 back.value = ""
 back.titleref = luci.dispatcher.build_url("admin", "network", "firewall", "redirect")
 
-name = s:option(Value, "_name", translate("Name"))
+name = s:taboption("general", Value, "_name", translate("Name"))
 name.rmempty = true
 name.size = 10
 
-iface = s:option(ListValue, "src", translate("Source zone"))
-iface.default = "wan"
-luci.model.uci.cursor():foreach("firewall", "zone",
-       function (section)
-               iface:value(section.name)
-       end)
-       
-s:option(Value, "src_ip", translate("Source IP address")).optional = true
-s:option(Value, "src_mac", translate("Source MAC-address")).optional = true
+src = s:taboption("general", Value, "src", translate("Source zone"))
+src.nocreate = true
+src.default = "wan"
+src.template = "cbi/firewall_zonelist"
 
-sport = s:option(Value, "src_port", translate("Source port"),
-       translate("Match incoming traffic originating from the given " ..
-               "source port or port range on the client host"))
-sport.optional = true
-sport:depends("proto", "tcp")
-sport:depends("proto", "udp")
-sport:depends("proto", "tcpudp")
-
-proto = s:option(ListValue, "proto", translate("Protocol"))
+proto = s:taboption("general", ListValue, "proto", translate("Protocol"))
 proto.optional = true
-proto:value("")
+proto:value("tcpudp", "TCP+UDP")
 proto:value("tcp", "TCP")
 proto:value("udp", "UDP")
-proto:value("tcpudp", "TCP+UDP")
 
-dport = s:option(Value, "src_dport", translate("External port"),
+dport = s:taboption("general", Value, "src_dport", translate("External port"),
        translate("Match incoming traffic directed at the given " ..
                "destination port or port range on this host"))
-dport.size = 5
+dport.datatype = "portrange"
 dport:depends("proto", "tcp")
 dport:depends("proto", "udp")
 dport:depends("proto", "tcpudp")
 
-to = s:option(Value, "dest_ip", translate("Internal IP address"),
+to = s:taboption("general", Value, "dest_ip", translate("Internal IP address"),
        translate("Redirect matched incoming traffic to the specified " ..
                "internal host"))
+to.datatype = "ip4addr"
 for i, dataset in ipairs(luci.sys.net.arptable()) do
        to:value(dataset["IP address"])
 end
 
-toport = s:option(Value, "dest_port", translate("Internal port (optional)"),
+toport = s:taboption("general", Value, "dest_port", translate("Internal port (optional)"),
        translate("Redirect matched incoming traffic to the given port on " ..
                "the internal host"))
 toport.optional = true
 toport.size = 5
 
+
+target = s:taboption("advanced", ListValue, "target", translate("Redirection type"))
+target:value("DNAT")
+target:value("SNAT")
+
+dest = s:taboption("advanced", Value, "dest", translate("Destination zone"))
+dest.nocreate = true
+dest.default = "lan"
+dest.template = "cbi/firewall_zonelist"
+
+src_dip = s:taboption("advanced", Value, "src_dip",
+       translate("Intended destination address"),
+       translate(
+               "For DNAT, match incoming traffic directed at the given destination "..
+               "ip address. For SNAT rewrite the source address to the given address."
+       ))
+
+src_dip.optional = true
+src_dip.datatype = "ip4addr"
+
+src_mac = s:taboption("advanced", Value, "src_mac", translate("Source MAC address"))
+src_mac.optional = true
+src_mac.datatype = "macaddr"
+
+src_ip = s:taboption("advanced", Value, "src_ip", translate("Source IP address"))
+src_ip.optional = true
+src_ip.datatype = "ip4addr"
+
+sport = s:taboption("advanced", Value, "src_port", translate("Source port"),
+       translate("Match incoming traffic originating from the given " ..
+               "source port or port range on the client host"))
+sport.optional = true
+sport.datatype = "portrange"
+sport:depends("proto", "tcp")
+sport:depends("proto", "udp")
+sport:depends("proto", "tcpudp")
+
+reflection = s:taboption("advanced", Flag, "reflection", translate("Enable NAT Loopback"))
+reflection.rmempty = true
+reflection:depends({ target = "DNAT", src = wan_zone })
+reflection.cfgvalue = function(...)
+       return Flag.cfgvalue(...) or "1"
+end
+
 return m
diff --git a/applications/luci-firewall/luasrc/model/cbi/luci_fw/traffic.lua b/applications/luci-firewall/luasrc/model/cbi/luci_fw/traffic.lua
deleted file mode 100644 (file)
index 3bdc6db..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
---[[
-LuCI - Lua Configuration Interface
-
-Copyright 2008 Steven Barth <steven@midlink.org>
-Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-$Id$
-]]--
-
-m = Map("firewall", translate("Traffic Control"))
-s = m:section(TypedSection, "forwarding", translate("Zone-to-Zone traffic"),
-       translate("Here you can specify which network traffic is allowed " ..
-               "to flow between network zones. Only new connections will " ..
-               "be matched.  Packets belonging to already open " ..
-               "connections are automatically allowed to pass the " ..
-               "firewall. If you experience occasional connection " ..
-               "problems try enabling MSS Clamping otherwise disable it " ..
-               "for performance reasons."))
-s.template  = "cbi/tblsection"
-s.addremove = true
-s.anonymous = true
-
-iface = s:option(ListValue, "src", translate("Source"))
-oface = s:option(ListValue, "dest", translate("Destination"))
-
-luci.model.uci.cursor():foreach("firewall", "zone",
-       function (section)
-                       iface:value(section.name)
-                       oface:value(section.name)
-       end)
-
-
-
-s = m:section(TypedSection, "rule", translate("Rules"))
-s.addremove = true
-s.anonymous = true
-s.template = "cbi/tblsection"
-s.extedit   = luci.dispatcher.build_url("admin", "network", "firewall", "rule", "%s")
-s.defaults.target = "ACCEPT"
-
-local created = nil
-
-function s.create(self, section)
-       created = TypedSection.create(self, section)
-end
-
-function s.parse(self, ...)
-       TypedSection.parse(self, ...)
-       if created then
-               m.uci:save("firewall")
-               luci.http.redirect(luci.dispatcher.build_url(
-                       "admin", "network", "firewall", "rule", created
-               ))
-       end
-end
-
-s:option(DummyValue, "_name", translate("Name"))
-s:option(DummyValue, "proto", translate("Protocol"))
-
-src = s:option(DummyValue, "src", translate("Source"))
-function src.cfgvalue(self, s)
-       return "%s:%s:%s" % {
-               self.map:get(s, "src") or "*",
-               self.map:get(s, "src_ip") or "0.0.0.0/0",
-               self.map:get(s, "src_port") or "*"
-       } 
-end
-
-dest = s:option(DummyValue, "dest", translate("Destination"))
-function dest.cfgvalue(self, s)
-       return "%s:%s:%s" % {
-               self.map:get(s, "dest") or translate("Device"),
-               self.map:get(s, "dest_ip") or "0.0.0.0/0",
-               self.map:get(s, "dest_port") or "*"
-       } 
-end
-
-
-s:option(DummyValue, "target", translate("Action"))
-
-
-return m
index 0ce41e3..7ee8fd8 100644 (file)
@@ -2,6 +2,7 @@
 LuCI - Lua Configuration Interface
 
 Copyright 2008 Steven Barth <steven@midlink.org>
+Copyright 2010 Jo-Philipp Wich <xm@subsignal.org>
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -11,62 +12,121 @@ You may obtain a copy of the License at
 
 $Id$
 ]]--
+
+local has_v2 = nixio.fs.access("/lib/firewall/fw.sh")
+local dsp = require "luci.dispatcher"
+
 arg[1] = arg[1] or ""
+
 m = Map("firewall", translate("Advanced Rules"),
        translate("Advanced rules let you customize the firewall to your " ..
                "needs. Only new connections will be matched. Packets " ..
                "belonging to already open connections are automatically " ..
                "allowed to pass the firewall."))
 
+m.redirect = dsp.build_url("admin", "network", "firewall")
+
+if not m.uci:get(arg[1]) == "rule" then
+       luci.http.redirect(m.redirect)
+       return
+end
+
 s = m:section(NamedSection, arg[1], "rule", "")
 s.anonymous = true
 s.addremove = false
 
+s:tab("general", translate("General Settings"))
+s:tab("advanced", translate("Advanced Options"))
+
 back = s:option(DummyValue, "_overview", translate("Overview"))
 back.value = ""
-back.titleref = luci.dispatcher.build_url("admin", "network", "firewall", "rule")
+back.titleref = dsp.build_url("admin", "network", "firewall", "rule")
 
 
-name = s:option(Value, "_name", translate("Name").." "..translate("(optional)"))
+name = s:taboption("general", Value, "_name", translate("Name").." "..translate("(optional)"))
 name.rmempty = true
 
-iface = s:option(ListValue, "src", translate("Source zone"))
-iface.rmempty = true
+src = s:taboption("general", Value, "src", translate("Source zone"))
+src.nocreate = true
+src.default = "wan"
+src.template = "cbi/firewall_zonelist"
 
-oface = s:option(ListValue, "dest", translate("Destination zone"))
-oface:value("", translate("any"))
-oface.rmempty = true
+dest = s:taboption("advanced", Value, "dest", translate("Destination zone"))
+dest.nocreate = true
+dest.default = "lan"
+dest.template = "cbi/firewall_zonelist"
 
-luci.model.uci.cursor():foreach("firewall", "zone",
-       function (section)
-               iface:value(section.name)
-               oface:value(section.name)
-       end)
-
-proto = s:option(Value, "proto", translate("Protocol"))
+proto = s:taboption("general", Value, "proto", translate("Protocol"))
 proto.optional = true
-proto:value("")
 proto:value("all", translate("Any"))
 proto:value("tcpudp", "TCP+UDP")
 proto:value("tcp", "TCP")
 proto:value("udp", "UDP")
 proto:value("icmp", "ICMP")
 
-s:option(Value, "src_ip", translate("Source address")).optional = true
-s:option(Value, "dest_ip", translate("Destination address")).optional = true
-s:option(Value, "src_mac", translate("Source MAC-address")).optional = true
-
-sport = s:option(Value, "src_port", translate("Source port"))
+icmpt = s:taboption("general", Value, "icmp_type", translate("Match ICMP type"))
+icmpt:depends("proto", "icmp")
+icmpt:value("any")
+icmpt:value("echo-reply")
+icmpt:value("destination-unreachable")
+icmpt:value("network-unreachable")
+icmpt:value("host-unreachable")
+icmpt:value("protocol-unreachable")
+icmpt:value("port-unreachable")
+icmpt:value("fragmentation-needed")
+icmpt:value("source-route-failed")
+icmpt:value("network-unknown")
+icmpt:value("host-unknown")
+icmpt:value("network-prohibited")
+icmpt:value("host-prohibited")
+icmpt:value("TOS-network-unreachable")
+icmpt:value("TOS-host-unreachable")
+icmpt:value("communication-prohibited")
+icmpt:value("host-precedence-violation")
+icmpt:value("precedence-cutoff")
+icmpt:value("source-quench")
+icmpt:value("redirect")
+icmpt:value("network-redirect")
+icmpt:value("host-redirect")
+icmpt:value("TOS-network-redirect")
+icmpt:value("TOS-host-redirect")
+icmpt:value("echo-request")
+icmpt:value("router-advertisement")
+icmpt:value("router-solicitation")
+icmpt:value("time-exceeded")
+icmpt:value("ttl-zero-during-transit")
+icmpt:value("ttl-zero-during-reassembly")
+icmpt:value("parameter-problem")
+icmpt:value("ip-header-bad")
+icmpt:value("required-option-missing")
+icmpt:value("timestamp-request")
+icmpt:value("timestamp-reply")
+icmpt:value("address-mask-request")
+icmpt:value("address-mask-reply")
+
+src_ip = s:taboption("general", Value, "src_ip", translate("Source address"))
+src_ip.optional = true
+src_ip.datatype = has_v2 and "ipaddr" or "ip4addr"
+
+sport = s:taboption("general", Value, "src_port", translate("Source port"))
+sport.optional = true
+sport.datatype = "portrange"
 sport:depends("proto", "tcp")
 sport:depends("proto", "udp")
 sport:depends("proto", "tcpudp")
 
-dport = s:option(Value, "dest_port", translate("Destination port"))
+dest_ip = s:taboption("general", Value, "dest_ip", translate("Destination address"))
+dest_ip.optional = true
+dest_ip.datatype = has_v2 and "ipaddr" or "ip4addr"
+
+dport = s:taboption("general", Value, "dest_port", translate("Destination port"))
+dport.optional = true
+dport.datatype = "portrange"
 dport:depends("proto", "tcp")
 dport:depends("proto", "udp")
 dport:depends("proto", "tcpudp")
 
-jump = s:option(ListValue, "target", translate("Action"))
+jump = s:taboption("general", ListValue, "target", translate("Action"))
 jump.rmempty = true
 jump.default = "ACCEPT"
 jump:value("DROP", translate("drop"))
@@ -74,4 +134,14 @@ jump:value("ACCEPT", translate("accept"))
 jump:value("REJECT", translate("reject"))
 
 
+s:taboption("advanced", Value, "src_mac", translate("Source MAC-address")).optional = true
+
+if has_v2 then
+       family = s:taboption("advanced", ListValue, "family", translate("Restrict to address family"))
+       family.rmempty = true
+       family:value("", translate("IPv4 and IPv6"))
+       family:value("ipv4", translate("IPv4 only"))
+       family:value("ipv6", translate("IPv6 only"))
+end
+
 return m
index edb82a9..f0e7b86 100644 (file)
@@ -14,6 +14,9 @@ $Id$
 
 local nw = require "luci.model.network"
 local fw = require "luci.model.firewall"
+local ds = require "luci.dispatcher"
+
+local has_v2 = nixio.fs.access("/lib/firewall/fw.sh")
 
 require("luci.tools.webadmin")
 m = Map("firewall", translate("Firewall"), translate("The firewall creates zones over your network interfaces to control network traffic flow."))
@@ -25,18 +28,22 @@ s = m:section(TypedSection, "defaults")
 s.anonymous = true
 s.addremove = false
 
-s:option(Flag, "syn_flood", translate("Enable SYN-flood protection"))
+s:tab("general", translate("General Settings"))
+s:tab("custom", translate("Custom Rules"))
+
 
-local di = s:option(Flag, "drop_invalid", translate("Drop invalid packets"))
+s:taboption("general", Flag, "syn_flood", translate("Enable SYN-flood protection"))
+
+local di = s:taboption("general", Flag, "drop_invalid", translate("Drop invalid packets"))
 di.rmempty = false
 function di.cfgvalue(...)
        return AbstractValue.cfgvalue(...) or "1"
 end
 
 p = {}
-p[1] = s:option(ListValue, "input", translate("Input"))
-p[2] = s:option(ListValue, "output", translate("Output"))
-p[3] = s:option(ListValue, "forward", translate("Forward"))
+p[1] = s:taboption("general", ListValue, "input", translate("Input"))
+p[2] = s:taboption("general", ListValue, "output", translate("Output"))
+p[3] = s:taboption("general", ListValue, "forward", translate("Forward"))
 
 for i, v in ipairs(p) do
        v:value("REJECT", translate("reject"))
@@ -44,14 +51,41 @@ for i, v in ipairs(p) do
        v:value("ACCEPT", translate("accept"))
 end
 
+custom = s:taboption("custom", Value, "_custom",
+       translate("Custom Rules (/etc/firewall.user)"))
+
+custom.template = "cbi/tvalue"
+custom.rows = 20
+
+function custom.cfgvalue(self, section)
+       return nixio.fs.readfile("/etc/firewall.user")
+end
+
+function custom.write(self, section, value)
+       nixio.fs.writefile("/etc/firewall.user", value)
+end
+
 
 s = m:section(TypedSection, "zone", translate("Zones"))
 s.template = "cbi/tblsection"
 s.anonymous = true
 s.addremove = true
+s.extedit   = ds.build_url("admin", "network", "firewall", "zones", "%s")
 
-name = s:option(Value, "name", translate("Name"))
-name.size = 8
+function s.create(self)
+       local z = fw:new_zone()
+       if z then
+               luci.http.redirect(
+                       ds.build_url("admin", "network", "firewall", "zones", z.sid)
+               )
+       end
+end
+
+info = s:option(DummyValue, "_info", translate("Zone ⇒ Forwardings"))
+info.template = "cbi/firewall_zoneforwards"
+function info.cfgvalue(self, section)
+       return self.map:get(section, "name")
+end
 
 p = {}
 p[1] = s:option(ListValue, "input", translate("Input"))
@@ -67,15 +101,166 @@ end
 s:option(Flag, "masq", translate("Masquerading"))
 s:option(Flag, "mtu_fix", translate("MSS clamping"))
 
-net = s:option(MultiValue, "network", translate("Network"))
-net.template = "cbi/network_netlist"
-net.widget = "checkbox"
-net.rmempty = true
-luci.tools.webadmin.cbi_add_networks(net)
 
-function net.cfgvalue(self, section)
-       local value = MultiValue.cfgvalue(self, section)
-       return value or name:cfgvalue(section)
+local created = nil
+
+--
+-- Redirects
+--
+
+s = m:section(TypedSection, "redirect", translate("Redirections"))
+s.template  = "cbi/tblsection"
+s.addremove = true
+s.anonymous = true
+s.extedit   = ds.build_url("admin", "network", "firewall", "redirect", "%s")
+
+function s.create(self, section)
+       created = TypedSection.create(self, section)
+end
+
+function s.parse(self, ...)
+       TypedSection.parse(self, ...)
+       if created then
+               m.uci:save("firewall")
+               luci.http.redirect(ds.build_url(
+                       "admin", "network", "firewall", "redirect", created
+               ))
+       end
+end
+
+name = s:option(DummyValue, "_name", translate("Name"))
+function name.cfgvalue(self, s)
+       return self.map:get(s, "_name") or "-"
+end
+
+proto = s:option(DummyValue, "proto", translate("Protocol"))
+function proto.cfgvalue(self, s)
+       local p = self.map:get(s, "proto")
+       if not p or p == "tcpudp" then
+               return "TCP+UDP"
+       else
+               return p:upper()
+       end
+end
+
+src = s:option(DummyValue, "src", translate("Source"))
+function src.cfgvalue(self, s)
+       local rv = "%s:%s:%s" % {
+               self.map:get(s, "src") or "*",
+               self.map:get(s, "src_ip") or "0.0.0.0/0",
+               self.map:get(s, "src_port") or "*"
+       }
+
+       local mac = self.map:get(s, "src_mac")
+       if mac then
+               rv = rv .. ", MAC " .. mac
+       end
+
+       return rv
+end
+
+via = s:option(DummyValue, "via", translate("Via"))
+function via.cfgvalue(self, s)
+       return "%s:%s:%s" % {
+               translate("Device"),
+               self.map:get(s, "src_dip") or "0.0.0.0/0",
+               self.map:get(s, "src_dport") or "*"
+       }
+end
+
+dest = s:option(DummyValue, "dest", translate("Destination"))
+function dest.cfgvalue(self, s)
+       return "%s:%s:%s" % {
+               self.map:get(s, "dest") or "*",
+               self.map:get(s, "dest_ip") or "0.0.0.0/0",
+               self.map:get(s, "dest_port") or "*"
+       }
+end
+
+target = s:option(DummyValue, "target", translate("Action"))
+function target.cfgvalue(self, s)
+       return self.map:get(s, "target") or "DNAT"
+end
+
+
+--
+-- Rules
+--
+
+s = m:section(TypedSection, "rule", translate("Rules"))
+s.addremove = true
+s.anonymous = true
+s.template = "cbi/tblsection"
+s.extedit   = ds.build_url("admin", "network", "firewall", "rule", "%s")
+s.defaults.target = "ACCEPT"
+
+function s.create(self, section)
+       local created = TypedSection.create(self, section)
+       m.uci:save("firewall")
+       luci.http.redirect(ds.build_url(
+               "admin", "network", "firewall", "rule", created
+       ))
+       return
 end
 
+name = s:option(DummyValue, "_name", translate("Name"))
+function name.cfgvalue(self, s)
+       return self.map:get(s, "_name") or "-"
+end
+
+if has_v2 then
+       family = s:option(DummyValue, "family", translate("Family"))
+       function family.cfgvalue(self, s)
+               local f = self.map:get(s, "family")
+               if f and f:match("4") then
+                       return translate("IPv4 only")
+               elseif f and f:match("6") then
+                       return translate("IPv6 only")
+               else
+                       return translate("IPv4 and IPv6")
+               end
+       end
+end
+
+proto = s:option(DummyValue, "proto", translate("Protocol"))
+function proto.cfgvalue(self, s)
+       local p = self.map:get(s, "proto")
+       local t = self.map:get(s, "icmp_type")
+       if p == "icmp" and t then
+               return "ICMP (%s)" % t
+       elseif p == "tcpudp" or not p then
+               return "TCP+UDP"
+       else
+               return p:upper()
+       end
+end
+
+src = s:option(DummyValue, "src", translate("Source"))
+function src.cfgvalue(self, s)
+       local rv = "%s:%s:%s" % {
+               self.map:get(s, "src") or "*",
+               self.map:get(s, "src_ip") or "0.0.0.0/0",
+               self.map:get(s, "src_port") or "*"
+       }
+
+       local mac = self.map:get(s, "src_mac")
+       if mac then
+               rv = rv .. ", MAC " .. mac
+       end
+
+       return rv
+end
+
+dest = s:option(DummyValue, "dest", translate("Destination"))
+function dest.cfgvalue(self, s)
+       return "%s:%s:%s" % {
+               self.map:get(s, "dest") or translate("Device"),
+               self.map:get(s, "dest_ip") or "0.0.0.0/0",
+               self.map:get(s, "dest_port") or "*"
+       }
+end
+
+
+s:option(DummyValue, "target", translate("Action"))
+
 return m