Key Pages: [ Rope Home Page | Basics (tutorial) | Language Reference | Download ]

Writing IpTables Rules With ROPE - Tutorial

ROPE is a programmable IpTables match module, used to descide whether IP packets passed to it match a particular set of criteria or not. It started life as a project to make the "string" match module of IpTables stronger (Rope is strong string) and evolved fairly quickly into the open-ended matching module we have here today.

Background - What Are Iptables Match Modules?

The match modules of iptables allow rules to take actions depending on whether packets match certain criteria or not. The standard distribution of netfilter / IpTables provides a range of useful modules of this type. These typically allow protocol types (TCP or UDP), source and destination addresses and ports etc to be checked. There is also a set of interesting "extras" than can be compiled into the kernel to provide some extended packet matching features. One such example is the "string" module that allows packets to be matched on the basis of the existance (or otherwise) of specified strings anywhere in the data payload portion of the packets. There are a number of other hidden treasures that can be used to significantly extend the features of the system.

These modules are included into an IpTables configuration using a command such as..

iptables -A FORWARD \
      -i eth0 \
      -p tcp \
      -s 192.168.0.10 \
      --sport 80 \
      -m string --string "application/mpeg" \
      -j DROP

The example above uses the "tcp" and "string" modules to match packets that come from port 80 on host 192.168.0.10 and contain the string "application/mpeg" anywhere in the data payload.

ROPE is a match module that allows more complex criteria to be specified without resorting to writing code in the C language or recompiling the linux kernel (once the Rope module itself has been compiled).

Building A Rope Match Module

In order to use ROPE to build a match rule, you first need to write the ROPE module that encodes your match criteria. As an example, we could look for the "Content-length" header of an HTTP download and check that the length does not exceed 1000000 bytes using the following module..

expecti_to( "Content-Length: " )
expect_while({isdigit}) put($n)
if( gt( atoi($n) 1000000 ) { yes } )
no

(There is a much more complete version of this module at HttpContentLength)

This module takes the following steps..

  1. Searches the data payload of the packet for the string "Content-length: ", but ignores letter case as it searches.
  2. If the string is not found, the module stops and returns a "not matched" status to netfilter.
  3. If the string is found, the module takes the digits that follow it, and stores them as a string in the register $n.
  4. The string in $n is converted to an integer and compared against the number 1000000. If $n is large than 1000000 then the module terminates and returns a "matched" status to IpTables.
  5. Otherwise, the module terminates with a "not matched" status.

The language in which modules like this are written is based on the idea of ReversePolish notation but extended to handle the concept of AnchorBrackets. The language is documented in detail in LanguageReference.

Compiling The Module

Rope modules are compiled using the ropec command, like this..

ropec -o compiled.rp source.rope

In order to allow the module to be loaded into the kernel by the IpTables utility, the compiled file must be placed in the directory /etc/rope.d/scripts and be given the extension ".rp".

Rope Comments

Comments in ROPE modules start with a hash character and end at the end of the line they started in - like "perl".

Here's some examples..

# This comment fills an entire line
print( "Hello" )  # this one comes after some code

Match And "No Match" Returns

A ROPE module finishes its execution by returning a "no" status to indicate that the packet does not match the criteria, or "yes" to indicate that it does match. These statuses are returned using the "yes" and "no" action words. For example: this module matches all packets..

yes

and this module never matches a single packet.

no

In addition to these two keywords, there are a number of actions that use the idea of "expecting" a particular content in the packet. These check the packet at the location specified in the $offset register for specified content. If the content is found, the module continues to execute. If the content is not found, then the expect-style action returns the "no" status just as if the "no" action had been invoked, and the module execution stops.

For example, this module

expect_str( "HTTP" )
println( "Got it" )
yes
  1. Checks for the string "HTTP" at the location $offset. If the string is not found there, the module stops with a "no" status and the 2nd and 3rd line of the module are not executed.
  2. If "HTTP" is" found at $offset, then the message "Got It" is printed.
  3. Finally - the module returns "yes" status, indicating that the packet does match the criteria.

Rope's Anchored Reverse Polish Notation

The ROPE programming language uses reverse polish notation, but has a few tricks up it's sleeves that plug some of the "holes" with that syntax.

So what is "reverse polish" notation? Well: simply put, it means that the operators (like "add") are put after the arguments they operate on, and that extensive and explicit use of a stack is made.

In well known languages like C, perl, Java etc, the act of adding two nunmbers and printing out the result looks something like this..

print( 10 + 139 );

In reverse polish notation, this would be written..

 10 139 + print

This works like this...

  1. The number "10" is pushed onto the stack.
  2. The number "139" is pushed onto the stack - the stack now contains 2 numbers with the 139 at the top, and the 10 just under it.
  3. The "+" takes the top two items from the stack, adds them together and pushes the result back. The stack now contains just the value 149.
  4. The "print" takes the top item from the stack and prints it.

ROPE uses this style of syntax except that it doesnt use "+" to denote addition, but the word "add". So the ROPE version of this module looks like this..

 10 130 add print

So that's "reverse polish", but what about the word "anchored" in the title of this section. Well - rope adds the ability to use brackets in the module to do two things..

  1. To make the module layout more "natural" to many programmers
  2. To allow actions (functions) to take variable numbers of arguments.

We'll explain this trick by using an example. The add-and-print module above can be written in any of these ways..

10 130 add print
add(10 130) print
print( 10 130 add )
print( add( 10 130 ) )

These are all valid. The way to understand this syntax extension is to understand that the "add(" or "print(" part of the module drops an anchor onto the stack which is pulled up again when the matching ")" is encountered, and the action is taken at that point. Lets take this version to show how this works..

add( 10 130 ) print

This does the following...

  1. Pushes an anchor containing a reference to the function "add" onto the stack.
  2. Pushes the numbers 10 and 130 onto the stack. The stack now contains three items - the anchor and two numbers.
  3. When the close bracket is executed, ROPE scans backwards down the stack to the anchor nearest the top. It pulls the anchor out of the stack and notes the number of items between the anchor and stack top (two in our example). It then executes the action associated with the anchor (ie: the "add" action).
  4. The "add" action does what it did before - adds the two numbers and pushes back the result.
  5. Finally - the "print" action pops off the sum and prints it.

The fact that "add" knows how many arguments it was passed with this syntax means that we can add multiple numbers, like this..

add( 10 130 58 982 ) print

Where as, if we had said..

10 130 58 982 add print

then the "add" function has no ability to count it's arguments and simply takes its default number (which is 2) off the stack and adds those.

Packet Header Fields

The full set of IP, UDP and TCP/IP packet header fields are available as '$' registers, using names like $ip_saddr or $tcp_dest - etc. Rules can therefore combine tests against the fields as well as the data payload. For example, to test for an HTTP request, try..

$tcp_dest 80 eq assert
expect_str( "GET " )
yes

IP Addresses

The header fields $ip_saddr and $ip_daddr contain the packet's source and destination IPv4 addresses (we don't handle V6 yet - that will come). Rope represents these as 4-byte strings in the original in-packet format which can be converted to a printable string using ipv4_ntoa, like this..

$ip_saddr ipv4_ntoa println

In addition, ROPE allows IP addresses to be included in the module source code. When this is done, they are treated as 4 byte strings, where each octet of the address is mapped to one byte of the string. You can use this fact to do simple address arithmetic. For example, to check for address in the 192.168.0.0/16 subnet, you could use something like..

$ip_saddr 255.255.0.0 and 192.168.0.0 eq assert

Note that and, or and xor are capable of operating on strings, provided that their lengths are the same.

(see: IpAddress for more details).

Lifting, Asserting And Expecting

A ROPE module has the task of inspecting an IP packet in order to determine whether it matches a set of user-defined criteria. In order to do this, it is clearly important that the module has access to the packet's contents. ROPE provides this access in two ways.

  1. Via pre-defined registers that map to key fields in the packet headers. For example, the register $ip_saddr holds the IP address from which the packet was sent (as a packed four-character representation), $tcp_source contains the source port number - etc. There are many more of these.
  2. Via the "lift" functions that lift portions of the packet onto the stack as ROPE strings. These can then be inspected at will.

The "lift" functions look like ...

lift(10)               -- lifts a string of 10 characters
lift_while({isidigit}) -- lifts characters until a non-digit is seen
lift_to( "\n" )        -- lifts characters up to a new line
lifti_to( "End" )      -- lifts characters up to the string "End", ignoring case.

All of these functions lift (take) bytes from the packet, starting at the position pointed to by the register $offset, and advance $offset by the number of bytes lifted. In this sense, "lift" is like C's "read" and $offset is like a combination of "ltell" and "lseek". For detailed documentation on these actions, see lift, lift_while, lift_to, lifti_to.

A common required for ROPE modules is to be able to "lift" some portion of the IP packet and compare the lifted string with some pre-defined string, exiting the module with a "no match" status if the two are not the same. In order to facilitate the scripting of this very common requirement, a number of "expect" actions are available.

This line..

if( lift(4) "HTTP" ne { no } )

Lifts four characters and returns "no" to Iptables if they don't contain the string "HTTP".

The following alternative syntax does exactly the same, but uses slightly less code..

lift(4) "HTTP" eq assert

In this example, we have introduced the assert action - this checks for a TRUE (not zero) value on the stack and terminates the module with a "no" status if it is not found (ie: the value is FALSE (zero)). If a TRUE value is on the stack, then the module continues. This is similar to the "assert" function of the C language.

An even more compact module line that provides the same logic is..

expect_str( "HTTP" )

This combines the "lift", the test and the assert into a single action.

There a variety of other "expect-style" actions that mirror the "lift" syntaxes but terminate the module with a "no" status if a match is not found. These actions are expect_str, expect_while, expect_to and expecti_to.

Scroll to Top