model.lua 12.7 KB
Newer Older
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
1 2
--[[
Base data model 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
]]--


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 25 26
local util = require('awall.util')
local contains = util.contains
local extend = util.extend
local filter = util.filter
local listpairs = util.listpairs
local maplist = util.maplist
27

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
28

29 30 31 32 33 34
local startswith = require('stringy').startswith


M.ConfigObject = M.class()

function M.ConfigObject:init(context, location)
35 36
   if context then
      self.context = context
37
      self.root = context.objects
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
38
   end
39
   self.location = location
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
40 41
end

42
function M.ConfigObject:create(cls, params)
43 44
   if type(cls) == 'string' then
      local name = cls
45
      cls = loadclass(cls)
46 47 48 49
      if not cls then
	 self:error('Support for '..name..' objects not installed')
      end
   end
50 51 52
   return cls.morph(params, self.context, self.location)
end

53
function M.ConfigObject:error(msg) raise(self.location..': '..msg) end
54

55
function M.ConfigObject:warning(msg)
56 57 58
   io.stderr:write(self.location..': '..msg..'\n')
end

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

61
function M.ConfigObject:info()
62 63
   local res = {}
   for i, trule in ipairs(self:trules()) do
64
      table.insert(res, {'  '..optfrag.location(trule), optfrag.command(trule)})
65 66 67 68
   end
   return res
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
69

70
M.Zone = M.class(M.ConfigObject)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
71

72
function M.Zone:optfrags(dir)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
73 74 75 76 77 78 79
   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
80 81 82
   local aopts = nil
   if self.addr then
      aopts = {}
83 84
      for i, hostdef in listpairs(self.addr) do
	 for i, addr in ipairs(resolve(hostdef, self)) do
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
85 86 87 88 89
	    table.insert(aopts,
			 {family=addr[1],
			  [aprop]=addr[2],
			  opts='-'..aopt..' '..addr[2]})
	 end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
90 91 92
      end
   end

93 94 95 96 97 98 99
   return combinations(
      maplist(
	 self.iface,
	 function(x) return {[iprop]=x, opts='-'..iopt..' '..x} end
      ),
      aopts
   )
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
100 101 102
end


103
M.fwzone = M.Zone()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
104 105


106
local IPSet = M.class(M.ConfigObject)
107 108

function IPSet:init(...)
109
   IPSet.super(self):init(...)
110 111 112

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

113
   if startswith(self.type, 'bitmap:') then
114 115 116 117
      if not self.range then self:error('Range not defined') end
      self.options = {self.type, 'range', self.range}
      self.family = 'inet'

118
   elseif startswith(self.type, 'hash:') then
119 120 121 122 123 124 125 126 127
      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


128
M.Rule = M.class(M.ConfigObject)
129

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
130

131 132
function M.Rule:init(...)
   M.Rule.super(self):init(...)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
133

134 135
   self.newchains = {}

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
136
   for i, prop in ipairs({'in', 'out'}) do
137 138 139 140 141 142 143 144 145
      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
146
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
147

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
148
   if self.service then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
149
      if type(self.service) == 'string' then self.label = self.service end
150 151 152 153 154 155 156
      self.service = maplist(
	 self.service,
	 function(s)
	    if type(s) ~= 'string' then return s end
	    return self.root.service[s] or self:error('Invalid service: '..s)
	 end
      )
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
157 158 159 160
   end
end


161
function M.Rule:direction(dir)
162 163 164 165 166 167
   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


168
function M.Rule:zoneoptfrags()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
169

170
   local function zonepair(zin, zout)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
171

172
      local function zofs(zone, dir)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
173
	 if not zone then return zone end
174
	 return zone:optfrags(dir)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
175 176 177 178
      end

      local chain, ofrags

179
      if zin == M.fwzone or zout == M.fwzone then
180
	 if zin == zout then return {} end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
181
	 local dir, z = 'in', zin
182
	 if zin == M.fwzone then dir, z = 'out', zout end
183
	 chain = dir:upper()..'PUT'
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
184 185
	 ofrags = zofs(z, dir)

186 187 188 189 190 191 192 193 194 195 196
      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
197 198
      else
	 chain = 'FORWARD'
199 200
	 ofrags = combinations(zofs(zin, 'in'), zofs(zout, 'out'))

201
	 if ofrags and not zout['route-back'] then
202 203 204 205 206 207
	    ofrags = filter(
	       ofrags,
	       function(of)
		  return not (of['in'] and of.out and of['in'] == of.out)
	       end
	    )
208
	 end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
209 210
      end

211 212 213
      return combinations(ofrags,
			  chain and {{chain=chain}} or {{chain='PREROUTING'},
							{chain='OUTPUT'}})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
214 215 216
   end

   local res = {}
217 218
   local izones = self[self:direction('in')] or {}
   local ozones = self[self:direction('out')] or {}
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
219

220 221
   for i = 1,math.max(1, table.maxn(izones)) do
      for j = 1,math.max(1, table.maxn(ozones)) do
222
	 extend(res, zonepair(izones[i], ozones[j]))
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
223 224 225 226 227 228 229
      end
   end

   return res
end


230
function M.Rule:servoptfrags()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
231 232 233

   if not self.service then return end

234
   local fports = {inet={}, inet6={}}
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
235 236 237
   local res = {}

   for i, serv in ipairs(self.service) do
238
      for i, sdef in listpairs(serv) do
239
	 if not sdef.proto then self:error('Protocol not defined') end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
240

241
	 if contains({6, 'tcp', 17, 'udp'}, sdef.proto) then
242 243 244 245 246 247 248 249
	    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
250
			extend(
251
			   ports[sdef.proto],
252
			   maplist(
253
			      sdef.port,
254
			      function(p) return tostring(p):gsub('-', ':') end
255 256 257 258 259
			   )
			)
		     else ports[sdef.proto] = {} end
		  end
	       end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
260 261 262 263 264 265 266
	    end

	 else

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

267 268
	    -- TODO multiple ICMP types per rule
	    local oname
269
	    if contains({1, 'icmp'}, sdef.proto) then
270 271
	       family = 'inet'
	       oname = 'icmp-type'
272
	    elseif contains({58, 'ipv6-icmp', 'icmpv6'}, sdef.proto) then
273 274
	       family = 'inet6'
	       oname = 'icmpv6-type'
275
	    elseif sdef.type or sdef['reply-type'] then
276
	       self:error('Type specification not valid with '..sdef.proto)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
277 278
	    end

279 280 281 282 283 284 285 286 287
	    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

288 289
	    if sdef.type then
	       opts = opts..' --'..oname..' '..(
290
		  self.reverse and sdef['reply-type'] or sdef.type
291 292
	       )
	    end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
293 294 295 296 297
	    table.insert(res, {family=family, opts=opts})
	 end
      end
   end

298
   local popt = ' --'..(self.reverse and 's' or 'd')..'port'
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
   for family, pports in pairs(fports) do
      local ofrags = {}

      for proto, ports in pairs(pports) do
	 local propt = '-p '..proto

	 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]
		     
321
		     pc = pc + (port:find(':') and 2 or 1)
322 323 324 325 326 327 328 329 330 331 332 333 334 335
		     if pc > 15 then break end
		     
		     opts = opts..sep..port
		     
		     table.remove(ports, 1)
		     len = len - 1
		  until len == 0
	       end

	       table.insert(ofrags, {opts=opts})
	    until len == 0

	 else table.insert(ofrags, {opts=propt}) end
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
336

337
      extend(res, combinations(ofrags, {{family=family}}))
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
338 339 340 341 342
   end

   return res
end

343 344
function M.Rule:destoptfrags()
   return self:create(M.Zone, {addr=self.dest}):optfrags(self:direction('out'))
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
345 346
end

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

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

351
function M.Rule:target()
352 353 354 355 356 357 358 359 360 361
   -- 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
362 363 364
end


365
function M.Rule:trules()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
366

367
   local function tag(ofrags, tag, value)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
368 369 370 371 372 373 374 375
      for i, ofrag in ipairs(ofrags) do
	 assert(not ofrag[tag])
	 ofrag[tag] = value
      end
   end

   local families

376
   local function setfamilies(ofrags)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
377 378 379 380 381 382 383 384 385 386 387 388
      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

389
   local function ffilter(ofrags)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
390
      if not ofrags or not ofrags[1] or not families then return ofrags end
391 392 393 394 395 396
      return filter(
	 ofrags,
	 function(of)
	    return not of.family or contains(families, of.family)
	 end
      )
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
397 398 399 400
   end

   local res = self:zoneoptfrags()

401
   if self.ipset then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
402
      local ipsetofrags = {}
403
      for i, ipset in listpairs(self.ipset) do
404
	 if not ipset.name then self:error('Set name not defined') end
405

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
406
	 local setdef = self.root.ipset and self.root.ipset[ipset.name]
407
	 if not setdef then self:error('Invalid set name') end
408

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
409
	 if not ipset.args then
410
	    self:error('Set direction arguments not defined')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
411
	 end
412

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
413
	 local setopts = '-m set --match-set '..ipset.name..' '
414 415
	 setopts = setopts..table.concat(util.map(util.list(ipset.args),
						  function(a)
416 417 418 419
						     if self:direction(a) == 'in' then
							return 'src'
						     end
						     return 'dst'
420 421
						  end),
					 ',')
422
	 table.insert(ipsetofrags, {family=setdef.family, opts=setopts})
423
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
424
      res = combinations(res, ipsetofrags)
425 426
   end

427
   if self.ipsec then
428 429
      res = combinations(res,
			 {{opts='-m policy --pol ipsec --dir '..self:direction(self.ipsec)}})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
430 431 432 433 434 435
   end

   res = combinations(res, self:servoptfrags())

   setfamilies(res)

436 437 438 439
   local addrofrags = combinations(
      self:create(M.Zone, {addr=self.src}):optfrags(self:direction('in')),
      self:destoptfrags()
   )
440
   local combined = res
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
441 442 443 444 445 446

   if addrofrags then
      addrofrags = ffilter(addrofrags)
      setfamilies(addrofrags)
      res = ffilter(res)

447 448 449 450 451 452 453
      combined = {}
      for i, ofrag in ipairs(res) do
	 local cc = combinations({ofrag}, addrofrags)
	 if #cc < #addrofrags then
	    combined = nil
	    break
	 end
454
	 extend(combined, cc)
455
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
456 457 458
   end

   local target
459
   if combined then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
460
      target = self:target()
461 462 463
      res = combined
   else
      target = self:newchain('address')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
464 465 466 467
   end

   tag(res, 'position', self:position())

468
   res = combinations(res, {{target=target}})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
469

470
   if not combined then
471
      extend(
472 473 474
	 res,
	 combinations(addrofrags, {{chain=target, target=self:target()}})
      )
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
475 476
   end

477
   extend(res, self:extraoptfrags())
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
478

479 480 481 482 483 484 485
   local tbl = self:table()

   local function convertchains(ofrags)
      local res = {}

      for i, ofrag in ipairs(ofrags) do

486
	 if contains(builtin[tbl], ofrag.chain) then table.insert(res, ofrag)
487
	 else
488 489 490
	    local ofs, recursive
	    if ofrag.chain == 'PREROUTING' then
	       ofs = {{chain='FORWARD'}, {chain='INPUT'}}
491
	    elseif ofrag.chain == 'POSTROUTING' then
492 493 494 495 496 497 498 499
	       ofs = {{chain='FORWARD'}, {chain='OUTPUT'}}
	       recursive = true
	    elseif ofrag.chain == 'INPUT' then
	       ofs = {{opts='-m addrtype --dst-type LOCAL', chain='PREROUTING'}}
	    elseif ofrag.chain == 'FORWARD' then
	       ofs = {
		  {opts='-m addrtype ! --dst-type LOCAL', chain='PREROUTING'}
	       }
500 501
	    end

502
	    if ofs then
503
	       ofrag.chain = nil
504 505
	       ofs = combinations(ofs, {ofrag})
	       if recursive then ofs = convertchains(ofs) end
506
	       extend(res, ofs)
507

508 509 510 511 512 513 514
	    else table.insert(res, ofrag) end
	 end
      end

      return res
   end

515
   res = convertchains(ffilter(res))
516
   tag(res, 'table', tbl, false)
517 518

   local function checkzof(ofrag, dir, chains)
519
      if ofrag[dir] and contains(chains, ofrag.chain) then
520 521 522 523 524 525 526 527
	 self:error('Cannot specify '..dir..'bound interface ('..ofrag[dir]..')')
      end
   end

   for i, ofrag in ipairs(res) do
      checkzof(ofrag, 'in', {'OUTPUT', 'POSTROUTING'})
      checkzof(ofrag, 'out', {'INPUT', 'PREROUTING'})
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
528
   
529
   return combinations(res, ffilter({{family='inet'}, {family='inet6'}}))
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
530 531
end

532
function M.Rule:extraoptfrags() return {} end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
533

534
function M.Rule:newchain(key)
535 536
   if self.newchains[key] then return self.newchains[key] end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
537 538 539
   if not self.context.lastid then self.context.lastid = {} end
   local lastid = self.context.lastid

540 541 542 543 544 545 546 547
   local res = key
   if self.label then res = res..'-'..self.label end
   if not lastid[res] then lastid[res] = -1 end
   lastid[res] = lastid[res] + 1
   res = res..'-'..lastid[res]

   self.newchains[key] = res
   return res
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
548 549
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
550

551 552 553
M.export = {zone={class=M.Zone}, ipset={class=IPSet, before='%modules'}}

return M