From 0e293d049f3d4dac748a1630b8258ea10b709db2 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Sun, 30 Sep 2007 23:03:40 -0300 Subject: [PATCH] Implement firewall handler. --- TODO | 2 +- config.py | 4 + services/__init__.py | 1 + services/firewall/__init__.py | 337 ++++++++++++++++++++++++ services/firewall/templates/iptables.sh | 29 ++ 5 files changed, 372 insertions(+), 1 deletion(-) create mode 100644 services/firewall/__init__.py create mode 100644 services/firewall/templates/iptables.sh diff --git a/TODO b/TODO index 087639c..18d2325 100644 --- 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 diff --git a/config.py b/config.py index 2addc68..9b0932d 100644 --- 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 = \ diff --git a/services/__init__.py b/services/__init__.py index 02829a4..c9ec9b3 100644 --- a/services/__init__.py +++ b/services/__init__.py @@ -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 index 0000000..1ac1e60 --- /dev/null +++ b/services/firewall/__init__.py @@ -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 index 0000000..57dacd5 --- /dev/null +++ b/services/firewall/templates/iptables.sh @@ -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 : -- 2.43.0