1
# Copyright 2014-2015 Canonical Limited.
3
# This file is part of charm-helpers.
5
# charm-helpers is free software: you can redistribute it and/or modify
6
# it under the terms of the GNU Lesser General Public License version 3 as
7
# published by the Free Software Foundation.
9
# charm-helpers is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU Lesser General Public License for more details.
14
# You should have received a copy of the GNU Lesser General Public License
15
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
18
This module contains helpers to add and remove ufw rules.
22
- open SSH port for subnet 10.0.3.0/24:
24
>>> from charmhelpers.contrib.network import ufw
26
>>> ufw.grant_access(src='10.0.3.0/24', dst='any', port='22', proto='tcp')
28
- open service by name as defined in /etc/services:
30
>>> from charmhelpers.contrib.network import ufw
32
>>> ufw.service('ssh', 'open')
34
- close service by port number:
36
>>> from charmhelpers.contrib.network import ufw
38
>>> ufw.service('4949', 'close') # munin
43
from charmhelpers.core import hookenv
45
__author__ = "Felipe Reyes <felipe.reyes@canonical.com>"
48
class UFWError(Exception):
52
class UFWIPv6Error(UFWError):
58
Check if `ufw` is enabled
60
:returns: True if ufw is enabled
62
output = subprocess.check_output(['ufw', 'status'],
63
universal_newlines=True,
65
'PATH': os.environ['PATH']})
67
m = re.findall(r'^Status: active\n', output, re.M)
72
def is_ipv6_ok(soft_fail=False):
74
Check if IPv6 support is present and ip6tables functional
76
:param soft_fail: If set to True and IPv6 support is broken, then reports
77
that the host doesn't have IPv6 support, otherwise a
78
UFWIPv6Error exception is raised.
79
:returns: True if IPv6 is working, False otherwise
82
# do we have IPv6 in the machine?
83
if os.path.isdir('/proc/sys/net/ipv6'):
84
# is ip6tables kernel module loaded?
85
lsmod = subprocess.check_output(['lsmod'], universal_newlines=True)
86
matches = re.findall('^ip6_tables[ ]+', lsmod, re.M)
88
# ip6tables support isn't complete, let's try to load it
90
subprocess.check_output(['modprobe', 'ip6_tables'],
91
universal_newlines=True)
92
# great, we could load the module
94
except subprocess.CalledProcessError as ex:
95
hookenv.log("Couldn't load ip6_tables module: %s" % ex.output,
97
# we are in a world where ip6tables isn't working
99
# so we inform that the machine doesn't have IPv6
102
raise UFWIPv6Error("IPv6 firewall support broken")
104
# the module is present :)
108
# the system doesn't have IPv6
114
Disable ufw IPv6 support in /etc/default/ufw
116
exit_code = subprocess.call(['sed', '-i', 's/IPV6=.*/IPV6=no/g',
119
hookenv.log('IPv6 support in ufw disabled', level='INFO')
121
hookenv.log("Couldn't disable IPv6 support in ufw", level="ERROR")
122
raise UFWError("Couldn't disable IPv6 support in ufw")
125
def enable(soft_fail=False):
129
:param soft_fail: If set to True silently disables IPv6 support in ufw,
130
otherwise a UFWIPv6Error exception is raised when IP6
132
:returns: True if ufw is successfully enabled
137
if not is_ipv6_ok(soft_fail):
140
output = subprocess.check_output(['ufw', 'enable'],
141
universal_newlines=True,
142
env={'LANG': 'en_US',
143
'PATH': os.environ['PATH']})
145
m = re.findall('^Firewall is active and enabled on system startup\n',
147
hookenv.log(output, level='DEBUG')
150
hookenv.log("ufw couldn't be enabled", level='WARN')
153
hookenv.log("ufw enabled", level='INFO')
161
:returns: True if ufw is successfully disabled
166
output = subprocess.check_output(['ufw', 'disable'],
167
universal_newlines=True,
168
env={'LANG': 'en_US',
169
'PATH': os.environ['PATH']})
171
m = re.findall(r'^Firewall stopped and disabled on system startup\n',
173
hookenv.log(output, level='DEBUG')
176
hookenv.log("ufw couldn't be disabled", level='WARN')
179
hookenv.log("ufw disabled", level='INFO')
183
def default_policy(policy='deny', direction='incoming'):
185
Changes the default policy for traffic `direction`
187
:param policy: allow, deny or reject
188
:param direction: traffic direction, possible values: incoming, outgoing,
191
if policy not in ['allow', 'deny', 'reject']:
192
raise UFWError(('Unknown policy %s, valid values: '
193
'allow, deny, reject') % policy)
195
if direction not in ['incoming', 'outgoing', 'routed']:
196
raise UFWError(('Unknown direction %s, valid values: '
197
'incoming, outgoing, routed') % direction)
199
output = subprocess.check_output(['ufw', 'default', policy, direction],
200
universal_newlines=True,
201
env={'LANG': 'en_US',
202
'PATH': os.environ['PATH']})
203
hookenv.log(output, level='DEBUG')
205
m = re.findall("^Default %s policy changed to '%s'\n" % (direction,
209
hookenv.log("ufw couldn't change the default policy to %s for %s"
210
% (policy, direction), level='WARN')
213
hookenv.log("ufw default policy for %s changed to %s"
214
% (direction, policy), level='INFO')
218
def modify_access(src, dst='any', port=None, proto=None, action='allow',
221
Grant access to an address or subnet
223
:param src: address (e.g. 192.168.1.234) or subnet
224
(e.g. 192.168.1.0/24).
225
:param dst: destiny of the connection, if the machine has multiple IPs and
226
connections to only one of those have to accepted this is the
228
:param port: destiny port
229
:param proto: protocol (tcp or udp)
230
:param action: `allow` or `delete`
231
:param index: if different from None the rule is inserted at the given
235
hookenv.log('ufw is disabled, skipping modify_access()', level='WARN')
238
if action == 'delete':
239
cmd = ['ufw', 'delete', 'allow']
240
elif index is not None:
241
cmd = ['ufw', 'insert', str(index), action]
243
cmd = ['ufw', action]
252
cmd += ['port', str(port)]
254
if proto is not None:
255
cmd += ['proto', proto]
257
hookenv.log('ufw {}: {}'.format(action, ' '.join(cmd)), level='DEBUG')
258
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
259
(stdout, stderr) = p.communicate()
261
hookenv.log(stdout, level='INFO')
263
if p.returncode != 0:
264
hookenv.log(stderr, level='ERROR')
265
hookenv.log('Error running: {}, exit code: {}'.format(' '.join(cmd),
270
def grant_access(src, dst='any', port=None, proto=None, index=None):
272
Grant access to an address or subnet
274
:param src: address (e.g. 192.168.1.234) or subnet
275
(e.g. 192.168.1.0/24).
276
:param dst: destiny of the connection, if the machine has multiple IPs and
277
connections to only one of those have to accepted this is the
279
:param port: destiny port
280
:param proto: protocol (tcp or udp)
281
:param index: if different from None the rule is inserted at the given
284
return modify_access(src, dst=dst, port=port, proto=proto, action='allow',
288
def revoke_access(src, dst='any', port=None, proto=None):
290
Revoke access to an address or subnet
292
:param src: address (e.g. 192.168.1.234) or subnet
293
(e.g. 192.168.1.0/24).
294
:param dst: destiny of the connection, if the machine has multiple IPs and
295
connections to only one of those have to accepted this is the
297
:param port: destiny port
298
:param proto: protocol (tcp or udp)
300
return modify_access(src, dst=dst, port=port, proto=proto, action='delete')
303
def service(name, action):
305
Open/close access to a service
307
:param name: could be a service name defined in `/etc/services` or a port
309
:param action: `open` or `close`
312
subprocess.check_output(['ufw', 'allow', str(name)],
313
universal_newlines=True)
314
elif action == 'close':
315
subprocess.check_output(['ufw', 'delete', 'allow', str(name)],
316
universal_newlines=True)
318
raise UFWError(("'{}' not supported, use 'allow' "
319
"or 'delete'").format(action))