filter.lua 10.1 KB
Newer Older
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
1 2
--[[
Filter module for Alpine Wall
3
Copyright (C) 2012-2014 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
local RECENT_MAX_COUNT = 20

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
24

25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
local TranslatingRule = class(Rule)

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(
      model.Zone, {addr=self.dnat}
   ):optfrags(self:direction('out'))
   assert(#natof == 1)
   table.insert(ofrags, natof[1])
   return ofrags
end


Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
41 42 43 44 45
local LoggingRule = class(TranslatingRule)

function LoggingRule:init(...)
   LoggingRule.super(self):init(...)
   if not self.action then self.action = 'accept' end
46 47 48
   if type(self.log) ~= 'table' then
      self.log = loadclass('log').get(self, self.log, self.action ~= 'accept')
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
end

function LoggingRule:actiontarget() return 'ACCEPT' end

function LoggingRule:target()
   if self.log then return self:newchain('log'..self.action) end
   return self:actiontarget()
end

function LoggingRule:logchain(log, action, target)
   if not log then return {}, target end
   local chain = self:newchain('log'..action)
   return combinations({{chain=chain}}, {log:optfrag(), {target=target}}), chain
end

function LoggingRule:extraoptfrags()
   return self:logchain(self.log, self.action, self:actiontarget())
end


69
local RelatedRule = class(TranslatingRule)
70 71 72 73 74 75 76 77

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] = {
78
	       family=sdef.family,
79
	       opts='-m conntrack --ctstate RELATED -m helper --helper '..helper
80 81 82 83 84 85 86
	    }
	 end
      end
   end
   return util.values(helpers)
end

87 88
function RelatedRule:target() return 'ACCEPT' end

89

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
90
local Filter = class(LoggingRule)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
91

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
92
function Filter:init(...)
93
   Filter.super(self):init(...)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
94 95

   -- alpine v2.4 compatibility
96
   if contains({'logdrop', 'logreject'}, self.action) then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
97
      self:warning('Deprecated action: '..self.action)
98
      self.action = self.action:sub(4, -1)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
99 100 101
   end

   local limit = self:limit()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
102
   if limit then
103 104 105
      if limit == 'conn-limit' and self['no-track'] then
	 self:error('Tracking required with connection limit')
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
106 107 108
      if type(self[limit]) ~= 'table' then
	 self[limit] = {count=self[limit]}
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
109
      self[limit].log = loadclass('log').get(self, self[limit].log, true)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
110
   end
111 112

   self.extrarules = {}
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
113 114
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
115 116 117
function Filter:trules()
   local res = {}

118
   local function extrarules(key, cls, options)
119 120 121
      local obj = self.extrarules[key]

      if not obj then
122 123
	 options = options or {}

124
	 local params = {label=(self.label and self.label..'-' or '')..key}
125 126 127
	 for i, attr in ipairs(
	    {'in', 'out', 'src', 'dest', 'dnat', 'ipset', 'ipsec', 'service'}
         ) do
128
	    params[attr] = (options.src or self)[attr]
129
	 end
130 131
	 util.update(params, options.update)
	 if options.discard then params[options.discard] = nil end
132 133 134

	 obj = self:create(cls, params)
	 self.extrarules[key] = obj
135
      end
136 137
      
      extend(res, obj:trules())
138 139
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
140
   if self.dnat then
141 142 143
      if self.action ~= 'accept' then
	 self:error('dnat option not allowed with '..self.action..' action')
      end
144 145 146
      if self['no-track'] then
	 self:error('dnat option not allowed with no-track')
      end
147
      if self.dnat:find('/') then
148
	 self:error('DNAT target cannot be a network address')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
149 150 151
      end
      for i, attr in ipairs({'ipsec', 'ipset'}) do
	 if self[attr] then
152
	    self:error('dnat and '..attr..' options cannot be used simultaneously')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
153 154 155 156
	 end
      end

      local dnataddr
157
      for i, addr in ipairs(resolve(self.dnat, self)) do
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
158 159
	 if addr[1] == 'inet' then
	    if dnataddr then
160
	       self:error(self.dnat..' resolves to multiple IPv4 addresses')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
161 162 163 164 165
	    end
	    dnataddr = addr[2]
	 end
      end
      if not dnataddr then
166
	 self:error(self.dnat..' does not resolve to any IPv4 address')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
167 168
      end

169
      extrarules('dnat', 'dnat', {update={['to-addr']=dnataddr}, discard='out'})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
170 171
   end

172
   if self.action == 'tarpit' or self['no-track'] then
173
      extrarules('no-track', 'no-track')
174
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
175

176
   extend(res, Filter.super(self):trules())
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
177

178
   if self.action == 'accept' then
179
      if self:position() == 'prepend' then
180
	 extrarules('final', LoggingRule, {update={log=self.log}})
181 182
      end

183 184 185 186
      local nr = #res

      if self.related then
	 for i, rule in listpairs(self.related) do
187 188 189
	    extrarules(
	       'related', RelatedRule, {src=rule, update={service=self.service}}
	    )
190 191 192 193
	 end
      else
	 -- TODO avoid creating unnecessary RELATED rules by introducing
	 -- helper direction attributes to service definitions
194
	 extrarules('related', RelatedRule)
195
	 extrarules('related-reply', RelatedRule, {update={reverse=true}})
196 197 198 199 200 201
      end

      if self['no-track'] then
	 if #res > nr then
	    self:error('Tracking required by service')
	 end
202 203
	 extrarules('no-track-reply', 'no-track', {update={reverse=true}})
	 extrarules('reply', 'filter', {update={reverse=true}})
204
      end
205 206
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
207 208 209
   return res
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
210 211 212 213 214
function Filter:limit()
   local res
   for i, limit in ipairs({'conn-limit', 'flow-limit'}) do
      if self[limit] then
	 if res then
215
	    self:error('Cannot specify multiple limits for a single filter rule')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
216 217 218 219 220 221 222 223
	 end
	 res = limit
      end
   end
   return res
end

function Filter:position()
224 225
   return not self['no-track'] and self:limit() == 'flow-limit'
      and 'prepend' or 'append'
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
226 227
end

228 229
function Filter:actiontarget()
   if self.action == 'tarpit' then return 'tarpit' end
230
   if contains({'accept', 'drop', 'reject'}, self.action) then
231
      return self.action:upper()
232
   end
233
   self:error('Invalid filter action: '..self.action)
234 235
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
236
function Filter:target()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
237
   if self:limit() then return self:newchain('limit') end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
238
   return Filter.super(self).target()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
239 240 241 242 243 244
end

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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
248
      local limitchain = self:newchain('limit')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
249
      local limitlog = self[limit].log
250
      local count = self[limit].count
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
251
      local interval = self[limit].interval or 1
252 253 254 255 256 257

      if count > RECENT_MAX_COUNT then
	 count = math.ceil(count / interval)
	 interval = 1
      end

258 259 260 261
      local ofrags = {}
      local logch, limitofs
      local accept = self:position() == 'append'

262
      if count > RECENT_MAX_COUNT then
263 264 265 266
	 if accept then
	    ofrags, logch = self:logchain(self.log, 'accept', 'ACCEPT')
	 else logch = 'RETURN' end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
267
	 limitofs = {
268
	    {
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
269 270
	       opts='-m hashlimit --hashlimit-upto '..count..'/second --hashlimit-burst '..count..' --hashlimit-mode srcip --hashlimit-name '..limitchain,
	       target=logch
271 272
	    },
	    {target='DROP'}
273
	 }
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
274
	 if limitlog then table.insert(limitofs, 2, limitlog:optfrag()) end
275

276
      else
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
277
	 ofrags, logch = self:logchain(limitlog, 'drop', 'DROP')
278

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
279 280
	 limitofs = combinations(
	    {{opts='-m recent --name '..limitchain}},
281 282 283
	    {
	       {
		  opts='--update --hitcount '..count..' --seconds '..interval,
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
284
		  target=logch
285
	       },
286
	       {opts='--set', target=accept and 'ACCEPT' or nil}
287 288
	    }
	 )
289 290 291
	 if accept and self.log then
	    table.insert(limitofs, 2, self.log:optfrag())
	 end
292
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
293

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
294 295 296
      extend(ofrags, combinations({{chain=limitchain}}, limitofs))
      return ofrags
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
297

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
298
   return Filter.super(self):extraoptfrags()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
299 300 301
end


302
local Policy = class(Filter)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
303 304 305 306

function Policy:servoptfrags() return nil end


307 308
local fchains = {{chain='FORWARD'}, {chain='INPUT'}, {chain='OUTPUT'}}

309
local function stateful(config)
310 311
   local res = {}

312 313 314 315 316
   for i, family in ipairs{'inet', 'inet6'} do

      local er = combinations(
	 fchains,
	 {{opts='-m conntrack --ctstate ESTABLISHED'}}
317
      )
318 319
      for i, chain in ipairs({'INPUT', 'OUTPUT'}) do
	 table.insert(
320
	    er, {chain=chain, opts='-'..chain:sub(1, 1):lower()..' lo'}
321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337
	 )
      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(
338
			Rule.morph{service={sdef}}:servoptfrags(),
339 340 341 342 343 344 345 346
			{{family=family}}
		     )
		     if of[1] then
			assert(#of == 1)
			of[1].target = 'CT --helper '..sdef['ct-helper']
			table.insert(ofrags, of[1])
		     end
		  end
347
	       end
348
	       visited[serv] = true
349 350 351
	    end
	 end
      end
352 353 354 355 356 357 358
      extend(
	 res,
	 combinations(
	    {{table='raw'}},
	    {{chain='PREROUTING'}, {chain='OUTPUT'}},
	    ofrags
	 )
359
      )
360
   end
361 362

   return res
363
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
364

365 366
local icmp = {{family='inet', table='filter', opts='-p icmp'}}
local icmp6 = {{family='inet6', table='filter', opts='-p icmpv6'}}
367 368 369 370 371 372 373
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'}}))
374 375

local function icmprules(ofrag, oname, types)
376 377 378 379 380 381 382 383 384
   extend(
      ir,
      combinations(ofrag,
		   {{chain='icmp-routing', target='ACCEPT'}},
		   util.map(types,
			    function(t)
			       return {opts='--'..oname..' '..t}
			    end))
   )
385 386 387
end
icmprules(icmp, 'icmp-type', {3, 11, 12})
icmprules(icmp6, 'icmpv6-type', {1, 2, 3, 4})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
388

389 390 391 392 393 394 395 396 397 398
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(
      {{chain='tarpit'}}, {{opts='-p tcp', target='TARPIT'}, {target='DROP'}}
   )
399
}