aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKaarle Ritvanen <kaarle.ritvanen@datakunkku.fi>2012-02-16 11:18:24 +0000
committerKaarle Ritvanen <kaarle.ritvanen@datakunkku.fi>2012-02-16 11:18:24 +0000
commit7a202674863e83b390935709d41e1cece959385a (patch)
tree0e3b442dbbbe1f049e1fbbdbfbff10b8585698c9
downloadawall-7a202674863e83b390935709d41e1cece959385a.tar.bz2
awall-7a202674863e83b390935709d41e1cece959385a.tar.xz
initial version
-rwxr-xr-xawall-cli12
l---------awall.lua1
-rw-r--r--awall/host.lua52
-rw-r--r--awall/init.lua62
-rw-r--r--awall/iptables.lua38
-rw-r--r--awall/model.lua346
-rw-r--r--awall/modules/filter.lua81
-rw-r--r--awall/modules/nat.lua82
-rw-r--r--awall/optfrag.lua48
-rw-r--r--awall/util.lua33
10 files changed, 755 insertions, 0 deletions
diff --git a/awall-cli b/awall-cli
new file mode 100755
index 0000000..f6a3c3f
--- /dev/null
+++ b/awall-cli
@@ -0,0 +1,12 @@
+#!/usr/bin/lua
+
+--[[
+Alpine Wall
+Copyright (C) 2012 Kaarle Ritvanen
+Licensed under the terms of GPL2
+]]--
+
+
+require 'awall'
+
+awall.translate()
diff --git a/awall.lua b/awall.lua
new file mode 120000
index 0000000..9a3d9cc
--- /dev/null
+++ b/awall.lua
@@ -0,0 +1 @@
+awall/init.lua \ No newline at end of file
diff --git a/awall/host.lua b/awall/host.lua
new file mode 100644
index 0000000..e413986
--- /dev/null
+++ b/awall/host.lua
@@ -0,0 +1,52 @@
+--[[
+Host address resolver for Alpine Wall
+Copyright (C) 2012 Kaarle Ritvanen
+Licensed under the terms of GPL2
+]]--
+
+
+module(..., package.seeall)
+
+local familypatterns = {ip4='%d[%.%d/]+',
+ ip6='[:%x/]+',
+ domain='[%a-][%.%w-]*'}
+
+local function getfamily(addr)
+ for k, v in pairs(familypatterns) do
+ if string.match(addr, '^'..v..'$') then return k end
+ end
+ error('Malformed host specification: '..addr)
+end
+
+local dnscache = {}
+
+function resolve(host)
+ local family = getfamily(host)
+ if family == 'domain' then
+
+ if not dnscache[host] then
+ dnscache[host] = {}
+ -- TODO use default server
+ for rec in io.popen('dig @8.8.8.8 '..host..' ANY'):lines() do
+ local name, rtype, addr =
+ string.match(rec, '^('..familypatterns.domain..')\t+%d+\t+IN\t+(A+)\t+(.+)')
+
+ if name and string.sub(name, 1, string.len(host) + 1) == host..'.' then
+ if rtype == 'A' then family = 'ip4'
+ elseif rtype == 'AAAA' then family = 'ip6'
+ else family = nil end
+
+ if family then
+ assert(getfamily(addr) == family)
+ table.insert(dnscache[host], {family, addr})
+ end
+ end
+ end
+ if not dnscache[host][1] then error('Invalid host name: '..host) end
+ end
+
+ return dnscache[host]
+ end
+
+ return {{family, host}}
+end
diff --git a/awall/init.lua b/awall/init.lua
new file mode 100644
index 0000000..d083507
--- /dev/null
+++ b/awall/init.lua
@@ -0,0 +1,62 @@
+--[[
+Alpine Wall main module
+Copyright (C) 2012 Kaarle Ritvanen
+Licensed under the terms of GPL2
+]]--
+
+module(..., package.seeall)
+
+require 'json'
+
+require 'awall.iptables'
+require 'awall.util'
+
+
+local modules = {}
+
+local modpath = arg[0] == '/usr/sbin/awall' and '/usr/share/lua/5.1' or '.'
+for line in io.popen('cd '..modpath..' && ls awall/model.lua awall/modules/*.lua'):lines() do
+ local name = string.gsub(string.sub(line, 1, -5), '/', '.')
+ require(name)
+ table.insert(modules, package.loaded[name])
+end
+
+
+function translate()
+
+ local data = ''
+ for line in io.lines('config.json') do data = data..line end
+ config = json.decode(data)
+
+ function insertrule(trule)
+ local t = awall.iptables.config[trule.family][trule.table][trule.chain]
+ if trule.position == 'prepend' then
+ table.insert(t, 1, trule.opts)
+ else
+ table.insert(t, trule.opts)
+ end
+ end
+
+ local locations = {}
+
+ for i, mod in ipairs(modules) do
+ for path, cls in pairs(mod.classmap) do
+ if config[path] then
+ awall.util.map(config[path], cls.morph)
+ table.insert(locations, config[path])
+ end
+ end
+
+ for i, rule in ipairs(mod.defrules) do insertrule(rule) end
+ end
+
+
+ for i, location in ipairs(locations) do
+ for i, rule in ipairs(location) do
+ for i, trule in ipairs(rule:trules()) do insertrule(trule) end
+ end
+ end
+
+ awall.iptables.dump()
+
+end
diff --git a/awall/iptables.lua b/awall/iptables.lua
new file mode 100644
index 0000000..bade70c
--- /dev/null
+++ b/awall/iptables.lua
@@ -0,0 +1,38 @@
+--[[
+Iptables file dumper for Alpine Wall
+Copyright (C) 2012 Kaarle Ritvanen
+Licensed under the terms of GPL2
+]]--
+
+
+module(..., package.seeall)
+
+local iptfiles = {ip4='iptables', ip6='ip6tables'}
+
+config = {}
+setmetatable(config,
+ {__index=function(t, k)
+ t[k] = {}
+ setmetatable(t[k], getmetatable(t))
+ return t[k]
+ end})
+
+function dump()
+ for family, tbls in pairs(config) do
+ local iptfile = io.output('output/'..iptfiles[family])
+ iptfile:write('# '..iptfiles[family]..' generated by awall\n')
+ for tbl, chains in pairs(tbls) do
+ iptfile:write('*'..tbl..'\n')
+ for chain, rules in pairs(chains) do
+ iptfile:write(':'..chain..' '..(chain == string.upper(chain) and
+ 'DROP' or '-')..' [0:0]\n')
+ end
+ for chain, rules in pairs(chains) do
+ for i, rule in ipairs(rules) do
+ iptfile:write('-A '..chain..' '..rule..'\n')
+ end
+ end
+ iptfile:write('COMMIT\n')
+ end
+ end
+end
diff --git a/awall/model.lua b/awall/model.lua
new file mode 100644
index 0000000..43da388
--- /dev/null
+++ b/awall/model.lua
@@ -0,0 +1,346 @@
+--[[
+Base data model for Alpine Wall
+Copyright (C) 2012 Kaarle Ritvanen
+Licensed under the terms of GPL2
+]]--
+
+
+module(..., package.seeall)
+
+require 'awall'
+require 'awall.host'
+require 'awall.util'
+require 'awall.optfrag'
+
+local util = awall.util
+local combinations = awall.optfrag.combinations
+
+
+local lastid = -1
+function newchain()
+ lastid = lastid + 1
+ return 'awall-'..lastid
+end
+
+
+function class(base)
+ local cls = {}
+ local mt = {__index = cls}
+
+ if base then setmetatable(cls, {__index = base}) end
+
+ function cls.new(...)
+ local inst = arg[1] and arg[1] or {}
+ cls.morph(inst)
+ return inst
+ end
+
+ function cls:morph()
+ setmetatable(self, mt)
+ self:init()
+ end
+
+ return cls
+end
+
+Object = class()
+function Object:init() end
+function Object:trules() return {} end
+
+
+Zone = class(Object)
+
+function Zone:optfrags(dir)
+ local iopt, aopt, iprop, aprop
+ if dir == 'in' then
+ iopt, aopt, iprop, aprop = 'i', 's', 'in', 'src'
+ elseif dir == 'out' then
+ iopt, aopt, iprop, aprop = 'o', 'd', 'out', 'dest'
+ else assert(false) end
+
+ -- TODO support for externally controlled ipsets
+
+ local aopts = {}
+ for i, hostdef in util.listpairs(self.addr) do
+ for i, addr in ipairs(awall.host.resolve(hostdef)) do
+ table.insert(aopts,
+ {family=addr[1],
+ [aprop]=addr[2],
+ opts='-'..aopt..' '..addr[2]})
+ end
+ end
+ if not aopts[1] then aopts = nil end
+
+ return combinations(util.maplist(self.iface,
+ function(x)
+ return {[iprop]=x,
+ opts='-'..iopt..' '..x}
+ end),
+ aopts)
+end
+
+
+fwzone = Zone.new()
+
+
+Rule = class(Object)
+
+
+function Rule:init()
+ local config = awall.config
+ for i, prop in ipairs({'in', 'out'}) do
+ self[prop] = self[prop] and util.maplist(self[prop],
+ function(z)
+ return z == '_fw' and fwzone or
+ config.zone[z] or
+ error('Invalid zone: '..z)
+ end) or self:defaultzones()
+ end
+ if self.service then
+ self.service = util.maplist(self.service,
+ function(s)
+ return config.service[s] or error('Invalid service: '..s)
+ end)
+ end
+end
+
+function Rule:defaultzones() return {nil, fwzone} end
+
+
+function Rule:checkzoneoptfrag(ofrag) end
+
+
+function Rule:zoneoptfrags()
+
+ function zonepair(zin, zout)
+ assert(zin ~= zout or not zin)
+
+ function zofs(zone, dir)
+ if not zone then return zone end
+ local ofrags = zone:optfrags(dir)
+ util.map(ofrags, function(x) self:checkzoneoptfrag(x) end)
+ return ofrags
+ end
+
+ local chain, ofrags
+
+ if zin == fwzone or zout == fwzone then
+ local dir, z = 'in', zin
+ if zin == fwzone then dir, z = 'out', zout end
+ chain = string.upper(dir)..'PUT'
+ ofrags = zofs(z, dir)
+
+ else
+ chain = 'FORWARD'
+ ofrags = combinations(zofs(zin, 'in'),
+ zofs(zout, 'out'))
+ end
+
+ if not ofrags then ofrags = {{}} end
+
+ for i, ofrag in ipairs(ofrags) do ofrag.fchain = chain end
+
+ return ofrags
+ end
+
+ local res = {}
+
+ for i = 1,math.max(1, table.maxn(self['in'])) do
+ izone = self['in'][i]
+ for i = 1,math.max(1, table.maxn(self.out)) do
+ ozone = self.out[i]
+ if izone ~= ozone or not izone then
+ for i, ofrags in ipairs(zonepair(izone, ozone)) do
+ table.insert(res, ofrags)
+ end
+ end
+ end
+ end
+
+ return res
+end
+
+
+function Rule:servoptfrags()
+
+ if not self.service then return end
+
+ function containskey(tbl, key)
+ for k, v in pairs(tbl) do if k == key then return true end end
+ return false
+ end
+
+ local ports = {}
+ local res = {}
+
+ for i, serv in ipairs(self.service) do
+ for i, sdef in util.listpairs(serv) do
+ if not sdef.proto then error('Protocol not defined') end
+
+ if util.contains({6, 'tcp', 17, 'udp'}, sdef.proto) then
+ local new = not containskey(ports, sdef.proto)
+ if new then ports[sdef.proto] = {} end
+
+ if new or ports[sdef.proto][1] then
+ if sdef.port then
+ for i, port in util.listpairs(sdef.port) do
+ table.insert(ports[sdef.proto], port)
+ end
+ else ports[sdef.proto] = {} end
+ end
+
+ else
+
+ local opts = '-p '..sdef.proto
+ local family = nil
+
+ if sdef.type then
+ -- TODO multiple ICMP types per rule
+ local oname
+ if util.contains({1, 'icmp'}, sdef.proto) then
+ family = 'ip4'
+ oname = 'icmp-type'
+ elseif util.contains({58, 'ipv6-icmp', 'icmpv6'}, sdef.proto) then
+ family = 'ip6'
+ oname = 'icmpv6-type'
+ else error('Type specification not valid with '..sdef.proto) end
+ opts = opts..' --'..oname..' '..sdef.type
+ end
+
+ table.insert(res, {family=family, opts=opts})
+ end
+ end
+ end
+
+ for proto, plist in pairs(ports) do
+ local opts = '-p '..proto
+ local len = table.maxn(plist)
+
+ if len == 1 then
+ opts = opts..' --dport '..plist[1]
+ elseif len > 1 then
+ opts = opts..' -m multiport --dports '
+ for i, port in ipairs(plist) do
+ if i > 1 then opts = opts..',' end
+ opts = opts..port
+ end
+ end
+
+ table.insert(res, {opts=opts})
+ end
+
+ return res
+end
+
+function Rule:table() return 'filter' end
+
+function Rule:chain() return nil end
+
+function Rule:position() return 'append' end
+
+function Rule:target()
+ if not self.action then error('Action not defined') end
+ return string.upper(self.action)
+end
+
+
+function Rule:trules()
+
+ function tag(ofrags, tag, value)
+ for i, ofrag in ipairs(ofrags) do
+ assert(not ofrag[tag])
+ ofrag[tag] = value
+ end
+ end
+
+ local families
+
+ function setfamilies(ofrags)
+ if ofrags then
+ families = {}
+ for i, ofrag in ipairs(ofrags) do
+ if not ofrag.family then
+ families = nil
+ return
+ end
+ table.insert(families, ofrag.family)
+ end
+ else families = nil end
+ end
+
+ function ffilter(ofrags)
+ if not ofrags or not ofrags[1] or not families then return ofrags end
+ local res = {}
+ for i, ofrag in util.listpairs(ofrags) do
+ if not ofrag.family or util.contains(families, ofrag.family) then
+ table.insert(res, ofrag)
+ end
+ end
+ return res
+ end
+
+ function appendtarget(ofrag, target)
+ ofrag.opts = (ofrag.opts and ofrag.opts..' ' or '')..'-j '..target
+ end
+
+ local res = self:zoneoptfrags()
+
+ if self.ipsec == 'true' then
+ res = combinations(res, {{opts='-m policy --pol ipsec'}})
+ end
+
+ res = combinations(res, self:servoptfrags())
+
+ setfamilies(res)
+ tag(res, 'chain', self:chain())
+
+ local addrofrags = combinations(Zone.new({addr=self.src}):optfrags('in'),
+ Zone.new({addr=self.dest}):optfrags('out'))
+
+ if addrofrags then
+ addrofrags = ffilter(addrofrags)
+ setfamilies(addrofrags)
+ res = ffilter(res)
+ end
+
+ local addrchain = false
+ for i, ofrag in ipairs(res) do
+ if not ofrag.chain then ofrag.chain = ofrag.fchain end
+ addrchain = addrchain or (self.src and ofrag.src) or (self.dest and ofrag.dest)
+ end
+
+ local target
+ if addrchain then
+ target = newchain()
+ else
+ target = self:target()
+ if addrofrags then res = combinations(res, addrofrags) end
+ end
+
+ tag(res, 'position', self:position())
+
+ for i, ofrag in ipairs(res) do appendtarget(ofrag, target) end
+
+ if addrchain then
+ for i, ofrag in ipairs(addrofrags) do
+ ofrag.chain = target
+ appendtarget(ofrag, self:target())
+ table.insert(res, ofrag)
+ end
+ end
+
+ for i, ofrag in ipairs(ffilter(self:extraoptfrags())) do
+ table.insert(res, ofrag)
+ end
+
+ tag(res, 'table', self:table(), false)
+
+ return combinations(res, ffilter({{family='ip4'}, {family='ip6'}}))
+end
+
+function Rule:extraoptfrags() return {} end
+
+
+classmap = {zone=Zone}
+
+defrules = {}
diff --git a/awall/modules/filter.lua b/awall/modules/filter.lua
new file mode 100644
index 0000000..cc579f9
--- /dev/null
+++ b/awall/modules/filter.lua
@@ -0,0 +1,81 @@
+--[[
+Filter module for Alpine Wall
+Copyright (C) 2012 Kaarle Ritvanen
+Licensed under the terms of GPL2
+]]--
+
+
+module(..., package.seeall)
+
+require 'awall.model'
+local model = awall.model
+
+local Filter = model.class(model.Rule)
+
+function Filter:limit()
+ local res
+ for i, limit in ipairs({'conn-limit', 'flow-limit'}) do
+ if self[limit] then
+ if res then
+ error('Cannot specify multiple limits for a single filter rule')
+ end
+ res = limit
+ end
+ end
+ return res
+end
+
+function Filter:position()
+ return self:limit() == 'flow-limit' and 'prepend' or 'append'
+end
+
+function Filter:target()
+ if not self:limit() then return model.Rule.target(self) end
+ if not self['limit-target'] then self['limit-target'] = model.newchain() end
+ return self['limit-target']
+end
+
+function Filter:extraoptfrags()
+ local res = {}
+ local limit = self:limit()
+ if limit then
+ if self.action ~= 'accept' then
+ error('Cannot specify limit for '..self.action..' filter')
+ end
+ local optbase = '-m recent --name '..self:target()
+ table.insert(res, {chain=self:target(),
+ opts=optbase..' --update --hitcount '..self[limit].count..' --seconds '..self[limit].interval..' -j LOGDROP'})
+ table.insert(res, {chain=self:target(),
+ opts=optbase..' --set -j ACCEPT'})
+ end
+ return res
+end
+
+
+
+local Policy = model.class(Filter)
+
+function Policy:servoptfrags() return nil end
+
+
+classmap = {policy=Policy, filter=Filter}
+
+defrules = {}
+for i, family in ipairs({'ip4', 'ip6'}) do
+ for i, target in ipairs({'DROP', 'REJECT'}) do
+ for i, opts in ipairs({'-m limit --limit 1/second -j LOG', '-j '..target}) do
+ table.insert(defrules,
+ {family=family,
+ table='filter',
+ chain='LOG'..target,
+ opts=opts})
+ end
+ end
+ for i, chain in ipairs({'FORWARD', 'INPUT'}) do
+ table.insert(defrules,
+ {family=family,
+ table='filter',
+ chain=chain,
+ opts='-m state --state RELATED,ESTABLISHED -j ACCEPT'})
+ end
+end
diff --git a/awall/modules/nat.lua b/awall/modules/nat.lua
new file mode 100644
index 0000000..4fae505
--- /dev/null
+++ b/awall/modules/nat.lua
@@ -0,0 +1,82 @@
+--[[
+NAT module for Alpine Wall
+Copyright (C) 2012 Kaarle Ritvanen
+Licensed under the terms of GPL2
+]]--
+
+
+module(..., package.seeall)
+
+require 'awall.model'
+require 'awall.util'
+
+local model = awall.model
+local util = awall.util
+
+
+local NATRule = model.class(model.Rule)
+
+function NATRule:init()
+ model.Rule.init(self)
+ if util.contains({self['in'], self.out}, fwzone) then
+ error('NAT rules not allowed for firewall zone')
+ end
+end
+
+function NATRule:defaultzones() return {nil} end
+
+function NATRule:checkzoneoptfrag(ofrag)
+ if ofrag[self.params.forbidif] then
+ error('Cannot specify '..self.params.forbidif..'bound interface for '..target..' rule')
+ end
+end
+
+function NATRule:trules()
+ local res = {}
+ for i, ofrags in ipairs(model.Rule.trules(self)) do
+ if ofrags.family == 'ip4' then table.insert(res, ofrags) end
+ end
+ return res
+end
+
+function NATRule:table() return 'nat' end
+
+function NATRule:chain() return self.params.chain end
+
+function NATRule:target()
+ if not self['ip-range'] then error('IP range not defined for NAT rule') end
+ local target = self.params.target..' --to-'..self.params.subject..' '..self['ip-range']
+ if self['port-range'] then target = target..':'..self['port-range'] end
+ return target
+end
+
+
+local DNATRule = model.class(NATRule)
+
+function DNATRule:init()
+ NATRule.init(self)
+ self.params = {forbidif='out', subject='destination',
+ chain='PREROUTING', target='DNAT'}
+end
+
+
+local SNATRule = model.class(NATRule)
+
+function SNATRule:init()
+ NATRule.init(self)
+ self.params = {forbidif='in', subject='source',
+ chain='POSTROUTING', target='SNAT'}
+end
+
+function SNATRule:target()
+ if self['ip-range'] then return NATRule.target(self) end
+ return 'MASQUERADE'..(self['port-range'] and ' --to-ports '..self['port-range'] or '')
+end
+
+
+classmap = {dnat=DNATRule, snat=SNATRule}
+
+-- TODO configuration of _nat ipset via config.json
+
+defrules = {{family='ip4', table='nat', chain='POSTROUTING',
+ opts='-m set --match-set _nat src ! --match-set _nat dst -j MASQUERADE'}}
diff --git a/awall/optfrag.lua b/awall/optfrag.lua
new file mode 100644
index 0000000..e46426e
--- /dev/null
+++ b/awall/optfrag.lua
@@ -0,0 +1,48 @@
+--[[
+Option fragment module for Alpine Wall
+Copyright (C) 2012 Kaarle Ritvanen
+Licensed under the terms of GPL2
+]]--
+
+
+module(..., package.seeall)
+
+function combinations(of1, of2)
+ if not of1 then
+ if not of2 then return nil end
+ return of2
+ end
+ if not of2 then return of1 end
+
+ local res = {}
+ for i, x in ipairs(of1) do
+ for i, y in ipairs(of2) do
+
+ local of = {}
+ for k, v in pairs(x) do
+ if k ~= 'opts' then of[k] = v end
+ end
+
+ local match = true
+ for k, v in pairs(y) do
+ if k ~= 'opts' then
+ if of[k] and v ~= of[k] then
+ match = false
+ break
+ end
+ of[k] = v
+ end
+ end
+
+ if match then
+ if x.opts then
+ if y.opts then of.opts = x.opts..' '..y.opts
+ else of.opts = x.opts end
+ else of.opts = y.opts end
+ table.insert(res, of)
+ end
+ end
+ end
+
+ return res[1] and res or nil
+end
diff --git a/awall/util.lua b/awall/util.lua
new file mode 100644
index 0000000..19a003d
--- /dev/null
+++ b/awall/util.lua
@@ -0,0 +1,33 @@
+--[[
+Utility module for Alpine Wall
+Copyright (C) 2012 Kaarle Ritvanen
+Licensed under the terms of GPL2
+]]--
+
+
+module(..., package.seeall)
+
+local function list(var)
+ if not var then return {} end
+ return type(var) == 'table' and var[1] and var or {var}
+end
+
+function listpairs(var)
+ return ipairs(list(var))
+end
+
+function map(var, func)
+ local res = {}
+ for k, v in pairs(var) do res[k] = func(v) end
+ return res
+end
+
+function maplist(var, func)
+ if not var then return var end
+ return map(list(var), func)
+end
+
+function contains(tbl, value)
+ for k, v in pairs(tbl) do if v == value then return true end end
+ return false
+end