model.lua 20.1 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
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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 17
local optfrag = require('awall.optfrag')
local combinations = optfrag.combinations
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
18

19
local raise = require('awall.uerror').raise
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
20

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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
32

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


M.ConfigObject = M.class()

function M.ConfigObject:init(context, location)
39 40
   if context then
      self.context = context
41
      self.root = context.objects
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
42
   end
43
   self.location = location
44 45

   self.extraobjs = {}
46
   self.uniqueids = {}
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
47 48
end

49 50 51 52 53 54 55 56
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

57 58
   if type(cls) == 'string' then
      local name = cls
59
      cls = loadclass(cls)
60 61 62 63
      if not cls then
	 self:error('Support for '..name..' objects not installed')
      end
   end
64

65
   if type(params) ~= 'table' then params = {params} end
66
   params.label = join(self.label, '-', label)
67

68 69 70
   local obj = cls.morph(params, self.context, self.location)
   if key then self.extraobjs[key] = obj end
   return obj
71 72
end

73
function M.ConfigObject:uniqueid(key)
74
   if not key then key = '' end
75 76
   if self.uniqueids[key] then return self.uniqueids[key] end

77
   local lastid = setdefault(self.context, 'lastid', {})
78
   local res = join(key, '-', self.label)
79
   lastid[res] = setdefault(lastid, res, -1) + 1
80 81 82 83 84 85
   res = res..'-'..lastid[res]

   self.uniqueids[key] = res
   return res
end

86
function M.ConfigObject:error(msg) raise(self.location..': '..msg) end
87

88
function M.ConfigObject:warning(msg)
89
   util.printmsg(self.location..': '..msg)
90 91
end

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

94
function M.ConfigObject:info()
95 96
   local res = {}
   for i, trule in ipairs(self:trules()) do
97
      table.insert(res, {'  '..optfrag.location(trule), optfrag.command(trule)})
98 99 100 101
   end
   return res
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
102

103
M.Zone = M.class(M.ConfigObject)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
104

105
function M.Zone:optfrags(dir)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
106 107 108 109 110 111 112
   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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
113 114 115
   local aopts = nil
   if self.addr then
      aopts = {}
116 117
      for i, hostdef in listpairs(self.addr) do
	 for i, addr in ipairs(resolve(hostdef, self)) do
118 119 120 121
	    table.insert(
	       aopts,
	       {family=addr[1], [aprop]=addr[2], match='-'..aopt..' '..addr[2]}
	    )
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
122
	 end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
123 124 125
      end
   end

126 127 128 129
   local popt
   if self.ipsec ~= nil then
      popt = {
	 {
130
	    match='-m policy --dir '..dir..' --pol '..
131 132 133 134 135
	       (self.ipsec and 'ipsec' or 'none')
	 }
      }
   end

136 137 138
   return combinations(
      maplist(
	 self.iface,
139
	 function(x) return {[iprop]=x, match='-'..iopt..' '..x} end
140
      ),
141 142
      aopts,
      popt
143
   )
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
144 145 146
end


147
M.fwzone = M.Zone()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
148 149


150
local IPSet = M.class(M.ConfigObject)
151 152

function IPSet:init(...)
153
   IPSet.super(self):init(...)
154 155 156

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

157
   if startswith(self.type, 'bitmap:') then
158 159 160 161
      if not self.range then self:error('Range not defined') end
      self.options = {self.type, 'range', self.range}
      self.family = 'inet'

162
   elseif startswith(self.type, 'hash:') then
163 164 165 166 167 168 169 170 171
      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


172
M.Rule = M.class(M.ConfigObject)
173

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
174

175 176
function M.Rule:init(...)
   M.Rule.super(self):init(...)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
177 178

   for i, prop in ipairs({'in', 'out'}) do
179 180 181 182 183 184 185 186 187
      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
188
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
189

190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
   -- 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
210
   if self.service then
211 212 213 214
      if not self.label and type(self.service) == 'string' then
	 self.label = self.service
      end

215 216 217 218 219 220
      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)
221
	 end
222 223 224 225 226 227 228
	 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
229 230 231 232
   end
end


233
function M.Rule:direction(dir)
234 235 236 237 238 239
   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


240
function M.Rule:zoneoptfrags()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
241

242
   local function zonepair(zin, zout)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
243

244
      local function zofs(zone, dir)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
245
	 if not zone then return zone end
246
	 return zone:optfrags(dir)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
247 248 249 250
      end

      local chain, ofrags

251
      if zin == M.fwzone or zout == M.fwzone then
252
	 if zin == zout then return {} end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
253
	 local dir, z = 'in', zin
254
	 if zin == M.fwzone then dir, z = 'out', zout end
255
	 chain = dir:upper()..'PUT'
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
256 257
	 ofrags = zofs(z, dir)

258 259 260 261 262 263 264 265 266 267 268
      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
269 270
      else
	 chain = 'FORWARD'
271 272
	 ofrags = combinations(zofs(zin, 'in'), zofs(zout, 'out'))

273
	 if ofrags and not zout['route-back'] then
274 275 276 277 278 279
	    ofrags = filter(
	       ofrags,
	       function(of)
		  return not (of['in'] and of.out and of['in'] == of.out)
	       end
	    )
280
	 end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
281 282
      end

283 284 285
      return combinations(ofrags,
			  chain and {{chain=chain}} or {{chain='PREROUTING'},
							{chain='OUTPUT'}})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
286 287 288
   end

   local res = {}
289 290
   local izones = self[self:direction('in')] or {}
   local ozones = self[self:direction('out')] or {}
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
291

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
292 293
   for i = 1,math.max(1, #izones) do
      for j = 1,math.max(1, #ozones) do
294
	 extend(res, zonepair(izones[i], ozones[j]))
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
295 296 297 298 299 300 301
      end
   end

   return res
end


302
function M.Rule:servoptfrags()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
303 304 305

   if not self.service then return end

306
   local fports = {inet={}, inet6={}}
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
307 308 309
   local res = {}

   for i, serv in ipairs(self.service) do
310
      for i, sdef in listpairs(serv) do
311
	 if contains({'tcp', 'udp'}, sdef.proto) then
312 313 314 315 316 317 318 319
	    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
320
			extend(
321
			   ports[sdef.proto],
322
			   maplist(
323
			      sdef.port,
324
			      function(p) return tostring(p):gsub('-', ':') end
325 326 327 328 329
			   )
			)
		     else ports[sdef.proto] = {} end
		  end
	       end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
330 331 332 333 334 335 336
	    end

	 else

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

337 338
	    -- TODO multiple ICMP types per rule
	    local oname
339
	    if sdef.proto == 'icmp' then
340 341
	       family = 'inet'
	       oname = 'icmp-type'
342
	    elseif contains({'ipv6-icmp', 'icmpv6'}, sdef.proto) then
343 344
	       family = 'inet6'
	       oname = 'icmpv6-type'
345
	    elseif sdef.type or sdef['reply-type'] then
346
	       self:error('Type specification not valid with '..sdef.proto)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
347 348
	    end

349 350 351 352 353 354 355 356 357
	    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

358 359
	    if sdef.type then
	       opts = opts..' --'..oname..' '..(
360
		  self.reverse and sdef['reply-type'] or sdef.type
361 362
	       )
	    end
363
	    table.insert(res, {family=family, match=opts})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
364 365 366 367
	 end
      end
   end

368
   local popt = ' --'..(self.reverse and 's' or 'd')..'port'
369
   for _, family in sortedkeys(fports) do
370
      local ofrags = {}
371
      local pports = fports[family]
372

373
      for _, proto in sortedkeys(pports) do
374
	 local propt = '-p '..proto
375
	 local ports = pports[proto]
376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392

	 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]
		     
393
		     pc = pc + (port:find(':') and 2 or 1)
394 395 396 397 398 399 400 401 402
		     if pc > 15 then break end
		     
		     opts = opts..sep..port
		     
		     table.remove(ports, 1)
		     len = len - 1
		  until len == 0
	       end

403
	       table.insert(ofrags, {match=opts})
404 405
	    until len == 0

406
	 else table.insert(ofrags, {match=propt}) end
407
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
408

409
      extend(res, combinations(ofrags, {{family=family}}))
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
410 411 412 413 414
   end

   return res
end

415 416
function M.Rule:destoptfrags()
   return self:create(M.Zone, {addr=self.dest}):optfrags(self:direction('out'))
417 418
end

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

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

423
function M.Rule:target()
424 425 426 427 428 429 430 431 432 433
   -- 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
434 435 436
end


437 438 439 440 441 442 443 444 445 446 447 448 449 450 451
function M.Rule:combine(ofs1, ofs2, key, unique)

   local function connect()
      local chain = self:uniqueid(key)
      local function setvar(name)
	 return function(of)
	    setdefault(of, name, chain)
	    return of
	 end
      end

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

   local chainless = filter(ofs2, function(of) return not of.chain end)
452
   local created
453 454 455 456 457 458 459 460
   local res = {}

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

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

461 462
	 local comb = combinations({of}, ofs)
	 if #comb < #ofs then return connect() end
463

464 465 466 467 468 469 470 471 472 473 474 475 476
	 if unique then
	    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
477 478 479 480 481 482 483 484 485 486 487
	 end

	 extend(res, comb)

      else table.insert(res, of) end
   end

   return res
end


488
function M.Rule:trules()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
489

490
   local function tag(ofrags, tag, value)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
491 492 493 494 495 496 497 498
      for i, ofrag in ipairs(ofrags) do
	 assert(not ofrag[tag])
	 ofrag[tag] = value
      end
   end

   local families

499
   local function setfamilies(ofrags)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
500 501 502 503 504 505 506 507 508 509 510 511
      if ofrags then
	 families = {}
	 for i, ofrag in ipairs(ofrags) do
	    if not ofrag.family then
	       families = nil
	       return
	    end
	    table.insert(families, ofrag.family)
	 end
      else families = nil end
   end

512
   local function ffilter(ofrags)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
513
      if not ofrags or not ofrags[1] or not families then return ofrags end
514 515 516 517 518 519
      return filter(
	 ofrags,
	 function(of)
	    return not of.family or contains(families, of.family)
	 end
      )
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
520 521
   end

522
   local ofrags = self:zoneoptfrags()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
523

524
   if self.ipset then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
525
      local ipsetofrags = {}
526
      for i, ipset in listpairs(self.ipset) do
527
	 if not ipset.name then self:error('Set name not defined') end
528

529
	 local setdef = self.root.ipset and self.root.ipset[ipset.name]
530
	 if not setdef then self:error('Invalid set name') end
531

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
532
	 if not ipset.args then
533
	    self:error('Set direction arguments not defined')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
534
	 end
535

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
536
	 local setopts = '-m set --match-set '..ipset.name..' '
537 538
	 setopts = setopts..table.concat(util.map(util.list(ipset.args),
						  function(a)
539 540 541 542
						     if self:direction(a) == 'in' then
							return 'src'
						     end
						     return 'dst'
543 544
						  end),
					 ',')
545
	 table.insert(ipsetofrags, {family=setdef.family, match=setopts})
546
      end
547
      ofrags = combinations(ofrags, ipsetofrags)
548 549
   end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
550 551 552 553 554 555 556
   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')

557
      local opts = '-m string --string '..util.quote(self.string.match)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
558 559 560 561 562 563 564 565 566 567

      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

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

570
   ofrags = combinations(ofrags, self:servoptfrags())
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
571

572 573
   tag(ofrags, 'position', self:position())

574
   setfamilies(ofrags)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
575

576 577 578 579
   local addrofrags = combinations(
      self:create(M.Zone, {addr=self.src}):optfrags(self:direction('in')),
      self:destoptfrags()
   )
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
580 581 582
   if addrofrags then
      addrofrags = ffilter(addrofrags)
      setfamilies(addrofrags)
583
      ofrags = self:combine(ffilter(ofrags), addrofrags, 'address')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
584 585
   end

586
   ofrags = self:mangleoptfrags(ofrags)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
587

588 589
   local custom = self:customtarget()
   for _, ofrag in ipairs(ofrags) do
590
      setdefault(ofrag, 'target', custom or self:target())
591
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
592

593 594 595 596 597 598 599
   local tbl = self:table()

   local function convertchains(ofrags)
      local res = {}

      for i, ofrag in ipairs(ofrags) do

600
	 if contains(builtin[tbl], ofrag.chain) then table.insert(res, ofrag)
601
	 else
602 603 604
	    local ofs, recursive
	    if ofrag.chain == 'PREROUTING' then
	       ofs = {{chain='FORWARD'}, {chain='INPUT'}}
605
	    elseif ofrag.chain == 'POSTROUTING' then
606 607 608
	       ofs = {{chain='FORWARD'}, {chain='OUTPUT'}}
	       recursive = true
	    elseif ofrag.chain == 'INPUT' then
609 610 611
	       ofs = {
	          {match='-m addrtype --dst-type LOCAL', chain='PREROUTING'}
	       }
612 613
	    elseif ofrag.chain == 'FORWARD' then
	       ofs = {
614
		  {match='-m addrtype ! --dst-type LOCAL', chain='PREROUTING'}
615
	       }
616 617
	    end

618
	    if ofs then
619
	       ofrag.chain = nil
620 621
	       ofs = combinations(ofs, {ofrag})
	       if recursive then ofs = convertchains(ofs) end
622
	       extend(res, ofs)
623

624 625 626 627 628 629 630
	    else table.insert(res, ofrag) end
	 end
      end

      return res
   end

631 632
   ofrags = convertchains(ffilter(ofrags))
   tag(ofrags, 'table', tbl, false)
633 634

   local function checkzof(ofrag, dir, chains)
635
      if ofrag[dir] and contains(chains, ofrag.chain) then
636 637 638 639
	 self:error('Cannot specify '..dir..'bound interface ('..ofrag[dir]..')')
      end
   end

640
   for i, ofrag in ipairs(ofrags) do
641 642 643
      checkzof(ofrag, 'in', {'OUTPUT', 'POSTROUTING'})
      checkzof(ofrag, 'out', {'INPUT', 'PREROUTING'})
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
644
   
645 646 647 648
   ofrags = filter(
      combinations(ofrags, ffilter({{family='inet'}, {family='inet6'}})),
      function(r) return self:trulefilter(r) end
   )
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
649

650
   local extra = self:extratrules(ofrags)
651
   if custom and extra[1] then self:error('Custom action not allowed here') end
652
   return extend(ofrags, extra)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
653 654 655 656 657
end

function M.Rule:customtarget()
   if self.action then
      local as = self.action:sub(1, 1)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
658 659 660
      if as == as:upper() or startswith(self.action, 'custom:') then
	 return self.action
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
661
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
662 663
end

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

666 667 668 669
function M.Rule:trulefilter(rule) return true end

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

670 671 672 673
function M.Rule:extrarules(label, cls, options)
   local params = {}

   for _, attr in ipairs(
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
674
      extend(
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
675
         {'in', 'out', 'src', 'dest', 'ipset', 'string', 'match', 'service'},
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
676 677
	 options.attrs
      )
678 679 680 681 682 683 684 685 686 687
   ) 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
688

689
M.Maskable = M.class(M.ConfigObject)
690

691 692
function M.Maskable:init(...)
   M.Maskable.super(self):init(...)
693

694 695 696 697 698 699 700 701 702 703 704 705 706 707 708
   -- 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
      for _, family in ipairs{'inet', 'inet6'} do
	 setdefault(self.mask, family, util.copy(self.mask))
	 for _, attr in ipairs{'src', 'dest'} do
	    self[attr..'-mask'][family] = self.mask[family][attr] or
	       ({src=({inet=32, inet6=128})[family], dest=0})[attr]
	 end
      end
709
   end
710

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
711 712 713
   self:initmask()
end

714
function M.Maskable:initmask()
715 716 717 718 719 720 721 722 723 724 725 726 727 728
   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
	 self[mask] = {inet=self[mask], inet6=self[mask]}
      end
      for _, family in ipairs{'inet', 'inet6'} do
	 local value = self[mask][family]
	 if not value then self[mask][family] = 0
	 elseif value == true then
	    self[mask][family] = ({inet=32, inet6=128})[family]
	 end
729 730
      end
   end
731 732
end

733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766
function M.Maskable:recentmask(name)
   local res = {}

   for _, family in ipairs{'inet', 'inet6'} do
      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
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
767
      end
768 769 770 771 772 773 774 775 776 777

      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
778
   end
779 780

   return res
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
781 782
end

783 784 785 786 787 788 789 790 791 792 793 794

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

function M.Limit:init(...)
   if not self.count then
      if not self[1] then
	 self:error('Packet count not defined for limit')
      end
      self.count = self[1]
   end

   setdefault(self, 'interval', 1)
795 796

   M.Limit.super(self):init(...)
797 798
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
799 800 801
function M.Limit:rate() return self.count / self.interval end

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

803
function M.Limit:limitofrags(name)
804
   local rate = self:rate()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
805 806 807 808 809 810 811 812 813 814
   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

815 816 817 818 819
   local ofrags = {}

   for _, family in ipairs{'inet', 'inet6'} do
      local keys = {}
      local maskopts = ''
820 821
      for _, addr in ipairs{'src', 'dest'} do
	 local mask = self[addr..'-mask'][family]
822
	 if mask > 0 then
823
	    local opt = ({src='src', dest='dst'})[addr]
824 825 826 827 828 829 830 831 832
	    table.insert(keys, opt..'ip')
	    maskopts = maskopts..' --hashlimit-'..opt..'mask '..mask
	 end
      end

      table.insert(
	 ofrags,
	 {
	    family=family,
833
	    match=keys[1] and
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
834 835 836 837
	       '-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
838 839 840 841 842
	 }
      )
   end

   return ofrags
843 844 845
end


Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
846 847 848 849 850
M.export = {
   custom={class=M.ConfigObject},
   ipset={class=IPSet, before='%modules'},
   zone={class=M.Zone}
}
851 852

return M