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 93 94 95 96
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()
   return ofs and combinations(ofs, {{match='--set'}}) or self:error(MASK_ERROR)
end


97 98
local TranslatingRule = class(Rule)

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

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

117 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
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

149

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

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

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

167 168
function LoggingRule:logdefault() return false end

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

171 172 173 174 175
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
176

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

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


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

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

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

211

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

316 317 318 319
      local nr = #res

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

function Policy:servoptfrags() return nil end


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

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

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

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

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

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

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

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