model.lua 12.8 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 65 66 67 68 69 70
      table.insert(
	 res,
	 {
	    '  '..optfrag.location(trule),
	    (trule.opts and trule.opts..' ' or '')..'-j '..trule.target
	 }
      )
71 72 73 74
   end
   return res
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
75

76
M.Zone = M.class(M.ConfigObject)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
77

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

99 100 101 102 103 104 105
   return combinations(
      maplist(
	 self.iface,
	 function(x) return {[iprop]=x, opts='-'..iopt..' '..x} end
      ),
      aopts
   )
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
106 107 108
end


109
M.fwzone = M.Zone()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
110 111


112
local IPSet = M.class(M.ConfigObject)
113 114

function IPSet:init(...)
115
   IPSet.super(self):init(...)
116 117 118

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

119
   if startswith(self.type, 'bitmap:') then
120 121 122 123
      if not self.range then self:error('Range not defined') end
      self.options = {self.type, 'range', self.range}
      self.family = 'inet'

124
   elseif startswith(self.type, 'hash:') then
125 126 127 128 129 130 131 132 133
      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


134
M.Rule = M.class(M.ConfigObject)
135

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
136

137 138
function M.Rule:init(...)
   M.Rule.super(self):init(...)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
139

140 141
   self.newchains = {}

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
142
   for i, prop in ipairs({'in', 'out'}) do
143 144 145 146 147 148 149 150 151
      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
152
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
153

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
154
   if self.service then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
155
      if type(self.service) == 'string' then self.label = self.service end
156 157 158 159 160 161 162
      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
163 164 165 166
   end
end


167
function M.Rule:direction(dir)
168 169 170 171 172 173
   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


174
function M.Rule:zoneoptfrags()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
175

176
   local function zonepair(zin, zout)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
177

178
      local function zofs(zone, dir)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
179
	 if not zone then return zone end
180
	 return zone:optfrags(dir)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
181 182 183 184
      end

      local chain, ofrags

185
      if zin == M.fwzone or zout == M.fwzone then
186
	 if zin == zout then return {} end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
187
	 local dir, z = 'in', zin
188
	 if zin == M.fwzone then dir, z = 'out', zout end
189
	 chain = dir:upper()..'PUT'
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
190 191
	 ofrags = zofs(z, dir)

192 193 194 195 196 197 198 199 200 201 202
      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
203 204
      else
	 chain = 'FORWARD'
205 206
	 ofrags = combinations(zofs(zin, 'in'), zofs(zout, 'out'))

207
	 if ofrags and not zout['route-back'] then
208 209 210 211 212 213
	    ofrags = filter(
	       ofrags,
	       function(of)
		  return not (of['in'] and of.out and of['in'] == of.out)
	       end
	    )
214
	 end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
215 216
      end

217 218 219
      return combinations(ofrags,
			  chain and {{chain=chain}} or {{chain='PREROUTING'},
							{chain='OUTPUT'}})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
220 221 222
   end

   local res = {}
223 224
   local izones = self[self:direction('in')] or {}
   local ozones = self[self:direction('out')] or {}
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
225

226 227
   for i = 1,math.max(1, table.maxn(izones)) do
      for j = 1,math.max(1, table.maxn(ozones)) do
228
	 extend(res, zonepair(izones[i], ozones[j]))
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
229 230 231 232 233 234 235
      end
   end

   return res
end


236
function M.Rule:servoptfrags()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
237 238 239

   if not self.service then return end

240
   local fports = {inet={}, inet6={}}
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
241 242 243
   local res = {}

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

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

	 else

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

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

285 286 287 288 289 290 291 292 293
	    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

294 295
	    if sdef.type then
	       opts = opts..' --'..oname..' '..(
296
		  self.reverse and sdef['reply-type'] or sdef.type
297 298
	       )
	    end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
299 300 301 302 303
	    table.insert(res, {family=family, opts=opts})
	 end
      end
   end

304
   local popt = ' --'..(self.reverse and 's' or 'd')..'port'
305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326
   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]
		     
327
		     pc = pc + (port:find(':') and 2 or 1)
328 329 330 331 332 333 334 335 336 337 338 339 340 341
		     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
342

343
      extend(res, combinations(ofrags, {{family=family}}))
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
344 345 346 347 348
   end

   return res
end

349 350
function M.Rule:destoptfrags()
   return self:create(M.Zone, {addr=self.dest}):optfrags(self:direction('out'))
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
351 352
end

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

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

357
function M.Rule:target()
358 359 360 361 362 363 364 365 366 367
   -- 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
368 369 370
end


371
function M.Rule:trules()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
372

373
   local function tag(ofrags, tag, value)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
374 375 376 377 378 379 380 381
      for i, ofrag in ipairs(ofrags) do
	 assert(not ofrag[tag])
	 ofrag[tag] = value
      end
   end

   local families

382
   local function setfamilies(ofrags)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
383 384 385 386 387 388 389 390 391 392 393 394
      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

395
   local function ffilter(ofrags)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
396
      if not ofrags or not ofrags[1] or not families then return ofrags end
397 398 399 400 401 402
      return filter(
	 ofrags,
	 function(of)
	    return not of.family or contains(families, of.family)
	 end
      )
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
403 404 405 406
   end

   local res = self:zoneoptfrags()

407
   if self.ipset then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
408
      local ipsetofrags = {}
409
      for i, ipset in listpairs(self.ipset) do
410
	 if not ipset.name then self:error('Set name not defined') end
411

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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
415
	 if not ipset.args then
416
	    self:error('Set direction arguments not defined')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
417
	 end
418

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

433
   if self.ipsec then
434 435
      res = combinations(res,
			 {{opts='-m policy --pol ipsec --dir '..self:direction(self.ipsec)}})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
436 437 438 439 440 441
   end

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

   setfamilies(res)

442 443 444 445
   local addrofrags = combinations(
      self:create(M.Zone, {addr=self.src}):optfrags(self:direction('in')),
      self:destoptfrags()
   )
446
   local combined = res
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
447 448 449 450 451 452

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

453 454 455 456 457 458 459
      combined = {}
      for i, ofrag in ipairs(res) do
	 local cc = combinations({ofrag}, addrofrags)
	 if #cc < #addrofrags then
	    combined = nil
	    break
	 end
460
	 extend(combined, cc)
461
      end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
462 463 464
   end

   local target
465
   if combined then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
466
      target = self:target()
467 468 469
      res = combined
   else
      target = self:newchain('address')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
470 471 472 473
   end

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

474
   res = combinations(res, {{target=target}})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
475

476
   if not combined then
477
      extend(
478 479 480
	 res,
	 combinations(addrofrags, {{chain=target, target=self:target()}})
      )
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
481 482
   end

483
   extend(res, self:extraoptfrags())
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
484

485 486 487 488 489 490 491
   local tbl = self:table()

   local function convertchains(ofrags)
      local res = {}

      for i, ofrag in ipairs(ofrags) do

492
	 if contains(builtin[tbl], ofrag.chain) then table.insert(res, ofrag)
493
	 else
494 495 496
	    local ofs, recursive
	    if ofrag.chain == 'PREROUTING' then
	       ofs = {{chain='FORWARD'}, {chain='INPUT'}}
497
	    elseif ofrag.chain == 'POSTROUTING' then
498 499 500 501 502 503 504 505
	       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'}
	       }
506 507
	    end

508
	    if ofs then
509
	       ofrag.chain = nil
510 511
	       ofs = combinations(ofs, {ofrag})
	       if recursive then ofs = convertchains(ofs) end
512
	       extend(res, ofs)
513

514 515 516 517 518 519 520
	    else table.insert(res, ofrag) end
	 end
      end

      return res
   end

521
   res = convertchains(ffilter(res))
522
   tag(res, 'table', tbl, false)
523 524

   local function checkzof(ofrag, dir, chains)
525
      if ofrag[dir] and contains(chains, ofrag.chain) then
526 527 528 529 530 531 532 533
	 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
534
   
535
   return combinations(res, ffilter({{family='inet'}, {family='inet6'}}))
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
536 537
end

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

540
function M.Rule:newchain(key)
541 542
   if self.newchains[key] then return self.newchains[key] end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
543 544 545
   if not self.context.lastid then self.context.lastid = {} end
   local lastid = self.context.lastid

546 547 548 549 550 551 552 553
   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
554 555
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
556

557 558 559
M.export = {zone={class=M.Zone}, ipset={class=IPSet, before='%modules'}}

return M