filter.lua 12.5 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
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
21 22


23 24 25 26
local RECENT_MAX_COUNT = 20

local FilterLimit = class(model.Limit)

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
function FilterLimit:initmask()
   if self.name then
      for _, attr in ipairs{'src-mask', 'dest-mask'} do
        if self[attr] then
           self:error('Attribute not allowed with a named limit: '..attr)
        end
      end

      local limits = self.root.limit
      self[(self.addr or 'src')..'-mask'] = limits and limits[self.name] or true
   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

46 47 48 49 50
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
51
      count = self:intrate()
52 53 54 55 56 57 58 59 60
      interval = 1
   end

   if count > RECENT_MAX_COUNT then return end

   local uofs = {}
   local sofs = {}

   for _, family in ipairs{'inet', 'inet6'} do
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
61 62 63
      local attr, len = self:maskmode(family)
      if not attr then return end

64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
      local mask = ''

      if family == 'inet' then
	 local octet
	 for i = 0, 3 do
	    if len <= i * 8 then octet = 0
	    elseif len > i * 8 + 7 then octet = 255
	    else octet = 256 - 2^(8 - len % 8) end
	    mask = util.join(mask, '.', octet)
	 end

      elseif family == 'inet6' then
	 while len > 0 do
	    if #mask % 5 == 4 then mask = mask..':' end
	    mask = mask..('%x'):format(16 - 2^math.max(0, 4 - len))
	    len = len - 4
	 end
	 while #mask % 5 < 4 do mask = mask..'0' end
	 if #mask < 39 then mask = mask..'::' end
      end

      local rec = {
	 {
	    family=family,
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
88 89
	    match='-m recent --name '..
	       (self.name and 'user:'..self.name or name)..' --r'..
90 91 92 93 94 95 96 97
	       ({src='source', dest='dest'})[attr]..' --mask '..mask
	 }
      }

      extend(
	 uofs,
	 combinations(
	    rec,
98
	    {{match='--update --hitcount '..count..' --seconds '..interval}}
99 100
	 )
      )
101
      extend(sofs, combinations(rec, {{match='--set'}}))
102 103 104 105 106 107
   end

   return uofs, sofs
end


108 109
local TranslatingRule = class(Rule)

110 111 112 113 114
function TranslatingRule:init(...)
   TranslatingRule.super(self):init(...)
   if type(self.dnat) == 'string' then self.dnat = {addr=self.dnat} end
end

115 116 117 118 119 120
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(
121
      model.Zone, {addr=self.dnat.addr}
122 123 124 125 126 127
   ):optfrags(self:direction('out'))
   assert(#natof == 1)
   table.insert(ofrags, natof[1])
   return ofrags
end

128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
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

160

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
161 162 163 164
local LoggingRule = class(TranslatingRule)

function LoggingRule:init(...)
   LoggingRule.super(self):init(...)
165
   util.setdefault(self, 'action', 'accept')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
166 167

   local custom = self:customtarget()
168
   if type(self.log) ~= 'table' then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
169
      self.log = loadclass('log').get(
170
	 self, self.log, not custom and self:logdefault()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
171 172 173 174
      )
   end
   if custom and self.log then
      self:error('Logging not allowed with custom action: '..self.action)
175
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
176 177
end

178 179
function LoggingRule:logdefault() return false end

180
function LoggingRule:target() return 'ACCEPT' end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
181

182 183 184 185 186
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
187

188 189 190 191
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
192 193
end

194
function LoggingRule:mangleoptfrags(ofrags)
195
   return self:combinelog(ofrags, self.log, self.action, self:target())
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
196 197 198
end


199
local RelatedRule = class(TranslatingRule)
200 201 202 203 204 205 206 207

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] = {
208
	       family=sdef.family,
209 210
	       match='-m conntrack --ctstate RELATED -m helper --helper '..
	          helper
211 212 213 214 215 216 217
	    }
	 end
      end
   end
   return util.values(helpers)
end

218 219
function RelatedRule:target() return 'ACCEPT' end

220

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
221
local Filter = class(LoggingRule)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
222

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
223
function Filter:init(...)
224
   Filter.super(self):init(...)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
225 226

   -- alpine v2.4 compatibility
227
   if contains({'logdrop', 'logreject'}, self.action) then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
228
      self:warning('Deprecated action: '..self.action)
229
      self.action = self.action:sub(4, -1)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
230 231 232
   end

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

244
function Filter:extratrules()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
245 246
   local res = {}

247 248
   local function extrarules(label, cls, options)
      options = options or {}
249 250
      options.attrs = 'dnat'
      extend(res, self:extrarules(label, cls, options))
251 252
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
253
   if self.dnat then
254 255 256
      if self.action ~= 'accept' then
	 self:error('dnat option not allowed with '..self.action..' action')
      end
257 258 259
      if self['no-track'] then
	 self:error('dnat option not allowed with no-track')
      end
260 261
      if self.ipset then
	 self:error('dnat and ipset options cannot be used simultaneously')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
262 263
      end

264 265 266 267
      if self.dnat.addr:find('/') then
	 self:error('DNAT target cannot be a network address')
      end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
268
      local dnataddr
269
      for i, addr in ipairs(resolve(self.dnat.addr, self)) do
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
270 271
	 if addr[1] == 'inet' then
	    if dnataddr then
272 273 274
	       self:error(
		  self.dnat.addr..' resolves to multiple IPv4 addresses'
	       )
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
275 276 277 278 279
	    end
	    dnataddr = addr[2]
	 end
      end
      if not dnataddr then
280
	 self:error(self.dnat.addr..' does not resolve to any IPv4 address')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
281 282
      end

283 284 285 286 287 288 289 290
      extrarules(
	 'dnat',
	 'dnat',
	 {
	    update={['to-addr']=dnataddr, ['to-port']=self.dnat.port},
	    discard='out'
	 }
      )
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
291 292
   end

293
   if self.action == 'tarpit' or self['no-track'] then
294
      extrarules('no-track', 'no-track')
295
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
296

297
   if self.action == 'accept' then
298
      if self:position() == 'prepend' then
299
	 extrarules('final', LoggingRule, {update={log=self.log}})
300 301
      end

302 303 304 305
      local nr = #res

      if self.related then
	 for i, rule in listpairs(self.related) do
306
	    extrarules(
307 308 309
	       'related',
	       RelatedRule,
	       {index=i, src=rule, update={service=self.service}}
310
	    )
311 312 313 314
	 end
      else
	 -- TODO avoid creating unnecessary RELATED rules by introducing
	 -- helper direction attributes to service definitions
315
	 extrarules('related', RelatedRule)
316
	 extrarules('related-reply', RelatedRule, {update={reverse=true}})
317 318 319 320 321 322
      end

      if self['no-track'] then
	 if #res > nr then
	    self:error('Tracking required by service')
	 end
323 324
	 extrarules('no-track-reply', 'no-track', {update={reverse=true}})
	 extrarules('reply', 'filter', {update={reverse=true}})
325
      end
326 327
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
328 329 330
   return res
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
331 332 333 334 335
function Filter:limit()
   local res
   for i, limit in ipairs({'conn-limit', 'flow-limit'}) do
      if self[limit] then
	 if res then
336
	    self:error('Cannot specify multiple limits for a single filter rule')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
337 338 339 340 341 342 343 344
	 end
	 res = limit
      end
   end
   return res
end

function Filter:position()
345 346
   return not self['no-track'] and self:limit() == 'flow-limit'
      and 'prepend' or 'append'
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
347 348
end

349 350 351 352
function Filter:logdefault()
   return contains({'drop', 'reject', 'tarpit'}, self.action)
end

353
function Filter:target()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
354
   if self.action == 'pass' then return end
355 356
   if self.action ~= 'accept' and not self:logdefault() then
      self:error('Invalid filter action: '..self.action)
357
   end
358
   return self.action == 'tarpit' and 'tarpit' or self.action:upper()
359 360
end

361
function Filter:mangleoptfrags(ofrags)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
362
   local limit = self:limit()
363
   if not limit then return Filter.super(self):mangleoptfrags(ofrags) end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
364

365 366 367 368 369 370
   local function incompatible(item)
      self:error('Limit incompatible with '..item)
   end

   if self:customtarget() or self:logdefault() then
      incompatible('action: '..self.action)
371
   end
372

373 374 375
   local limitchain = self:uniqueid('limit')
   local limitlog = self[limit].log
   local limitobj = self:create(FilterLimit, self[limit], 'limit')
376

377
   local ofs
378
   local conn = limit == 'conn-limit'
379 380 381
   local target = self:target()
   local ct = conn and target
   local pl = not target and self.log
382

383
   local uofs, sofs = limitobj:recentofrags(limitchain)
384

385
   if uofs then
386
      ofs = self:combinelog(uofs, limitlog, 'drop', 'DROP')
387 388 389 390 391 392 393

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

395
   else
396 397
      if pl then incompatible('action or log') end

398
      local limofs = limitobj:limitofrags(limitchain)
399
      ofs = ct and Filter.super(self):mangleoptfrags(limofs) or
400
	 combinations(limofs, {{target='RETURN'}})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
401

402
      extend(ofs, self:actofrags(limitlog, 'DROP'))
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
403
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
404

405
   return self:combine(ofrags, ofs, 'limit', true)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
406 407 408
end


409
local Policy = class(Filter)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
410 411 412 413

function Policy:servoptfrags() return nil end


414 415
local fchains = {{chain='FORWARD'}, {chain='INPUT'}, {chain='OUTPUT'}}

416
local function stateful(config)
417 418
   local res = {}

419 420 421 422
   for i, family in ipairs{'inet', 'inet6'} do

      local er = combinations(
	 fchains,
423
	 {{match='-m conntrack --ctstate ESTABLISHED'}}
424
      )
425 426
      for i, chain in ipairs({'INPUT', 'OUTPUT'}) do
	 table.insert(
427
	    er, {chain=chain, match='-'..chain:sub(1, 1):lower()..' lo'}
428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444
	 )
      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(
445
			Rule.morph{service={sdef}}:servoptfrags(),
446 447 448 449 450 451 452 453
			{{family=family}}
		     )
		     if of[1] then
			assert(#of == 1)
			of[1].target = 'CT --helper '..sdef['ct-helper']
			table.insert(ofrags, of[1])
		     end
		  end
454
	       end
455
	       visited[serv] = true
456 457 458
	    end
	 end
      end
459 460 461 462 463 464 465
      extend(
	 res,
	 combinations(
	    {{table='raw'}},
	    {{chain='PREROUTING'}, {chain='OUTPUT'}},
	    ofrags
	 )
466
      )
467
   end
468 469

   return res
470
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
471

472 473
local icmp = {{family='inet', table='filter', match='-p icmp'}}
local icmp6 = {{family='inet6', table='filter', match='-p icmpv6'}}
474 475 476 477 478 479 480
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'}}))
481 482

local function icmprules(ofrag, oname, types)
483 484
   extend(
      ir,
485 486 487 488 489
      combinations(
         ofrag,
	 {{chain='icmp-routing', target='ACCEPT'}},
	 util.map(types, function(t) return {match='--'..oname..' '..t} end)
      )
490
   )
491 492 493
end
icmprules(icmp, 'icmp-type', {3, 11, 12})
icmprules(icmp6, 'icmpv6-type', {1, 2, 3, 4})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
494

495 496 497 498 499 500 501 502
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(
503
      {{chain='tarpit'}}, {{match='-p tcp', target='TARPIT'}, {target='DROP'}}
504
   )
505
}