filter.lua 9.93 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
119
120
121
122
   local function extrarules(key, cls, extra, src)
      local obj = self.extrarules[key]

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

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

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

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

167
      extrarules('dnat', 'dnat', {['to-addr']=dnataddr, out=nil})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
168
169
   end

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

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

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

181
182
183
184
      local nr = #res

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

      if self['no-track'] then
	 if #res > nr then
	    self:error('Tracking required by service')
	 end
198
199
	 extrarules('no-track-reply', 'no-track', {reverse=true})
	 extrarules('reply', 'filter', {reverse=true})
200
      end
201
202
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
203
204
205
   return res
end

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

function Filter:position()
220
221
   return not self['no-track'] and self:limit() == 'flow-limit'
      and 'prepend' or 'append'
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
222
223
end

224
225
function Filter:actiontarget()
   if self.action == 'tarpit' then return 'tarpit' end
226
   if contains({'accept', 'drop', 'reject'}, self.action) then
227
      return self.action:upper()
228
   end
229
   self:error('Invalid filter action: '..self.action)
230
231
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
232
function Filter:target()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
233
   if self:limit() then return self:newchain('limit') end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
234
   return Filter.super(self).target()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
235
236
237
238
239
240
end

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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
244
      local limitchain = self:newchain('limit')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
245
      local limitlog = self[limit].log
246
      local count = self[limit].count
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
247
      local interval = self[limit].interval or 1
248
249
250
251
252
253

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

254
255
256
257
      local ofrags = {}
      local logch, limitofs
      local accept = self:position() == 'append'

258
      if count > RECENT_MAX_COUNT then
259
260
261
262
	 if accept then
	    ofrags, logch = self:logchain(self.log, 'accept', 'ACCEPT')
	 else logch = 'RETURN' end

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

272
      else
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
273
	 ofrags, logch = self:logchain(limitlog, 'drop', 'DROP')
274

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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
290
291
292
      extend(ofrags, combinations({{chain=limitchain}}, limitofs))
      return ofrags
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
293

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
294
   return Filter.super(self):extraoptfrags()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
295
296
297
end


298
local Policy = class(Filter)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
299
300
301
302

function Policy:servoptfrags() return nil end


303
304
local fchains = {{chain='FORWARD'}, {chain='INPUT'}, {chain='OUTPUT'}}

305
local function stateful(config)
306
307
   local res = {}

308
309
310
311
312
   for i, family in ipairs{'inet', 'inet6'} do

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

   return res
359
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
360

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

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

385
386
387
388
389
390
391
392
393
394
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'}}
   )
395
}