filter.lua 8.92 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
   if limit then
63 64 65
      if limit == 'conn-limit' and self['no-track'] then
	 self:error('Tracking required with connection limit')
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
66 67 68
      if type(self[limit]) ~= 'table' then
	 self[limit] = {count=self[limit]}
      end
69
      self[limit].log = log(self, self[limit].log, true)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
70
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
71 72
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
73
function Filter:destoptfrags()
74
   local ofrags = Filter.super(self):destoptfrags()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
75 76
   if not self.dnat then return ofrags end

77
   ofrags = combinations(ofrags, {{family='inet6'}})
78
   local natof = self:create(model.Zone, {addr=self.dnat}):optfrags('out')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
79 80 81 82 83 84 85 86
   assert(#natof == 1)
   table.insert(ofrags, natof[1])
   return ofrags
end

function Filter:trules()
   local res = {}

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

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

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

131
      extrarules('dnat', {['to-addr']=dnataddr, out=nil})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
132 133
   end

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

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

140 141 142 143 144
   if self.action == 'accept' then
      local nr = #res

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

      if self['no-track'] then
	 if #res > nr then
	    self:error('Tracking required by service')
	 end
	 extrarules('no-track', {reverse=true})
159
	 extrarules('filter', {reverse=true})
160
      end
161 162
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
163 164 165
   return res
end

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

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

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

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

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

200 201 202 203 204 205 206 207
   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
208 209
   end

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

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

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

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

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

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



259
local Policy = class(Filter)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
260 261 262 263

function Policy:servoptfrags() return nil end


264 265
local fchains = {{chain='FORWARD'}, {chain='INPUT'}, {chain='OUTPUT'}}

266
local function stateful(config)
267 268
   local res = {}

269 270 271 272 273
   for i, family in ipairs{'inet', 'inet6'} do

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

   return res
320
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
321

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

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

346 347 348 349 350 351 352 353 354 355
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'}}
   )
356
}