filter.lua 9.93 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
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 121 122
   local function extrarules(key, cls, extra, src)
      local obj = self.extrarules[key]

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

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

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

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

167
      extrarules('dnat', 'dnat', {['to-addr']=dnataddr, out=nil})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
168 169
   end

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

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

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

181 182 183 184
      local nr = #res

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

      if self['no-track'] then
	 if #res > nr then
	    self:error('Tracking required by service')
	 end
198 199
	 extrarules('no-track-reply', 'no-track', {reverse=true})
	 extrarules('reply', 'filter', {reverse=true})
200
      end
201 202
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
203 204 205
   return res
end

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

function Filter:position()
220 221
   return not self['no-track'] and self:limit() == 'flow-limit'
      and 'prepend' or 'append'
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
222 223
end

224 225
function Filter:actiontarget()
   if self.action == 'tarpit' then return 'tarpit' end
226
   if contains({'accept', 'drop', 'reject'}, self.action) then
227
      return self.action:upper()
228
   end
229
   self:error('Invalid filter action: '..self.action)
230 231
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
232
function Filter:target()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
233
   if self:limit() then return self:newchain('limit') end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
234
   return Filter.super(self).target()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
235 236 237 238 239 240
end

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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
244
      local limitchain = self:newchain('limit')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
245
      local limitlog = self[limit].log
246
      local count = self[limit].count
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
247
      local interval = self[limit].interval or 1
248 249 250 251 252 253

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

254 255 256 257
      local ofrags = {}
      local logch, limitofs
      local accept = self:position() == 'append'

258
      if count > RECENT_MAX_COUNT then
259 260 261 262
	 if accept then
	    ofrags, logch = self:logchain(self.log, 'accept', 'ACCEPT')
	 else logch = 'RETURN' end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
263
	 limitofs = {
264
	    {
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
265 266
	       opts='-m hashlimit --hashlimit-upto '..count..'/second --hashlimit-burst '..count..' --hashlimit-mode srcip --hashlimit-name '..limitchain,
	       target=logch
267 268
	    },
	    {target='DROP'}
269
	 }
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
270
	 if limitlog then table.insert(limitofs, 2, limitlog:optfrag()) end
271

272
      else
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
273
	 ofrags, logch = self:logchain(limitlog, 'drop', 'DROP')
274

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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
290 291 292
      extend(ofrags, combinations({{chain=limitchain}}, limitofs))
      return ofrags
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
293

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
294
   return Filter.super(self):extraoptfrags()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
295 296 297
end


298
local Policy = class(Filter)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
299 300 301 302

function Policy:servoptfrags() return nil end


303 304
local fchains = {{chain='FORWARD'}, {chain='INPUT'}, {chain='OUTPUT'}}

305
local function stateful(config)
306 307
   local res = {}

308 309 310 311 312
   for i, family in ipairs{'inet', 'inet6'} do

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

   return res
359
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
360

361 362
local icmp = {{family='inet', table='filter', opts='-p icmp'}}
local icmp6 = {{family='inet6', table='filter', opts='-p icmpv6'}}
363 364 365 366 367 368 369
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'}}))
370 371

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

385 386 387 388 389 390 391 392 393 394
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'}}
   )
395
}