#!/usr/bin/env python
#####################################################################
# $Id: firefly.py 60 2005-02-16 21:57:52Z  $
#
# Stefano Rivera's Firewall
# Version 1.0.11
# Copyright (C) 2003, 2004 Stefano Rivera <scripts "at" rivera.za.net>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; version 2 of the
# License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public
# License along with this program; if not, write to the Free
# Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
# MA 02111-1307 USA
#####################################################################

import copy
import cPickle
import fileinput
import getopt
import logging
import os
import os.path
import re
import string
import socket
import sys

# Logging:
logLevel = logging.INFO

# Version
__version__ = "1.0.11"

# Default Global Parameters
defaultParams = {
  'extif':'ppp0',
  'extnet':'0.0.0.0/0',
  'inbw':'56kbit',
  'intbw':'100mbit',
  'intif':'eth0',
  'ip':'/bin/ip',
  'iptables':'/sbin/iptables',
  'logdrops':False,
  'logrejects':False,
  'nat':False,
  'outbw':'48kbit',
  'sfqperturb':10,
  'sysctl':'/sbin/sysctl',
  'tc':'/sbin/tc',
  'uncleanrate':'15/minute',
}

# And some more, these are command-line only
defaultCmdLineParams = {
  'cache':'/var/cache/firefly.cache',
  'configfile':'/etc/firefly.cfg',
  'initialize':False,
  'reload':False,
  'showconfig':False,
  'testrun':False,
  'verbose':False,
}

# The Global Parameters overrideable by the command-line
overrideableParams = (
  'extif',
  'intif',
  'iptables',
  'ip',
  'sysctl',
  'tc',
)
# Default Section Parameters
sectParams = {
  'blockreservedips': False,
  'logunclean': False,
}

# Known protocols
protoTable = {
  'tcp':("-p tcp", "--source-port", "--destination-port"),
  'udp':("-p udp", "--source-port", "--destination-port"),
  'icmp':("-p icmp", "--icmp-type", None),
  'tcp-flags':("-p tcp", "--tcp-flags", None),
  'mac':("-m mac", "--mac-source", None),
  'mark':("-m mark", "--mark", None),
  'state':("-m state", "--state", None),
  'tos':("-m tos", "--tos", None),
  'ah':("-m ah", "--ahspi", None),
  'esp':("-m esp", "--espspi", None),
  'length':("-m length", "--length", None),
  'ttl':("-m ttl", "--ttl", None),
  'unclean':("-m unclean", "--unclean", None),
}

# Internet Reserved Networks
reservedIPs = (
  "10.0.0.0/8",
  "172.16.0.0/12",
  "192.168.0.0/16",
  "127.0.0.0/8",
  "192.0.2.0/24",
)

# Check if string is numeric
numericRE = re.compile("^\d+$")

#####################################################################
def version():
  "Print version & licence information"
  print("Firefly " + __version__ + """
Copyright (C) 2003,2004 Stefano Rivera

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
""")

#####################################################################
def usage():
  "Print usage information"
  print ("""Firefly, Stefano Rivera's firewall script
  Copyright (C) 2003  Stefano Rivera
  Read the source code or call --version for licence details

Usage:
  firefly [-hivrst] [long options]
  Options:
    -h    Print this help
    -i    Reload all the tables (Automatically done on first call after boot)
    -v    Be *very* verbose
    -r    Reload the configuration file (done automatically when modfied)
    -s    Show the configuration (but still configure)
    -t    Don't actually do anything
  Some useful long options:
    --configfile [file]  Use the specified configuration file
    --extif [iface]      Use the specified interface as the external interface
    --intif [iface]      Use the specified interface as the internal interface
    --version            Display version number and licence details

Report bugs to <scripts "at" rivera.za.net>
""")

#####################################################################
def parseOptions():
  "Parse the command line options"
  cmdParams = defaultCmdLineParams
  try:
    opts, extraOpts = getopt.getopt(sys.argv[1:],
      "c:hirstvV", [
        "cache=",
        "configfile=",
        "extif=",
        "help",
        "initialize",
        "intif=",
        "iptables=",
        "ip=",
        "tc=",
        "verbose",
        "reload",
        "showconfig",
        "sysctl",
        "testrun",
        "version",
      ])
  except getopt.GetoptError:
    usage()
    sys.exit(2)

  longform = {
    '-c':'--configfile',
    '-h':'--help',
    '-i':'--initialize',
    '-v':'--verbose',
    '-r':'--reload',
    '-s':'--showconfig',
    '-t':'--testrun',
    '-V':'--version',
  }
 
  for o, p in opts:
    # Convert all short forms to their long equivalent.
    if o[1] != "-":
      o = longform[o]
    # Some options have special needs, the rest just get dumped in 'params'
    if o == "--help":
      usage()
      sys.exit(0)
    elif o == "--version":
      version()
      sys.exit(0)
    elif o[:2] == "--":
      if p == "":
        cmdParams[o[2:]] = True
      else:
        cmdParams[o[2:]] = p

  return cmdParams

#####################################################################
#####################################################################
class FirewallData:
  "Root of the configuration tree"
  
  ###################################################################
  def __init__(self):
    self.sections = []
    self.params = {}
  
  ###################################################################
  def parseFile(self, file):
    "Parse the configuration file"
    log.debug("Parsing Config")
    
    # Read file
    commentRE = re.compile("(.*?)\\s*#")
    lines = []
    lineNo = 0
    for line in fileinput.input(file):
      lineNo += 1
      
      comment = commentRE.match(line)
      if comment != None:
        line = comment.group(1)
      
      line = string.strip(line)
      
      if len(line) == 0:
        continue
        
      lines.append((line, lineNo))
    
    # Parse sections
    sectionRE = re.compile("^\\[(.*)\\]$")
    currentSection = Section("default") # Hack to read top section
    passon = []
    foundSections = []
    for line, lineNo in lines:
      section = sectionRE.match(line)
      if section != None:
        # Parse the completed section
        currentSection.parse(passon)
        if currentSection.name == "default": # Hack continued
          self.params = currentSection.params
          # Command line can override certain options
          for opt in overrideableParams:
            if opt in cmdLineOptions:
              self.params[opt] = cmdLineOptions[opt]
          # End hack
        
        # Prepare for the next section
        name = string.lower(section.group(1))
        if name[:3] == "qos":
          currentSection = QOSSection(name)
        else:
          currentSection = Section(name)
        passon = []
        foundSections.append(name)
        self.sections.append(currentSection)
      else:
        passon.append((line, lineNo))
    currentSection.parse(passon)
    
    foundSections.sort()
    if foundSections != ["forward-in", "forward-out", "from-ext", "from-int", \
                         "qos-in", "qos-out", "to-ext", "to-int"]:
      log.error("Configuration file does not contain all 8 sections. "
                + "Found sections: %s, aborting!" % str(foundSections))
      sys.exit(2)

  ###################################################################
  def show(self):
    "Dump the configuration"
    print("Sections:")
    for section in self.sections:
      section.show()

  ###################################################################
  def build(self):
    "Build the firewall"
    # Flush the main tables and hold fort 'till the wall is built
    for chain in ("INPUT", "OUTPUT", "FORWARD"):
      iptables("-P %s DROP" % chain)
      iptables("-F %s" % chain)
    
    for chain in ("PREROUTING", "OUTPUT", "POSTROUTING"):
      iptables("-t nat -F %s" % chain)
    
    for chain in ("INPUT", "OUTPUT", "FORWARD", "PREROUTING", "POSTROUTING"):
      iptables("-t mangle -F %s" % chain)

    # Total (re-)build.
    if cmdLineOptions['initialize']:
      log.debug("Initializing tables")
      iptables ("-Z") # Reset counters
      
      # Flush em all!
      for table in ("filter", "nat", "mangle"):
        iptables("-t %s -F" % table)
        iptables("-t %s -X" % table)
      
      # My drop and reject tables (allow for easy logging)
      for chain, action in (("my-drop", "DROP"),("my-reject", "REJECT")):
        iptables("-N %s" % chain)
        iptables("-A %s -j %s" % (chain, action))
      
      # My Internet reserved IPs Table - drops anything mentioning them.
      iptables("-N internet-reserved-ips")
      for net in reservedIPs:
        iptables("-A internet-reserved-ips -s %s -j my-drop" % net)
        iptables("-A internet-reserved-ips -d %s -j my-drop" % net)
      
      # Locally origionating packets
      iptables("-t mangle -N qos-in-local")
      iptables("-t mangle -A qos-in-local -j CLASSIFY --set-class 1:2")
      iptables("-t mangle -A qos-in-local -j ACCEPT")

      # For rerouting chains
      iptables("-t nat -N reroutes")

      # Now for the tough bit
      for section in self.sections:
        section.build()

      # My magic table
      iptables("-N firefly")
    else:
      log.debug("Quick-loading tables")
    
    # PPP Needs to have it's QoS system rebuilt
    if cmdLineOptions['initialize'] or self.params['extif'][:3] == "ppp":
       # Flush QOS and prepare
      for iface, bw in (
        (self.params['intif'], self.params['inbw']),
        (self.params['extif'], self.params['outbw']),
      ):
        tc("qdisc del dev %s root" % iface, True)
        tc("qdisc add dev %s root handle 1: htb" % iface)
        tc("class add dev %s parent 1: classid 1:1 htb rate %s ceil %s" \
           % (iface, bw, bw))
        tc("filter add dev %s parent 1: protocol ip prio 2 u32 match u8 0 0 at 0 flowid 1:1" \
           % iface) # Hack - no default match...
      # Me -> int
      tc("class add dev %s parent 1: classid 1:2 htb rate %s" \
         % (self.params['intif'], self.params['intbw']))
      
      for section in self.sections:
        if section.name == "qos-out":
          section.build(True)
   
    # Now finish up & enable the firewall
    iptables("-A INPUT -i lo -j ACCEPT")
    iptables("-A INPUT -i %s -s %s -d %s -j from-int" \
      % (self.params['intif'], self.params['intnet'], self.params['intnet']))
    iptables("-A INPUT -i %s -s %s -d %s -j from-ext" \
      % (self.params['extif'], self.params['extnet'], self.params['extip']))
    iptables("-A INPUT -j my-drop")

    iptables("-A OUTPUT -o lo -j ACCEPT")
    iptables("-A OUTPUT -o %s -d %s -s %s -j to-int" \
      % (self.params['intif'], self.params['intnet'], self.params['intnet']))
    iptables("-A OUTPUT -o %s -d %s -s %s -j to-ext" \
      % (self.params['extif'], self.params['extnet'], self.params['extip']))
    iptables("-A OUTPUT -j my-reject")
    
    iptables("-t nat -A PREROUTING -j reroutes")
    
    # Forwarding and NAT
    if self.params['nat']:
      sysctl("net.ipv4.ip_forward", 1)
      
      iptables("-A FORWARD -i %s -o %s -j forward-in" \
               % (self.params['extif'], self.params['intif']))
      iptables("-A FORWARD -i %s -o %s -j forward-out" \
               % (self.params['intif'], self.params['extif']))
      iptables("-A FORWARD -j my-drop")
      iptables("-t nat -A POSTROUTING -s %s -o %s -j MASQUERADE" \
               % (self.params['intnet'], self.params['extif']))
    
    # QOS mangling
    iptables("-t mangle -A PREROUTING -i %s -j qos-out" \
             % self.params['intif']) # int-net -> ext-net
    iptables("-t mangle -A PREROUTING -i %s -j qos-in" \
             % self.params['extif']) # ext-net -> int-net
    iptables("-t mangle -A OUTPUT -d %s -j qos-in-local" \
             % self.params['intnet']) # me -> int-net
    iptables("-t mangle -A OUTPUT -j qos-out") # me -> ext-net

    # Now we can start logging, if enabled
    if self.params['logdrops']:
      iptables("-I my-drop 1 -j LOG --log-level debug --log-prefix " \
               + '"Dropped: "')
    if self.params['logrejects']:
      iptables("-I my-reject 1 -j LOG --log-level debug --log-prefix " \
               + '"Rejected: "')
    
    log.debug("Loaded")

#####################################################################
#####################################################################
class Section:
  "One section of the config file"
  chainRE = re.compile("^(Accept|Reject|Drop|DNAT)( (to|with) (.*))?" \
                       + "( source)?:$", re.IGNORECASE)
  
  def __init__(self, name):
    self.name = name
    
    if name == "default":
      self.params = defaultParams
    else:
      self.params = copy.deepcopy(sectParams)
    self.chains = []

  ###################################################################
  def parse(self, lines):
    "Parse the configuration file section"
    paramRE = re.compile("^(.*?)\\s*=\\s*(.*)$")
    currentChain = None
    passon = []
    for line, lineNo in lines:
      param = paramRE.match(line)
      if param != None:
        name = string.lower(param.group(1))
        value = param.group(2)
        self.parseParam(name, value, lineNo)
        continue
        
      chain = self.chainRE.match(line)
      
      # I.e. completed a chain or end of lines
      if chain != None and currentChain != None:
        currentChain.parse(passon)
        
      if chain != None:
        currentChain = self.prepareChain(chain, lineNo)
        self.chains.append(currentChain)
        passon = []
        continue

      passon.append((line, lineNo))
    if currentChain != None and passon != []:
      currentChain.parse(passon)
      
  ###################################################################
  def parseParam(self, name, value, lineNo):
    if string.lower(value) in ("true", "yes"):
      value = True
    elif string.lower(value) in ("false", "no"):
      value = False
    elif numericRE.match(value):
      value = int(value)
    elif name == "version":
      fileVersion = value.split(".")
      programVersion = __version__.split(".")
      if fileVersion[0] != programVersion[0] \
         or fileVersion[1] != programVersion[1]: # Major and minor match
        log.critical("[%d] Wrong version config file, aborting!" % lineNo)
        sys.exit(2)
    self.params[name] = value
    log.debug("[%d] %s: Param: %s = %s" % (lineNo, self.name, name, str(value)))
  
  ###################################################################
  def prepareChain(self, chain, lineNo):  
    action = string.upper(chain.group(1))
    parameter = chain.group(4)
    source = chain.group(5)
    if parameter != None and action not in ("REJECT", "DNAT"):
      log.error("[%d] Cannot use parameters with %s chains, skipping" \
                % (lineNo, action))
      return None
    if action == "DNAT":
      try:
        socket.gethostbyname(parameter)
      except socket.gaierror:
        log.warning(("[%s] Cannot resolve host %s, attempting to use it" \
                     + " anyway...") % (lineNo, parameter))
    if source != None:
      source = True
    else:
      source = False
    log.debug("[%d] %s: Chain: %s %s %s" \
              % (lineNo, self.name, action, parameter, source))
    return Chain(self, action, parameter, source)
  
  ###################################################################
  def show(self):
    "Dump the configuration"
    print(" %s:" % self.name)
    print("  Paramaters:")
    for name, value in self.params.items():
      print("   %-15s = %s" % (name, str(value)))
    print("  Chains:")
    for chain in self.chains:
      chain.show()
  
  ###################################################################
  def build(self):
    "Build the firewall"

    # Create my chain
    iptables("-N %s" % self.name)

    # Block Reserved IPs
    if self.params['blockreservedips']:
      iptables("-A %s -j internet-reserved-ips" % self.name)

    # Log unclean packets
    if self.params['logunclean']:
      iptables(("-A %s -m unclean -m limit --limit %s -j LOG --log-prefix " \
                + '"Unclean: "') % (self.name, config.params['uncleanrate']))

    for chain in self.chains:
      chain.build()

#####################################################################
#####################################################################
class QOSSection(Section):
  "A QOS Section"
  chainRE = re.compile('^Limit ((\d+)(kbps|mbps|kbit|mbit|bps)|\*) '
                       + 'Ceiling ((\d+)(kbps|mbps|kbit|mbit|bps)|\*) '
                       + 'Prio (\d+)( TOS (\S+))?( SFQ)?:$', re.IGNORECASE)
  
  def __init__(self, name):
    self.name = name
    
    if name == "qos-in":
      self.dev = config.params['intif']
    elif name == "qos-out":
      self.dev = config.params['extif']
    else:
      log.error("A QOS Section cannot have the name %s, aborting!" % name)
      sys.exit(2)
    
    self.params = copy.deepcopy(sectParams)
    self.chains = []

    QOSChain.nextHandle = 10

  ###################################################################
  def prepareChain(self, chain, lineNo):
    limit = chain.group(1)
    ceiling = chain.group(4)
    prio = int(chain.group(7))
    tos = chain.group(9)
    sfq = (chain.group(10) != None)
    if limit == "*":
      limit = config.params[self.name[4:] + "bw"] # get inBW or outBW...
    if ceiling == "*":
      ceiling = config.params[self.name[4:] + "bw"]
    log.debug("[%d] %s: QOS-Chain: %s %s %s %s %s" \
              % (lineNo, self.name, limit, ceiling, prio, tos, str(sfq)))
    return QOSChain(self, limit, ceiling, prio, tos, sfq)
  
  ###################################################################
  def build(self, pppInterfaceOnly = False):
    "Build the firewall"
    # Create my chain
    iptables("-t mangle -N %s" % self.name)

    for chain in self.chains:
      chain.build(pppInterfaceOnly)

#####################################################################
#####################################################################
class Chain:
  "Holds one IPTables chain"
  nextRerouteHandle = 1

  def __init__(self, parent, action, parameter, source):
    self.parent = parent
    self.action = action
    self.parameter = parameter
    self.source = source
    
    if action == "REROUTE":
      self.rerouteHandle = Chain.nextRerouteHandle
      Chain.nextRerouteHandle += 1
    
    self.rows = []
  
  ###################################################################
  def parse(self, lines):
    rowRE = re.compile("^(\\S+)/((\\d+:\\d+)|([^\\t\\n\\r\\f\\v:]+?))( src)?$")
    for line, lineNo in lines:
      row = rowRE.match(line)
      if row != None:
        proto = string.lower(row.group(1))
        ports = row.group(2)
        isRange = (row.group(3) != None)
        source = (row.group(5) != None)
        if proto not in protoTable.keys() and proto != "*":
          log.warning("[%d] %s: Unknown protocol, skipping..." \
                      % (lineNo, proto))
          continue
        if isRange and proto not in ("tcp", "udp"):
          log.error("[%d] %s/%s: Ranges cannot be defined for %s, skipping..." \
                    % (lineNo, proto, ports, proto))
          continue
        # Try a port sanity check:
        if proto in ("tcp", "udp"):
          if numericRE.match(ports) == None and not isRange and ports != "*":
            try:
              socket.getservbyname(ports, proto)
            except socket.error:
              log.warning("[%d] %s/%s: Port unknown, skipping..." \
                          % (lineNo, proto, ports))
              continue
        
        self.rows.append(Row(proto, ports, source))
        log.debug("[%d] %s: Row: %s/%s %s" % (lineNo, self.parent.name, proto, ports, str(source)))
  
  ###################################################################
  def show(self):
    "Dump the configuration"
    print("    %s %s %s:" % (self.action, self.parameter, str(self.source)))
    for row in self.rows:
      row.show()
  
  ###################################################################
  def build(self):
    "Build the chain"
    
    # If it's a rerouting chain, set up the destination chain
    if self.action == "DNAT":
      parameter = socket.gethostbyname(self.parameter)
      
      self.rerouteHandle = self.nextRerouteHandle
      self.nextRerouteHandle += 1
      
      iptables("-t nat -N reroute-%d" % self.rerouteHandle)
      iptables("-t nat -A reroute-%d -j DNAT --to-destination %s" \
               % (self.rerouteHandle, parameter))
      iptables("-t nat -A reroute-%d -j ACCEPT" % self.rerouteHandle)

    for row in self.rows:
      if self.parameter == None:
        if self.action in ("REJECT", "DROP"):
          action = "my-" + string.lower(self.action)
        else:
          action = self.action
        iptables("-A %s %s -j %s" \
                 % (self.parent.name, row.iptablesSyntax(), action))
      elif self.action == "REJECT":
        iptables("-A %s %s -j REJECT --reject-with %s" \
                 % (self.parent.name, row.iptablesSyntax(), self.parameter))
      elif self.action == "DNAT":
        iptables("-t nat -A reroutes %s -j reroute-%d" \
                 % (row.iptablesSyntax(), self.rerouteHandle))
        iptables("-A %s %s -j ACCEPT" \
                 % (self.parent.name, row.iptablesSyntax()))

#####################################################################
#####################################################################
class QOSChain(Chain):
  "Holds one QOS chain"
  nextHandle = 10
  
  def __init__(self, parent, limit, ceiling, prio, tos, sfq):
    self.parent = parent
    self.limit = limit
    self.ceiling = ceiling
    self.prio = prio
    self.tos = tos
    self.sfq = sfq

    self.handle = QOSChain.nextHandle
    QOSChain.nextHandle += 1
    self.rows = []
  
  ###################################################################
  def show(self):
    "Dump the configuration"
    print("    %s %s %s %s %s %s:" % (self.handle, self.limit, self.ceiling, \
                                      self.prio, self.tos, str(self.sfq)))
    for row in self.rows:
      row.show()
  
  ###################################################################
  def build(self, pppInterfaceOnly):
    "Build the chain"
    tc("class add dev %s parent 1:1 classid 1:%d htb rate %s ceil %s prio %d" \
       % (self.parent.dev, self.handle, self.limit, self.ceiling, self.prio))
    if self.sfq:
      tc("qdisc add dev %s parent 1:%d handle %d: sfq perturb %d" \
         % (self.parent.dev, self.handle, self.handle, \
            config.params['sfqperturb']))
    else:
      tc("qdisc add dev %s parent 1:%d handle %d" % \
         (self.parent.dev, self.handle, self.handle))
    
    if pppInterfaceOnly:
      return
    
    subchain = "%s-%d" % (self.parent.name, self.handle)
    iptables("-t mangle -N %s" % subchain)
    if self.tos != None:
      iptables("-t mangle -A %s -j TOS --set-tos %s" % (subchain, self.tos))
    iptables("-t mangle -A %s -j CLASSIFY --set-class 1:%d" \
             % (subchain, self.handle))
    iptables("-t mangle -A %s -j ACCEPT" % subchain)
    for row in self.rows:
      iptables("-t mangle -A %s %s -j %s" \
               % (self.parent.name, row.iptablesSyntax(), subchain))

#####################################################################
#####################################################################
class Row:
  "Holds one row"

  def __init__(self, proto, ports, source):
    self.proto = proto
    self.ports = ports
    self.source = source
  
  #####################################################################
  def iptablesSyntax(self):
    "Return the iptable options to match the specified protocol and port"
    if self.proto in protoTable.keys():
      if self.ports == "*":
        return protoTable[self.proto][0]
      else:
        if (not self.source) and protoTable[self.proto][2] != None:
          arg3 = protoTable[self.proto][2]
        else:
          arg3 = protoTable[self.proto][1]
        return ("%s %s %s" \
                % (protoTable[self.proto][0], arg3, self.ports))
    elif self.proto == "*" and self.ports == "*":
      return ("")
    else:
      log.warning("Unkwnown protocol, guessing command line: " + self.proto)
      return ("-m %s --%s %s" % (self.proto, self.proto, self.ports))
  
  ###################################################################
  def show(self):
    "Dump the configuration"
    print("     %s/%s %s:" % (self.proto, self.ports, str(self.source)))

#####################################################################
def setupLogging():
  "Return a basic logger, set to the level logLevel"
  myLogger = logging.getLogger()
  logging.basicConfig()
  myLogger.setLevel(logLevel)
  return myLogger

#####################################################################
def findIP(iface):
  "Find the IP address and the network for the given interface"
  f = os.popen("%s addr show %s " % (config.params['ip'], iface))
  raw = f.readlines()
  f.close()
  
  if raw == []:
    if iface[:3] != "ppp": # PPP interfaces arn't there when the line is down
      log.critical("Cannot resolve IP address for interface %s, aborting..." \
                   % iface)
      sys.exit(2)
    else:
      return ("127.254.254.254", "127.254.254.254/32")
  
  ifconfigRE = re.compile("inet ((\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+))(/\\d+)? ")
  match = ifconfigRE.search(str(raw)) # He He - Hack
  ip = match.group(1)
  mask = match.group(6)
  if mask == None:
    network = ip + "/32"
  else:
    network = ip + mask
  return (ip, network)

#####################################################################
def findIPs():
  "Find all the IP addresses"
  config.params['extip'] = findIP(config.params['extif'])[0]
  config.params['intip'], config.params['intnet'] \
    = findIP(config.params['intif'])

#####################################################################
def iptables(cmdline, quiet=False):
  "Call iptables with the specified command line, blocking"
  log.debug("IPTABLES: " + cmdline)
  if quiet:
    cmdline += " > /dev/null 2> /dev/null"
  if not cmdLineOptions['testrun']:
    # Shift to get the return value
    return os.system("%s %s" % (config.params['iptables'], cmdline)) >> 8

#####################################################################
def tc(cmdline, quiet=False):
  "Call tc with the specified command line, blocking"
  log.debug("TC: " + cmdline)
  if quiet:
    cmdline += " > /dev/null 2> /dev/null"
  if not cmdLineOptions['testrun']:
    # Shift to get the return value
    return os.system("%s %s" % (config.params['tc'], cmdline)) >> 8

#####################################################################
def sysctl(file, value, quiet=False):
  "Call sysctl with the specified file and value"
  cmdline = "-w %s=%s >/dev/null" % (file, str(value))
  log.debug("SYSCTL: %s" % cmdline)
  if quiet:
    cmdline += " 2>/dev/null"
  if not cmdLineOptions['testrun']:
    # Shift to get the return value
    return os.system("%s %s" % (config.params['sysctl'], cmdline)) >> 8

#####################################################################
def readConfigFile():
  "Read the config from a file, and cache"
  global config
  config = FirewallData()
  config.parseFile(cmdLineOptions['configfile'])
  
  cache = open(cmdLineOptions['cache'], "w")
  cPickle.dump(config, cache, cPickle.HIGHEST_PROTOCOL)
  cache.close()

#####################################################################
def readConfigCache():
  "Read the config from the cache"
  global config
  
  if not os.path.isfile(cmdLineOptions['configfile']):
    log.critical("Config file missing: %s, aborting!\n" \
                 % cmdLineOptions['configfile'])
    sys.exit(2)
  
  if cmdLineOptions['reload'] or not os.path.isfile(cmdLineOptions['cache']) \
     or os.path.getmtime(cmdLineOptions['configfile']) \
        > os.path.getmtime(cmdLineOptions['cache']):
    readConfigFile()
    cmdLineOptions['initialize'] = True
  else:
    cache = open(cmdLineOptions['cache'], "r")
    config = cPickle.load(cache)
    cache.close()
    
    if config.params['version'] != __version__:
      readConfigFile()

#####################################################################
def main():
  "Called from command line"
  global log
  global cmdLineOptions

  log = setupLogging()
  
  cmdLineOptions = parseOptions()
  
  if cmdLineOptions['verbose']:
    log.setLevel(logging.DEBUG)

  if not os.path.isfile(cmdLineOptions['configfile']):
    log.critical("Config file missing: %s, aborting!\n" \
                 % cmdLineOptions['configfile'])
    sys.exit(2)
    
  readConfigCache()

  # Test for our magic table
  if iptables("-L firefly", True) == 1:
    cmdLineOptions['initialize'] = True

  findIPs()

  if cmdLineOptions['showconfig']:
    config.show()

  config.build()

#####################################################################
#####################################################################
#####################################################################
if __name__ == '__main__':
  main()
