filter.lua 11 KB
Newer Older
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
1 2
--[[
Filter module for Alpine Wall
3
Copyright (C) 2012-2014 Kaarle Ritvanen
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
4
See LICENSE file for license details
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
5 6 7
]]--


Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
8
local loadclass = require('awall').loadclass
9
local resolve = require('awall.host')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
10

11
local model = require('awall.model')
12 13 14
local class = model.class
local Rule = model.Rule

15
local combinations = require('awall.optfrag').combinations
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
16

17
local util = require('awall.util')
18
local contains = util.contains
19
local extend = util.extend
20
local listpairs = util.listpairs
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
21 22


23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
local RECENT_MAX_COUNT = 20

local FilterLimit = class(model.Limit)

function FilterLimit:recentofrags(name)
   local count = self.count
   local interval = self.interval

   if count > RECENT_MAX_COUNT then
      count = self:rate()
      interval = 1
   end

   if count > RECENT_MAX_COUNT then return end

   local uofs = {}
   local sofs = {}

   for _, family in ipairs{'inet', 'inet6'} do
      if type(self.mask[family].mode) ~= 'table' then return end
      local mask = ''
      local attr, len = unpack(self.mask[family].mode)

      if family == 'inet' then
	 local octet
	 for i = 0, 3 do
	    if len <= i * 8 then octet = 0
	    elseif len > i * 8 + 7 then octet = 255
	    else octet = 256 - 2^(8 - len % 8) end
	    mask = util.join(mask, '.', octet)
	 end

      elseif family == 'inet6' then
	 while len > 0 do
	    if #mask % 5 == 4 then mask = mask..':' end
	    mask = mask..('%x'):format(16 - 2^math.max(0, 4 - len))
	    len = len - 4
	 end
	 while #mask % 5 < 4 do mask = mask..'0' end
	 if #mask < 39 then mask = mask..'::' end
      end

      local rec = {
	 {
	    family=family,
	    opts='-m recent --name '..name..' --r'..
	       ({src='source', dest='dest'})[attr]..' --mask '..mask
	 }
      }

      extend(
	 uofs,
	 combinations(
	    rec,
	    {{opts='--update --hitcount '..count..' --seconds '..interval}}
	 )
      )
      extend(sofs, combinations(rec, {{opts='--set'}}))
   end

   return uofs, sofs
end


87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
local TranslatingRule = class(Rule)

function TranslatingRule:destoptfrags()
   local ofrags = TranslatingRule.super(self):destoptfrags()
   if not self.dnat then return ofrags end

   ofrags = combinations(ofrags, {{family='inet6'}})
   local natof = self:create(
      model.Zone, {addr=self.dnat}
   ):optfrags(self:direction('out'))
   assert(#natof == 1)
   table.insert(ofrags, natof[1])
   return ofrags
end


Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
103 104 105 106
local LoggingRule = class(TranslatingRule)

function LoggingRule:init(...)
   LoggingRule.super(self):init(...)
107
   util.setdefault(self, 'action', 'accept')
108 109 110
   if type(self.log) ~= 'table' then
      self.log = loadclass('log').get(self, self.log, self.action ~= 'accept')
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
111 112 113 114 115
end

function LoggingRule:actiontarget() return 'ACCEPT' end

function LoggingRule:target()
116
   if self.log then return self:uniqueid('log'..self.action) end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
117 118 119 120 121
   return self:actiontarget()
end

function LoggingRule:logchain(log, action, target)
   if not log then return {}, target end
122
   local chain = self:uniqueid('log'..action)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
123 124 125 126 127 128 129 130
   return combinations({{chain=chain}}, {log:optfrag(), {target=target}}), chain
end

function LoggingRule:extraoptfrags()
   return self:logchain(self.log, self.action, self:actiontarget())
end


131
local RelatedRule = class(TranslatingRule)
132 133 134 135 136 137 138 139

function RelatedRule:servoptfrags()
   local helpers = {}
   for i, serv in listpairs(self.service) do
      for i, sdef in listpairs(serv) do
	 local helper = sdef['ct-helper']
	 if helper then
	    helpers[helper] = {
140
	       family=sdef.family,
141
	       opts='-m conntrack --ctstate RELATED -m helper --helper '..helper
142 143 144 145 146 147 148
	    }
	 end
      end
   end
   return util.values(helpers)
end

149 150
function RelatedRule:target() return 'ACCEPT' end

151

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
152
local Filter = class(LoggingRule)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
153

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
154
function Filter:init(...)
155
   Filter.super(self):init(...)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
156 157

   -- alpine v2.4 compatibility
158
   if contains({'logdrop', 'logreject'}, self.action) then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
159
      self:warning('Deprecated action: '..self.action)
160
      self.action = self.action:sub(4, -1)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
161 162 163
   end

   local limit = self:limit()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
164
   if limit then
165 166 167
      if limit == 'conn-limit' and self['no-track'] then
	 self:error('Tracking required with connection limit')
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
168 169 170
      if type(self[limit]) ~= 'table' then
	 self[limit] = {count=self[limit]}
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
171
      self[limit].log = loadclass('log').get(self, self[limit].log, true)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
172
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
173 174
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
175 176 177
function Filter:trules()
   local res = {}

178 179
   local function extrarules(label, cls, options)
      options = options or {}
180

181 182 183 184 185
      local params = {}
      for i, attr in ipairs(
	 {'in', 'out', 'src', 'dest', 'dnat', 'ipset', 'ipsec', 'service'}
      ) do
	 params[attr] = (options.src or self)[attr]
186
      end
187 188 189 190
      util.update(params, options.update)
      if options.discard then params[options.discard] = nil end

      extend(res, self:create(cls, params, label, options.index):trules())
191 192
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
193
   if self.dnat then
194 195 196
      if self.action ~= 'accept' then
	 self:error('dnat option not allowed with '..self.action..' action')
      end
197 198 199
      if self['no-track'] then
	 self:error('dnat option not allowed with no-track')
      end
200
      if self.dnat:find('/') then
201
	 self:error('DNAT target cannot be a network address')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
202 203 204
      end
      for i, attr in ipairs({'ipsec', 'ipset'}) do
	 if self[attr] then
205
	    self:error('dnat and '..attr..' options cannot be used simultaneously')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
206 207 208 209
	 end
      end

      local dnataddr
210
      for i, addr in ipairs(resolve(self.dnat, self)) do
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
211 212
	 if addr[1] == 'inet' then
	    if dnataddr then
213
	       self:error(self.dnat..' resolves to multiple IPv4 addresses')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
214 215 216 217 218
	    end
	    dnataddr = addr[2]
	 end
      end
      if not dnataddr then
219
	 self:error(self.dnat..' does not resolve to any IPv4 address')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
220 221
      end

222
      extrarules('dnat', 'dnat', {update={['to-addr']=dnataddr}, discard='out'})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
223 224
   end

225
   if self.action == 'tarpit' or self['no-track'] then
226
      extrarules('no-track', 'no-track')
227
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
228

229
   extend(res, Filter.super(self):trules())
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
230

231
   if self.action == 'accept' then
232
      if self:position() == 'prepend' then
233
	 extrarules('final', LoggingRule, {update={log=self.log}})
234 235
      end

236 237 238 239
      local nr = #res

      if self.related then
	 for i, rule in listpairs(self.related) do
240
	    extrarules(
241 242 243
	       'related',
	       RelatedRule,
	       {index=i, src=rule, update={service=self.service}}
244
	    )
245 246 247 248
	 end
      else
	 -- TODO avoid creating unnecessary RELATED rules by introducing
	 -- helper direction attributes to service definitions
249
	 extrarules('related', RelatedRule)
250
	 extrarules('related-reply', RelatedRule, {update={reverse=true}})
251 252 253 254 255 256
      end

      if self['no-track'] then
	 if #res > nr then
	    self:error('Tracking required by service')
	 end
257 258
	 extrarules('no-track-reply', 'no-track', {update={reverse=true}})
	 extrarules('reply', 'filter', {update={reverse=true}})
259
      end
260 261
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
262 263 264
   return res
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
265 266 267 268 269
function Filter:limit()
   local res
   for i, limit in ipairs({'conn-limit', 'flow-limit'}) do
      if self[limit] then
	 if res then
270
	    self:error('Cannot specify multiple limits for a single filter rule')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
271 272 273 274 275 276 277 278
	 end
	 res = limit
      end
   end
   return res
end

function Filter:position()
279 280
   return not self['no-track'] and self:limit() == 'flow-limit'
      and 'prepend' or 'append'
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
281 282
end

283 284
function Filter:actiontarget()
   if self.action == 'tarpit' then return 'tarpit' end
285
   if contains({'accept', 'drop', 'reject'}, self.action) then
286
      return self.action:upper()
287
   end
288
   self:error('Invalid filter action: '..self.action)
289 290
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
291
function Filter:target()
292
   if self:limit() then return self:uniqueid('limit') end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
293
   return Filter.super(self).target()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
294 295 296 297 298 299
end

function Filter:extraoptfrags()
   local limit = self:limit()
   if limit then
      if self.action ~= 'accept' then
300
	 self:error('Cannot specify limit for '..self.action..' filter')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
301
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
302

303
      local limitchain = self:uniqueid('limit')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
304
      local limitlog = self[limit].log
305
      local limitobj = self:create(FilterLimit, self[limit], 'limit')
306

307 308 309 310
      local ofrags = {}
      local logch, limitofs
      local accept = self:position() == 'append'

311
      local uofs, sofs = limitobj:recentofrags(limitchain)
312

313
      if uofs then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
314
	 ofrags, logch = self:logchain(limitlog, 'drop', 'DROP')
315

316
	 limitofs = combinations(uofs, {{target=logch}})
317
	 if accept and self.log then
318
	    table.insert(limitofs, self.log:optfrag())
319
	 end
320 321 322
	 extend(
	    limitofs, combinations(sofs, {{target=accept and 'ACCEPT' or nil}})
	 )
323 324 325 326 327 328

      else
	 if accept then
	    ofrags, logch = self:logchain(self.log, 'accept', 'ACCEPT')
	 else logch = 'RETURN' end

329 330 331 332 333
	 limitofs = combinations(
	    limitobj:limitofrags(limitchain), {{target=logch}}
	 )
	 if limitlog then table.insert(limitofs, limitlog:optfrag()) end
	 table.insert(limitofs, {target='DROP'})
334
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
335

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
336 337 338
      extend(ofrags, combinations({{chain=limitchain}}, limitofs))
      return ofrags
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
339

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
340
   return Filter.super(self):extraoptfrags()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
341 342 343
end


344
local Policy = class(Filter)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
345 346 347 348

function Policy:servoptfrags() return nil end


349 350
local fchains = {{chain='FORWARD'}, {chain='INPUT'}, {chain='OUTPUT'}}

351
local function stateful(config)
352 353
   local res = {}

354 355 356 357 358
   for i, family in ipairs{'inet', 'inet6'} do

      local er = combinations(
	 fchains,
	 {{opts='-m conntrack --ctstate ESTABLISHED'}}
359
      )
360 361
      for i, chain in ipairs({'INPUT', 'OUTPUT'}) do
	 table.insert(
362
	    er, {chain=chain, opts='-'..chain:sub(1, 1):lower()..' lo'}
363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379
	 )
      end
      extend(
	 res,
	 combinations(er, {{family=family, table='filter', target='ACCEPT'}})
      )

      -- TODO avoid creating unnecessary CT rules by inspecting the
      -- filter rules' target families and chains
      local visited = {}
      local ofrags = {}
      for i, rule in listpairs(config.filter) do
	 for i, serv in listpairs(rule.service) do
	    if not visited[serv] then
	       for i, sdef in listpairs(serv) do
		  if sdef['ct-helper'] then
		     local of = combinations(
380
			Rule.morph{service={sdef}}:servoptfrags(),
381 382 383 384 385 386 387 388
			{{family=family}}
		     )
		     if of[1] then
			assert(#of == 1)
			of[1].target = 'CT --helper '..sdef['ct-helper']
			table.insert(ofrags, of[1])
		     end
		  end
389
	       end
390
	       visited[serv] = true
391 392 393
	    end
	 end
      end
394 395 396 397 398 399 400
      extend(
	 res,
	 combinations(
	    {{table='raw'}},
	    {{chain='PREROUTING'}, {chain='OUTPUT'}},
	    ofrags
	 )
401
      )
402
   end
403 404

   return res
405
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
406

407 408
local icmp = {{family='inet', table='filter', opts='-p icmp'}}
local icmp6 = {{family='inet6', table='filter', opts='-p icmpv6'}}
409 410 411 412 413 414 415
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'}}))
416 417

local function icmprules(ofrag, oname, types)
418 419 420 421 422 423 424 425 426
   extend(
      ir,
      combinations(ofrag,
		   {{chain='icmp-routing', target='ACCEPT'}},
		   util.map(types,
			    function(t)
			       return {opts='--'..oname..' '..t}
			    end))
   )
427 428 429
end
icmprules(icmp, 'icmp-type', {3, 11, 12})
icmprules(icmp6, 'icmpv6-type', {1, 2, 3, 4})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
430

431 432 433 434 435 436 437 438 439 440
return {
   export={
      filter={class=Filter, before={'dnat', 'no-track'}},
      policy={class=Policy, after='%filter-after'},
      ['%filter-before']={rules=stateful, before='filter'},
      ['%filter-after']={rules=ir, after='filter'}
   },
   achains=combinations(
      {{chain='tarpit'}}, {{opts='-p tcp', target='TARPIT'}, {target='DROP'}}
   )
441
}