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

--[[
Alpine Wall
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
5
Copyright (C) 2012-2017 Kaarle Ritvanen
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
20 21
function help()
   io.stderr:write([[
22
Alpine Wall
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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:
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
28 29

Translate policy files to firewall configuration files:
30
    awall translate [-o|--output <dir>] [-V|--verify]
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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]
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
43

44
    This command genereates firewall configuration from the policy
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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.
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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.

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
56 57 58 59
Enable/disable optional policies:
    awall {enable|disable} <policy>...

List optional policies:
60
    awall list [-a|--all]
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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).

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
86
]])
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
87
   os.exit(2)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
88 89
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
90
if not arg[1] then help() end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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'}
)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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(
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
189 190
	 os.exit()
      end
191

192
      if contains({'disable', 'enable'}, mode) then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
199 200 201 202
	    opind = opind + 1
	 until opind > #arg
	 os.exit()
      end
203

204

205
      local input = policyset:load()
206

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
207
      if mode == 'dump' then level = 0 + (arg[opind] or 0) end
208

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


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

220
	 local expinput = input:expand()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
221

222
	 local function capitalize(cls)
223
	    return cls:sub(1, 1):upper()..cls:sub(2, -1)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
224
	 end
225

226
	 for _, cls in sortedkeys(input.data) do
227
	    if level > 2 or (level == 2 and cls ~= 'service') or contains(
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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]
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
240

241
		  if level == 0 then table.insert(items, {key, expj, src})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
242 243

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

		     if level > 3 then
254
			local obj = config.objects[cls][key]
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
263 264
	       table.sort(items, function(a, b) return a[1] < b[1] end)

265
	       if level == 0 then printtabular(items)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
271 272 273
	       end
	    end
	 end
274

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
314
	 os.exit(lpc.wait(pid) / 256)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
315
   
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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)

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
321 322
      elseif mode == 'activate' then

323
	 iptables.backup()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
324

325 326
	 local pid, interrupted

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

344
	    local stdio, stdout
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
353
	    lpc.wait(pid)
354
	 end
355

356
	 local function revert()
357
	    iptables.revert()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
358
	    os.exit(2)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
359
	 end
360

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

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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
368
	       kill()
369

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

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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
381 382 383 384
	 else
	    if not force then kill() end
	    revert()
	 end
385 386


Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
387 388 389
      elseif mode == 'fallback' then
   
	 for i, sig in ipairs({'HUP', 'PIPE'}) do
390
	    signal(posix['SIG'..sig], 'SIG_IGN')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
400
      else assert(false) end
401

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
402
   end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
403
) then os.exit(2) end