Commit 7a202674 authored by Kaarle Ritvanen's avatar Kaarle Ritvanen

initial version

parents
#!/usr/bin/lua
--[[
Alpine Wall
Copyright (C) 2012 Kaarle Ritvanen
Licensed under the terms of GPL2
]]--
require 'awall'
awall.translate()
awall/init.lua
\ No newline at end of file
--[[
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
--[[
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
--[[
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
--[[
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 = {}
--[[
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
--[[
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'}}
--[[
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
--[[
Utility module for Alpine Wall
Copyright (C) 2012 Kaarle Ritvanen
Licensed under the terms of GPL2
]]--
module(..., package.seeall)