]> git.llucax.com Git - software/pymin.git/commitdiff
Implement firewall handler.
authorLeandro Lucarella <llucax@gmail.com>
Mon, 1 Oct 2007 02:03:40 +0000 (23:03 -0300)
committerLeandro Lucarella <llucax@gmail.com>
Mon, 1 Oct 2007 02:03:40 +0000 (23:03 -0300)
TODO
config.py
services/__init__.py
services/firewall/__init__.py [new file with mode: 0644]
services/firewall/templates/iptables.sh [new file with mode: 0644]

diff --git a/TODO b/TODO
index 087639c956d3f716c88b46c3aed53cdbe3134b96..18d2325e3265bb690e54be4f07b9a46b2def81da 100644 (file)
--- a/TODO
+++ b/TODO
@@ -16,7 +16,7 @@ Ideas / TODO:
 
 * Agregar logging.
 
-* Agregar validación.
+* Agregar validación con formencode.
 
 Estas cosas quedan sujetas a necesitada y a definición del protocolo.
 Para mí lo ideal es que el protocolo de red sea igual que la consola del
index 2addc6823caeee0252b641511d616dd24e30cf08..9b0932da5db699fd8e60418196bb176231c690b8 100644 (file)
--- a/config.py
+++ b/config.py
@@ -15,6 +15,10 @@ routes = dict \
         pickle_dir = 'var/lib/pymin/pickle/dhcp',
         config_dir = 'var/lib/pymin/config/dhcp',
     ),
+    firewall = FirewallHandler(
+        pickle_dir = 'var/lib/pymin/pickle/firewall',
+        config_dir = 'var/lib/pymin/config/firewall',
+    ),
 )
 
 bind_addr = \
index 02829a438961d0af4fe563988ab09d66df301ff8..c9ec9b35df321e518cc4056abc2546a096273c23 100644 (file)
@@ -1,4 +1,5 @@
 # vim: set encoding=utf-8 et sw=4 sts=4 :
 
 from services.dhcp import DhcpHandler
+from services.firewall import FirewallHandler
 
diff --git a/services/firewall/__init__.py b/services/firewall/__init__.py
new file mode 100644 (file)
index 0000000..1ac1e60
--- /dev/null
@@ -0,0 +1,337 @@
+# 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)
+
diff --git a/services/firewall/templates/iptables.sh b/services/firewall/templates/iptables.sh
new file mode 100644 (file)
index 0000000..57dacd5
--- /dev/null
@@ -0,0 +1,29 @@
+#!/bin/sh
+
+<%!
+
+# TODO escape shell commands more securely
+def s(text):
+    return repr(text.encode('utf-8'))
+
+def optional(switch, value):
+    if value is not None:
+        return '%s %s' % (switch, s(value))
+    return ''
+
+%>
+
+% for (index, rule) in enumerate(rules):
+/sbin/iptables -t filter \
+    -I ${rule.chain|s} ${index+1|s} \
+    -j ${rule.target|s} \
+    ${optional('-s', rule.src)} \
+    ${optional('-d', rule.dst)} \
+    ${optional('-p', rule.protocol)} \
+    ${optional('-m', rule.protocol)} \
+    ${optional('--sport', rule.src_port)} \
+    ${optional('--dport', rule.dst_port)}
+
+%endfor
+
+<%doc> vim: set filetype=python sw=4 sts=4 et : </%doc>