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 185
   return self:customtarget() and ofrags or
      self:combinelog(ofrags, self.log, self.action, self:target())
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
186 187 188
end


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

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

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

210

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

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

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

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

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

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

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

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

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

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

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

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

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

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

310 311 312 313
      local nr = #res

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

function Policy:servoptfrags() return nil end


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

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

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

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

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

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

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

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