filter.lua 8.83 KB
Newer Older
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
1 2
--[[
Filter module for Alpine Wall
3
Copyright (C) 2012-2014 Kaarle Ritvanen
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
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
local RelatedRule = class(Rule)
25 26 27 28 29 30 31 32

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

42 43
function RelatedRule:target() return 'ACCEPT' end

44

45
local Filter = class(Rule)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
46

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
47
function Filter:init(...)
48
   Filter.super(self):init(...)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
49

50 51
   if not self.action then self.action = 'accept' end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
52
   -- alpine v2.4 compatibility
53
   if contains({'logdrop', 'logreject'}, self.action) then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
54
      self:warning('Deprecated action: '..self.action)
55
      self.action = self.action:sub(4, -1)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
56 57
   end

58 59
   local log = require('awall').loadclass('log').get
   self.log = log(self, self.log, self.action ~= 'accept')
60

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
61
   local limit = self:limit()
62 63 64 65
   if limit then
      if type(self[limit]) ~= 'table' then
	 self[limit] = {count=self[limit]}
      end
66
      self[limit].log = log(self, self[limit].log, true)
67
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
68 69
end

70
function Filter:destoptfrags()
71
   local ofrags = Filter.super(self):destoptfrags()
72 73
   if not self.dnat then return ofrags end

74
   ofrags = combinations(ofrags, {{family='inet6'}})
75
   local natof = self:create(model.Zone, {addr=self.dnat}):optfrags('out')
76 77 78 79 80 81 82 83
   assert(#natof == 1)
   table.insert(ofrags, natof[1])
   return ofrags
end

function Filter:trules()
   local res = {}

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

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

      local dnataddr
116
      for i, addr in ipairs(resolve(self.dnat, self)) do
117 118
	 if addr[1] == 'inet' then
	    if dnataddr then
119
	       self:error(self.dnat..' resolves to multiple IPv4 addresses')
120 121 122 123 124
	    end
	    dnataddr = addr[2]
	 end
      end
      if not dnataddr then
125
	 self:error(self.dnat..' does not resolve to any IPv4 address')
126 127
      end

128
      extrarules('dnat', {['to-addr']=dnataddr, out=nil})
129 130
   end

131 132 133
   if self.action == 'tarpit' or self['no-track'] then
      extrarules('no-track')
   end
134

135
   extend(res, Filter.super(self):trules())
136

137 138 139 140 141
   if self.action == 'accept' then
      local nr = #res

      if self.related then
	 for i, rule in listpairs(self.related) do
142
	    extrarules(RelatedRule, {service=self.service}, rule)
143 144 145 146
	 end
      else
	 -- TODO avoid creating unnecessary RELATED rules by introducing
	 -- helper direction attributes to service definitions
147 148
	 extrarules(RelatedRule)
	 extrarules(RelatedRule, {reverse=true})
149 150 151 152 153 154 155 156 157
      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
158 159
   end

160 161 162
   return res
end

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

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

180 181
function Filter:actiontarget()
   if self.action == 'tarpit' then return 'tarpit' end
182
   if contains({'accept', 'drop', 'reject'}, self.action) then
183
      return self.action:upper()
184
   end
185
   self:error('Invalid filter action: '..self.action)
186 187
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
188
function Filter:target()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
189 190
   if self:limit() then return self:newchain('limit') end
   if self.log then return self:newchain('log'..self.action) end
191
   return self:actiontarget()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
192 193 194 195
end

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

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

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

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

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

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

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

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



256
local Policy = class(Filter)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
257 258 259 260

function Policy:servoptfrags() return nil end


261 262
local fchains = {{chain='FORWARD'}, {chain='INPUT'}, {chain='OUTPUT'}}

263
local function stateful(config)
264 265
   local res = {}

266 267 268 269 270
   for i, family in ipairs{'inet', 'inet6'} do

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

   return res
317
end
318

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

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

343 344 345 346 347 348 349 350 351 352
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'}}
   )
353
}