filter.lua 9.47 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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
local LoggingRule = class(TranslatingRule)

function LoggingRule:init(...)
   LoggingRule.super(self):init(...)
   if not self.action then self.action = 'accept' end
   self.log = loadclass('log').get(self, self.log, self.action ~= 'accept')
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


67
local RelatedRule = class(TranslatingRule)
68
69
70
71
72
73
74
75

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

85
86
function RelatedRule:target() return 'ACCEPT' end

87

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
88
local Filter = class(LoggingRule)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
89

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

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

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

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

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

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

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

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

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

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

167
168
169
170
171
   if self.action == 'accept' then
      local nr = #res

      if self.related then
	 for i, rule in listpairs(self.related) do
172
	    extrarules(RelatedRule, {service=self.service}, rule)
173
174
175
176
	 end
      else
	 -- TODO avoid creating unnecessary RELATED rules by introducing
	 -- helper direction attributes to service definitions
177
178
	 extrarules(RelatedRule)
	 extrarules(RelatedRule, {reverse=true})
179
180
181
182
183
184
185
      end

      if self['no-track'] then
	 if #res > nr then
	    self:error('Tracking required by service')
	 end
	 extrarules('no-track', {reverse=true})
186
	 extrarules('filter', {reverse=true})
187
      end
188
189
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
190
191
192
   return res
end

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

function Filter:position()
207
208
   return not self['no-track'] and self:limit() == 'flow-limit'
      and 'prepend' or 'append'
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
209
210
end

211
212
function Filter:actiontarget()
   if self.action == 'tarpit' then return 'tarpit' end
213
   if contains({'accept', 'drop', 'reject'}, self.action) then
214
      return self.action:upper()
215
   end
216
   self:error('Invalid filter action: '..self.action)
217
218
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
219
function Filter:target()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
220
   if self:limit() then return self:newchain('limit') end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
221
   return Filter.super(self).target()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
222
223
224
225
226
227
end

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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
231
      local limitchain = self:newchain('limit')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
232
      local limitlog = self[limit].log
233
      local count = self[limit].count
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
234
      local interval = self[limit].interval or 1
235
236
237
238
239
240

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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
241
      local ofrags, logch, limitofs
242
      if count > RECENT_MAX_COUNT then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
243
244
	 ofrags, logch = self:logchain(self.log, 'accept', 'ACCEPT')
	 limitofs = {
245
	    {
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
246
247
	       opts='-m hashlimit --hashlimit-upto '..count..'/second --hashlimit-burst '..count..' --hashlimit-mode srcip --hashlimit-name '..limitchain,
	       target=logch
248
249
	    },
	    {target='DROP'}
250
	 }
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
251
	 if limitlog then table.insert(limitofs, 2, limitlog:optfrag()) end
252
      else
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
253
254
255
	 ofrags, logch = self:logchain(limitlog, 'drop', 'DROP')
	 limitofs = combinations(
	    {{opts='-m recent --name '..limitchain}},
256
257
258
	    {
	       {
		  opts='--update --hitcount '..count..' --seconds '..interval,
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
259
		  target=logch
260
	       },
261
	       {opts='--set', target='ACCEPT'}
262
263
	    }
	 )
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
264
	 if self.log then table.insert(limitofs, 2, self.log:optfrag()) end
265
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
266

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
267
268
269
      extend(ofrags, combinations({{chain=limitchain}}, limitofs))
      return ofrags
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
270

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
271
   return Filter.super(self):extraoptfrags()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
272
273
274
end


275
local Policy = class(Filter)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
276
277
278
279

function Policy:servoptfrags() return nil end


280
281
local fchains = {{chain='FORWARD'}, {chain='INPUT'}, {chain='OUTPUT'}}

282
local function stateful(config)
283
284
   local res = {}

285
286
287
288
289
   for i, family in ipairs{'inet', 'inet6'} do

      local er = combinations(
	 fchains,
	 {{opts='-m conntrack --ctstate ESTABLISHED'}}
290
      )
291
292
      for i, chain in ipairs({'INPUT', 'OUTPUT'}) do
	 table.insert(
293
	    er, {chain=chain, opts='-'..chain:sub(1, 1):lower()..' lo'}
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
	 )
      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(
311
			Rule.morph{service={sdef}}:servoptfrags(),
312
313
314
315
316
317
318
319
			{{family=family}}
		     )
		     if of[1] then
			assert(#of == 1)
			of[1].target = 'CT --helper '..sdef['ct-helper']
			table.insert(ofrags, of[1])
		     end
		  end
320
	       end
321
	       visited[serv] = true
322
323
324
	    end
	 end
      end
325
326
327
328
329
330
331
      extend(
	 res,
	 combinations(
	    {{table='raw'}},
	    {{chain='PREROUTING'}, {chain='OUTPUT'}},
	    ofrags
	 )
332
      )
333
   end
334
335

   return res
336
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
337

338
339
local icmp = {{family='inet', table='filter', opts='-p icmp'}}
local icmp6 = {{family='inet6', table='filter', opts='-p icmpv6'}}
340
341
342
343
344
345
346
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'}}))
347
348

local function icmprules(ofrag, oname, types)
349
350
351
352
353
354
355
356
357
   extend(
      ir,
      combinations(ofrag,
		   {{chain='icmp-routing', target='ACCEPT'}},
		   util.map(types,
			    function(t)
			       return {opts='--'..oname..' '..t}
			    end))
   )
358
359
360
end
icmprules(icmp, 'icmp-type', {3, 11, 12})
icmprules(icmp6, 'icmpv6-type', {1, 2, 3, 4})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
361

362
363
364
365
366
367
368
369
370
371
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'}}
   )
372
}