filter.lua 8.95 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
]]--


8
local resolve = require('awall.host')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
9

10
local model = require('awall.model')
11
12
13
local class = model.class
local Rule = model.Rule

14
local combinations = require('awall.optfrag').combinations
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
15

16
local util = require('awall.util')
17
local contains = util.contains
18
local extend = util.extend
19
local listpairs = util.listpairs
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
20

21
22
local RECENT_MAX_COUNT = 20

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
23

24
local RelatedRule = class(Rule)
25
26
27
28
29
30
31
32

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] = {
33
	       family=sdef.family,
34
	       opts='-m conntrack --ctstate RELATED -m helper --helper '..helper
35
36
37
38
39
40
41
	    }
	 end
      end
   end
   return util.values(helpers)
end

42
43
function RelatedRule:target() return 'ACCEPT' end

44

45
local Filter = class(Rule)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
46

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
47
function Filter:init(...)
48
   Filter.super(self):init(...)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
49

50
51
   if not self.action then self.action = 'accept' end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
52
   -- alpine v2.4 compatibility
53
   if contains({'logdrop', 'logreject'}, self.action) then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
54
      self:warning('Deprecated action: '..self.action)
55
      self.action = self.action:sub(4, -1)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
56
57
   end

58
59
   local log = require('awall').loadclass('log').get
   self.log = log(self, self.log, self.action ~= 'accept')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
60

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
61
   local limit = self:limit()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
62
   if limit then
63
64
65
      if limit == 'conn-limit' and self['no-track'] then
	 self:error('Tracking required with connection limit')
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
66
67
68
      if type(self[limit]) ~= 'table' then
	 self[limit] = {count=self[limit]}
      end
69
      self[limit].log = log(self, self[limit].log, true)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
70
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
71
72
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
73
function Filter:destoptfrags()
74
   local ofrags = Filter.super(self):destoptfrags()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
75
76
   if not self.dnat then return ofrags end

77
   ofrags = combinations(ofrags, {{family='inet6'}})
78
   local natof = self:create(model.Zone, {addr=self.dnat}):optfrags('out')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
79
80
81
82
83
84
85
86
   assert(#natof == 1)
   table.insert(ofrags, natof[1])
   return ofrags
end

function Filter:trules()
   local res = {}

87
88
   local function extrarules(cls, extra, src)
      if not src then src = self end
89
      local params = {}
90
91
92
93
      for i, attr in ipairs(
	 {'in', 'out', 'src', 'dest', 'ipset', 'ipsec', 'service'}
      ) do
	 params[attr] = src[attr]
94
      end
95
      util.update(params, extra)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
96
      return extend(res, self:create(cls, params):trules())
97
98
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
99
   if self.dnat then
100
101
102
      if self.action ~= 'accept' then
	 self:error('dnat option not allowed with '..self.action..' action')
      end
103
104
105
      if self['no-track'] then
	 self:error('dnat option not allowed with no-track')
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
106
      if not self.dest then
107
	 self:error('Destination address must be specified with DNAT')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
108
      end
109
      if self.dnat:find('/') then
110
	 self:error('DNAT target cannot be a network address')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
111
112
113
      end
      for i, attr in ipairs({'ipsec', 'ipset'}) do
	 if self[attr] then
114
	    self:error('dnat and '..attr..' options cannot be used simultaneously')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
115
116
117
118
	 end
      end

      local dnataddr
119
      for i, addr in ipairs(resolve(self.dnat, self)) do
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
120
121
	 if addr[1] == 'inet' then
	    if dnataddr then
122
	       self:error(self.dnat..' resolves to multiple IPv4 addresses')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
123
124
125
126
127
	    end
	    dnataddr = addr[2]
	 end
      end
      if not dnataddr then
128
	 self:error(self.dnat..' does not resolve to any IPv4 address')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
129
130
      end

131
      extrarules('dnat', {['to-addr']=dnataddr, out=nil})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
132
133
   end

134
135
136
   if self.action == 'tarpit' or self['no-track'] then
      extrarules('no-track')
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
137

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

140
141
142
143
144
   if self.action == 'accept' then
      local nr = #res

      if self.related then
	 for i, rule in listpairs(self.related) do
145
	    extrarules(RelatedRule, {service=self.service}, rule)
146
147
148
149
	 end
      else
	 -- TODO avoid creating unnecessary RELATED rules by introducing
	 -- helper direction attributes to service definitions
150
151
	 extrarules(RelatedRule)
	 extrarules(RelatedRule, {reverse=true})
152
153
154
155
156
157
158
      end

      if self['no-track'] then
	 if #res > nr then
	    self:error('Tracking required by service')
	 end
	 extrarules('no-track', {reverse=true})
159
	 extrarules('filter', {reverse=true})
160
      end
161
162
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
163
164
165
   return res
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
166
167
168
169
170
function Filter:limit()
   local res
   for i, limit in ipairs({'conn-limit', 'flow-limit'}) do
      if self[limit] then
	 if res then
171
	    self:error('Cannot specify multiple limits for a single filter rule')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
172
173
174
175
176
177
178
179
	 end
	 res = limit
      end
   end
   return res
end

function Filter:position()
180
181
   return not self['no-track'] and self:limit() == 'flow-limit'
      and 'prepend' or 'append'
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
182
183
end

184
185
function Filter:actiontarget()
   if self.action == 'tarpit' then return 'tarpit' end
186
   if contains({'accept', 'drop', 'reject'}, self.action) then
187
      return self.action:upper()
188
   end
189
   self:error('Invalid filter action: '..self.action)
190
191
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
192
function Filter:target()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
193
194
   if self:limit() then return self:newchain('limit') end
   if self.log then return self:newchain('log'..self.action) end
195
   return self:actiontarget()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
196
197
198
199
end

function Filter:extraoptfrags()
   local res = {}
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
200

201
202
203
204
205
206
207
208
   local function logchain(log, action, target)
      if not log then return target end
      local chain = self:newchain('log'..action)
      extend(
	 res,
	 combinations({{chain=chain}}, {log:optfrag(), {target=target}})
      )
      return chain
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
209
210
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
211
212
213
   local limit = self:limit()
   if limit then
      if self.action ~= 'accept' then
214
	 self:error('Cannot specify limit for '..self.action..' filter')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
215
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
216

217
      local chain = self:newchain('limit')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
218
      local limitlog = self[limit].log
219
      local count = self[limit].count
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
220
      local interval = self[limit].interval or 1
221
222
223
224
225
226
227
228
229

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

      local ofrags
      if count > RECENT_MAX_COUNT then
	 ofrags = {
230
	    {
231
	       opts='-m hashlimit --hashlimit-upto '..count..'/second --hashlimit-burst '..count..' --hashlimit-mode srcip --hashlimit-name '..chain,
232
233
234
	       target=logchain(self.log, 'accept', 'ACCEPT')
	    },
	    {target='DROP'}
235
	 }
236
	 if limitlog then table.insert(ofrags, 2, limitlog:optfrag()) end
237
238
239
240
241
242
      else
	 ofrags = combinations(
	    {{opts='-m recent --name '..chain}},
	    {
	       {
		  opts='--update --hitcount '..count..' --seconds '..interval,
243
		  target=logchain(limitlog, 'drop', 'DROP')
244
	       },
245
	       {opts='--set', target='ACCEPT'}
246
247
	    }
	 )
248
	 if self.log then table.insert(ofrags, 2, self.log:optfrag()) end
249
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
250

251
      extend(res, combinations({{chain=chain}}, ofrags))
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
252

253
   else logchain(self.log, self.action, self:actiontarget()) end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
254
   
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
255
256
257
258
259
   return res
end



260
local Policy = class(Filter)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
261
262
263
264

function Policy:servoptfrags() return nil end


265
266
local fchains = {{chain='FORWARD'}, {chain='INPUT'}, {chain='OUTPUT'}}

267
local function stateful(config)
268
269
   local res = {}

270
271
272
273
274
   for i, family in ipairs{'inet', 'inet6'} do

      local er = combinations(
	 fchains,
	 {{opts='-m conntrack --ctstate ESTABLISHED'}}
275
      )
276
277
      for i, chain in ipairs({'INPUT', 'OUTPUT'}) do
	 table.insert(
278
	    er, {chain=chain, opts='-'..chain:sub(1, 1):lower()..' lo'}
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
	 )
      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(
296
			Rule.morph{service={sdef}}:servoptfrags(),
297
298
299
300
301
302
303
304
			{{family=family}}
		     )
		     if of[1] then
			assert(#of == 1)
			of[1].target = 'CT --helper '..sdef['ct-helper']
			table.insert(ofrags, of[1])
		     end
		  end
305
	       end
306
	       visited[serv] = true
307
308
309
	    end
	 end
      end
310
311
312
313
314
315
316
      extend(
	 res,
	 combinations(
	    {{table='raw'}},
	    {{chain='PREROUTING'}, {chain='OUTPUT'}},
	    ofrags
	 )
317
      )
318
   end
319
320

   return res
321
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
322

323
324
local icmp = {{family='inet', table='filter', opts='-p icmp'}}
local icmp6 = {{family='inet6', table='filter', opts='-p icmpv6'}}
325
326
327
328
329
330
331
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'}}))
332
333

local function icmprules(ofrag, oname, types)
334
335
336
337
338
339
340
341
342
   extend(
      ir,
      combinations(ofrag,
		   {{chain='icmp-routing', target='ACCEPT'}},
		   util.map(types,
			    function(t)
			       return {opts='--'..oname..' '..t}
			    end))
   )
343
344
345
end
icmprules(icmp, 'icmp-type', {3, 11, 12})
icmprules(icmp6, 'icmpv6-type', {1, 2, 3, 4})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
346

347
348
349
350
351
352
353
354
355
356
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'}}
   )
357
}