module(..., package.seeall) -- Load libraries require("modelfunctions") fs = require("acf.fs") format = require("acf.format") validator = require("acf.validator") -- Set variables local packagename = "iptables" local servicename = "iptables" local configfile = "/etc/conf.d/iptables" local path = "PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin " local tables = {"filter", "nat", "mangle"} local details -- ################################################################################ -- LOCAL FUNCTIONS local getdetails = function() if not details then details = {} local cmd = path .. "iptables -t filter -n -L -v" for i,tab in ipairs(tables) do local f = io.popen( (string.gsub(cmd, "filter", tab)) ) details[tab] = {table=tab} local file = f:read("*a") f:close() for line in string.gmatch(file, "([^\n]+)") do if string.match(line, "^Chain") then local name = string.match(line, "^%w+%s+(%S+)") local policy = string.match(line, "policy (%w+)") local references = string.match(line, "(%d+) references") table.insert(details[tab], {name=name, policy=policy, references=references}) elseif not string.match(line, "target%s+prot") then local block = {} block.packets, block.bytes, block.rule = string.match(line, "^%s*(%S+)%s+(%S+)%s+(.*%S)") table.insert(details[tab][#details[tab]], block) --table.insert(details[tab][#details[tab]], line) elseif not details[tab].header then details[tab].header = line end end end end end local find_chain = function(tab, chain) getdetails() if details[tab] then for i,chn in ipairs(details[tab]) do if chain == chn.name then return chn end end end return nil end local save = function() local cmd = path .. "/etc/init.d/"..servicename.." save 2>&1" local f = io.popen( cmd ) f:close() details = nil end local function validate_rule(rule) local success = true function basiccheck(val) if string.find(val.value, "[%s\'\"]") then val.errtxt = "Cannot contain spaces or quotes" success = false end end success = modelfunctions.validateselect(rule.value.table) and success basiccheck(rule.value.chain) if rule.value.position.value ~= "" and not validator.is_integer(rule.value.position.value) then rule.value.position.errtxt = "Must be a number" success = false end basiccheck(rule.value.protocol) basiccheck(rule.value.source) basiccheck(rule.value.destination) basiccheck(rule.value.jump) basiccheck(rule.value.goto) basiccheck(rule.value.in_interface) basiccheck(rule.value.out_interface) if #rule.value.fragment.value > 1 or string.match(rule.value.fragment.value, "[^%+!]") then rule.value.fragment.errtxt = "Invalid entry" success = false end if rule.value.set_counters.value ~= "" and not string.match(rule.value.set_counters.value, "^%d+%s+%d+$") then rule.value.set_counters.errtxt = "Invalid entry" success = false end basiccheck(rule.value.addrtype_src_type) basiccheck(rule.value.addrtype_dst_type) if string.find(rule.value.comment.value, "[\'\"]") then rule.value.comment.errtxt = "Cannot contain quotes" success = false end basiccheck(rule.value.icmp_type) basiccheck(rule.value.src_range) basiccheck(rule.value.dst_range) basiccheck(rule.value.mac_source) basiccheck(rule.value.sports) basiccheck(rule.value.dports) basiccheck(rule.value.ports) basiccheck(rule.value.state) basiccheck(rule.value.tcp_sport) basiccheck(rule.value.tcp_dport) basiccheck(rule.value.udp_sport) basiccheck(rule.value.udp_dport) return success, rule end local function generate_rule_specification(rule) local spec = {} -- notfirst parameter indicates that the "not" (!) must come before the option name function addparameter(value, option, notfirst) if value ~= "" then if string.find(value, "^!") then if notfirst then spec[#spec + 1] = '!' value = string.sub(value, 2) else value = string.sub(value,1,1) .. " " .. string.sub(value,2) end end spec[#spec + 1] = option spec[#spec + 1] = format.escapespecialcharacters(value) end end function addmodule(values, mod) for i,value in ipairs(values) do if value ~= "" then spec[#spec + 1] = "-m "..mod break end end end addparameter(rule.value.protocol.value, "-p", true) addparameter(rule.value.source.value, "-s", true) addparameter(rule.value.destination.value, "-d", true) addparameter(rule.value.jump.value, "-j") addparameter(rule.value.goto.value, "-g") addparameter(rule.value.in_interface.value, "-i", true) addparameter(rule.value.out_interface.value, "-o", true) if rule.value.fragment.value == "!" then spec[#spec + 1] = "! -f" elseif rule.value.fragment.value ~= "" then spec[#spec + 1] = "-f" end addparameter(rule.value.set_counters.value, "-c") addmodule({rule.value.addrtype_src_type.value, rule.value.addrtype_dst_type.value}, "addrtype") addparameter(rule.value.addrtype_src_type.value, "--src-type") addparameter(rule.value.addrtype_dst_type.value, "--dst-type") addmodule({rule.value.comment.value}, "comment") if rule.value.comment.value ~= "" then spec[#spec + 1] = "--comment" spec[#spec + 1] = '"' .. rule.value.comment.value .. '"' end addmodule({rule.value.icmp_type.value}, "icmp") addparameter(rule.value.icmp_type.value, "--icmp-type", true) addmodule({rule.value.src_range.value, rule.value.dst_range.value}, "iprange") addparameter(rule.value.src_range.value, "--src-range", true) addparameter(rule.value.dst_range.value, "--dst-range", true) addmodule({rule.value.mac_source.value}, "mac") addparameter(rule.value.mac_source.value, "--mac-source", true) addmodule({rule.value.sports.value, rule.value.dports.value, rule.value.ports.value}, "multiport") addparameter(rule.value.sports.value, "--sports", true) addparameter(rule.value.dports.value, "--dports", true) addparameter(rule.value.ports.value, "--ports", true) addmodule({rule.value.state.value}, "state") addparameter(rule.value.state.value, "--state") addmodule({rule.value.tcp_sport.value, rule.value.tcp_dport.value}, "tcp") addparameter(rule.value.tcp_sport.value, "--sport", true) addparameter(rule.value.tcp_dport.value, "--dport", true) addmodule({rule.value.udp_sport.value, rule.value.udp_dport.value}, "udp") addparameter(rule.value.udp_sport.value, "--sport", true) addparameter(rule.value.udp_dport.value, "--dport", true) return table.concat(spec, " ") end -- ################################################################################ -- PUBLIC FUNCTIONS function getstatus() return modelfunctions.getstatus(servicename, packagename, "IPtables Status") end function getstatusdetails() getdetails() local retval = {} for i,tab in ipairs(tables) do local chains = 0 local rules = 0 for i,chain in ipairs(details[tab]) do chains = chains + 1 rules = rules + #chain end retval[tab] = {chains=chains, rules=rules} end return cfe({ type="structure", value=retval, label="IPtables Status Details" }) end function getrules(tab) getdetails() tab = tab or "filter" return cfe({ type="structure", value=details[tab] or {}, label=string.gsub(tab, "^.", string.upper).." Rules" }) end function read_chain(tab, chain) local retval = {} retval.table = cfe({ type="select", value=tab or "filter", label="Table", option=tables }) retval.chain = cfe({ value=chain or "", label="Chain" }) getdetails() if tab and not details[tab] then retval.table.errtxt = "Invalid table" end if chain then local chn = find_chain(retval.table.value, chain) if not chn then retval.chain.errtxt = "Cannot find chain" elseif chn.policy then -- only built-in chains can have policies, and the target can only be DROP or ACCEPT retval.policy = cfe({ type="select", value=chn.policy, label="Policy", option={"DROP", "ACCEPT"} }) end end return cfe({ type="group", value=retval, label="Chain" }) end function update_chain(self, chain) local success = true getdetails() if not details[chain.value.table.value] then chain.value.table.errtxt = "Invalid table" success = false elseif not find_chain(chain.value.table.value, chain.value.chain.value) then chain.value.chain.errtxt = "Invalid chain" success = false end if success then if chain.value.policy then local cmd = path .. "iptables -t "..format.escapespecialcharacters(chain.value.table.value).." -P "..format.escapespecialcharacters(chain.value.chain.value).." "..format.escapespecialcharacters(chain.value.policy.value).." 2>&1" local f = io.popen(cmd) local errtxt = f:read("*a") f:close() if errtxt ~= "" then chain.errtxt = errtxt end save() end else chain.errtxt = "Failed to update chain" end return chain end function create_chain(self, chain) local success = true getdetails() if not details[chain.value.table.value] then chain.value.table.errtxt = "Invalid table" success = false elseif find_chain(chain.value.table.value, chain.value.chain.value) then chain.value.chain.errtxt = "Chain already exists" success = false end if string.find(chain.value.chain.value, "[%s\'\"]") then chain.value.chain.errtxt = "Cannot contain spaces or quotes" success = false end if success then local cmd = path .. "iptables -t "..format.escapespecialcharacters(chain.value.table.value).." -N "..format.escapespecialcharacters(chain.value.chain.value).." 2>&1" local f = io.popen(cmd) local errtxt = f:read("*a") if errtxt ~= "" then chain.errtxt = errtxt end f:close() save() else chain.errtxt = "Failed to create chain" end return chain end function get_delete_chain(self, clientdata) local retval = {} retval.table = cfe({ type="select", value=clientdata.table or "filter", label="Table", option=tables }) retval.chain = cfe({ value=clientdata.chain or "", label="Chain" }) return cfe({ type="group", value=retval, label="Delete Chain" }) end function delete_chain(self, chain) local chn = find_chain(chain.value.table.value, chain.value.chain.value) if not chn then chain.errtxt = "Could not find chain" elseif chn.policy then chain.errtxt = "Cannot delete built-in chain" elseif chn.references and tonumber(chn.references) > 0 then chain.errtxt = "Cannot delete chain with references" else local cmd = path .. "iptables -t "..format.escapespecialcharacters(chain.value.table.value).." -X "..format.escapespecialcharacters(chain.value.chain.value).." 2>&1" local f = io.popen(cmd) local errtxt = f:read("*a") if errtxt ~= "" then chain.errtxt = errtxt end save() end return chain end function read_rule(tab, chain, pos) local retval = {} -- Identification retval.table = cfe({ type="select", value=tab or "filter", label="Table", option=tables }) retval.chain = cfe({ value=chain or "", label="Chain" }) retval.position = cfe({ value=pos or "", label="Position" }) -- Basics retval.protocol = cfe({ value="all", label="Protocol", descr="One of tcp, udp, icmp, or all, or a numeric value representing a protocol, or a protocol name from /etc/protocols. A '!' before the protocol inverts the test." }) retval.source = cfe({ label="Source", descr="A network name or IP address (may have mask of type /xxx.xxx.xxx.xxx or /xx). A '!' before the address specification inverts the sense of the address." }) retval.destination = cfe({ label="Destination", descr="A network name or IP address (may have mask of type /xxx.xxx.xxx.xxx or /xx). A '!' before the address specification inverts the sense of the address." }) retval.jump = cfe({ label="Target", descr="Specify the target of the rule - one of ACCEPT, DROP, QUEUE, or RETURN, or the name of a user-defined chain." }) retval.goto = cfe({ label="Goto", descr="Processing should continue in the specified chain" }) retval.in_interface = cfe({ label="In Interface", descr="Name of an interface via which a packet was received. A '!' before the interface inverts the sense. A '+' ending the interface will match any interface that begins with this name." }) retval.out_interface = cfe({ label="Out Interface", descr="Name of an interface via which a packet is going to be sent. A '!' before the interface inverts the sense. A '+' ending the interface will match any interface that begins with this name." }) retval.fragment = cfe({ label="Fragment", descr="A '+' specifies the second and further packets of fragmented packets. A '!' specifies only head fragments or unfragmented packets." }) retval.set_counters = cfe({ label="Set Counters", descr="'Number number' to initialize the packet and byte counters."}) -- Extensions retval.addrtype_src_type = cfe({ type="select", label="Source Address Type", option={"", "UNSPEC", "UNICAST", "LOCAL", "BROADCAST", "ANYCAST", "MULTICAST", "BLACKHOLE", "UNREACHABLE", "PROHIBIT"} }) retval.addrtype_dst_type = cfe({ type="select", label="Destination Address Type", option={"", "UNSPEC", "UNICAST", "LOCAL", "BROADCAST", "ANYCAST", "MULTICAST", "BLACKHOLE", "UNREACHABLE", "PROHIBIT"} }) retval.comment = cfe({ label="Comment" }) retval.icmp_type = cfe({ label="ICMP Type", descr="Type by name or number. A '!' before the type inverts the test." }) retval.src_range = cfe({ label="Source IP Range", descr="'ip-ip' to match source IP. A '!' before the range inverts the test." }) retval.dst_range = cfe({ label="Destination IP Range", descr="'ip-ip' to match destination IP. A '!' before the range inverts the test." }) retval.mac_source = cfe({ label="Source MAC address", descr="'XX:XX:XX:XX:XX:XX' to match the ethernet source MAC. A '!' before the address inverts the test." }) retval.sports = cfe({ label="Source Ports", descr="Up to 15 comma-separated ports can be specified. Port ranges (port:port) count as two ports. A '!' before the port definition inverts the test." }) retval.dports = cfe({ label="Destination Ports", descr="Up to 15 comma-separated ports can be specified. Port ranges (port:port) count as two ports. A '!' before the port definition inverts the test." }) retval.ports = cfe({ label="Ports", descr="Up to 15 comma-separated ports can be specified. Port ranges (port:port) count as two ports. A '!' before the port definition inverts the test." }) retval.state = cfe({ label="State", descr="Comma-separated list of states to match. Possible states are INVALID, ESTABLISHED, NEW, and RELATED." }) retval.tcp_sport = cfe({ label="TCP Source Port", descr="A port or port range (port:port) specification. This can either be a service name or port number. A '!' before the port definition inverts the test." }) retval.tcp_dport = cfe({ label="TCP Destination Port", descr="A port or port range (port:port) specification. This can either be a service name or port number. A '!' before the port definition inverts the test." }) retval.udp_sport = cfe({ label="UDP Source Port", descr="A port or port range (port:port) specification. This can either be a service name or port number. A '!' before the port definition inverts the test." }) retval.udp_dport = cfe({ label="UDP Destination Port", descr="A port or port range (port:port) specification. This can either be a service name or port number. A '!' before the port definition inverts the test." }) getdetails() if tab and not details[tab] then retval.table.errtxt = "Invalid table" end local chn if chain then chn = find_chain(retval.table.value, chain) if not chn then retval.chain.errtxt = "Cannot find chain" end end if pos and chn then if not chn[tonumber(pos)] then retval.position.errtxt = "Cannot find rule" else -- We found the rule, update the settings local words = {} for word in string.gmatch(chn[tonumber(pos)].rule, "%S+") do words[#words + 1] = word end -- the target might be missing, so we need to check if #words[2] == 2 and string.match(words[2], "^[%-!]") then table.insert(words, 1, "") end retval.jump.value = words[1] or "" retval.protocol.value = words[2] or "" if words[3] == "-f" then retval.fragment.value = "+" elseif words[3] == "!f" then retval.fragment.value = "!" end retval.in_interface.value = words[4] or "" if retval.in_interface.value == "*" then retval.in_interface.value = "" end retval.out_interface.value = words[5] or "" if retval.out_interface.value == "*" then retval.out_interface.value = "" end retval.source.value = words[6] or "" retval.destination.value = words[7] or "" local i=8 while i <= #words do if words[i] == "[goto]" then retval.goto.value = retval.jump.value retval.jump.value = "" elseif words[i] == "src-type" then retval.addrtype_src_type.value = words[i+1] i = i+1 elseif words[i] == "dst-type" then retval.addrtype_dst_type.value = words[i+1] i = i+1 elseif words[i] == "icmptype" then retval.icmp_type.value = words[i+1] i = i+1 elseif words[i] == "icmp" then retval.icmp_type.value = "!" .. words[i+2] i = i+2 elseif words[i] == "source" and words[i+1] == "IP" and words[i+2] == "range" then if words[i+3] == "!" then retval.src_range.value = "!" .. words[i+4] i = i+4 else retval.src_range.value = words[i+3] i = i+3 end elseif words[i] == "destination" and words[i+1] == "IP" and words[i+2] == "range" then if words[i+3] == "!" then retval.dst_range.value = "!" .. words[i+4] i = i+4 else retval.dst_range.value = words[i+3] i = i+3 end elseif words[i] == "MAC" then if words[i+1] == "!" then retval.mac_source.value = "!" .. words[i+2] i = i+2 else retval.mac_source.value = words[i+1] i = i+1 end elseif words[i] == "multiport" then if words[i+2] == "!" then retval[words[i+1]].value = "!" .. words[i+3] i = i+3 else retval[words[i+1]].value = words[i+2] i = i+2 end elseif words[i] == "state" then retval.state.value = words[i+1] i = i+1 elseif words[i] == "tcp" then while words[i+1] do if string.match(words[i+1], "^spt") then retval.tcp_sport.value = string.match(words[i+1], ":(.+)$") i = i+1 elseif string.match(words[i+1], "^dpt") then retval.tcp_dport.value = string.match(words[i+1], ":(.+)$") i = i+1 else break end end elseif words[i] == "udp" then while words[i+1] do if string.match(words[i+1], "^spt") then retval.udp_sport.value = string.match(words[i+1], ":(.+)$") i = i+1 elseif string.match(words[i+1], "^dpt") then retval.udp_dport.value = string.match(words[i+1], ":(.+)$") i = i+1 else break end end end i = i+1 end retval.comment.value = string.match(chn[tonumber(pos)].rule, "/%*%s+(.*)%s%*/") or "" end end return cfe({ type="group", value=retval, label="Rule" }) end function create_rule(self, rule) local success, rule = validate_rule(rule) if success then local spec = generate_rule_specification(rule) local cmd = path .. "iptables -t " .. format.escapespecialcharacters(rule.value.table.value) if tonumber(rule.value.position.value) then cmd = cmd .. " -I " .. format.escapespecialcharacters(rule.value.chain.value) .. " " .. format.escapespecialcharacters(rule.value.position.value) else cmd = cmd .. " -A " .. format.escapespecialcharacters(rule.value.chain.value) end cmd = cmd .. " " .. spec .. " 2>&1" local f = io.popen(cmd) rule.errtxt = f:read("*a") f:close() if string.match(rule.errtxt, "^%s*$") then rule.errtxt = nil end else rule.errtxt = "Failed to create rule" end return rule end function update_rule(self, rule) local success, rule = validate_rule(rule) if not tonumber(rule.value.position.value) then rule.value.position.errtxt = "Must be a number" success = false end if success then local spec = generate_rule_specification(rule) local cmd = path .. "iptables -t " .. format.escapespecialcharacters(rule.value.table.value) .. " -R " .. format.escapespecialcharacters(rule.value.chain.value) .. " " .. format.escapespecialcharacters(rule.value.position.value) .. " " .. spec .. " 2>&1" local f = io.popen(cmd) rule.errtxt = f:read("*a") f:close() if string.match(rule.errtxt, "^%s*$") then rule.errtxt = nil end else rule.errtxt = "Failed to update rule" end return rule end function get_delete_rule(self, clientdata) local retval = {} -- Identification retval.table = cfe({ type="select", value=clientdata.table or "filter", label="Table", option=tables }) retval.chain = cfe({ value=clientdata.chain or "", label="Chain" }) retval.position = cfe({ value=clientdata.position or "", label="Position" }) return cfe({ type="group", value=retval, label="Delete Rule" }) end function delete_rule(self, rule) if "" == rule.value.table.value or "" == rule.value.chain.value or "" == rule.value.position.value then rule.errtxt = "Incomplete specification - must define table, chain, and position" else local cmd = path .. "iptables -t " .. format.escapespecialcharacters(rule.value.table.value) .. " -D " .. format.escapespecialcharacters(rule.value.chain.value) .. " " .. format.escapespecialcharacters(rule.value.position.value) local f = io.popen(cmd) rule.errtxt = f:read("*a") f:close() if string.match(rule.errtxt, "^%s*$") then rule.errtxt = nil end end return rule end function readrulesfile() local rulesfile = format.get_ini_entry(fs.read_file(configfile) or "", "", "IPTABLES_SAVE") return modelfunctions.getfiledetails(rulesfile) end function updaterulesfile(self, filedetails) local rulesfile = format.get_ini_entry(fs.read_file(configfile) or "", "", "IPTABLES_SAVE") return modelfunctions.setfiledetails(self, filedetails, {rulesfile}) end function get_startstop(self, clientdata) return modelfunctions.get_startstop(servicename) end function startstop_service(self, startstop, action) return modelfunctions.startstop_service(startstop, action) end