model.lua 19.8 KB
Newer Older
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
1 2
--[[
Base data model for Alpine Wall
3
Copyright (C) 2012-2017 Kaarle Ritvanen
4
See LICENSE file for license details
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
5 6 7
]]--


8
local M = {}
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
9 10


11 12 13 14
local loadclass = require('awall').loadclass
M.class = require('awall.class')
local resolve = require('awall.host')
local builtin = require('awall.iptables').builtin
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
15

16
local optfrag = require('awall.optfrag')
17
local FAMILIES = optfrag.FAMILIES
18
local combinations = optfrag.combinations
19
local prune = optfrag.prune
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
20

21
local raise = require('awall.uerror').raise
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
22

23 24
local util = require('awall.util')
local contains = util.contains
25
local copy = util.copy
26 27
local extend = util.extend
local filter = util.filter
28
local join = util.join
29
local listpairs = util.listpairs
30
local map = util.map
31
local maplist = util.maplist
32
local setdefault = util.setdefault
33
local sortedkeys = util.sortedkeys
34

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
35

36 37 38
local startswith = require('stringy').startswith


39 40 41
local ADDRLEN = {inet=32, inet6=128}


42 43 44
M.ConfigObject = M.class()

function M.ConfigObject:init(context, location)
45 46
   if context then
      self.context = context
47
      self.root = context.objects
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
48
   end
49
   self.location = location
50 51

   self.extraobjs = {}
52
   self.uniqueids = {}
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
53 54
end

55 56 57 58 59 60 61 62
function M.ConfigObject:create(cls, params, label, index)
   local key
   if label then
      key = label..(index or '')
      local obj = self.extraobjs[key]
      if obj then return obj end
   end

63 64
   if type(cls) == 'string' then
      local name = cls
65
      cls = loadclass(cls)
66 67 68 69
      if not cls then
	 self:error('Support for '..name..' objects not installed')
      end
   end
70

71
   if type(params) ~= 'table' then params = {params} end
72
   params.label = join(self.label, '-', label)
73

74 75 76
   local obj = cls.morph(params, self.context, self.location)
   if key then self.extraobjs[key] = obj end
   return obj
77 78
end

79
function M.ConfigObject:uniqueid(key)
80
   if not key then key = '' end
81 82
   if self.uniqueids[key] then return self.uniqueids[key] end

83
   local lastid = setdefault(self.context, 'lastid', {})
84
   local res = join(key, '-', self.label)
85
   lastid[res] = setdefault(lastid, res, -1) + 1
86 87 88 89 90 91
   res = res..'-'..lastid[res]

   self.uniqueids[key] = res
   return res
end

92
function M.ConfigObject:error(msg) raise(self.location..': '..msg) end
93

94
function M.ConfigObject:warning(msg)
95
   util.printmsg(self.location..': '..msg)
96 97
end

98
function M.ConfigObject:trules() return {} end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
99

100
function M.ConfigObject:info()
101 102 103 104 105 106
   local rules = {}
   for _, trule in ipairs(self:trules()) do
      local loc = optfrag.location(trule)
      table.insert(
	 setdefault(rules, loc, {}), {'  '..loc, optfrag.command(trule)}
      )
107
   end
108 109 110

   local res = {}
   for _, loc in sortedkeys(rules) do extend(res, rules[loc]) end
111 112 113
   return res
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
114

115
M.Zone = M.class(M.ConfigObject)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
116

117
function M.Zone:optfrags(dir)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
118 119 120 121 122 123 124
   local iopt, aopt, iprop, aprop
   if dir == 'in' then
      iopt, aopt, iprop, aprop = 'i', 's', 'in', 'src'
   elseif dir == 'out' then
      iopt, aopt, iprop, aprop = 'o', 'd', 'out', 'dest'
   else assert(false) end

125 126 127
   local aopts = nil
   if self.addr then
      aopts = {}
128 129
      for i, hostdef in listpairs(self.addr) do
	 for i, addr in ipairs(resolve(hostdef, self)) do
130 131 132 133
	    table.insert(
	       aopts,
	       {family=addr[1], [aprop]=addr[2], match='-'..aopt..' '..addr[2]}
	    )
134
	 end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
135 136 137
      end
   end

138 139 140 141
   local popt
   if self.ipsec ~= nil then
      popt = {
	 {
142
	    match='-m policy --dir '..dir..' --pol '..
143 144 145 146 147
	       (self.ipsec and 'ipsec' or 'none')
	 }
      }
   end

148 149 150
   return combinations(
      maplist(
	 self.iface,
151
	 function(x) return {[iprop]=x, match='-'..iopt..' '..x} end
152
      ),
153 154
      aopts,
      popt
155
   )
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
156 157 158
end


159
M.fwzone = M.Zone()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
160 161


162
local IPSet = M.class(M.ConfigObject)
163 164

function IPSet:init(...)
165
   IPSet.super(self):init(...)
166 167 168

   if not self.type then self:error('Type not defined') end

169
   if startswith(self.type, 'bitmap:') then
170 171 172 173
      if not self.range then self:error('Range not defined') end
      self.options = {self.type, 'range', self.range}
      self.family = 'inet'

174
   elseif startswith(self.type, 'hash:') then
175 176 177 178 179 180 181 182 183
      if not self.family then self:error('Family not defined') end
      self.options = {self.type, 'family', self.family}

   elseif self.type == 'list:set' then self.options = {self.type}

   else self:error('Invalid type: '..self.type) end
end


184
M.Rule = M.class(M.ConfigObject)
185

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
186

187 188
function M.Rule:init(...)
   M.Rule.super(self):init(...)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
189 190

   for i, prop in ipairs({'in', 'out'}) do
191 192 193 194 195 196 197 198 199
      self[prop] = self[prop] and maplist(
	 self[prop],
	 function(z)
	    if type(z) ~= 'string' then return z end
	    return z == '_fw' and M.fwzone or
	       self.root.zone[z] or
	       self:error('Invalid zone: '..z)
	 end
      )
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
200
   end
201

202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
   -- alpine v3.4 compatibility
   if self.ipsec then
      if not contains({'in', 'out'}, self.ipsec) then
	 self:error('Invalid ipsec policy direction')
      end
      self:warning('ipsec deprecated in rules, define in zones instead')
      local zones = self[self.ipsec]
      if zones then
	 self[self.ipsec] = maplist(
	    zones,
	    function(z)
	       return self:create(
		  M.Zone, {iface=z.iface, addr=z.addr, ipsec=true}
	       )
	    end
	 )
      else self[self.ipsec] = {self:create(M.Zone, {ipsec=true})} end
      self.ipsec = nil
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
222
   if self.service then
223 224 225 226
      if not self.label and type(self.service) == 'string' then
	 self.label = self.service
      end

227 228 229 230 231 232
      self.service = util.list(self.service)

      for i, serv in ipairs(self.service) do
	 if type(serv) == 'string' then
	    self.service[i] = self.root.service[serv] or
	       self:error('Invalid service: '..serv)
233
	 end
234 235 236 237 238 239 240
	 for i, sdef in listpairs(self.service[i]) do
	    if not sdef.proto then self:error('Protocol not defined') end
	    sdef.proto = (
	       {[1]='icmp', [6]='tcp', [17]='udp', [58]='ipv6-icmp'}
            )[sdef.proto] or sdef.proto
	 end
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
241 242 243 244
   end
end


245
function M.Rule:direction(dir)
246 247 248 249 250 251
   if dir == 'in' then return self.reverse and 'out' or 'in' end
   if dir == 'out' then return self.reverse and 'in' or 'out' end
   self:error('Invalid direction: '..dir)
end


252
function M.Rule:zoneoptfrags()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
253

254
   local function zonepair(zin, zout)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
255

256
      local function zofs(zone, dir)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
257
	 if not zone then return zone end
258
	 return zone:optfrags(dir)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
259 260 261 262
      end

      local chain, ofrags

263
      if zin == M.fwzone or zout == M.fwzone then
264
	 if zin == zout then return {} end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
265
	 local dir, z = 'in', zin
266
	 if zin == M.fwzone then dir, z = 'out', zout end
267
	 chain = dir:upper()..'PUT'
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
268 269
	 ofrags = zofs(z, dir)

270 271 272 273 274 275 276 277 278 279 280
      elseif not zin or not zout then

	 if zin then
	    chain = 'PREROUTING'
	    ofrags = zofs(zin, 'in')

	 elseif zout then
	    chain = 'POSTROUTING'
	    ofrags = zofs(zout, 'out')
	 end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
281 282
      else
	 chain = 'FORWARD'
283 284
	 ofrags = combinations(zofs(zin, 'in'), zofs(zout, 'out'))

285
	 if ofrags and not zout['route-back'] then
286 287 288 289 290 291
	    ofrags = filter(
	       ofrags,
	       function(of)
		  return not (of['in'] and of.out and of['in'] == of.out)
	       end
	    )
292
	 end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
293 294
      end

295 296 297
      return combinations(ofrags,
			  chain and {{chain=chain}} or {{chain='PREROUTING'},
							{chain='OUTPUT'}})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
298 299 300
   end

   local res = {}
301 302
   local izones = self[self:direction('in')] or {}
   local ozones = self[self:direction('out')] or {}
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
303

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
304 305
   for i = 1,math.max(1, #izones) do
      for j = 1,math.max(1, #ozones) do
306
	 extend(res, zonepair(izones[i], ozones[j]))
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
307 308 309 310 311 312 313
      end
   end

   return res
end


314
function M.Rule:servoptfrags()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
315 316 317 318 319

   if not self.service then return end

   local res = {}

320 321 322
   local fports = {}
   map(FAMILIES, function(f) fports[f] = {} end)

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
323
   for i, serv in ipairs(self.service) do
324
      for i, sdef in listpairs(serv) do
325
	 if contains({'tcp', 'udp'}, sdef.proto) then
326 327 328 329 330 331 332 333
	    for family, ports in pairs(fports) do
	       if not sdef.family or family == sdef.family then

		  local new = not ports[sdef.proto]
		  if new then ports[sdef.proto] = {} end

		  if new or ports[sdef.proto][1] then
		     if sdef.port then
334
			extend(
335
			   ports[sdef.proto],
336
			   maplist(
337
			      sdef.port,
338
			      function(p) return tostring(p):gsub('-', ':') end
339 340 341 342 343
			   )
			)
		     else ports[sdef.proto] = {} end
		  end
	       end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
344 345 346 347 348 349 350
	    end

	 else

	    local opts = '-p '..sdef.proto
	    local family = nil

351 352
	    -- TODO multiple ICMP types per rule
	    local oname
353
	    if sdef.proto == 'icmp' then
354 355
	       family = 'inet'
	       oname = 'icmp-type'
356
	    elseif contains({'ipv6-icmp', 'icmpv6'}, sdef.proto) then
357 358
	       family = 'inet6'
	       oname = 'icmpv6-type'
359
	    elseif sdef.type or sdef['reply-type'] then
360
	       self:error('Type specification not valid with '..sdef.proto)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
361 362
	    end

363 364 365 366 367 368 369 370 371
	    if sdef.family then
	       if not family then family = sdef.family
	       elseif family ~= sdef.family then
		  self:error(
		     'Protocol '..sdef.proto..' is incompatible with '..sdef.family
		  )
	       end
	    end

372 373
	    if sdef.type then
	       opts = opts..' --'..oname..' '..(
374
		  self.reverse and sdef['reply-type'] or sdef.type
375 376
	       )
	    end
377
	    table.insert(res, {family=family, match=opts})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
378 379 380 381
	 end
      end
   end

382
   local popt = ' --'..(self.reverse and 's' or 'd')..'port'
383
   for _, family in sortedkeys(fports) do
384
      local ofrags = {}
385
      local pports = fports[family]
386

387
      for _, proto in sortedkeys(pports) do
388
	 local propt = '-p '..proto
389
	 local ports = pports[proto]
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406

	 if ports[1] then
	    local len = #ports
	    repeat
	       local opts

	       if len == 1 then
		  opts = propt..popt..' '..ports[1]
		  len = 0

	       else
		  opts = propt..' -m multiport'..popt..'s '
		  local pc = 0
		  repeat
		     local sep = pc == 0 and '' or ','
		     local port = ports[1]
		     
407
		     pc = pc + (port:find(':') and 2 or 1)
408 409 410 411 412 413 414 415 416
		     if pc > 15 then break end
		     
		     opts = opts..sep..port
		     
		     table.remove(ports, 1)
		     len = len - 1
		  until len == 0
	       end

417
	       table.insert(ofrags, {match=opts})
418 419
	    until len == 0

420
	 else table.insert(ofrags, {match=propt}) end
421
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
422

423
      extend(res, combinations(ofrags, {{family=family}}))
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
424 425 426 427 428
   end

   return res
end

429 430
function M.Rule:destoptfrags()
   return self:create(M.Zone, {addr=self.dest}):optfrags(self:direction('out'))
431 432
end

433
function M.Rule:table() return 'filter' end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
434

435
function M.Rule:position() return 'append' end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
436

437
function M.Rule:target()
438 439 440 441 442 443 444 445 446 447
   -- alpine v2.7 compatibility
   if self.action == 'accept' then
      self:warning("'accept' action deprecated in favor of 'exclude'")
      self.action = 'exclude'
   end

   if self.action == 'exclude' then return 'ACCEPT' end
   if self.action and self.action ~= 'include' then
      self:error('Invalid action: '..self.action)
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
448 449 450
end


451 452 453 454 455 456
function M.Rule:combine(ofs1, ofs2, key, unique)

   local function connect()
      local chain = self:uniqueid(key)
      local function setvar(name)
	 return function(of)
457 458 459
	    local res = copy(of)
	    setdefault(res, name, chain)
	    return res
460 461 462 463 464 465
	 end
      end

      return extend(map(ofs1, setvar('target')), map(ofs2, setvar('chain')))
   end

466 467
   ofs1, ofs2 = prune(ofs1, ofs2)

468
   local chainless = filter(ofs2, function(of) return not of.chain end)
469
   local created
470 471 472 473 474 475 476 477
   local res = {}

   for _, of in ipairs(ofs1) do
      if of.target == nil then

	 local ofs = combinations(chainless, {{family=of.family}})
	 assert(#ofs > 0)

478 479
	 local comb = combinations({of}, ofs)
	 if #comb < #ofs then return connect() end
480

481
	 if unique then
482 483
	    if #self:convertchains{of} > 1 then return connect() end

484 485 486 487 488 489 490 491 492 493 494 495
	    for _, c in ipairs(comb) do
	       if c.family then
	          if not created then created = {}
		  elseif created == true or created[c.family] then
		     return connect()
		  end
		  created[c.family] = true
	       else
	          if created then return connect() end
		  created = true
	       end
	    end
496 497 498 499 500 501 502
	 end

	 extend(res, comb)

      else table.insert(res, of) end
   end

503
   return extend(res, filter(ofs2, function(of) return of.chain end))
504 505 506
end


507
function M.Rule:trules()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
508

509
   local function tag(ofrags, tag, value)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
510 511 512 513 514 515
      for i, ofrag in ipairs(ofrags) do
	 assert(not ofrag[tag])
	 ofrag[tag] = value
      end
   end

516
   local ofrags = self:zoneoptfrags()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
517

518
   if self.ipset then
519
      local ipsetofrags = {}
520
      for i, ipset in listpairs(self.ipset) do
521
	 if not ipset.name then self:error('Set name not defined') end
522

523
	 local setdef = self.root.ipset and self.root.ipset[ipset.name]
524
	 if not setdef then self:error('Invalid set name') end
525

526
	 if not ipset.args then
527
	    self:error('Set direction arguments not defined')
528
	 end
529

530
	 local setopts = '-m set --match-set '..ipset.name..' '
531 532
	 setopts = setopts..table.concat(util.map(util.list(ipset.args),
						  function(a)
533 534 535 536
						     if self:direction(a) == 'in' then
							return 'src'
						     end
						     return 'dst'
537 538
						  end),
					 ',')
539
	 table.insert(ipsetofrags, {family=setdef.family, match=setopts})
540
      end
541
      ofrags = combinations(ofrags, ipsetofrags)
542 543
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
544 545 546 547 548 549 550
   if self.string then
      if type(self.string) == 'string' then
	 self.string = {match=self.string}
      end
      if not self.string.match then self:error('String match not defined') end
      setdefault(self.string, 'algo', 'bm')

551
      local opts = '-m string --string '..util.quote(self.string.match)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
552 553 554 555 556 557 558 559 560 561

      for _, attr in ipairs{'algo', 'from', 'to'} do
	 if self.string[attr] then
	    opts = opts..' --'..attr..' '..self.string[attr]
	 end
      end

      ofrags = combinations(ofrags, {{match=opts}})
   end

562
   if self.match then ofrags = combinations(ofrags, {{match=self.match}}) end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
563

564
   ofrags = combinations(ofrags, self:servoptfrags())
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
565

566 567
   tag(ofrags, 'position', self:position())

568 569 570 571
   local addrofrags = combinations(
      self:create(M.Zone, {addr=self.src}):optfrags(self:direction('in')),
      self:destoptfrags()
   )
572
   if addrofrags then ofrags = self:combine(ofrags, addrofrags, 'address') end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
573

574
   ofrags = prune(self:mangleoptfrags(ofrags), ofrags)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
575

576 577
   local custom = self:customtarget()
   for _, ofrag in ipairs(ofrags) do
578
      setdefault(ofrag, 'target', custom or self:target())
579
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
580

581
   ofrags = self:convertchains(ofrags)
582
   tag(ofrags, 'table', self:table(), false)
583 584

   local function checkzof(ofrag, dir, chains)
585
      if ofrag[dir] and contains(chains, ofrag.chain) then
586 587 588 589
	 self:error('Cannot specify '..dir..'bound interface ('..ofrag[dir]..')')
      end
   end

590
   for i, ofrag in ipairs(ofrags) do
591 592 593
      checkzof(ofrag, 'in', {'OUTPUT', 'POSTROUTING'})
      checkzof(ofrag, 'out', {'INPUT', 'PREROUTING'})
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
594
   
595
   ofrags = filter(
596
      combinations(ofrags, optfrag.FAMILYFRAGS),
597 598
      function(r) return self:trulefilter(r) end
   )
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
599

600
   local extra = self:extratrules(ofrags)
601
   if custom and extra[1] then self:error('Custom action not allowed here') end
602
   return extend(ofrags, extra)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
603 604 605 606 607
end

function M.Rule:customtarget()
   if self.action then
      local as = self.action:sub(1, 1)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
608 609 610
      if as == as:upper() or startswith(self.action, 'custom:') then
	 return self.action
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
611
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
612 613
end

614
function M.Rule:mangleoptfrags(ofrags) return ofrags end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
615

616 617 618 619
function M.Rule:trulefilter(rule) return true end

function M.Rule:extratrules(rules) return {} end

620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641
function M.Rule:convertchains(ofrags)
   local res = {}

   for _, ofrag in ipairs(ofrags) do

      if contains(builtin[self:table()], ofrag.chain) then
	 table.insert(res, ofrag)

      else
	 local ofs, recursive
	 if ofrag.chain == 'PREROUTING' then
	    ofs = {{chain='FORWARD'}, {chain='INPUT'}}
	 elseif ofrag.chain == 'POSTROUTING' then
	    ofs = {{chain='FORWARD'}, {chain='OUTPUT'}}
	    recursive = true
	 elseif ofrag.chain == 'INPUT' then
	    ofs = {{match='-m addrtype --dst-type LOCAL', chain='PREROUTING'}}
	 elseif ofrag.chain == 'FORWARD' then
	    ofs = {{match='-m addrtype ! --dst-type LOCAL', chain='PREROUTING'}}
	 end

	 if ofs then
642 643 644
	    local of = copy(ofrag)
	    of.chain = nil
	    ofs = combinations(ofs, {of})
645 646 647 648 649 650 651 652 653 654
	    if recursive then ofs = self:convertchains(ofs) end
	    extend(res, ofs)

	 else table.insert(res, ofrag) end
      end
   end

   return res
end

655 656 657 658
function M.Rule:extrarules(label, cls, options)
   local params = {}

   for _, attr in ipairs(
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
659
      extend(
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
660
         {'in', 'out', 'src', 'dest', 'ipset', 'string', 'match', 'service'},
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
661 662
	 options.attrs
      )
663 664 665 666 667 668 669 670 671 672
   ) do
      params[attr] = (options.src or self)[attr]
   end

   util.update(params, options.update)
   if options.discard then params[options.discard] = nil end

   return self:create(cls, params, label, options.index):trules()
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
673

674
M.Maskable = M.class(M.ConfigObject)
675

676 677
function M.Maskable:init(...)
   M.Maskable.super(self):init(...)
678

679 680 681 682 683 684 685 686
   -- alpine v3.5 compatibility
   if self.mask then
      self:warning(
	 "'mask' attribute is deprecated, please use 'src-mask' and 'dest-mask'"
      )
      self['src-mask'] = {}
      self['dest-mask'] = {}
      if type(self.mask) == 'number' then self.mask = {src=self.mask} end
687
      for _, family in ipairs(FAMILIES) do
688
	 setdefault(self.mask, family, copy(self.mask))
689 690
	 for _, attr in ipairs{'src', 'dest'} do
	    self[attr..'-mask'][family] = self.mask[family][attr] or
691
	       ({src=ADDRLEN[family], dest=0})[attr]
692 693
	 end
      end
694
   end
695

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
696 697 698
   self:initmask()
end

699
function M.Maskable:initmask()
700 701 702 703 704 705
   setdefault(self, 'src-mask', not self['dest-mask'])
   setdefault(self, 'dest-mask', false)

   for _, addr in ipairs{'src', 'dest'} do
      local mask = addr..'-mask'
      if type(self[mask]) ~= 'table' then
706 707 708
	 local m = self[mask]
	 self[mask] = {}
	 map(FAMILIES, function(f) self[mask][f] = m end)
709
      end
710
      for _, family in ipairs(FAMILIES) do
711 712
	 local value = self[mask][family]
	 if not value then self[mask][family] = 0
713
	 elseif value == true then self[mask][family] = ADDRLEN[family] end
714 715
      end
   end
716 717
end

718 719 720
function M.Maskable:recentmask(name)
   local res = {}

721
   for _, family in ipairs(FAMILIES) do
722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751
      local addr, len
      for _, a in ipairs{'src', 'dest'} do
	 local mask = self[a..'-mask'][family]
	 if mask > 0 then
	    if addr then return end
	    addr = a
	    len = mask
	 end
      end
      if not addr then return end

      local mask = ''

      if family == 'inet' then
	 local octet
	 for i = 0, 3 do
	    if len <= i * 8 then octet = 0
	    elseif len > i * 8 + 7 then octet = 255
	    else octet = 256 - 2^(8 - len % 8) end
	    mask = util.join(mask, '.', octet)
	 end

      elseif family == 'inet6' then
	 while len > 0 do
	    if #mask % 5 == 4 then mask = mask..':' end
	    mask = mask..('%x'):format(16 - 2^math.max(0, 4 - len))
	    len = len - 4
	 end
	 while #mask % 5 < 4 do mask = mask..'0' end
	 if #mask < 39 then mask = mask..'::' end
752 753

      else assert(false) end
754 755 756 757 758 759 760 761 762 763

      table.insert(
	 res,
	 {
	    family=family,
	    match='-m recent --name '..
	       (self.name and 'user:'..self.name or name)..' --r'..
	       ({src='source', dest='dest'})[addr]..' --mask '..mask
	 }
      )
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
764
   end
765 766

   return res
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
767 768
end

769 770 771 772

M.Limit = M.class(M.Maskable)

function M.Limit:init(...)
773
   setdefault(self, 'count', self[1] or 1)
774
   setdefault(self, 'interval', 1)
775 776

   M.Limit.super(self):init(...)
777 778
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
779 780 781
function M.Limit:rate() return self.count / self.interval end

function M.Limit:intrate() return math.ceil(self:rate()) end
782

783
function M.Limit:limitofrags(name)
784
   local rate = self:rate()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
785 786 787 788 789 790 791 792 793 794
   local unit
   for _, quantum in ipairs{
      {1, 'second'}, {60, 'minute'}, {60, 'hour'}, {24, 'day'}
   } do
      rate = rate * quantum[1]
      unit = quantum[2]
      if rate >= 1 then break end
   end
   rate = math.ceil(rate)..'/'..unit

795 796
   local ofrags = {}

797
   for _, family in ipairs(FAMILIES) do
798 799
      local keys = {}
      local maskopts = ''
800 801
      for _, addr in ipairs{'src', 'dest'} do
	 local mask = self[addr..'-mask'][family]
802
	 if mask > 0 then
803
	    local opt = ({src='src', dest='dst'})[addr]
804 805 806 807 808 809 810 811 812
	    table.insert(keys, opt..'ip')
	    maskopts = maskopts..' --hashlimit-'..opt..'mask '..mask
	 end
      end

      table.insert(
	 ofrags,
	 {
	    family=family,
813
	    match=keys[1] and
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
814 815 816 817
	       '-m hashlimit --hashlimit-upto '..rate..' --hashlimit-burst '..
	       self:intrate()..' --hashlimit-mode '..table.concat(keys, ',')..
	       maskopts..' --hashlimit-name '..(name or self:uniqueid()) or
	       '-m limit --limit '..rate
818 819 820 821 822
	 }
      )
   end

   return ofrags
823 824 825
end


Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
826 827 828 829 830
M.export = {
   custom={class=M.ConfigObject},
   ipset={class=IPSet, before='%modules'},
   zone={class=M.Zone}
}
831 832

return M