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


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
16
local listpairs = util.listpairs
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
17

18 19
local RECENT_MAX_COUNT = 20

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
20

21 22 23 24 25 26 27 28 29
local RelatedRule = model.class(model.Rule)

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

39 40
function RelatedRule:target() return 'ACCEPT' end

41

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
42 43
local Filter = model.class(model.Rule)

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
44 45 46 47 48 49 50 51 52
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

53 54
   local log = require('awall').loadclass('log').get
   self.log = log(self, self.log, self.action ~= 'accept')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
55

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
56
   local limit = self:limit()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
57 58 59 60
   if limit then
      if type(self[limit]) ~= 'table' then
	 self[limit] = {count=self[limit]}
      end
61
      self[limit].log = log(self, self[limit].log, true)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
62
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
63 64
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
65 66 67 68
function Filter:destoptfrags()
   local ofrags = model.Rule.destoptfrags(self)
   if not self.dnat then return ofrags end

69
   ofrags = combinations(ofrags, {{family='inet6'}})
70
   local natof = self:create(model.Zone, {addr=self.dnat}):optfrags('out')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
71 72 73 74 75 76 77 78
   assert(#natof == 1)
   table.insert(ofrags, natof[1])
   return ofrags
end

function Filter:trules()
   local res = {}

79 80
   local function extrarules(cls, extra, src)
      if not src then src = self end
81
      local params = {}
82 83 84 85
      for i, attr in ipairs(
	 {'in', 'out', 'src', 'dest', 'ipset', 'ipsec', 'service'}
      ) do
	 params[attr] = src[attr]
86
      end
87
      util.update(params, extra)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
88
      return extend(res, self:create(cls, params):trules())
89 90
   end

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

      local dnataddr
111
      for i, addr in ipairs(resolve(self.dnat, self)) do
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
112 113
	 if addr[1] == 'inet' then
	    if dnataddr then
114
	       self:error(self.dnat..' resolves to multiple IPv4 addresses')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
115 116 117 118 119
	    end
	    dnataddr = addr[2]
	 end
      end
      if not dnataddr then
120
	 self:error(self.dnat..' does not resolve to any IPv4 address')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
121 122
      end

123
      extrarules('dnat', {['to-addr']=dnataddr, out=nil})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
124 125
   end

126 127 128
   if self.action == 'tarpit' or self['no-track'] then
      extrarules('no-track')
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
129

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

132 133 134 135 136
   if self.action == 'accept' then
      local nr = #res

      if self.related then
	 for i, rule in listpairs(self.related) do
137
	    extrarules(RelatedRule, {service=self.service}, rule)
138 139 140 141
	 end
      else
	 -- TODO avoid creating unnecessary RELATED rules by introducing
	 -- helper direction attributes to service definitions
142 143
	 extrarules(RelatedRule)
	 extrarules(RelatedRule, {reverse=true})
144 145 146 147 148 149 150 151 152
      end

      if self['no-track'] then
	 if #res > nr then
	    self:error('Tracking required by service')
	 end
	 extrarules('no-track', {reverse=true})
	 extrarules('filter', {reverse=true, action='accept', log=false})
      end
153 154
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
155 156 157
   return res
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
158 159 160 161 162
function Filter:limit()
   local res
   for i, limit in ipairs({'conn-limit', 'flow-limit'}) do
      if self[limit] then
	 if res then
163
	    self:error('Cannot specify multiple limits for a single filter rule')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
164 165 166 167 168 169 170 171 172 173 174
	 end
	 res = limit
      end
   end
   return res
end

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

175 176 177 178 179 180 181 182
function Filter:actiontarget()
   if self.action == 'tarpit' then return 'tarpit' end
   if util.contains({'drop', 'reject'}, self.action) then
      return string.upper(self.action)
   end
   return model.Rule.target(self)
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
183
function Filter:target()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
184 185
   if self:limit() then return self:newchain('limit') end
   if self.log then return self:newchain('log'..self.action) end
186
   return self:actiontarget()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
187 188 189 190
end

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

192 193 194 195 196 197 198 199
   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
200 201
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
202 203 204
   local limit = self:limit()
   if limit then
      if self.action ~= 'accept' then
205
	 self:error('Cannot specify limit for '..self.action..' filter')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
206
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
207

208
      local chain = self:newchain('limit')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
209
      local limitlog = self[limit].log
210
      local count = self[limit].count
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
211
      local interval = self[limit].interval or 1
212 213 214 215 216 217 218 219 220

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

      local ofrags
      if count > RECENT_MAX_COUNT then
	 ofrags = {
221
	    {
222
	       opts='-m hashlimit --hashlimit-upto '..count..'/second --hashlimit-burst '..count..' --hashlimit-mode srcip --hashlimit-name '..chain,
223 224 225
	       target=logchain(self.log, 'accept', 'ACCEPT')
	    },
	    {target='DROP'}
226
	 }
227
	 if limitlog then table.insert(ofrags, 2, limitlog:optfrag()) end
228 229 230 231 232 233
      else
	 ofrags = combinations(
	    {{opts='-m recent --name '..chain}},
	    {
	       {
		  opts='--update --hitcount '..count..' --seconds '..interval,
234
		  target=logchain(limitlog, 'drop', 'DROP')
235
	       },
236
	       {opts='--set', target='ACCEPT'}
237 238
	    }
	 )
239
	 if self.log then table.insert(ofrags, 2, self.log:optfrag()) end
240
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
241

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

244
   else logchain(self.log, self.action, self:actiontarget()) end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
245
   
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
246 247 248 249 250 251 252 253 254 255
   return res
end



local Policy = model.class(Filter)

function Policy:servoptfrags() return nil end


256 257
local fchains = {{chain='FORWARD'}, {chain='INPUT'}, {chain='OUTPUT'}}

258 259 260
function stateful(config)
   local res = {}

261 262 263 264 265
   for i, family in ipairs{'inet', 'inet6'} do

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

   return res
316
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
317

318 319
local icmp = {{family='inet', table='filter', opts='-p icmp'}}
local icmp6 = {{family='inet6', table='filter', opts='-p icmpv6'}}
320 321 322 323 324 325 326
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'}}))
327 328

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

342 343 344
export = {
   filter={class=Filter, before={'dnat', 'no-track'}},
   policy={class=Policy, after='%filter-after'},
345
   ['%filter-before']={rules=stateful, before='filter'},
346 347 348
   ['%filter-after']={rules=ir, after='filter'}
}

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
349 350 351
achains = combinations({{chain='tarpit'}},
		       {{opts='-p tcp', target='TARPIT'},
			{target='DROP'}})
352