model.lua 12.9 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 53 54

   if self.label then
      params.label = self.label..(params.label and '-'..params.label or '')
   end

55 56 57
   return cls.morph(params, self.context, self.location)
end

58
function M.ConfigObject:error(msg) raise(self.location..': '..msg) end
59

60
function M.ConfigObject:warning(msg)
61 62 63
   io.stderr:write(self.location..': '..msg..'\n')
end

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

66
function M.ConfigObject:info()
67 68
   local res = {}
   for i, trule in ipairs(self:trules()) do
69
      table.insert(res, {'  '..optfrag.location(trule), optfrag.command(trule)})
70 71 72 73
   end
   return res
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
74

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

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

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


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


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

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

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

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

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


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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
135

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

139
   self.uniqueids = {}
140

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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
153
   if self.service then
154 155 156 157
      if not self.label and type(self.service) == 'string' then
	 self.label = self.service
      end

158 159 160 161 162 163 164
      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
165 166 167 168
   end
end


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


176
function M.Rule:zoneoptfrags()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
177

178
   local function zonepair(zin, zout)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
179

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

      local chain, ofrags

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

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

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

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

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

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

   return res
end


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

   if not self.service then return end

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

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

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

	 else

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

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

287 288 289 290 291 292 293 294 295
	    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

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

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

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

   return res
end

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

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

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

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


373
function M.Rule:trules()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
374

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

   local families

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

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

   local res = self:zoneoptfrags()

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

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

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

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

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

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

   setfamilies(res)

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

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

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

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

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

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

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

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

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

   local function convertchains(ofrags)
      local res = {}

      for i, ofrag in ipairs(ofrags) do

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

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

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

      return res
   end

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

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

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

541 542
function M.Rule:uniqueid(key)
   if self.uniqueids[key] then return self.uniqueids[key] end
543

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

547 548 549 550 551 552
   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]

553
   self.uniqueids[key] = res
554
   return res
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
555 556
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
557

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

return M