filter.lua 12.7 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
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
37 38 39

   elseif self.update ~= nil then
      self:error('Attribute allowed only with named limits: update')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
40 41 42 43 44 45 46 47 48
   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

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

   if count > RECENT_MAX_COUNT then return end

60
   local update = self.update ~= false
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
61
   local cofs = {}
62
   local sofs = update and {} or nil
63 64

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

68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
      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
92 93
	    match='-m recent --name '..
	       (self.name and 'user:'..self.name or name)..' --r'..
94 95 96 97 98
	       ({src='source', dest='dest'})[attr]..' --mask '..mask
	 }
      }

      extend(
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
99
	 cofs,
100 101
	 combinations(
	    rec,
102 103 104 105
	    {
	       {match='--'..(update and 'update' or 'rcheck')..' --hitcount '..
		   count..' --seconds '..interval}
	    }
106 107
	 )
      )
108
      if sofs then extend(sofs, combinations(rec, {{match='--set'}})) end
109 110
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
111
   return cofs, sofs
112 113 114
end


115 116
local TranslatingRule = class(Rule)

117 118 119 120 121
function TranslatingRule:init(...)
   TranslatingRule.super(self):init(...)
   if type(self.dnat) == 'string' then self.dnat = {addr=self.dnat} end
end

122 123 124 125 126 127
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(
128
      model.Zone, {addr=self.dnat.addr}
129 130 131 132 133 134
   ):optfrags(self:direction('out'))
   assert(#natof == 1)
   table.insert(ofrags, natof[1])
   return ofrags
end

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 160 161 162 163 164 165 166
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

167

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
168 169 170 171
local LoggingRule = class(TranslatingRule)

function LoggingRule:init(...)
   LoggingRule.super(self):init(...)
172
   util.setdefault(self, 'action', 'accept')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
173 174

   local custom = self:customtarget()
175
   if type(self.log) ~= 'table' then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
176
      self.log = loadclass('log').get(
177
	 self, self.log, not custom and self:logdefault()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
178 179 180 181
      )
   end
   if custom and self.log then
      self:error('Logging not allowed with custom action: '..self.action)
182
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
183 184
end

185 186
function LoggingRule:logdefault() return false end

187
function LoggingRule:target() return 'ACCEPT' end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
188

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

195 196 197 198
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
199 200
end

201
function LoggingRule:mangleoptfrags(ofrags)
202
   return self:combinelog(ofrags, self.log, self.action, self:target())
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
203 204 205
end


206
local RelatedRule = class(TranslatingRule)
207 208 209 210 211 212 213 214

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] = {
215
	       family=sdef.family,
216 217
	       match='-m conntrack --ctstate RELATED -m helper --helper '..
	          helper
218 219 220 221 222 223 224
	    }
	 end
      end
   end
   return util.values(helpers)
end

225 226
function RelatedRule:target() return 'ACCEPT' end

227

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
228
local Filter = class(LoggingRule)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
229

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
230
function Filter:init(...)
231
   Filter.super(self):init(...)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
232 233

   -- alpine v2.4 compatibility
234
   if contains({'logdrop', 'logreject'}, self.action) then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
235
      self:warning('Deprecated action: '..self.action)
236
      self.action = self.action:sub(4, -1)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
237 238 239
   end

   local limit = self:limit()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
240
   if limit then
241 242 243
      if limit == 'conn-limit' and self['no-track'] then
	 self:error('Tracking required with connection limit')
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
244 245 246
      if type(self[limit]) ~= 'table' then
	 self[limit] = {count=self[limit]}
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
247
      self[limit].log = loadclass('log').get(self, self[limit].log, true)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
248
   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
   return not self['no-track'] and self:limit() == 'flow-limit'
      and 'prepend' or 'append'
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
354 355
end

356 357 358 359
function Filter:logdefault()
   return contains({'drop', 'reject', 'tarpit'}, self.action)
end

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

368
function Filter:mangleoptfrags(ofrags)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
369
   local limit = self:limit()
370
   if not limit then return Filter.super(self):mangleoptfrags(ofrags) end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
371

372 373 374 375 376 377
   local function incompatible(item)
      self:error('Limit incompatible with '..item)
   end

   if self:customtarget() or self:logdefault() then
      incompatible('action: '..self.action)
378
   end
379

380 381 382
   local limitchain = self:uniqueid('limit')
   local limitlog = self[limit].log
   local limitobj = self:create(FilterLimit, self[limit], 'limit')
383

384
   local ofs
385
   local conn = limit == 'conn-limit'
386 387 388
   local target = self:target()
   local ct = conn and target
   local pl = not target and self.log
389

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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
392 393
   if cofs then
      ofs = self:combinelog(cofs, limitlog, 'drop', 'DROP')
394 395 396 397 398

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

402
   else
403 404
      if pl then incompatible('action or log') end

405
      local limofs = limitobj:limitofrags(limitchain)
406
      ofs = ct and Filter.super(self):mangleoptfrags(limofs) or
407
	 combinations(limofs, {{target='RETURN'}})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
408

409
      extend(ofs, self:actofrags(limitlog, 'DROP'))
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
410
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
411

412
   return self:combine(ofrags, ofs, 'limit', true)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
413 414 415
end


416
local Policy = class(Filter)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
417 418 419 420

function Policy:servoptfrags() return nil end


421 422
local fchains = {{chain='FORWARD'}, {chain='INPUT'}, {chain='OUTPUT'}}

423
local function stateful(config)
424 425
   local res = {}

426 427 428 429
   for i, family in ipairs{'inet', 'inet6'} do

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

   return res
477
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
478

479 480
local icmp = {{family='inet', table='filter', match='-p icmp'}}
local icmp6 = {{family='inet6', table='filter', match='-p icmpv6'}}
481 482 483 484 485 486 487
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'}}))
488 489

local function icmprules(ofrag, oname, types)
490 491
   extend(
      ir,
492 493 494 495 496
      combinations(
         ofrag,
	 {{chain='icmp-routing', target='ACCEPT'}},
	 util.map(types, function(t) return {match='--'..oname..' '..t} end)
      )
497
   )
498 499 500
end
icmprules(icmp, 'icmp-type', {3, 11, 12})
icmprules(icmp6, 'icmpv6-type', {1, 2, 3, 4})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
501

502 503 504 505 506 507 508 509
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(
510
      {{chain='tarpit'}}, {{match='-p tcp', target='TARPIT'}, {target='DROP'}}
511
   )
512
}