filter.lua 9.06 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
local resolve = require('awall.host')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
9

10
local model = require('awall.model')
11 12 13
local class = model.class
local Rule = model.Rule

14
local combinations = require('awall.optfrag').combinations
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
15

16
local util = require('awall.util')
17
local contains = util.contains
18
local extend = util.extend
19
local listpairs = util.listpairs
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
20

21 22
local RECENT_MAX_COUNT = 20

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
23

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


local RelatedRule = class(TranslatingRule)
41 42 43 44 45 46 47 48

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] = {
49
	       family=sdef.family,
50
	       opts='-m conntrack --ctstate RELATED -m helper --helper '..helper
51 52 53 54 55 56 57
	    }
	 end
      end
   end
   return util.values(helpers)
end

58 59
function RelatedRule:target() return 'ACCEPT' end

60

61
local Filter = class(TranslatingRule)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
62

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
63
function Filter:init(...)
64
   Filter.super(self):init(...)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
65

66 67
   if not self.action then self.action = 'accept' end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
68
   -- alpine v2.4 compatibility
69
   if contains({'logdrop', 'logreject'}, self.action) then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
70
      self:warning('Deprecated action: '..self.action)
71
      self.action = self.action:sub(4, -1)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
72 73
   end

74 75
   local log = require('awall').loadclass('log').get
   self.log = log(self, self.log, self.action ~= 'accept')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
76

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
77
   local limit = self:limit()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
78
   if limit then
79 80 81
      if limit == 'conn-limit' and self['no-track'] then
	 self:error('Tracking required with connection limit')
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
82 83 84
      if type(self[limit]) ~= 'table' then
	 self[limit] = {count=self[limit]}
      end
85
      self[limit].log = log(self, self[limit].log, true)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
86
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
87 88
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
89 90 91
function Filter:trules()
   local res = {}

92 93
   local function extrarules(cls, extra, src)
      if not src then src = self end
94
      local params = {}
95
      for i, attr in ipairs(
96
	 {'in', 'out', 'src', 'dest', 'dnat', 'ipset', 'ipsec', 'service'}
97 98
      ) do
	 params[attr] = src[attr]
99
      end
100
      util.update(params, extra)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
101
      return extend(res, self:create(cls, params):trules())
102 103
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
104
   if self.dnat then
105 106 107
      if self.action ~= 'accept' then
	 self:error('dnat option not allowed with '..self.action..' action')
      end
108 109 110
      if self['no-track'] then
	 self:error('dnat option not allowed with no-track')
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
111
      if not self.dest then
112
	 self:error('Destination address must be specified with DNAT')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
113
      end
114
      if self.dnat:find('/') then
115
	 self:error('DNAT target cannot be a network address')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
116 117 118
      end
      for i, attr in ipairs({'ipsec', 'ipset'}) do
	 if self[attr] then
119
	    self:error('dnat and '..attr..' options cannot be used simultaneously')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
120 121 122 123
	 end
      end

      local dnataddr
124
      for i, addr in ipairs(resolve(self.dnat, self)) do
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
125 126
	 if addr[1] == 'inet' then
	    if dnataddr then
127
	       self:error(self.dnat..' resolves to multiple IPv4 addresses')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
128 129 130 131 132
	    end
	    dnataddr = addr[2]
	 end
      end
      if not dnataddr then
133
	 self:error(self.dnat..' does not resolve to any IPv4 address')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
134 135
      end

136
      extrarules('dnat', {['to-addr']=dnataddr, out=nil})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
137 138
   end

139 140 141
   if self.action == 'tarpit' or self['no-track'] then
      extrarules('no-track')
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
142

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

145 146 147 148 149
   if self.action == 'accept' then
      local nr = #res

      if self.related then
	 for i, rule in listpairs(self.related) do
150
	    extrarules(RelatedRule, {service=self.service}, rule)
151 152 153 154
	 end
      else
	 -- TODO avoid creating unnecessary RELATED rules by introducing
	 -- helper direction attributes to service definitions
155 156
	 extrarules(RelatedRule)
	 extrarules(RelatedRule, {reverse=true})
157 158 159 160 161 162 163
      end

      if self['no-track'] then
	 if #res > nr then
	    self:error('Tracking required by service')
	 end
	 extrarules('no-track', {reverse=true})
164
	 extrarules('filter', {reverse=true})
165
      end
166 167
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
168 169 170
   return res
end

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

function Filter:position()
185 186
   return not self['no-track'] and self:limit() == 'flow-limit'
      and 'prepend' or 'append'
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
187 188
end

189 190
function Filter:actiontarget()
   if self.action == 'tarpit' then return 'tarpit' end
191
   if contains({'accept', 'drop', 'reject'}, self.action) then
192
      return self.action:upper()
193
   end
194
   self:error('Invalid filter action: '..self.action)
195 196
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
197
function Filter:target()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
198 199
   if self:limit() then return self:newchain('limit') end
   if self.log then return self:newchain('log'..self.action) end
200
   return self:actiontarget()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
201 202 203 204
end

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

206 207 208 209 210 211 212 213
   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
214 215
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
216 217 218
   local limit = self:limit()
   if limit then
      if self.action ~= 'accept' then
219
	 self:error('Cannot specify limit for '..self.action..' filter')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
220
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
221

222
      local chain = self:newchain('limit')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
223
      local limitlog = self[limit].log
224
      local count = self[limit].count
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
225
      local interval = self[limit].interval or 1
226 227 228 229 230 231 232 233 234

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

      local ofrags
      if count > RECENT_MAX_COUNT then
	 ofrags = {
235
	    {
236
	       opts='-m hashlimit --hashlimit-upto '..count..'/second --hashlimit-burst '..count..' --hashlimit-mode srcip --hashlimit-name '..chain,
237 238 239
	       target=logchain(self.log, 'accept', 'ACCEPT')
	    },
	    {target='DROP'}
240
	 }
241
	 if limitlog then table.insert(ofrags, 2, limitlog:optfrag()) end
242 243 244 245 246 247
      else
	 ofrags = combinations(
	    {{opts='-m recent --name '..chain}},
	    {
	       {
		  opts='--update --hitcount '..count..' --seconds '..interval,
248
		  target=logchain(limitlog, 'drop', 'DROP')
249
	       },
250
	       {opts='--set', target='ACCEPT'}
251 252
	    }
	 )
253
	 if self.log then table.insert(ofrags, 2, self.log:optfrag()) end
254
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
255

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

258
   else logchain(self.log, self.action, self:actiontarget()) end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
259
   
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
260 261 262 263 264
   return res
end



265
local Policy = class(Filter)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
266 267 268 269

function Policy:servoptfrags() return nil end


270 271
local fchains = {{chain='FORWARD'}, {chain='INPUT'}, {chain='OUTPUT'}}

272
local function stateful(config)
273 274
   local res = {}

275 276 277 278 279
   for i, family in ipairs{'inet', 'inet6'} do

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

   return res
326
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
327

328 329
local icmp = {{family='inet', table='filter', opts='-p icmp'}}
local icmp6 = {{family='inet6', table='filter', opts='-p icmpv6'}}
330 331 332 333 334 335 336
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'}}))
337 338

local function icmprules(ofrag, oname, types)
339 340 341 342 343 344 345 346 347
   extend(
      ir,
      combinations(ofrag,
		   {{chain='icmp-routing', target='ACCEPT'}},
		   util.map(types,
			    function(t)
			       return {opts='--'..oname..' '..t}
			    end))
   )
348 349 350
end
icmprules(icmp, 'icmp-type', {3, 11, 12})
icmprules(icmp6, 'icmpv6-type', {1, 2, 3, 4})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
351

352 353 354 355 356 357 358 359 360 361
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'}}
   )
362
}