awall-cli 9.14 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(1)
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
util = require('awall.util')
contains = util.contains
119
printmsg = util.printmsg
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
120

121 122 123 124 125 126 127 128 129
if not contains(
   {
      'translate',
      'activate',
      'fallback',
      'flush',
      'enable',
      'disable',
      'list',
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
130 131
      'dump',
      'diff'
132 133 134
   },
   mode
) then help() end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
135

136 137
pol_paths = {}
for i, cls in ipairs{'mandatory', 'optional', 'private'} do
138
   path = os.getenv('AWALL_PATH_'..cls:upper())
139 140
   if path then pol_paths[cls] = util.split(path, ':') end
end
141

142
if stringy.endswith(arg[0], '/awall-cli') then
143
   basedir = arg[0]:sub(1, -11)
144
   util.setdefault(pol_paths, 'mandatory', {'/etc/awall'})
145 146 147
   table.insert(pol_paths.mandatory, basedir..'/json')
end

148 149
uerror = require('awall.uerror')
call = uerror.call
150

151
if not call(
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
152 153
   function()
      
154 155
      local awall = require('awall')
      local printtabular = util.printtabular
156
      local sortedkeys = util.sortedkeys
157

158
      local policyset = awall.PolicySet(pol_paths)
159

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
160
      if mode == 'list' then
161 162
	 local imported = policyset:load().policies
	 local data = {}
163
	 
164
	 for i, name in sortedkeys(policyset.policies) do
165
	    local policy = policyset.policies[name]
166 167 168

	    if all or policy.type == 'optional' then
	       if policy.enabled then status = 'enabled'
169
	       elseif contains(imported, name) then status = 'required'
170 171
	       else status = 'disabled' end

172
	       local polinfo = {name, status, policy:load().description}
173 174 175 176 177 178 179 180 181 182

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

	       table.insert(data, polinfo)
	    end
	 end
	 
183
	 printtabular(data)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
184 185
	 os.exit()
      end
186

187
      if contains({'disable', 'enable'}, mode) then
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
188 189
	 if opind > #arg then help() end
	 repeat
190 191
	    local name = arg[opind]
	    local policy = policyset.policies[name]
192 193
	    if not policy then uerror.raise('No such policy: '..name) end
	    policy[mode](policy)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
194 195 196 197
	    opind = opind + 1
	 until opind > #arg
	 os.exit()
      end
198

199

200
      local input = policyset:load()
201

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

204
      local config
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
205 206
      if mode ~= 'dump' or level > 3 then
	 awall.loadmodules(basedir)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
207
	 config = awall.Config(input)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
208
      end
209 210


Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
211
      local function dump(level)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
212 213 214
	 local json = require('cjson').new()
	 json.encode_sort_keys(true)

215
	 local expinput = input:expand()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
216

217
	 local function capitalize(cls)
218
	    return cls:sub(1, 1):upper()..cls:sub(2, -1)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
219
	 end
220

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

	       local clsdata = input.data[cls]
229
	       local items = {}
230 231 232

	       for _, key in sortedkeys(clsdata) do
		  local exp = expinput[cls][key]
233
		  local expj = json.encode(exp)
234
		  local src = input.source[cls][key]
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
235

236
		  if level == 0 then table.insert(items, {key, expj, src})
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
237 238

		  else
239
		     local value = clsdata[key]
240
		     local data = {
241
			{capitalize(cls)..' '..key, json.encode(value)},
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
242 243
			{
			   '('..src..')',
244
			   util.compare(exp, value) and '' or '-> '..expj
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
245 246 247 248
			}
		     }

		     if level > 3 then
249
			local obj = config.objects[cls][key]
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
250 251 252 253 254
			if type(obj) == 'table' and obj.info then
			   util.extend(data, obj:info())
			end
		     end
	       
255
		     table.insert(items, {key, data})
256 257
		  end
	       end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
258 259
	       table.sort(items, function(a, b) return a[1] < b[1] end)

260
	       if level == 0 then printtabular(items)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
261 262 263 264
	       else
		  util.printtabulars(
		     util.map(items, function(x) return x[2] end)
		  )
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
265
		  io.write('\n')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
266 267 268
	       end
	    end
	 end
269

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
270
	 if level > 4 then config:print() end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
271 272 273 274 275 276 277
      end

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

278
      local sysdumpfile = '/etc/iptables/awall-save'
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
279 280
      local dumpfile = outputdir and outputdir..'/dump' or sysdumpfile

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
281

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
282 283 284 285 286 287
      local iptables = require('awall.iptables')


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

      elseif mode == 'diff' then
288
	 if not posix.stat(dumpfile) then
289
	    printmsg('Please translate or activate first')
290 291 292
	    os.exit(1)
	 end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
293 294 295 296 297 298 299
	 local pid, stdin, stdout = lpc.run(
	    'diff', '-w', '--', dumpfile, '/proc/self/fd/0'
	 )

	 filedump(stdin)
	 stdin:close()

300 301 302 303 304
	 local data
	 repeat
	    data = stdout:read('*a')
	    io.stdout:write(data)
	 until data == ''
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
305
	 stdout:close()
306 307

	 lpc.wait(pid)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
308
   
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
309 310
      elseif mode == 'translate' then
	 if verify then config:test() end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
311 312 313
	 config:dump(outputdir)	 
	 filedump(dumpfile)

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
314 315
      elseif mode == 'activate' then

316
	 iptables.backup()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
317

318 319
	 local pid, interrupted

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
320
	 if not force then
321 322
	    signal(
	       posix.SIGCHLD,
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
323 324 325 326 327
	       function()
		  if pid and lpc.wait(pid, 1) then os.exit(2) end
	       end
	    )
	    for i, sig in ipairs({'INT', 'TERM'}) do
328 329
	       signal(
		  posix['SIG'..sig],
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
330 331 332 333 334
		  function()
		     interrupted = true
		     io.stdin:close()
		  end
	       )
335
	    end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
336

337
	    local stdio, stdout
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
338 339 340
	    pid, stdio, stdout = lpc.run(arg[0], 'fallback')
	    stdio:close()
	    stdout:close()
341 342
	 end

343
	 local function kill()
344 345
	    signal(posix.SIGCHLD, 'SIG_DFL')
	    posix.kill(pid, posix.SIGTERM)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
346
	    lpc.wait(pid)
347
	 end
348

349
	 local function revert()
350
	    iptables.revert()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
351 352
	    os.exit(1)
	 end
353

354
	 if call(config.activate, config) then
355

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
356
	    if not force then
357
	       printmsg('New firewall configuration activated')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
358 359
	       io.stderr:write('Press RETURN to commit changes permanently: ')
	       interrupted = not io.read()
360

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
361
	       kill()
362

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
363
	       if interrupted then
364 365
		  printmsg(
		     '\nActivation canceled, reverting to the old configuration'
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
366 367 368 369
		  )
		  revert()
	       end
	    end
370

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
371
	    config:dump()
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
372
	    filedump(sysdumpfile)
373

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
374 375 376 377
	 else
	    if not force then kill() end
	    revert()
	 end
378 379


Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
380 381 382
      elseif mode == 'fallback' then
   
	 for i, sig in ipairs({'HUP', 'PIPE'}) do
383
	    signal(posix['SIG'..sig], 'SIG_IGN')
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
384
	 end
385

386
	 posix.sleep(10)
387

388
	 printmsg('\nTimeout, reverting to the old configuration')
389
	 iptables.revert()
390

391
      elseif mode == 'flush' then iptables.flush()
392

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
393
      else assert(false) end
394

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
395 396
   end
) then os.exit(1) end