filter.lua 12.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
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 42 43
      interval = 1
   end

   if count > RECENT_MAX_COUNT then return end

   local uofs = {}
   local sofs = {}

   for _, family in ipairs{'inet', 'inet6'} do
      if type(self.mask[family].mode) ~= 'table' then return end
      local mask = ''
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
44
      local attr, len = table.unpack(self.mask[family].mode)
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67

      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,
68
	    match='-m recent --name '..name..' --r'..
69 70 71 72 73 74 75 76
	       ({src='source', dest='dest'})[attr]..' --mask '..mask
	 }
      }

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

   return uofs, sofs
end


87 88
local TranslatingRule = class(Rule)

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

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

107 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
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

139

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

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

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

157 158
function LoggingRule:logdefault() return false end

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

function LoggingRule:target()
162
   if self.log then return self:uniqueid('log'..self.action) end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
163 164 165 166 167
   return self:actiontarget()
end

function LoggingRule:logchain(log, action, target)
   if not log then return {}, target end
168
   local chain = self:uniqueid('log'..action)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
169 170

   local ofrags = log:optfrags()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
171
   if target then table.insert(ofrags, {target=target}) end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
172 173

   return combinations({{chain=chain}}, ofrags), chain
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
174 175 176
end

function LoggingRule:extraoptfrags()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
177 178 179
   return self.log and
      self:logchain(self.log, self.action, self:actiontarget()) or
      LoggingRule.super(self):extraoptfrags()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
180 181 182
end


183
local RelatedRule = class(TranslatingRule)
184 185 186 187 188 189 190 191

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

202 203
function RelatedRule:target() return 'ACCEPT' end

204

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
205
local Filter = class(LoggingRule)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
206

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
207
function Filter:init(...)
208
   Filter.super(self):init(...)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
209 210

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

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

228
function Filter:extratrules()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
229 230
   local res = {}

231 232
   local function extrarules(label, cls, options)
      options = options or {}
233 234
      options.attrs = 'dnat'
      extend(res, self:extrarules(label, cls, options))
235 236
   end

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

248 249 250 251
      if self.dnat.addr:find('/') then
	 self:error('DNAT target cannot be a network address')
      end

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

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

277
   if self.action == 'tarpit' or self['no-track'] then
278
      extrarules('no-track', 'no-track')
279
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
280

281
   if self.action == 'accept' then
282
      if self:position() == 'prepend' then
283
	 extrarules('final', LoggingRule, {update={log=self.log}})
284 285
      end

286 287 288 289
      local nr = #res

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

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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
312 313 314
   return res
end

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

function Filter:position()
329 330
   return not self['no-track'] and self:limit() == 'flow-limit'
      and 'prepend' or 'append'
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
331 332
end

333 334 335 336
function Filter:logdefault()
   return contains({'drop', 'reject', 'tarpit'}, self.action)
end

337
function Filter:actiontarget()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
338
   if self.action == 'pass' then return end
339 340
   if self.action ~= 'accept' and not self:logdefault() then
      self:error('Invalid filter action: '..self.action)
341
   end
342
   return self.action == 'tarpit' and 'tarpit' or self.action:upper()
343 344
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
345
function Filter:target()
346
   if self:limit() then return self:uniqueid('limit') end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
347
   return Filter.super(self).target()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
348 349 350 351
end

function Filter:extraoptfrags()
   local limit = self:limit()
352
   if not limit then return Filter.super(self):extraoptfrags() end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
353

354 355 356
   if self.action ~= 'accept' then
      self:error('Cannot specify limit for '..self.action..' filter')
   end
357

358 359 360
   local limitchain = self:uniqueid('limit')
   local limitlog = self[limit].log
   local limitobj = self:create(FilterLimit, self[limit], 'limit')
361

362 363 364
   local ofs = {}
   local logch, limitofs
   local accept = self:position() == 'append'
365

366
   local uofs, sofs = limitobj:recentofrags(limitchain)
367

368 369
   if uofs then
      ofs, logch = self:logchain(limitlog, 'drop', 'DROP')
370

371 372 373 374 375
      limitofs = combinations(uofs, {{target=logch}})
      if accept and self.log then extend(limitofs, self.log:optfrags()) end
      extend(
	 limitofs, combinations(sofs, {{target=accept and 'ACCEPT' or nil}})
      )
376

377 378 379
   else
      if accept then ofs, logch = self:logchain(self.log, 'accept', 'ACCEPT')
      else logch = 'RETURN' end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
380

381 382 383 384 385
      limitofs = combinations(
	 limitobj:limitofrags(limitchain), {{target=logch}}
      )
      if limitlog then extend(limitofs, limitlog:optfrags()) end
      table.insert(limitofs, {target='DROP'})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
386
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
387

388 389
   extend(ofs, combinations({{chain=limitchain}}, limitofs))
   return ofs
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
390 391 392
end


393
local Policy = class(Filter)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
394 395 396 397

function Policy:servoptfrags() return nil end


398 399
local fchains = {{chain='FORWARD'}, {chain='INPUT'}, {chain='OUTPUT'}}

400
local function stateful(config)
401 402
   local res = {}

403 404 405 406
   for i, family in ipairs{'inet', 'inet6'} do

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

   return res
454
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
455

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

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

479 480 481 482 483 484 485 486
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(
487
      {{chain='tarpit'}}, {{match='-p tcp', target='TARPIT'}, {target='DROP'}}
488
   )
489
}