filter.lua 11.9 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 27 28 29 30 31
local RECENT_MAX_COUNT = 20

local FilterLimit = class(model.Limit)

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
32
      count = self:intrate()
33 34 35 36 37 38 39 40 41
      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
42 43 44
      local attr, len = self:maskmode(family)
      if not attr then return end

45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
      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,
69
	    match='-m recent --name '..name..' --r'..
70 71 72 73 74 75 76 77
	       ({src='source', dest='dest'})[attr]..' --mask '..mask
	 }
      }

      extend(
	 uofs,
	 combinations(
	    rec,
78
	    {{match='--update --hitcount '..count..' --seconds '..interval}}
79 80
	 )
      )
81
      extend(sofs, combinations(rec, {{match='--set'}}))
82 83 84 85 86 87
   end

   return uofs, sofs
end


88 89
local TranslatingRule = class(Rule)

90 91 92 93 94
function TranslatingRule:init(...)
   TranslatingRule.super(self):init(...)
   if type(self.dnat) == 'string' then self.dnat = {addr=self.dnat} end
end

95 96 97 98 99 100
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(
101
      model.Zone, {addr=self.dnat.addr}
102 103 104 105 106 107
   ):optfrags(self:direction('out'))
   assert(#natof == 1)
   table.insert(ofrags, natof[1])
   return ofrags
end

108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
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

140

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
141 142 143 144
local LoggingRule = class(TranslatingRule)

function LoggingRule:init(...)
   LoggingRule.super(self):init(...)
145
   util.setdefault(self, 'action', 'accept')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
146 147

   local custom = self:customtarget()
148
   if type(self.log) ~= 'table' then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
149
      self.log = loadclass('log').get(
150
	 self, self.log, not custom and self:logdefault()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
151 152 153 154
      )
   end
   if custom and self.log then
      self:error('Logging not allowed with custom action: '..self.action)
155
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
156 157
end

158 159
function LoggingRule:logdefault() return false end

160
function LoggingRule:target() return 'ACCEPT' end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
161

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

168 169 170 171
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
172 173
end

174
function LoggingRule:mangleoptfrags(ofrags)
175
   return self:combinelog(ofrags, self.log, self.action, self:target())
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
176 177 178
end


179
local RelatedRule = class(TranslatingRule)
180 181 182 183 184 185 186 187

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] = {
188
	       family=sdef.family,
189 190
	       match='-m conntrack --ctstate RELATED -m helper --helper '..
	          helper
191 192 193 194 195 196 197
	    }
	 end
      end
   end
   return util.values(helpers)
end

198 199
function RelatedRule:target() return 'ACCEPT' end

200

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
201
local Filter = class(LoggingRule)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
202

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
203
function Filter:init(...)
204
   Filter.super(self):init(...)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
205 206

   -- alpine v2.4 compatibility
207
   if contains({'logdrop', 'logreject'}, self.action) then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
208
      self:warning('Deprecated action: '..self.action)
209
      self.action = self.action:sub(4, -1)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
210 211 212
   end

   local limit = self:limit()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
213
   if limit then
214 215 216
      if limit == 'conn-limit' and self['no-track'] then
	 self:error('Tracking required with connection limit')
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
217 218 219
      if type(self[limit]) ~= 'table' then
	 self[limit] = {count=self[limit]}
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
220
      self[limit].log = loadclass('log').get(self, self[limit].log, true)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
221
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
222 223
end

224
function Filter:extratrules()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
225 226
   local res = {}

227 228
   local function extrarules(label, cls, options)
      options = options or {}
229 230
      options.attrs = 'dnat'
      extend(res, self:extrarules(label, cls, options))
231 232
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
233
   if self.dnat then
234 235 236
      if self.action ~= 'accept' then
	 self:error('dnat option not allowed with '..self.action..' action')
      end
237 238 239
      if self['no-track'] then
	 self:error('dnat option not allowed with no-track')
      end
240 241
      if self.ipset then
	 self:error('dnat and ipset options cannot be used simultaneously')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
242 243
      end

244 245 246 247
      if self.dnat.addr:find('/') then
	 self:error('DNAT target cannot be a network address')
      end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
248
      local dnataddr
249
      for i, addr in ipairs(resolve(self.dnat.addr, self)) do
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
250 251
	 if addr[1] == 'inet' then
	    if dnataddr then
252 253 254
	       self:error(
		  self.dnat.addr..' resolves to multiple IPv4 addresses'
	       )
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
255 256 257 258 259
	    end
	    dnataddr = addr[2]
	 end
      end
      if not dnataddr then
260
	 self:error(self.dnat.addr..' does not resolve to any IPv4 address')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
261 262
      end

263 264 265 266 267 268 269 270
      extrarules(
	 'dnat',
	 'dnat',
	 {
	    update={['to-addr']=dnataddr, ['to-port']=self.dnat.port},
	    discard='out'
	 }
      )
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
271 272
   end

273
   if self.action == 'tarpit' or self['no-track'] then
274
      extrarules('no-track', 'no-track')
275
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
276

277
   if self.action == 'accept' then
278
      if self:position() == 'prepend' then
279
	 extrarules('final', LoggingRule, {update={log=self.log}})
280 281
      end

282 283 284 285
      local nr = #res

      if self.related then
	 for i, rule in listpairs(self.related) do
286
	    extrarules(
287 288 289
	       'related',
	       RelatedRule,
	       {index=i, src=rule, update={service=self.service}}
290
	    )
291 292 293 294
	 end
      else
	 -- TODO avoid creating unnecessary RELATED rules by introducing
	 -- helper direction attributes to service definitions
295
	 extrarules('related', RelatedRule)
296
	 extrarules('related-reply', RelatedRule, {update={reverse=true}})
297 298 299 300 301 302
      end

      if self['no-track'] then
	 if #res > nr then
	    self:error('Tracking required by service')
	 end
303 304
	 extrarules('no-track-reply', 'no-track', {update={reverse=true}})
	 extrarules('reply', 'filter', {update={reverse=true}})
305
      end
306 307
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
308 309 310
   return res
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
311 312 313 314 315
function Filter:limit()
   local res
   for i, limit in ipairs({'conn-limit', 'flow-limit'}) do
      if self[limit] then
	 if res then
316
	    self:error('Cannot specify multiple limits for a single filter rule')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
317 318 319 320 321 322 323 324
	 end
	 res = limit
      end
   end
   return res
end

function Filter:position()
325 326
   return not self['no-track'] and self:limit() == 'flow-limit'
      and 'prepend' or 'append'
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
327 328
end

329 330 331 332
function Filter:logdefault()
   return contains({'drop', 'reject', 'tarpit'}, self.action)
end

333
function Filter:target()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
334
   if self.action == 'pass' then return end
335 336
   if self.action ~= 'accept' and not self:logdefault() then
      self:error('Invalid filter action: '..self.action)
337
   end
338
   return self.action == 'tarpit' and 'tarpit' or self.action:upper()
339 340
end

341
function Filter:mangleoptfrags(ofrags)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
342
   local limit = self:limit()
343
   if not limit then return Filter.super(self):mangleoptfrags(ofrags) end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
344

345 346 347 348 349 350
   local function incompatible(item)
      self:error('Limit incompatible with '..item)
   end

   if self:customtarget() or self:logdefault() then
      incompatible('action: '..self.action)
351
   end
352

353 354 355
   local limitchain = self:uniqueid('limit')
   local limitlog = self[limit].log
   local limitobj = self:create(FilterLimit, self[limit], 'limit')
356

357
   local ofs
358
   local conn = limit == 'conn-limit'
359 360 361
   local target = self:target()
   local ct = conn and target
   local pl = not target and self.log
362

363
   local uofs, sofs = limitobj:recentofrags(limitchain)
364

365
   if uofs then
366
      ofs = self:combinelog(uofs, limitlog, 'drop', 'DROP')
367 368 369 370 371 372 373

      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)))
374

375
   else
376 377
      if pl then incompatible('action or log') end

378
      local limofs = limitobj:limitofrags(limitchain)
379
      ofs = ct and Filter.super(self):mangleoptfrags(limofs) or
380
	 combinations(limofs, {{target='RETURN'}})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
381

382
      extend(ofs, self:actofrags(limitlog, 'DROP'))
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
383
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
384

385
   return self:combine(ofrags, ofs, 'limit', true)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
386 387 388
end


389
local Policy = class(Filter)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
390 391 392 393

function Policy:servoptfrags() return nil end


394 395
local fchains = {{chain='FORWARD'}, {chain='INPUT'}, {chain='OUTPUT'}}

396
local function stateful(config)
397 398
   local res = {}

399 400 401 402
   for i, family in ipairs{'inet', 'inet6'} do

      local er = combinations(
	 fchains,
403
	 {{match='-m conntrack --ctstate ESTABLISHED'}}
404
      )
405 406
      for i, chain in ipairs({'INPUT', 'OUTPUT'}) do
	 table.insert(
407
	    er, {chain=chain, match='-'..chain:sub(1, 1):lower()..' lo'}
408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424
	 )
      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(
425
			Rule.morph{service={sdef}}:servoptfrags(),
426 427 428 429 430 431 432 433
			{{family=family}}
		     )
		     if of[1] then
			assert(#of == 1)
			of[1].target = 'CT --helper '..sdef['ct-helper']
			table.insert(ofrags, of[1])
		     end
		  end
434
	       end
435
	       visited[serv] = true
436 437 438
	    end
	 end
      end
439 440 441 442 443 444 445
      extend(
	 res,
	 combinations(
	    {{table='raw'}},
	    {{chain='PREROUTING'}, {chain='OUTPUT'}},
	    ofrags
	 )
446
      )
447
   end
448 449

   return res
450
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
451

452 453
local icmp = {{family='inet', table='filter', match='-p icmp'}}
local icmp6 = {{family='inet6', table='filter', match='-p icmpv6'}}
454 455 456 457 458 459 460
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'}}))
461 462

local function icmprules(ofrag, oname, types)
463 464
   extend(
      ir,
465 466 467 468 469
      combinations(
         ofrag,
	 {{chain='icmp-routing', target='ACCEPT'}},
	 util.map(types, function(t) return {match='--'..oname..' '..t} end)
      )
470
   )
471 472 473
end
icmprules(icmp, 'icmp-type', {3, 11, 12})
icmprules(icmp6, 'icmpv6-type', {1, 2, 3, 4})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
474

475 476 477 478 479 480 481 482
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(
483
      {{chain='tarpit'}}, {{match='-p tcp', target='TARPIT'}, {target='DROP'}}
484
   )
485
}