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
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 241 242 243 244 245 246 247 248 249 250

   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
251 252
end

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

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

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

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

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

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

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

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

311 312 313 314
      local nr = #res

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

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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
337 338 339
   return res
end

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

function Filter:position()
354 355 356 357 358
   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
359 360
end

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

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

373
function Filter:mangleoptfrags(ofrags)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
374
   local limit = self:limit()
375 376 377 378 379 380
   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
381

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

386 387
   if self['update-limit'] then incompatible('update-limit') end

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

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

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

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

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

      local nxt
      if ct then
	 extend(ofs, self:actofrags(self.log))
	 nxt = target
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
411
      elseif sofs and not (pl and pl:target()) then nxt = false end
412
      extend(ofs, combinations(sofs, self:actofrags(pl, nxt)))
413

414
   else
415 416
      if pl then incompatible('action or log') end

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

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

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


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

function Policy:servoptfrags() return nil end


433 434
local fchains = {{chain='FORWARD'}, {chain='INPUT'}, {chain='OUTPUT'}}

435
local function stateful(config)
436 437
   local res = {}

438 439 440 441
   for i, family in ipairs{'inet', 'inet6'} do

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

   return res
489
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
490

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

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

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