Design of the Bridge Keeper (dfd_keeper)
1 Metadata
1.1 Copyright and Distribution Control
The latest version is always here:
This is a copyrighted work, with some rights reserved. This work is licensed under the
Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 United States License (
http://creativecommons.org/licenses/by-nc-nd/3.0/us/). This means you may redistribute it for non-commercial purposes, and that you must attribute me properly (without suggesting I endorse your work).
1.2 What is a Dynamic Firewall Daemon?
The dynamic firewall daemon (DFD) is a concept, whereas dfd_keeper is an implementation of that concept, using python, and working with the pf firewall (targeting OpenBSD specifically, but also probably most other BSDs).
1.3 Where to Download
1.4 Terminology
2 Overview
The dynamic firewall daemon manages your firewall rules, providing a cross-platform text-based API so that clients (you, or an automated script) can all connect to it and request changes. Your python script defines what sort of changes are allowed. It is, essentially, firewall middleware, and frees clients from having to know anything about the firewall’s configuration.
The basic usage of this is that you learn enough python to write a script which defines three things:
rules meaning firewall rules, as python strings
variables get expanded by DFD (and they are not python nor pf.conf variables)
commands are things that users type, as well as the python code which executes them
This is made easy by importing from the dfd_keeper module. In fact, there already is an example script called keeper_example.py that gives you a functional firewall, in many situations.
Your script decides to listen on a TCP port, and goes into the main loop of execution. Clients may then connect to it and issue text-based commands which you have defined, and those commands may affect the variables you have defined, and those variable changes affect how rules expand. Connecting to it is possible through telnet or better yet, netcat, since it speaks both EOL conventions.
The commands you have defined must conform to a specific pattern, but you don’t even have to put them in a dispatch table because dfd_keeper finds them and extracts the help message through introspection.
The most important thing to know about dfd_keeper’s implementation is that it is written in python and uses the Twisted module to support asynchronous I/O. This enables it to handle multiple clients at once without worrying about threads or multiple processes. However, this means that instead of coding control-flow constructs, you instead write code to handle events, and the main control loop is nearly invisible to you. One of the consequences of this is that when it does a blocking call, for example when it calls pfctl to load a new rule set, the main loop waits on it. This can be a while if pfctl has to look up DNS name entries, but this is a relatively small price to pay for not having the concerns about thread deadlock, race conditions, mutexes, and semaphores.
The second most important thing to understand is that dfd_keeper is not a program, but rather a library or framework for creating programs that control your firewall. There is an example, keeper_example.py which shows how you do that. Basically, you set some things up, define some commands, and then start the main event loop (called the reactor) which won’t exit until you stop the program via its command interface.
Once the main event loop is running, you can connect to DFD by using netcat (nc) or telnet. The server is listening on a port, and you telnet there and issue text-based commands to interact with the firewall.
2.1 Features
-
Be able to quickly switch between multiple firewall configurations
-
Don’t need to remember ordering of rules for pf
-
Easily generate the contents of a table (e.g. bogon list at http://www.cymru.com/Bogons/)
-
Rules which self-destruct at a certain time
-
Rules which may be turned on or off in groups
-
Rules which don’t render into the ruleset until they are needed (the sophisticated way of commenting them out)
2.2 Sample Transcript
2.3 How Do I Use It?
The distro has an INSTALL file but basically you need to install these packages. Fortunately, they exist in package and ports form on the latest OpenBSD.
-
Zope
-
py-xml
-
py-zopeinterface
-
py-twisted-core
Download the source (see
1.3↑), unpack it properly, go in the dfd_keeper directory, and run this as root:
python ./setup.py install
Create your own keeper script
You can start with the keeper_example.py in the distribution.
Run it in test mode first
./keeper_example.py --test &
nc -v -v localhost 8007
Start with “help”.
Modify the script to do what you want
You’ll have to be able to understand python, but it’s very easy to learn.
Run it in test and syntax mode to double-check it does what you want
./keeper_example.py --syntax
Run it from /etc/rc.local
cp keeper_example.py /etc/keeper.py
if [ -x /etc/keeper.py ]; then /etc/keeper.py >> /var/log/keeper 2>&1 &; fi
3 Gory Details
3.1 Rendering
Basically, rendering means converting the python objects in memory into firewall rules. This is not a simple stringification, because rules may be invalid (not to be rendered), or they may refer to variables which are not defined, or they may need to be removed because they have expired (timed out).
Essentially, rendering starts at the top of the container hierarchy, and each level asks the lower levels to render (if they are valid) and then concatenates the results in a list. Thus, the rule containers start as a tree, but end up with a linear list of rules. At the very bottom level, the rules themselves have substitutions performed on them and end up as strings.
The rule containers (i.e. everything but the bottom of the hierarchy) must inherit apply_mixin and render_mixin, whereas only the bottommost layer inherits from rule_base.
3.2 Variables
During rendering, DFD does some variable substitution; any variable (denoted with a dollar sign prefixed to its name) is expanded to its value within a certain namespace. Generally you define this namespace inside your script, and then set it up for reference prior to entering the main loop. This process is a bit klugey, but it works.
Variables are considered dynamic state and are saved between invocation, whereas rules are defined by the script and are not saved. If the list of variables used by the rules varies from the list used in the prior invocation, the prior values are not restored.
4 Rule Classes
4.1 named_list
This is a structure I created which is a cross between a dictionary and a list. You can think of it as a dictionary which remembers the order you added things to it, or two lists which allow lookup by key in the key list into the corresponding value on the value list. It is internally implemented as two lists.
The importance of this is that I wanted people to be able to add rules to the various sections of the ruleset without having to worry about the order that pf enforces. By creating entries named after the sections of the pf.conf, I was able to let people add to any of them, and yet serialize all the sections in the right order before providing it to pf.
4.2 apply_mixin
When all rule containers inherit this, it makes it possible to do something to every contained rule. This is accomplished as a depth-first traversal, and the method which it finally calls also has a list of ancestors containers, which allows it to do things like remove itself from its parent easily.
4.3 render_mixin
It has a method for finding out if it is valid to participate in rendering. It also has a render method that will render all enclosed rules in this class in a linear order; to accomplish this, it uses a helper function called flatten. Basically, a list of lists will flatten to a single list. Rendering is done recursively and during rendering it flattens at every level, so no matter how many levels deep, the result which bubbles up to the top will be a list of rules (the way this is encoded is a bit weird, and could be simpler). Render also has the important function of expanding all variables it sees with their values from some namespace. This allows you to modify DFD variables and have those changes appear in rules. It uses the traditional way of marking a variable; it prefixes it with a dollar sign.
This also removes sub-items which return a true response to expired method call. It also makes the default to return False.
4.4 rule_container
This basically inherits from apply_mixin and render_mixin, and serves as something other rule containers may derive from.
4.5 ruleset
This is the class for the top-level ruleset used by pf. It inherits from rule_container (of course) and has seven rule_holders named after the seven parts of a pf.conf.
It is an enumerated list, with sections familiar to any pf user:
-
macros
-
tables
-
options
-
scrub
-
queueing
-
translation
-
filter
4.6 rule_holder
A generic container for holding rules, inherits from rule_container.
This is derived from a list but it defines a method called make_rule which allows you to create a rule inside of this container. It also defines a __str__ which will convert all the contained rules into a string representation.
This corresponds to one particular rule. It takes a string as input, and if you call str() on it, it will return that string. It also defines __repr__ for saving state, and a render method which will return the variable-substituted rule according to some namespace that is passed in, assuming it is valid. If it is not valid, or if the variable expansion used an empty value, it will return an empty list instead of a rule. It inherits from rule_base (see
↓)
It allows for rule expiration semantics, so that rules can “die” at a certain time. This can be done at an absolute time in seconds since the Epoch (“expires”), or as a number of seconds from now (“lifetime”). The lifetime keyword is is so that client scripts need not know the current time in order to create a rule that expires a fixed time in the future.
It also lets the runtime know how long until it expires, so the runtime event loop can schedule a pass to expire it. It also has a method called apply that allows you to call a function on a rule and all of its ancestors. It also defines a method called valid that tells us if the rule is active and has not expired. You see, rules can be marked inactive, and they will not render until they become active. This is usually done when you want to turn on or off a group of rules in a programmatic way.
4.8 template
This was inspired by string.Template but it lets us expand rules according to some namespace via the substitute.
4.9 rule_queue
This is a variant of rule_series that will only hold a certain number of rules. Once you get the maximum number, adding a single rule will get rid of the oldest rule in a first-in first-out policy.
5 Run-Time Support Classes
5.1 syslogger
logs to syslog
5.2 outlogger
logs to stdout
5.3 logger
Implements a singleton class for logging, points to either syslogger or outlogger.
6 PF Classes
6.1 ParameterError
Raised when you didn’t specify a parameter properly
6.2 FlushError
Raised when pf rejects a state flush
This is the largest class in the system and mainly deals with interacting with pf.
This python object represents the pf firewall. Generally, you instantiate it, and then tell it to synchronize, and it will then provide all the firewall rules to pfctl via stdin.
When you initialize the pf object (there is no need for more than one), you may pass it a namespace (dictionary) in which to look up DFD variables.
There is a chance to control three logging variables.
You may also put it in one of three modes.
daemon means it will daemonize itself and then behave as in normal mode
normal means it will affect the pf rules on this system
syntax means it will not affect the pf rules, but instead will use pfctl -n to syntax-check your rules
test means that it isn’t going to talk to pf at all; this is handy for testing things out on machines that don’t have the pf firewall.
This represents a pf.conf macro, and can be expanded programmatically.
This represents a pf.conf table, and its string representation puts angle brackets around its name. When defined, it gets a unique name based on a class-level counter.
6.6 setlist
This is a helper class where you can add something to this list, but not if it’s already there. Basically this is a cross between a set and a list.
6.7 pflist
This derives from setlist and renders to the “{ blah1 blah2}” syntax that pf expects from lists.
6.8 anchor
This represents an anchor object in the pf.conf syntax. You can tell it what type it is, such as rdr, nat, or binat.
This setlist represents something which can expand to a simple string, or a pf.conf list of items, depending on how many there are. It was needed back when enclosing a single value in braces wasn’t legal pf.conf syntax, and that came in handy when you wanted an object that could hold one or more than one value in the rule. To put it another way, it adds the curly braces around itself if it has more than one object. So this basically is magic because it behaves as either macro (for one value) or pflist (for two values). This was very important when the original pf.conf syntax wouldn’t accept braces around a single object.
7 Twisted Classes
7.1 tcp_factory
This factory from the Twisted framework creates pairs of objects, takers and commanders, which represent the connection from a user and the commands that user may enter. These objects have to be linked together to get stuff done.
This class is tailored for tcp command takers and allows you to easily specify a commander and protocol to instantiate. Optionally you may specify a banner string, responses for valid and invalid commands, and the prompt you wish to display. If you wish to disable any of these just make them an empty string. You may also configure the name of the quit command (or None if you don’t wish to have one).
7.2 pfctl_process
This is a module that allows the Twisted engine to interact with the pfctl command.
7.3 line_receiver
This class takes care of processing lines of input to or from clients. It assumes all incoming strings end with LF or CR-LF. It strips the carriage return so that people may use telnet, which has that end-of-line convention. It also adds carriage returns in the right place on outbound traffic so that telnet accepts it. Fortunately netcat will ignore them. You have to derive from this and implement line_received to do something with it, since this is an abstract base class.
This class inherits from twisted.protocols.basic.LineOnlyReceiver.
7.4 stateful_queue
This class appears to allow you to enqueue a number of method calls using append or extend method calls. Each of these triggers the queue to start processing the commands, as long as it is not paused. There are pause and resume commands which handle that.
7.5 asynchronous_command
This is returned by a command to indicate that it will send back output asynchronously, so go ahead and return to the client and await notification.
7.6 tcp_command_taker
This Twisted framework object receives method calls when connections are made, have sent a line of text, It is also responsible for making sure that the user-entered command is not zero-length, and that the exists and has the proper number of arguments. It also handles a few commands inline, such as quitting a connection or stopping the “reactor” altogether. It makes use (by composition) of the stateful_queue class described above. Every time a line comes in, it is broken up by semicolons, and then extended onto the stateful_queue for processing. When a command throws an asynchronous_command exception, it prints the output it has so far and pauses the queue for the async event to restart it
This class inherits from line_receiver.
This class is instantiated once by tcp_factory for every incoming connection as the protocol object.
7.7 chronos
When you construct this class and pass it an instance of the pf class, it expires any already-expired rules and schedules callbacks to delete any rules due to expire in the future.
7.8 ipv4.address
This represents a dotted quad
7.9 ipv4.range
This represents a range of addresses using an address and mask (CIDR style)
7.10 sync_proxy
This is a proxy between two pieces of code; here it is being used between pf and pfctl_process. I wrote this for sites which might have a great deal of things going on, such that calling pfctl that often would make it a bottleneck. The sync_proxy can count the number of sync requests, and only send one every sync_every times, where sync_every is given to the constructor and defaults to one.
7.11 helper_functions
This class exists to implement things to ease the difficulty of writing scripts. For example, it handles taking the user’s input and dispatching to a command that you have written.
-
It enables some basic commands (such as getting help or printing the version string)
-
Its toggle_factory method makes commands that toggle rules (with a given tag) on or off. Note that this means you can’t set the state to be on, you only get to toggle its current state.
-
Its switch_factory method makes commands that turn rules on or off, because most of the time you want to set the state, not flip it.
-
Its list_factory method makes commands that add or delete things to list variables which are then used in rules
-
Its show command displays all the rules loaded in the pf object
-
Its number command does the same with line numbers (pfctl reports syntax errors with line numbers)
-
Its variables command lists all the variables used by your script
-
Its sync command synchronizes the pf object’s rules with the pf firewall via pfctl
-
Its flush command flushes the pf firewall’s state table
8 Other
There are a few more methods related to daemonizing, handling interrupts and saving data (variables, actually) persistently.
9 TODO
-
compare python, perl, and Twisted IPv4 classes to mine and steal ideas
-
Determine if there is any common code between dfd_tbk and dfd_keeper than can be refactored and extracted into its own class.
-
It would be nice to be able to negate the value of lists, so that if the list were empty, it expands to any, whereas now it merely aborts the rule
-
Consider examining the template/variable distinction in web templating systems to see if we get any ideas about how to model rules and variables
9.1 Improve Security
-
address-based access control
-
consider some kind of application-layer encryption
9.2 Ease of Use
-
Create a command-line option that sync’s every N seconds. This is to re-lookup dynamic DNS names periodically.
-
consider daemonizing unless a certain flag is passed in rather than requiring a flag
-
add help for stop and quit commands
-
implement save state command
-
add a least-recently-used rule container that detects duplicates
-
sanity check that names are valid domain names or IPs
-
add commands to determine how long the keeper has been running
-
Implement threshhold commands; n times in t seconds triggers an action.
9.3 Careful coding
-
create test harness for logging
-
Make sure we close every file we open, even if an exception is raised. The try...finally is a convenient way to do this
-
Carefully check return codes of pfctl for errors, figure out how to propogate to user
-
figure out how to detect a failed sync
-
make sure we support SIGTERM properly
-
We run one pfctl process before everything else, including the reactor. This is bad because SIGCHLD isn’t set yet, and so it won’t be reaped and will become a zombie. Fix that.
-
cleaner variable substitution algorithm
-
figure out why rules are being printed to stdout when not in daemon mode
-
return standard output and error of pfctl command
9.4 Improved Parsing
-
allow for spaces in command line arguments (more complex parsing, like sh)
-
allow user to send multiple commands on one line (more complex parsing, like sh)
9.5 Impractical TODO Items
-
I had considered adding readline support, but it appears to be incompatible with the line-based protocol classes and the twisted framework in general. The python readline classes are meant primarily for use with the interactive command prompt. It’s too bad, since command completion would have been a real boon.
10 Mailing List
11 Sample Transcript
So what does it look like to use DFD? Well, since it’s just a framework, it can look any way you want, but here’s a brief transcript from the example script that comes with dfd_keeper:
$ ./keeper_example.py --test --port 8008 &
$ nc -v -v localhost 8008
localhost [127.0.0.1] 8008 (?) open
Your wish is my command.
dfd_keeper>help
drop_state:
-
Drop a particular state table entry. Takes src and optional dst.
bittorrent:
-
Specify the bittorrent client, or nothing to turn off forwarding.
sync:
-
Synchronize the rules with pf. This is done automatically.
version:
-
No help for this command.
show:
-
This command shows the active rules to the client.
number:
-
This command enumerates the lines of the pf input for debugging.
flush:
-
Flush the state table. This is done automatically.
help:
-
Show help to the user. A command may be provided as an argument.
variables:
-
This command shows the current state variable namespace.
wan:
-
Switches on/off connectivity with the Internet. For emergencies only!
block:
-
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.
It is done.
dfd_keeper>show
nat on sis0 from re0:network to any -> (sis0)
# Allow all loopback traffic.
pass quick on lo0
# Default deny.
block all
# Allow LAN to bomb out quickly.
block return on re0
# Don’t allow other networks to impersonate LAN.
antispoof quick for re0
# Block leakage of LAN stuff to anywhere else.
block out log quick on ! re0 to re0:network
# Allow firewall to talk to LAN.
pass out quick on re0 from re0 to re0:network keep state
# Allow anything in from LAN that isn’t destined to the LAN.
pass in quick on re0 to ! re0:network keep state allow-opts
# Allow LAN hosts to SSH into this box.
pass in quick on re0 proto tcp from any to re0 port ssh flags S/SA
# Allow connections out WAN, and randomize SEQ #s.
pass out quick on sis0 all modulate state allow-opts
It is done.
dfd_keeper>block add 1.2.3.4
It is done.
dfd_keeper>show
nat on sis0 from re0:network to any -> (sis0)
# Allow all loopback traffic.
pass quick on lo0
# Default deny.
block all
# Allow LAN to bomb out quickly.
block return on re0
# Don’t allow other networks to impersonate LAN.
antispoof quick for re0
# Block leakage of LAN stuff to anywhere else.
block out log quick on ! re0 to re0:network
# Block hosts we have specified in both directions.
block in log quick on sis0 from 1.2.3.4 to any block out log quick on sis0 from any to 1.2.3.4
# Allow firewall to talk to LAN.
pass out quick on re0 from re0 to re0:network keep state
# Allow anything in from LAN that isn’t destined to the LAN.
pass in quick on re0 to ! re0:network keep state allow-opts
# Allow LAN hosts to SSH into this box.
pass in quick on re0 proto tcp from any to re0 port ssh flags S/SA
# Allow connections out WAN, and randomize SEQ #s.
pass out quick on sis0 all modulate state allow-opts
It is done.
dfd_keeper>block add www.google.com
It is done.
dfd_keeper>show
nat on sis0 from re0:network to any -> (sis0)
# Allow all loopback traffic.
pass quick on lo0
# Default deny.
block all
# Allow LAN to bomb out quickly.
block return on re0
# Don’t allow other networks to impersonate LAN.
antispoof quick for re0
# Block leakage of LAN stuff to anywhere else.
block out log quick on ! re0 to re0:network
# Block hosts we have specified in both directions.
block in log quick on sis0 from { 1.2.3.4 www.google.com } to any block out log quick on sis0 from any to { 1.2.3.4 www.google.com }
# Allow firewall to talk to LAN.
pass out quick on re0 from re0 to re0:network keep state
# Allow anything in from LAN that isn’t destined to the LAN.
pass in quick on re0 to ! re0:network keep state allow-opts
# Allow LAN hosts to SSH into this box.
pass in quick on re0 proto tcp from any to re0 port ssh flags S/SA
# Allow connections out WAN, and randomize SEQ #s.
pass out quick on sis0 all modulate state allow-opts
It is done.
dfd_keeper>