--[[
Iptables file dumper for Alpine Wall
Copyright (C) 2012-2019 Kaarle Ritvanen
See LICENSE file for license details
]]--


local class = require('awall.class')
local ACTIVE = require('awall.family').ACTIVE
local raise = require('awall.uerror').raise

local util = require('awall.util')
local printmsg = util.printmsg
local sortedkeys = util.sortedkeys


local lpc = require('lpc')
local posix = require('posix')


local M = {}

local families = {inet={cmd='iptables',
			file='rules-save',
			procfile='/proc/net/ip_tables_names'},
		  inet6={cmd='ip6tables',
			 file='rules6-save',
			 procfile='/proc/net/ip6_tables_names'}}

M.builtin = {
   filter={'FORWARD', 'INPUT', 'OUTPUT'},
   mangle={'FORWARD', 'INPUT', 'OUTPUT', 'POSTROUTING', 'PREROUTING'},
   nat={'INPUT', 'OUTPUT', 'POSTROUTING', 'PREROUTING'},
   raw={'OUTPUT', 'PREROUTING'},
   security={'FORWARD', 'INPUT', 'OUTPUT'}
}

local backupdir = '/var/run/awall'


local _actfamilies
local function actfamilies()
   if _actfamilies then return _actfamilies end
   _actfamilies = {}
   for _, family in ipairs(ACTIVE) do
      if posix.stat(families[family].procfile) then
	 table.insert(_actfamilies, family)
      else printmsg('Warning: firewall not enabled for '..family) end
   end
   return _actfamilies
end

function M.isenabled() return #actfamilies() > 0 end


local BaseIPTables = class()

function BaseIPTables:print()
   for _, family in sortedkeys(families) do
      self:dumpfile(family, io.output())
      io.write('\n')
   end
end

function BaseIPTables:dump(dir)
   for family, tbls in pairs(families) do
      local file = io.open(dir..'/'..families[family].file, 'w')
      self:dumpfile(family, file)
      file:close()
   end
end

function BaseIPTables:restore(test)
   for _, family in ipairs(actfamilies()) do
      local pid, stdin, stdout = lpc.run(
	 families[family].cmd..'-restore', table.unpack{test and '-t' or nil}
      )
      stdout:close()
      self:dumpfile(family, stdin)
      stdin:close()
      assert(lpc.wait(pid) == 0)
   end
end

function BaseIPTables:activate()
   M.flush()
   self:restore(false)
end

function BaseIPTables:test() self:restore(true) end


M.IPTables = class(BaseIPTables)

function M.IPTables:init()
   self.config = {}
   setmetatable(self.config,
		{__index=function(t, k)
			    t[k] = {}
			    setmetatable(t[k], getmetatable(t))
			    return t[k]
			 end})
end

function M.IPTables:dumpfile(family, iptfile)
   iptfile:write('# '..families[family].file..' generated by awall\n')
   local tables = self.config[family]
   for i, tbl in sortedkeys(tables) do
      iptfile:write('*'..tbl..'\n')
      local chains = tables[tbl]
      for i, chain in sortedkeys(chains) do
	 local policy = '-'
	 if util.contains(M.builtin[tbl], chain) then
	    policy = tbl == 'filter' and 'DROP' or 'ACCEPT'
	 end
	 iptfile:write(':'..chain..' '..policy..' [0:0]\n')
      end
      for i, chain in sortedkeys(chains) do
	 for i, rule in ipairs(chains[chain]) do
	    iptfile:write('-A '..chain..' '..rule..'\n')
	 end
      end
      iptfile:write('COMMIT\n')
   end
end


local Current = class(BaseIPTables)

function Current:dumpfile(family, iptfile)
   local pid, stdin, stdout = lpc.run(families[family].cmd..'-save')
   stdin:close()
   for line in stdout:lines() do iptfile:write(line..'\n') end
   stdout:close()
   assert(lpc.wait(pid) == 0)
end


local Backup = class(BaseIPTables)

function Backup:dumpfile(family, iptfile)
   for line in io.lines(backupdir..'/'..families[family].file) do
      iptfile:write(line..'\n')
   end
end


function M.backup()
   posix.mkdir(backupdir)
   Current():dump(backupdir)
end

function M.revert() Backup():activate() end

function M.flush()
   local empty = M.IPTables()
   for _, family in pairs(actfamilies()) do
      for tbl in io.lines(families[family].procfile) do
	 if M.builtin[tbl] then
	    for _, chain in ipairs(M.builtin[tbl]) do
	       empty.config[family][tbl][chain] = {}
	    end
	 else printmsg('Warning: not flushing unknown table: '..tbl) end
      end
   end
   empty:restore(false)
end

return M
