filter.lua 7.42 KB
Newer Older
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
1 2
--[[
Filter module for Alpine Wall
3
Copyright (C) 2012-2013 Kaarle Ritvanen
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
4 5 6 7 8 9
Licensed under the terms of GPL2
]]--


module(..., package.seeall)

10 11 12
local resolve = require('awall.host').resolve
local model = require('awall.model')
local combinations = require('awall.optfrag').combinations
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
13

14 15
local util = require('awall.util')
local extend = util.extend
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
16

17 18
local RECENT_MAX_COUNT = 20

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
19

20
local Log = model.class(model.ConfigObject)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
21

22
function Log:optfrag()
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
   local optmap = {
      log={level='level', prefix='prefix'},
      nflog={
	 group='group',
	 prefix='prefix',
	 range='range',
	 threshold='threshold'
      },
      ulog={
	 group='nlgroup',
	 prefix='prefix',
	 range='cprange',
	 threshold='qthreshold'
      }
   }

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
39
   local mode = self.mode or 'log'
40 41 42 43 44 45 46 47
   if not optmap[mode] then self:error('Invalid logging mode: '..mode) end

   local target = string.upper(mode)

   for s, t in pairs(optmap[mode]) do
      if self[s] then target = target..' --'..mode..'-'..t..' '..self[s] end
   end

48 49
   return {
      opts=self.limit and '-m limit --limit '..self.limit..'/second',
50
      target=target
51
   }
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
52
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
53

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
54 55 56

local Filter = model.class(model.Rule)

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
function Filter:init(...)
   model.Rule.init(self, unpack(arg))

   -- alpine v2.4 compatibility
   if util.contains({'logdrop', 'logreject'}, self.action) then
      self:warning('Deprecated action: '..self.action)
      self.action = string.sub(self.action, 4, -1)
   end

   local function log(spec, default)
      if spec == nil then spec = default end
      if spec == false then return end
      if spec == true then spec = '_default' end
      return self.root.log[spec] or self:error('Invalid log: '..spec)
   end

   self.log = log(self.log, self.action ~= 'accept')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
74

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
75
   local limit = self:limit()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
76 77 78 79 80 81
   if limit then
      if type(self[limit]) ~= 'table' then
	 self[limit] = {count=self[limit]}
      end
      self[limit].log = log(self[limit].log, true)
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
82 83
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
84 85 86 87
function Filter:destoptfrags()
   local ofrags = model.Rule.destoptfrags(self)
   if not self.dnat then return ofrags end

88
   ofrags = combinations(ofrags, {{family='inet6'}})
89
   local natof = self:create(model.Zone, {addr=self.dnat}):optfrags('out')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
90 91 92 93 94 95 96 97
   assert(#natof == 1)
   table.insert(ofrags, natof[1])
   return ofrags
end

function Filter:trules()
   local res = {}

98 99 100 101 102 103
   local function extrarules(cls, extra)
      local params = {}
      for i, attr in ipairs({'in', 'out', 'src', 'dest',
			     'ipset', 'ipsec', 'service'}) do
	 params[attr] = self[attr]
      end
104
      util.update(params, extra)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
105
      return extend(res, self:create(cls, params):trules())
106 107
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
108
   if self.dnat then
109 110 111
      if self.action ~= 'accept' then
	 self:error('dnat option not allowed with '..self.action..' action')
      end
112 113 114
      if self['no-track'] then
	 self:error('dnat option not allowed with no-track')
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
115
      if not self.dest then
116
	 self:error('Destination address must be specified with DNAT')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
117 118
      end
      if string.find(self.dnat, '/') then
119
	 self:error('DNAT target cannot be a network address')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
120 121 122
      end
      for i, attr in ipairs({'ipsec', 'ipset'}) do
	 if self[attr] then
123
	    self:error('dnat and '..attr..' options cannot be used simultaneously')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
124 125 126 127
	 end
      end

      local dnataddr
128
      for i, addr in ipairs(resolve(self.dnat, self)) do
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
129 130
	 if addr[1] == 'inet' then
	    if dnataddr then
131
	       self:error(self.dnat..' resolves to multiple IPv4 addresses')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
132 133 134 135 136
	    end
	    dnataddr = addr[2]
	 end
      end
      if not dnataddr then
137
	 self:error(self.dnat..' does not resolve to any IPv4 address')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
138 139
      end

140
      extrarules('dnat', {['to-addr']=dnataddr, out=nil})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
141 142
   end

143 144 145
   if self.action == 'tarpit' or self['no-track'] then
      extrarules('no-track')
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
146

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
147
   extend(res, model.Rule.trules(self))
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
148

149 150 151 152 153
   if self['no-track'] and self.action == 'accept' then
      extrarules('no-track', {reverse=true})
      extrarules('filter', {reverse=true, action='accept', log=false})
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
154 155 156
   return res
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
157 158 159 160 161
function Filter:limit()
   local res
   for i, limit in ipairs({'conn-limit', 'flow-limit'}) do
      if self[limit] then
	 if res then
162
	    self:error('Cannot specify multiple limits for a single filter rule')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
163 164 165 166 167 168 169 170 171 172 173 174
	 end
	 res = limit
      end
   end
   return res
end

function Filter:position()
   return self:limit() == 'flow-limit' and 'prepend' or 'append'
end

function Filter:target()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
175 176 177
   if self:limit() then return self:newchain('limit') end
   if self.log then return self:newchain('log'..self.action) end
   return model.Rule.target(self)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
178 179 180 181
end

function Filter:extraoptfrags()
   local res = {}
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
182

183 184 185 186 187 188 189 190
   local function logchain(log, action, target)
      if not log then return target end
      local chain = self:newchain('log'..action)
      extend(
	 res,
	 combinations({{chain=chain}}, {log:optfrag(), {target=target}})
      )
      return chain
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
191 192
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
193 194 195
   local limit = self:limit()
   if limit then
      if self.action ~= 'accept' then
196
	 self:error('Cannot specify limit for '..self.action..' filter')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
197
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
198

199
      local chain = self:newchain('limit')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
200
      local limitlog = self[limit].log
201
      local count = self[limit].count
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
202
      local interval = self[limit].interval or 1
203 204 205 206 207 208 209 210 211

      if count > RECENT_MAX_COUNT then
	 count = math.ceil(count / interval)
	 interval = 1
      end

      local ofrags
      if count > RECENT_MAX_COUNT then
	 ofrags = {
212 213 214 215 216
	    {
	       opts='-m limit --limit '..count..'/second',
	       target=logchain(self.log, 'accept', 'ACCEPT')
	    },
	    {target='DROP'}
217
	 }
218
	 if limitlog then table.insert(ofrags, 2, limitlog:optfrag()) end
219 220 221 222 223 224
      else
	 ofrags = combinations(
	    {{opts='-m recent --name '..chain}},
	    {
	       {
		  opts='--update --hitcount '..count..' --seconds '..interval,
225
		  target=logchain(limitlog, 'drop', 'DROP')
226
	       },
227
	       {opts='--set', target='ACCEPT'}
228 229
	    }
	 )
230
	 if self.log then table.insert(ofrags, 2, self.log:optfrag()) end
231
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
232

233
      extend(res, combinations({{chain=chain}}, ofrags))
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
234

235
   else logchain(self.log, self.action, model.Rule.target(self)) end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
236
   
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
237 238 239 240 241 242 243 244 245 246
   return res
end



local Policy = model.class(Filter)

function Policy:servoptfrags() return nil end


247 248 249
local fchains = {{chain='FORWARD'}, {chain='INPUT'}, {chain='OUTPUT'}}

local dar = combinations(fchains,
250
			 {{opts='-m conntrack --ctstate RELATED,ESTABLISHED'}})
251 252 253 254 255
for i, chain in ipairs({'INPUT', 'OUTPUT'}) do
   table.insert(dar,
		{chain=chain,
		 opts='-'..string.lower(string.sub(chain, 1, 1))..' lo'})
end
256 257 258 259 260
dar = combinations(
   dar,
   {{table='filter', target='ACCEPT'}},
   {{family='inet'}, {family='inet6'}}
)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
261

262 263
local icmp = {{family='inet', table='filter', opts='-p icmp'}}
local icmp6 = {{family='inet6', table='filter', opts='-p icmpv6'}}
264 265 266 267 268 269 270
local ir = combinations(
   icmp6,
   {{chain='INPUT'}, {chain='OUTPUT'}},
   {{target='ACCEPT'}}
)
extend(ir, combinations(icmp6, {{chain='FORWARD', target='icmp-routing'}}))
extend(ir, combinations(icmp, fchains, {{target='icmp-routing'}}))
271 272

local function icmprules(ofrag, oname, types)
273 274 275 276 277 278 279 280 281
   extend(
      ir,
      combinations(ofrag,
		   {{chain='icmp-routing', target='ACCEPT'}},
		   util.map(types,
			    function(t)
			       return {opts='--'..oname..' '..t}
			    end))
   )
282 283 284
end
icmprules(icmp, 'icmp-type', {3, 11, 12})
icmprules(icmp6, 'icmpv6-type', {1, 2, 3, 4})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
285

286 287 288 289 290 291 292 293
export = {
   filter={class=Filter, before={'dnat', 'no-track'}},
   log={class=Log},
   policy={class=Policy, after='%filter-after'},
   ['%filter-before']={rules=dar, before='filter'},
   ['%filter-after']={rules=ir, after='filter'}
}

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
294 295 296
achains = combinations({{chain='tarpit'}},
		       {{opts='-p tcp', target='TARPIT'},
			{target='DROP'}})
297