/python-iptables

Python bindings for iptables

Primary LanguagePython

Introduction

About python-iptables

Iptables is the tool that is used to manage netfilter, the standard packet filtering and manipulation framework under Linux. As the iptables manpage puts it:

Iptables is used to set up, maintain, and inspect the tables of IPv4 packet filter rules in the Linux kernel. Several different tables may be defined.

Each table contains a number of built-in chains and may also contain user- defined chains.

Each chain is a list of rules which can match a set of packets. Each rule specifies what to do with a packet that matches. This is called a target, which may be a jump to a user-defined chain in the same table.

Python-iptables provides a pythonesque wrapper via python bindings to iptables under Linux. Interoperability with iptables is achieved via using the iptables C libraries (libiptc, libxtables, and the iptables extensions), not calling the iptables binary and parsing its output. It is meant primarily for dynamic and/or complex routers and firewalls, where rules are often updated or changed, or Python programs wish to interface with the Linux iptables framework..

buildstatus

Bitdeli

Installing via pip

The usual way:

pip install --upgrade python-iptables

Compiling from source

First make sure you have iptables installed (most Linux distributions install it by default). Python-iptables needs the shared libraries libiptc.so and libxtables.so coming with iptables, they are installed in /lib on Ubuntu.

You can compile python-iptables in the usual distutils way:

% cd python-iptables
% python setup.py build

If you like, python-iptables can also be installed into a virtualenv:

% mkvirtualenv python-iptables
% python setup.py install

If you install python-iptables as a system package, make sure the directory where distutils installs shared libraries is in the dynamic linker's search path (it's in /etc/ld.so.conf or in one of the files in the folder /etc/ld.co.conf.d). Under Ubuntu distutils by default installs into /usr/local/lib.

Now you can run the tests:

% sudo PATH=$PATH ./test.py
WARNING: this test will manipulate iptables rules.
Don't do this on a production machine.
Would you like to continue? y/n y
[...]

The PATH=$PATH part is necessary after sudo if you have installed into a virtualenv, since sudo will reset your environment to a system setting otherwise..

Once everything is in place you can fire up python to check whether the package can be imported:

% sudo PATH=$PATH python
>>> import iptc
>>>

Of course you need to be root to be able to use iptables.

What is supported

The basic iptables framework and all the match/target extensions are supported by python-iptables, including IPv4 and IPv6 ones. All IPv4 and IPv6 tables are supported as well.

Full documentation with API reference is available here.

Examples

Rules

In python-iptables, you usually first create a rule, and set any source/destination address, in/out interface and protocol specifiers, for example:

>>> import iptc
>>> rule = iptc.Rule()
>>> rule.in_interface = "eth0"
>>> rule.src = "192.168.1.0/255.255.255.0"
>>> rule.protocol = "tcp"

This creates a rule that will match TCP packets coming in on eth0, with a source IP address of 192.168.1.0/255.255.255.0.

A rule may contain matches and a target. A match is like a filter matching certain packet attributes, while a target tells what to do with the packet (drop it, accept it, transform it somehow, etc). One can create a match or target via a Rule:

>>> rule = iptc.Rule()
>>> m = rule.create_match("tcp")
>>> t = rule.create_target("DROP")

Match and target parameters can be changed after creating them. It is also perfectly valid to create a match or target via instantiating them with their constructor, but you still need a rule and you have to add the matches and the target to their rule manually:

>>> rule = iptc.Rule()
>>> match = iptc.Match(rule, "tcp")
>>> target = iptc.Target(rule, "DROP")
>>> rule.add_match(match)
>>> rule.target = target

Any parameters a match or target might take can be set via the attributes of the object. To set the destination port for a TCP match:

>>> rule = iptc.Rule()
>>> rule.protocol = "tcp"
>>> match = rule.create_match("tcp")
>>> match.dport = "80"

To set up a rule that matches packets marked with 0xff:

>>> rule = iptc.Rule()
>>> rule.protocol = "tcp"
>>> match = rule.create_match("mark")
>>> match.mark = "0xff"

Parameters are always strings. You can supply any string as the parameter value, but note that most extensions validate their parameters. For example this:

>>> rule = iptc.Rule()
>>> rule.protocol = "tcp"
>>> rule.target = iptc.Target(rule, "ACCEPT")
>>> match = iptc.Match(rule, "state")
>>> chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), "INPUT")
>>> match.state = "RELATED,ESTABLISHED"
>>> rule.add_match(match)
>>> chain.insert_rule(rule)

will work. However, if you change the state parameter:

>>> rule = iptc.Rule()
>>> rule.protocol = "tcp"
>>> rule.target = iptc.Target(rule, "ACCEPT")
>>> match = iptc.Match(rule, "state")
>>> chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), "INPUT")
>>> match.state = "RELATED,ESTABLISHED,FOOBAR"
>>> rule.add_match(match)
>>> chain.insert_rule(rule)

python-iptables will throw an exception:

Traceback (most recent call last):
  File "state.py", line 7, in <module>
    match.state = "RELATED,ESTABLISHED,FOOBAR"
  File "/home/user/Projects/python-iptables/iptc/ip4tc.py", line 369, in __setattr__
    self.parse(name.replace("_", "-"), value)
  File "/home/user/Projects/python-iptables/iptc/ip4tc.py", line 286, in parse
    self._parse(argv, inv, entry)
  File "/home/user/Projects/python-iptables/iptc/ip4tc.py", line 516, in _parse
    ct.cast(self._ptrptr, ct.POINTER(ct.c_void_p)))
  File "/home/user/Projects/python-iptables/iptc/xtables.py", line 736, in new
    ret = fn(*args)
  File "/home/user/Projects/python-iptables/iptc/xtables.py", line 1031, in parse_match
    argv[1]))
iptc.xtables.XTablesError: state: parameter error -2 (RELATED,ESTABLISHED,FOOBAR)

In certain cases you might need to use quoting inside the parameter string, for example:

>>> rule = iptc.Rule()
>>> rule.src = "127.0.0.1"
>>> rule.protocol = "udp"
>>> rule.target = rule.create_target("ACCEPT")
>>> match = rule.create_match("comment")
>>> match.comment = "this is a test comment"
>>> chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), "INPUT")
>>> chain.insert_rule(rule)

will only add the comment this instead of the expected this is a test comment. Use quoting inside the comment string itself:

>>> comment = "this is a test comment"
>>> match.comment = "\"%s\"" % (comment)

When you are ready constructing your rule, add them to the chain you want it to show up in:

>>> chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), "INPUT")
>>> chain.insert_rule(rule)

This will put your rule into the INPUT chain in the filter table.

Chains and tables

You can of course also check what a rule's source/destination address, in/out inteface etc is. To print out all rules in the FILTER table:

>>> import iptc
>>> table = iptc.Table(iptc.Table.FILTER)
>>> for chain in table.chains:
>>>     print "======================="
>>>     print "Chain ", chain.name
>>>     for rule in chain.rules:
>>>         print "Rule", "proto:", rule.protocol, "src:", rule.src, "dst:", \
>>>               rule.dst, "in:", rule.in_interface, "out:", rule.out_interface,
>>>         print "Matches:",
>>>         for match in rule.matches:
>>>             print match.name,
>>>         print "Target:",
>>>         print rule.target.name
>>> print "======================="

As you see in the code snippet above, rules are organized into chains, and chains are in tables. You have a fixed set of tables; for IPv4:

  • FILTER,
  • NAT,
  • MANGLE and
  • RAW.

For IPv6 the tables are:

  • FILTER,
  • MANGLE,
  • RAW and
  • SECURITY.

To access a table:

>>> import iptc
>>> table = iptc.Table(iptc.Table.FILTER)
>>> print table.name
filter

To create a new chain in the FILTER table:

>>> import iptc
>>> table = iptc.Table(iptc.Table.FILTER)
>>> chain = table.create_chain("testchain")

$ sudo iptables -L -n
[...]
Chain testchain (0 references)
target     prot opt source               destination

To access an existing chain:

>>> import iptc
>>> table = iptc.Table(iptc.Table.FILTER)
>>> chain = iptc.Chain(table, "INPUT")
>>> chain.name
'INPUT'
>>> len(chain.rules)
10
>>>

More about matches and targets

There are basic targets, such as DROP and ACCEPT. E.g. to reject packets with source address 127.0.0.1/255.0.0.0 coming in on any of the eth interfaces:

>>> import iptc
>>> chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), "INPUT")
>>> rule = iptc.Rule()
>>> rule.in_interface = "eth+"
>>> rule.src = "127.0.0.1/255.0.0.0"
>>> target = iptc.Target(rule, "DROP")
>>> rule.target = target
>>> chain.insert_rule(rule)

To instantiate a target or match, we can either create an object like above, or use the rule.create_target(target_name) and rule.create_match(match_name) methods. For example, in the code above target could have been created as:

>>> target = rule.create_target("DROP")

instead of:

>>> target = iptc.Target(rule, "DROP")
>>> rule.target = target

The former also adds the match or target to the rule, saving a call.

Another example, using a target which takes parameters. Let's mark packets going to 192.168.1.2 UDP port 1234 with 0xffff:

>>> import iptc
>>> chain = iptc.Chain(iptc.Table(iptc.Table.MANGLE), "PREROUTING")
>>> rule = iptc.Rule()
>>> rule.dst = "192.168.1.2"
>>> rule.protocol = "udp"
>>> match = iptc.Match(rule, "udp")
>>> match.dport = "1234"
>>> rule.add_match(match)
>>> target = iptc.Target(rule, "MARK")
>>> target.set_mark = "0xffff"
>>> rule.target = target
>>> chain.insert_rule(rule)

Matches are optional (specifying a target is mandatory). E.g. to insert a rule to NAT TCP packets going out via eth0:

>>> import iptc
>>> chain = iptc.Chain(iptc.Table(iptc.Table.NAT), "POSTROUTING")
>>> rule = iptc.Rule()
>>> rule.protocol = "tcp"
>>> rule.out_interface = "eth0"
>>> target = iptc.Target(rule, "MASQUERADE")
>>> target.to_ports = "1234"
>>> rule.target = target
>>> chain.insert_rule(rule)

Here only the properties of the rule decide whether the rule will be applied to a packet.

Matches are optional, but we can add multiple matches to a rule. In the following example we will do that, using the iprange and the tcp matches:

>>> import iptc
>>> rule = iptc.Rule()
>>> rule.protocol = "tcp"
>>> match = iptc.Match(rule, "tcp")
>>> match.dport = "22"
>>> rule.add_match(match)
>>> match = iptc.Match(rule, "iprange")
>>> match.src_range = "192.168.1.100-192.168.1.200"
>>> match.dst_range = "172.22.33.106"
>>> rule.add_match(match)
>>> rule.target = iptc.Target(rule, "DROP")
>>> chain = iptc.Chain(iptc.Table.(iptc.Table.FILTER), "INPUT")
>>> chain.insert_rule(rule)

This is the python-iptables equivalent of the following iptables command:

# iptables -A INPUT -p tcp –destination-port 22 -m iprange –src-range 192.168.1.100-192.168.1.200 –dst-range 172.22.33.106 -j DROP

You can of course negate matches, just like when you use ! in front of a match with iptables. For example:

>>> import iptc
>>> rule = iptc.Rule()
>>> match = iptc.Match(rule, "mac")
>>> match.mac_source = "!00:11:22:33:44:55"
>>> rule.add_match(match)
>>> rule.target = iptc.Target(rule, "ACCEPT")
>>> chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), "INPUT")
>>> chain.insert_rule(rule)

This results in:

$ sudo iptables -L -n
Chain INPUT (policy ACCEPT)
target     prot opt source               destination
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            MAC ! 00:11:22:33:44:55

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

Counters

You can query rule and chain counters, e.g.:

>>> import iptc
>>> table = iptc.Table(iptc.Table.FILTER)
>>> chain = iptc.Chain(table, 'OUTPUT')
>>> for rule in chain.rules:
>>>         (packets, bytes) = rule.get_counters()
>>>         print packets, bytes

However, the counters are only refreshed when the underlying low-level iptables connection is refreshed in Table via table.refresh(). For example:

>>> import time, sys
>>> import iptc
>>> table = iptc.Table(iptc.Table.FILTER)
>>> chain = iptc.Chain(table, 'OUTPUT')
>>> for rule in chain.rules:
>>>         (packets, bytes) = rule.get_counters()
>>>         print packets, bytes
>>> print "Please send some traffic"
>>> sys.stdout.flush()
>>> time.sleep(3)
>>> for rule in chain.rules:
>>>         # Here you will get back the same counter values as above
>>>         (packets, bytes) = rule.get_counters()
>>>         print packets, bytes

This will show you the same counter values even if there was traffic hitting your rules. You have to refresh your table to get update your counters:

>>> import time, sys
>>> import iptc
>>> table = iptc.Table(iptc.Table.FILTER)
>>> chain = iptc.Chain(table, 'OUTPUT')
>>> for rule in chain.rules:
>>>         (packets, bytes) = rule.get_counters()
>>>         print packets, bytes
>>> print "Please send some traffic"
>>> sys.stdout.flush()
>>> time.sleep(3)
>>> table.refresh()  # Here: refresh table to update rule counters
>>> for rule in chain.rules:
>>>         (packets, bytes) = rule.get_counters()
>>>         print packets, bytes