diff options
author | Kaarle Ritvanen <kaarle.ritvanen@datakunkku.fi> | 2012-02-16 11:18:24 +0000 |
---|---|---|
committer | Kaarle Ritvanen <kaarle.ritvanen@datakunkku.fi> | 2012-02-16 11:18:24 +0000 |
commit | 7a202674863e83b390935709d41e1cece959385a (patch) | |
tree | 0e3b442dbbbe1f049e1fbbdbfbff10b8585698c9 | |
download | awall-7a202674863e83b390935709d41e1cece959385a.tar.bz2 awall-7a202674863e83b390935709d41e1cece959385a.tar.xz |
initial version
-rwxr-xr-x | awall-cli | 12 | ||||
l--------- | awall.lua | 1 | ||||
-rw-r--r-- | awall/host.lua | 52 | ||||
-rw-r--r-- | awall/init.lua | 62 | ||||
-rw-r--r-- | awall/iptables.lua | 38 | ||||
-rw-r--r-- | awall/model.lua | 346 | ||||
-rw-r--r-- | awall/modules/filter.lua | 81 | ||||
-rw-r--r-- | awall/modules/nat.lua | 82 | ||||
-rw-r--r-- | awall/optfrag.lua | 48 | ||||
-rw-r--r-- | awall/util.lua | 33 |
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 |