filter.lua 13.2 KB
Newer Older
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
1 2
--[[
Filter module for Alpine Wall
3
Copyright (C) 2012-2017 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
21 22 23 24 25 26 27 28 29 30 31 32 33
local setdefault = util.setdefault


local function initmask(obj)
   for _, attr in ipairs{'src-mask', 'dest-mask'} do
      if obj[attr] then
	 obj:error('Attribute not allowed with a named limit: '..attr)
      end
   end

   local limits = obj.root.limit
   obj[(obj.addr or 'src')..'-mask'] = limits and limits[obj.name] or true
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
34 35


36 37 38 39
local RECENT_MAX_COUNT = 20

local FilterLimit = class(model.Limit)

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
40
function FilterLimit:initmask()
41
   if self.name then initmask(self)
42 43
   elseif self.update ~= nil then
      self:error('Attribute allowed only with named limits: update')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
44 45 46 47 48 49 50 51 52
   end

   FilterLimit.super(self):initmask()

   if self.name and not self:recentofrags() then
      self:error('Attribute allowed only with low-rate limits: name')
   end
end

53 54 55 56 57
function FilterLimit:recentofrags(name)
   local count = self.count
   local interval = self.interval

   if count > RECENT_MAX_COUNT then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
58
      count = self:intrate()
59 60 61 62 63
      interval = 1
   end

   if count > RECENT_MAX_COUNT then return end

64
   local update = self.update ~= false
65

66 67
   local ofs = self:recentmask(name)
   if not ofs then return end
68

69 70 71 72 73 74 75
   return combinations(
      ofs,
      {
	 {match='--'..(update and 'update' or 'rcheck')..' --hitcount '..
	     count..' --seconds '..interval}
      }
   ), update and combinations(ofs, {{match='--set'}}) or nil
76 77 78
end


79 80 81 82 83 84 85 86 87 88 89 90 91 92
local LimitReference = class(model.Maskable)

function LimitReference:initmask()
   if not self.name then
      if not self[1] then self:error('Limit name not defined') end
      self.name = self[1]
   end
   initmask(self)

   LimitReference.super(self):initmask()
end

function LimitReference:recentofrags()
   local ofs = self:recentmask()
93 94
   return ofs and combinations(ofs, {{match='--set'}}) or
      self:error('Invalid address mask for limit')
95 96 97
end


98 99
local TranslatingRule = class(Rule)

100 101 102 103 104
function TranslatingRule:init(...)
   TranslatingRule.super(self):init(...)
   if type(self.dnat) == 'string' then self.dnat = {addr=self.dnat} end
end

105 106 107 108 109 110
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(
111
      model.Zone, {addr=self.dnat.addr}
112 113 114 115 116 117
   ):optfrags(self:direction('out'))
   assert(#natof == 1)
   table.insert(ofrags, natof[1])
   return ofrags
end

118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
function TranslatingRule:servoptfrags()
   local ofrags = TranslatingRule.super(self):servoptfrags()
   if not (self.dnat and self.dnat.port) then return ofrags end

   ofrags = combinations(ofrags, {{family='inet6'}})

   local protos = {}
   for _, serv in listpairs(self.service) do
      for _, sdef in listpairs(serv) do
	 if sdef.family ~= 'inet6' then
	    if not contains({'tcp', 'udp'}, sdef.proto) then
	       self:error('Cannot do port translation for '..sdef.proto)
	    end
	    protos[sdef.proto] = true
	 end
      end
   end
   for proto, _ in pairs(protos) do
      extend(
	 ofrags,
	 combinations(
	    self:create(
	       model.Rule, {service={proto=proto, port=self.dnat.port}}
	    ):servoptfrags(),
	    {{family='inet'}}
	 )
      )
   end

   return ofrags
end

150

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
151 152 153 154
local LoggingRule = class(TranslatingRule)

function LoggingRule:init(...)
   LoggingRule.super(self):init(...)
155
   setdefault(self, 'action', 'accept')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
156 157

   local custom = self:customtarget()
158
   if type(self.log) ~= 'table' then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
159
      self.log = loadclass('log').get(
160
	 self, self.log, not custom and self:logdefault()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
161 162 163 164
      )
   end
   if custom and self.log then
      self:error('Logging not allowed with custom action: '..self.action)
165
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
166 167
end

168 169
function LoggingRule:logdefault() return false end

170
function LoggingRule:target() return 'ACCEPT' end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
171

172 173 174 175 176
function LoggingRule:actofrags(log, target)
   local res = log and log:optfrags() or {}
   if target ~= nil then table.insert(res, {target=target}) end
   return res
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
177

178 179 180
function LoggingRule:combinelog(ofrags, log, action, target)
   local actions = self:actofrags(log, target)
   return actions[1] and
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
181 182
      self:combine(ofrags, actions, 'log'..action, log and log:target()) or
      ofrags
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
183 184
end

185
function LoggingRule:mangleoptfrags(ofrags)
186 187
   return self:customtarget() and ofrags or
      self:combinelog(ofrags, self.log, self.action, self:target())
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
188 189 190
end


191
local RelatedRule = class(TranslatingRule)
192 193 194 195 196 197 198 199

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] = {
200
	       family=sdef.family,
201 202
	       match='-m conntrack --ctstate RELATED -m helper --helper '..
	          helper
203 204 205 206 207 208 209
	    }
	 end
      end
   end
   return util.values(helpers)
end

210 211
function RelatedRule:target() return 'ACCEPT' end

212

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
213
local Filter = class(LoggingRule)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
214

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
215
function Filter:init(...)
216 217 218
   local ul = self['update-limit']
   if ul then setdefault(self, 'action', 'pass') end

219
   Filter.super(self):init(...)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
220 221

   -- alpine v2.4 compatibility
222
   if contains({'logdrop', 'logreject'}, self.action) then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
223
      self:warning('Deprecated action: '..self.action)
224
      self.action = self.action:sub(4, -1)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
225 226 227
   end

   local limit = self:limit()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
228
   if limit then
229 230 231
      if limit == 'conn-limit' and self['no-track'] then
	 self:error('Tracking required with connection limit')
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
232 233 234
      if type(self[limit]) ~= 'table' then
	 self[limit] = {count=self[limit]}
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
235
      self[limit].log = loadclass('log').get(self, self[limit].log, true)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
236
   end
237

238 239 240 241
   if ul and self.action ~= 'pass' then
      self:error('Cannot specify action with update-limit')
   end
end
242

243 244 245 246
function Filter:updatelimit()
   local ul = util.copy(self['update-limit'])

   if type(ul) == 'table' then
247 248 249
      if not contains({'conn', 'flow'}, setdefault(ul, 'measure', 'conn')) then
	 self:error('Invalid value for measure: '..ul.measure)
      end
250

251 252 253 254
      if self['no-track'] and ul.measure == 'conn' then
	 self:error('Tracking required when measuring connection rate')
      end
   end
255 256

   return ul and self:create(LimitReference, ul, 'update-limit')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
257 258
end

259
function Filter:extratrules()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
260 261
   local res = {}

262 263
   local function extrarules(label, cls, options)
      options = options or {}
264 265
      options.attrs = 'dnat'
      extend(res, self:extrarules(label, cls, options))
266 267
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
268
   if self.dnat then
269 270 271
      if self.action ~= 'accept' then
	 self:error('dnat option not allowed with '..self.action..' action')
      end
272 273 274
      if self['no-track'] then
	 self:error('dnat option not allowed with no-track')
      end
275 276
      if self.ipset then
	 self:error('dnat and ipset options cannot be used simultaneously')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
277 278
      end

279 280 281 282
      if self.dnat.addr:find('/') then
	 self:error('DNAT target cannot be a network address')
      end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
283
      local dnataddr
284
      for i, addr in ipairs(resolve(self.dnat.addr, self)) do
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
285 286
	 if addr[1] == 'inet' then
	    if dnataddr then
287 288 289
	       self:error(
		  self.dnat.addr..' resolves to multiple IPv4 addresses'
	       )
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
290 291 292 293 294
	    end
	    dnataddr = addr[2]
	 end
      end
      if not dnataddr then
295
	 self:error(self.dnat.addr..' does not resolve to any IPv4 address')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
296 297
      end

298 299 300 301 302 303 304 305
      extrarules(
	 'dnat',
	 'dnat',
	 {
	    update={['to-addr']=dnataddr, ['to-port']=self.dnat.port},
	    discard='out'
	 }
      )
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
306 307
   end

308
   if self.action == 'tarpit' or self['no-track'] then
309
      extrarules('no-track', 'no-track')
310
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
311

312
   if self.action == 'accept' then
313
      if self:position() == 'prepend' then
314
	 extrarules('final', LoggingRule, {update={log=self.log}})
315 316
      end

317 318 319 320
      local nr = #res

      if self.related then
	 for i, rule in listpairs(self.related) do
321
	    extrarules(
322 323 324
	       'related',
	       RelatedRule,
	       {index=i, src=rule, update={service=self.service}}
325
	    )
326 327 328 329
	 end
      else
	 -- TODO avoid creating unnecessary RELATED rules by introducing
	 -- helper direction attributes to service definitions
330
	 extrarules('related', RelatedRule)
331
	 extrarules('related-reply', RelatedRule, {update={reverse=true}})
332 333 334 335 336 337
      end

      if self['no-track'] then
	 if #res > nr then
	    self:error('Tracking required by service')
	 end
338 339
	 extrarules('no-track-reply', 'no-track', {update={reverse=true}})
	 extrarules('reply', 'filter', {update={reverse=true}})
340
      end
341 342
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
343 344 345
   return res
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
346 347 348 349 350
function Filter:limit()
   local res
   for i, limit in ipairs({'conn-limit', 'flow-limit'}) do
      if self[limit] then
	 if res then
351
	    self:error('Cannot specify multiple limits for a single filter rule')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
352 353 354 355 356 357 358 359
	 end
	 res = limit
      end
   end
   return res
end

function Filter:position()
360
   local ul = self:updatelimit()
361
   return not self['no-track'] and (
362
      self:limit() == 'flow-limit' or (ul and ul.measure == 'flow')
363
   ) and 'prepend' or 'append'
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
364 365
end

366 367 368 369
function Filter:logdefault()
   return contains({'drop', 'reject', 'tarpit'}, self.action)
end

370
function Filter:target()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
371
   if self.action == 'pass' then return end
372 373
   if self.action ~= 'accept' and not self:logdefault() then
      self:error('Invalid filter action: '..self.action)
374
   end
375
   return self.action == 'tarpit' and 'tarpit' or self.action:upper()
376 377
end

378
function Filter:mangleoptfrags(ofrags)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
379
   local limit = self:limit()
380 381
   local ul = self:updatelimit()

382
   if not limit then
383 384
      if ul then
	 ofrags = self:combine(ofrags, ul:recentofrags())
385 386 387
      end
      return Filter.super(self):mangleoptfrags(ofrags)
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
388

389 390 391 392
   local function incompatible(item)
      self:error('Limit incompatible with '..item)
   end

393
   if ul then incompatible('update-limit') end
394

395 396
   if self:customtarget() or self:logdefault() then
      incompatible('action: '..self.action)
397
   end
398

399 400 401
   local limitchain = self:uniqueid('limit')
   local limitlog = self[limit].log
   local limitobj = self:create(FilterLimit, self[limit], 'limit')
402

403
   local ofs
404
   local final = self:position() == 'append'
405
   local target = self:target()
406
   local ft = final and target
407
   local pl = not target and self.log
408

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
409
   local cofs, sofs = limitobj:recentofrags(limitchain)
410

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
411 412
   if cofs then
      ofs = self:combinelog(cofs, limitlog, 'drop', 'DROP')
413 414

      local nxt
415
      if ft then
416 417
	 extend(ofs, self:actofrags(self.log))
	 nxt = target
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
418
      elseif sofs and not (pl and pl:target()) then nxt = false end
419
      extend(ofs, combinations(sofs, self:actofrags(pl, nxt)))
420

421
   else
422 423
      if pl then incompatible('action or log') end

424
      local limofs = limitobj:limitofrags(limitchain)
425
      ofs = ft and Filter.super(self):mangleoptfrags(limofs) or
426
	 combinations(limofs, {{target='RETURN'}})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
427

428
      extend(ofs, self:actofrags(limitlog, 'DROP'))
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
429
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
430

431
   return self:combine(ofrags, ofs, 'limit', true)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
432 433 434
end


435
local Policy = class(Filter)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
436 437 438 439

function Policy:servoptfrags() return nil end


440 441
local fchains = {{chain='FORWARD'}, {chain='INPUT'}, {chain='OUTPUT'}}

442
local function stateful(config)
443 444
   local res = {}

445 446 447 448
   for i, family in ipairs{'inet', 'inet6'} do

      local er = combinations(
	 fchains,
449
	 {{match='-m conntrack --ctstate ESTABLISHED'}}
450
      )
451 452
      for i, chain in ipairs({'INPUT', 'OUTPUT'}) do
	 table.insert(
453
	    er, {chain=chain, match='-'..chain:sub(1, 1):lower()..' lo'}
454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470
	 )
      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(
471
			Rule.morph{service={sdef}}:servoptfrags(),
472 473 474 475 476 477 478 479
			{{family=family}}
		     )
		     if of[1] then
			assert(#of == 1)
			of[1].target = 'CT --helper '..sdef['ct-helper']
			table.insert(ofrags, of[1])
		     end
		  end
480
	       end
481
	       visited[serv] = true
482 483 484
	    end
	 end
      end
485 486 487 488 489 490 491
      extend(
	 res,
	 combinations(
	    {{table='raw'}},
	    {{chain='PREROUTING'}, {chain='OUTPUT'}},
	    ofrags
	 )
492
      )
493
   end
494 495

   return res
496
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
497

498 499
local icmp = {{family='inet', table='filter', match='-p icmp'}}
local icmp6 = {{family='inet6', table='filter', match='-p icmpv6'}}
500 501 502 503 504 505 506
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'}}))
507 508

local function icmprules(ofrag, oname, types)
509 510
   extend(
      ir,
511 512 513 514 515
      combinations(
         ofrag,
	 {{chain='icmp-routing', target='ACCEPT'}},
	 util.map(types, function(t) return {match='--'..oname..' '..t} end)
      )
516
   )
517 518 519
end
icmprules(icmp, 'icmp-type', {3, 11, 12})
icmprules(icmp6, 'icmpv6-type', {1, 2, 3, 4})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
520

521 522 523 524 525 526 527 528
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(
529
      {{chain='tarpit'}}, {{match='-p tcp', target='TARPIT'}, {target='DROP'}}
530
   )
531
}