filter.lua 7.89 KB
Newer Older
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
1
2
--[[
Filter module for Alpine Wall
3
Copyright (C) 2012-2013 Kaarle Ritvanen
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
4
5
6
7
8
9
Licensed under the terms of GPL2
]]--


module(..., package.seeall)

10
11
12
local resolve = require('awall.host').resolve
local model = require('awall.model')
local combinations = require('awall.optfrag').combinations
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
13

14
15
local util = require('awall.util')
local extend = util.extend
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
16

17
18
local RECENT_MAX_COUNT = 20

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
19

20
local Log = model.class(model.ConfigObject)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
21

22
function Log:optfrag()
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
   local optmap = {
      log={level='level', prefix='prefix'},
      nflog={
	 group='group',
	 prefix='prefix',
	 range='range',
	 threshold='threshold'
      },
      ulog={
	 group='nlgroup',
	 prefix='prefix',
	 range='cprange',
	 threshold='qthreshold'
      }
   }

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
39
   local mode = self.mode or 'log'
40
41
   if not optmap[mode] then self:error('Invalid logging mode: '..mode) end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
   local selector, opts
   for i, sel in ipairs{'every', 'limit', 'probability'} do
      local value = self[sel]
      if value then
	 if selector then
	    self:error('Cannot combine '..sel..' with '..selector)
	 end
	 selector = sel

	 if sel == 'every' then
	    opts = '-m statistic --mode nth --every '..value..' --packet 0'
	 elseif sel == 'limit' then
	    opts = '-m limit --limit '..value..'/second'
	 elseif sel == 'probability' then
	    opts = '-m statistic --mode random --probability '..value
	 else assert(false) end
      end
   end

61
62
63
64
65
66
   local target = string.upper(mode)

   for s, t in pairs(optmap[mode]) do
      if self[s] then target = target..' --'..mode..'-'..t..' '..self[s] end
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
67
   return {opts=opts, target=target}
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
68
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
69

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
70
71
72

local Filter = model.class(model.Rule)

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
function Filter:init(...)
   model.Rule.init(self, unpack(arg))

   -- alpine v2.4 compatibility
   if util.contains({'logdrop', 'logreject'}, self.action) then
      self:warning('Deprecated action: '..self.action)
      self.action = string.sub(self.action, 4, -1)
   end

   local function log(spec, default)
      if spec == nil then spec = default end
      if spec == false then return end
      if spec == true then spec = '_default' end
      return self.root.log[spec] or self:error('Invalid log: '..spec)
   end

   self.log = log(self.log, self.action ~= 'accept')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
90

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
91
   local limit = self:limit()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
92
93
94
95
96
97
   if limit then
      if type(self[limit]) ~= 'table' then
	 self[limit] = {count=self[limit]}
      end
      self[limit].log = log(self[limit].log, true)
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
98
99
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
100
101
102
103
function Filter:destoptfrags()
   local ofrags = model.Rule.destoptfrags(self)
   if not self.dnat then return ofrags end

104
   ofrags = combinations(ofrags, {{family='inet6'}})
105
   local natof = self:create(model.Zone, {addr=self.dnat}):optfrags('out')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
106
107
108
109
110
111
112
113
   assert(#natof == 1)
   table.insert(ofrags, natof[1])
   return ofrags
end

function Filter:trules()
   local res = {}

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

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

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

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

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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
163
   extend(res, model.Rule.trules(self))
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
164

165
166
167
168
169
   if self['no-track'] and self.action == 'accept' then
      extrarules('no-track', {reverse=true})
      extrarules('filter', {reverse=true, action='accept', log=false})
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
170
171
172
   return res
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
173
174
175
176
177
function Filter:limit()
   local res
   for i, limit in ipairs({'conn-limit', 'flow-limit'}) do
      if self[limit] then
	 if res then
178
	    self:error('Cannot specify multiple limits for a single filter rule')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
179
180
181
182
183
184
185
186
187
188
189
190
	 end
	 res = limit
      end
   end
   return res
end

function Filter:position()
   return self:limit() == 'flow-limit' and 'prepend' or 'append'
end

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

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

199
200
201
202
203
204
205
206
   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
207
208
   end

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

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

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

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

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

251
   else logchain(self.log, self.action, model.Rule.target(self)) end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
252
   
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
253
254
255
256
257
258
259
260
261
262
   return res
end



local Policy = model.class(Filter)

function Policy:servoptfrags() return nil end


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

local dar = combinations(fchains,
266
			 {{opts='-m conntrack --ctstate RELATED,ESTABLISHED'}})
267
268
269
270
271
for i, chain in ipairs({'INPUT', 'OUTPUT'}) do
   table.insert(dar,
		{chain=chain,
		 opts='-'..string.lower(string.sub(chain, 1, 1))..' lo'})
end
272
273
274
275
276
dar = combinations(
   dar,
   {{table='filter', target='ACCEPT'}},
   {{family='inet'}, {family='inet6'}}
)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
277

278
279
local icmp = {{family='inet', table='filter', opts='-p icmp'}}
local icmp6 = {{family='inet6', table='filter', opts='-p icmpv6'}}
280
281
282
283
284
285
286
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'}}))
287
288

local function icmprules(ofrag, oname, types)
289
290
291
292
293
294
295
296
297
   extend(
      ir,
      combinations(ofrag,
		   {{chain='icmp-routing', target='ACCEPT'}},
		   util.map(types,
			    function(t)
			       return {opts='--'..oname..' '..t}
			    end))
   )
298
299
300
end
icmprules(icmp, 'icmp-type', {3, 11, 12})
icmprules(icmp6, 'icmpv6-type', {1, 2, 3, 4})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
301

302
303
304
305
306
307
308
309
export = {
   filter={class=Filter, before={'dnat', 'no-track'}},
   log={class=Log},
   policy={class=Policy, after='%filter-after'},
   ['%filter-before']={rules=dar, before='filter'},
   ['%filter-after']={rules=ir, after='filter'}
}

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
310
311
312
achains = combinations({{chain='tarpit'}},
		       {{opts='-p tcp', target='TARPIT'},
			{target='DROP'}})
313