awall-cli 9.31 KB
Newer Older
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
1 2 3 4
#!/usr/bin/lua

--[[
Alpine Wall
5
Copyright (C) 2012-2017 Kaarle Ritvanen
6
See LICENSE file for license details
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
7 8
]]--

9
get_opts = require('alt_getopt').get_opts
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
10
lpc = require('lpc')
11 12 13 14

posix = require('posix')
signal = posix.signal

15
stringy = require('stringy')
16

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
17 18 19
-- Lua 5.1 compatibility
if not table.unpack then table.unpack = unpack end

20 21
function help()
   io.stderr:write([[
22
Alpine Wall
23
Copyright (C) 2012-2017 Kaarle Ritvanen
24 25 26 27
This is free software with ABSOLUTELY NO WARRANTY,
available under the terms of the GNU General Public License, version 2

Usage:
28 29

Translate policy files to firewall configuration files:
30
    awall translate [-o|--output <dir>] [-V|--verify]
31 32 33 34 35 36 37 38 39 40 41

    The --verify option makes awall verify the configuration using the
    test mode of iptables-restore before overwriting the old files.

    Specifying the output directory allows testing awall policies
    without overwriting the current iptables and ipset configuration
    files. By default, awall generates the configuration to
    /etc/iptables and /etc/ipset.d, which are read by the init
    scripts.

Run-time activation of new firewall configuration:
42
    awall activate [-f|--force]
43

44
    This command genereates firewall configuration from the policy
45
    files and enables it. If the user confirms the new configuration
46 47 48
    by hitting RETURN within 10 seconds or the --force option is used,
    the configuration is saved to the files. Otherwise, the old
    configuration is restored.
49

50 51 52 53 54 55
Flush firewall configuration:
    awall flush

    This command deletes all firewall rules and configures it to drop
    all packets.

56 57 58 59
Enable/disable optional policies:
    awall {enable|disable} <policy>...

List optional policies:
60
    awall list [-a|--all]
61 62 63 64 65 66 67

    The 'enabled' status means that the policy has been enabled by the
    user. The 'disabled' status means that the policy is not in
    use. The 'required' status means that the policy has not been
    enabled by the user but is in use because it is required by
    another policy which is in use.

68 69 70
    Normally, the command lists only optional policies. Specifying
    --all makes it list all policies and more information about them.

71
Dump variable and zone definitions:
72 73
    awall dump [level]

74
    Verbosity level is an integer in range 0-5 and defaults to 0.
75

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
76 77 78 79 80 81 82 83 84 85
Show difference between modified and saved configurations:
    awall diff [-o|--output <dir>]

    Displays the difference in the input policy files and generated
    output files since the last 'translate' or 'activate' command.

    When the --output option is used, the updated configuration is
    compared to the generated files in the specified directory
    (generated by the equivalent 'translate' command).

86
]])
87
   os.exit(2)
88 89
end

90
if not arg[1] then help() end
91 92 93 94 95

if not stringy.startswith(arg[1], '-') then
   mode = arg[1]
   table.remove(arg, 1)
end
96

97
opts, opind = get_opts(
98 99 100 101
   arg,
   'afo:V',
   {all='a', force='f', ['output-dir']='o', verify='V'}
)
102
for switch, value in pairs(opts) do
103 104 105
   if switch == 'a' then all = true
   elseif switch == 'f' then force = true
   elseif switch == 'c' then verbose = true
106
   elseif switch == 'V' then verify = true
107
   elseif switch == 'o' then outputdir = value
108
   else assert(false) end
109
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
110

111 112 113 114 115 116
if not mode then
   mode = arg[opind]
   opind = opind + 1
end


117 118 119 120 121 122
dev_mode = stringy.endswith(arg[0], '/awall-cli')
if dev_mode then
   basedir = arg[0]:sub(1, -11)
   package.path = basedir..'/?/init.lua;'..basedir..'/?.lua;'..package.path
end

123 124
util = require('awall.util')
contains = util.contains
125
printmsg = util.printmsg
126

127 128 129 130 131 132 133 134 135
if not contains(
   {
      'translate',
      'activate',
      'fallback',
      'flush',
      'enable',
      'disable',
      'list',
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
136 137
      'dump',
      'diff'
138 139 140
   },
   mode
) then help() end
141

142 143
pol_paths = {}
for i, cls in ipairs{'mandatory', 'optional', 'private'} do
144
   path = os.getenv('AWALL_PATH_'..cls:upper())
145 146
   if path then pol_paths[cls] = util.split(path, ':') end
end
147

148
if dev_mode then
149
   util.setdefault(pol_paths, 'mandatory', {'/etc/awall'})
150 151 152
   table.insert(pol_paths.mandatory, basedir..'/json')
end

153 154
uerror = require('awall.uerror')
call = uerror.call
155

156
if not call(
157 158
   function()
      
159 160
      local awall = require('awall')
      local printtabular = util.printtabular
161
      local sortedkeys = util.sortedkeys
162

163
      local policyset = awall.PolicySet(pol_paths)
164

165
      if mode == 'list' then
166 167
	 local imported = policyset:load().policies
	 local data = {}
168
	 
169
	 for i, name in sortedkeys(policyset.policies) do
170
	    local policy = policyset.policies[name]
171 172 173

	    if all or policy.type == 'optional' then
	       if policy.enabled then status = 'enabled'
174
	       elseif contains(imported, name) then status = 'required'
175 176
	       else status = 'disabled' end

177
	       local polinfo = {name, status, policy:load().description}
178 179 180 181 182 183 184 185 186 187

	       if all then
		  table.insert(polinfo, 2, policy.type)
		  table.insert(polinfo, 4, policy.path)
	       end

	       table.insert(data, polinfo)
	    end
	 end
	 
188
	 printtabular(data)
189 190
	 os.exit()
      end
191

192
      if contains({'disable', 'enable'}, mode) then
193 194
	 if opind > #arg then help() end
	 repeat
195 196
	    local name = arg[opind]
	    local policy = policyset.policies[name]
197 198
	    if not policy then uerror.raise('No such policy: '..name) end
	    policy[mode](policy)
199 200 201 202
	    opind = opind + 1
	 until opind > #arg
	 os.exit()
      end
203

204

205
      local input = policyset:load()
206

207
      if mode == 'dump' then level = 0 + (arg[opind] or 0) end
208

209
      local config
210 211
      if mode ~= 'dump' or level > 3 then
	 awall.loadmodules(basedir)
212
	 config = awall.Config(input)
213
      end
214 215


Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
216
      local function dump(level)
217 218 219
	 local json = require('cjson').new()
	 json.encode_sort_keys(true)

220
	 local expinput = input:expand()
221

222
	 local function capitalize(cls)
223
	    return cls:sub(1, 1):upper()..cls:sub(2, -1)
224
	 end
225

226
	 for _, cls in sortedkeys(input.data) do
227
	    if level > 2 or (level == 2 and cls ~= 'service') or contains(
228 229 230
	       {'variable', 'zone'},
	       cls
	    ) then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
231
	       if level == 0 then io.write(capitalize(cls)..'s:\n') end
232 233

	       local clsdata = input.data[cls]
234
	       local items = {}
235 236 237

	       for _, key in sortedkeys(clsdata) do
		  local exp = expinput[cls][key]
238
		  local expj = json.encode(exp)
239
		  local src = input.source[cls][key]
240

241
		  if level == 0 then table.insert(items, {key, expj, src})
242 243

		  else
244
		     local value = clsdata[key]
245
		     local data = {
246
			{capitalize(cls)..' '..key, json.encode(value)},
247 248
			{
			   '('..src..')',
249
			   util.compare(exp, value) and '' or '-> '..expj
250 251 252 253
			}
		     }

		     if level > 3 then
254
			local obj = config.objects[cls][key]
255 256 257 258 259
			if type(obj) == 'table' and obj.info then
			   util.extend(data, obj:info())
			end
		     end
	       
260
		     table.insert(items, {key, data})
261 262
		  end
	       end
263 264
	       table.sort(items, function(a, b) return a[1] < b[1] end)

265
	       if level == 0 then printtabular(items)
266 267 268 269
	       else
		  util.printtabulars(
		     util.map(items, function(x) return x[2] end)
		  )
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
270
		  io.write('\n')
271 272 273
	       end
	    end
	 end
274

275
	 if level > 4 then config:print() end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
276 277 278 279 280 281 282
      end

      local function filedump(file)
	 io.output(file)
	 dump(5)
      end

283
      local sysdumpfile = '/etc/iptables/awall-save'
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
284 285
      local dumpfile = outputdir and outputdir..'/dump' or sysdumpfile

286

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
287 288 289 290 291 292
      local iptables = require('awall.iptables')


      if mode == 'dump' then dump(level)

      elseif mode == 'diff' then
293
	 if not posix.stat(dumpfile) then
294
	    printmsg('Please translate or activate first')
295
	    os.exit(2)
296 297
	 end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
298 299 300 301 302 303 304
	 local pid, stdin, stdout = lpc.run(
	    'diff', '-w', '--', dumpfile, '/proc/self/fd/0'
	 )

	 filedump(stdin)
	 stdin:close()

305 306
	 local data
	 repeat
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
307
	    -- Lua 5.2 compatibility: prefix with *
308
	    data = stdout:read('*a')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
309

310 311
	    io.stdout:write(data)
	 until data == ''
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
312
	 stdout:close()
313

314
	 os.exit(lpc.wait(pid) / 256)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
315
   
316 317
      elseif mode == 'translate' then
	 if verify then config:test() end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
318 319 320
	 config:dump(outputdir)	 
	 filedump(dumpfile)

321 322
      elseif mode == 'activate' then

323
	 iptables.backup()
324

325 326
	 local pid, interrupted

327
	 if not force then
328 329
	    signal(
	       posix.SIGCHLD,
330
	       function()
331
		  if pid and lpc.wait(pid, 1) then os.exit(1) end
332 333 334
	       end
	    )
	    for i, sig in ipairs({'INT', 'TERM'}) do
335 336
	       signal(
		  posix['SIG'..sig],
337 338 339 340 341
		  function()
		     interrupted = true
		     io.stdin:close()
		  end
	       )
342
	    end
343

344
	    local stdio, stdout
345 346 347
	    pid, stdio, stdout = lpc.run(arg[0], 'fallback')
	    stdio:close()
	    stdout:close()
348 349
	 end

350
	 local function kill()
351 352
	    signal(posix.SIGCHLD, 'SIG_DFL')
	    posix.kill(pid, posix.SIGTERM)
353
	    lpc.wait(pid)
354
	 end
355

356
	 local function revert()
357
	    iptables.revert()
358
	    os.exit(2)
359
	 end
360

361
	 if call(config.activate, config) then
362

363
	    if not force then
364
	       printmsg('New firewall configuration activated')
365 366
	       io.stderr:write('Press RETURN to commit changes permanently: ')
	       interrupted = not io.read()
367

368
	       kill()
369

370
	       if interrupted then
371 372
		  printmsg(
		     '\nActivation canceled, reverting to the old configuration'
373 374 375 376
		  )
		  revert()
	       end
	    end
377

378
	    config:dump()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
379
	    filedump(sysdumpfile)
380

381 382 383 384
	 else
	    if not force then kill() end
	    revert()
	 end
385 386


387 388 389
      elseif mode == 'fallback' then
   
	 for i, sig in ipairs({'HUP', 'PIPE'}) do
390
	    signal(posix['SIG'..sig], 'SIG_IGN')
391
	 end
392

393
	 posix.sleep(10)
394

395
	 printmsg('\nTimeout, reverting to the old configuration')
396
	 iptables.revert()
397

398
      elseif mode == 'flush' then iptables.flush()
399

400
      else assert(false) end
401

402
   end
403
) then os.exit(2) end