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


Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
8
local loadclass = require('awall').loadclass
9
local resolve = require('awall.host')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
10

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

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

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

22 23
local RECENT_MAX_COUNT = 20

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


Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
41 42 43 44 45
local LoggingRule = class(TranslatingRule)

function LoggingRule:init(...)
   LoggingRule.super(self):init(...)
   if not self.action then self.action = 'accept' end
46 47 48
   if type(self.log) ~= 'table' then
      self.log = loadclass('log').get(self, self.log, self.action ~= 'accept')
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
49 50 51 52 53
end

function LoggingRule:actiontarget() return 'ACCEPT' end

function LoggingRule:target()
54
   if self.log then return self:uniqueid('log'..self.action) end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
55 56 57 58 59
   return self:actiontarget()
end

function LoggingRule:logchain(log, action, target)
   if not log then return {}, target end
60
   local chain = self:uniqueid('log'..action)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
61 62 63 64 65 66 67 68
   return combinations({{chain=chain}}, {log:optfrag(), {target=target}}), chain
end

function LoggingRule:extraoptfrags()
   return self:logchain(self.log, self.action, self:actiontarget())
end


69
local RelatedRule = class(TranslatingRule)
70 71 72 73 74 75 76 77

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] = {
78
	       family=sdef.family,
79
	       opts='-m conntrack --ctstate RELATED -m helper --helper '..helper
80 81 82 83 84 85 86
	    }
	 end
      end
   end
   return util.values(helpers)
end

87 88
function RelatedRule:target() return 'ACCEPT' end

89

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
90
local Filter = class(LoggingRule)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
91

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
92
function Filter:init(...)
93
   Filter.super(self):init(...)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
94 95

   -- alpine v2.4 compatibility
96
   if contains({'logdrop', 'logreject'}, self.action) then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
97
      self:warning('Deprecated action: '..self.action)
98
      self.action = self.action:sub(4, -1)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
99 100 101
   end

   local limit = self:limit()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
102
   if limit then
103 104 105
      if limit == 'conn-limit' and self['no-track'] then
	 self:error('Tracking required with connection limit')
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
106 107 108
      if type(self[limit]) ~= 'table' then
	 self[limit] = {count=self[limit]}
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
109
      self[limit].log = loadclass('log').get(self, self[limit].log, true)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
110
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
111 112
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
113 114 115
function Filter:trules()
   local res = {}

116 117
   local function extrarules(label, cls, options)
      options = options or {}
118

119 120 121 122 123
      local params = {}
      for i, attr in ipairs(
	 {'in', 'out', 'src', 'dest', 'dnat', 'ipset', 'ipsec', 'service'}
      ) do
	 params[attr] = (options.src or self)[attr]
124
      end
125 126 127 128
      util.update(params, options.update)
      if options.discard then params[options.discard] = nil end

      extend(res, self:create(cls, params, label, options.index):trules())
129 130
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
131
   if self.dnat then
132 133 134
      if self.action ~= 'accept' then
	 self:error('dnat option not allowed with '..self.action..' action')
      end
135 136 137
      if self['no-track'] then
	 self:error('dnat option not allowed with no-track')
      end
138
      if self.dnat:find('/') then
139
	 self:error('DNAT target cannot be a network address')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
140 141 142
      end
      for i, attr in ipairs({'ipsec', 'ipset'}) do
	 if self[attr] then
143
	    self:error('dnat and '..attr..' options cannot be used simultaneously')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
144 145 146 147
	 end
      end

      local dnataddr
148
      for i, addr in ipairs(resolve(self.dnat, self)) do
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
149 150
	 if addr[1] == 'inet' then
	    if dnataddr then
151
	       self:error(self.dnat..' resolves to multiple IPv4 addresses')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
152 153 154 155 156
	    end
	    dnataddr = addr[2]
	 end
      end
      if not dnataddr then
157
	 self:error(self.dnat..' does not resolve to any IPv4 address')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
158 159
      end

160
      extrarules('dnat', 'dnat', {update={['to-addr']=dnataddr}, discard='out'})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
161 162
   end

163
   if self.action == 'tarpit' or self['no-track'] then
164
      extrarules('no-track', 'no-track')
165
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
166

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

169
   if self.action == 'accept' then
170
      if self:position() == 'prepend' then
171
	 extrarules('final', LoggingRule, {update={log=self.log}})
172 173
      end

174 175 176 177
      local nr = #res

      if self.related then
	 for i, rule in listpairs(self.related) do
178
	    extrarules(
179 180 181
	       'related',
	       RelatedRule,
	       {index=i, src=rule, update={service=self.service}}
182
	    )
183 184 185 186
	 end
      else
	 -- TODO avoid creating unnecessary RELATED rules by introducing
	 -- helper direction attributes to service definitions
187
	 extrarules('related', RelatedRule)
188
	 extrarules('related-reply', RelatedRule, {update={reverse=true}})
189 190 191 192 193 194
      end

      if self['no-track'] then
	 if #res > nr then
	    self:error('Tracking required by service')
	 end
195 196
	 extrarules('no-track-reply', 'no-track', {update={reverse=true}})
	 extrarules('reply', 'filter', {update={reverse=true}})
197
      end
198 199
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
200 201 202
   return res
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
203 204 205 206 207
function Filter:limit()
   local res
   for i, limit in ipairs({'conn-limit', 'flow-limit'}) do
      if self[limit] then
	 if res then
208
	    self:error('Cannot specify multiple limits for a single filter rule')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
209 210 211 212 213 214 215 216
	 end
	 res = limit
      end
   end
   return res
end

function Filter:position()
217 218
   return not self['no-track'] and self:limit() == 'flow-limit'
      and 'prepend' or 'append'
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
219 220
end

221 222
function Filter:actiontarget()
   if self.action == 'tarpit' then return 'tarpit' end
223
   if contains({'accept', 'drop', 'reject'}, self.action) then
224
      return self.action:upper()
225
   end
226
   self:error('Invalid filter action: '..self.action)
227 228
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
229
function Filter:target()
230
   if self:limit() then return self:uniqueid('limit') end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
231
   return Filter.super(self).target()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
232 233 234 235 236 237
end

function Filter:extraoptfrags()
   local limit = self:limit()
   if limit then
      if self.action ~= 'accept' then
238
	 self:error('Cannot specify limit for '..self.action..' filter')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
239
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
240

241
      local limitchain = self:uniqueid('limit')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
242
      local limitlog = self[limit].log
243
      local count = self[limit].count
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
244
      local interval = self[limit].interval or 1
245 246 247 248 249 250

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

251 252 253 254
      local ofrags = {}
      local logch, limitofs
      local accept = self:position() == 'append'

255
      if count > RECENT_MAX_COUNT then
256 257 258 259
	 if accept then
	    ofrags, logch = self:logchain(self.log, 'accept', 'ACCEPT')
	 else logch = 'RETURN' end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
260
	 limitofs = {
261
	    {
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
262 263
	       opts='-m hashlimit --hashlimit-upto '..count..'/second --hashlimit-burst '..count..' --hashlimit-mode srcip --hashlimit-name '..limitchain,
	       target=logch
264 265
	    },
	    {target='DROP'}
266
	 }
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
267
	 if limitlog then table.insert(limitofs, 2, limitlog:optfrag()) end
268

269
      else
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
270
	 ofrags, logch = self:logchain(limitlog, 'drop', 'DROP')
271

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
272 273
	 limitofs = combinations(
	    {{opts='-m recent --name '..limitchain}},
274 275 276
	    {
	       {
		  opts='--update --hitcount '..count..' --seconds '..interval,
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
277
		  target=logch
278
	       },
279
	       {opts='--set', target=accept and 'ACCEPT' or nil}
280 281
	    }
	 )
282 283 284
	 if accept and self.log then
	    table.insert(limitofs, 2, self.log:optfrag())
	 end
285
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
286

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
287 288 289
      extend(ofrags, combinations({{chain=limitchain}}, limitofs))
      return ofrags
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
290

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
291
   return Filter.super(self):extraoptfrags()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
292 293 294
end


295
local Policy = class(Filter)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
296 297 298 299

function Policy:servoptfrags() return nil end


300 301
local fchains = {{chain='FORWARD'}, {chain='INPUT'}, {chain='OUTPUT'}}

302
local function stateful(config)
303 304
   local res = {}

305 306 307 308 309
   for i, family in ipairs{'inet', 'inet6'} do

      local er = combinations(
	 fchains,
	 {{opts='-m conntrack --ctstate ESTABLISHED'}}
310
      )
311 312
      for i, chain in ipairs({'INPUT', 'OUTPUT'}) do
	 table.insert(
313
	    er, {chain=chain, opts='-'..chain:sub(1, 1):lower()..' lo'}
314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330
	 )
      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(
331
			Rule.morph{service={sdef}}:servoptfrags(),
332 333 334 335 336 337 338 339
			{{family=family}}
		     )
		     if of[1] then
			assert(#of == 1)
			of[1].target = 'CT --helper '..sdef['ct-helper']
			table.insert(ofrags, of[1])
		     end
		  end
340
	       end
341
	       visited[serv] = true
342 343 344
	    end
	 end
      end
345 346 347 348 349 350 351
      extend(
	 res,
	 combinations(
	    {{table='raw'}},
	    {{chain='PREROUTING'}, {chain='OUTPUT'}},
	    ofrags
	 )
352
      )
353
   end
354 355

   return res
356
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
357

358 359
local icmp = {{family='inet', table='filter', opts='-p icmp'}}
local icmp6 = {{family='inet6', table='filter', opts='-p icmpv6'}}
360 361 362 363 364 365 366
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'}}))
367 368

local function icmprules(ofrag, oname, types)
369 370 371 372 373 374 375 376 377
   extend(
      ir,
      combinations(ofrag,
		   {{chain='icmp-routing', target='ACCEPT'}},
		   util.map(types,
			    function(t)
			       return {opts='--'..oname..' '..t}
			    end))
   )
378 379 380
end
icmprules(icmp, 'icmp-type', {3, 11, 12})
icmprules(icmp6, 'icmpv6-type', {1, 2, 3, 4})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
381

382 383 384 385 386 387 388 389 390 391
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'}}
   )
392
}