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
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
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')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
60

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
61
   local limit = self:limit()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
67
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
68 69
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
70
function Filter:destoptfrags()
71
   local ofrags = Filter.super(self):destoptfrags()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
103
      if not self.dest then
104
	 self:error('Destination address must be specified with DNAT')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
105
      end
106
      if self.dnat:find('/') then
107
	 self:error('DNAT target cannot be a network address')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
112 113 114 115
	 end
      end

      local dnataddr
116
      for i, addr in ipairs(resolve(self.dnat, self)) do
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
117 118
	 if addr[1] == 'inet' then
	    if dnataddr then
119
	       self:error(self.dnat..' resolves to multiple IPv4 addresses')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
126 127
      end

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

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

135
   extend(res, Filter.super(self):trules())
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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
}