filter.lua 9.61 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 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
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
39 40 41 42
local LoggingRule = class(TranslatingRule)

function LoggingRule:init(...)
   LoggingRule.super(self):init(...)
43
   util.setdefault(self, 'action', 'accept')
44 45 46
   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
47 48 49 50 51
end

function LoggingRule:actiontarget() return 'ACCEPT' end

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

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

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


67
local RelatedRule = class(TranslatingRule)
68 69 70 71 72 73 74 75

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

85 86
function RelatedRule:target() return 'ACCEPT' end

87

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
88
local Filter = class(LoggingRule)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
89

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

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

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

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

114 115
   local function extrarules(label, cls, options)
      options = options or {}
116

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

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

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

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

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

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

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

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

172 173 174 175
      local nr = #res

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

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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
198 199 200
   return res
end

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

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

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

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

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

239
      local limitchain = self:uniqueid('limit')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
240
      local limitlog = self[limit].log
241
      local limitobj = self:create(model.Limit, self[limit], 'limit')
242

243 244 245 246
      local ofrags = {}
      local logch, limitofs
      local accept = self:position() == 'append'

247
      local uopts, sopts = limitobj:recentopts(limitchain)
248

249
      if uopts then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
250
	 ofrags, logch = self:logchain(limitlog, 'drop', 'DROP')
251

252
	 limitofs = {{opts=uopts, target=logch}}
253
	 if accept and self.log then
254
	    table.insert(limitofs, self.log:optfrag())
255
	 end
256
	 table.insert(limitofs, {opts=sopts, target=accept and 'ACCEPT' or nil})
257 258 259 260 261 262

      else
	 if accept then
	    ofrags, logch = self:logchain(self.log, 'accept', 'ACCEPT')
	 else logch = 'RETURN' end

263 264 265 266 267
	 limitofs = combinations(
	    limitobj:limitofrags(limitchain), {{target=logch}}
	 )
	 if limitlog then table.insert(limitofs, limitlog:optfrag()) end
	 table.insert(limitofs, {target='DROP'})
268
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
269

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
270 271 272
      extend(ofrags, combinations({{chain=limitchain}}, limitofs))
      return ofrags
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
273

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
274
   return Filter.super(self):extraoptfrags()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
275 276 277
end


278
local Policy = class(Filter)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
279 280 281 282

function Policy:servoptfrags() return nil end


283 284
local fchains = {{chain='FORWARD'}, {chain='INPUT'}, {chain='OUTPUT'}}

285
local function stateful(config)
286 287
   local res = {}

288 289 290 291 292
   for i, family in ipairs{'inet', 'inet6'} do

      local er = combinations(
	 fchains,
	 {{opts='-m conntrack --ctstate ESTABLISHED'}}
293
      )
294 295
      for i, chain in ipairs({'INPUT', 'OUTPUT'}) do
	 table.insert(
296
	    er, {chain=chain, opts='-'..chain:sub(1, 1):lower()..' lo'}
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313
	 )
      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(
314
			Rule.morph{service={sdef}}:servoptfrags(),
315 316 317 318 319 320 321 322
			{{family=family}}
		     )
		     if of[1] then
			assert(#of == 1)
			of[1].target = 'CT --helper '..sdef['ct-helper']
			table.insert(ofrags, of[1])
		     end
		  end
323
	       end
324
	       visited[serv] = true
325 326 327
	    end
	 end
      end
328 329 330 331 332 333 334
      extend(
	 res,
	 combinations(
	    {{table='raw'}},
	    {{chain='PREROUTING'}, {chain='OUTPUT'}},
	    ofrags
	 )
335
      )
336
   end
337 338

   return res
339
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
340

341 342
local icmp = {{family='inet', table='filter', opts='-p icmp'}}
local icmp6 = {{family='inet6', table='filter', opts='-p icmpv6'}}
343 344 345 346 347 348 349
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'}}))
350 351

local function icmprules(ofrag, oname, types)
352 353 354 355 356 357 358 359 360
   extend(
      ir,
      combinations(ofrag,
		   {{chain='icmp-routing', target='ACCEPT'}},
		   util.map(types,
			    function(t)
			       return {opts='--'..oname..' '..t}
			    end))
   )
361 362 363
end
icmprules(icmp, 'icmp-type', {3, 11, 12})
icmprules(icmp6, 'icmpv6-type', {1, 2, 3, 4})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
364

365 366 367 368 369 370 371 372 373 374
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'}}
   )
375
}