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

--[[
Alpine Wall
5
Copyright (C) 2012-2014 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
require 'alt_getopt'
10
require 'lfs'
11
require 'signal'
12 13
require 'stringy'

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
14 15
function help()
   io.stderr:write([[
16
Alpine Wall
17
Copyright (C) 2012-2014 Kaarle Ritvanen
18 19 20 21
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
22 23

Translate policy files to firewall configuration files:
24
    awall translate [-o|--output <dir>] [-V|--verify]
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
25 26 27 28 29 30 31 32 33 34 35

    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:
36
    awall activate [-f|--force]
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
37

38
    This command genereates firewall configuration from the policy
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
39
    files and enables it. If the user confirms the new configuration
40 41 42
    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
43

44 45 46 47 48 49
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
50 51 52 53
Enable/disable optional policies:
    awall {enable|disable} <policy>...

List optional policies:
54
    awall list [-a|--all]
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
55 56 57 58 59 60 61

    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.

62 63 64
    Normally, the command lists only optional policies. Specifying
    --all makes it list all policies and more information about them.

65
Dump variable and zone definitions:
66 67
    awall dump [level]

68
    Verbosity level is an integer in range 0-5 and defaults to 0.
69

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
70
]])
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
71
   os.exit(1)
72 73
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
74
if not arg[1] then help() end
75 76 77 78 79

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

81 82 83 84 85
opts, opind = alt_getopt.get_opts(
   arg,
   'afo:V',
   {all='a', force='f', ['output-dir']='o', verify='V'}
)
86
for switch, value in pairs(opts) do
87 88 89
   if switch == 'a' then all = true
   elseif switch == 'f' then force = true
   elseif switch == 'c' then verbose = true
90
   elseif switch == 'V' then verify = true
91
   elseif switch == 'o' then outputdir = value
92
   else assert(false) end
93
end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
94

95 96 97 98 99 100 101
if not mode then
   mode = arg[opind]
   opind = opind + 1
end


require 'awall.util'
102
util = awall.util
103

104
if not util.contains({'translate', 'activate', 'fallback', 'flush',
105 106
		      'enable', 'disable', 'list', 'dump'},
		     mode) then help() end
107

108 109
pol_paths = {}
for i, cls in ipairs{'mandatory', 'optional', 'private'} do
110
   path = os.getenv('AWALL_PATH_'..cls:upper())
111 112
   if path then pol_paths[cls] = util.split(path, ':') end
end
113

114
if stringy.endswith(arg[0], '/awall-cli') then
115
   basedir = arg[0]:sub(1, -11)
116 117 118 119 120 121 122
   if not pol_paths.mandatory then
      pol_paths.mandatory = {'/etc/awall'}
   end
   table.insert(pol_paths.mandatory, basedir..'/json')
end

local uerror = require('awall.uerror')
123

124
if not uerror.call(
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
125 126 127
   function()
      
      require 'awall'
128

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
129
      policyset = awall.PolicySet(pol_paths)
130

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
131
      if mode == 'list' then
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
	 imported = policyset:load().policies
	 data = {}
	 
	 for i, name in util.sortedkeys(policyset.policies) do
	    policy = policyset.policies[name]

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

	       polinfo = {name, status, policy:load().description}

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

	       table.insert(data, polinfo)
	    end
	 end
	 
	 util.printtabular(data)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
155 156
	 os.exit()
      end
157

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
158 159 160
      if util.contains({'disable', 'enable'}, mode) then
	 if opind > #arg then help() end
	 repeat
161 162 163 164
	    name = arg[opind]
	    policy = policyset.policies[name]
	    if not policy then uerror.raise('No such policy: '..name) end
	    policy[mode](policy)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
165 166 167 168
	    opind = opind + 1
	 until opind > #arg
	 os.exit()
      end
169

170

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
171
      input = policyset:load()
172

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

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
175 176
      if mode ~= 'dump' or level > 3 then
	 awall.loadmodules(basedir)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
177
	 config = awall.Config(input)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
178
      end
179 180


Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
181
      require 'awall.iptables'
182

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
183 184 185 186 187
      if mode == 'dump' then
	 require 'json'
	 expinput = input:expand()

	 function capitalize(cls)
188
	    return cls:sub(1, 1):upper()..cls:sub(2, -1)
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
189
	 end
190

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
191 192 193 194 195 196
	 for cls, objs in pairs(input.data) do
	    if level > 2 or (level == 2 and cls ~= 'service') or util.contains(
	       {'variable', 'zone'},
	       cls
	    ) then
	       if level == 0 then print(capitalize(cls)..'s:') end
197
	 
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
	       items = {}
	       for k, v in pairs(objs) do
		  exp = expinput[cls][k]
		  expj = json.encode(exp)
		  src = input.source[cls][k]

		  if level == 0 then table.insert(items, {k, expj, src})

		  else
		     data = {
			{capitalize(cls)..' '..k, json.encode(v)},
			{
			   '('..src..')',
			   util.compare(exp, v) and '' or '-> '..expj
			}
		     }

		     if level > 3 then
			obj = config.objects[cls][k]
			if type(obj) == 'table' and obj.info then
			   util.extend(data, obj:info())
			end
		     end
	       
		     table.insert(items, {k, data})
223 224
		  end
	       end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
225 226 227 228 229 230 231 232 233 234 235
	       table.sort(items, function(a, b) return a[1] < b[1] end)

	       if level == 0 then util.printtabular(items)
	       else
		  util.printtabulars(
		     util.map(items, function(x) return x[2] end)
		  )
		  print()
	       end
	    end
	 end
236

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261
	 if level > 4 then config:print() end

      elseif mode == 'translate' then
	 if verify then config:test() end
	 config:dump(outputdir)
   
      elseif mode == 'activate' then

	 awall.iptables.backup()

	 if not force then
	    signal.signal(
	       'SIGCHLD',
	       function()
		  if pid and lpc.wait(pid, 1) then os.exit(2) end
	       end
	    )
	    for i, sig in ipairs({'INT', 'TERM'}) do
	       signal.signal(
		  'SIG'..sig,
		  function()
		     interrupted = true
		     io.stdin:close()
		  end
	       )
262
	    end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
263 264 265 266 267

	    require 'lpc'
	    pid, stdio, stdout = lpc.run(arg[0], 'fallback')
	    stdio:close()
	    stdout:close()
268 269
	 end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
270 271 272 273
	 function kill()
	    signal.signal('SIGCHLD', 'default')
	    signal.kill(pid, 'SIGTERM')
	    lpc.wait(pid)
274
	 end
275

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
276 277 278 279
	 function revert()
	    awall.iptables.revert()
	    os.exit(1)
	 end
280

281
	 if uerror.call(config.activate, config) then
282

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
283 284 285 286
	    if not force then
	       io.stderr:write('New firewall configuration activated\n')
	       io.stderr:write('Press RETURN to commit changes permanently: ')
	       interrupted = not io.read()
287

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
288
	       kill()
289

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
290 291 292 293 294 295 296
	       if interrupted then
		  io.stderr:write(
		     '\nActivation canceled, reverting to the old configuration\n'
		  )
		  revert()
	       end
	    end
297

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
298
	    config:dump()
299

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
300 301 302 303
	 else
	    if not force then kill() end
	    revert()
	 end
304 305


Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
306 307 308 309 310
      elseif mode == 'fallback' then
   
	 for i, sig in ipairs({'HUP', 'PIPE'}) do
	    signal.signal('SIG'..sig, function() end)
	 end
311

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
312 313
	 require 'lsleep'
	 lsleep.sleep(10)
314

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
315 316
	 io.stderr:write('\nTimeout, reverting to the old configuration\n')
	 awall.iptables.revert()
317

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
318
      elseif mode == 'flush' then awall.iptables.flush()
319

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
320
      else assert(false) end
321

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
322 323
   end
) then os.exit(1) end