#! /usr/bin/env python
# -*- mode:python -*-
# $Id: keeper_example.py 12429 2008-10-04 03:24:02Z vax $

"""
Sample script for using dfd_keeper.

TODO: Add knowledge of internal/external IPs and modify rules to check errors.
"""

# Parse command-line options.
import getopt, sys

import textwrap

from dfd_keeper import *

state_file="keeper_example_state"

usage = "Usage: %s [--daemon] [--file=statefile] [--help] [--lan ifc] [--nosyslog] [--port NNNN] [--syntax] [--test] [--wan ifc] [--zap]" \
        % sys.argv[0]
try:
    opts, args = getopt.getopt(sys.argv[1:], "df:hl:np:stw:z",
                               [ "daemon", "file", "help", "lan=", "nosyslog", "port=", "syntax", "test", "wan=", "zap" ])
except getopt.GetoptError:
    raise SystemExit, usage
mode = "normal"
daemon=False
syslog=True
port=8007
zap_states=False

p = set_persist_hooks(state_file)
# NOTE: These are right for me, but probably not you.
p["wan_if"] = "sis0"
p["lan_if"] = "re0"
p["bt_client"] = ""
p["block"] = ""
for (o, a) in opts:
    if o in ("-d", "--daemon"):
        daemon = True
    if o in ("-f", "--file"):
        state_file = a
    if o in ("-h", "--help"):
        print usage
        raise SystemExit
    if o in ("-s", "--syntax"):
        # Don't actually modify any rules, just syntax check
        mode = "syntax"
    if o in ("-t", "--test"):
        # Don't actually invoke pfctl
        mode = "test"
    if o in ("-p", "--port"):
        port = int(a)
    if o in ("-w", "--wan"):
        p["wan_if"] = a
    if o in ("-l", "--lan"):
        p["lan_if"] = a
    if o in ("-n", "--nosyslog"):
        syslog = False
    if o in ("-z", "--zap"):
        zap_states = True


# No saved packet_filter object, create one.
rs = ruleset(namespace=p)
nat = rs["translation"]
# NOTE: This assumes your WAN interface could be DHCP.
# If it isn't, you don't need the parens around $wan_if.
nat.make_rule("nat on $wan_if from $lan_if:network to any -> ($wan_if)")
# As an example of how to do p2p behind NAT, set up the ability to
# port forward back to a client.  See the dfd_sniff program for how to
# automatically trigger this rule.
nat.make_rule("""
rdr pass on $wan_if proto { udp tcp } from any to ($wan_if) port { 6881:6890 } -> $bt_client
""")
# Retrieve the filter table.
f = rs["filter"]
f.make_rule("""
# Allow all loopback traffic.
pass quick on lo0

# Default deny.
block all

# Allow LAN to bomb out quickly.
block return on $lan_if

# Don't allow other networks to impersonate LAN.
antispoof quick for $lan_if

# Block leakage of LAN stuff to anywhere else.
block out log quick on ! $lan_if to $lan_if:network
""")
f.make_rule("""
# Block hosts we have specified in both directions.
block in log quick on $wan_if from $block to any
block out log quick on $wan_if from any to $block
""")
f.make_rule("""
# Allow firewall to talk to LAN.
pass out quick on $lan_if from $lan_if to $lan_if:network keep state

# Allow anything in from LAN that isn't destined to the LAN.
pass in quick on $lan_if to ! $lan_if:network keep state allow-opts

# Allow LAN hosts to SSH into this box.
pass in quick on $lan_if proto tcp from any to $lan_if port ssh flags S/SA
""")
f.make_rule("""
# Allow connections out WAN, and randomize SEQ #s.
pass out quick on $wan_if all modulate state allow-opts
""", tag="wan")
packet_filter = pf(rs, p, mode, syslog)

# Force sync so kernel and packet filter agree on rules
packet_filter.force_sync()

if zap_states:
    # Flush all states in case they aren't allowed any more
    packet_filter.flush()

if daemon:
    daemonize()

# This code illustrates how you might use this module in a program.
# It listens to a port and accepts CRLF-terminated commands.
# Actually the CR is optional, for compatibility with netcat.

class command_dispatch(helper_functions):
    """
    This class defines a command suite for pf.

    When initializing it, you pass it a configured set of rules.
    By defining commands of the form cmd_whatever, you make that
    command available to anyone to use via tcp_command_taker.

    Implementor note: to print to the client, just return a string!
    """

    # This is what all the user-accessible commands begin with.
    prefix="cmd_"

    def __init__(self, packet_filter):
        self.pf = packet_filter

        # Create the block list command.
        block_help = """\
        block [add|del] host
        Block an IP from sending in data via WAN interface either direction.
        XXX: Assumes it is on the remote side of that interface.
        """
        self.list_factory("block", p["block"],"add",
                          textwrap.dedent(block_help).strip())

        # Here is how you specify a custom help message for the command.
        help="Switches on/off connectivity with the Internet.\n" + \
              "For emergencies only!\n"
        self.switch_factory("wan", help=help, flushing=True)
        # In addition to toggling active flags on wan-tagged rules,
        # you could also just set p["wan_if"] = "" and that would
        # disable any rule with $wan_if in it.

        # Pull in stuff from helper_functions.
        for c in (helper_functions.end_user):
            setattr(self.__class__, self.prefix + c,
                    getattr(helper_functions, c))

    # The rest of the user-accessible commands follow.

    def cmd_drop_state(self, src, dst=None):
        """Drop a particular state table entry.  Takes src and optional dst."""
        src = str(ipv4.address(src))
        if dst == None:
            self.pf.flush(src)
        else:
            self.pf.flush(src, str(ipv4.address(dst)))

    def cmd_bittorrent(self, host=""):
        """Specify the bittorrent client, or nothing to turn off forwarding."""
        t = self.pf.namespace["bt_client"]
        if t != "": self.pf.flush(t)
        self.pf.namespace["bt_client"] = host

def command_factory(p=packet_filter):

    # Create a command module to use for executing commands.
    command_module = command_dispatch(p)

    # Set this up so that it syncs the rules every time you enter a command.
    synch_cmd = sync_proxy(command_module, command_module._sync)

    return synch_cmd

# NOTE: This binds to the localhost interface only.
# Do not change this unless you really know what you are doing.  If you
# know what you're doing, remove the last argument and it will bind to
# the wildcard address.

reactor.listenTCP(port, tcp_factory(command_factory, tcp_command_taker),
                  interface='localhost')

# Start running.
reactor.run()

