filter.lua 13.1 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 180
function LoggingRule:combinelog(ofrags, log, action, target)
   local actions = self:actofrags(log, target)
   return actions[1] and
      self:combine(ofrags, actions, 'log'..action, log) or ofrags
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
181 182
end

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


188
local RelatedRule = class(TranslatingRule)
189 190 191 192 193 194 195 196

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

207 208
function RelatedRule:target() return 'ACCEPT' end

209

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
210
local Filter = class(LoggingRule)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
211

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

216
   Filter.super(self):init(...)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
217 218

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

   local limit = self:limit()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
225
   if limit then
226 227 228
      if limit == 'conn-limit' and self['no-track'] then
	 self:error('Tracking required with connection limit')
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
229 230 231
      if type(self[limit]) ~= 'table' then
	 self[limit] = {count=self[limit]}
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
232
      self[limit].log = loadclass('log').get(self, self[limit].log, true)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
233
   end
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248

   if ul then
      if self.action ~= 'pass' then
	 self:error('Cannot specify action with update-limit')
      end

      if not contains({'conn', 'flow'}, setdefault(ul, 'measure', 'conn')) then
	 self:error('Invalid value for measure: '..ul.measure)
      end
      if self['no-track'] and ul.measure == 'conn' then
	 self:error('Tracking required when measuring connection rate')
      end

      self:create(LimitReference, ul, 'update-limit')
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
249 250
end

251
function Filter:extratrules()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
252 253
   local res = {}

254 255
   local function extrarules(label, cls, options)
      options = options or {}
256 257
      options.attrs = 'dnat'
      extend(res, self:extrarules(label, cls, options))
258 259
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
260
   if self.dnat then
261 262 263
      if self.action ~= 'accept' then
	 self:error('dnat option not allowed with '..self.action..' action')
      end
264 265 266
      if self['no-track'] then
	 self:error('dnat option not allowed with no-track')
      end
267 268
      if self.ipset then
	 self:error('dnat and ipset options cannot be used simultaneously')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
269 270
      end

271 272 273 274
      if self.dnat.addr:find('/') then
	 self:error('DNAT target cannot be a network address')
      end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
275
      local dnataddr
276
      for i, addr in ipairs(resolve(self.dnat.addr, self)) do
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
277 278
	 if addr[1] == 'inet' then
	    if dnataddr then
279 280 281
	       self:error(
		  self.dnat.addr..' resolves to multiple IPv4 addresses'
	       )
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
282 283 284 285 286
	    end
	    dnataddr = addr[2]
	 end
      end
      if not dnataddr then
287
	 self:error(self.dnat.addr..' does not resolve to any IPv4 address')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
288 289
      end

290 291 292 293 294 295 296 297
      extrarules(
	 'dnat',
	 'dnat',
	 {
	    update={['to-addr']=dnataddr, ['to-port']=self.dnat.port},
	    discard='out'
	 }
      )
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
298 299
   end

300
   if self.action == 'tarpit' or self['no-track'] then
301
      extrarules('no-track', 'no-track')
302
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
303

304
   if self.action == 'accept' then
305
      if self:position() == 'prepend' then
306
	 extrarules('final', LoggingRule, {update={log=self.log}})
307 308
      end

309 310 311 312
      local nr = #res

      if self.related then
	 for i, rule in listpairs(self.related) do
313
	    extrarules(
314 315 316
	       'related',
	       RelatedRule,
	       {index=i, src=rule, update={service=self.service}}
317
	    )
318 319 320 321
	 end
      else
	 -- TODO avoid creating unnecessary RELATED rules by introducing
	 -- helper direction attributes to service definitions
322
	 extrarules('related', RelatedRule)
323
	 extrarules('related-reply', RelatedRule, {update={reverse=true}})
324 325 326 327 328 329
      end

      if self['no-track'] then
	 if #res > nr then
	    self:error('Tracking required by service')
	 end
330 331
	 extrarules('no-track-reply', 'no-track', {update={reverse=true}})
	 extrarules('reply', 'filter', {update={reverse=true}})
332
      end
333 334
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
335 336 337
   return res
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
338 339 340 341 342
function Filter:limit()
   local res
   for i, limit in ipairs({'conn-limit', 'flow-limit'}) do
      if self[limit] then
	 if res then
343
	    self:error('Cannot specify multiple limits for a single filter rule')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
344 345 346 347 348 349 350 351
	 end
	 res = limit
      end
   end
   return res
end

function Filter:position()
352 353 354 355 356
   return not self['no-track'] and (
      self:limit() == 'flow-limit' or (
	 self['update-limit'] and self['update-limit'].measure == 'flow'
      )
   ) and 'prepend' or 'append'
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
357 358
end

359 360 361 362
function Filter:logdefault()
   return contains({'drop', 'reject', 'tarpit'}, self.action)
end

363
function Filter:target()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
364
   if self.action == 'pass' then return end
365 366
   if self.action ~= 'accept' and not self:logdefault() then
      self:error('Invalid filter action: '..self.action)
367
   end
368
   return self.action == 'tarpit' and 'tarpit' or self.action:upper()
369 370
end

371
function Filter:mangleoptfrags(ofrags)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
372
   local limit = self:limit()
373 374 375 376 377 378
   if not limit then
      if self['update-limit'] then
	 ofrags = self:combine(ofrags, self['update-limit']:recentofrags())
      end
      return Filter.super(self):mangleoptfrags(ofrags)
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
379

380 381 382 383
   local function incompatible(item)
      self:error('Limit incompatible with '..item)
   end

384 385
   if self['update-limit'] then incompatible('update-limit') end

386 387
   if self:customtarget() or self:logdefault() then
      incompatible('action: '..self.action)
388
   end
389

390 391 392
   local limitchain = self:uniqueid('limit')
   local limitlog = self[limit].log
   local limitobj = self:create(FilterLimit, self[limit], 'limit')
393

394
   local ofs
395
   local conn = limit == 'conn-limit'
396 397 398
   local target = self:target()
   local ct = conn and target
   local pl = not target and self.log
399

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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
402 403
   if cofs then
      ofs = self:combinelog(cofs, limitlog, 'drop', 'DROP')
404 405 406 407 408

      local nxt
      if ct then
	 extend(ofs, self:actofrags(self.log))
	 nxt = target
409
      elseif sofs and not pl then nxt = false end
410
      extend(ofs, combinations(sofs, self:actofrags(pl, nxt)))
411

412
   else
413 414
      if pl then incompatible('action or log') end

415
      local limofs = limitobj:limitofrags(limitchain)
416
      ofs = ct and Filter.super(self):mangleoptfrags(limofs) or
417
	 combinations(limofs, {{target='RETURN'}})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
418

419
      extend(ofs, self:actofrags(limitlog, 'DROP'))
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
420
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
421

422
   return self:combine(ofrags, ofs, 'limit', true)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
423 424 425
end


426
local Policy = class(Filter)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
427 428 429 430

function Policy:servoptfrags() return nil end


431 432
local fchains = {{chain='FORWARD'}, {chain='INPUT'}, {chain='OUTPUT'}}

433
local function stateful(config)
434 435
   local res = {}

436 437 438 439
   for i, family in ipairs{'inet', 'inet6'} do

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

   return res
487
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
488

489 490
local icmp = {{family='inet', table='filter', match='-p icmp'}}
local icmp6 = {{family='inet6', table='filter', match='-p icmpv6'}}
491 492 493 494 495 496 497
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'}}))
498 499

local function icmprules(ofrag, oname, types)
500 501
   extend(
      ir,
502 503 504 505 506
      combinations(
         ofrag,
	 {{chain='icmp-routing', target='ACCEPT'}},
	 util.map(types, function(t) return {match='--'..oname..' '..t} end)
      )
507
   )
508 509 510
end
icmprules(icmp, 'icmp-type', {3, 11, 12})
icmprules(icmp6, 'icmpv6-type', {1, 2, 3, 4})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
511

512 513 514 515 516 517 518 519
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(
520
      {{chain='tarpit'}}, {{match='-p tcp', target='TARPIT'}, {target='DROP'}}
521
   )
522
}