filter.lua 9.76 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
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
111
112
end

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

116
117
   local function extrarules(cls, extra, src)
      if not src then src = self end
118
      local params = {}
119
      for i, attr in ipairs(
120
	 {'in', 'out', 'src', 'dest', 'dnat', 'ipset', 'ipsec', 'service'}
121
122
      ) do
	 params[attr] = src[attr]
123
      end
124
      util.update(params, extra)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
125
      return extend(res, self:create(cls, params):trules())
126
127
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
128
   if self.dnat then
129
130
131
      if self.action ~= 'accept' then
	 self:error('dnat option not allowed with '..self.action..' action')
      end
132
133
134
      if self['no-track'] then
	 self:error('dnat option not allowed with no-track')
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
135
      if not self.dest then
136
	 self:error('Destination address must be specified with DNAT')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
137
      end
138
      if self.dnat:find('/') then
139
	 self:error('DNAT target cannot be a network address')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
140
141
142
      end
      for i, attr in ipairs({'ipsec', 'ipset'}) do
	 if self[attr] then
143
	    self:error('dnat and '..attr..' options cannot be used simultaneously')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
144
145
146
147
	 end
      end

      local dnataddr
148
      for i, addr in ipairs(resolve(self.dnat, self)) do
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
149
150
	 if addr[1] == 'inet' then
	    if dnataddr then
151
	       self:error(self.dnat..' resolves to multiple IPv4 addresses')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
152
153
154
155
156
	    end
	    dnataddr = addr[2]
	 end
      end
      if not dnataddr then
157
	 self:error(self.dnat..' does not resolve to any IPv4 address')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
158
159
      end

160
      extrarules('dnat', {['to-addr']=dnataddr, out=nil})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
161
162
   end

163
164
165
   if self.action == 'tarpit' or self['no-track'] then
      extrarules('no-track')
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
166

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

169
   if self.action == 'accept' then
170
171
172
173
      if self:position() == 'prepend' then
	 extrarules(LoggingRule, {log=self.log})
      end

174
175
176
177
      local nr = #res

      if self.related then
	 for i, rule in listpairs(self.related) do
178
	    extrarules(RelatedRule, {service=self.service}, rule)
179
180
181
182
	 end
      else
	 -- TODO avoid creating unnecessary RELATED rules by introducing
	 -- helper direction attributes to service definitions
183
184
	 extrarules(RelatedRule)
	 extrarules(RelatedRule, {reverse=true})
185
186
187
188
189
190
191
      end

      if self['no-track'] then
	 if #res > nr then
	    self:error('Tracking required by service')
	 end
	 extrarules('no-track', {reverse=true})
192
	 extrarules('filter', {reverse=true})
193
      end
194
195
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
196
197
198
   return res
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
199
200
201
202
203
function Filter:limit()
   local res
   for i, limit in ipairs({'conn-limit', 'flow-limit'}) do
      if self[limit] then
	 if res then
204
	    self:error('Cannot specify multiple limits for a single filter rule')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
205
206
207
208
209
210
211
212
	 end
	 res = limit
      end
   end
   return res
end

function Filter:position()
213
214
   return not self['no-track'] and self:limit() == 'flow-limit'
      and 'prepend' or 'append'
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
215
216
end

217
218
function Filter:actiontarget()
   if self.action == 'tarpit' then return 'tarpit' end
219
   if contains({'accept', 'drop', 'reject'}, self.action) then
220
      return self.action:upper()
221
   end
222
   self:error('Invalid filter action: '..self.action)
223
224
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
225
function Filter:target()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
226
   if self:limit() then return self:newchain('limit') end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
227
   return Filter.super(self).target()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
228
229
230
231
232
233
end

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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
237
      local limitchain = self:newchain('limit')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
238
      local limitlog = self[limit].log
239
      local count = self[limit].count
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
240
      local interval = self[limit].interval or 1
241
242
243
244
245
246

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

247
248
249
250
      local ofrags = {}
      local logch, limitofs
      local accept = self:position() == 'append'

251
      if count > RECENT_MAX_COUNT then
252
253
254
255
	 if accept then
	    ofrags, logch = self:logchain(self.log, 'accept', 'ACCEPT')
	 else logch = 'RETURN' end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
256
	 limitofs = {
257
	    {
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
258
259
	       opts='-m hashlimit --hashlimit-upto '..count..'/second --hashlimit-burst '..count..' --hashlimit-mode srcip --hashlimit-name '..limitchain,
	       target=logch
260
261
	    },
	    {target='DROP'}
262
	 }
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
263
	 if limitlog then table.insert(limitofs, 2, limitlog:optfrag()) end
264

265
      else
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
266
	 ofrags, logch = self:logchain(limitlog, 'drop', 'DROP')
267

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
268
269
	 limitofs = combinations(
	    {{opts='-m recent --name '..limitchain}},
270
271
272
	    {
	       {
		  opts='--update --hitcount '..count..' --seconds '..interval,
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
273
		  target=logch
274
	       },
275
	       {opts='--set', target=accept and 'ACCEPT' or nil}
276
277
	    }
	 )
278
279
280
	 if accept and self.log then
	    table.insert(limitofs, 2, self.log:optfrag())
	 end
281
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
282

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
283
284
285
      extend(ofrags, combinations({{chain=limitchain}}, limitofs))
      return ofrags
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
286

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
287
   return Filter.super(self):extraoptfrags()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
288
289
290
end


291
local Policy = class(Filter)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
292
293
294
295

function Policy:servoptfrags() return nil end


296
297
local fchains = {{chain='FORWARD'}, {chain='INPUT'}, {chain='OUTPUT'}}

298
local function stateful(config)
299
300
   local res = {}

301
302
303
304
305
   for i, family in ipairs{'inet', 'inet6'} do

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

   return res
352
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
353

354
355
local icmp = {{family='inet', table='filter', opts='-p icmp'}}
local icmp6 = {{family='inet6', table='filter', opts='-p icmpv6'}}
356
357
358
359
360
361
362
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'}}))
363
364

local function icmprules(ofrag, oname, types)
365
366
367
368
369
370
371
372
373
   extend(
      ir,
      combinations(ofrag,
		   {{chain='icmp-routing', target='ACCEPT'}},
		   util.map(types,
			    function(t)
			       return {opts='--'..oname..' '..t}
			    end))
   )
374
375
376
end
icmprules(icmp, 'icmp-type', {3, 11, 12})
icmprules(icmp6, 'icmpv6-type', {1, 2, 3, 4})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
377

378
379
380
381
382
383
384
385
386
387
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'}}
   )
388
}