filter.lua 9.47 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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
local LoggingRule = class(TranslatingRule)

function LoggingRule:init(...)
   LoggingRule.super(self):init(...)
   if not self.action then self.action = 'accept' end
   self.log = loadclass('log').get(self, self.log, self.action ~= 'accept')
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


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

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

161 162 163
   if self.action == 'tarpit' or self['no-track'] then
      extrarules('no-track')
   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 168 169 170 171
   if self.action == 'accept' then
      local nr = #res

      if self.related then
	 for i, rule in listpairs(self.related) do
172
	    extrarules(RelatedRule, {service=self.service}, rule)
173 174 175 176
	 end
      else
	 -- TODO avoid creating unnecessary RELATED rules by introducing
	 -- helper direction attributes to service definitions
177 178
	 extrarules(RelatedRule)
	 extrarules(RelatedRule, {reverse=true})
179 180 181 182 183 184 185
      end

      if self['no-track'] then
	 if #res > nr then
	    self:error('Tracking required by service')
	 end
	 extrarules('no-track', {reverse=true})
186
	 extrarules('filter', {reverse=true})
187
      end
188 189
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
190 191 192
   return res
end

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

function Filter:position()
207 208
   return not self['no-track'] and self:limit() == 'flow-limit'
      and 'prepend' or 'append'
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
209 210
end

211 212
function Filter:actiontarget()
   if self.action == 'tarpit' then return 'tarpit' end
213
   if contains({'accept', 'drop', 'reject'}, self.action) then
214
      return self.action:upper()
215
   end
216
   self:error('Invalid filter action: '..self.action)
217 218
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
219
function Filter:target()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
220
   if self:limit() then return self:newchain('limit') end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
221
   return Filter.super(self).target()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
222 223 224 225 226 227
end

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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
231
      local limitchain = self:newchain('limit')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
232
      local limitlog = self[limit].log
233
      local count = self[limit].count
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
234
      local interval = self[limit].interval or 1
235 236 237 238 239 240

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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
241
      local ofrags, logch, limitofs
242
      if count > RECENT_MAX_COUNT then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
243 244
	 ofrags, logch = self:logchain(self.log, 'accept', 'ACCEPT')
	 limitofs = {
245
	    {
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
246 247
	       opts='-m hashlimit --hashlimit-upto '..count..'/second --hashlimit-burst '..count..' --hashlimit-mode srcip --hashlimit-name '..limitchain,
	       target=logch
248 249
	    },
	    {target='DROP'}
250
	 }
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
251
	 if limitlog then table.insert(limitofs, 2, limitlog:optfrag()) end
252
      else
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
253 254 255
	 ofrags, logch = self:logchain(limitlog, 'drop', 'DROP')
	 limitofs = combinations(
	    {{opts='-m recent --name '..limitchain}},
256 257 258
	    {
	       {
		  opts='--update --hitcount '..count..' --seconds '..interval,
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
259
		  target=logch
260
	       },
261
	       {opts='--set', target='ACCEPT'}
262 263
	    }
	 )
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
264
	 if self.log then table.insert(limitofs, 2, self.log:optfrag()) end
265
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
266

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
267 268 269
      extend(ofrags, combinations({{chain=limitchain}}, limitofs))
      return ofrags
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
270

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
271
   return Filter.super(self):extraoptfrags()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
272 273 274
end


275
local Policy = class(Filter)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
276 277 278 279

function Policy:servoptfrags() return nil end


280 281
local fchains = {{chain='FORWARD'}, {chain='INPUT'}, {chain='OUTPUT'}}

282
local function stateful(config)
283 284
   local res = {}

285 286 287 288 289
   for i, family in ipairs{'inet', 'inet6'} do

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

   return res
336
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
337

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

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

362 363 364 365 366 367 368 369 370 371
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'}}
   )
372
}