filter.lua 10.1 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
end

function LoggingRule:actiontarget() return 'ACCEPT' end

function LoggingRule:target()
54
   if self.log then return self:uniqueid('log'..self.action) end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
55 56 57 58 59
   return self:actiontarget()
end

function LoggingRule:logchain(log, action, target)
   if not log then return {}, target end
60
   local chain = self:uniqueid('log'..action)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
61 62 63 64 65 66 67 68
   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
   local function extrarules(label, cls, options)
      options = options or {}
      local key = label..(options.index or '')
121 122 123
      local obj = self.extrarules[key]

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

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

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

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

169
      extrarules('dnat', 'dnat', {update={['to-addr']=dnataddr}, discard='out'})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
170 171
   end

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

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

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

183 184 185 186
      local nr = #res

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

      if self['no-track'] then
	 if #res > nr then
	    self:error('Tracking required by service')
	 end
204 205
	 extrarules('no-track-reply', 'no-track', {update={reverse=true}})
	 extrarules('reply', 'filter', {update={reverse=true}})
206
      end
207 208
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
209 210 211
   return res
end

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

function Filter:position()
226 227
   return not self['no-track'] and self:limit() == 'flow-limit'
      and 'prepend' or 'append'
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
228 229
end

230 231
function Filter:actiontarget()
   if self.action == 'tarpit' then return 'tarpit' end
232
   if contains({'accept', 'drop', 'reject'}, self.action) then
233
      return self.action:upper()
234
   end
235
   self:error('Invalid filter action: '..self.action)
236 237
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
238
function Filter:target()
239
   if self:limit() then return self:uniqueid('limit') end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
240
   return Filter.super(self).target()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
241 242 243 244 245 246
end

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

250
      local limitchain = self:uniqueid('limit')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
251
      local limitlog = self[limit].log
252
      local count = self[limit].count
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
253
      local interval = self[limit].interval or 1
254 255 256 257 258 259

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

260 261 262 263
      local ofrags = {}
      local logch, limitofs
      local accept = self:position() == 'append'

264
      if count > RECENT_MAX_COUNT then
265 266 267 268
	 if accept then
	    ofrags, logch = self:logchain(self.log, 'accept', 'ACCEPT')
	 else logch = 'RETURN' end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
269
	 limitofs = {
270
	    {
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
271 272
	       opts='-m hashlimit --hashlimit-upto '..count..'/second --hashlimit-burst '..count..' --hashlimit-mode srcip --hashlimit-name '..limitchain,
	       target=logch
273 274
	    },
	    {target='DROP'}
275
	 }
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
276
	 if limitlog then table.insert(limitofs, 2, limitlog:optfrag()) end
277

278
      else
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
279
	 ofrags, logch = self:logchain(limitlog, 'drop', 'DROP')
280

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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
296 297 298
      extend(ofrags, combinations({{chain=limitchain}}, limitofs))
      return ofrags
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
299

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
300
   return Filter.super(self):extraoptfrags()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
301 302 303
end


304
local Policy = class(Filter)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
305 306 307 308

function Policy:servoptfrags() return nil end


309 310
local fchains = {{chain='FORWARD'}, {chain='INPUT'}, {chain='OUTPUT'}}

311
local function stateful(config)
312 313
   local res = {}

314 315 316 317 318
   for i, family in ipairs{'inet', 'inet6'} do

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

   return res
365
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
366

367 368
local icmp = {{family='inet', table='filter', opts='-p icmp'}}
local icmp6 = {{family='inet6', table='filter', opts='-p icmpv6'}}
369 370 371 372 373 374 375
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'}}))
376 377

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

391 392 393 394 395 396 397 398 399 400
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'}}
   )
401
}