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 352 353
end

function Filter:extraoptfrags()
   local limit = self:limit()
   if limit then
      if self.action ~= 'accept' then
354
	 self:error('Cannot specify limit for '..self.action..' filter')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
355
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
356

357
      local limitchain = self:uniqueid('limit')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
358
      local limitlog = self[limit].log
359
      local limitobj = self:create(FilterLimit, self[limit], 'limit')
360

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

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

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

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

      else
	 if accept then
378
	    ofs, logch = self:logchain(self.log, 'accept', 'ACCEPT')
379 380
	 else logch = 'RETURN' end

381 382 383
	 limitofs = combinations(
	    limitobj:limitofrags(limitchain), {{target=logch}}
	 )
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
384
	 if limitlog then extend(limitofs, limitlog:optfrags()) end
385
	 table.insert(limitofs, {target='DROP'})
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
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
391

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
392
   return Filter.super(self):extraoptfrags()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
393 394 395
end


396
local Policy = class(Filter)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
397 398 399 400

function Policy:servoptfrags() return nil end


401 402
local fchains = {{chain='FORWARD'}, {chain='INPUT'}, {chain='OUTPUT'}}

403
local function stateful(config)
404 405
   local res = {}

406 407 408 409
   for i, family in ipairs{'inet', 'inet6'} do

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

   return res
457
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
458

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

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

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