filter.lua 9.76 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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
end

function LoggingRule:actiontarget() return 'ACCEPT' end

function LoggingRule:target()
   if self.log then return self:newchain('log'..self.action) end
   return self:actiontarget()
end

function LoggingRule:logchain(log, action, target)
   if not log then return {}, target end
   local chain = self:newchain('log'..action)
   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(cls, extra, src)
      if not src then src = self end
118
      local params = {}
119
      for i, attr in ipairs(
120
	 {'in', 'out', 'src', 'dest', 'dnat', 'ipset', 'ipsec', 'service'}
121 122
      ) do
	 params[attr] = src[attr]
123
      end
124
      util.update(params, extra)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
125
      return extend(res, self:create(cls, params):trules())
126 127
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
128
   if self.dnat then
129 130 131
      if self.action ~= 'accept' then
	 self:error('dnat option not allowed with '..self.action..' action')
      end
132 133 134
      if self['no-track'] then
	 self:error('dnat option not allowed with no-track')
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
135
      if not self.dest then
136
	 self:error('Destination address must be specified with DNAT')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
137
      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', {['to-addr']=dnataddr, out=nil})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
161 162
   end

163 164 165
   if self.action == 'tarpit' or self['no-track'] then
      extrarules('no-track')
   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 171 172 173
      if self:position() == 'prepend' then
	 extrarules(LoggingRule, {log=self.log})
      end

174 175 176 177
      local nr = #res

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

      if self['no-track'] then
	 if #res > nr then
	    self:error('Tracking required by service')
	 end
	 extrarules('no-track', {reverse=true})
192
	 extrarules('filter', {reverse=true})
193
      end
194 195
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
196 197 198
   return res
end

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

function Filter:position()
213 214
   return not self['no-track'] and self:limit() == 'flow-limit'
      and 'prepend' or 'append'
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
215 216
end

217 218
function Filter:actiontarget()
   if self.action == 'tarpit' then return 'tarpit' end
219
   if contains({'accept', 'drop', 'reject'}, self.action) then
220
      return self.action:upper()
221
   end
222
   self:error('Invalid filter action: '..self.action)
223 224
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
225
function Filter:target()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
226
   if self:limit() then return self:newchain('limit') end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
227
   return Filter.super(self).target()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
228 229 230 231 232 233
end

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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
237
      local limitchain = self:newchain('limit')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
238
      local limitlog = self[limit].log
239
      local count = self[limit].count
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
240
      local interval = self[limit].interval or 1
241 242 243 244 245 246

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

247 248 249 250
      local ofrags = {}
      local logch, limitofs
      local accept = self:position() == 'append'

251
      if count > RECENT_MAX_COUNT then
252 253 254 255
	 if accept then
	    ofrags, logch = self:logchain(self.log, 'accept', 'ACCEPT')
	 else logch = 'RETURN' end

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

265
      else
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
266
	 ofrags, logch = self:logchain(limitlog, 'drop', 'DROP')
267

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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
283 284 285
      extend(ofrags, combinations({{chain=limitchain}}, limitofs))
      return ofrags
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
286

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
287
   return Filter.super(self):extraoptfrags()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
288 289 290
end


291
local Policy = class(Filter)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
292 293 294 295

function Policy:servoptfrags() return nil end


296 297
local fchains = {{chain='FORWARD'}, {chain='INPUT'}, {chain='OUTPUT'}}

298
local function stateful(config)
299 300
   local res = {}

301 302 303 304 305
   for i, family in ipairs{'inet', 'inet6'} do

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

   return res
352
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
353

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

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

378 379 380 381 382 383 384 385 386 387
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'}}
   )
388
}