+# vim: set encoding=utf-8 et sw=4 sts=4 :
+
+# TODO See if it's better (more secure) to execute commands via python instead
+# of using script templates.
+
+from mako.template import Template
+from mako.runtime import Context
+from os import path
+try:
+ import cPickle as pickle
+except ImportError:
+ import pickle
+
+try:
+ from seqtools import Sequence
+except ImportError:
+ # NOP for testing
+ class Sequence: pass
+try:
+ from dispatcher import Handler, handler, HandlerError
+except ImportError:
+ # NOP for testing
+ class HandlerError(RuntimeError): pass
+ class Handler: pass
+ def handler(help):
+ def wrapper(f):
+ return f
+ return wrapper
+
+__ALL__ = ('FirewallHandler', 'Error', 'RuleError', 'RuleAlreadyExistsError',
+ 'RuleNotFoundError')
+
+pickle_ext = '.pkl'
+pickle_rules = 'rules'
+
+config_filename = 'iptables.sh'
+
+template_dir = path.join(path.dirname(__file__), 'templates')
+
+class Error(HandlerError):
+ r"""
+ Error(command) -> Error instance :: Base FirewallHandler exception class.
+
+ All exceptions raised by the FirewallHandler inherits from this one, so you can
+ easily catch any FirewallHandler exception.
+
+ message - A descriptive error message.
+ """
+
+ def __init__(self, message):
+ r"Initialize the Error object. See class documentation for more info."
+ self.message = message
+
+ def __str__(self):
+ return self.message
+
+class RuleError(Error, KeyError):
+ r"""
+ RuleError(rule) -> RuleError instance.
+
+ This is the base exception for all rule related errors.
+ """
+
+ def __init__(self, rule):
+ r"Initialize the object. See class documentation for more info."
+ self.message = 'Rule error: "%s"' % rule
+
+class RuleAlreadyExistsError(RuleError):
+ r"""
+ RuleAlreadyExistsError(rule) -> RuleAlreadyExistsError instance.
+
+ This exception is raised when trying to add a rule that already exists.
+ """
+
+ def __init__(self, rule):
+ r"Initialize the object. See class documentation for more info."
+ self.message = 'Rule already exists: "%s"' % rule
+
+class RuleNotFoundError(RuleError):
+ r"""
+ RuleNotFoundError(rule) -> RuleNotFoundError instance.
+
+ This exception is raised when trying to operate on a rule that doesn't
+ exists.
+ """
+
+ def __init__(self, rule):
+ r"Initialize the object. See class documentation for more info."
+ self.message = 'Rule not found: "%s"' % rule
+
+class Rule(Sequence):
+ r"""Rule(chain, target[, src[, dst[, ...]]]) -> Rule instance.
+
+ chain - INPUT, OUTPUT or FORWARD.
+ target - ACCEPT, REJECT or DROP.
+ src - Source subnet as IP/mask.
+ dst - Destination subnet as IP/mask.
+ protocol - ICMP, UDP, TCP or ALL.
+ src_port - Source port (only for UDP or TCP protocols).
+ dst_port - Destination port (only for UDP or TCP protocols).
+ """
+
+ def __init__(self, chain, target, src=None, dst=None, protocol=None,
+ src_port=None, dst_port=None):
+ r"Initialize object, see class documentation for details."
+ self.chain = chain
+ self.target = target
+ self.src = src
+ self.dst = dst
+ self.protocol = protocol
+ # TODO Validate that src_port and dst_port could be not None only
+ # if the protocol is UDP or TCP
+ self.src_port = src_port
+ self.dst_port = dst_port
+
+ def update(self, chain=None, target=None, src=None, dst=None, protocol=None,
+ src_port=None, dst_port=None):
+ r"update([chain[, ...]]) -> Update the values of a rule (see Rule doc)."
+ if chain is not None: self.chain = chain
+ if target is not None: self.target = target
+ if src is not None: self.src = src
+ if dst is not None: self.dst = dst
+ if protocol is not None: self.protocol = protocol
+ # TODO Validate that src_port and dst_port could be not None only
+ # if the protocol is UDP or TCP
+ if src_port is not None: self.src_port = src_port
+ if dst_port is not None: self.dst_port = dst_port
+
+ def __cmp__(self, other):
+ r"Compares two Rule objects."
+ if self.chain == other.chain \
+ and self.target == other.target \
+ and self.src == other.src \
+ and self.dst == other.dst \
+ and self.protocol == other.protocol \
+ and self.src_port == other.src_port \
+ and self.dst_port == other.dst_port:
+ return 0
+ return cmp(id(self), id(other))
+
+ def as_tuple(self):
+ r"Return a tuple representing the rule."
+ return (self.chain, self.target, self.src, self.dst, self.protocol,
+ self.src_port)
+
+class RuleHandler(Handler):
+ r"""RuleHandler(rules) -> RuleHandler instance :: Handle a list of rules.
+
+ This class is a helper for FirewallHandler to do all the work related to rules
+ administration.
+
+ rules - A list of Rule objects.
+ """
+
+ def __init__(self, rules):
+ r"Initialize the object, see class documentation for details."
+ self.rules = rules
+
+ @handler(u'Add a new rule.')
+ def add(self, *args, **kwargs):
+ r"add(rule) -> None :: Add a rule to the rules list (see Rule doc)."
+ rule = Rule(*args, **kwargs)
+ if rule in self.rules:
+ raise RuleAlreadyExistsError(rule)
+ self.rules.append(rule)
+
+ @handler(u'Update a rule.')
+ def update(self, index, *args, **kwargs):
+ r"update(index, rule) -> None :: Update a rule (see Rule doc)."
+ # TODO check if the modified rule is the same of an existing one
+ index = int(index) # TODO validation
+ try:
+ self.rules[index].update(*args, **kwargs)
+ except IndexError:
+ raise RuleNotFoundError(index)
+
+ @handler(u'Delete a rule.')
+ def delete(self, index):
+ r"delete(index) -> Rule :: Delete a rule from the list returning it."
+ index = int(index) # TODO validation
+ try:
+ return self.rules.pop(index)
+ except IndexError:
+ raise RuleNotFoundError(index)
+
+ @handler(u'Get information about a rule.')
+ def get(self, index):
+ r"get(rule) -> Rule :: Get all the information about a rule."
+ index = int(index) # TODO validation
+ try:
+ return self.rules[index]
+ except IndexError:
+ raise RuleNotFoundError(index)
+
+ @handler(u'Get information about all rules.')
+ def show(self):
+ r"show() -> list of Rules :: List all the complete rules information."
+ return self.rules
+
+class FirewallHandler(Handler):
+ r"""FirewallHandler([pickle_dir[, config_dir]]) -> FirewallHandler instance.
+
+ Handles firewall commands using iptables.
+
+ pickle_dir - Directory where to write the persistent configuration data.
+
+ config_dir - Directory where to store de generated configuration files.
+
+ Both defaults to the current working directory.
+ """
+
+ def __init__(self, pickle_dir='.', config_dir='.'):
+ r"Initialize FirewallHandler object, see class documentation for details."
+ self.pickle_dir = pickle_dir
+ self.config_dir = config_dir
+ filename = path.join(template_dir, config_filename)
+ self.template = Template(filename=filename)
+ try:
+ self._load()
+ except IOError:
+ # This is the first time the handler is used, create a basic
+ # setup using some nice defaults
+ self.rules = list() # TODO defaults?
+ self._dump()
+ self._write_config()
+ self.rule = RuleHandler(self.rules)
+
+ # Does this (start, stop, restart, reload) makes sense???
+ # Implement a "try" command that apply the changes for some time and
+ # then goes back to the previous configuration if the changes are not
+ # commited. TODO
+ @handler(u'Start the service.')
+ def start(self):
+ r"start() -> None :: Start the firewall."
+ #esto seria para poner en una interfaz
+ #y seria el hook para arrancar el servicio
+ pass
+
+ @handler(u'Stop the service.')
+ def stop(self):
+ r"stop() -> None :: Stop the firewall."
+ #esto seria para poner en una interfaz
+ #y seria el hook para arrancar el servicio
+ pass
+
+ @handler(u'Restart the service.')
+ def restart(self):
+ r"restart() -> None :: Restart the firewall."
+ #esto seria para poner en una interfaz
+ #y seria el hook para arrancar el servicio
+ pass
+
+ @handler(u'Reload the service config (without restarting, if possible).')
+ def reload(self):
+ r"reload() -> None :: Reload the configuration of the firewall."
+ #esto seria para poner en una interfaz
+ #y seria el hook para arrancar el servicio
+ pass
+
+ @handler(u'Commit the changes (reloading the service, if necessary).')
+ def commit(self):
+ r"commit() -> None :: Commit the changes and reload the firewall."
+ #esto seria para poner en una interfaz
+ #y seria que hace el pickle deberia llamarse
+ #al hacerse un commit
+ self._dump()
+ self._write_config()
+ self.reload() # TODO exec the script
+
+ @handler(u'Discard all the uncommited changes.')
+ def rollback(self):
+ r"rollback() -> None :: Discard the changes not yet commited."
+ self._load()
+
+ def _dump(self):
+ r"_dump() -> None :: Dump all persistent data to pickle files."
+ # XXX podría ir en una clase base
+ self._dump_var(self.rules, pickle_rules)
+
+ def _load(self):
+ r"_load() -> None :: Load all persistent data from pickle files."
+ # XXX podría ir en una clase base
+ self.rules = self._load_var(pickle_rules)
+
+ def _pickle_filename(self, name):
+ r"_pickle_filename() -> string :: Construct a pickle filename."
+ # XXX podría ir en una clase base
+ return path.join(self.pickle_dir, name) + pickle_ext
+
+ def _dump_var(self, var, name):
+ r"_dump_var() -> None :: Dump a especific variable to a pickle file."
+ # XXX podría ir en una clase base
+ pkl_file = file(self._pickle_filename(name), 'wb')
+ pickle.dump(var, pkl_file, 2)
+ pkl_file.close()
+
+ def _load_var(self, name):
+ r"_load_var() -> object :: Load a especific pickle file."
+ # XXX podría ir en una clase base
+ return pickle.load(file(self._pickle_filename(name)))
+
+ def _write_config(self):
+ r"_write_config() -> None :: Generate all the configuration files."
+ # XXX podría ir en una clase base, ver como generalizar variables a
+ # reemplazar en la template
+ out_file = file(path.join(self.config_dir, config_filename), 'w')
+ ctx = Context(out_file, rules=self.rules)
+ self.template.render_context(ctx)
+ out_file.close()
+
+if __name__ == '__main__':
+
+ import os
+
+ fw_handler = FirewallHandler()
+
+ def dump():
+ print '-' * 80
+ print 'Rules:'
+ print fw_handler.rule.show()
+ print '-' * 80
+
+ dump()
+
+ fw_handler.rule.add('input','drop','icmp')
+
+ fw_handler.rule.update(0, dst='192.168.0.188/32')
+
+ fw_handler.rule.add('output','accept', '192.168.1.0/24')
+
+ fw_handler.commit()
+
+ dump()
+
+ for f in (pickle_rules + pickle_ext, config_filename):
+ os.unlink(f)
+