filter.lua 7.89 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
   if not optmap[mode] then self:error('Invalid logging mode: '..mode) end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
   local selector, opts
   for i, sel in ipairs{'every', 'limit', 'probability'} do
      local value = self[sel]
      if value then
	 if selector then
	    self:error('Cannot combine '..sel..' with '..selector)
	 end
	 selector = sel

	 if sel == 'every' then
	    opts = '-m statistic --mode nth --every '..value..' --packet 0'
	 elseif sel == 'limit' then
	    opts = '-m limit --limit '..value..'/second'
	 elseif sel == 'probability' then
	    opts = '-m statistic --mode random --probability '..value
	 else assert(false) end
      end
   end

61 62 63 64 65 66
   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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
67
   return {opts=opts, target=target}
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
68
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
69

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
70 71 72

local Filter = model.class(model.Rule)

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
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
90

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
91
   local limit = self:limit()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
92 93 94 95 96 97
   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
98 99
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
100 101 102 103
function Filter:destoptfrags()
   local ofrags = model.Rule.destoptfrags(self)
   if not self.dnat then return ofrags end

104
   ofrags = combinations(ofrags, {{family='inet6'}})
105
   local natof = self:create(model.Zone, {addr=self.dnat}):optfrags('out')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
106 107 108 109 110 111 112 113
   assert(#natof == 1)
   table.insert(ofrags, natof[1])
   return ofrags
end

function Filter:trules()
   local res = {}

114 115 116 117 118 119
   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
120
      util.update(params, extra)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
121
      return extend(res, self:create(cls, params):trules())
122 123
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
124
   if self.dnat then
125 126 127
      if self.action ~= 'accept' then
	 self:error('dnat option not allowed with '..self.action..' action')
      end
128 129 130
      if self['no-track'] then
	 self:error('dnat option not allowed with no-track')
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
131
      if not self.dest then
132
	 self:error('Destination address must be specified with DNAT')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
133 134
      end
      if string.find(self.dnat, '/') then
135
	 self:error('DNAT target cannot be a network address')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
136 137 138
      end
      for i, attr in ipairs({'ipsec', 'ipset'}) do
	 if self[attr] then
139
	    self:error('dnat and '..attr..' options cannot be used simultaneously')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
140 141 142 143
	 end
      end

      local dnataddr
144
      for i, addr in ipairs(resolve(self.dnat, self)) do
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
145 146
	 if addr[1] == 'inet' then
	    if dnataddr then
147
	       self:error(self.dnat..' resolves to multiple IPv4 addresses')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
148 149 150 151 152
	    end
	    dnataddr = addr[2]
	 end
      end
      if not dnataddr then
153
	 self:error(self.dnat..' does not resolve to any IPv4 address')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
154 155
      end

156
      extrarules('dnat', {['to-addr']=dnataddr, out=nil})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
157 158
   end

159 160 161
   if self.action == 'tarpit' or self['no-track'] then
      extrarules('no-track')
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
162

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

165 166 167 168 169
   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
170 171 172
   return res
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
173 174 175 176 177
function Filter:limit()
   local res
   for i, limit in ipairs({'conn-limit', 'flow-limit'}) do
      if self[limit] then
	 if res then
178
	    self:error('Cannot specify multiple limits for a single filter rule')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
179 180 181 182 183 184 185 186 187 188 189 190
	 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
191 192 193
   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
194 195 196 197
end

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

199 200 201 202 203 204 205 206
   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
207 208
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
209 210 211
   local limit = self:limit()
   if limit then
      if self.action ~= 'accept' then
212
	 self:error('Cannot specify limit for '..self.action..' filter')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
213
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
214

215
      local chain = self:newchain('limit')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
216
      local limitlog = self[limit].log
217
      local count = self[limit].count
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
218
      local interval = self[limit].interval or 1
219 220 221 222 223 224 225 226 227

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

      local ofrags
      if count > RECENT_MAX_COUNT then
	 ofrags = {
228 229 230 231 232
	    {
	       opts='-m limit --limit '..count..'/second',
	       target=logchain(self.log, 'accept', 'ACCEPT')
	    },
	    {target='DROP'}
233
	 }
234
	 if limitlog then table.insert(ofrags, 2, limitlog:optfrag()) end
235 236 237 238 239 240
      else
	 ofrags = combinations(
	    {{opts='-m recent --name '..chain}},
	    {
	       {
		  opts='--update --hitcount '..count..' --seconds '..interval,
241
		  target=logchain(limitlog, 'drop', 'DROP')
242
	       },
243
	       {opts='--set', target='ACCEPT'}
244 245
	    }
	 )
246
	 if self.log then table.insert(ofrags, 2, self.log:optfrag()) end
247
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
248

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

251
   else logchain(self.log, self.action, model.Rule.target(self)) end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
252
   
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
253 254 255 256 257 258 259 260 261 262
   return res
end



local Policy = model.class(Filter)

function Policy:servoptfrags() return nil end


263 264 265
local fchains = {{chain='FORWARD'}, {chain='INPUT'}, {chain='OUTPUT'}}

local dar = combinations(fchains,
266
			 {{opts='-m conntrack --ctstate RELATED,ESTABLISHED'}})
267 268 269 270 271
for i, chain in ipairs({'INPUT', 'OUTPUT'}) do
   table.insert(dar,
		{chain=chain,
		 opts='-'..string.lower(string.sub(chain, 1, 1))..' lo'})
end
272 273 274 275 276
dar = combinations(
   dar,
   {{table='filter', target='ACCEPT'}},
   {{family='inet'}, {family='inet6'}}
)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
277

278 279
local icmp = {{family='inet', table='filter', opts='-p icmp'}}
local icmp6 = {{family='inet6', table='filter', opts='-p icmpv6'}}
280 281 282 283 284 285 286
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'}}))
287 288

local function icmprules(ofrag, oname, types)
289 290 291 292 293 294 295 296 297
   extend(
      ir,
      combinations(ofrag,
		   {{chain='icmp-routing', target='ACCEPT'}},
		   util.map(types,
			    function(t)
			       return {opts='--'..oname..' '..t}
			    end))
   )
298 299 300
end
icmprules(icmp, 'icmp-type', {3, 11, 12})
icmprules(icmp6, 'icmpv6-type', {1, 2, 3, 4})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
301

302 303 304 305 306 307 308 309
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
310 311 312
achains = combinations({{chain='tarpit'}},
		       {{opts='-p tcp', target='TARPIT'},
			{target='DROP'}})
313